React router動態載入元件-介面卡模式的應用

我是leon發表於2018-09-12

前言

本文講述怎麼實現動態載入元件,並藉此闡述介面卡模式。

一、普通路由例子

import Center from `page/center`;
import Data from `page/data`;

function App(){
    return (
        <Router>
          <Switch>
            <Route exact path="/" render={() => (<Redirect to="/center" />)} />
            <Route path="/data" component={Data} />
            <Route path="/center" component={Center} />
            <Route render={() => <h1 style={{ textAlign: `center`, marginTop: `160px`, color:`rgba(255, 255, 255, 0.7)` }}>頁面不見了</h1>} />
          </Switch>
        </Router>
    );
}

以上是最常見的React router。在簡單的單頁應用中,這樣寫是ok的。因為打包後的單一js檔案bundle.js也不過200k左右,gzip之後,對載入效能並沒有太大的影響。
但是,當產品經歷多次迭代後,追加的頁面導致bundle.js的體積不斷變大。這時候,優化就變得很有必要。

二、如何優化

優化使用到的一個重要理念就是——按需載入
可以結合例子進行理解為:只載入當前頁面需要用到的元件。

比如當前訪問的是/center頁,那麼只需要載入Center元件即可。不需要載入Data元件。

業界目前實現的方案有以下幾種:

  • react-router的動態路由getComponent方法(router4已不支援)
  • 使用react-loadable小工具庫
  • 自定義高階元件進行按需載入

而這些方案共通的點,就是利用webpack的code splitting功能(webpack1使用require.ensure,webpack2/webpack3使用import),將程式碼進行分割。

接下來,將介紹如何用自定義高階元件實現按需載入。

三、自定義高階元件

3.1 webpack的import方法

webpack將import()看做一個分割點並將其請求的module打包為一個獨立的chunk。import()以模組名稱作為引數名並且返回一個Promise物件

因為import()返回的是Promise物件,所以不能直接給<Router/>使用。

3.2 採用介面卡模式封裝import()

介面卡模式(Adapter):將一個類的介面轉換成客戶希望的另外一個介面。Adapter模式使得原本由於介面不相容而不能一起工作的那些類可以一起工作。

當前場景,需要解決的是,使用import()非同步載入元件後,如何將載入的元件交給React進行更新。
方法也很容易,就是利用state。當非同步載入好元件後,呼叫setState方法,就可以通知到。
那麼,依照這個思路,新建個高階元件,運用介面卡模式,來對import()進行封裝。

3.3 實現非同步載入方法asyncComponent

import React from `react`;

export const asyncComponent = loadComponent => (

    class AsyncComponent extends React.Component {
        constructor(...args){
            super(...args);
    
            this.state = {
                Component: null,
            };

            this.hasLoadedComponent = this.hasLoadedComponent.bind(this);
        }
        componentWillMount() {
            if(this.hasLoadedComponent()){
                return;
            }
    
            loadComponent()
                .then(module => module.default ? module.default : module)
                .then(Component => {
                    this.setState({
                        Component
                    });
                })
                .catch(error => {
                    /*eslint-disable*/
                    console.error(`cannot load Component in <AsyncComponent>`);
                    /*eslint-enable*/
                    throw error;
                })
        }
        hasLoadedComponent() {
            return this.state.Component !== null;
        }
        render(){
            const {
                Component
            } = this.state;

            return (Component) ? <Component {...this.props} /> : null;
        }
    }
);
// 使用方式 

const Center = asyncComponent(()=>import(/* webpackChunkName: `pageCenter` */`page/center`));

如例子所示,新建一個asyncComponent方法,用於接收import()返回的Promise物件。
componentWillMount時(服務端渲染也有該生命週期方法),執行import(),並將非同步載入的元件,setstate,觸發元件重新渲染。

3.4 釋疑

  • state.Component初始化
this.state = {
    Component: null,
};

這裡的null,主要用於判斷非同步元件是否已經載入。

  • module.default ? module.default : module

這裡是為了相容具名default兩種export寫法。

  • return (Component) ? <Component {...this.props} /> : null;

這裡的null,其實可以用<LoadingComponent />代替。作用是:當非同步元件還沒載入好時,起到佔位的作用。
this.props是通過AsyncComponent元件透傳給非同步元件的。

3.5 修改webpack構建

output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath(`js/[name].[chunkhash].js`),
    chunkFilename: utils.assetsPath(`js/[id].[chunkhash].js`)
}

在輸出項中,增加chunkFilename即可。

四、小結

自定義高階元件的好處,是可以按最少的改動,來優化已有的舊專案。
像上面的例子,只需要改變import元件的方式即可。花最少的代價,就可以得到頁面效能的提升。
其實,react-loadable也是按這種思路去實現的,只不過增加了很多附屬的功能點而已。

參考

  1. 基於webpack Code Splitting實現react元件的按需載入
  2. react中使用webpack2的import()非同步載入元件的實現

相關文章