React-Router
简介
React Router 是一个基于 React 之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 之间的同步。
使用 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)
如果你不熟悉 JSX,也可以用普通对象来替代。
复制 const routes = {
path: '/',
component: App,
childRoutes: [
{path: 'about', component: About},
{path: 'inbox', component: Inbox}
]
}
React.render(<Router routes={routes} />, document.body)
获取 URL 参数
为了从服务器获取 message 数据,我们首先需要知道它的信息。当渲染组件时,React Router 会自动向 Route 组件中注入一些有用的信息,尤其是路径中动态部分的参数。
复制 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'
的值
基础
添加首页
当URL为/
时,我们想渲染一个在 App
中的组件。不过此时,App 的 render中的this.prop.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)
让 UI 从 URL 中解耦出来
通过在定义 path 中使用 /messages/:id
替换 messages/:id
在多层前套路由中使用绝对路径的能力让我们对 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)
进入和离开 Hook
Route 可以定义 onEnter
和 onLeave
两个 hook,这些 hook 会在页面跳转确认时触发一次。这些 hook 对于一些情况非常的有用,例如权限验证或者在路由跳转前将一些数据持久化保存起来。
在路由跳转过程中
onLeave
会在所有将离开的路由中触发,从最下层的路由开始直到最外层路由结束。
替换配置方式
如果你不想使用 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
嵌套关系
当一个给定的 URL 被调用时,整个集合中(命中的部分)都会被渲染。嵌套路由被描述成一种树形结构
路径语法
匹配一个(或一部分)URL的一个字符串模式。大部分的路由路径都可以直接按照字面量理解,除了下边的特殊符号
:paramName
- 匹配一段位于 /
、?
或#
之后的URL
*
- 匹配任意字符直到命中下一个字符或者整个 URL 的末尾
优先级
路由算法会根据定义的顺序自顶向下匹配路由
Histories
React Router是建立在 history 上的。简而言之,一个 history 知道如何去监听浏览器地址栏的变化,并解析这个 URL 转化为 location 对象,然后 router 使用它匹配到路由,最后正确的渲染页面。
使用
复制 import { browserHistory } from 'react-router'
render(
<Router history={browserHistory} routes={routes} />,
document.getElementById('app')
)
browserHistory
他使用浏览器中 History API 用于处理 URL
服务器配置
服务器需要做好处理 URL 的准备。处理应用启动最初的/这样的请求是没问题的,但当用户来回跳转并在/account/123
刷新时,服务器就会收到来自 account/123
的请求,这时需要处理这个 URL 并响应对应的 JavaScript 代码。
hashHistory
Hash History 使用 URL 中的 hash #
部分去创建形如example.com/#/some/path
的路由。
对比
Hash History 不需要服务器任何配置就可以运行,但是我们不推荐在实际线上环境中使用,因为每一个 web 应用都应该渴望使用 browserHistory。
传递参数
当一个 History 通过应用程序中的 push 或 replace跳转时,他可以在新的 location中存储 location state而不显示在 URL 中,这就像是在一个 HTML 中 post 的表单数据。
createMemoryHistory
Memory History 不会在地址栏被操作或读取。同时它也非常适合测试和其他渲染环境
和另外两种 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')
)
默认路由 (IndexRoute)
与 IndexLink
默认路由 (IndexRoute)
复制 <Router>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>
Index Links
如果你在这个 app 中使用 <Link to="/">Home</Link>
, 它会一直处于激活状态,因为所有的 URL 的开头都是 /
。 这确实是个问题,因为我们仅仅希望在 Home
被渲染后,激活并链接到它。
如果需要在 Home
路由被渲染后才激活的指向 /
的链接,请使用 <IndexLink to="/">Home</IndexLink>
高级用法
动态路由
React Router 适用于小型网站,也可以支持大型网站。
对于大型应用来说,一个首当其冲的问题就是所需加载的 JavaScript 的大小。程序应当只加载渲染页所需的 JavaScript。
路由是个非常适于做代码分拆的地方:他的责任就是配置好每个 view
React Router 里的路径匹配以及组件加载都是异步完成,不仅允许你延迟加载组件,并且可以延迟加载路由配置。在首次加载包中你只需要有一个路径定义,路由会自动解析剩下的路径。
Route 可以定义 getChildRoutes
,getIndexRoute
和getComponents
这几个函数。他们都是异步执行,并且只有在需要的时候才被调用。称之为“逐渐匹配”。
匹配 URL 并只加载该 URL 对应页面所需的路径配置和组件。
复制 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
钩子,使得组件可以拦截正在发生的跳转,或在离开 route 前提示用户。
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?'
},
})
服务端渲染
服务端渲染与客户端渲染有些许不同
在渲染之前获得数据(用 router 帮你完成这点)
为了迎合这一需求,需要在 <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>
路由切换时,组件生命周期的变化情况
1.当用户打开应用 '/'
页面
2.当用户从 '/'
跳转到 '/invoice/123'
componentWillReceiveProps, componentDidUpdate
App 从 router 中接收到新的 props,所以触发了两个生命周期方法
3.当用户从 /invoice/123
跳转到 /invoice/789
componentWillReceiveProps, componentDidUpdate
componentWillReceiveProps, componentDidUpdate
4.当从 /invoice/789
跳转到 /accounts/123
componentWillReceiveProps, componentDidUpdate
获取数据
我们在 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}/>
}
})