程式碼拆分是 webpack 最引人注目的功能之一。此功能讓你可以將程式碼拆分為各種套件,然後可以依需求或平行載入。它可用於產生較小的套件並控制資源載入優先順序,如果使用得當,可能會對載入時間產生重大影響。
有三個可用的程式碼拆分一般方法
entry
設定手動拆分程式碼。SplitChunksPlugin
來刪除重複項並拆分區塊。這是目前為止最簡單、最直觀的分割程式碼方式。不過,它比較手動,而且有一些我們會探討的陷阱。讓我們看看我們如何從主程式碼分割另一個模組
專案
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
如前所述,這種做法有一些陷阱
這兩點中的第一點對我們的範例來說肯定是一個問題,因為 lodash
也導入到 ./src/index.js
中,因此會在兩個程式碼中重複。讓我們在下一個區段中移除這個重複。
dependOn
選項允許在區塊之間共用模組
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
腳本標籤時,這樣可以獲得更好的最佳化和一致的執行順序。
SplitChunksPlugin
允許我們將共用相依項抽取到現有的進入點區塊或全新的區塊。讓我們用它來消除前一個範例中的 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
組態選項後,我們現在應該會看到重複的相依項從 index.bundle.js
和 another.bundle.js
中移除。此外掛程式應該會注意到我們已將 lodash
分離到一個單獨的區塊,並從我們的 main bundle 中移除無用負載。讓我們執行 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
以下是社群提供的其他一些有用的外掛程式和載入器,用於分割程式碼
mini-css-extract-plugin
:用於將 CSS 從主應用程式中分割出來。在動態程式碼分割方面,webpack 支援兩種類似的技術。第一種也是推薦的方法是使用 import()
語法,它符合 ECMAScript 動態匯入提案。第二種是 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',
- },
- },
};
我們也會更新專案以移除現在未使用的檔案
專案
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
現在,我們不會靜態匯入 lodash
,而是使用動態匯入來分隔一個區塊
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 模組建立一個人工命名空間物件。如需有關此原因的更多資訊,請閱讀 webpack 4: import() and CommonJs。
讓我們執行 webpack,看看 lodash
是否已分隔到一個獨立的套件
...
[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
函式。以下是簡化程式碼的方法
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 4.6.0+ 新增支援預先擷取和預先載入。
在宣告匯入時使用這些內嵌指令,可讓 webpack 輸出「資源提示」,告訴瀏覽器以下資訊:
一個範例是有一個 HomePage
元件,它會呈現一個 LoginButton
元件,然後在被按一下後,依需求載入一個 LoginModal
元件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
這會導致 <link rel="prefetch" href="login-modal-chunk.js">
附加在頁面標頭中,這會指示瀏覽器在閒置時間預先擷取 login-modal-chunk.js
檔案。
與預先擷取相比,預先載入指令有許多差異
一個範例是有一個 Component
,它總是依賴一個應在個別區塊中的大型函式庫。
讓我們想像一個元件 ChartComponent
,它需要一個龐大的 ChartingLibrary
。它在呈現時會顯示一個 LoadingIndicator
,並立即依需求匯入 ChartingLibrary
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
當要求一個使用 ChartComponent
的頁面時,也會透過 <link rel="preload">
要求 charting-library-chunk
。假設 page-chunk
較小且完成得較快,頁面會顯示一個 LoadingIndicator
,直到已經要求的 charting-library-chunk
完成。由於它只需要一個回合行程,而不是兩個,因此這會稍微縮短載入時間。特別是在延遲較高的環境中。
有時您需要自行控制預載。例如,任何動態匯入的預載都可以透過非同步指令碼完成。這在串流伺服器端渲染的情況下會很有用。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// Do something with the error.
// For example, we can retry the request in case of any net error
});
如果指令碼載入在 webpack 自行載入該指令碼之前失敗(如果該指令碼不在頁面上,webpack 會建立一個指令碼標籤來載入其程式碼),則該捕捉處理常式不會啟動,直到 chunkLoadTimeout 未傳遞。這種行為可能會出乎意料。但這是可以解釋的——webpack 無法擲出任何錯誤,因為 webpack 不知道該指令碼已失敗。錯誤發生後,webpack 會立即將 onerror 處理常式新增到指令碼。
若要防止此類問題,您可以新增自己的 onerror 處理常式,在發生任何錯誤時移除指令碼
<script
src="https://example.com/dist/dynamicComponent.js"
async
onerror="this.remove()"
></script>
在這種情況下,會移除有錯誤的指令碼。webpack 將建立自己的指令碼,並且會在沒有任何逾時的情況下處理任何錯誤。
一旦您開始分割程式碼,分析輸出以查看模組在哪裡結束會很有用。官方分析工具 是個不錯的起點。還有一些其他社群支援的選項
參閱 延遲載入 以取得更具體的範例,瞭解如何在實際應用程式中使用 import()
,以及 快取 以瞭解如何更有效地分割程式碼。