React router動態加載組件-適配器模式的應用

前言

本文講述怎麼實現動態加載組件,並藉此闡述適配器模式。javascript

1、普通路由例子

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的體積不斷變大。這時候,優化就變得頗有必要。java

2、如何優化

優化使用到的一個重要理念就是——按需加載
能夠結合例子進行理解爲:只加載當前頁面須要用到的組件。react

好比當前訪問的是/center頁,那麼只須要加載Center組件便可。不須要加載Data組件。webpack

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

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

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

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

3、自定義高階組件

3.1 webpack的import方法

webpack將import()看作一個分割點並將其請求的module打包爲一個獨立的chunk。import()以模塊名稱做爲參數名而且返回一個Promise對象segmentfault

由於import()返回的是Promise對象,因此不能直接給<Router/>使用。react-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便可。

4、小結

自定義高階組件的好處,是能夠按最少的改動,來優化已有的舊項目。
像上面的例子,只須要改變import組件的方式便可。花最少的代價,就能夠獲得頁面性能的提高。
其實,react-loadable也是按這種思路去實現的,只不過增長了不少附屬的功能點而已。

參考

  1. 基於webpack Code Splitting實現react組件的按需加載
  2. react中使用webpack2的import()異步加載組件的實現
相關文章
相關標籤/搜索