前言 本文講述怎麼實現動態載入組件,並藉此闡述適配器模式。 一、普通路由例子 以上是最常見的 。在簡單的單頁應用中,這樣寫是ok的。因為打包後的單一js文件 也不過200k左右, 之後,對載入性能並沒有太大的影響。 但是,當產品經歷多次迭代後,追加的頁面導致 的體積不斷變大。這時候,優化就變得很有必 ...
前言
本文講述怎麼實現動態載入組件,並藉此闡述適配器模式。
一、普通路由例子
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()
,並將非同步載入的組件,set
入state
,觸發組件重新渲染。
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
也是按這種思路去實現的,只不過增加了很多附屬的功能點而已。