安装
npm install history react-router@latest
`</pre>
react-router 依赖 history 模块
<pre>`import {Router, Route, Link} from 'react-router'
可以从 lib 目录 require 需要的部分
`</pre>
import { Router } from 'react-router/lib/Router'
<pre>`import React from 'react'
import { Router, Route, Link } from 'react-router'
const App = React.createClass({/*..*/})
const About = React.createClass({/*...*/})
const Users = React.createClass({
render(){
return(
<div>
<h1>Users</h1>
<div className = "master">
<ul>
{/* 在本应用中用 Link 去链接路由*/}
{this.state.users.map(user => (
<li key={user.id}><Link to={`/user/${user.id}`}>{user.name}</Link></li>
))}
</ul>
</div>
<div className ="detail">
{this.props.children}
</div>
</div>
)
}
})
const User = React.createClass({
componentDidMount(){
this.setState({
// 路由应该通过有用的信息来呈现, 比如 URL 的参数
user: findUserById(this.props.params.userId)
})
},
render(){
return(
<div>
<h2>{this.state.user.name}</h2>
{/* 等等 */}
</div>
)
}
})
/**
* 路由配置说明(不用加载整个配置, 只需要加载一个想要的跟路由, 也可以延迟加载这个配置)
*/
React.render(
(<Router>
<Route path = "/" component = {App}>
<Route path = "about" component = {About}/>
<Route path = "users" component = {Users}>
<Route path = "/user/:userId" component = {User}/>
</Route>
<Route path = "*" component = {NoMatch}/>
</Route>
</Router>),
document.body
)
`</pre>
<pre>`import React from 'react'
import { render } from 'react-dom'
import { Router, Route, Link } from 'react-router'
const App = React.createClass({
render(){
return (
<div>
<h1>App</h1>
{/* 把 <a> 变成 <Link> */}
<ul>
<li><Link to="/about"></Link></li>
<li><Link to="/inbox"></Link></li>
</ul>
{/*
接着用` this.props.children` 替换`<Child>`, this.props.children 就是 App 的子组件 Inbox 和 About
*/}
{this.props.children}
</div>
)
}
})
React.render((
<Router>
<Route path = "/" component = {App}>
<Route path="about" component = {About}
<Route path="inbox" component = {Inbox}
</Route>
</Router>
))
`</pre>
<pre>`const Message = React.createClass({
render(){
return(
<h3>Message</h3>
)
}
})
const Inbox = React.createClass({
render(){
return(
<div>
<h2>Inbox</h2>
{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}>
<Route path="message/:id" component={Message}/>
</Route>
</Route>
</Router>
),document.body)
`</pre>
访问 URL = inbox/message/Jkei 会匹配一个新路由, 其指向 App => Inbox => Message
<pre>`<App>
<Inbox>
<Message params = {id: "Jkei"} />
</Inbox>
</App>
`</pre>
### 获取 URL 参数
为了从服务器获取 message 数据, 我们首先需要知道他的信息, 当渲染组件的时候, React Router 自动向 Route 组件注入一些有用的信息, 尤其是路径中动态部分的参数, 比如上例中的`:id`
<pre>`const Message = React.createClass({
componentDidMount(){
const id = this.props.params.id
fetchMessgae(id,function(err,message){
this.setState({message:message})
})
}
})
`</pre>
### 路由配置
路由配置是一组指令, 用来告诉 router 如何匹配 URL, 以及匹配后的动作.
<pre>`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 = "message/:id" component = {Message} />
</Route>
</Route>
</Router>
),document.body)
`</pre>
### 添加首页
当 URL 为 "/" 的时候, 渲染的 App 组件的 render 中的 this.props.children 还是 undefined, 这种情况我们可以使用`IndexRoute`来设置一个默认页
<pre>`import {IndexRoute} from 'react-router'
const Dashboard = React.createClass({
render(){
return <div>Welcome to the App!</div>
}
})
React.render((
<Router>
{/* 当 URL 为 / 的时候渲染 Dashboard*/}
<IndexRoute component = {Dashboard} />
<Route path="about" component = {About} />
</Router>
),document.body)
`</pre>
现在的 Sitemap 如下:
<table>
<thead>
<tr>
<th>URL</th>
<th>Component</th>
</tr>
</thead>
<tbody>
<tr>
<td>/</td>
<td>App => Dashboard</td>
</tr>
<tr>
<td>/about</td>
<td>App => About</td>
</tr>
</tbody>
</table>
### 将 UI 于 URL 解耦
如果我们可以将 `/inbox` 从 `/inbox/message:id` 中去除, 并且还能够让 Message 嵌套在 App->Inbox 中渲染, 那会非常赞, 绝对路径可以做到这一点
<pre>`React.render((
<Router>
<Route path = "/" component = {App}>
<IndexRoute component = {Dashboard} />
<Route path = "inbox" component = {Inbox} >
<Route path = "/message:id" component = {Message} />
</Route>
</Route>
</Router>
),document.body)
`</pre>
在多层嵌套路由中使用感觉对路径可以提高逻辑性.
Simtemap 如下:
<table>
<thead>
<tr>
<th>URL</th>
<th>Component</th>
</tr>
</thead>
<tbody>
<tr>
<td>/</td>
<td>App => Dashboard</td>
</tr>
<tr>
<td>/message/:id</td>
<td>App => Inbox => Message</td>
</tr>
</tbody>
</table>
提醒, 绝对路径可能在动态路由中无法使用
同时, 上述配置存在一个问题, 当用户访问/inbox/message/6的时候回无法访问
使用<Redirect>修正
<pre>`<Route path = "indox" component = {Indox} >
<Route path = "/message/:id" component = {Message} />
<Redirect from = "message/:id" to = "/message/:id" />
</Route>
`</pre>
### 进入和离开的 Hook
Route 可以定义 `onEnter` 和 `onLeave` 两个 Hook, 这些 hook 会在页面跳转确认时触发一次,
在路由跳转过程中, `onLeave` hook 会在爱所有将离开的路由中触发, 从最下层的自路由开始直到最外层的父路由结束, 而`onEnter` hook 则从自外层的父路由开始直到最下层的子路由结束
比如从 /message/5 -> /about 依次触发
- /message/:id 的 onLeave
- /inbox 的 onLeave
- /about 的 onEnter
### 路由匹配原理
路由拥有三个属性来决定是否匹配一个 URL
- 嵌套关系
- 路径语法
- 优先级
#### 嵌套关系
当一个给定的 URL 被调用时, 整个集合(命中的部分)都会被渲染, 嵌套路由被描述成一种树形结构, React Router 会深度优化遍历整个路由配置来寻找一个与给定的 URL 匹配的路由
#### 路径语法
路由路径是匹配一个(或一部分) URL 的一个字符串模式, 大部分的路由路径都可以按照字面量理解, 除了一下几个特殊字符:
- `:paramName` - 匹配一段位于`/`,`?`, `#`之后的 URL, 命中的部分作为一个参数
- `()` - 内部的内容被认为是可选的
- `*` - 匹配任意字符(非贪婪)直到命中下一个字符或整个 URL 的末尾, 并创建一个 splat 参数
<pre>`<Route path = '/hello/:name' /> // 匹配 /hello/michael 和 /hello/john
<Route path = '/hello/(:name)'/> // 匹配 /hello, /hello/michael, /hello/john
<Route path = '/files/*.*' /> // 匹配 files/hello.js, files/hello.jpg
`</pre>
使用绝对路径可以使路由忽略嵌套关系
#### 优先级
兄弟路由的前一个优先级高
### Histories
React Router 是建立在 history 之上的, 一个 history 知道如何去监听浏览器地址变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由. 最后正确渲染组件
常用的 history 有三种形式
- createHashHistory
- createBrowserHistory
- createMemoryHistory
从 history 库中获取他们
<pre>`import createBrowserHistory from 'history/lib/createBrowserHistory'
`</pre>
#### createHashHistory
这是一个你会获得的默认 history, 如果不指定某个 history (即<Router>{...}</Router>) 他用到的是 URL 中的 hash(#) 部分去创建形如 example.com/#/some/path 的路由
HashHistory 是默认的, 可以在服务器中不作任何配置就可以运行, 并且在全部浏览器中都可以使用, 但是不推荐实际生产中使用它, 因为每一个 web 应用都应该有目的地去使用 createBrowserHistory
#### 像`?_k=ckuvup` 没用的在 URL 中是什么?
当一个 history 通过应用程序的`pushState`或`replaceState`跳转时, 他可以在新的 location 中存储"lcoation state" 而不现实在 URL 中, 就像在一个 HTML 中 post 的表单数据
在 DOM API 中, 这些 hash history 通过 window.location.hash = newHash 很简单地被用于, 且不用存储他们的 location state, 但是我们希望全部的 history 都能够使用 location state, 因此要为每个 location 添加一个唯一的 key, 并把他们的状态存储在 session storage 中, 当访客点击前进或后退的时候, 可以恢复这些 location state.
#### createBrowserHistory
这是 react-router 创建浏览器应用推荐的 History, 使用 History API 在浏览器中被创建用于处理 URL, 新建一个像这样真实的 URL: example.com/some/path
##### 服务器配置
首先服务器应该能够处理 URL 请求, 处理应用启动最初的`/`这样的请求应该没问题,但是当用户来回跳转并在`/ accounts/23`刷新时, 服务器会受到来自`/account/23`的请求, 这时你就需要处理这个 URL 并在响应中包含 JS 程序代码
一个 express 的应用看起来可能是这样的:
<pre>`const express = require('express')
const path = require('path')
const port = process.env.PORT || 8080
const app = express()
// 通常用于加载静态资源
app.use(express.static(__dirname + '/public'))
// 在你应用 JS 文件中包含一个 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)
`</pre>
当服务器找不到其他文件的时候, 服务器生成静态文件和操作 index.html 文件.
### 实例展示
<pre>`import React from 'react'
import createBrowserHistory from 'history/createBrowserHistory'
import { Router, Route, IndexRoute } from 'react-router'
import App from '../componnents/App'
import Home from '../componnents/Home'
import About from '../componnents/About'
import Features from '../componnents/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.body
)
`</pre>
### 默认路由(IndexRoute)与 IndexLink
如果设置了 IndexRoute => Home, 就要设置跳转到 Home 的路由'/', 即`<Link to='/'>Home</Link>`, 他会一直处于激活状态(因为所有子路由都经过'/'), 我们仅希望在 Home 被渲染后激活并连接到他, 即需要在 Home 路由被渲染后才激活指向'/' 的链接, 请使用`<IndexLink to='/'>Home</IndexLink>`
### 动态路由
React Router 适用于小型网站, 也可以支持大型网站
对于大型应用而言, 一个首当其冲的问题就是所需要加载的 JS 的大小, 程序应当只加载当前渲染页需要的的 JS, 有些开发者称之为"代码拆分", 在用户浏览过程中按需加载
对于底层细节的修改不应该需要他上面每一层级都进行修改, 举个例子, 为一个照片浏览页添加一个路径不应该影响到首页加载的 JS 大小, 也不能因为多个团队共用一个大型路由配置文件二造成合并时的冲突.
路由是个非常适合做代码拆分的地方: 他的责任就是配置好每个 view
React Routerzhogn 的路径匹配及组件加载都是异步完成的, 不仅允许你延迟加载组件, **并且可以延迟加载路由配置**. 在首次加载包中你只需要有一个路径定义, 路由会自动解析剩下的路径
Route 可以定义`getChildRoutes`, `getIndexRoute`, 和`getComponents` 这几个函数, 他们都是异步执行的, 并且只有需要时被调用, 这种方式成为"逐渐匹配"
React Router 会逐渐地匹配 URL 并且只加载该 URL 对应页面所需要路径配置和组件
如果配合 webpack 代码拆分工具使用的话, 一个原本繁琐的架构就会变得简明
<pre>`const CourseRoute = {
path: 'course/:courseId',
/* 当 get routes/indexRoute/Component 的时候 require 相应的资源
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, requrie('./components/Course'))
})
}
}
`</pre>
### 跳转前确认
React Router 提供一个`routerWillLeave` 生命周期钩子, 这使得 React 可以拦截正在发生的跳转, 或者在离开 Route 前提示用户
`routerWillLeave` 返回值有一下两种:
1\. `return false` 取消此次跳转
2\. `return` 返回提示信息, 在离开 route 前提示用户进行确认
在 Route 组件中引入`Lifecycle` mixin 来安装这个钩子
<pre>`import { Lifecycle } from 'react-router'
const Home = React.createClass({
mixins: [Lifecycle],
routerWillLeave(nextLocation){
if(!this.state.isSaved){
return 'Your work is not saved!'
}
}
})
`</pre>
### 服务端渲染
服务端渲染与客户端渲染有些许不同, 需要
- 发生错误的时候发送一个`500`响应
- 需要重定向时发送一个`30x`响应
- 需要在渲染之前获得数据(用 router 完成)
### 组件生命周期
路由配置如下:
<pre>`<Route path = '/' component = {App} >
<IndexRoute component = {Home} />
<Route path = 'invoices/:invoiceId' component = {Invoice}/>
<Route path = 'accounts/:accountId' component = {Account}/>
</Route>
`</pre>
#### 路由切换时, 组件生命周期的变化
当用户打开’/‘ 页面
Component LifeCycle App componentDidMount Home componentDidMount Invoice N/A Account N/A 当用户从’/‘ 跳转到 ‘/invoices/123’
Component LifeCycle App componentWillReceiveProps, componentDidUpdate Home componentWillUnmount Invoice componentDidMount Account N/A 获取数据
最简单的通过 router 获取数据的方法是通过组件生命周期 Hook 来实现.
在 Invoice 中添加一个简单的数据获取功能
`let Invoice = React.createClass({ getInitialState(){ return{ invoice: null } }, componentDidMount(){ this.setState({ this.fetchInvoice() }) }, componentDidUpdate(prevProps){ 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.ingoreFetch){ this.setState({invoice: data.invoice}) } }) }, render(){ return <InvoiceView invoice = {this.state.invoice} /> } })