react-router 升級小記

前言

最近將公司項目的 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 你有不少理由使用它,但對於咱們來講惟一的理由或者用處就是用於在頁面組件以外導航,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 版本。

異步組件與 Code Splitting

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 對象都是全新的。

搭配 Redux

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

相關文章
相關標籤/搜索