Open libin1991 opened 5 years ago
本文讲述怎么实现动态加载组件,并借此阐述适配器模式。
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的体积不断变大。这时候,优化就变得很有必要。
React router
bundle.js
gzip
优化使用到的一个重要理念就是——按需加载。 可以结合例子进行理解为:只加载当前页面需要用到的组件。
比如当前访问的是/center页,那么只需要加载Center组件即可。不需要加载Data组件。
/center
Center
Data
业界目前实现的方案有以下几种:
getComponent
而这些方案共通的点,就是利用webpack的code splitting功能(webpack1使用require.ensure,webpack2/webpack3使用import),将代码进行分割。
code splitting
require.ensure
import
接下来,将介绍如何用自定义高阶组件实现按需加载。
webpack将import()看做一个分割点并将其请求的module打包为一个独立的chunk。import()以模块名称作为参数名并且返回一个Promise对象。
import()
因为import()返回的是Promise对象,所以不能直接给<Router/>使用。
<Router/>
适配器模式(Adapter):将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
当前场景,需要解决的是,使用import()异步加载组件后,如何将加载的组件交给React进行更新。 方法也很容易,就是利用state。当异步加载好组件后,调用setState方法,就可以通知到。 那么,依照这个思路,新建个高阶组件,运用适配器模式,来对import()进行封装。
state
setState
适配器模式
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(),并将异步加载的组件,set入state,触发组件重新渲染。
asyncComponent
componentWillMount
set
this.state = { Component: null, }; 复制代码
这里的null,主要用于判断异步组件是否已经加载。
null
module.default ? module.default : module
这里是为了兼容具名和default两种export写法。
具名
default
export
return (Component) ? <Component {...this.props} /> : null;
这里的null,其实可以用<LoadingComponent />代替。作用是:当异步组件还没加载好时,起到占位的作用。 this.props是通过AsyncComponent组件透传给异步组件的。
<LoadingComponent />
this.props
AsyncComponent
output: { path: config.build.assetsRoot, filename: utils.assetsPath('js/[name].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') } 复制代码
在输出项中,增加chunkFilename即可。
chunkFilename
自定义高阶组件的好处,是可以按最少的改动,来优化已有的旧项目。 像上面的例子,只需要改变import组件的方式即可。花最少的代价,就可以得到页面性能的提升。 其实,react-loadable也是按这种思路去实现的,只不过增加了很多附属的功能点而已。
react-loadable
喜欢我文章的朋友,可以通过以下方式关注我:
前言
本文讲述怎么实现动态加载组件,并借此阐述适配器模式。
一、普通路由例子
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
组件。业界目前实现的方案有以下几种:
getComponent
方法(router4已不支持)而这些方案共通的点,就是利用webpack的
code splitting
功能(webpack1使用require.ensure
,webpack2/webpack3使用import
),将代码进行分割。接下来,将介绍如何用自定义高阶组件实现按需加载。
三、自定义高阶组件
3.1 webpack的import方法
因为
import()
返回的是Promise对象,所以不能直接给<Router/>
使用。3.2 采用适配器模式封装import()
当前场景,需要解决的是,使用
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()
,并将异步加载的组件,set
入state
,触发组件重新渲染。3.4 释疑
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
也是按这种思路去实现的,只不过增加了很多附属的功能点而已。参考
喜欢我文章的朋友,可以通过以下方式关注我: