Umi.js权限路由模块源码精读

2019-08-13

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;
  }

这一段代码用ComponentAuthRouteOldComponent这三个引用,像滚雪球一样,将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良好的中间件设计使权限组件复用更加简单。