代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級,如果使用合理,會極大影響加載時間。
常用的代碼分離方法有三種:
這是迄今為止最簡單直觀的分離代碼的方式。不過,這種方式手動配置較多,并有一些隱患,我們將會解決這些問題。先來看看如何從 main bundle 中分離 another module(另一個模塊):
project
webpack-demo
|- package.json
|- package-lock.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');
module.exports = {
- entry: './src/index.js',
+ mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
這將生成如下構建結果:
...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms
正如前面提到的,這種方式存在一些隱患:
以上兩點中,第一點對我們的示例來說無疑是個問題,因為之前我們在 ?./src/index.js
? 中也引入過 lodash,這樣就在兩個 bundle 中造成重復引用。在下一章節(jié)會移除重復的模塊。
配置 dependOn option 選項,這樣可以在多個 chunk 之間共享模塊:
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
如果我們要在一個 HTML 頁面上使用多個入口時,還需設置 optimization.runtimeChunk: 'single',否則還會遇到這里所述的麻煩。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
構建結果如下:
...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms
由上可知,除了生成 shared.bundle.js,index.bundle.js 和 another.bundle.js 之外,還生成了一個 runtime.bundle.js 文件。
盡管可以在 webpack 中允許每個頁面使用多入口,應盡可能避免使用多入口的入口:entry: { page: ['./analytics', './app'] }。如此,在使用 async 腳本標簽時,會有更好的優(yōu)化以及一致的執(zhí)行順序。
SplitChunksPlugin 插件可以將公共的依賴模塊提取到已有的入口 chunk 中,或者提取到一個新生成的 chunk。讓我們使用這個插件,將之前的示例中重復的 lodash 模塊去除:
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
使用 optimization.splitChunks 配置選項之后,現(xiàn)在應該可以看出,index.bundle.js 和 another.bundle.js 中已經(jīng)移除了重復的依賴模塊。需要注意的是,插件將 lodash 分離到單獨的 chunk,并且將其從 main bundle 中移除,減輕了大小。執(zhí)行 npm run build 查看效果:
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms
以下是由社區(qū)提供,一些對于代碼分離很有幫助的 plugin 和 loader:
當涉及到動態(tài)代碼拆分時,webpack 提供了兩個類似的技術。第一種,也是推薦選擇的方式是,使用符合 ECMAScript 提案 的 import() 語法 來實現(xiàn)動態(tài)導入。第二種,則是 webpack 的遺留功能,使用 webpack 特定的 require.ensure。讓我們先嘗試使用第一種……
在我們開始之前,先從上述示例的配置中移除掉多余的 entry 和 optimization.splitChunks,因為接下來的演示中并不需要它們:
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
- another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- optimization: {
- splitChunks: {
- chunks: 'all',
- },
- },
};
我們將更新我們的項目,移除現(xiàn)在未使用的文件:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
現(xiàn)在,我們不再使用 statically import(靜態(tài)導入) lodash,而是通過 dynamic import(動態(tài)導入) 來分離出一個 chunk:
src/index.js
-import _ from 'lodash';
-
-function component() {
+function getComponent() {
- const element = document.createElement('div');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import('lodash')
+ .then(({ default: _ }) => {
+ const element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- return element;
+ return element;
+ })
+ .catch((error) => 'An error occurred while loading the component');
}
-document.body.appendChild(component());
+getComponent().then((component) => {
+ document.body.appendChild(component);
+});
我們之所以需要 default,是因為 webpack 4 在導入 CommonJS 模塊時,將不再解析為 module.exports 的值,而是為 CommonJS 模塊創(chuàng)建一個 artificial namespace 對象,更多有關背后原因的信息,請閱讀 webpack 4: import() and CommonJs。
讓我們執(zhí)行 webpack,查看 lodash 是否會分離到一個單獨的 bundle:
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
./src/index.js 434 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms
由于 import() 會返回一個 promise,因此它可以和 async 函數(shù)一起使用。下面是如何通過 async 函數(shù)簡化代碼:
src/index.js
-function getComponent() {
+async function getComponent() {
+ const element = document.createElement('div');
+ const { default: _ } = await import('lodash');
- return import('lodash')
- .then(({ default: _ }) => {
- const element = document.createElement('div');
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- return element;
- })
- .catch((error) => 'An error occurred while loading the component');
+ return element;
}
getComponent().then((component) => {
document.body.appendChild(component);
});
Webpack v4.6.0+ 增加了對預獲取和預加載的支持。
在聲明 import 時,使用下面這些內置指令,可以讓 webpack 輸出 "resource hint(資源提示)",來告知瀏覽器:
下面這個 prefetch 的簡單示例中,有一個 HomePage 組件,其內部渲染一個 LoginButton 組件,然后在點擊后按需加載 LoginModal 組件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
這會生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到頁面頭部,指示著瀏覽器在閑置時間預取 login-modal-chunk.js 文件。
與 prefetch 指令相比,preload 指令有許多不同之處:
下面這個簡單的 preload 示例中,有一個 Component,依賴于一個較大的 library,所以應該將其分離到一個獨立的 chunk 中。
我們假想這里的圖表組件 ChartComponent 組件需要依賴一個體積巨大的 ChartingLibrary 庫。它會在渲染時顯示一個 LoadingIndicator(加載進度條) 組件,然后立即按需導入 ChartingLibrary:
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
在頁面中使用 ChartComponent 時,在請求 ChartComponent.js 的同時,還會通過 <link rel="preload"> 請求 charting-library-chunk。假定 page-chunk 體積比 charting-library-chunk 更小,也更快地被加載完成,頁面此時就會顯示 LoadingIndicator(加載進度條) ,等到 charting-library-chunk 請求完成,LoadingIndicator 組件才消失。這將會使得加載時間能夠更短一點,因為只進行單次往返,而不是兩次往返。尤其是在高延遲環(huán)境下。
有時你需要自己控制預加載。例如,任何動態(tài)導入的預加載都可以通過異步腳本完成。這在流式服務器端渲染的情況下很有用。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// 在發(fā)生錯誤時做一些處理
// 例如,我們可以在網(wǎng)絡錯誤的情況下重試請求
});
如果在 webpack 開始加載該腳本之前腳本加載失?。ㄈ绻撃_本不在頁面上,webpack 只是創(chuàng)建一個 script 標簽來加載其代碼),則該 catch 處理程序將不會啟動,直到 chunkLoadTimeout 未通過。此行為可能是意料之外的。但這是可以解釋的 - webpack 不能拋出任何錯誤,因為 webpack 不知道那個腳本失敗了。Webpack 將在錯誤發(fā)生后立即將 onerror 處理腳本添加到 script 中。
為了避免上述問題,你可以添加自己的 onerror 處理腳本,將會在錯誤發(fā)生時移除該 script。
<script
src="https://example.com/dist/dynamicComponent.js" rel="external nofollow"
async
onerror="this.remove()"
></script>
在這種情況下,錯誤的 script 將被刪除。Webpack 將創(chuàng)建自己的 script,并且任何錯誤都將被處理而沒有任何超時。
一旦開始分離代碼,一件很有幫助的事情是,分析輸出結果來檢查模塊在何處結束。 官方分析工具 是一個不錯的開始。還有一些其他社區(qū)支持的可選項:
接下來,查看 延遲加載 來學習如何在實際一個真實應用程序中使用 import() 的具體示例,以及查看 緩存 來學習如何有效地分離代碼。
更多建議: