PJCHENder 未整理筆記

Webpack 學習筆記(Webpack Note)

2018-05-17

Webpack 學習筆記(Webpack Note)

1
2
3
4
5
$ npx webpack
$ npx webpack --config webpack.config.js # default is webpack.config.js
$ npx webpack -p # build in production
$ npx webpack-dev-server --open # run in dev-server
$ npm run build -- --mode production --display-used-exports # 顯示有用到的 exports

未閱讀

概念(Concept)

過去我們會將套件直接從 <head></head> 中透過 <script src=""> 載入,這種做法稱作「隱式依賴關係(implicit dependency)」,因為在 index.js 中沒有宣告它需要使用什麼套件,而是預期它會在全域載入。但這麼做有幾個缺點:

  • 沒辦法清楚看到程式碼依賴外部的函式庫。
  • 如果依賴不存在,或者引入順序錯誤,應用程序將無法正常運行。
  • 如果依賴被引入但是並沒有使用,瀏覽器將被迫下載無用代碼。

因此,我們可以用 webpack 來管理我們的程式碼。

Webpack 包含 4 個核心部分:

  • Entry
  • Output
  • Loaders
  • Plugins
  • (Mode)

Concepts @ Webpack Concept

Module, chunk, and bundle

Module: Discrete chunks of functionality that provide a smaller surface area than a full program. Well-written modules provide solid abstractions and encapsulation boundaries which make up a coherent design and clear purpose.

Chunk: This webpack-specific term is used internally to manage the bundling process. Bundles are composed out of chunks, of which there are several types (e.g. entry and child). Typically, chunks directly correspond with the output bundles however, there are some configurations that don’t yield a one-to-one relationship.

Bundle: Produced from a number of distinct modules, bundles contain the final versions of source files that have already undergone the loading and compilation process.

chunk 是在 webpack process 中的許多 modulesbundle 則是射出的 chunk 或許多 chunks。

注意:在 Webpack 4 中有部分 API 有變動,若下面文章有無法使用的,可以參考 webpack 4 announcement @ GitHub issues 查看更新項目。

最陽春的 webpack 設定檔(Entry & Output)

Webpack 的設定檔(configuration file)就是一個匯出為物件的 JS 檔案。

Configuration @ Webpack Concept

安裝

1
$ npm i webpack webpack-cli webpack-dev-server --save-dev

package.json

1
2
3
4
5
// package.json
"scripts": {
"build:dev": "webpack --config webpack.config.js",
"start": "webpack-dev-server --config webpack.config.js"
}

Entry and Output

在 webpack 的設定檔中,有兩個最基本的必填屬性,分別是 entryoutput

  • context 中,可以設定要讀取檔案的根資料夾(base directory),預設是使用設定檔放置的資料夾。
  • entry 中,我們可以放相對路徑。預設是 src/index.js
  • output
    • path 中,我們則是一點要放絕對路徑,在這裡我們可以使用 Node 提供的 path module 來取得當前資料夾的絕對路徑,慣例上會使用 builddist。預設是 dist/
    • [name] ,它會被 entry 中的 key 換掉
    • [chunkhash] 則可讓瀏覽器知道是否需要重新載入檔案
    • filename 在慣例上則是會使用 bundle.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js
const path = require('path');

const config = {
mode: 'development',
context: path.resolve(__dirname, 'src'),
entry: './index.js', // 若沒設定 context 則要寫 `./src/index.js`
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'foo.bundle.js',
},
};

module.exports = config;

打包成多支 output

設定兩個 entry,會根據 entry 的 Key 產生兩隻檔案:app.bundle.jsprint.bundle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js

const path = require('path');

module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
// publicPath: path.resolve(__dirname, 'dist')
},
};

打包

1
2
3
4
5
# 使用 npm
$ npm run start

# 使用 npx
$ npx webpack

Loader

Loader @ Webpack Concept

在 webpack 中可以套用許多不同的 modules,最常用的 modules 是各種 loadersloader 的功用就是告訴 webpack 該如何處理匯入的檔案,通常是 Javascript(例如,babel-loader),但 webpack 不限於處理 Javascript,其他資源檔像是 Sass(sass-loader),圖片等也都可以處理,只要提供對應的 loader。

同時 loader 也可以串連使用,概念上類似於 Linux 中的 pipe,A Loader 處理完之後把結果交給 B Loader 繼續轉換,以此類推,但要特別留意,串連的時候則是以反方向執行(由右至左)

⚠️ Loader 執行的順序是從陣列最後一個元素開始,往第一個元素的方向執行。

Loader - CSS(單純載入 CSS)

keywords: style-loader, css-loader

安裝

1
$ npm i css-loader style-loader --save-dev
  • css-loader 是讓我們可以在 JavaScript 中使用 import 載入 CSS 檔
  • style-loader 是把 CSS 樣式載入 HTML 中

使用

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
module: {
rules: [
{
test: /\.css$/,
// loader 的順序很重要,會先執行 css-loader 接著才是 style-loader
use: ['style-loader', 'css-loader'],
},
];
}

Loader - SCSS / SASS

keywords: style-loader, css-loader, sass-loader, node-sass
  • 不需要使用 SASS:只時要打包 CSS 檔案的話,只需透過 style-loadercss-loader,做法可參考 Asset Management - Loading CSS @ Webpack Guide,如此 webpack 會在該頁面將特定的 CSS 灌入 <head></head> 內。
  • 需要使用 SASS :則須再透過 sass-loadernode-sass
  • 需要安裝 file-loader 並進行相關設定後才能處理圖片資源。
  • 若希望抽成 css 檔,須透過 extract-text-webpack-plugin ,但目前 webpack 4 仍不支援。

安裝

1
npm install css-loader style-loader sass-loader node-sass --save-dev
  • sass-loader 讓我們可以在 JS 檔中使用 import 匯入 SCSS 檔

設定

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

module: {
rules: [
{
test: /\.scss$/,
// loader 的順序很重要,一定要先從 sass-loader 開始,接著 css-loader,最後 style-loader
use: ['style-loader', 'css-loader', 'sass-loader'],
},
];
}

使用

1
2
3
// ./src/index.js

import './styles/style.scss';

Issues

Loader - Babel

babel-loader @ webpack

安裝 babel

1
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader

在這裡我們要先來學習使用 Babel,它可以用來幫我們將 ES6 或更之後的語法轉譯成 ES5 或其他版本的 JS 語法,其中包含了三個主要模組:

  • babel-loader:用來告訴 babel 如何和 webpack 合作。
  • babel-core:知道如何載入程式碼、解析和輸出檔案(但不包含編譯)。
  • babel-preset-env:讓 babel 知道如何將不同版本的 ES 語法進行轉譯。

建立 babel 設定檔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.js
module: {
rules: [
// babel-loader
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
// targets: "> 0.25%, not dead"
targets: {
chrome: '69',
},
},
],
], // End of presets
plugins: ['@babel/plugin-transform-runtime'],
}, // End of options
},
}, // End of Babel
];
}

⚠️ 關於 babel 的設定除了寫在 webpack 中 babel-loader 的 options 內,也可以用額外定義一支 .babelrc 的方式設定。

更多設定參數:@babel/preset-env @ Babel Docs

錯誤處理:@babel/polyfill

在某些需要支援瀏覽器兼容性的情況下(例如使用,async 但又要支援舊版瀏覽器),Babel 需要使用特殊的 runtime 時,需要額外安裝 @babel/polyfill,此時需要在 babel-loader 中搭配 useBuiltIns 這個 options 使用,才不會把整包 polyfill 都載入

:exclamation: @babel/polyfill 要安裝在 Dependencies 而非 DevDependencies。

@babel/polyfill @ Babel Docs

Loader - 處理圖片

keywords: file-loader, url-loader

相關 loader

  • file-loader:將圖片打包到 output 中,並處理檔名。
  • url-loader:做的事情和 file-loader 很接近,但是當圖片檔案大小,小於設定值時,可以轉換成 DataURL。

安裝

1
npm install --save-dev url-loader

設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js
module: {
rules: [
// url loader (for image)
{
test: /\.(jpe?g|png|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 40000 /* 小於 40kB 的圖片轉成 base64 */,
},
},
],
},
];
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ./src/image-viewer.js
import midImgUrl from './../assets/mid.jpeg';
import minImgUrl from './../assets/min.jpeg';

// midImg 會是被壓縮過的檔案名稱
const midImg = document.createElement('img');
midImg.src = midImgUrl;
document.body.appendChild(midImg);

// minImg 是被注入在 bundle.js 中,可以直接使用
const minImg = document.createElement('img');
minImg.src = minImgUrl;
document.body.appendChild(minImg);

export { midImg, minImg };

Loader - 處理字型

keywords: file-loader

要處理字型可以使用 file-loader

設定

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader'],
},
],
},
};

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
/* main.scss */

@font-face {
font-family: 'Roboto';
src: url('./../fonts/Roboto-Regular.ttf') format('ttf');
font-weight: normal;
font-style: normal;
}

.hello {
color: tomato;
font-family: 'Roboto';
}

Loader - 處理資料(JSON, CSV, XML)

keywords: csv-loader, xml-loader

安裝

1
npm install --save-dev csv-loader xml-loader

Webpack 內建就可以處理 JSON 格式,可以不用安裝額外的 loader。

Plugin(外掛)

由於插件可以傳入參數/選項,你必須在 webpack 配置中,向 plugins 屬性傳入 new 實例。

Plugin @ Webpack Concept

Plugin - 清除 dist 資料夾

keywords: clean-webpack-plugin

透過 clean-webpack-plugin 可以讓 webpack 每次打包前都清除特定資料夾。

1
$ npm install clean-webpack-plugin --save-dev

webpack.config.js

1
2
3
4
5
6
7
8
9
10
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
// ...
plugins: [
new CleanWebpackPlugin(),
// ...
],
};

HtmlWebpackPlugin - 操作與注入 HTML

keywords: html-webpack-plugin

透過 HtmlWebpackPlugin 可以產生 HTML5 的檔案,並且把透過 webpack 打包好的檔案透過 <script> 放在 <body> 內。

安裝

1
npm install html-webpack-plugin --save-dev

設定

HtmlWebpackPlugin 會自動拿 output.publicPath 的路徑,灌到 template 的 <script> 中,如果不給的話,當網址切換時可能會無法順利載到 bundle 過的檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
// ...
plugins: [
/* 只是要在 HTML 添加打包好的 webpack 檔案 */
// new HtmlWebpackPlugin(),

/* 或者也可以定義要使用的樣版,或其他更多參數 */
new HtmlWebpackPlugin({
title: 'Webpack 4 - Output Management with HtmlWebpackPlugin',
template: './index.html', // 以 index.html 這支檔案當作模版注入 html
}),
],
};

MiniCssExtractPlugin - 獨立的 CSS 檔

Webpack 4 以前使使用 extract-text-webpack-plugin;Webpack 4 之後則是使用 mini-css-extract-plugin

安裝

1
npm install --save-dev mini-css-extract-plugin

設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ...

module: {
rules: [
// 處理 SASS 檔案
{
test: /\.(sa|sc|c)ss$/,
// 如果希望開發環境就打包出 CSS 檔案,可以直接使用 MiniCssExtractPlugin.loader,但可能會沒有 HMR
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
// 'postcss-loader',
'sass-loader'
]
}
]
},
plugins: [
// 將樣式輸出成 css 檔
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
];

MiniCssExtractPlugin @ Github

開發環境(Development)

這部分的內容建議只使用在開發環境下,避免在上線模式使用

Development @ Webpack Guides

使用 Source map

透過 source map 可以在程式碼出錯時,方便我們找到錯誤的程式碼在哪一隻檔案:

1
2
3
4
5
6
// webpack.config.js

module.exports = {
devtool: 'inline-source-map',
// ...
};

使用觀察模式(Using Watch Mode)

透過 watch 指令可以讓 webpack 監控所有依賴圖(dependency graph)下的檔案有無變更,如果其中有檔案變更了,程式碼就會被重新編譯,因此不需要手動執行 build 指令。唯一的缺點是,為了看到修改後的實際效果,你需要刷新瀏覽器

透過 watch mode 仍需要手動重新整理瀏覽器。

1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"name": "development",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"watch": "webpack --watch",
"build": "webpack"
}
}

使用 webpack-dev-server

透過 webpack-dev-server 可以幫助我們不用每更改一次檔案就執行一次 npm run build 去打包檔案,而且它會自動幫我們重新整理瀏覽器

webpack-dev-server @ webpack > configuration

安裝

1
npm install --save-dev webpack-dev-server

設定

webpack.config.js

告訴 webpack-dev-server 從 ./dist 資料夾提供檔案到 localhost:8080

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
devServer: {
contentBase: path.join(__dirname, 'dist'), // 網站內容從哪來,預設會使用 '/'
publicPath: '/assets/', // 打包好的檔案將在這個路由下取用
compress: false, // 使用 gzip 壓縮
port: 8080,
index: 'index.html',
hot: true, // 使用 HMR
host: '0.0.0.0', // 預設是 localhost,設定則可讓外網存取
open: true, // 打開瀏覽器
},
// ...
};

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* --open 於瀏覽器打開 webpack-dev-server
* --inline: 瀏覽器自動重新整理(live reload)
* --hot: 啟動 HotModuleReplacement
**/

{
"scripts": {
// ...
"start": "webpack-dev-server --open",
// "dev": "webpack-dev-server --inline --hot",
}
}

DevServer @ Webpack Configuration

Hot Module Replacement (HMR)

**模組熱替換(Hot Module Replacement, HMR)**的功能可以讓 APP 在開發的過程中,不需要全部重新載入就可以改變、新增或移除模組。HMR  不適用於上線環境,這意味著它應當只在開發環境使用

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
const webpack = require('webpack');

module.exports = {
// ...
devServer: {
contentBase: './dist',
hot: true,
},
plugins: [
new webpack.HashedModuleIdsPlugin(), // 避免所有檔案的 hash 都改變,適合用在 production
new webpack.NamedModulesPlugin(), // 和上面那行功能相似,但適合用在 development
new webpack.HotModuleReplacementPlugin(),
],
};

webpack.HashedModuleIdsPlugin() 的使用可以參考 Caching: module-identifies @ Webpack

要注意的是這個做會更新有修改的檔案,但若該檔案內容已經被其他檔案快取起來,則需要該內容也需要被重新更新,舉例來說,可以透過 module.hot.accept() 來定義幫某檔案被更新時,要連動更新的內容:

1
2
3
4
5
6
7
8
9
10
11
import printMe from './print.js'

function component () {
...
}

if (module.hot) {
module.hot.accept('./print.js', function () {
// 當 print.js 改變時,在這裡做些什麼...
})
}

HMR with stylesheet

直接在 config 中使用 style-loadercss-loader ,並把樣式載入到 index.js 中,HMR 即會在 CSS 檔案有變更時,重 build 和重新整理瀏覽器。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js

module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};

index.js

1
2
// ./src/index.js
import './style.css';

正式環境(Production Mode)

拆檔

由於開發環境和正式環境的目的不同,因此一般會建議針對不同的環境撰寫不同的 webpack 設定檔(webpack.dev.js, webpack.prod.js)。對於兩個環境通用的設定檔,我們會額外建立一支 webpack.common.js ,並且透過 webpack-merge 這個套件來將這些 webpack 設定檔合併。

1
$ npm install --save-dev webpack-merge
1
2
3
4
5
6
7
8
9
10
11
// webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
},
});

參考 Production Setup @ Webpack -Guide

使用 source map

即使在正式環境仍建議放入 source-map,它可以方便我們除錯和進行測試。但在正式環境應避免使用 inline-***eval-*** 這種,因為它們會增加打包後檔案的大小。

1
2
3
4
5
6
7
8
9
10
11
12
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');

module.exports = merge(common, {
devtool: 'source-map',
plugins: [
new UglifyJSPlugin({
sourceMap: true,
}),
],
});

source mapping @ Webpack Guide - Production

定義環境(Specify Environment)

許多套件都會根據 process.env.NODE_ENV 來對打包的結果進行調整(例如添加或移除註解),透過 webpack.DefinePlugin() 我們可以定義環境變數:

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.prod.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
});

特別需要留意的是在設定檔中(webpack.config.js),process.env.NODE_ENV 還不會被設定成 production ,因此如果再設定檔中使用 process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js' 這樣的設定是無效的。

Specify the environment @ Webpack Guide - Production

使用指令

除了在設定檔中設定 mode: production 以及上面所說的設定外,也可以用指令的方式,但還是建議放在設定檔比較清楚:

1
2
3
4
$ npx webpack -p        # 等同於 --optimize-minimize --define

$ npx webpack --optimize-minimize # 會自動套用 UglifyJSPlugin
$ npx webpack --define process.env.NODE_ENV="'production'" # 等同於使用 DefinePlugin

代碼分離(Code Splitting)

傳統的網頁會把所有的 JavaScript 檔案打包成一支容量較大 bundle.js,但這樣對效能是不好的,透過 code splitting 則可以把 JavaScript 檔案切分成許多小包(chunk),根據需要傳送最好的程式碼來提升效能。

許多的 Apps 將所有的程式碼都放到單一個 bundle 檔案,並在網頁第一次載入時就執行它們,這個檔案不只支援一開始的路由(頁面),並且支援了所有路由(頁面)中的所有互動功能-甚至是那些不會被造訪到的頁面。

有幾個可以達到代碼分離的效果:

  • Vendor Splitting:將外部函式的程式碼(例如,React, lodash)從 App 的程式碼中移除,避免當函式或 App 的程式碼有變動時,使用者需要全部重新下載一次程式碼。每個 App 都應該要做到這個部分
  • Prevent Duplication: 使用 SplitChunksPlugin 中的 optimization.splitChunks 移除重複的代碼並切塊(split chunks)。
  • Multiple Entry Points: 手動在 entry 中設定。適合使用的情況是在,沒有套用 client side routing;或者是部分使用 server side routing、其他則部分則是 SPA 的情形
  • Dynamic Imports: 透過在模組中呼叫 inline function 來動態的使用 import() 達到,最適合使用在 SPA

Entry Points

這麼做可能會重複載入相同的模組,需進一步使用 SplitChunksPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js
const path = require('path');

// 如果 index.js 和 another_module.js 有 import 相同的模組時,會重複載入
module.exports = {
entry: {
index: path.join(__dirname, 'src', 'index.js'),
another: path.join(__dirname, 'src', 'another_modules.js'),
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

避免重複: SplitChunksPlugin

原本來說,chunks 會在 webpack 內部的 graph 透過 parent-child 關係連結在一起,而 SplitChunksPlugin 則是要避免它們彼此重複相依,進一步達到優化。

在 Webpack v4 之後,移除了 CommonsChunkPlugin,採用 optimization.splitChunks

使用 SplitChunksPlugin 可以移除重複的代碼並切塊(通常有多個 entry points 時),或者自己定義要把哪些檔案拆成 chunks(如下所示):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const path = require('path');

module.exports = {
entry: {
index: './src/index.js',
another: './src/another_module.js',
},
optimization: {
// 在這裡使用 SplitChunksPlugin
splitChunks: {
cacheGroups: {
// 把所有 node_modules 內的程式碼打包成一支 vendors.bundle.js
vendors: {
test: /[\\/]node_modules[\\/]/i,
name: 'vendors',
chunks: 'all',
},
},
},
// 把 webpack runtime 也打包成一支 runtime.bundle.js
runtimeChunk: {
name: 'runtime',
},
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

SplitChunksPlugin @ Webpack > Plugin

動態載入(Dynamic Import)

使用 import() 可以動態載入模組,import( ) 背後是透過 Promise 運作,最基本的寫法如下:

1
2
3
4
5
6
7
8
9
const getUserModule = () => import('./common/usersAPI');

const btn = document.getElementById('btn');

btn.addEventListener('click', () => {
getUserModule().then(({ getUsers }) => {
getUsers().then((json) => console.log(json));
});
});

如果想要控制 chunk 的名稱,則可以使用 /* webpackChunkName: "name_here" */,例如:

1
2
3
// 載入的 chunk 名稱會是 usersAPI
const getUserModule = () =>
import(/* webpackChunkName: "usersAPI" */ './common/usersAPI');

以動態載入 lodash 為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.js
function getComponent() {
return import(/* webpackChunkName: "lodash" */ 'lodash')
.then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
})
.catch((error) =>
console.log('An error occurred while loading component.', error)
);
}

getComponent().then((component) => {
document.body.appendChild(component);
});

因此也可以用 async await

1
2
3
4
5
6
7
8
9
10
11
12
async function getComponent() {
var element = document.createElement('div');
const { default: _ } = await import(
/* webpackChunkName: "lodash" */ 'lodash'
);
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}

getComponent().then((component) => {
document.body.appendChild(component);
});

在 React 中也可以使用動態載入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react';

export default class App extends Component {
constructor() {
super();

this.state = {
Form: undefined,
};
}

render() {
const { Form } = this.state;

return (
<div className="app">
{Form ? <Form /> : <button onClick={this.showForm}>Show form</button>}
</div>
);
}

// 但按鈕被點擊的時候才會動態載入
showForm = async () => {
const { default: Form } = await import('./Form');
this.setState({ Form });
};
}

提前請求(prefetch)或載入(preload)模組

在 webpack 4.6.0+ 支援:

  • prefetch: 資源可能在稍後的瀏覽(navigation)中會用到
  • preload: 資源可能會在當前的瀏覽(navigation)中用到

prefetching/preloading modules @ Webpack Guide - Splitting Code

其他

分析 bundle 中的內容

Stats Data @ Webpack

除了透過 webpack 的指令,可以檢視這個 bundle 中各個套件、模組的詳細資訊,並可以用檢視檔案大小:

1
$ webpack --profile --json > compilation-stats.json
  • webpack:更可以透過 webpack-bundle-analyzer 這個套件讓檢視 bundle 包裡面各模組的檔案大小。
  • source-map:另外透過 source-map-explorer 這個 npm 套件,只要有提供 source map,同樣可以分析 bundle 的內容。

Tree Shaking

Tree shaking 這個術語,通常用於描述移除 JavaScript 上下文中的未使用到的程式碼(dead-code)。在 Webpack 4 中透過 package.jsonsideEffects 屬性作為標記,向 compiler 提供提示,說明哪些檔案是 pure 的,因此可以安全地刪除這些未使用的部分。

  • 在 production mode 的時候,webpack 會自動移除沒用到的 dead code。
  • 若有使用 babel,記得不要讓 babel 編譯 modules 部分

參考筆記 [Reduce JavaScript Payloads](/Users/pjchen/Projects/Note/source/_posts/WebPack/[Webpack] Reduce JavaScript Payloads with Tree Shaking.md)。

拆檔載入 - System.import

為了要達到 codeSplitting 的效果,我們要用到 ES6 中的 System.import('module') 這個方法,當滑鼠點擊的時候,它會以非同步的方式載入特定的 module(在這裡是 image-viewer.js)以及和這個 module 相依的其他 modules。

透過 System.import 它會回給我們一個 Promise

1
2
3
4
5
6
7
8
9
10
11
// ./src/index.js

const button = document.createElement('button');
button.innerText = 'Click Me';
button.onclick = () => {
// 點擊時才載入 image-viewer.js
// System.import 會回傳一個 Promise 物件
System.import('./image-viewer').then((module) => {
console.log('module', module);
});
};

發佈

設定

webpack -p 時,webpack 會把所有的 js 檔壓縮。

1
2
3
4
5
6
7
8
// package.json

"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && webpack",
"dev": "npm run clean && webpack-dev-server --inline --hot",
"deploy": "npm run clean && webpack -p"
}

Resolve

alias

當我們設定 alias 後:

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
'@': path.resolve(__dirname, 'src'),
},
},
};

import 模組時可以直接使用別名載入:

1
2
3
4
5
// 不必寫:import Utility from '../../utilities/utility';
import Utility from 'Utilities/utility';

// 不必寫:import rootReducer from '../reducers';
import rootReducer from '@/reducers';

extension

1
2
3
4
5
6
module.exports = {
//...
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json'], // default
},
};

import 模組時可以不用寫副檔名:

1
import File from '../path/to/file';

Resolve @ Webpack

參考資料

掃描二維條碼,分享此文章