React Router学习笔记
React Router(本文后续简写为RR)是一个专门服务于React应用的强大的路由库。利用它你可以轻松的建立URL和UI之间的对应关系、在浏览器历史记录中自由导航。
本章先手工实现一个简单的路由机制,然后利用RR进行改造,以了解RR的优势和基本功能。
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 44 45 46 47 |
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 { // 使用Hash部分进行路由 route: window.location.hash.substr( 1 ) } }, componentDidMount() { // 当Hash部分变化后,需要改变React状态进行重渲染 window.addEventListener( 'hashchange', () => { this.setState( { route: window.location.hash.substr( 1 ) } ) } ) }, render() { // 根据状态,也就是URL的Hash的不同,决定渲染哪个子组件 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> ) } } ) render( <App />, document.body ) |
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 |
import React from 'react' import ReactDom from 'react-dom' import { Router, Route, IndexRoute, Link, hashHistory } from 'react-router' const App = React.createClass( { render() { return ( <div> <h1>App</h1> /* 将a替换为RR提供的Link组件 */ <ul> <li><Link to="/about" >About</Link></li> <li><Link to='/inbox' >Inbox</Link></li> </ul> /* 这里不需要手工判断渲染哪个子路由组件,RR知道 */ {this.props.children} </div> ) } } ) /** * 渲染的是由Router/Route定义的路由规则,而非UI组件 */ ReactDom.render( ( /* 使用hashHistory,表示路由的判断依据是URL的Hash部分 */ <Router history={hashHistory}> /* path指定路由基准URL */ <Route path="/" component={App}> /* 路由的嵌套层次,与组件的嵌套层次对应 */ <IndexRoute component={Home}/> /* 默认UI */ <Route path="about" component={About}/> /* Hash部分是About时,例如/#/about */ <Route path="inbox" component={Inbox}/> /* Hash部分是Inbox时 */ </Route> </Router> ), document.body ) |
可以看到,RR知道如何(根据配置信息)构建嵌套的UI,你不需要手工的读取URL并找到对应的Child组件, App与其内部的子组件实现了解耦。
现在,我们在inbox这个路径下面再嵌套一级子路由:
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 |
const Message = React.createClass( { render() { return <h3>Message</h3> } } ) const Inbox = React.createClass( { render() { return ( <div> <h2>Inbox</h2> /* 渲染下一级子路由组件 */ {this.props.children} </div> ) } } ) ReactDom.render( ( <Router history={hashHistory}> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="about" component={About}/> <Route path="inbox" component={Inbox}> /* 嵌套路由 */ <IndexRoute component={InboxStats}/> /* 嵌套路由的默认UI */ /* 在类似/inbox/messages/123这样的URL下渲染Message组件 */ <Route path="messages/:id" component={Message}/> </Route> </Route> </Router> ), document.body ) |
这样,当你访问/inbox/messages/123时,RR将会构建如下组件层次:
1 2 3 4 5 |
<App> <Inbox> <Message params={{ id: 'Jkei3c32' }}/> </Inbox> </App> |
当你访问/inbox时,则构建如下组件层次:
1 2 3 4 5 |
<App> <Inbox> <InboxStats/> </Inbox> </App> |
总之,URL的层次和组件的层次具有对应关系,上层组件的this.props.children,等于匹配的下层路由的component属性所指向的组件。注意这种对应关系不一定是“严格”的:
- URL中的一个“层次”,可以跨越多个以斜杠 / 划分的片断
- 组件中的一个“层次”,可以包含多级React元素。层次的结果取决于你在何处声明this.props.children
这种对应关系很自然,但是如果要手工实现的话需要编写不少罗嗦的代码。
React组件会被RR自动注入路径变量,例如messages/:id中的id,可以通过 props.params.id 读取到:
1 2 3 4 5 6 7 8 |
const Message = React.createClass( { componentDidMount() { const id = this.props.params.id fetchMessage( id, function ( err, message ) { this.setState( { message: message } ) } ) } } ) |
URL中附带的查询参数也被注入,例如/user?name=alex中的name,可以通过 props.location.query.name 读取到。
该组件用于将RR引入到React应用程序中,渲染时一般将其作为JSX根元素:
1 2 3 4 5 |
ReactDom.render( ( <Router history={hashHistory}> /* ... */ </Router> ), document.body ) |
属性 | 说明 | ||
history | 该router需要监听的history对象,通常是browserHistory或hashHistory | ||
children | 一个或者多个Route、PlainRoute组件。指定路由的规则集 | ||
routes | children的别名 | ||
createElement | 当router准备渲染一个组件树分支时,调用此函数进行React元素的创建。你可以用该方法控制创建过程:
|
||
onError |
方法签名: onError(error) 进行路由匹配的时候可能出现错误,这些错误通常来自于那些异步的特性,例如route.getComponents、route.getIndexRoute、route.getChildRoutes等。你可以基于此方法进行捕获和处理 |
||
onUpdate |
一旦router改变其状态以对URL变更进行响应,即调用此方法 |
这是一个配置组件(Configuration Components),定义一个路由规则,该规则将一个URL片断和一个React组件对应起来。路由规则可以嵌套,并与URL嵌套、组件嵌套对应。
JSX中的Route元素实际上等价于Route的子类型PlainRoute。
属性列表:
这是一个配置组件,属于特殊的路由规则,当(下层)URL片断为空时,匹配此规则。例如:
1 2 3 |
<Route path="/" component={App}> <IndexRoute component={Dashboard} /> </Route> |
当你访问URL / 时,会渲染App组件,且其children为一个Dashboard组件。
这是一个配置组件,用于URL的重定向,例如:
1 |
<Redirect from="messages/:id" to="/messages/:id" /> |
属性 | 说明 |
from | 需要重定向的URL,包含路径变量部分 |
to | 重定向到的目标 |
query | 查询参数部分,默认情况下自动把from的查询参数带过去 |
这是一个配置组件,指定指定默认使用的下层URL。例如:
1 2 3 4 5 |
<Route path="/" component={App}> <IndexRedirect to="/welcome" /> // 访问 / 时自动重定向到 /welcome <Route path="welcome" component={Welcome} /> </Route> |
该组件用于触发路由切换,以便在应用程序中导航,该组件渲染为a标签。
如果Link指向的路由恰恰是应用中的当前路由(URL匹配),则RR自动给Link添加activeClassName样式类,并且其activeStyle指定的样式被启用。
属性列表:
属性 | 说明 | ||
to |
一个位置描述符(location descriptor)对象,或者一个字符串。如果不指定该属性,生成一个没有href的a标签 使用字符串时,指定需要链接到的绝对路径。使用位置描述符时,可以指定以下属性:
示例:
|
||
activeClassName | to指定的路由是当前路由时,启用的样式类 | ||
activeStyle | to指定的路由是当前路由时,启用的样式 | ||
onClick | 点击时执行的函数,在此函数调用e.preventDefault()可以阻止路由切换 | ||
onlyActiveOnIndex |
仅当to与当前路由精确匹配时,才认为是Active。等价于 <IndexLink> 组件 如果不指定该属性,那么 <Link to="/">Home</Link> 这个链接总是Active,因为URL总是以/开头。此时可以使用 <IndexLink to="/">Home</IndexLink> 代替,这样仅当URL为/时才匹配 |
同级别的Route,先声明的具有更高的优先级。
URL语法和匹配规则如下:
语法 | 匹配说明 | ||
:paramName |
匹配一个URL片断,直到遇到 / 、 ? 或者 # ,组件自动获得 params.paramName 属性。举例:
|
||
() | 包围URL的一部分,表示该部分是可选的。举例:
|
||
* | 非贪婪的通配,遇到此通配符后面指定的那个字符之前一直匹配。捕获到的匹配项会存入
params.splat 属性中。举例:
|
||
** | 贪婪的通配,直到遇到
/ 、
? 或者
# 。捕获到的匹配项会存入
params.splat 属性中。举例:
|
RR在history库的基础上构建,一个history对象可以监听浏览器的URL的改变,并把URL解析为一个location对象。RR使用此location对象来匹配路由规则并渲染正确的组件树。
缺省可用的history实现包括三种:
History实现 | 说明 | ||||||
browserHistory |
对于运行在浏览器中的应用,这是推荐的实现。它使用浏览器内置的History API来操控URL,能够创建“真实的”URL,例如 gmem.cc/some/path 服务器配置 要在所有浏览器中使用这种History实现,需要服务器的支持。你可能需要将某个通配的路径映射到同一个HTML文件,例如gmem.cc/**总是映射到gmem.cc/index.html。 使用Express作为服务器时,可以参考如下代码:
使用Nginx时,可以使用try_files指令:
使用Apache时可以使用rewrite模块:
IE8,IE9支持 RR会自动检测浏览器是否支持History API,如果不支持,所有URL转换都会导致完全的页面reload |
||||||
hashHistory |
仅仅使用URL的哈希(#)部分,生成gmem.cc/#/some/path风格的URL 该实现的优点是不需要配置服务器。缺点是URL比较难堪而且不支持服务器端渲染 |
||||||
createMemoryHistory | 不会操控浏览器地址栏,使用RR进行服务器端渲染时使用,也可以用于测试React Native等其它渲染环境 |
用于包装其它React组件,为其提供 props.router 属性。函数签名:
1 2 3 4 5 6 |
/** * Component 被包装的组件 * options 选项: * withRef 如果为true,则包装后的组件的getWrappedInstance()返回被包装组件 */ withRouter(Component, [options]) |
props.router属性与context.router是同一种对象。
依据给定的路由状态,渲染对应的组件树。Router组件使用了该组件,在React组件的上下文对象上添加一个 this.context.router 属性。
该对象提供与路由有关的方法和数据,你可以使用该对象进行编程式的路由控制:
属性/方法 | 说明 | ||
push(pathOrLoc) | 切换路由到一个新的URL,并在浏览器历史中压入条目。示例:
|
||
replace(pathOrLoc) | 类似于push,但是替换浏览器历史的当前条目 | ||
go(n) | 在浏览器历史中前进或者后退 | ||
goBack() | 在浏览器历史中后退一步 | ||
goForward() | 在浏览器历史中前进一步 | ||
setRouteLeaveHook(route, hook) | 注册一个钩子,在离开route这个路由时调用,用于导航确认 | ||
createPath(pathOrLoc, query) | 根据router的配置,把查询参数对象转换为URL路径名(不包括协议、域名部分) | ||
createHref(pathOrLoc, query) |
创建一个URL,如果使用hashHitory,会自动在URL路径名前面添加#/ |
||
isActive(pathOrLoc, indexOnly) | 判断pathOrLoc是否对应当前路由。如果匹配路由R,则同样匹配R的祖先路由,那么在R及其祖先路由对应的Component中调用该方法,均返回true |
所谓路由组件,是指路由规则(Route)关联的组件,当路由规则被匹配、且父路由组件输出了当前路由组件时,路由组件被自动渲染。
路由组件被渲染时,RR自动为其注入一些属性。
属性 | 说明 |
location | 当前location对象 |
params | URL中的动态部分,包括路径变量捕获 |
route | 导致此组件被渲染的路由对象 |
router | 与context.router相同 |
routeParams |
捕获到的、在Route.path中直接声明的路径变量。如果Route.path为users/:userId 而当前URL为/users/123/portfolios/345。那么:
|
children | 匹配的、将被渲染的子路由组件。如果当前路由使用命名组件,则该属性为undefined。各命名组件将作为this.props的直接属性 |
对于大型应用来说,下载尽可能少的JavaScript文件以启动应用很重要,最好仅下载与当前的UI相关的JS。如果不这样做,用户将忍受过长的加载时间。生产环境下我们通常使用模块化,配合代码分割(Code Splitting)技术来满足尽快启动的需求,然后随着用户的操作不断加载用到的JS。
路由定义了UI的样子,很自然的可以作为代码分割点。
RR支持异步的读取路由规则、异步的加载组件。在最初的捆绑文件(Bundle,即启动应用的那个代码分割块)中,你只需要提供一个路由规则,其它规则可以后续按需加载。
Route组件可以定义getChildRoutes、getIndexRoute、getComponents方法,这些方法仅在需要的时候才会被调用以完成规则匹配和组件渲染。RR称这种方式为渐进匹配(gradual matching)——逐步的匹配URL片段且仅仅加载必要的信息。
下面是一个动态路由的示例:
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 |
const CourseRoute = { path: 'course/:courseId', // 当尝试导航到course/**/**时,下面的方法被调用 getChildRoutes(partialNextState, callback) { // 这里使用Webpack的CommonJS扩展 require.ensure([], function (require) { callback(null, [ // 同步加载子路由定义模块 require('./routes/Announcements'), require('./routes/Assignments'), require('./routes/Grades'), ]) }) }, // 当尝试导航到course/**时,下面的方法被调用 getIndexRoute(partialNextState, callback) { require.ensure([], function (require) { callback(null, { component: require('./components/Index'), }) }) }, // 当尝试导航到course/**时,该方法被调用,加载对应的组件 getComponents(nextState, callback) { require.ensure([], function (require) { callback(null, require('./components/Course')) }) } } |
你可以调用router的 setRouteLeaveHook 方法,设置一个钩子,当离开某个路由时,该钩子会被执行,你可以利用此钩子:
- 向用户做出提示
- 阻止导航的发生
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// v2.4.0引入的withRouter可以向组件注入当前router对象 const Home = withRouter( React.createClass({ componentDidMount() { // 为当前组件对应的route对象设置钩子 this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave) }, routerWillLeave(nextLocation) { // 返回false禁止导航 // 返回字符串则让用户确认是否导航 if (!this.state.isSaved) return 'Your work is not saved! Are you sure you want to leave?' }, }) ) |
你可以使用 withRouter 包装一个组件,从而通过 this.props.router 获得当前router对象的引用。有了router对象后你就可以随意导航(触发路由切换)了。
在React组件外部,例如Redux中间件或者Flux Action的代码中,你可以通过history对象进行导航:
1 2 3 4 5 6 7 |
import {browserHistory} from 'react-router' // 导航到 /some/path. browserHistory.push('/some/path') // 后退到前一个URL browserHistory.goBack() |
为了简便,RR通过顶级模块react-router暴露了完整的API。这导致整个RR库及其依赖被包含到入口点Bundle中,从而增加了Bundle的大小。为了避免此问题,可以从react-router/lib的子模块进行导入:
1 2 3 4 5 |
import { Link, Route, Router } from 'react-router' // 可以改写为: import Link from 'react-router/lib/Link' import Route from 'react-router/lib/Route' import Router from 'react-router/lib/Router' |
Leave a Reply