1. 程式人生 > >react-router 4 升級攻略

react-router 4 升級攻略

react-router 從 2 / 3 升級到 4.x.x

1. 說明

react-router 版本更新到 4.x.x 已經有一段時間了,但是由於 API 改動較大,為了專案的穩定,一直沒有更新,最近有了空閒時間,整體對 react 的專案進行了一次升級,這裡記錄一下 如何從 react-router 2.x.x/3.x.x 遷移到 react-router 4.x.x

本次使用 react-router 版本為 4.1.0

原始碼地址

導航

  1. 說明
  2. 重要檔案版本
  3. 新的API說明
  4. 從 2 / 3 遷移到 4.x.x

2. 重要檔案版本

    {
        "postcss-loader": "^1.3.3",
        "react-hot-loader": "^3.0.0-beta.7",
        "webpack": "^2.6.1",
        "webpack-dev-server": "^2.5.0",
        "webpack-merge": "^4.1.0",

        "es6-promise": "^4.1.0",
        "history": "^4.6.3",
        "isomorphic-fetch": "^2.2.1",
        "pure-render-decorator
": "^1.2.1", "react": "^15.6.1", "react-addons-css-transition-group": "^15.6.0", "react-dom": "^15.6.1", "react-redux": "^5.0.5", "react-router-dom": "^4.1.1", "react-router-redux": "^5.0.0-alpha.6", "react-tap-event-plugin": "^2.0.1", "redux
": "^3.7.0", "redux-thunk": "^2.2.0" }

注意:有很多 webpack 配置需要的外掛沒有列出,因為改動不是很大,對專案執行沒有很大的影響,具體的 package.json 可以看原始碼,這個是舊版的模板,新版本會在近期提交(新版原始碼

  1. postcss-loader 外掛 最新版本已經到了 2.x.x ,但是最新版與 1.x.x配置使用上有出入,如果使用新版本遇到報錯,可以檢視 官方文件 或者 這篇部落格, 大概意思就是不能像之前一樣使用 webpack.LoaderOptionsPlugin 配置 這個外掛 例如 配置 autoprefixer ,可以新增一個 postcss.config.js 配置檔案,或者 在 寫成下邊這樣,我這裡還是使用的 1.x.x 版本
    {
        loader:"postcss-loader",
        options: {    
            sourceMap: true,       
            plugins: (loader) => [
                require('autoprefixer')(), 
            ]
        }
    }
  1. webpack 使用的是 2.x.x 的版本,因為測試的時候出了點 BUG ,所以沒有升級到最新的 3.x.x

  2. react 相關元件都升級到最新版本,其中 為配合 react-router 4 的使用 react-router-redux 使用的是 5.0.0-alpha.6, 而且增加了 history 元件

  3. react-router-dom 程式碼是基於 react-router 的 而且做了升級,只用下載 react-router-dom 就可以了

3. 新的 API 說明

這裡檢視官方文件

1. <Router>

Router 是所有路由元件共用的底層介面,用來與Redux狀態管理庫的history 保持同步,但是在 4.x.x版本中一般不使用這個元件,而是使用 <BrowserRouter> <HashRouter>

    <Router history={history}>
      <App/>
    </Router>

2. <BrowserRouter>

使用 HTML5 提供的 history API 來保持 UI 和 URL 的同步,一下是他的屬性

  1. basename: 設定基準的 URL,使用場景:應用部署在 伺服器的二級目錄,將其設定為目錄名稱,不能以 /結尾,設定之後跳轉的頁面都會加上 basename的字首

  2. forceRefresh: 是否強制重新整理頁面,用於適配不支援 h5 history 的瀏覽器

  3. getConfirmation: 彈出瀏覽器的確認框,進行確認

3. <Route>

與 2 / 3 版本的 Route 作用一致,都是在 location 匹配 path 的路徑的時候,渲染指定元件,但是寫法上有變化,而且增加了一些設定

  1. 注意:4.x.x 版本的 <Route>不在有 onEnter onLeave 這樣的路由鉤子函式,如果需要這個功能,要在 Route 對應元件中 寫在 componentWillMount 或者 componentWillUnmount

  2. 渲染內容方法-1:component 類似 2 / 3 中的 component 屬性,值為一個 react 元件,只有地址匹配的時候才會渲染元件

    // 定義元件
    class App extends React.Component {
        constructor(props) {
            super(props);
        }
        render() {
            return <span>App</span>;
        }
    }

    // Route
    <Route component={App}/>
  1. 渲染內容方法-2:render 值可以選擇傳一個在地址匹配時被呼叫的函式,而不是建立一個元件,但是需要一個返回值,返回一個元件或者null
    <Route render={(props) => (
        <App>
            <SomeCom>
        </App>
    )/>
  1. 渲染內容方法-3:childrenrender 一樣,但是不會匹配地址,路徑不匹配時 URL的match 值為 null,可以用來根據路由是否匹配動態調整UI

  2. 繫結在 Route 上的元件或者函式都會獲得以下的屬性 component繫結的元件可以通過 this.props.[history, location, match] 獲取;render, children 下的函式;會得到 param = {match, location, history} 的引數

    // 虛擬碼
    history: {
        goBack(), // 瀏覽器回退
        goForward(), // 前進
        push(), // 新增跳轉
        replace(), // 覆蓋跳轉
    }
    location: {
        hash, // hash (#123)
        pathname, // 路徑 (/home)
        search, // (?id=123)
        state, // ({name: 'name', id: 'id'})
        // query, // 4.x.x 沒有 query 了,要從 search 中解析
    }
    match: {
        isExact, // 是否整個URL都需要匹配
        params, // 路徑引數,通過解析URL中動態的部分獲得的鍵值對
        path, // 路徑格式
        url, // URL匹配的部分
        params, // 
    }
  1. path 屬性, 用於匹配 location 路徑 如果沒有 path 屬性則總是匹配

  2. exact 屬性 是否需要完全匹配 ,不會判斷末尾的/

    /*
        以下兩個就是不完全匹配,只有前半部分 一致 /one 
        如果沒有設定 exact=true,則可以進入到 path 對應的元件,
        如果設定 exact 則 不能渲染元件
    */ 
    path: /one
    location.pathname : /one/two
  1. strict 屬性 用來強制判斷路徑結尾是否含有/, 只有path 和 loation.pathname 的結尾都含有或者都不含有/ 才會匹配

  2. 關於路由巢狀,4.x.x 不能像以前的版本那樣嵌套了,需要新的方式,會在下邊的 從 2 / 3 遷移到 4.x.x 中有演示

4. <Redirect>

與 2 / 3 版本一樣都是用來重定向到新的地址,預設會覆蓋訪問記錄中的原地址,但是多了一些屬性

  1. to={string|object} 重定向的目標,可以是字串,也可以是一個地址的物件

  2. from={string} 匹配需要重定向的地址

  3. push bool 表示是否需要不替換地址,值為true的時候,不會把訪問記錄中的地址覆蓋

    <Redirect push to={{pathname: '/login', search: '?id=123'}}>

5. <Switch/>

用來渲染匹配地址的第一個 或者 , 可以用來配置過度動畫,更多介紹看這裡

與 2 / 3 相同,增加了屬性 replace 表示是否替換掉原地址

7. withRouter

很重要的一個功能,將普通元件用 withRouter 包裹後,元件就會獲得 location history match 三個屬性,可以用來直接為子元件提供 歷史相關的功能


export default withRouter(App);

// App.js
const {history, location, match} = this.props;

4. 從 2 / 3 遷移到 4.x.x

由於API 改動比較大,使用方式也有變化,因此

1. 構建工具

webpack 的配置檔案方面沒有什麼需要改動的,如果你的外掛postcss-loader 更新了,則需要修改一下配置部分

    // webpack.config.js
    rules: [
        {
            test: /\.(scss|sass|css)$/,
            use: [
                'style-loader',
                'css-loader',
                {
                    loader: 'postcss-loader',
                    options: {
                        sourceMap: false,
                        plugins: (loader) => [
                            autoprefixer(config.autoConfig) // autoConfig 為 autoprefixer 的配置
                        ]
                    }
                },
                'sass-loader' + config.sassLoaderSuffix // sass的配置
            ]
        }
    ]
    ...
    // 取消掉之前的 postcss 外掛配置
    plugins: [
        // new webpack.LoaderOptionsPlugin({
         //    options: {
         //       context: '/',
         //       minimize: true,
         //       postcss: [autoprefixer(config.autoConfig)]
         //    }
         // }),
    ]
2. app 入口檔案 app/js/index.js

改動不大

    /*
       app/js/index.js
       入口檔案, 配置 webpack 熱載入模組
    */
    import '../scss/index.scss'; // 引入樣式

    import React from 'react';
    import ReactDOM from 'react-dom';
    import {AppContainer} from 'react-hot-loader';
    import injectTapEventPlugin from 'react-tap-event-plugin';

    // 引入路由配置模組
    import Root from './routes.js';

    const mountNode = document.getElementById('app'); // 設定要掛在的點

    // react 的外掛,提供onTouchTap() // 更新後沒有測試是否好使
    injectTapEventPlugin();

    // 封裝 render
    const render = (Component) => {
        ReactDOM.render((
            <AppContainer>
                <Component/>
            </AppContainer>
        ), mountNode);
    };

    render(Root); // 初始化
    console.log(process.env.NODE_ENV);

    if (module.hot && process.env.NODE_ENV !== 'production') {
        module.hot.accept('./routes.js', (err) => {
            if (err) {
                console.log(err);
            }
            /*
                解除安裝 react 模組後 重灌
            */ 
            ReactDOM.unmountComponentAtNode(mountNode);
            render(Root);
        });
    }
3. Root 基礎的路由配置檔案 app/js/routes.js

這部分變動很大,上一個版本還有一個叫Root.js 的檔案 用來協助配置 路由,這個版本合併在一起

    /*
       Root, Router 配置
    */
    import React from 'react';
    import {Provider} from 'react-redux';
    import {
        /*
            注意:這裡也可以使用 BrowserRouter ,但是為了配合 redux 使用,引入了 react-router-redux,提供了一個用來關聯的 ConnectedRouter
        */ 
        // BrowserRouter as Router, 
        Route, 
        Switch, 
        Redirect
    } from 'react-router-dom';
    import {ConnectedRouter} from 'react-router-redux'; // 版本需要 5.0.0-alpha.6
    import createHistory from 'history/createBrowserHistory';

    import store from './store/index';
    import {App, Home, Test} from './containers/index';

    const history = createHistory();
    // Router 下邊只能有一個節點
    const Root = () => (
       <Provider store={store}>
          <ConnectedRouter history={history} >
             <div>
                <Switch>
                   <Route path="/" render={(props) => (
                      <App>
                         <Switch>
                            <Route path="/" exact component={Home}/>
                            <Route path="/home" component={Home}/>
                            <Route path="/test" component={Test}/>
                            <Redirect from="/undefined" to={{pathname: '/', search: '?mold=redirect'}}/>
                         </Switch>
                      </App>
                   )}/>
                   <Route render={() => (<Redirect to="/"/>)}/>
                </Switch>
             </div>
          </ConnectedRouter>
       </Provider>
    );
    export default Root;
4. 路由巢狀的寫法

4.x.x 定義每一個 Route 都是普通的 react 元件,所有使用的時候也要像普通的元件一樣,巢狀的時候,可以按照下邊的方式

    // 定義外層路由
    const Root = () => {
        <Provider store={store}>
          <ConnectedRouter history={history} >
             <Route path="/" exact component={App}/>
          </ConnectedRouter>
       </Provider>
    }

    // App.js 元件中定義在App下的路由
    import {Route, Link} from 'react-router-dom';

    class App extends React.Component {
        constructor(props) {
            super(props);
        }
        render() {
            return (
                <div id="app-container">
                    <header className="app-header">成員列表</header>
                    <div className="app-body">
                        <Link to="/test">點選進入 Test 頁面</Link>
                        <Route path="/test" component={Test}/>
                    </div>
                 </div>
            );

        }
    }

上邊這樣的寫法,與沒有升級前的寫法變化很大,但是更加靈活了,不過如果用來版本遷移,工作量會很大,下邊是一個用於相容上個版本程式碼的寫法,可以把所有路由集中在一個檔案中管理

    // 方法就是在需要有子級路由的地方使用 render 方法,方法中使用父級路由對應的元件將其他子級路由包裹住
    const Root = () => (
        <Provider store={store}>
            <ConnectedRouter history={history}>
                <div>
                    <Switch>
                        <Route path="/" render={(props) => (
                            <App>
                                <Switch>
                                    <Route path="/" exact component={Login}/>
                                    <Route path="/login" component={Login}/>
                                    <Route path="/unable" component={Unable}/>
                                    <Route path="/show" render={(props) => (
                                        <Show>
                                            <Switch>
                                                <Route path="/show" exact component={Home}/>
                                                <Route path="/show/test" component={Test}/>
                                            </Switch>
                                        </Show>
                                    )}/>
                                    <Route path="/404" component={NotFoundPage}/>
                                </Switch>
                            </App>
                        )}/>
                    </Switch>
                </div>
            </ConnectedRouter>
        </Provider>
    );
5. store 的配置 app/js/store

增加了 router 的中介軟體

    // store/configureStore.js

    import { compose, createStore, applyMiddleware } from 'redux';
    import { routerMiddleware } from 'react-router-redux'; // 新增 route 中介軟體
    // 引入thunk 中介軟體,處理非同步操作
    import thunk from 'redux-thunk';
    import createHistory from 'history/createBrowserHistory'; // 引入 history

    export const history = createHistory();
    const routeMiddleware = routerMiddleware(history);

    const middleware = [routeMiddleware, thunk];
    /*
        輔助使用chrome瀏覽器進行redux除錯
    */
    const composeEnhancers =
      process.env.NODE_ENV !== 'production' &&
      typeof window === 'object' &&
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
          // Specify here name, actionsBlacklist, actionsCreators and other options
        }) : compose;

    /*
       呼叫 applyMiddleware ,使用 middleware 來增強 createStore
    */
    const configureStore = composeEnhancers(
       applyMiddleware(...middleware)
    )(createStore);

    export default configureStore; 
    // store/index.js
    import configureStore from './configureStore';
    import reducer from '../reducers';

    // 給增強後的store傳入reducer
    const store = configureStore(reducer);

    export default store;
6. reducer 配置 app/js/reducers/index.js

與升級前沒有區別

    // 引入reducer
    import {combineReducers} from 'redux';
    import {routerReducer} from 'react-router-redux';
    import rootReducer from './rootReduer';

    // 合併到主reducer
    const reducer = combineReducers({
        rootReducer,
        routing: routerReducer
    });

    export default reducer;
7. 元件中使用

寫在 <Router component={Component}> 中的元件自動獲得,location,history,match 三個屬性

普通元件使用 需要通過父元件傳遞或者通過withRouter


    import React, { Component } from 'react';
    import {withRouter} from 'react-router-dom';

    import '../../../scss/app.scss';

    class AppCom extends Component {
       constructor(props) {
          super(props);
       }
       render() {
        // 獲得的 location 屬性
          const path = this.props.location.pathname;

          return (
             <div id="app-container">
                <header className="app-header">地址:{path}</header>
                <div className="app-body">
                   {this.props.children}
                </div>
             </div>
          );
       }
    }

    export default withRouter(AppCom)
8. 手動跳轉頁面與路由鉤子函式onEnter onLeave

上一個版本中大量使用了 browserHistory 進行頁面跳轉,這個版本中需要使用 傳入的 history 來進行跳轉

新版本中路由的onEnter onLeave 方法取消,可以通過元件的 componentWillMount 和 componentWillUnmount 實現

    import React, { Component } from 'react';
    import {withRouter} from 'react-router-dom';

    import confPut from './utils/confPut';

    import '../../../scss/home.scss';

    class TestCom extends Component {
       constructor(props) {
          super(props);
       }
       componentWillMount() {
            // 代替 原 Route 元件的 onEnter()
       }
       componentWillUnmount() {
            // 代替 原 Route 元件的 onLeave()
       }
       handleClick = () => {
          confPut('add', 'will');
          this.props.history.push({
             pathname: '/home',
             search: '?name=testname'
          });
       }
       render() {
          console.log(this.props);

          return (
             <div className="test-container">
                this is Test Page
                <button onClick={this.handleClick}>點選回到</button>
             </div>
          );
       }
    }

    export default withRouter(TestCom);

5. 總結

版本升級工作還在進行中,應該還會有其他的問題出現,這裡只是一個階段的記錄,還有一些像 code splitting 還沒有嘗試,留著下一階段試驗。