今年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
v4版本保持一致,react-router-redux
發佈了v5.0.0版本,你固然也可使用它來實現這個功能。redux
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",
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...
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)) }
一切彷佛都很順利,接着第一個坑來了
根據history
API提供的
// 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
在建立的時候在內部已經引入了一個history
,updateLocation(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版本經過
getComponet
和require.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 }
這個
asyncComponent
函數接受一個importComponent
的參數,importComponent
調用時候將動態引入給定的組件。在
componentDidMount
咱們只是簡單地調用importComponent
函數,並將動態加載的組件保存在狀態中。最後,若是完成渲染,咱們有條件地提供組件。在這裏咱們若是不寫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組件。
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 ? 。