深入理解 react-router 路由系统

450 查看

在 web 应用开发中,路由系统是不可或缺的一部分。在浏览器当前的 URL 发生变化时,路由系统会做出一些响应,用来保证用户界面与 URL 的同步。随着单页应用时代的到来,为之服务的前端路由系统也相继出现了。有一些独立的第三方路由系统,比如 director,代码库也比较轻量。当然,主流的前端框架也都有自己的路由,比如 Backbone、Ember、Angular、React 等等。那 react-router 相对于其他路由系统又针对 React 做了哪些优化呢?它是如何利用了 React 的 UI 状态机特性呢?又是如何将 JSX 这种声明式的特性用在路由中?

一个简单的示例

现在,我们通过一个简易的博客系统示例来解释刚刚遇到的疑问,它包含了查看文章归档、文章详细、登录、退出以及权限校验几个功能,该系统的完整代码托管在 JS Bin(注意,文中示例代码使用了与之对应的 ES6 语法),你可以点击链接查看。此外,该实例全部基于最新的 react-router 1.0 进行编写。下面看一下 react-router 的应用实例:

 

如果你以前并没有接触过 react-router,相反只是用过刚才提到的 Backbone 的路由或者是 director,你一定会对这种声明式的写法感到惊讶。不过细想这也是情理之中,毕竟是只服务与 React 类库,引入它的特性也是无可厚非。仔细看一下,你会发现:

  • Router 与 Route 一样都是 react 组件,它的 history 对象是整个路由系统的核心,它暴露了很多属性和方法在路由系统中使用;
  • Route 的 path 属性表示路由组件所对应的路径,可以是绝对或相对路径,相对路径可继承;
  • Redirect 是一个重定向组件,有 from 和 to 两个属性;
  • Route 的 onEnter 钩子将用于在渲染对象的组件前做拦截操作,比如验证权限;
  • 在 Route 中,可以使用 component 指定单个组件,或者通过 components 指定多个组件集合;
  • param 通过 /:param 的方式传递,这种写法与 express 以及 ruby on rails 保持一致,符合 RestFul 规范;

下面再看一下如果使用 director 来声明这个路由系统会是怎样一番景象呢:

从代码的优雅程度、可读性以及维护性上看绝对 react-router 在这里更胜一筹。分析上面的代码,每个路由的渲染逻辑都相对独立的,这样就需要写很多重复的代码,这里虽然可以借助 React 的 setState 来统一管理路由返回的组件,将 render 方法做一定的封装,但结果却是要多维护一个 state,在 react-router 中这一步根本不需要。此外,这种命令式的写法与 React 代码放在一起也是略显突兀。而 react-router 中的声明式写法在组件继承上确实很清晰易懂,而且更加符合 React 的风格。包括这里的默认路由、重定向等等都使用了这种声明式。相信读到这里你已经放弃了在 React 中使用 react-router 外的路由系统!

接下来,还是回到 react-router 示例中,看一下路由组件内部的代码:

 

上面的代码表示了博客系统的登录以及退出功能。登录成功,默认跳转到 /about 路径下,如果在 state 对象中存储了 nextPathname,则跳转到该路径下。在这里需要指出每一个路由(Route)中声明的组件(比如 SignIn)在渲染之前都会被传入一些 props,具体是在源码中的 RoutingContext.js 中完成,主要包括:

  • history 对象,它提供了很多有用的方法可以在路由系统中使用,比如刚刚用到的history.replaceState,用于替换当前的 URL,并且会将被替换的 URL 在浏览器历史中删除。函数的第一个参数是 state 对象,第二个是路径;
  • location 对象,它可以简单的认为是 URL 的对象形式表示,这里要提的是 location.state,这里 state 的含义与 HTML5 history.pushState API 中的 state 对象一样。每个 URL 都会对应一个 state 对象,你可以在对象里存储数据,但这个数据却不会出现在 URL 中。实际上,数据被存在了 sessionStorage 中;

事实上,刚才提到的两个对象同时存在于路由组件的 context 中,你还可以通过 React 的 context API 在组件的子级组件中获取到这两个对象。比如在 SignIn 组件的内部又包含了一个 SignInChild 组件,你就可以在组件内部通过 this.context.history 获取到 history 对象,进而调用它的 API 进行跳转等操作。

接下来,我们一起看一下 Archives 组件内部的代码: