跳至主要内容

[note] Webpack 學習筆記

此篇為各筆記之整理,非原創內容,內容主要參考自 webpack 官方網站:Webpack Guides

$ npm install webpack webpack-cli --save-dev
$ npx webpack # 預設會以 ./src/index.js 為 entry;以 ./dist/main.js 為 output。

# 預設就會吃 webpack.config.js,因此除非檔名有不同,否則可以省略
$ npx webpack --config 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 包含幾個核心部分:

  • Entry
  • Output
  • Loaders
  • Plugins
  • Mode
  • Browser Compatibility

Concepts @ Webpack Concept

Entry

告訴 webpack 要使用的進入點是哪隻檔案,預設會使用 ./src/index.js

Output

要把 bundle 過的檔案放在哪裡及其檔名,預設會使用 /dist/main.js

Loaders(modules)

雖然 webpack 本來就可以了解 JavaScript 和 JSON 檔,但實際的應用程式中仍包含許多其他類型的檔案(例如,圖檔),這時候會需要透過 loader 來處理 JS / JSON 以外的檔案類型。在設定時,是透過 module.rules 的屬性來設定。

// https://webpack.js.org/concepts/
module.exports = {
// ...
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
warning

特別留意使用 regex 來選擇要套用該 loader 的檔案類型是,不要加上單引號或雙引號,也就是使用 /\.txt$/,而不是 '/\.txt$/'"/\.txt$/"

Plugins

Loaders 是用來處理特定類型的檔案,而 plugins 則可以用來執行某些功能更廣泛的任務,像是打包最佳化(bundle optimization)、資源管理(asset management)和注入環境變數。

// https://webpack.js.org/concepts/
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
提示

由於你可以根據需求在設定檔的不同位置重複使用該 plugin,因此在使用 plugin 是需使用 new 來確保每個 plugin 是獨立的 instance。

Mode

預設值是 production,還可以是 developmentnone

透過這個設定,webpack 會自動啟用最佳化的策略。

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 的設定檔(configuration file)就是一個匯出為物件的 JS 檔案。

Configuration @ Webpack Concept

安裝

$ npm install webpack webpack-cli webpack-dev-server --save-dev

可以在 package.json 中加入設定檔:

// package.json
"scripts": {
"build:dev": "webpack --config webpack.config.js",
"start": "webpack-dev-server --config webpack.config.js"
}

使用

# 使用 npm
$ npm run start

# 使用 npx
$ npx webpack

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
// 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;

多個 entry 和多個 output bundles

Output Management @ Webpack > Guides

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

// 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')
},
};

清除 dist 資料夾

在 Webpack 5 中,只需要 output 項目中加上 clean: true 即可,不需要在額外安裝 plugin:

diff --git a/webpack.config.js b/webpack.config.js
index 3a50e6d..0ac5f88 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -10,6 +10,7 @@ const config = {
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
+ clean: true,
},
devServer: {
contentBase: './dist',

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 繼續轉換,以此類推,但要特別留意,串連的時候則是以反方向執行(由右至左)

warning

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

Loading CSS(單純載入 CSS)

如果希望能在 JavaScript 中 import CSS 檔的話,需要安裝與設定對應的 loader。

安裝

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

使用

// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/i, // 記得不用加上雙引號
// loader 的順序很重要,會先執行 css-loader 接著才是 style-loader
use: [
'style-loader', // 是把 CSS 樣式載入 HTML 中
'css-loader', // 在 JavaScript 中可以 `import` 載入 CSS 檔
],
},
],
},
};

效果

從 inspector 中,我們可以看到所撰寫的 CSS 樣式會被注入到 index.html<head> 中:

webpack css loader

Loading SCSS / 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 仍不支援。

安裝

# 安裝和 sass/scss 有關的 loader
npm install --save-dev sass-loader node-sass

# 有需要的話也需要搭配 css-loader 和 style-loader
npm install --save-dev css-loader styled-loader
  • sass-loader 讓我們可以在 JS 檔中使用 import 匯入 SCSS 檔
  • 可能會需要搭配 style-loadermini-css-extract-plugin
  • 一定要先執行 sass-loader 才能執行 css-loader,最後才是 styled-loader(或 mini-css-extract-plugin)。由於 loader 載入的順序是逆轉的,因此 sass-loader 會放在陣列的最後面。

設定

// webpack.config.js

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

使用

// ./src/index.js

import './styles/style.scss';

issues

使用 PostCSS

PostCSS 可以幫我們處理瀏覽器支援度的問題,針對特定的屬性加上必要的 prefix:

安裝

npm install --save-dev postcss-loader postcss-preset-env

設定 PostCSS

使用 postcss-preset-env

// postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')({
browsers: 'last 2 versions',
}),
],
};

設定 Webpack

// webpack.config.js

module: {
rules: [
{
test: /\.(scss|css)$/i,
// loader 的順序是重要的,postcss-loader 需要在 sass-loader 之後,css-loader 前
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
},
];
}

效果

打包後的 CSS 則會產生所需的 prefix:

.hello {
+ display: -webkit-box;
+ display: -ms-flexbox;
display: flex;
}

Loader - Babel

安裝 babel

# 讓 babel 能夠運作
npm install --save-dev @babel/core @babel/preset-env

# webpack loader for babel
npm install --save-dev babel-loader

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

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

建立 babel 設定檔

在專案根目錄新增 .babelrc,並且放入 babel 的設定:

// .babelrc
{
"presets": ["@babel/preset-env"]
}
備註

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

使用 babel-loader

// webpack.config.js
module: {
rules: [
// babel-loader
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
];
}

錯誤處理:@babel/polyfill

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

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

@babel/polyfill @ Babel Docs

Loading Images- 處理圖片

loading images @ webpack guides

在 Webpack 5 中,使用內建的 Asset Modules 就可以載入圖片。

設定

// webpack.config.js

module: {
// ...
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
];
}

使用

當我們在 JavaScript 中 import 圖檔後,這個圖片就會變處理並添加到 output 的資料夾中,而 WebpackLogo 這個變數會變成對應可以被使用的 URL:

// index.js
import WebpackLogo from './webpack-logo.svg';

CSS 的部分,有使用 css-loader 的話,在 CSS 中也可以直接載入圖檔使用,webpack 會用最終圖檔的 URL 替換掉 url('./icon.jpeg')

/* style.css */
.hello {
color: red;
background-image: url('./icon.jpeg');
background-repeat: no-repeat;
}

HTML 的部分,有使用 html-loader 一樣也可以直接使用圖檔,webpack 同樣會用最終圖檔的 URL 替換掉 src="./webpack-logo.svg" 的部分:

<img src="./webpack-log.svg" />

Loading Fonts - 處理字型

Loading Fonts @ webpack

在 Webpack 5 中要處理字型也可以直接使用內建的 Asset Modules。

設定

// webpack.config.js

module: {
// ...
rules: [
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
];
}

使用

下載字體後,透過 @font-face 設定字體:

diff --git a/src/style.css b/src/style.css
index aa457c9..727a76f 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,5 +1,20 @@
+@font-face {
+ font-family: 'Poppins';
+ src: url('./Poppins-Regular.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Poppins-Bold';
+ src: url('./Poppins-Bold.ttf') format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+
.hello {
color: red;
background-image: url('./icon.jpeg');
background-repeat: no-repeat;
+ font-family: 'Poppins';
}

Loading Data - 處理資料 Part 1(JSON, CSV, XML)

Loading data @ webpack

keywords: csv-loader, xml-loader

Webpack 內建就可以處理 JSON 格式,可以不用安裝額外的 loader,直接 import Data from './data.json' 即可;但如果要匯入的是 CSV 或 XML 的話則需要搭配 csv-loaderxml-loader

透過這種方式,除非是即時的資料,否則就不需要在 runtime 時才透過 AJAX 取得資料,而是可以在 build time 時就把資料載入。

安裝

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

設定

// webpack.config.js

module: {
// ...
rules: [
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
];
}

使用

// index.js
import Data from './data.xml';
import Notes from './data.csv';

Loading Data - 處理資料 Part 2(TOML, YAML, JSON5)

安裝

npm install toml yamljs json5 --save-dev

設定

diff --git a/webpack.config.js b/webpack.config.js
index 695b501..227d4b3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,4 +1,7 @@
const path = require('path');
+const toml = require('toml');
+const yaml = require('yamljs');
+const json5 = require('json5');

const config = {
mode: 'development',
@@ -32,6 +35,27 @@ const config = {
test: /\.xml$/i,
use: ['xml-loader'],
},
+ {
+ test: /\.toml$/i,
+ type: 'json',
+ parser: {
+ parse: toml.parse,
+ },
+ },
+ {
+ test: /\.yaml$/i,
+ type: 'json',
+ parser: {
+ parse: yaml.parse,
+ },
+ },
+ {
+ test: /\.json5$/i,
+ type: 'json',
+ parser: {
+ parse: json5.parse,
+ },
+ },
],
},
};

使用

// index.js
import toml from './data.toml';
import yaml from './data.yaml';
import json5 from './data.json5';

Plugin(外掛)

同一個 plugin 可以被重複使用,一次在使用 plugin 時,需要在前方加上 new,以確保每次使用的都是獨立的 instance。

Plugin @ Webpack Concept

HtmlWebpackPlugin:將 CSS 與 JS 注入 HTML 中

keywords: html-webpack-plugin

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

此外,如果你有使用 mini-css-extract-plugin 來產生 CSS 檔,HtmlWebpackPlugin 也會將 CSS 檔以 <link> 注入到 HTML 的 <head> 內。

安裝

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

設定

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

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

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

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

HtmlWebpackPlugin 還有其他可用的參數可以參考 html-webpack-plugin > options 的說明。

MiniCssExtractPlugin:將 stylesheet 拆成獨立的 CSS 檔

安裝

npm install --save-dev mini-css-extract-plugin
備註

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

設定

建議在 production 的 mode 使用 mini-css-extract-plugin,來產生多隻 CSS 並載入;在 development 的 mode 則使用 styled-loader 直接將 CSS 注入 DOM 內。

要特別留意的是,「不要同時」使用 styled-loadermini-css-extract-plugin

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const devMode = process.env.NODE_ENV !== 'production';
// ...

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'
})
];

效果

可以看到,使用 mini-css-extract-plugin 的話,CSS 會被抽成獨立的 .css 檔:

webpack mini-css-extract-plugin

開發環境(Development)

這部分的內容建議只使用在開發環境下,避免在正式模式(production)使用

Development @ Webpack Guides

使用 Source map

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

// webpack.config.js

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

使用開發環境的工具

檔案每次都要執行 npm run build 是非常煩人而且瑣碎的,開發時 webpack 提供了幾個方便的工具,只要偵測到檔案有變更就會重新 compile。這些工具包含:

最常用到的是 webpack-dev-server。

使用觀察模式(Using Watch Mode)

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

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

diff --git a/package.json b/package.json
index 4222149..5139e03 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"start": "node ./dist/bundle.js",
+ "watch": "webpack --watch",
"build": "webpack --mode=production",
"dev": "webpack serve --open",
"test": "echo \"Error: no test specified\" && exit 1"

使用 webpack-dev-server

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

webpack-dev-server @ webpack > configuration

安裝

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

設定

webpack.config.js

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

  • webpack-dev-server 會根據 output.path 中的定義來 serve 檔案,也就是檔案會可以在以下 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 路徑取得
  • webpack-dev-server 在 compile 後,並不會寫出任何 output 的檔案,而是將這些檔案保存在記憶體中。
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, // 打開瀏覽器

// 如果是 SPA 的話,記得要開啟這個選項,
// 如此當使用者不是從「首頁」進入時,頁面才能正確顯示
historyApiFallback: true,
},
// ...
};

package.json

/**
* --open 自動打開瀏覽器
* --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-dev-middleware,那麼應該要搭配 webpack-hot-middleware 來啟動 HMR 的功能。

webpack.config.js

只需要在 webpack.config.jsdevServer 中加上 hot: true 即可啟用,或者也可以透過 CLI 的方式執行 webpack serve --hot-only

// 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

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

import printMe from './print.js'

function component () {
...
}

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

HMR with stylesheet

style-loadercss-loader 本來就有整合 HMR 的功能,因此會自動啟用。

正式環境(Production Mode)

拆檔

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

$ npm install --save-dev webpack-merge
// 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-*** 這種,因為它們會增加打包後檔案的大小。

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() 我們可以定義環境變數:

// 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 以及上面所說的設定外,也可以用指令的方式,但還是建議放在設定檔比較清楚:

$ 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

// 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(如下所示):

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 運作,最基本的寫法如下:

const getUserModule = () => import('./common/usersAPI');

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

btn.addEventListener('click', () => {
// 也可以在這直接呼叫 import,例如
// import('./common/usersAPI').then(/*...*/)
getUserModule().then((module) => {
const { getUsers } = module;
getUsers().then((data) => console.log(data));
});
});

// 也可以用 async/await
btn.addEventListener('click', async () => {
// 也可以在這直接呼叫 import,例如
// const module = await import('./common/usersAPI');
const module = await getUserModule();
const { getUsers } = module;
const data = await getUsers();
});

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

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

以動態載入 lodash 為例:

// 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

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 中也可以使用動態載入:

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

使用 Webpack 建立 React 專案

How to React with Webpack 5 - Setup Tutorial @ robinwieruch

在整合 React 前,需要先安裝 babel-loader,並進行對應的設定,可以參考上面文件的說用。

安裝

npm install --save-dev @babel/preset-react

設定 babelrc

.babelrc 中加上 @babel/preset-react

diff --git a/.babelrc b/.babelrc
index a29ac99..4f06b0c 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,6 @@
{
"presets": [
- "@babel/preset-env"
+ "@babel/preset-env",
+ "@babel/preset-react"
]
}

設定 webpack

webpack.config.js 中,讓 babel-loader 能夠處理 .jsx 的檔案:

diff --git a/webpack.config.js b/webpack.config.js
index b0e92dd..61c116c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -19,7 +19,7 @@ const config = {
module: {
rules: [
{
- test: /\.(js)$/,
+ test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},

由於 React 會 mount 在 HTML 中特定 id 的 DOM 上,因此在 ./src/index.html 中,加上 <div id="app"></div>

<!-- ./src/index.html -->
<body>
<div id="app"></div>
</body>

並且在 HtmlWebpackPlugin 要以這個 HTML 作為 template:

diff --git a/webpack.config.js b/webpack.config.js
index b0e92dd..e888117 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -47,6 +47,7 @@ const config = {
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
+ template: './src/index.html',
}),
new MiniCssExtractPlugin(),
],

使用

接著就可以使用 React 了:

import React from 'react';
import ReactDOM from 'react-dom';
import WebpackLogo from './webpack-logo.svg';
import './style.scss';
import printMe from './print.js';

const title = 'React with Webpack and Babel';

const App = () => {
return (
<div>
<h1 className="hello">{title}</h1>
<img src={WebpackLogo} alt="webpack-logo" />
<button type="button" onClick={() => printMe()}>
Click Me
</button>
</div>
);
};

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);

其他

環境變數(Environment Variables)

設定 process.env.NODE_ENV

如果你單純只是想設定「一個」 ENV 的變數來判斷執行的環境,可以使用 --node-env 這個參數:

npx webpack --node-env production   # process.env.NODE_ENV = 'production'

如此,即可在 webpack.config.json 中直接透過 process.env.NODE_ENV 取得。

webpack 內部使用的 env

若你需要定義多個環境變數,並在 webpack.config.js 中取得該變數值,只需要在執行 webpack 的時候透過 --env xxx 即可,使用多個 --env 即可帶入多個環境變數。

env 會是一個物件:

  • --env production:則 env.production 的值會是 true
  • --env goal=local:則 env.platform 的值會是 "app"
npx webpack --env platform=app --env production

取得環境變數的方式則是在 module.exports 後面帶入函式:

const config = {
mode: 'development',
// ...
};

module.exports = (env) => {
console.log({ env });
return config;
};

/* env 會是如下的物件 */
// {
// platform: 'app',
// production: true
// }

分析 bundle 中的內容

Stats Data @ Webpack

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

$ 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

// ./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 檔壓縮。

// 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 後:

// webpack.config.js
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
'@': path.resolve(__dirname, 'src'),
},
},
};

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

// 不必寫:import Utility from '../../utilities/utility';
import Utility from 'Utilities/utility';

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

extension

module.exports = {
//...
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json'], // default
},
};

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

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

Resolve @ Webpack

Webpack 4 (Legacy)

Loading Image

keywords: file-loader, url-loader

相關 loader

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

安裝

npm install --save-dev url-loader

設定

// webpack.config.js
module: {
rules: [
// url loader (for image)
{
test: /\.(jpe?g|png|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 40000 /* 小於 40kB 的圖片轉成 base64 */,
},
},
],
},
];
}

使用

// ./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 };

Loading Fonts

keywords: file-loader

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

設定

// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader'],
},
],
},
};

使用

/* 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';
}

Plugin - 清除 dist 資料夾

keywords: clean-webpack-plugin

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

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

參考資料