Webpack 的 Code Splitting 特性能夠把代碼分離到不同的 bundle 中,然後可以按需加載或並行加載這些文件。
什麼是代碼分割(code splitting)#
我們知道,在使用 webpack 打包 react 應用時,webpack 將整個應用打包成一個 js 文件,當用戶訪問首屏時,會一次性加載整個 js 文件,這就造成了首屏渲染速度變慢的問題。於是,webpack 開發了代碼分割的特性,
此特性能夠把代碼分離到不同的 bundle 中,然後可以按需加載或並行加載這些文件。代碼分離可以用於獲取更小的 bundle,以及控制資源加載優先級,如果使用合理,會極大影響加載時間。
有三種常用的代碼分離方法:
- 入口起點:使用 entry 配置手動地分離代碼。
- 防止重複:使用 CommonsChunkPlugin 去重和分離 chunk。
- 動態導入:通過模塊的內聯函數調用來分離代碼。
本文只討論動態導入(dynamic imports)的方法。
動態導入#
當涉及到動態代碼拆分時,webpack 提供了兩個類似的技術。對於動態導入,第一種,也是優先選擇的方式是,使用符合 ECMAScript 提案 的 import () 語法。第二種,則是使用 webpack 特定的 require.ensure。讓我們先嘗試使用第一種……
注意:import () 調用會在內部用到 promises。如果在舊有版本瀏覽器中使用 import (),記得使用 一個 polyfill 庫(例如 es6-promise 或 promise-polyfill),來 shim Promise。
下面結合 react-router 4 來實現 react 的代碼分割。
react code splitting and lazy load#
在使用 react-router4 進行代碼分割的路上,社區已經有成熟的第三方庫進行了實現,如 react-loadable。在此處將介紹如何不借助第三方庫實現代碼分割。
此處我們假設你已經對 react、react-router4、webpack 有基本的了解,可以搭建簡單的開發環境。下面先看一下我搭建的項目入口文件。
src 下 index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('app'));
src 下 App.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import Routes from './routes';
export default () => (
<BrowserRouter>
<Switch>
{
Routes.map(({name, path, exact = true, component }) => (
<Route path={path} exact={exact} component={component} key={name} />
))
}
</Switch>
</BrowserRouter>
)
在 App.js 中,使用了 react-router-dom 來做路由,routes.js 文件作為路由配置文件,下面使用路由配置遍歷生成路由組件。
下面來看一下路由配置 routes.js。
import AsyncComponent from './components/acync-component';
export default [
{
name: '首頁',
icon: 'home',
path: '/',
component: AsyncComponent(() => import('./containers/home'))
},
{
name: '詳情',
icon: 'detail',
path: '/detail',
component: AsyncComponent(() => import('./containers/detail'))
}
]
routes.js 中配置了路由組件需要的參數,需要注意的是在路由參數中使用了異步組件 AsyncComponent,注意這裡並沒有進行組件的引入,而是傳給了 AsyncComponent 一個函數,它將在 AsyncComponent(() => import('./containers/home')) 組件被創建時進行動態引入。同時,這種傳入一個函數作為參數,而非直接傳入一個字符串的寫法能夠讓 webpack 意識到此處需要進行代碼分割。
使用 import()需要使用像 Babel 預處理器和 Syntax Dynamic Import Babel Plugin。由於 import () 會返回一個 promise,因此它可以和 async 函數一起使用,使用 acync 函數需要使用 babel-plugin-transform-runtime。
安裝 babel 插件:
npm i -D babel-plugin-syntax-dynamic-import babel-plugin-transform-runtime
本項目使用的其他 babel 插件還有
babel-core、babel-loader、babel-preset-env、babel-preset-react 等,主要用於 jsx 編譯
配置.babelrc
根目錄下新建.babelrc,配置如下:
{
"presets":["react","env"],
"plugins": ["syntax-dynamic-import", "transform-runtime"]
}
異步組件 AsyncComponent#
本項目的 AsyncComponent 放在 src/components/async-component/index.js 中,代碼如下:
import React, { Component } from 'react';
export default (loadComponent, placeholder = '拼命加載中。。。。') => {
return class AsyncComponent extends Component {
constructor() {
super();
this.state = {
Child: null
}
this.unmount = false;
}
componentWillUnmount() {
this.unmount = true;
}
async componentDidMount() {
const { default: Child } = await loadComponent();
if (this.unmount) return;
this.setState({
Child
})
}
render() {
const { Child } = this.state;
return (
Child ? <Child {...this.props} /> : placeholder
)
}
}
}
首先,這是一個高階組件,返回一個新的組件,傳入兩個參數,一個是需要動態加載組件的方法,第二個是動態加載時的佔位符,也可以傳入一個 Loading 組件。
在返回的 AsyncComponent 內部,constructor 中,初始化一個 state 為 Child,值為 null,並定義 this.unmount =false,用於表示組件是否被卸載。
使用 acync 定義異步方法,componentDidMount 中,使用 await 異步執行傳入的第一個參數,用於動態加載當前路由的組件。
注意:
注意當調用 ES6 模塊的 import () 方法(引入模塊)時,必須指向模塊的 .default 值,因為它才是 promise 被處理後返回的實際的 module 對象。
故此處使用 ES6 的對象解構獲取到模塊的 default 並賦值到 Child 上。
然後判斷組件被卸載的狀態,被卸載即返回。
下面將 Child 設置到 state 上。
在 render 方法中,從 state 中獲取 Child,然後使用三元運算符判斷 Child 是否為 true,為 true 則渲染 Child 組件,並注入 this.props,否則渲染佔位符。
組件 componentWillUnmount 時,設置 this.unmout 為 true。
測試驗證#
在 containers 中新建兩個文件夾 home 和 detail,在兩個文件夾下編寫 index.js 作為兩個路由組件。代碼如下:
home/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import '../../style.css';
import icon from '../../assets/404.png';
export default class Home extends React.Component {
render() {
return (
<div>
<h1>hello webpack</h1>
<img src={icon} alt=""/>
<Link to="/detail">詳情</Link>
</div>
)
}
}
detail/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import '../../style.css';
import icon from '../../assets/404.png';
export default class Detail extends React.Component {
render() {
return (
<div>
<h1>hello webpack detail</h1>
<img src={icon} alt=""/>
</div>
)
}
}
在根目錄下 package.json 配置啟動腳本:
"scripts": {
"start": "webpack-dev-server --mode development",
"build": "webpack --mode production"
},
然後運行 npm start 啟動項目:
打開瀏覽器訪問 localhost:8080
可以看到頁面先加載了 main.js 和 0.js,點擊詳情按鈕跳轉到http://localhost:8080/detail。
馬上加載了 1.js,這樣就實現了代碼分割,每個路由都是動態加載的。提升了首屏渲染速度。