React Router 是一个基于 React 之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 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)
可以使用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
将会是
这个元素。
现在的sitemap
如下所示:
URL | 组件 |
---|---|
/ | App -> Dashboard |
/about | App -> About |
/inbox | App -> Inbox |
/inbox/messages/:id | App -> Inbox -> Message |
绝对路径可以将 /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发生了改变,我们可以使用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)
路由拥有三个属性来决定是否“匹配“一个 URL:
嵌套关系
嵌套路由被描述成一种树形结构。React Router 会深度优先遍历整个路由配置来寻找一个与给定的 URL 相匹配的路由。
路径语法
:paramName
– 匹配一段位于 /
、?
或 #
之后的 URL。 命中的部分将被作为一个参数
()
– 在它内部的内容被认为是可选的
*
– 匹配任意字符(非贪婪的)直到命中下一个字符或者整个 URL 的末尾,并创建一个 splat 参数
// 匹配 /hello/michael 和 /hello/ryan
// 匹配 /hello, /hello/michael 和 /hello/ryan
// 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg
如果一个路由使用了相对路径,那么完整的路径将由它的所有祖先节点的路径和自身指定的相对路径拼接而成。使用绝对路径可以使路由匹配行为忽略嵌套关系。
优先级
路由算法会根据定义的顺序自顶向下匹配路由。因此,当拥有两个兄弟路由节点配置时,必须确认前一个路由不会匹配后一个路由中的路径。例如:
<Route path="/comments" ... />
<Redirect from="/comments" ... />
一个 history
知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location
对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。
常用的 history 有三种形式, 但也可以使用 React Router 实现自定义的 history
browserHistory
hashHistory
createMemoryHistory
Browser history 是使用 React Router 的应用推荐的 history。它使用浏览器中的 History API 用于处理 URL,创建一个像example.com/some/path
这样真实的 URL 。
Hash history 使用 URL 中的 hash(#)
部分去创建形如 example.com/#/some/path
的路由。
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')
)
当用户访问 /
时, App 组件被渲染,但组件内的子元素却没有, App
内部的 this.props.children
为 undefined 。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
可以参与进来。
如果需要在 Home
路由被渲染后才激活的指向 /
的链接,请使用
React Router 里的路径匹配以及组件加载都是异步完成的,不仅允许延迟加载组件,并且可以延迟加载路由配置。
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 前提示用户进行确认。可以在 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?'
},
// ...
})
推荐使用 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?'
},
// ...
})
服务端渲染与客户端渲染有些许不同,因为你需要:
发生错误时发送一个 500
的响应
需要重定向时发送一个 30x
的响应
在渲染之前获得数据 (用 router 完成这点)
为了迎合这一需求,要在 API 下一层使用:
match
在渲染之前根据location
匹配 route
RoutingContext
同步渲染 route 组件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')
}
})
})
假如路由配置如下:
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="invoices/:invoiceId" component={Invoice}/>
<Route path="accounts/:accountId" component={Account}/>
</Route>
路由切换时,组件生命周期的变化情况如下:
当用户打开应用的/
页面
组件 | 生命周期 |
---|---|
App | componentDidMount |
Home | componentDidMount |
Invoice | N/A |
Account | N/A |
当用户从/
跳转到/invoice/123
组件 | 生命周期 |
---|---|
App | componentWillReceiveProps ,componentDidUpdate |
Home | componentWillUnmount |
Invoice | componentDidMount |
Account | N/A |
App
从 router 中接收到新的 props(例如 children
、params
、location
等数据), 所以 App
触发了 componentWillReceiveProps
和 componentDidUpdate
两个生命周期方法Home
不再被渲染,所以它将被移除Invoice
首次被挂载当用户从/invoice/123
跳转到/invoice/789
组件 | 生命周期 |
---|---|
App | componentWillReceiveProps ,componentDidUpdate |
Home | N/A |
Invoice | componentWillReceiveProps ,componentDidUpdate |
Account | N/A |
所有的组件之前都已经被挂载, 所以只是从 router 更新了 props.
当从/invoice/789
跳转到/accounts/123
组件 | 生命周期 |
---|---|
App | componentWillReceiveProps ,componentDidUpdate |
Home | N/A |
Invoice | componentWillUnmount |
Account | componentDidMount |
虽然还有其他通过 router 获取数据的方法, 但是最简单的方法是通过组件生命周期 Hook 来实现。示例如下,在 Invoice
组件里实现一个简单的数据获取功能。
let Invoice = React.createClass({
getInitialState () {
return {
invoice: null
}
},
componentDidMount () {
// 上面的步骤2,在此初始化数据
this.fetchInvoice()
},
componentDidUpdate (prevProps) {
// 上面步骤3,通过参数更新数据
let oldId = prevProps.params.invoiceId
let newId = this.props.params.invoiceId
if (newId !== oldId)
this.fetchInvoice()
},
componentWillUnmount () {
// 上面步骤四,在组件移除前忽略正在进行中的请求
this.ignoreLastFetch = true
},
fetchInvoice () {
let url = `/api/invoices/${this.props.params.invoiceId}`
this.request = fetch(url, (err, data) => {
if (!this.ignoreLastFetch)
this.setState({ invoice: data.invoice })
})
},
render () {
return <InvoiceView invoice={this.state.invoice} />
}
})
虽然在组件内部可以使用 this.context.router
来实现导航,但许多应用想要在组件外部使用导航。使用Router组件上被赋予的history可以在组件外部实现导航。
// your main file that renders a Router
import { Router, browserHistory } from 'react-router'
import routes from './app/routes'
render(<Router history={browserHistory} routes={routes}/>, el)
// somewhere like a redux/flux action file:
import { browserHistory } from 'react-router'
browserHistory.push('/some/path')