Umi.js权限路由模块源码精读
Umi.js介绍
Umi是阿里系推出的react应用框架。Umi用于解决react企业级开发问题。Umi也是Ant Design Pro选用的几大重要组件之一。在写下这篇博文时,在github上已经有5.6k赞,人气很高,也有许多成功的项目。
Umi是以路由为基础的应用框架,在路由上延伸出了各种功能,扩展到了React应用生命周期的方方面面。所以理解其路由模块对理解和应用Umi非常重要。
更多Umi相关知识请看官网:https://umijs.org/zh/
说点题外话,可以略过这一小段。感觉react和Vue的生态还不一样,Vue一般都是用指定的一些全家桶产品,路由就钦定Vue-Router
,状态管理就钦定Vuex
;而React…完全就感觉是在玩积木,各种各有的库都有,文档有的全有的不全,随时都需要手撕源码看里面的实现才知道怎么用,真叫一个渐进式…
动机
配置式路由非常适合大型项目,因为配置其实就是没什么特殊的JS对象,非常易读,而且通过es6的模块语法可以非常方便地拓展。Umi就是用类似Vue-Router
的配置式路由为基础的:
routes: [
{ path: '/', component: './pages/index.js' },
{ path: '/users/', component: './pages/users/index.js' },
{ path: '/users/list', component: './pages/users/list.js' },
]
而且,还集成了权限路由功能,直接通过通过配置路由的 Routes
属性来实现即可。
routes: [
{ path: '/', component: './pages/index.js' },
{ path: '/list', component: './pages/list.js', Routes: ['./routes/PrivateRoute.js'] },
]
而其底层使用的React-Router
却只是组件声明式路由
<Router>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User}/>
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
感觉就非常的魔法,当然js的世界没有魔法。我们可以大致猜测一下Umi做了什么事:
- 读取路由配置对象,将其渲染为
React Router
的路由组件<Router>
等 - 使用一些设计模式(不卖关子了,其实就是HOC高阶组件)来实现权限路由功能
源码位置
我读的源码版本为umi@2.8.17
路径:packages > umi > src > renderRoutes.js
源码分析
在这个文件中,渲染路由的主要方法也就是export default
的方法renderRoutes
,我们直接看这个方法,在文件第121行。
第一行请求了一个插件:
const plugins = require('umi/_runtimePlugin');
umi设计者以“插件”的形式来管理功能模块,对二次开发和社区非常友好,但本质上,就是像写一个utils
实现的功能是一样的,只是定义了一些实用方法,不理解并不影响阅读源码,我们可以不去管他。
从第二行开始的结构是:
return routes ? ( <Switch>...</Switch> ) : null
也就是当routes有值时才渲染,也很好理解。这个routes就对应路由配置文件中的routes数组(甚至名字都一样)。那么我们直接看routes有值时的情况吧。
Umi的设计的路由配置是一个路由对应一个组件,这样对于大型项目来说路由更加的清晰。React Router本来的设计是一个路由可以匹配到的所有组件都进行渲染,非常的灵活。用了<Switch>
,才会只渲染第一个匹配到的组件。所以,我们也可以看到renderRoutes
方法中返回的根组件是<Switch>
。
接下来的代码结构是:
routes.map((route,i))=>{
const RouteRoute = ...
...
return <RouteRoute>...</RouteRoute>
}
也就是遍历路由配置routes
数组中所有的配置对象(route
),每一个配置对象都根据实际情况渲染出一个路由组件RouteRoute
(这起名风格我也是醉了,不看代码上下文完全没法猜意思)。
首先对重定向路由配置项进行响应。重定向配置的渲染比较简单,React Router中正好有对应的组件Redirect
,渲染出来即可。
// 对应的配置项例子:{path:'/', redirect: '/index'}
if (route.redirect) {
return (
<Redirect
from={route.path}
to={route.redirect}
...
/>
);
}
接下来分情况处理带权限的路由和不带权限的路由,权限路由的配置即为Routes
属性,可以复习一下配置示例:
// 在Routes属性中指定一个或多个权限组件
{ path: '/list', component: './pages/list.js', Routes: ['./routes/PrivateRoute.js'] }
所以源码中有:
const RouteRoute = route.Routes ? withRoutes(route) : RouteWithProps;
直接检查Routes属性,如果有,说明是权限路由,用方法withRoutes
进行响应;如果没有,用RouteWithProps
进行响应。
先看渲染没有Routes属性的普通路由的方法RouteWithProps
(源码第28行),非常简单,就是转换成一个React-Router
的<Route>
组件并传透props。
const RouteWithProps = ({ path, exact, strict, render, location, sensitive, ...rest }) => (
<Route
path={path}
exact={exact}
strict={strict}
location={location}
sensitive={sensitive}
render={props => render({ ...props, ...rest })}
/>
);
再看渲染复杂的权限路由的方法withRoutes
(源码第38行),首先看见了一个缓存对象
if (RouteInstanceMap.has(route)) {
return RouteInstanceMap.get(route);
}
...
RouteInstanceMap.set(route, ret);
return ret;
为什么需要有这个对象呢?其实是umi设计者考虑到许多路由其实是用的同一个权限组件,那么在每次渲染权限路由时,将其缓存起来,下次再请求同一个权限路由,直接返回就好了,性能就有所提高。
接下来是一个巧妙的迭代,看代码:
const { Routes } = route;
let len = Routes.length - 1;
let Component = args => {
const { render, ...props } = args;
return render(props);
};
while (len >= 0) {
const AuthRoute = Routes[len];
const OldComponent = Component;
Component = props => (
<AuthRoute {...props}>
<OldComponent {...props} />
</AuthRoute>
);
len -= 1;
}
这一段代码用Component
、AuthRoute
、OldComponent
这三个引用,像滚雪球一样,将Routes中的权限路由配置一层一层包裹起来,最里层是Routes
数组中第一个权限组件,最外层是最后一个权限组件,好像一个洋葱。最后,我们得到的这个Component
组件,即为包裹好的权限组件的引用。这时候就可以看作一个普通路由组件了,所有的权限组件都包裹进去了,再复用渲染普通路由组件的方法RouteWithProps
即可:
const ret = args => {
const { render, ...rest } = args;
return (
<RouteWithProps
{...rest}
render={props => {
return <Component {...props} route={route} render={render} />;
}}
/>
);
};
那为什么这样一层一层包裹,就可以实现权限路由呢?我们最开始说过权限路由是一个高阶组件。所谓高阶组件,在设计上可以理解是为一个组件函数,接收一个组件函数和一些其他参数,返回一个新的组件函数。而这些参数,是路由中带的(比如url位置),那么在这个高阶组件中,我们就可以写判断逻辑,比如用url去用户的路由权限,如果不匹配则返回一个<Redirect>
把用户重定向到404或者登录页;匹配则返回正确的组件。这样就实现了好像一个“路由守卫”(Vue-Router中的概念,指每次路由切换都会触发的监听器函数)。当有多个权限组件时,外层的权限组件验证成功,再返回内层的权限组件,直到全部验证成功才返回最内层真正的组件。
比如umi官网上的权限组件实例:
export default (props) => {
return (
<div>
<div>PrivateRoute (routes/PrivateRoute.js)</div>
{ props.children }
</div>
);
}
其实这个例子我觉得写的不好,没有教用户怎么去做判断。不过高阶组件的思想是相同的。
这样看起来很麻烦,像Vue-Router一样直接暴露一个路由守卫函数接口不好吗?仔细想想,其实不是的,这样一层一层包裹的权限组件,可以更容易的实现中间件复用,对于大型应用来说更加适合。比如三个权限组件,一个判断登陆状态,一个判断用户组权限,一个判断用户权限,这三个权限组件都可以作为中间件放到其他项目里复用,而且放在路由权限配置里很清晰,如果有好的组件命名规范,那更加一目了然。
总结
- umi接收路由权限配置,渲染成React-Router的组件式路由。
- umi使用
<Switch>
来实现一个路由对应一个组件。 - umi使用缓存来在渲染权限路由时提高效率。
- umi接收权限组件,像洋葱一样将其一层层包裹起来,权限验证时再一层一层打开,直到实际的页面组件。
- umi使用高阶组件的方式实现“路由守卫功能”。
- umi良好的中间件设计使权限组件复用更加简单。