在 web 应用开发中,路由系统是不可或缺的一部分。在浏览器当前的 URL 发生变化时,路由系统会做出一些响应,用来保证用户界面与 URL 的同步。随着单页应用时代的到来,为之服务的前端路由系统也相继出现了。有一些独立的第三方路由系统,比如 director,代码库也比较轻量。当然,主流的前端框架也都有自己的路由,比如 Backbone、Ember、Angular、React 等等。那 react-router 相对于其他路由系统又针对 React 做了哪些优化呢?它是如何利用了 React 的 UI 状态机特性呢?又是如何将 JSX 这种声明式的特性用在路由中?
一个简单的示例
现在,我们通过一个简易的博客系统示例来解释刚刚遇到的疑问,它包含了查看文章归档、文章详细、登录、退出以及权限校验几个功能,该系统的完整代码托管在 JS Bin(注意,文中示例代码使用了与之对应的 ES6 语法),你可以点击链接查看。此外,该实例全部基于最新的 react-router 1.0 进行编写。下面看一下 react-router 的应用实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import React from 'react'; import { render, findDOMNode } from 'react-dom'; import { Router, Route, Link, IndexRoute, Redirect } from 'react-router'; import { createHistory, createHashHistory, useBasename } from 'history'; // 此处用于添加根路径 const history = useBasename(createHashHistory)({ queryKey: '_key', basename: '/blog-app', }); React.render(( <Router history={history}> <Route path="/" component={BlogApp}> <IndexRoute component={SignIn}/> <Route path="signIn" component={SignIn}/> <Route path="signOut" component={SignOut}/> <Redirect from="/archives" to="/archives/posts"/> <Route onEnter={requireAuth} path="archives" component={Archives}> <Route path="posts" components={{ original: Original, reproduce: Reproduce, }}/> </Route> <Route path="article/:id" component={Article}/> <Route path="about" component={About}/> </Route> </Router> ), document.getElementById('example')); |
如果你以前并没有接触过 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 来声明这个路由系统会是怎样一番景象呢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import React from 'react'; import { render } from 'react-dom'; import { Router } from 'director'; const App = React.createClass({ getInitialState() { return { app: null } }, componentDidMount() { const router = Router({ '/signIn': { on() { this.setState({ app: (<BlogApp><SignIn/></BlogApp>) }) }, }, '/signOut': { 结构与 signIn 类似 }, '/archives': { '/posts': { on() { this.setState({ app: (<BlogApp><Archives original={Original} reproduct={Reproduct}/></BlogApp>) }) }, }, }, '/article': { '/:id': { on (id) { this.setState({ app: (<BlogApp><Article id={id}/></BlogApp>) }) }, }, }, }); }, render() { return <div>{React.cloneElement(this.state.app)}</div>; }, }) render(<App/>, document.getElementById('example')); |
从代码的优雅程度、可读性以及维护性上看绝对 react-router 在这里更胜一筹。分析上面的代码,每个路由的渲染逻辑都相对独立的,这样就需要写很多重复的代码,这里虽然可以借助 React 的 setState 来统一管理路由返回的组件,将 render 方法做一定的封装,但结果却是要多维护一个 state,在 react-router 中这一步根本不需要。此外,这种命令式的写法与 React 代码放在一起也是略显突兀。而 react-router 中的声明式写法在组件继承上确实很清晰易懂,而且更加符合 React 的风格。包括这里的默认路由、重定向等等都使用了这种声明式。相信读到这里你已经放弃了在 React 中使用 react-router 外的路由系统!
接下来,还是回到 react-router 示例中,看一下路由组件内部的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
const SignIn = React.createClass({ handleSubmit(e) { e.preventDefault(); const email = findDOMNode(this.refs.name).value; const pass = findDOMNode(this.refs.pass).value; // 此处通过修改 localStorage 模拟了登录效果 if (pass !== 'password') { return; } localStorage.setItem('login', 'true'); const location = this.props.location; if (location.state && location.state.nextPathname) { this.props.history.replaceState(null, location.state.nextPathname); } else { // 这里使用 replaceState 方法做了跳转,但在浏览器历史中不会多一条记录,因为是替换了当前的记录 this.props.history.replaceState(null, '/about'); } }, render() { if (hasLogin()) { return <p>你已经登录系统!<Link to="/signOut">点此退出</Link></p>; } return ( <form onSubmit={this.handleSubmit}> <label><input ref="name"/></label><br/> <label><input ref="pass"/></label> (password)<br/> <button type="submit">登录</button> </form> ); } }); const SignOut = React.createClass({ componentDidMount() { localStorage.setItem('login', 'false'); }, render() { return <p>已经退出!</p>; } }) |
上面的代码表示了博客系统的登录以及退出功能。登录成功,默认跳转到 /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 组件内部的代码: