React Router 使用總結

使用 React 開發單網頁應用時,React Router 必不可少。剛開始接觸 React Router 時,跟着文檔一步步作,雖然有些概念不太理解,但最終還算是完成了項目。後來閱讀了 你不知道的 React Router 4 這篇文章,意識到先前在項目中的某些用法中的用法不太正確。學習的過程當中,走了些彎路。本文算是對我本人的使用經驗的一點梳理與總結,但願能讀者帶來一些啓發。完整的項目地址點擊這裏,該項目使用了 ant.designhtml

1. 動態路由 (Dynamic Routing)

React Router v4 引入了動態路由(Dynamic Routing)的概念。與動態路由相對應的是靜態路由(Static Routing)。若是你使用過 Express 或者 koa 的話,那麼對靜態路由再熟悉不過了。下面的例子中,咱們使用了 koa-route,讓路由與相應的 Controller 綁定。react

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 的對應關係。簡單來講就是路由表示不變的。webpack

React Router 把全部的東西都視爲組件,Route 也是組件。假設有下面這樣一個 Route。git

<Route path="/article" component={ArticleList} />
複製代碼

若是這個 Route 還未渲染,當咱們打開 /article 這個連接時,ArticleList 組件就根本不會渲染。只有 Route 渲染了,路由纔會生效。與前面的靜態路由相比,如今的路由表是變化的。在運行時,路由能夠動態的添加進來。當打開頁面時,並不必定能知道全部的路由與組件的對應關係。github

由於路由是不斷變化的,咱們編寫的組件跟以往有很大不一樣。考慮下面的一個單網頁應用。web

登陸頁面

其餘頁面

咱們觀察到整個網站的頁面能夠分爲兩類:登陸頁面與其餘的頁面。在 App.js 中,能夠先添加兩個 Route。npm

// App.js
<Switch>
  <Route path="/login" exact component={Login} /> <Route path="/" component={PrimaryLayout} /> </Switch> 複製代碼

路由能夠不斷的被添加進來的,因此如今咱們無需把全部的路由在 App.js 中羅列出來。在 PrimaryLayout.js 中,再添加所需的路由。redux

// 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 中, 那麼只會渲染第一個符合規則的路由。api

Route 有一個名爲 render 的 prop。設置這個 render 函數,那麼就能夠在路由中作出複雜的邏輯處理。react-router

<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 中。這是一個很是實用的函數,下面以四個小例子闡述它的用法。

  1. 與 redux 的 connect 配合

在前面咱們說過 Route 是組件,路由表是不斷變化的。在項目中使用了 redux 來管理數據,當數據沒有變化時,組件也就不會從新渲染。假設在組件中某個 Route 組件並未被渲染,數據也未發生變化,即使當前頁面的連接發生了變化,也不會有任何的路由匹配該連接。由於這時候 Route 組件仍是未被渲染!如何知道連接變化了呢?這時候就須要 withRouter 了。

import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Component))
複製代碼
  1. 獲取當前的路由

以下圖所示,左側的側邊欄應該根據連接的變化,決定哪一塊展開,哪一塊高亮。經過 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 對象每次都是變化的,故這裏不用做比較。

同理,麪包屑也可使用這種方式實現。

  1. 頁面的跳轉

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)
複製代碼
  1. 獲取路由中的參數

在上文的 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 的時須要注意問題。因爲本文中的許多代碼只是片斷,僅僅爲了闡述概念,細節能夠參看這個項目的源碼

參考資料
相關文章
相關標籤/搜索