React Router是一个基于 React 之上的强大路由库,它可让你向应用中快速地添加视图和数据流,同时保持页面与URL间的同步。在没有react-router的时候,咱们须要对URL进行监听,当URL的hash部分(指的是 # 后的部分)变化后,根据hash来渲染不一样的组件。看起来很直接,但它很快就会变得复杂起来,当咱们的组件嵌套层级增长的时候,为了让咱们的URL解析变得更智能,咱们须要编写不少代码来实现指定URL应该渲染哪个嵌套的UI组件分支,因此咱们须要另一个解决方案,这个时候react-router出现了。html
路由配置是一组指令,用来告诉 router 如何匹配 URL以及匹配后如何执行代码。咱们来经过一个简单的例子解释一下如何编写路由配置。node
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:react
URL | 组件 |
---|---|
/ | App |
/about | App -> About |
/indox | App -> Inbox |
/inbox/message/:id | App -> Indox -> Message |
若是咱们能够将 /inbox
从 /inbox/messages/:id
中去除,而且还可以让 Message 嵌套在 App -> Inbox 中渲染,那会很是赞。绝对路径可让咱们作到这一点。webpack
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 对应关系以下:git
URL | 组件 |
---|---|
/ | App |
/about | App -> About |
/indox | App -> Inbox |
/message/:id | App -> Indox -> Message |
在Message组件中,咱们能够经过如下方式获取路由参数中的idgithub
const Message = React.createClass({ render(){ // 只适用于 /message/:id 形式的路由参数 // 若要获取 ?id=yourId 形式的参数,使用this.props.location.query const { id } = this.props.params; return <div>参数id:{id}</div> } })
Route 能够定义 onEnter 和 onLeave 两个 hook ,这些hook会在页面跳转确认时触发一次。这些 hook 对于一些状况很是的有用,例如权限验证或者在路由跳转前将一些数据持久化保存起来。web
在路由跳转过程当中,onLeave hook 会在全部将离开的路由中触发,从最下层的子路由开始直到最外层父路由结束。而后onEnter hook会从最外层的父路由开始直到最下层子路由结束。算法
继续咱们上面的例子,若是一个用户点击连接,从 /messages/5
跳转到 /about
,下面是这些 hook 的执行顺序:express
/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" ... />
React Router 是创建在 history
之上的。 简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 而后 router 使用它匹配到路由,最后正确地渲染对应的组件。
经常使用的 history 有三种形式, 可是你也可使用 React Router 实现自定义的 history。
browserHistory
hashHistory
createMemoryHistory
你能够从 React Router 中引入它们:
// JavaScript 模块导入(译者注:ES6 形式) import { browserHistory } from 'react-router'
而后将它们传递给<Router>
:
render( <Router history={browserHistory} routes={routes} />, document.getElementById('app') )
Browser history 是使用 React Router 的应用推荐的 history。它使用浏览器中的 History API 用于处理 URL,建立一个像example.com/some/path这样真实的 URL 。
服务器须要作好处理 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)
若是咱们能使用浏览器自带的 window.history API,那么咱们的特性就能够被浏览器所检测到。若是不能,那么任何调用跳转的应用就会致使 全页面刷新,它容许在构建应用和更新浏览器时会有一个更好的用户体验,但仍然支持的是旧版的。
你可能会想为何咱们不后退到 hash history,问题是这些 URL 是不肯定的。若是一个访客在 hash history 和 browser history 上共享一个 URL,而后他们也共享同一个后退功能,最后咱们会以产生笛卡尔积数量级的、无限多的 URL 而崩溃。
Hash history 使用 URL 中的 hash(#)部分去建立形如 example.com/#/some/path
的路由。
createHashHistory
吗?Hash history 不须要服务器任何配置就能够运行,若是你刚刚入门,那就使用它吧。可是咱们不推荐在实际线上环境中用到它,由于每个 web 应用都应该渴望使用 browserHistory
。
?_k=ckuvup
没用的在 URL 中是什么?当一个 history 经过应用程序的 push 或 replace 跳转时,它能够在新的 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。
Memory history 不会在地址栏被操做或读取。这就解释了咱们是如何实现服务器渲染的。同时它也很是适合测试和其余的渲染环境(像 React Native )。
和另外两种history的一点不一样是你必须建立它,这种方式便于测试。
const history = createMemoryHistory(location)
import React from 'react' import { render } from 'react-dom' import { browserHistory, 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' render( <Router history={browserHistory}> <Route path='/' component={App}> <IndexRoute component={Home} /> <Route path='about' component={About} /> <Route path='features' component={Features} /> </Route> </Router>, document.getElementById('app') )
React Router 适用于小型网站,对于大型应用来讲,一个首当其冲的问题就是所需加载的 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')) }) } }
React Router 提供一个 routerWillLeave
生命周期钩子,这使得 React 组件能够拦截正在发生的跳转,或在离开 route 前提示用户。routerWillLeave
返回值有如下两种:
return false
取消这次跳转
return
返回提示信息,在离开 route 前提示用户进行确认。
服务端渲染与客户端渲染有些许不一样,由于你须要:
发生错误时发送一个 500
的响应
须要重定向时发送一个 30x 的响应
在渲染以前得到数据 (用 router 帮你完成这点)
为了迎合这一需求,你要在 Router API 下一层使用:
使用 match
在渲染以前根据 location
匹配 route
使用 RoutingContext
同步渲染 route 组件
它看起来像一个虚拟的 JavaScript 服务器:
import { renderToString } from 'react-dom/server' import { match, RoutingContext } from 'react-router' import routes from './routes' serve((req, res) => { // 注意!这里的 req.url 应该是从初始请求中得到的 // 完整的 URL 路径,包括查询字符串。 match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { if (error) { res.send(500, error.message) } else if (redirectLocation) { res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { res.send(200, renderToString(<RoutingContext {...renderProps} />)) } else { res.send(404, 'Not found') } }) })
至于加载数据,你能够用 renderProps
去构建任何你想要的形式——例如在 route 组件中添加一个静态的 load
方法,或如在 route 中添加数据加载的方法——由你决定。