开始
无论是使用Vue,还是React,开发的单页应用程序,可能只是该站点的一部分(某一个功能块)
一个单页应用里,可能会划分为多个页面(几乎完全不同的页面效果)(组件)
React Router
如果要在单页应用中完成组件的切换,需要实现下面两个功能:
- 根据不同的页面地址,展示不同的组件(核心)
- 完成无刷新的地址切换
我们把实现了以上两个功能的插件,称之为路由
- react-router:路由核心库,包含诸多和路由功能相关的核心代码
- react-router-dom:利用路由核心库,结合实际的页面,实现跟页面路由密切相关的功能
如果是在页面中实现路由,需要安装react-router-dom库
路由组件
Router组件
它本身不做任何展示,仅提供路由模式配置,另外,该组件会产生一个上下文,上下文中会提供一些实用的对象和方法,供其他相关组件使用
- HashRouter:该组件,使用hash模式匹配
- BrowserRouter:该组件,使用BrowserHistory模式匹配
通常情况下,Router组件只有一个,将该组件包裹整个页面
Route组件
根据不同的地址,展示不同的组件
重要属性:
-
path:匹配的路径
- 默认情况下,不区分大小写,可以设置sensitive属性为true,来区分大小写
- 默认情况下,只匹配初始目录,如果要精确匹配,配置exact属性为true
- 如果不写path,则会匹配任意路径
-
component:匹配成功后要显示的组件
-
children:
- 传递React元素,无论是否匹配,一定会显示children,并且会忽略component属性
- 传递一个函数,该函数有多个参数,这些参数来自于上下文,该函数返回react元素,则一定会显示返回的元素,并且忽略component属性
Route组件可以写到任意的地方,只要保证它是Router组件的后代元素
Switch组件
写到Switch组件中的Route组件,当匹配到第一个Route后,会立即停止匹配
由于Switch组件会循环所有子元素,然后让每个子元素去完成匹配,若匹配到,则渲染对应的组件,然后停止循环。因此,不能在Switch的子元素中使用除Route外的其他组件。
Link
生成一个无刷新跳转的a元素
-
to
-
字符串:跳转的目标地址
-
对象:
- pathname:url路径
- search
- hash
- state:附加的状态信息
-
-
replace:bool,表示是否是替换当前地址,默认是false
-
innerRef:可以将内部的a元素的ref附着在传递的对象或函数参数上
- 函数
- ref对象
NavLink
是一种特殊的Link,Link组件具备的功能,它都有
它具备的额外功能是:根据当前地址和链接地址,来决定该链接的样式
- activeClassName: 匹配时使用的类名
- activeStyle: 匹配时使用的内联样式
- exact: 是否精确匹配
- sensitive:匹配时是否区分大小写
- strict:是否严格匹配最后一个斜杠
Redirect
重定向组件,当加载到该组件时,会自动跳转(无刷新)到另外一个地址
-
to:跳转的地址
- 字符串
- 对象
-
push: 默认为false,表示跳转使用替换的方式,设置为true后,则使用push的方式跳转
-
from:当匹配到from地址规则时才进行跳转
-
exact: 是否精确匹配from
-
sensitive:from匹配时是否区分大小写
-
strict:from是否严格匹配最后一个斜杠
基础
一、路由配置
1.Route
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
| URL | 组件 |
|---|---|
/ | App |
/about | App -> About |
/inbox | App -> Inbox |
/inbox/messages/:id | App -> Inbox -> Message |
2. 默认路由IndexRoute组件
这样写当我们的路由地址是/的时候APP没有子元素,我们可以这样做
<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>
添加 <IndexRoute component={Dashboard} /> 当路由是/的时候默认匹配到IndexRoute的component组件
3.UI 从 URL 中解耦
如果我们想 通过路由/messages/2 匹配 /inbox/messages/:id 我们可以这样配置
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
{/* 使用 /messages/:id 替换 messages/:id */}
<Route path="/messages/:id" component={Message} />
</Route>
</Route>
</Router>
| URL | 组件 |
|---|---|
/messages/:id | App -> Inbox -> Message |
在messages/:id 前面加个 / 改成 /messages/:id 这样可以吧Message组件渲染到APP-Inbox下面 又可以通过 /messages/:id直接访问到, 原来需要/inbox/messages/:id
4.路由重定向Redirect组件
这样有个问题
当我们访问原来的/inbox/messages/:id 时就匹配不到了,我们可以这样做
<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>
添加一个<Redirect from="messages/:id" to="/messages/:id" />当访问路由/inbox/messages/:id的时候定位到/messages/:id
5.另外还有两个钩子 onEnter 和 onLeave
这些 hook 对于一些情况非常的有用,权限验证或者在路由跳转前将一些数据持久化保存起来
onEnter进入触发
onEnter离开出发
继续我们上面的例子,如果一个用户点击链接,从 /messages/5 跳转到 /about,下面是这些 hook 的执行顺序:
/messages/:id的onLeave/inbox的onLeave/about的onEnter
5.我们也可以使用vue-router的方式配置
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地址组成
例:www.react.com:443/news/1-2-1.…
-
协议名(schema):https
-
主机名(host):www.react.com
- ip地址
- 预设值:localhost
- 域名
- 局域网中电脑名称
-
端口号(port):443
- 如果协议是http,端口号是80,则可以省略端口号
- 如果协议是https,端口号是443,则可以省略端口号
-
路径(path):/news/1-2-1.html
-
地址参数(search、query):?a=1&b=2
- 附带的数据
- 格式:属性名=属性值&属性名=属性值....
-
哈希(hash、锚点)
- 附带的数据
路由拥有三个属性来决定是否“匹配“一个 URL:
- 附带的数据
路由拥有三个属性来决定是否“匹配“一个 URL:
- 1.嵌套关系
- 2.路径法则
- 3.优先级
1.嵌套关系
定义Route和html页面一样div嵌套div每个元素上有个path属性 根据url匹配path 如果匹配成功就显示对应的component,react-router内部使用的是proper-url-join插件进行判断的, 感兴趣的可以了解一下
2.路径语法
-
:name: 动态匹配
-
(name): 可选的 也可以(:name)动态可选
-
※: 匹配任意字符
-
- 正则约定 例子:
<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
如果使用相对路径 那么url是祖先的path和自己的path拼接而成, 如果使用绝对路径可以忽略嵌套关系。
3.优先级
从外到内,从浅到深
你必须确认前一个路由不会匹配后一个路由中的路径。
4.正则约定
三、Histories
React Router建立在history之上。 history知道如何监听地址栏的变化,并解析这个url中的地址,然后router使用它去匹配路由,最后把组件渲染到页面。
1. 常用的三种history
-
- browserHistory
-
- hashHistory
-
- createMemoryHistory 使用es6的方式引入
import { browserHistory } from 'react-router'
在<Router>中的history属性中使用他们
<Router history={browserHistory} routes={routes}
2. browserHistory
使用浏览器History的方式匹配路由, HTML5出现后,新增了History Api,从此以后,浏览器拥有了改变路径而不刷新页面的方式
History表示浏览器的历史记录,它使用栈的方式存储。
-
history.length:获取栈中数据量
-
history.pushState:向当前历史记录栈中加入一条新的记录
- 参数1:附加的数据,自定义的数据,可以是任何类型
- 参数2:页面标题,目前大部分浏览器不支持
- 参数3:新的地址
-
history.replaceState:将当前指针指向的历史记录,替换为某个记录
- 参数1:附加的数据,自定义的数据,可以是任何类型
- 参数2:页面标题,目前大部分浏览器不支持
- 参数3:新的地址
example.com/some/path这样真实的 URL 。 但是刷新页面会丢失需要服务器配合使用
// 在你应用 JavaScript 文件中包含了一个 script 标签
// 的 index.html 中处理任何一个 route
app.get('*', function (request, response){
response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})
3.hashHistory
使用 URL 中的 hash(#)部分去创建形如 example.com/#/some/path 的路由。
我们刚开始学习可以使用这种方式, 上线最好使用browserHistory,因为它更像真实的路由
3.createMemoryHistory
Memory history 不会在地址栏被操作或读取,通常用于服务器渲染,测试。另外和上面两种不同的是我们需要创建他
const history = createMemoryHistory(location)
history的实例应用。
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')
4.路由信息
history
它并不是window.history对象,我们利用该对象无刷新跳转地址
为什么没有直接使用history对象
- React-Router中有两种模式:Hash、History,如果直接使用window.history,只能支持一种模式
- 当使用windows.history.pushState方法时,没有办法收到任何通知,将导致React无法知晓地址发生了变化,结果导致无法重新渲染组件
-
push:将某个新的地址入栈(历史记录栈)
- 参数1:新的地址
- 参数2:可选,附带的状态数据
-
replace:将某个新的地址替换掉当前栈中的地址
-
go: 与window.history一致
-
forward: 与window.history一致
-
back: 与window.history一致
location
与history.location完全一致,是同一个对象,但是,与window.location不同
location对象中记录了当前地址的相关信息
我们通常使用第三方库query-string,用于解析地址栏中的数据.
match
该对象中保存了,路由匹配的相关信息
- isExact:事实上,当前的路径和路由配置的路径是否是精确匹配的
- params:获取路径规则中对应的数据
实际上,在书写Route组件的path属性时,可以书写一个string pattern(字符串正则)
react-router使用了第三方库:Path-to-RegExp,该库的作用是,将一个字符串正则转换成一个真正的正则表达式。
向某个页面传递数据的方式:
- 使用state:在push页面时,加入state
- 利用search:把数据填写到地址栏中的?后
- 利用hash:把数据填写到hash后
- params:把数据填写到路径中
非路由组件获取路由信息
某些组件,并没有直接放到Route中,而是嵌套在其他普通组件中,因此,它的props中没有路由信息,如果这些组件需要获取到路由信息,可以使用下面两种方式:
- 将路由信息从父组件一层一层传递到子组件
- 使用react-router提供的高阶组件withRouter,包装要使用的组件,该高阶组件会返回一个新组件,新组件将向提供的组件注入路由信息。
高级
1. 动态路由
在做大型应用中我们为了优化项目通常吧大文件才分成小文件,避免冲突, 并且对文件配置按需加载,以拆分代码的方式优化项目。 于react代码分割配合使用。
另外一种情况比如编制页面,我们需要通过id进入对应的编制页面, 还有分享出去的链接/user/3当使用浏览器打卡地址时希望看到id为3的详情页。
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'))
})
}
}
2. 跳转前确认
提供一个 routerWillLeave 生命周期钩子,这使得 React 组件可以拦截正在发生的跳转,或在离开 route 前提示用户。routerWillLeave 返回值有以下两种:
return false取消此次跳转return返回提示信息,在离开 route 前提示用户进行确认。
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?'
},
// ...
})
3.服务端渲染
服务端渲染与客户端渲染有些许不同,因为你需要:
- 发生错误时发送一个
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 中添加数据加载的方法——由你决定。
4. 组件生命周期
在开发应用时,理解路由组件的生命周期是非常重要的。 后面我们会以获取数据这个最常见的场景为例,介绍一下路由改变时,路由组件生命周期的变化情况。
路由组件的生命周期和 React 组件相比并没有什么不同。 所以让我们先忽略路由部分,只考虑在不同 URL 下,这些组件是如何被渲染的。
路由配置如下:
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="invoices/:invoiceId" component={Invoice}/>
<Route path="accounts/:accountId" component={Account}/>
</Route>
路由切换时,组件生命周期的变化情况
1. 当用户打开应用的 '/' 页面
| 组件 | 生命周期 |
|---|---|
| App | componentDidMount |
| Home | componentDidMount |
| Invoice | N/A |
| Account | N/A |
2. 当用户从 '/' 跳转到 '/invoice/123'
| 组件 | 生命周期 |
|---|---|
| App | componentWillReceiveProps, componentDidUpdate |
| Home | componentWillUnmount |
| Invoice | componentDidMount |
| Account | N/A |
App从 router 中接收到新的 props(例如children、params、location等数据), 所以App触发了componentWillReceiveProps和componentDidUpdate两个生命周期方法Home不再被渲染,所以它将被移除Invoice首次被挂载
3. 当用户从 /invoice/123 跳转到 /invoice/789
| 组件 | 生命周期 |
|---|---|
| App | componentWillReceiveProps, componentDidUpdate |
| Home | N/A |
| Invoice | componentWillReceiveProps, componentDidUpdate |
| Account | N/A |
所有的组件之前都已经被挂载, 所以只是从 router 更新了 props.
** 4. 当从 /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}/>
}
})
5.导航守卫
导航守卫:当离开一个页面,进入另一个页面时,触发的事件
1.route属性: onEnter进入触发 onLeave离开出发
2.history对象
返回卸载监听回调
history.listen((location,action) =>{
if(this.props.onChange){
const prevLocation = this.props.location;
this.props.onChange(location,action,prevLocation)
}
})
-
listen: 添加一个监听器,监听地址的变化,当地址发生变化时,会调用传递的函数
-
参数:函数,运行时间点:发生在即将跳转到新页面时
-
参数1:location对象,记录当前的地址信息
-
参数2:action,一个字符串,表示进入该地址的方式
-
POP:出栈
- 通过点击浏览器后退、前进
- 调用history.go
- 调用history.goBack
- 调用history.goForward
-
PUSH:入栈
- history.push
-
REPLACE:替换
- history.replace
-
-
-
返回结果:函数,可以调用该函数取消监听
-
-
block:设置一个阻塞,并同时设置阻塞消息,当页面发生跳转时,会进入阻塞,并将阻塞消息传递到路由根组件的getUserConfirmation方法。
- 返回一个回调函数,用于取消阻塞器
3.路由根组件
getUserConfirmation
-
参数:函数
-
参数1:阻塞消息
-
字符串消息
-
函数,函数的返回结果是一个字符串,用于表示阻塞消息
- 参数1:location对象
- 参数2:action值
-
-
参数2:回调函数,调用该函数并传递true,则表示进入到新页面,否则,不做任何操作
-
另外还有一篇详细的react-router-dom