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对象

由于import()返回的是Promise对象,因此不能直接给<Router/>使用。segmentfault

3.2 采用适配器模式封装import()

适配器模式(Adapter):将一个类的接口转换成客户但愿的另一个接口。Adapter模式使得本来因为接口不兼容而不能一块儿工做的那些类能够一块儿工做。

当前场景,须要解决的是,使用import()异步加载组件后,如何将加载的组件交给React进行更新。
方法也很容易,就是利用state。当异步加载好组件后,调用setState方法,就能够通知到。
那么,依照这个思路,新建个高阶组件,运用适配器模式,来对import()进行封装。react-router

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()异步加载组件的实现

喜欢我文章的朋友,能够经过如下方式关注我:

wall的我的博客

相关文章
相关标签/搜索