使用 React 開發單網頁應用時,React Router 必不可少。剛開始接觸 React Router 時,跟著文件一步步做,雖然有些概念不太理解,但最終還算是完成了專案。後來閱讀了 你不知道的 React Router 4 這篇文章,意識到先前在專案中的某些用法中的用法不太正確。學習的過程中,走了些彎路。本文算是對我本人的使用經驗的一點梳理與總結,希望能讀者帶來一些啟發。完整的專案地址點選這裡,該專案使用了 ant.design。
1. 動態路由 (Dynamic Routing)
React Router v4 引入了動態路由(Dynamic Routing)的概念。與動態路由相對應的是靜態路由(Static Routing)。如果你使用過 Express 或者 koa 的話,那麼對靜態路由再熟悉不過了。下面的例子中,我們使用了 koa-route,讓路由與相應的 Controller 繫結。
const route = require('koa-route')
app.use(route.get('/article', Controller1))
app.use(route.get('/article/:id', Controller2))
app.use(route.get('/article/:id/edit', Controller3))
複製程式碼
像上面這種形式就是靜態路由。靜態路由的最明顯的特徵是:在程式碼中,把要處理的路由全部羅列出來。在專案開始執行時,我們就知道了所有的路由與 Controller 的對應關係。簡單來說就是路由表示不變的。
React Router 把所有的東西都視為元件,Route 也是元件。假設有下面這樣一個 Route。
<Route path="/article" component={ArticleList} />
複製程式碼
如果這個 Route 還未渲染,當我們開啟 /article 這個連結時,ArticleList 元件就根本不會渲染。只有 Route 渲染了,路由才會生效。與前面的靜態路由相比,現在的路由表是變化的。在執行時,路由可以動態的新增進來。當開啟頁面時,並不一定能知道所有的路由與元件的對應關係。
因為路由是不斷變化的,我們編寫的元件跟以往有很大不同。考慮下面的一個單網頁應用。
我們觀察到整個網站的頁面可以分為兩類:登入頁面與其他的頁面。在 App.js 中,可以先新增兩個 Route。
// App.js
<Switch>
<Route path="/login" exact component={Login} />
<Route path="/" component={PrimaryLayout} />
</Switch>
複製程式碼
路由可以不斷的被新增進來的,所以現在我們無需把所有的路由在 App.js 中羅列出來。在 PrimaryLayout.js 中,再新增所需的路由。
// PrimaryLayout.js
<Switch>
<Route path="/article" exact component={ArticleList} />
<Route path="/article/:id" exact component={ArticleDetail} />
<Route path="/article/:id/edit" exact component={ArticleEdit} />
</Switch>
複製程式碼
2. Route 元件
Route 元件的功能比較單一:當連結符合匹配規則時,渲染元件。注意到在上面的程式碼中,Route 元件巢狀在 Switch 元件中。一個連結符合多個 Route 的匹配規則時,那麼多個元件都會被渲染。如果把 Route 巢狀在 Switch 中, 那麼只會渲染第一個符合規則的路由。
Route 有一個名為 render 的 prop。設定這個 render 函式,那麼就可以在路由中做出複雜的邏輯處理。
<Switch>
<Route path="/login" exact
render={() => auth ? <Redirect to="/product" /> : <Login />
<Route path="/"
render={() => auth ? <PrimaryLayout/> : <Redirect to="/login"/>} />
</Switch>
複製程式碼
變數 auth 為使用者的登入狀態,當使用者已登入時無法直接訪問 login 頁面,未登入時無法訪問之後需要許可權的頁面。對於更為複雜的許可權管理,按照相同的方式編寫 render 函式即可。
3. Router 元件與 history
Router 元件是比較底層的元件。實際開發中,我們通常選用 BrowserRouter 或者 HashRouter。
// index.js
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'))
複製程式碼
BrowserRouter 與 HashRouter 都是對 Router 的封裝,自帶了一個 history 物件。這二者的最大區別在於自身的 history 物件的不同。
import { createBrowserHistory, createHashHistory } from 'history'
const history = createBrowserHistory()
// 或者下面這樣
// const history = createHashHistory()
<Router history={history}>
<App/>
</Router>
複製程式碼
BrowserRouter 與 HashRouter 的 props,例如:basename, getUserConfirmation 等,都可以在建立 history 物件時進行設定。
const history = createBrowserHistory({
basename: '/admin'
})
複製程式碼
4. withRouter
withRouter 是一個高階元件,把 match,location,history 三個物件注入到元件的 props 中。這是一個非常實用的函式,下面以四個小例子闡述它的用法。
- 與 redux 的 connect 配合
在前面我們說過 Route 是元件,路由表是不斷變化的。在專案中使用了 redux 來管理資料,當資料沒有變化時,元件也就不會重新渲染。假設在元件中某個 Route 元件並未被渲染,資料也未發生變化,即便當前頁面的連結發生了變化,也不會有任何的路由匹配該連結。因為這時候 Route 元件還是未被渲染!如何知道連結變化了呢?這時候就需要 withRouter 了。
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Component))
複製程式碼
- 獲取當前的路由
如下圖所示,左側的側邊欄應該根據連結的變化,決定哪一塊展開,哪一塊高亮。通過 withRouter 封裝一下左側元件,元件就可以響應連結的變化了。
class LeftSider extends React.Component {
componentDidMount() {
this.setHighLightKeys(this.props)
}
componentWillReceiveProps(nextProps) {
this.setHighLightKeys(nextProps)
}
shouldComponentUpdate(nextProps, nextState) {
const { match, location } = this.props
if (!(match === nextProps.match && location === nextProps.location)) {
return true
}
return nextState !== this.state
}
}
export default withRouter(LeftSider)
複製程式碼
注意到 shouldComponentUpdate 函式中只是比較了兩次 match 與 location 的是否相同,並未比較 history 物件。history 物件每次都是變化的,故這裡不用作比較。
同理,麵包屑也可以使用這種方式實現。
- 頁面的跳轉
React Router 提供了 Link,NavLink 與 Redirect 元件來控制頁面的跳轉。但是我在一個 Button 的點選事件中控制頁面的跳轉,這幾個元件就無法使用了。這裡,或許你會想到使用 location 物件。
// 錯誤的方式!!!
location.href = '/article'
複製程式碼
這種方式可行,但不正確。如果先前使用的 BrowserRouter 變成 HashRouter 的話,這種方式就失效了。withRouter 封裝的元件中的 props 包含 history,通過 history 物件來控制頁面的跳轉。history 物件有 push,replace 與 go 等方法,呼叫這些方式實現頁面的跳轉。
class Comoponent extends React.Component {
handleClick () {
this.props.history.push('/article')
}
}
export default withRouter(Component)
複製程式碼
- 獲取路由中的引數
在上文的 ArticleDetail 元件中,我們需要知道當前路由中的 id 是多少。 元件 props 的 match 物件裡包含了路由中的引數。
class ArticleDetail extends React.Component {
state = {
id: null
}
componentDidMount () {
const { id } = this.props.match
this.setState({ id })
}
}
複製程式碼
5. 程式碼分離
現在使用 react-loadable 來實現元件的非同步載入,一切變得容易多了。在之前的 React Router 文件中是按照下面這種方式實現元件的非同步載入的。
// 一種比較繁瑣的方式
import Component from 'bundle-loader!./Component'
// 為此還要編寫一個元件
class Bundle extends React.Component {
state = {
// short for "module" but that's a keyword in js, so "mod"
mod: null
}
componentWillMount () {
this.load(this.props)
}
componentWillReceiveProps (nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps)
}
}
load (props) {
this.setState({
mod: null
})
props.load((mod) => {
this.setState({
// handle both es imports and cjs
mod: mod.default ? mod.default : mod
})
})
}
render () {
return this.state.mod ? this.props.children(this.state.mod) : null
}
}
// 載入非同步元件
<Bundle load={Component}>
{(Container) => <Container {...props}/>}
</Bundle>
複製程式碼
如果使用 react-loadable,短短几行程式碼就完成了。
import Loadable from 'react-loadable'
const Loading = () => <Spin />
const LogIn = Loadable({
loader: () => import('../components/Login'),
loading: Loading
})
複製程式碼
更進一步,通過命名 chunk 來給這些拆分之後的檔案起名或者把非同步元件按組分塊。
const LogIn = Loadable({
loader: () => import(/* webpackChunkName: "Login" */'../components/Login'),
loading: Loading
})
複製程式碼
6. 總結
本文對 React Router 的重要知識點做了梳理,結合自己的開發經驗談了一下 React Router 的時需要注意問題。由於本文中的許多程式碼只是片段,僅僅為了闡述概念,細節可以參看這個專案的原始碼。