webpack 程式碼分離
程式碼分離是 webpack 中最引人注目的特性之一。此特效能夠把程式碼分離到不同的 bundle 中,然後可以按需載入或並行載入這些檔案。程式碼分離可以用於獲取更小的 bundle,以及控制資源載入優先順序,如果使用合理,會極大影響載入時間。
有三種常用的程式碼分離方法:
- 入口起點:使用
entry
配置手動地分離程式碼。 - 防止重複:使用
CommonsChunkPlugin
去重和分離 chunk。 - 動態匯入:通過模組的行內函數呼叫來分離程式碼。
1. 入口起點(entry points)
這是迄今為止最簡單、最直觀的分離程式碼的方式。不過,這種方式手動配置較多,並有一些陷阱,我們將會解決這些問題。先來看看如何從 main bundle 中分離另一個模組:
project
webpack-demo |- package.json |- webpack.config.js |- /dist |- /src |- index.js + |- another-module.js |- /node_modules
another-module.js
import _ from 'lodash'; console.log( _.join(['Another', 'module', 'loaded!'], ' ') );
webpack.config.js
const path = require('path'); const HTMLWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, plugins: [ new HTMLWebpackPlugin({ title:'Code Splitting' }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };
這將生成如下構建結果:
Hash: 309402710a14167f42a8 Version: webpack 2.6.1 Time: 570ms Asset Size Chunks Chunk Names index.bundle.js 544 kB 0 [emitted] [big] index another.bundle.js 544 kB 1 [emitted] [big] another [0] ./~/lodash/lodash.js 540 kB {0} {1} [built] [1] (webpack)/buildin/global.js 509 bytes {0} {1} [built] [2] (webpack)/buildin/module.js 517 bytes {0} {1} [built] [3] ./src/another-module.js 87 bytes {1} [built] [4] ./src/index.js 216 bytes {0} [built]
正如前面提到的,這種方法存在一些問題:
- 如果入口 chunks 之間包含重複的模組,那些重複模組都會被引入到各個 bundle 中。
- 這種方法不夠靈活,並且不能將核心應用程式邏輯進行動態拆分程式碼。
以上兩點中,第一點對我們的示例來說無疑是個問題,因為之前我們在 ./src/index.js
中也引入過 lodash
,這樣就在兩個 bundle 中造成重複引用。接著,我們通過使用 CommonsChunkPlugin
來移除重複的模組。
2. 防止重複(prevent duplication)
CommonsChunkPlugin
外掛可以將公共的依賴模組提取到已有的入口 chunk 中,或者提取到一個新生成的 chunk。讓我們使用這個外掛,將之前的示例中重複的 lodash
模組去除:
webpack.config.js
const path = require('path'); + const webpack = require('webpack'); const HTMLWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, plugins: [ new HTMLWebpackPlugin({ title: 'Code Splitting' - }) + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'common' // 指定公共 bundle 的名稱。 + }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };
這裡我們使用 CommonsChunkPlugin
之後,現在應該可以看出,index.bundle.js
中已經移除了重複的依賴模組。需要注意的是,CommonsChunkPlugin 外掛將 lodash
分離到單獨的 chunk,並且將其從 main bundle 中移除,減輕了大小。執行 npm run build
檢視效果:
Hash: 70a59f8d46ff12575481 Version: webpack 2.6.1 Time: 510ms Asset Size Chunks Chunk Names index.bundle.js 665 bytes 0 [emitted] index another.bundle.js 537 bytes 1 [emitted] another common.bundle.js 547 kB 2 [emitted] [big] common [0] ./~/lodash/lodash.js 540 kB {2} [built] [1] (webpack)/buildin/global.js 509 bytes {2} [built] [2] (webpack)/buildin/module.js 517 bytes {2} [built] [3] ./src/another-module.js 87 bytes {1} [built] [4] ./src/index.js 216 bytes {0} [built]
以下是由社群提供的,一些對於程式碼分離很有幫助的外掛和 loaders:
ExtractTextPlugin
: 用於將 CSS 從主應用程式中分離。bundle-loader
: 用於分離程式碼和延遲載入生成的 bundle。promise-loader
: 類似於bundle-loader
,但是使用的是 promises。
CommonsChunkPlugin
外掛還可以通過使用顯式的 vendor chunks 功能,從應用程式程式碼中分離 vendor 模組。
3. 動態匯入(dynamic imports)
當涉及到動態程式碼拆分時,webpack 提供了兩個類似的技術。對於動態匯入,第一種,也是優先選擇的方式是,使用符合 ECMAScript 提案 的 import()
語法。第二種,則是使用 webpack 特定的 require.ensure
。讓我們先嚐試使用第一種……
import()
呼叫使用會在內部用到 promises。如果在舊有版本瀏覽器中使用import()
,記得使用 一個 polyfill 庫(例如 es6-promise 或 promise-polyfill),來 shimPromise
。
在我們開始本節之前,先從配置中移除掉多餘的 entry
和 CommonsChunkPlugin
,因為接下來的演示中並不需要它們:
webpack.config.js
const path = require('path'); - const webpack = require('webpack'); const HTMLWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { + index: './src/index.js' - index: './src/index.js', - another: './src/another-module.js' }, plugins: [ new HTMLWebpackPlugin({ title: 'Code Splitting' - }), + }) - new webpack.optimize.CommonsChunkPlugin({ - name: 'common' // 指定公共 bundle 的名稱。 - }) ], output: { filename: '[name].bundle.js', + chunkFilename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };
注意,這裡使用了 chunkFilename
,它決定非入口 chunk 的名稱。想了解 chunkFilename
更多資訊,請檢視 output 相關文件。接著,更新我們的專案,移除掉那些現在不會用到的檔案:
project
webpack-demo |- package.json |- webpack.config.js |- /dist |- /src |- index.js - |- another-module.js |- /node_modules
現在,我們不再使用靜態匯入 lodash
,而是通過使用動態匯入來分離一個 chunk:
src/index.js
- import _ from 'lodash'; - - function component() { + function getComponent() { - var element = document.createElement('div'); - - // Lodash, now imported by this script - element.innerHTML = _.join(['Hello', 'webpack'], ' '); + return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => { + var element = document.createElement('div'); + + element.innerHTML = _.join(['Hello', 'webpack'], ' '); + + return element; + + }).catch(error => 'An error occurred while loading the component'); } - document.body.appendChild(component()); + getComponent().then(component => { + document.body.appendChild(component); + })
注意,在註釋中使用了 webpackChunkName
。這樣做會導致我們的 bundle 被命名為 lodash.bundle.js
,而不是 [id].bundle.js
。想了解更多關於 webpackChunkName
和其他可用選項,請檢視 import()
相關文件。讓我們執行 webpack,檢視 lodash
是否會分離到一個單獨的 bundle:
Hash: a27e5bf1dd73c675d5c9 Version: webpack 2.6.1 Time: 544ms Asset Size Chunks Chunk Names lodash.bundle.js 541 kB 0 [emitted] [big] lodash index.bundle.js 6.35 kB 1 [emitted] index [0] ./~/lodash/lodash.js 540 kB {0} [built] [1] ./src/index.js 377 bytes {1} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built]
如果你已經通過類似 babel 的前處理器(pre-processor)啟用 async
函式,請注意,你可以像下面那樣簡化程式碼,因為 import()
語句恰好會返回 promises:
src/index.js
- function getComponent() { + async function getComponent() { - return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => { - var element = document.createElement('div'); - - element.innerHTML = _.join(['Hello', 'webpack'], ' '); - - return element; - - }).catch(error => 'An error occurred while loading the component'); + var element = document.createElement('div'); + const _ = await import(/* webpackChunkName: "lodash" */ 'lodash'); + + element.innerHTML = _.join(['Hello', 'webpack'], ' '); + + return element; } getComponent().then(component => { document.body.appendChild(component); });
4. bundle 分析(bundle analysis)
如果我們以分離程式碼作為開始,那麼就以檢查模組作為結束,分析輸出結果是很有用處的。官方分析工具 是一個好的初始選擇。下面是一些社群支援(community-supported)的可選工具:
- webpack-chart: webpack 資料互動餅圖。
- webpack-visualizer: 視覺化並分析你的 bundle,檢查哪些模組佔用空間,哪些可能是重複使用的。
- webpack-bundle-analyzer: 一款分析 bundle 內容的外掛及 CLI 工具,以便捷的、互動式、可縮放的樹狀圖形式展現給使用者。