項目實踐:從react-router v3遷移到v4

原文:https://github.com/YutHelloWo...html

前言

今年3月初發布了react-router v4,相較以前的v3和v2版本作了一個破壞性的升級。遵循一切皆React Component的理念。靜態路由變成了動態路由。這裏記錄下v3項目如何遷移到v4。
項目地址:https://github.com/YutHelloWo...node

遷移步驟

  • 對React-Router和Redux同步進行重構react

  • 重寫路由git

  • 代碼分割github

  • 瑣碎的API替換web

詳細代碼參閱這個PRjson


React-Router和Redux同步

這裏咱們仍然不使用react-router-redux這個庫。爲了和react-routerv4版本保持一致,react-router-redux發佈了v5.0.0版本,你固然也可使用它來實現這個功能。redux

1. 替換依賴包

v3咱們引入的是react-router包,在v4咱們只引入react-router-dom這個包。安裝react-router-dom時會同時安裝history瀏覽器

package.jsonreact-router

-    "react-router": "^3.0.0",
+    "react-router-dom": "^4.1.2",

2. 改寫對browserHistory的建立和當前location的獲取

location.js

// v3
import { browserHistory } from 'react-router'

// 獲取當前location
const initialState = browserHistory.getCurrentLocation()

==>

// v4
import createHistory from 'history/createBrowserHistory'

export const history = createHistory()

// Get the current location.
const initialState = history.location

這裏替換的是history,和當前location的獲取方法。在v3,browserHistory存在於react-router中,而v4把history抽離了出來,提供了createBrowserHistory ,createHashHistory ,createMemoryHistory 三種建立history的方法。v4中建立的history導出,在後面會須要用到。

history API詳見: https://github.com/ReactTrain...

3. 對history綁定監聽事件,把location的改變同步到Redux的store中

createStore

// v3
import { browserHistory } from 'react-router'
import { updateLocation } from './location'

store.unsubscribeHistory = browserHistory.listen(updateLocation(store))

updateLocation用來把location的更新同步到store中。

export const updateLocation = ({ dispatch }) => {
  return (nextLocation) => dispatch(locationChange(nextLocation))
}

一切彷佛都很順利,接着第一個坑來了

根據historyAPI提供的

// Listen for changes to the current location.
const unlisten = history.listen((location, action) => {
  // location is an object like window.location
  console.log(action, location.pathname, location.state)
})

修改createStore.js

==>

// v4
import { updateLocation, history } from './location'

// 監聽瀏覽器history變化,綁定到store。取消監聽直接調用store.unsubscribeHistory()
store.unsubscribeHistory = history.listen(updateLocation(store))

接着修改app.js

// v3
// ...
import { browserHistory, Router } from 'react-router'

// ...
<Router history={browserHistory} children={routes} />

==>

// ...
import {  BrowserRouter, Route } from 'react-router-dom'

// ...
<BrowserRouter>
  <div>
    <Route path='/' component={CoreLayout} />
  </div>
</BrowserRouter>
//...

咱們到瀏覽器中查看,發現URL變化並無觸發updateLocation(store),state並無變化。

What a f**k!
問題出在BrowserRouter在建立的時候在內部已經引入了一個historyupdateLocation(store)應該監聽的是內部的這個history。這裏貼下BrowserRouter.js的代碼

import React from 'react'
import PropTypes from 'prop-types'
import createHistory from 'history/createBrowserHistory'
import { Router } from 'react-router'

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  static propTypes = {
    basename: PropTypes.string,
    forceRefresh: PropTypes.bool,
    getUserConfirmation: PropTypes.func,
    keyLength: PropTypes.number,
    children: PropTypes.node
  }

  history = createHistory(this.props)

  render() {
    return <Router history={this.history} children={this.props.children}/>
  }
}

export default BrowserRouter

因而,咱們放棄使用BrowserRouter,而使用Router

修改app.js

==>

// v4
import { Router, Route } from 'react-router-dom'
//...

<Router history={history}>
  <div>
    <Route path='/' component={CoreLayout} />
  </div>
</Router>

這樣,這個坑算是填上了。也就完成了history和store之間的同步。


重寫路由

v4取消了PlainRoute 中心化配置路由。Route是一個react component。
取消了IndexRoute,經過Switch來組件提供了類似的功能,當<Switch>被渲染時,它僅會渲染與當前路徑匹配的第一個子<Route>

routes/index.js

// v3
//..
export const createRoutes = (store) => ({
  path        : '/',
  component   : CoreLayout,
  indexRoute  : Home,
  childRoutes : [
    CounterRoute(store),
    ZenRoute(store),
    ElapseRoute(store),
    RouteRoute(store),
    PageNotFound(),
    Redirect
  ]
})
//...

==>

// ...
const Routes = () => (
  <Switch>
    <Route exact path='/' component={Home} />
    <Route path='/counter' component={AsyncCounter} />
    <Route path='/zen' component={AsyncZen} />
    <Route path='/elapse' component={AsyncElapse} />
    <Route path='/route/:id' component={AsyncRoute} />
    <Route path='/404' component={AsyncPageNotFound} />
    <Redirect from='*' to='/404' />
  </Switch>
)

export default Routes
//

這裏路由的定義方式由PlainRoute Object改寫成了組件嵌套形式,在PageLayout.js中插入<Routes />


代碼分割

v3版本經過getComponetrequire.ensure實現代碼分割和動態路由。在v4版本,咱們新增異步高階組件,並使用import()替代require.ensure()

Counter/index.js

// v3
import { injectReducer } from '../../store/reducers'

export default (store) => ({
  path : 'counter',
  /*  動態路由 */
  getComponent (nextState, cb) {
    /* 代碼分割 */
    require.ensure([], (require) => {
      const Counter = require('./containers/CounterContainer').default
      const reducer = require('./modules/counter').default

      /*  將counterReducer注入rootReducer  */
      injectReducer(store, { key : 'counter', reducer })

      cb(null, Counter)
    }, 'counter')
  }
})

首先,新增AsyncComponent.js

import React from 'react'

export default function asyncComponent (importComponent) {
  class AsyncComponent extends React.Component {
    constructor (props) {
      super(props)

      this.state = {
        component: null,
      }
    }

    async componentDidMount () {
      const { default : component } = await importComponent()

      this.setState({
        component: component
      })
    }

    render () {
      const C = this.state.component

      return C
        ? <C {...this.props} />
        : null
    }
  }

  return AsyncComponent
}
  1. 這個asyncComponent 函數接受一個importComponent 的參數,importComponent 調用時候將動態引入給定的組件。

  2. componentDidMount 咱們只是簡單地調用importComponent 函數,並將動態加載的組件保存在狀態中。

  3. 最後,若是完成渲染,咱們有條件地提供組件。在這裏咱們若是不寫null的話,也可提供一個菊花圖,表明着組件正在渲染。

接着,改寫Counter/index.js

==>

import { injectReducer } from '../../store/reducers'
import { store } from '../../main'
import Counter from './containers/CounterContainer'
import reducer from './modules/counter'

injectReducer(store, { key : 'counter', reducer })

export default Counter

一旦加載Counter/index.js,就會把counterReducer注入到Rudecer中,並加載Counter組件。


瑣碎API的替換

v4 移除了onEnter onLeave等屬性,history替換router屬性,新增match

this.props.router.push('/')

==>

this.props.history.push('/')
this.props.params.id

==>

this.props.match.params.id

總結

這裏能夠看出,使用v4替換v3,對於大型項目並非一件輕鬆的事情,有許多小坑要踩,這就是社區不少項目仍然使用v2/v3的緣由。筆者認爲,v4更符合React的組件思想,因而作了一個實踐。最後歡迎指正拍磚,捂臉求star ? 。

參考

相關文章
相關標籤/搜索