最近將公司項目的 react-router 從 v3 版本升到了 v4 版本,react-router v4 跟 v3 徹底不兼容,是一次完全的重寫。這也給升級形成了極大的困難,與其說升級不如說是對 router 層重寫。以前我也將項目的 react 從 v15 版本升級到了 v16 版本,相較而言升級 react-router 比升級 react 困難多了。升級過程當中踩了很多的坑,也有一些值得分享的點。寫成一篇小文,供你們參考。react
react-router v4 跟 react 同樣拆成了兩部分,核心的 react-router 和依運行環境而定的 react-router-dom 或 react-router-native(跟 react-dom 和 react-native 同樣)。本文要說的是瀏覽器環境,也就是 react-router + react-router-domwebpack
先安裝依賴(推薦使用 yarn)git
yarn add react-router react-router-dom history
爲何要安裝 history 後面會解釋。github
以前咱們項目中使用了 react-router-redux 你有不少理由使用它,但對於咱們來講惟一的理由或者用處就是用於在頁面組件以外導航,react-router-redux 讓你能夠在任何地方經過 dispatch 處理頁面跳轉,如:store.dispatch(push('/'))。由於這個咱們就必須使用 react-router-redux 嗎?固然不須要,有更簡單的辦法實現這個需求。因此此次升級我移除了react-router-redux, 寫做此文時支持 react-router v4 的 react-router-redux 還處於 v5.0.0-alpha.7 也是緣由之一。web
還記得以前安裝的 history 嗎?history 是 react-router 惟二的主要依賴之一,之因此要顯式安裝,是由於咱們要使用它來實現頁面組件外導航。如下以 browser history 爲例(hash history 和 memory history 都是同樣的):redux
咱們不使用 react-router-dom 提供的 BrowserRouter 而是本身實現一個react-native
// history.js import createHistory from 'history/createBrowserHistory'; const history = createHistory(); export default history;
// index.js import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router'; import history from './history'; import App from './app'; ReactDOM.render( <Router history={history}> <App /> </Router>, document.getElementById('app') );
搞定!就這麼簡單,這樣在任何地方只要引用 history 就可使用它進行導航操做,如 history.push('/'),更多使用方式請參考 history 文檔。其實 react-router-dom 的 BrowserRouter 跟咱們作了一樣的事,區別在於咱們這麼作能把 history 暴露出來。這個 history 就是頁面組件 props 裏面的 history 天然也就能作一樣的事情。瀏覽器
react-router v3 是面向配置的,組件寫法只是一種語法糖。而 react-router v4 是徹底面向組件的,提供的 Route Switch 等都是真正的組件。這也就致使只能按組件的方式寫路由,不能寫配置。可是 v3 那樣的配置確實有一些方便之處,如統一管理、使用方便等。緩存
多虧 JSX 靈活的語法,咱們依然有辦法按配置的方式寫 react-router v4 的路由。babel
// routes.js import Home from './home'; import About from './about'; import Help from './help'; export default [{ path: '/', exact: true, component: Home }, { path: '/about', component: About }, { path: '/help', component: Help }];
// app.js import React from 'react'; import { Switch, Route } from 'react-router'; import routes from './routes'; import NotFound from './not-found'; class App extends React.Component { render() { return ( <Switch> {routes.map((route, i) => <Route key={i} exact={!!route.exact} path={route.path} component={route.component} />)} <Route component={NotFound} /> </Switch> ); } } export default App;
這樣咱們就用配置的方式寫出了面向組件的路由,兼顧二者的優勢。若是有嵌套路由需求,能夠參考官方示例。官方也提供了一個 react-router-config, 不過我沒有使用,一來以爲不必,二來寫做此文時它還處於 v1.0.0-beta.4 版本。
Web 應用最大的一個優點就是沒必要下載整個應用,只用下載須要的部分就可使用。要達到這樣的目標,就須要對代碼進行分片,異步加載組件。惋惜 react-router v4 沒有像 v3 同樣提供加載異步組件的接口。這部分工做就須要咱們本身來處理。
咱們能夠建立一個高階組件 Bundle,專門用來加載異步組件。
// bundle.js import React from 'react'; class Bundle extends React.Component { constructor(props) { super(props); this.state = { Component: null }; props.load().then(Component => this.setState({ Component: Component.default })); } render() { const { load, ...props } = this.props; const Component = this.state.Component; return Component ? <Component {...props} /> : null; } } export default Bundle;
而後修改一下 routes.js
// routes.js import React from 'react'; import Bundle from './bundle'; export default [{ path: '/', exact: true, component(props) { // 這裏的 component 函數也是一個高階組件 return <Bundle {...props} load={() => import('./home')} />; } }, { path: '/about', component(props) { return <Bundle {...props} load={() => import('./about')} />; } }, { path: '/help', component(props) { return <Bundle {...props} load={() => import('./help')} />; } }];
這樣每一個頁面都會打包成單獨的 JS,訪問相應頁面纔會去異步加載對應的組件。這樣也能夠作精細化緩存控制。
須要注意的是 import() 語法在寫做本文時還處於 Stage 2 的狀態,需給 Babel 添加 syntax-dynamic-import 插件才能正常工做,另外需 webpack 2 及以上才支持。
由於各類緣由 react-router v4 再也不解析 ?key=value 這樣的 URL 的查詢參數,頁面組件 props.location 中只有 search 字符串。這跟 v3 不兼容,並且很不方便。咱們有辦法兼容一下嗎?固然有,這時候以前寫的 histroy.js 又有新的用處了。
// history.js import qs from 'qs'; import createHistory from 'history/createBrowserHistory'; function addQuery(history) { const location = history.location; history.location = { ...location, query: qs.parse(location.search, { ignoreQueryPrefix: true }) }; } const history = createHistory(); addQuery(history); export const unlisten = history.listen(() => { // 每次頁面跳轉都會執行 addQuery(history); }); export default history;
這樣咱們就能在頁面組件 props.location.query 拿到解析好的 URL 查詢參數了,跟 v3 完美兼容。還有個額外的好處是在任何地方引用 history 均可以拿到解析好的 URL 查詢參數。須要注意的是,在 history 的設計中,history 對象是 Mutable 的,因此咱們能夠直接修改 history。可是 history.location 是 Immutable 的,因此咱們要確保每個 location 對象都是全新的。
react-router v4 跟 redux 搭配有一個大坑(mobx 應該也有一樣的問題),詳情請看這篇文章,這裏就再也不贅述。簡單來講,若是一個組件用 redux 的 connect 包裝過,又️不是 Route 的子組件,那麼 history 的變動就不會觸發這個組件的更新,它的子組件天然也不會更新。好比應用的根組件(上文的 App)。
解決方案也很簡單,能夠用 react-router v4 提供的 withRouter 再包裝一遍:withRouter(connect(...)(App)),或者讓 App 作爲 Router 的子組件,原理都同樣。我採用的後者。
// app.js import React from 'react'; import { connect } from 'react-redux'; import { Switch, Route } from 'react-router'; import routes from './routes'; import NotFound from './not-found'; class App extends React.Component { render() { return ( <Switch> {routes.map((route, i) => <Route key={i} exact={!!route.exact} path={route.path} component={route.component} />)} <Route component={NotFound} /> </Switch> ); } } function mapStateToProps(state) { return { someState: state.someState }; } export default connect(mapStateToProps)(App);
// index.js import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import store from './store'; import history from './history'; import App from './app'; ReactDOM.render( <Provider store={store}> <Router history={history}> <Route component={App} /> {/* 沒有 path 就會匹配全部路由 */} </Router> </Provider>, document.getElementById('app') );
不得不說升級 react-router 很困難,坑也不少。可是把坑一個個填完,最終完美升級也是一件頗有意思,頗有成就感的事。但願這篇文章能對你有所幫助。
另外完整的 Demo 請戳個人 GitHub,喜歡的話點個 Star 吧 :P