简介
React Router 是一个基于 React 之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
为了向你说明 React Router 解决的问题,让我们先来创建一个不使用它的应用。所有文档中的示例代码都会使用 ES6/ES2015 语法和语言特性。
不使用 React Router
import React from 'react' import { render } from 'react-dom' const About = React.createClass({/*...*/}) const Inbox = React.createClass({/*...*/}) const Home = React.createClass({/*...*/}) const App = React.createClass({ getInitialState() { return { route: window.location.hash.substr(1) } }, componentDidMount() { window.addEventListener('hashchange', () => { this.setState({ route: window.location.hash.substr(1) }) }) }, render() { let Child switch (this.state.route) { case '/about': Child = About; break; case '/inbox': Child = Inbox; break; default: Child = Home; } return ( <div> <h1>App</h1> <ul> <li><a href="#/about">About</a></li> <li><a href="#/inbox">Inbox</a></li> </ul> <Child/> </div> ) } }) React.render(<App />, document.body)
当 URL 的 hash 部分(指的是 #
后的部分)变化后,<App>
会根据 this.state.route
来渲染不同的<Child>
。看起来很直接,但它很快就会变得复杂起来。
使用 React Router 后
让我们用 React Router 重构这个应用。
import React from 'react' import { render } from 'react-dom' // 首先我们需要导入一些组件... import { Router, Route, Link } from 'react-router' // 然后我们从应用中删除一堆代码和 // 增加一些 <Link> 元素... const App = React.createClass({ render() { return ( <div> <h1>App</h1> {/* 把 <a> 变成 <Link> */} <ul> <li><Link to="/about">About</Link></li> <li><Link to="/inbox">Inbox</Link></li> </ul> {/* 接着用 `this.props.children` 替换 `<Child>` router 会帮我们找到这个 children */} {this.props.children} </div> ) } }) // 最后,我们用一些 <Route> 来渲染 <Router>。 // 这些就是路由提供的我们想要的东西。 React.render(( <Router> <Route path="/" component={App}> <Route path="about" component={About} /> <Route path="inbox" component={Inbox} /> </Route> </Router> ), document.body)
React Router 知道如何为我们搭建嵌套的 UI,因此我们不用手动找出需要渲染哪些 <Child>
组件。举个例子,对于一个完整的 /about
路径,React Router 会搭建出 <App><About /></App>
。
在内部,router 会将你树级嵌套格式的 <Route>
转变成路由配置,但如果你不熟悉 JSX,你也可以用普通对象来替代:
const routes = { path: '/', component: App, childRoutes: [ { path: 'about', component: About }, { path: 'inbox', component: Inbox }, ] } React.render(<Router routes={routes} />, document.body)
添加更多的 UI
好了,现在我们准备在 inbox UI 内嵌套 inbox messages。
// 新建一个组件让其在 Inbox 内部渲染 const Message = React.createClass({ render() { return <h3>Message</h3> } }) const Inbox = React.createClass({ render() { return ( <div> <h2>Inbox</h2> {/* 渲染这个 child 路由组件 */} {this.props.children || "Welcome to your Inbox"} </div> ) } }) React.render(( <Router> <Route path="/" component={App}> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> {/* 添加一个路由,嵌套进我们想要嵌套的 UI 里 */} <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
现在访问 URL inbox/messages/Jkei3c32
将会匹配到一个新的路由,并且它成功指向了 App -> Inbox -> Message
这个 UI 的分支。
<App> <Inbox> <Message params={ {id: 'Jkei3c32'} } /> </Inbox> </App>
获取 URL 参数
为了从服务器获取 message 数据,我们首先需要知道它的信息。当渲染组件时,React Router 会自动向 Route 组件中注入一些有用的信息,尤其是路径中动态部分的参数。我们的例子中,它指的是 :id
。
const Message = React.createClass({ componentDidMount() { // 来自于路径 `/inbox/messages/:id` const id = this.props.params.id fetchMessage(id, function (err, message) { this.setState({ message: message }) }) }, // ... })
你也可以通过 query 字符串来访问参数。比如你访问 /foo?bar=baz
,你可以通过访问this.props.location.query.bar
从 Route 组件中获得 "baz"
的值。
这就是 React Router 的奥秘。应用的 UI 以盒子中嵌套盒子的方式来表现;然后你可以让这些盒子与 URL 始终保持同步,而且很容易地把它们链接起来。
基础
路由配置
路由配置是一组指令,用来告诉 router 如何匹配 URL以及匹配后如何执行代码。我们来通过一个简单的例子解释一下如何编写路由配置。
import React from 'react' import { Router, Route, Link } from 'react-router' const App = React.createClass({ render() { return ( <div> <h1>App</h1> <ul> <li><Link to="/about">About</Link></li> <li><Link to="/inbox">Inbox</Link></li> </ul> {this.props.children} </div> ) } }) const About = React.createClass({ render() { return <h3>About</h3> } }) const Inbox = React.createClass({ render() { return ( <div> <h2>Inbox</h2> {this.props.children || "Welcome to your Inbox"} </div> ) } }) const Message = React.createClass({ render() { return <h3>Message {this.props.params.id}</h3> } }) React.render(( <Router> <Route path="/" component={App}> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
通过上面的配置,这个应用知道如何渲染下面四个 URL:
URL | 组件 |
---|---|
/ |
App |
/about |
App -> About |
/inbox |
App -> Inbox |
/inbox/messages/:id |
App -> Inbox -> Message |
添加首页
想象一下当 URL 为 /
时,我们想渲染一个在 App
中的组件。不过在此时,App
的 render
中的this.props.children
还是 undefined
。这种情况我们可以使用 IndexRoute 来设置一个默认页面。
import { IndexRoute } from 'react-router' const Dashboard = React.createClass({ render() { return <div>Welcome to the app!</div> } }) React.render(( <Router> <Route path="/" component={App}> {/* 当 url 为/时渲染 Dashboard */} <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
现在,App
的 render
中的 this.props.children
将会是 <Dashboard>
这个元素。这个功能类似 Apache 的DirectoryIndex 以及 nginx的index指令,上述功能都是在当请求的 URL 匹配某个目录时,允许你制定一个类似index.html
的入口文件。
我们的 sitemap 现在看起来如下:
URL | 组件 |
---|---|
/ |
App -> Dashboard |
/about |
App -> About |
/inbox |
App -> Inbox |
/inbox/messages/:id |
App -> Inbox -> Message |
让 UI 从 URL 中解耦出来
如果我们可以将 /inbox
从 /inbox/messages/:id
中去除,并且还能够让 Message
嵌套在 App ->Inbox
中渲染,那会非常赞。绝对路径可以让我们做到这一点。
React.render(( <Router> <Route path="/" component={App}> <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> {/* 使用 /messages/:id 替换 messages/:id */} <Route path="/messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
在多层嵌套路由中使用绝对路径的能力让我们对 URL 拥有绝对的掌控。我们无需在 URL 中添加更多的层级,从而可以使用更简洁的 URL。
我们现在的 URL 对应关系如下:
URL | 组件 |
---|---|
/ |
App -> Dashboard |
/about |
App -> About |
/inbox |
App -> Inbox |
/messages/:id |
App -> Inbox -> Message |
提醒:绝对路径可能在动态路由中无法使用
兼容旧的 URL
等一下,我们刚刚改变了一个 URL! 这样不好。 现在任何人访问 /inbox/messages/5
都会看到一个错误页面。
不要担心。我们可以使用 <Redirect> 使这个 URL 重新正常工作。
import { Redirect } from 'react-router' React.render(( <Router> <Route path="/" component={App}> <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> <Route path="/messages/:id" component={Message} /> {/* 跳转 /inbox/messages/:id 到 /messages/:id */} <Redirect from="messages/:id" to="/messages/:id" /> </Route> </Route> </Router> ), document.body)
现在当有人点击 /inbox/messages/5
这个链接,他们会被自动跳转到 /messages/5
。 :raised_hands:
进入和离开的Hook
Route 可以定义 onEnter
和 onLeave
两个 hook ,这些hook会在页面跳转确认时触发一次。这些 hook 对于一些情况非常的有用,例如权限验证或者在路由跳转前将一些数据持久化保存起来。
在路由跳转过程中,onLeave
hook 会在所有将离开的路由中触发,从最下层的子路由开始直到最外层父路由结束。然后onEnter
hook会从最外层的父路由开始直到最下层子路由结束。
继续我们上面的例子,如果一个用户点击链接,从 /messages/5
跳转到 /about
,下面是这些 hook 的执行顺序:
/messages/:id
的onLeave
/inbox
的onLeave
/about
的onEnter
替换的配置方式
因为 route 一般被嵌套使用,所以使用 JSX 这种天然具有简洁嵌套型语法的结构来描述它们的关系非常方便。然而,如果你不想使用 JSX,也可以直接使用原生 route 数组对象。
上面我们讨论的路由配置可以被写成下面这个样子:
const routeConfig = [ { path: '/', component: App, indexRoute: { component: Dashboard }, childRoutes: [ { path: 'about', component: About }, { path: 'inbox', component: Inbox, childRoutes: [ { path: '/messages/:id', component: Message }, { path: 'messages/:id', onEnter: function (nextState, replaceState) { replaceState(null, '/messages/' + nextState.params.id) } } ] } ] } ] React.render(<Router routes={routeConfig} />, document.body)
路由匹配原理
路由拥有三个属性来决定是否“匹配“一个 URL:
- 嵌套关系 和
- 它的
路径语法
- 它的 优先级
嵌套关系
React Router 使用路由嵌套的概念来让你定义 view 的嵌套集合,当一个给定的 URL 被调用时,整个集合中(命中的部分)都会被渲染。嵌套路由被描述成一种树形结构。React Router 会深度优先遍历整个理由配置来寻找一个与给定的 URL 相匹配的路由。
路径语法
路由路径是匹配一个(或一部分)URL 的 一个字符串模式。大部分的路由路径都可以直接按照字面量理解,除了以下几个特殊的符号:
:paramName
– 匹配一段位于/
、?
或#
之后的 URL。 命中的部分将被作为一个参数()
– 在它内部的内容被认为是可选的*
– 匹配任意字符(非贪婪的)直到命中下一个字符或者整个 URL 的末尾,并创建一个splat
参数
<Route path="/hello/:name"> // 匹配 /hello/michael 和 /hello/ryan <Route path="/hello(/:name)"> // 匹配 /hello, /hello/michael 和 /hello/ryan <Route path="/files/*.*"> // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg
如果一个路由使用了相对路径
,那么完整的路径将由它的所有祖先节点的路径
和自身指定的相对路径
拼接而成。使用绝对路径
可以使路由匹配行为忽略嵌套关系。
优先级
最后,路由算法会根据定义的顺序自顶向下匹配路由。因此,当你拥有两个兄弟路由节点配置时,你必须确认前一个路由不会匹配后一个路由中的路径
。例如,千万不要这么做:
<Route path="/comments" ... /> <Redirect from="/comments" ... />
Histories
React Router 是建立在 history 之上的。 简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location
对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。
常用的 history 有三种形式, 但是你也可以使用 React Router 实现自定义的 history。
从 history 库中获取它们:
// JavaScript 模块导入(译者注:ES6 形式) import createBrowserHistory from 'history/lib/createBrowserHistory' // 或者以 commonjs 的形式导入 const createBrowserHistory = require('history/lib/createBrowserHistory')
createHashHistory
这是一个你会获取到的默认 history ,如果你不指定某个 history (即 <Router>{/* your routes */}</Router>
)。它用到的是 URL 中的 hash(#
)部分去创建形如 example.com/#/some/path
的路由。
我应该使用 createHashHistory
吗?
Hash history 是默认的,因为它可以在服务器中不作任何配置就可以运行,并且它在全部常用的浏览器包括 IE8+ 都可以用。但是我们不推荐在实际生产中用到它,因为每一个 web 应用都应该有目的地去使用createBrowserHistory
。
像这样 ?_k=ckuvup
没用的在 URL 中是什么?
当一个 history 通过应用程序的 pushState
或 replaceState
跳转时,它可以在新的 location 中存储 “location state” 而不显示在 URL 中,这就像是在一个 HTML 中 post 的表单数据。
在 DOM API 中,这些 hash history 通过 window.location.hash = newHash
很简单地被用于跳转,且不用存储它们的location state。但我们想全部的 history 都能够使用location state,因此我们要为每一个 location 创建一个唯一的 key,并把它们的状态存储在 session storage 中。当访客点击“后退”和“前进”时,我们就会有一个机制去恢复这些 location state。
你也可以不使用这个特性。
// 选择退出连续的 state, 不推荐使用 let history = createHistory({ queryKey: false });
createBrowserHistory
Browser history 是由 React Router 创建浏览器应用推荐的 history。它使用 History API 在浏览器中被创建用于处理 URL,新建一个像这样真实的 URL example.com/some/path
。
服务器配置
首先服务器应该能够处理 URL 请求。处理应用启动最初的 /
这样的请求应该没问题,但当用户来回跳转并在 /accounts/123
刷新时,服务器就会收到来自 /accounts/123
的请求,这时你需要处理这个 URL 并在响应中包含 JavaScript 程序代码。
一个 express 的应用可能看起来像这样的:
const express = require('express') const path = require('path') const port = process.env.PORT || 8080 const app = express() // 通常用于加载静态资源 app.use(express.static(__dirname + '/public')) // 在你应用 JavaScript 文件中包含了一个 script 标签 // 的 index.html 中处理任何一个 route app.get('*', function (request, response){ response.sendFile(path.resolve(__dirname, 'public', 'index.html')) }) app.listen(port) console.log("server started on port " + port)
IE8, IE9 支持情况
如果我们能使用浏览器自带的 window.history
API,那么我们的特性就可以被浏览器所检测到。如果不能,那么任何调用跳转的应用就会导致 全页面刷新,它允许在构建应用和更新浏览器时会有一个更好的用户体验,但仍然支持的是旧版的。
你可能会想为什么我们不后退到 hash history,问题是这些 URL 是不确定的。如果一个访客在 hash history 和 browser history 上共享一个 URL,然后他们也共享同一个后退功能,最后我们会以产生笛卡尔积数量级的、无限多的 URL 而崩溃。
createMemoryHistory
Memory history 不会在地址栏被操作或读取。这就解释了我们是如何实现服务器渲染的。同时它也非常适合测试和其他的渲染环境(像 React Native )
实现示例
import React from 'react' import createBrowserHistory from 'history/lib/createBrowserHistory' import { Router, Route, IndexRoute } from 'react-router' import App from '../components/App' import Home from '../components/Home' import About from '../components/About' import Features from '../components/Features' React.render( <Router history={createBrowserHistory()}> <Route path='/' component={App}> <IndexRoute component={Home} /> <Route path='about' component={About} /> <Route path='features' component={Features} /> </Route> </Router>, document.getElementById('app') )
默认路由(IndexRoute)与 IndexLink
默认路由(IndexRoute)
在解释 默认路由(IndexRoute)
的用例之前,我们来设想一下,一个不使用默认路由的路由配置是什么样的:
<Router> <Route path="/" component={App}> <Route path="accounts" component={Accounts}/> <Route path="statements" component={Statements}/> </Route> </Router>
当用户访问 /
时, App 组件被渲染,但组件内的子元素却没有, App
内部的 this.props.children
为 undefined 。 你可以简单地使用 `{this.props.children ||
}` 来渲染一些默认的 UI 组件。
但现在,Home
无法参与到比如 onEnter
hook 这些路由机制中来。 在 Home
的位置,渲染的是Accounts
和 Statements
。 由此,router 允许你使用 IndexRoute
,以使 Home
作为最高层级的路由出现.
<Router> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="accounts" component={Accounts}/> <Route path="statements" component={Statements}/> </Route> </Router>
现在 App
能够渲染 {this.props.children}
了, 我们也有了一个最高层级的路由,使 Home
可以参与进来。
Index Links
如果你在这个app中使用 <Link to="/">Home</Link>
, 它会一直处于激活状态,因为所有的 URL 的开头都是 /
。 这确实是个问题,因为我们仅仅希望在 Home
被渲染后,激活并链接到它。
如果需要在 Home
路由被渲染后才激活的指向 /
的链接,请使用 <IndexLink to="/">Home</IndexLink>
动态路由
React Router 适用于小型网站,比如 React.js Training,也可以支持 Facebook 和 Twitter 这类大型网站。
对于大型应用来说,一个首当其冲的问题就是所需加载的 JavaScript 的大小。程序应当只加载当前渲染页所需的 JavaScript。有些开发者将这种方式称之为“代码分拆” — 将所有的代码分拆成多个小包,在用户浏览过程中按需加载。
对于底层细节的修改不应该需要它上面每一层级都进行修改。举个例子,为一个照片浏览页添加一个路径不应该影响到首页加载的 JavaScript 的大小。也不能因为多个团队共用一个大型的路由配置文件而造成合并时的冲突。
路由是个非常适于做代码分拆的地方:它的责任就是配置好每个 view。
React Router 里的路径匹配以及组件加载都是异步完成的,不仅允许你延迟加载组件,并且可以延迟加载路由配置。在首次加载包中你只需要有一个路径定义,路由会自动解析剩下的路径。
Route 可以定义 getChildRoutes
,getIndexRoute
和 getComponents
这几个函数。它们都是异步执行,并且只有在需要时才被调用。我们将这种方式称之为 “逐渐匹配”。 React Router 会逐渐的匹配URL并只加载该URL对应页面所需的路径配置和组件。
如果配合 webpack 这类的代码分拆工具使用的话,一个原本繁琐的构架就会变得更简洁明了。
const CourseRoute = { path: 'course/:courseId', getChildRoutes(location, callback) { require.ensure([], function (require) { callback(null, [ require('./routes/Announcements'), require('./routes/Assignments'), require('./routes/Grades'), ]) }) }, getIndexRoute(location, callback) { require.ensure([], function (require) { callback(null, require('./components/Index')) }) }, getComponents(location, callback) { require.ensure([], function (require) { callback(null, require('./components/Course')) }) } }
现在,可以看一下 webpack 都做了哪些。开玩笑,我现在不想让你伤心。
运行 huge apps 实例,打开浏览器的审查元素选项。你会发现在路由发生改变时,资源按需加载。
跳转前确认
React Router 提供一个 routerWillLeave
生命周期钩子,这使得 React 组件可以拦截正在发生的跳转,或在离开 route 前提示用户。routerWillLeave
返回值有以下两种:
return false
取消此次跳转return
返回提示信息,在离开 route 前提示用户进行确认。
你可以在 route 组件 中引入 Lifecycle
mixin 来安装这个钩子。
import { Lifecycle } from 'react-router' const Home = React.createClass({ // 假设 Home 是一个 route 组件,它可能会使用 // Lifecycle mixin 去获得一个 routerWillLeave 方法。 mixins: [ Lifecycle ], routerWillLeave(nextLocation) { if (!this.state.isSaved) return 'Your work is not saved! Are you sure you want to leave?' }, // ... })
如果你在组件中使用了 ES6 类,你可以借助 react-mixin 包将 Lifecycle
mixin 添加到组件中,不过我们推荐使用 React.createClass
来创建组件,初始化路由的生命周期钩子函数。
如果你想在一个深层嵌套的组件中使用 routerWillLeave
钩子,只需在 route 组件 中引入 RouteContext
mixin,这样就会把 route
放到 context 中。
import { Lifecycle, RouteContext } from 'react-router' const Home = React.createClass({ // route 会被放到 Home 和它子组件及孙子组件的 context 中, // 这样在层级树中 Home 及其所有子组件都可以拿到 route。 mixins: [ RouteContext ], render() { return <NestedForm /> } }) const NestedForm = React.createClass({ // 后代组件使用 Lifecycle mixin 获得 // 一个 routerWillLeave 的方法。 mixins: [ Lifecycle ], routerWillLeave(nextLocation) { if (!this.state.isSaved) return 'Your work is not saved! Are you sure you want to leave?' }, // ... })
在组件外部使用导航
虽然在路由组件内部,可以获取 this.props.history
来实现导航。也可以引入 History
mixin,并用它提供的 this.history
来实现导航。然而,很多应用仍然希望可以在他们的组件外部使用导航功能。
这个非常简单,要做的就是拿到你的 history 对象:
你可以在应用的任何地方,创建一个模块来输出你的 history 对象。
// history.js import createBrowserHistory from 'history/lib/createBrowserHistory' export default createBrowserHistory()
然后引入这个模块渲染一个 <Router>
:
// index.js import history from './history' React.render(<Router history={history}/>, el)
现在你可以在应用的其他地方使用这个 history 对象了,例如一个 flux action 文件
// actions.js import history from './history' history.replaceState(null, '/some/path')
文章评论