深入學習 webpack (基礎篇)

語言: CN / TW / HK
ead>

webpack 是一個現代 JavaScript 應用程式的靜態模組打包工具,它對於前端工程師來說可謂是如雷貫耳,基本上現在的大型應用都是通過 webpack 進行構建的。

webpack 具有高度可配置性,它擁有非常豐富的配置。在過去一段時間內曾有人將熟練配置 webpack 的人稱呼為 “webapck 工程師”。當然,這稱呼只是個玩笑話,但也能從側面瞭解到 webpack 配置的靈活與複雜。

為了能夠熟練掌握 webpack 的使用,接下來通過幾個例子循序漸進的學習如何使用 webpack。

以下 Demo 都可以在 Github 的 webpack-example 中找到對應的示例,歡迎 star~

本篇文章內容略長,建議先馬後看。由於平臺不支援程式碼摺疊,因此建議直接看原文 從零構建 webpack 腳手架(基礎篇) | Anran758's blog 以獲得更好的閱讀體驗。

起步

[email protected] 開始,就可以不用再引入配置檔案來打包專案。若沒有提供配置的話,webpack 將按照預設規則進行打包。預設情況下 src/index 是專案的原始碼入口,打包後的程式碼會輸出到 dist/main.js 上。

首先來初始化一個專案,專案名為 getting-started

``` shell

建立專案資料夾

mkdir getting-started

進入專案目錄

cd getting-started

npm 專案

npm init -y ```

初始化專案後,專案目錄會新增一個 package.json,該檔案記錄了專案依賴的相關資訊。若想要使用 webpack 的話需要安裝它的依賴: webpack (本體)和 webpack-cli (可以在命令列操作 webpack 的工具):

``` shell

-D 和 --save-dev 選項都可以用於安裝開發依賴

npm i --save-dev webpack webpack-cli

npm i -D webpack webpack-cli

或者使用 yarn 安裝開發依賴

yarn add -D webpack webpack-cli ```

接著建立 webpack 所需的預設入口檔案 src/index.js 以及測試模組所用的 src/log.js 檔案。此時的專案結構大致如下:

diff . ├── package.json + ├── src + │ ├── index.js + │ └── log.js └── node_modules

`` js // src/log.js export const log = (name) => console.log(Hello ${name}!`);

// src/index.js import { log } from './log'

log('anran758'); ```

src/log.js 匯出了一個工具函式,它負責向控制檯傳送訊息。src/index.js 是預設的入口檔案,它引入 log 函式並呼叫了它。

上面的程式碼很簡單,像這種模組化的程式碼按照傳統 <script src> 引入的話,瀏覽器是不能正確執行的。可以在根目錄上建立一個 index.html 引入 js 指令碼來測試一下:

``` html /index.html

Test

```

建立檔案後,將上例程式碼複製到 index.html 中。儲存並開啟該檔案,看看瀏覽器能否正確處理模組邏輯。不出意外的話,檔案在瀏覽器開啟後,瀏覽器開發者工具會丟擲錯誤資訊:

error Uncaught SyntaxError: Cannot use import statement outside a module

言下之意就是說瀏覽器不能正確的解析 ES module 語句,此時 webpack 就可以派上用場啦~ 在 package.json 中的 scripts 欄位中新增如下命令:

diff /package.json "scripts": { + "build": "webpack" - "test": "echo \"Error: no test specified\" && exit 1" },

在命令列輸入 npm run build 呼叫 webpack 對當前專案進行編譯,編譯後的結果會輸出到 dist/main.js 檔案中(即便本地沒有 dist 目錄,它都會自動建立該目錄)。輸出檔案後,修改 index.html 對 js 的引用:

``` diff /index.html
+ -

```

重新重新整理頁面後就能看到 log 正確的輸出了 Hello anran758!。點選 log 右側的連結,可以跳轉至 Source 面板,將程式碼格式化後可以清晰地看到編譯後 js 的變化:

使用配置

當然,上例程式碼只不過是小試牛刀。對於正式的專案會有更復雜的需求,因此需要自定義配置。webpack 主要有兩種方式接收配置:

第一種: 通過 Node.js API引入 webpack 包,在呼叫 webpack 函式時傳入配置:

``` js const webpack = require("webpack");

const webpackConfig = { // webpack 配置物件 }

webpack(webpackConfig, (err, stats) => { if (err || stats.hasErrors()) { // 在這裡處理錯誤 }

// 處理完成 }); ```

第二種: 通過 webpack-cli 在終端使使用 webpack 時指定配置。

shell webpack [--config webpack.config.js]

兩種方法內配置都是相似的,只是呼叫的形式不同。本篇先使用 webpack-cli 來做示例。

webpack 接受一個特定的配置檔案,配置檔案要求匯出一個物件、函式、Promise 或多個配置物件組成的陣列。

現在將上一章的 Demo 複製一份出來,並重命名為 getting-started-config,在該目錄下新建 webpack.config.js 檔案,檔案內容如下:

``` js const path = require('path');

module.exports = { // 起點或是應用程式的起點入口 entry: "./src/index", output: { // 編譯後的輸出路徑 // 注意此處必須是絕對路徑,不然 webpack 將會拋錯(使用 Node.js 的 path 模組) path: path.resolve(__dirname, "dist"),

// 輸出 bundle 的名稱
filename: "bundle.js",

} } ```

上面的配置主要是定義了程式入口、編譯後的檔案輸出目錄。然後在 src/index.js 中修改一些內容用來打包後測試檔案是否被正確被編譯:

``` diff src/index.js import { log } from './log'

  • log('本節在測試配置噢');
  • log('anran758'); ```

隨後在終端輸入 num run build 進行編譯,可以看到 dist 目錄下多了個 bundle.js

``` shell $ npm run build

webpack --config ./webpack.config.js

Hash: 3cd5f3bbfaf23f01de37 Version: webpack 4.43.0 Time: 117ms Built at: 05/06/2020 1:01:37 PM Asset Size Chunks Chunk Names bundle.js 1010 bytes 0 [emitted] main Entrypoint main = bundle.js [0] ./src/index.js + 1 modules 123 bytes {0} [built] | ./src/index.js 62 bytes [built] | ./src/log.js 61 bytes [built]

WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: http://webpack.js.org/configuration/mode/ ```

由於我們輸出的檔名被修改了,此時還得修改 html 的引入路徑。但每改一次輸出目錄,HTML 中的引入路徑也得跟著改,這樣替換的話就比較容易出紕漏。那能不能讓 webpack 自動幫我們插入資源呢?答案是可以的。

Plugin

webpack 提供外掛(plugin)的功能,它可以用於各種方式自定義 webpack 構建過程。

html-webpack-plugin 可以在執行 webpack 時自動生成一個 HTML 檔案,並將打包後的 js 程式碼自動插入到文件中。下面來安裝它:

shell npm i --D html-webpack-plugin

安裝後在 webpack.config.js 中使用該外掛:

``` diff const path = require('path'); + const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = { // 起點或是應用程式的起點入口 entry: "./src/index",

// 輸出配置
output: {
  // 編譯後的輸出路徑
  // 注意此處必須是絕對路徑,不然 webpack 將會拋錯(使用 Node.js 的 path 模組)
  path: path.resolve(__dirname, "dist"),

  // 輸出 bundle 的名稱
  filename: "bundle.js",
},
  • plugins: [
  • new HtmlWebpackPlugin({
  • title: 'Test Configuration'
  • })
  • ], } ```

重新編譯後 HTML 也被輸出到 dist 目錄下。檢視 dist/index.html 的原始碼可以發現:不僅原始碼被壓縮了,同時 <script> 標籤也正確的引入了 bundle.js

此時目錄結構如下:

後續目錄展示會將 node_modulespackage-lock.jsonyarn.lock 這種對專案架構講解影響不大的目錄省略掉。

example . ├── dist │ ├── bundle.js │ ├── index.html │ └── main.js ├── index.html ├── package.json ├── src │ ├── index.js │ └── log.js └── webpack.config.js

處理完資源自動插入的問題後,還有一個問題需要我們處理:雖然 webpack 現在能自動生成 HTML 並插入指令碼,但我們還得在 HTML 中寫其他程式碼邏輯呀,總不能去改 /dist/index.html 檔案吧?

這個問題也很好解決。html-webpack-plugin 在初始化例項時,傳入的配置中可以加上 template 屬性來指定模板。配置後直接在指定模板上進行編碼就可以解決這個問題了:

``` diff const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = { // 起點或是應用程式的起點入口 entry: "./src/index",

// 輸出配置
output: {
  // 編譯後的輸出路徑
  // 注意此處必須是絕對路徑,不然 webpack 將會拋錯(使用 Node.js 的 path 模組)
  path: path.resolve(__dirname, "dist"),

  // 輸出 bundle 的名稱
  filename: "bundle.js",
},
plugins: [
  // html-webpack-plugin
  // http://github.com/jantimon/html-webpack-plugin#configuration
  new HtmlWebpackPlugin({
    title: 'Test Configuration',
  • template: path.resolve(__dirname, "./index.html"), }) ], }

```

使用模板後 html-webpack-plugin 也會自動將指令碼插入到模板中。因此可以將模板中的 <script> 給去掉了。為了測試輸出的檔案是否使用了模板,在 <body> 內隨便插入一句話,重新打包後預覽輸出的檔案是否包含這句話:

``` diff /index.html

+ Test Config - Test
+

Test Config

-

```

修改檔案後,重新打包就能看到模板也被壓縮輸出至 /dist/index.html 了,script 標籤也正常的插入了。

清理目錄

現在來看編譯後的目錄,我們發現 dist/mian.js 這檔案是使用配置之前編譯出來的檔案,現在我們的專案已經不再需要它了。這種歷史遺留的舊檔案就應該在每次編譯之前就被扔進垃圾桶,只輸出最新的結果。

clean-webpack-pluginrimraf 可以完成清理功能。前者是比較流行的 webpack 清除外掛,後者是通用的 unix 刪除命令(安裝該依賴包後 windows 平臺也能用)。如果僅是清理 /dist 目錄下檔案的話,個人是比較傾向使用 rimraf的,因為它更小更靈活。而 clean-webpack-plugin 是針對 webpack 輸出做的一系列操作。

在終端安裝依賴:

shell npm i -D rimraf

rimraf 的命令列的語法是: rimraf <path> [<path> ...],我們在 package.jsonscirpts 中修改 build 的命令:

diff /package.json "scripts": { + "build": "rimraf ./dist && webpack --config ./webpack.config.js" - "build": "webpack --config ./webpack.config.js" }

``` shell $ npm run build

rimraf ./dist && webpack --config ./webpack.config.js

Hash: 763fe4b004e1c33c6876 Version: webpack 4.43.0 Time: 342ms Built at: 05/06/2020 2:35:49 PM Asset Size Chunks Chunk Names bundle.js 1010 bytes 0 [emitted] main index.html 209 bytes [emitted]
Entrypoint main = bundle.js [0] ./src/index.js + 1 modules 123 bytes {0} [built] | ./src/index.js 62 bytes [built] | ./src/log.js 61 bytes [built]

WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: http://webpack.js.org/configuration/mode/ Child HtmlWebpackCompiler: 1 asset Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0 1 module ```

這樣 webpack 輸出的 /dist 目錄始終是最新的東西。

loader

在正常的頁面中,引入 css 樣式表會讓頁面變得更美觀。引入圖片可以讓頁面內容更豐富。

然而 webpack 本體只能處理原生的 JavaScript 模組,你讓它處理 css 或圖片資源,它是無法直接處理的。為了處理這種問題,webpack 提供了 loader 的機制,用於對模組外的原始碼進行轉換。

loader 一般是單獨的包,我們可以在社群找到對應 loader 來處理特定的資源。在使用前通過 npm 安裝到專案的開發依賴中即可。loader 可以通過配置內聯Cli 這三種方式來使用。下文主要以 配置 的方式來使用。

css

往常引入 css 樣式表無非就是在 html 中通過 <link> 標籤引入。現在想通過 webpack 來管理依賴得需要安裝對應的 loader 來處理這些事。

css-loader 可以讓 webpack 可以引入 css 資源。光有讓 webpack 識別 css 的能還不夠。為了能將 css 資源進行匯出,還要安裝 mini-css-extract-plugin 外掛:

現在將上一節的 Demo 複製並重名為 getting-started-loader-css。進入新的專案目錄後安裝依賴:

shell npm install -D css-loader mini-css-extract-plugin

在更改配置之前,為了使專案結構更清晰,咱們按照檔案型別重新調整原始碼目錄結構。將 src 下的 js 檔案都放進 js 資料夾中。同時建立 /src/css/style.css 樣式表。調整後的目錄結構如下:

shell . ├── package.json ├── src │ ├── index.html │ ├── css │ │ └── style.css │ └── js │ ├── index.js │ └── log.js └── webpack.config.js

現在將 Flexbox 佈局用例 中結尾的 Demo 遷移到專案中,測試一下效果:

``` html /src/index.html

Test

Alice

I

Pixiv Content ID: 65843704

Birthday

II

Pixiv Content ID: 70487844

Dream

III

Pixiv Content ID: 65040104

Daliy

IV

Pixiv Content ID: 64702860

Schoolyard

V

Pixiv Content ID: 67270728

**/src/css/style.css:** css html { font-family: 'helvetica neue'; font-size: 20px; font-weight: 200; background: #f7f7f7; }

body, p { margin: 0; }

.panels { display: flex; min-height: 100vh; overflow: hidden; }

.panel { flex: 1; display: flex; align-items: center; justify-content: center; flex-direction: column; color: white; background: #ececec; text-align: center; box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.1); transition: font-size 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11), flex 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11), background 0.2s; font-size: 20px; background-size: cover; background-position: center; cursor: pointer; }

.panel1 { background-color: #f4f8ea; }

.panel2 { background-color: #fffcdd; }

.panel3 { background-color: #beddcf; }

.panel4 { background-color: ​#c3cbd8; }

.panel5 { background-color: #dfe0e4; }

.item { flex: 1 0 auto; display: flex; justify-content: center; align-items: center; transition: transform 0.5s; font-size: 1.6em; font-family: 'Amatic SC', cursive; text-shadow: 0 0 4px rgba(0, 0, 0, 0.72), 0 0 14px rgba(0, 0, 0, 0.45); }

.name { transform: translateY(-100%); }

.panel .index { font-size: 4em !important; width: 100%; }

.desc { transform: translateY(100%); }

.open-active .name, .open-active .desc { transform: translateY(0); width: 100%; }

.panel.open { flex: 3; font-size: 40px; } ```

/src/js/index.js

``` js import { log } from './log' import '../css/style.css';

function installEvent() { const panels = document.querySelectorAll('.panel')

function toggleOpen() { panels.forEach(item => { if (item === this) return; item.classList.remove('open') });

this.classList.toggle('open'); }

function toggleActicon(e) { if (e.propertyName.includes('flex-grow')) { this.classList.toggle('open-active') } }

// 給每個元素註冊事件 panels.forEach(panel => { panel.addEventListener('click', toggleOpen) panel.addEventListener('transitionend', toggleActicon) }) }

installEvent(); log('本節在測試配置噢'); ```

修改 webpack 配置,引入 css-loadermini-css-extract-plugin。既然已經對原始碼目錄進行分類了,那順便也給輸出目錄的檔案也進行分類整理吧:

``` diff // /webpack.config.js const path = require('path'); + const HtmlWebpackPlugin = require('html-webpack-plugin'); + const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = { // 起點或是應用程式的起點入口 entry: "./src/js/index",

// 輸出配置 output: { // 編譯後的輸出路徑 // 注意此處必須是絕對路徑,不然 webpack 將會拋錯(使用 Node.js 的 path 模組) path: path.resolve(__dirname, "dist"),

// 輸出 bundle 的名稱
  • filename: "bundle.js",
  • filename: "js/bundle.js",
  • },
  • module: {
  • rules: [
  • {
  • test: /.css$/i,
  • use: [MiniCssExtractPlugin.loader, 'css-loader'],
  • },
  • ],
  • }, plugins: [ // html-webpack-plugin // http://github.com/jantimon/html-webpack-plugin#configuration new HtmlWebpackPlugin({ title: 'Test Configuration',
  • template: path.resolve(__dirname, "./index.html"),
  • template: path.resolve(__dirname, "./src/index.html"),
  • }), +
  • // 提取 css 到單獨的檔案
  • // http://github.com/webpack-contrib/mini-css-extract-plugin
  • new MiniCssExtractPlugin({
  • // 選項類似於 webpackOptions.output 中的相同選項,該選項是可選的
  • filename: 'css/index.css',
  • }) ], } ```

現在我們根據上面的配置來解讀 loader 的使用:

在上面的配置中,module 規定了如何處理專案中的不同型別的模組。rules 是建立模組時,匹配請求的 rule (規則)陣列。rule 是一個物件,其中最常見的屬性就是 testuseloader

rule.test 是匹配條件,通常會給它提供一個正則表示式或是由正則表示式組成的陣列。如果配置了 test 屬性,那這個 rule 將匹配指定條件。比如匹配條件寫為 test: /\.css$/i,這意味著給字尾為 .css 的檔案使用 loader

rule.use 顧名思義就是使用,給符合匹配條件的檔案使用 loader。它可以接收一個字串,這個字串會通過 webpack 的 resolveLoader 選項進行解析。該選項可以不配置,它內建有解析規則。比如下例中預設會從 node_modules 中查詢依賴:

js use: 'css-loader'

rule.use 還可以是應用於模組的 UseEntry 物件。UseEntry 物件內主要有 loaderoptions 兩個屬性:

js // use 傳入 UseEntry 型別的物件 use: { // 必選項,要告訴 webpack 使用什麼 loader loader: 'css-loader', // 可選項,傳遞給 loader 選項 options: { modules: true } },

如果 UseEntry 物件內只設置 loader 屬性,那它與單傳的字串的效果是一樣的。而 options 是傳遞給 loader 的配置項,不同 loader 會提供有不同的 options。值得注意的是,如果 use 是以物件形式傳入,loader 屬性是必填的,而 options 是可選的

rule.use 還可以是一個函式,函式形參是正在載入的模組物件引數,最終該函式要返回 UseEntry 物件或陣列:

js use: (info) => { console.log(info); return { loader: 'svgo-loader', options: { plugins: [{ cleanupIDs: { prefix: basename(info.resource) } }] } } }

打印出函式的形參 info 可以看到該物件擁有如下屬性:

  • compiler: 當前的 webpack 編譯器(可以未定義)
  • issuer: 匯入正在載入的模組的模組的路徑
  • realResource: 始終是要載入的模組的路徑
  • resource: 要載入的模組的路徑,通常等於 realResource。除非在請求字串中通過 !=! 覆蓋資源名。

由此可見,使用函式方式可用於按模組更改 loader 選項。

rule.use 最常見的使用形式還是提供一個數組,陣列中每項可以是字串、UseEntry 物件、UseEntry 函式。這也是一個套娃的過程:

js use: [{ loader: MiniCssExtractPlugin.loader }, 'css-loader'],

這裡需要注意的是,rule 中使用多個 loader 要注意其順序。使用陣列 loader 將會從右至左進行應用

比如上例中最先通過 css-loader 來處理 .css 檔案的引入問題,再通過 MiniCssExtractPlugin.loader (Tips: 該值是 loader 的絕對路徑)來提取出檔案。如果反過來應用就會出問題了,webpack 都不知道如何引用 css 檔案,自然提取不出東西啦。

rule.loaderrule.use 的縮寫,等價於 rule.use: [{ loader }]。webpack 像這樣簡寫的配置屬性還有很多,這樣做有利也有弊。對於熟手來說,提供這種簡便選項可以減少配置的巢狀關係,但對新手來說,這配置有種錯綜複雜的感覺。

js { // 匹配檔案規則 test: /\.css$/i, // rule.use 簡寫形式 loader: 'css-loader' }

接下來回歸正題。重新編譯 webpack,編譯後的目錄結構如下:

shell . ├── dist │ ├── css │ │ └── index.css │ ├── index.html │ └── js │ └── bundle.js ├── package.json ├── src │ ├── css │ │ └── style.css │ ├── index.html │ └── js │ ├── index.js │ └── log.js └── webpack.config.js

image

圖片資源也是專案中的常見資源,引入圖片資源同樣需要安裝 loader。處理圖片資源的 loader 主要有兩種,分別是 url-loaderfile-loader

file-loader

file-loader 是將 import/require() 引入的檔案解析為 url,並把檔案輸出到輸出目錄中。

複製一份新 Demo 並重命名為 getting-started-loader-images。在安裝 loader 之前先做一個小優化:

如果我們會頻繁修改原始碼檔案,修改完後又要重新編譯,這個步驟實際是有點繁瑣的。webpack 有個 watch 選項可以監聽檔案變化,若檔案有修改 webpack 將自動編譯(若修改的是配置檔案的話,還是需要重新執行命令)。

package.jsonscript 中給 webpack 新增 -w 選項:

json "scripts": { "build:watch": "rimraf ./dist && webpack --config ./webpack.config.js -w" },

接下來就可以安裝依賴了:

shell npm i -D file-loader

新建一個 /src/images 資料夾,往裡面新增一些圖片:

diff . ├── package.json ├── src │ ├── css │ │ └── style.css + │ ├── images + │ │ ├── 01.jpg + │ │ ├── 02.png + │ │ ├── 03.jpg + │ │ ├── 04.png + │ │ ├── 05.png + │ │ ├── 06.jpg + │ │ ├── webpack.jpg + │ │ └── webpack.svg │ ├── index.html │ └── js │ ├── index.js │ └── log.js └── webpack.config.js

webpack.config.js 中配置 loader

diff rules: [ { test: /\.html$/i, loader: 'html-loader', }, { // 匹配檔案規則 test: /\.css$/i, // use 從右至左進行應用 use: [MiniCssExtractPlugin.loader, 'css-loader'], }, + { + test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[hash:8].[ext]' + }, + }, + }, ],

預設情況下圖片會被輸出到 dist 目錄中,檔名也會被更改為一長串的雜湊值。為了保持目錄整潔,將要被輸出的圖片資源都歸類到 img 目錄中。

可以通過設定 namepublicPath 來指定目錄:

``` js // 直接設定 name use: { loader: 'file-loader', options: { name: 'img/[name].[hash:8].[ext]', }, },

// 或者使用 publicPath,效果與上例等價 use: { loader: 'file-loader', options: { publicPath: 'img', name: '[name].[hash:8].[ext]', }, }, ```

name 屬性的值可以用 / 分層。除去最末尾一層的是檔名,前面每層 / 分隔都是巢狀的資料夾。比如值為 static/img/[name].[hash:8].[ext] 最後輸出的結果是:根目錄建立一個 static 目錄,static 內又會建立一個 img 目錄,img 內輸出被引用的圖片資源。

由於匹配的圖片資源有很多,咱們不能寫死輸出的檔名,不然會引發重名問題,作業系統不準這樣幹。這時 佔位符(placeholder)就能排上用場了。name 中方括號包裹起來的是佔位符,不同佔位符會被替換成不同的資訊。

比如上例中使用了三個佔位符: name 是檔案的名稱、hash 是指定用於對檔案內容進行 hash (雜湊)處理的 hash 方法,後面冒號加數值代表擷取 hash 的長度為 8、ext 是檔案的副檔名。在檔名加入 hash 的用意是針對瀏覽器快取而特意加入的。現在可以不用在意這種優化問題,未來會專門另起一篇文章講優化的問題。

現在修改完 webapck 配置,接著再來完善上一節的 Demo。在 /src/css/styles.css 中使用 backgournd-image 引入圖片:

``` css / 省略其他程式碼... / .panel1 { background-color: #f4f8ea; background-image: url('../images/01.jpg'); }

.panel2 { background-color: #fffcdd; background-image: url('../images/02.png'); }

.panel3 { background-color: #beddcf; background-image: url('../images/03.jpg'); }

.panel4 { background-color: ​#c3cbd8; background-image: url('../images/04.png'); }

.panel5 { background-color: #dfe0e4; background-image: url('../images/05.png'); } ```

重新編譯後的結果如下:

``` shell

rimraf ./dist && webpack --config ./webpack.config.js -w

webpack is watching the files…

Hash: 398663f1f4d417d17c94 Version: webpack 4.43.0 Time: 1086ms Built at: 05/29/2020 2:19:03 PM Asset Size Chunks Chunk Names css/index.css 1.72 KiB 0 [emitted] main img/01.a8e7ddb2.jpg 170 KiB [emitted]
img/02.46713ed3.png 744 KiB [emitted] [big]
img/03.70b4bb75.jpg 529 KiB [emitted] [big]
img/04.b7d3aa38.png 368 KiB [emitted] [big]
img/05.875a8bc2.png 499 KiB [emitted] [big]
index.html 990 bytes [emitted]
js/bundle.js 1.33 KiB 0 [emitted] main Entrypoint main = css/index.css js/bundle.js [0] ./src/css/style.css 39 bytes {0} [built] [1] ./src/js/index.js + 1 modules 938 bytes {0} [built] | ./src/js/index.js 873 bytes [built] | ./src/js/log.js 60 bytes [built] + 1 hidden module

WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: http://webpack.js.org/configuration/mode/

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: img/04.b7d3aa38.png (368 KiB) img/05.875a8bc2.png (499 KiB) img/02.46713ed3.png (744 KiB) img/03.70b4bb75.jpg (529 KiB)

WARNING in webpack performance recommendations: You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application. For more info visit http://webpack.js.org/guides/code-splitting/ Child HtmlWebpackCompiler: 1 asset Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0 [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 1.01 KiB {0} [built] Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!src/css/style.css: Entrypoint mini-css-extract-plugin = * [0] ./node_modules/css-loader/dist/cjs.js!./src/css/style.css 3.09 KiB {0} [built] [3] ./src/images/01.jpg 63 bytes {0} [built] [4] ./src/images/02.png 63 bytes {0} [built] [5] ./src/images/03.jpg 63 bytes {0} [built] [6] ./src/images/04.png 63 bytes {0} [built] [7] ./src/images/05.png 63 bytes {0} [built] + 2 hidden modules ```

當我們重新開啟 /dist/index.html 時會發現圖片並沒有加載出來?檢視 css 原始碼後發現原來是路徑有問題,編譯後的路徑是 img/01.a8e7ddb2.jpg 這種相對路徑。

由於 css 本身有一個資料夾,通過相對路徑引入,那就會從 css 目錄下進行查詢。實際找到的是 dist/css/img/01.a8e7ddb2.jpg 這條路徑。

遇到這種情況怎麼辦呢?我們可以給 MiniCssExtractPlugin.loader 新增 publicPath 選項用以修正路徑,重新編譯後就可以看到圖片正確被載入了:

js { // 匹配檔案規則 test: /\.css$/i, // use 從右至左進行應用 use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../', } }, 'css-loader' ], },

在 js 中也可以引用檔案,開啟 /src/js/index.js, 在原先的基礎上新增如下程式碼:

``` js import img1 from '../images/06.jpg'; import img2 from '../images/webpack.jpg'; import img3 from '../images/webpack.svg';

// 省略其他程式碼...

log('測試圖片引入~'); console.log('img1 --> ', img1); console.log('img2 --> ', img2); console.log('img3 --> ', img3); ```

重新編譯後可以在 Console 面板可以看到 js 輸出了檔案資源的路徑:

url-loader

url-loader 功能也類似於 file-loader,不同的是當檔案大小(單位 byte)小於 limit 時,可以返回一個 DataURL

為什麼要用 DataURL 呢?我們知道頁面上每載入一個圖片資源,都會發起一個 HTTP 請求。而建立 HTTP 請求的過程是需要花時間的。因此可以將檔案轉為 DataURL 嵌入 html/css/js 檔案中,這樣可以有效減少 HTTP 建立連線時所帶來額外的時間開銷了。同時 html/css/js 檔案也可以被瀏覽器快取,DataURL 被引入後也能一同被快取。

圖片轉 DataURL 也有缺點,那就是編碼後文本儲存所佔的空間比圖片會更大。這其實就是傳輸體積與 HTTP 連線數的權衡。所以最佳做法是將小圖片轉為 DataURL,轉換後並不會有過多體積溢位,而大尺寸圖片照常引入即可。

安裝 url-loader:

shell npm install url-loader -D

修改 webpack.config.js

js rules: [ { // 匹配檔案規則 test: /\.css$/i, // use 從右至左進行應用 use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader' ], }, { test: /\.(png|jpe?g|gif|webp)(\?.*)?$/, use: { loader: 'url-loader', options: { limit: 10000, name: 'img/[name].[hash:8].[ext]' }, }, }, { test: /\.(svg)(\?.*)?$/, use: { loader: 'file-loader', options: { name: 'img/[name].[hash:8].[ext]' }, }, }, ],

在上例中將 pngjpgjpeggifwebp 檔案交給 url-loader 處理,而 svg 仍由 file-loader 處理。這樣做的理由是: DataURL 內聯 svg 會破壞 sprite 系統 (將多個 svg 合為一張使用的技術) 中使用的Fragment Identifiers,因此不將 svg 轉為 DataURL

url-loader 設定匹配規則後,配置 namelimit 選項。url-loadername 選項與 file-loadername 作用是相同的,就不再累述。

limit 是指定以位元組(byte) 為單位的檔案最大尺寸。當檔案尺寸小於等於 limit 所設的值,那檔案將會被轉為 DataURL。相反,若檔案尺寸大於 limit 時,則使用備用 loader。預設備用 loaderfile-loader。可以設定 fallback 選項來修改備用 loader

js { loader: 'url-loader', options: { limit: 10000, name: 'img/[name].[hash:8].[ext]' fallback: 'file-loader' } }

limit 的選值不易過大,可以設為 10240 (10KB)或 10000,也可以根據專案實際情況進行調整。

現在來測試 limit 的效果。unix 系統可以在終端使用 ls -l 命令來檢視檔案資訊:

shell ➜ getting-started-loader-images git:(master) ✗ cd ./src/images ➜ images git:(master) ✗ ls -l total 6144 -rwxr-xr-x 1 anran staff 173596 May 28 17:41 01.jpg -rwxr-xr-x 1 anran staff 761560 May 28 17:41 02.png -rwxr-xr-x 1 anran staff 542065 May 28 17:41 03.jpg -rwxr-xr-x 1 anran staff 376562 May 28 17:41 04.png -rwxr-xr-x 1 anran staff 510812 May 28 17:41 05.png -rw-r--r-- 1 anran staff 760117 May 28 17:41 06.jpg -rw-r--r--@ 1 anran staff 6943 May 30 13:54 webpack.jpg -rw------- 1 anran staff 647 May 28 21:33 webpack.svg

從輸出的資訊可以看到 webpack.svg (647B) 和 webpack.jpg (6943B) 的檔案尺寸都低於設定的 limit: 10000。由於 svg 檔案不通過 url-loader 處理,那按照預想它將會被輸出到 /dist/img 中。webpack.jpg 可以被 url-loader,那編譯後應該被嵌入到 js 程式碼中。

重新編譯測試一下:

``` shell ➜ getting-started-loader-images git:(master) ✗ npm run build

[email protected] build /Users/anran/project_my/webpack-example/getting-started-loader-images rimraf ./dist && webpack --config ./webpack.config.js

Hash: 8d2e8c8220e86d46e388 Version: webpack 4.43.0 Time: 692ms Built at: 05/30/2020 2:08:46 PM Asset Size Chunks Chunk Names css/index.css 1.63 KiB 0 [emitted] main img/01.a8e7ddb2.jpg 170 KiB [emitted]
img/02.46713ed3.png 744 KiB [emitted] [big]
img/03.70b4bb75.jpg 529 KiB [emitted] [big]
img/04.b7d3aa38.png 368 KiB [emitted] [big]
img/05.875a8bc2.png 499 KiB [emitted] [big]
img/06.5b8e9d1e.jpg 742 KiB [emitted] [big]
img/webpack.258a5471.svg 647 bytes [emitted]
index.html 990 bytes [emitted]
js/bundle.js 10.5 KiB 0 [emitted] main Entrypoint main = css/index.css js/bundle.js [0] ./src/css/style.css 39 bytes {0} [built] [1] ./src/js/index.js + 4 modules 10.1 KiB {0} [built] | ./src/js/index.js 881 bytes [built] | ./src/js/log.js 60 bytes [built] | ./src/images/06.jpg 63 bytes [built] | ./src/images/webpack.jpg 9.08 KiB [built] | ./src/images/webpack.svg 68 bytes [built] + 1 hidden module

WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: http://webpack.js.org/configuration/mode/

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: img/04.b7d3aa38.png (368 KiB) img/03.70b4bb75.jpg (529 KiB) img/05.875a8bc2.png (499 KiB) img/02.46713ed3.png (744 KiB) img/06.5b8e9d1e.jpg (742 KiB)

WARNING in webpack performance recommendations: You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application. For more info visit http://webpack.js.org/guides/code-splitting/ Child HtmlWebpackCompiler: 1 asset Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0 [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 1.37 KiB {0} [built] Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!src/css/style.css: Entrypoint mini-css-extract-plugin = * [0] ./node_modules/css-loader/dist/cjs.js!./src/css/style.css 2.98 KiB {0} [built] [3] ./src/images/01.jpg 63 bytes {0} [built] [4] ./src/images/02.png 63 bytes {0} [built] [5] ./src/images/03.jpg 63 bytes {0} [built] [6] ./src/images/04.png 63 bytes {0} [built] [7] ./src/images/05.png 63 bytes {0} [built] + 2 hidden modules ```

輸出目錄:

shell . ├── dist │ ├── css │ │ └── index.css │ ├── img │ │ ├── 01.a8e7ddb2.jpg │ │ ├── 02.46713ed3.png │ │ ├── 03.70b4bb75.jpg │ │ ├── 04.b7d3aa38.png │ │ ├── 05.875a8bc2.png │ │ ├── 06.5b8e9d1e.jpg │ │ └── webpack.258a5471.svg │ ├── index.html │ └── js │ └── bundle.js ├── package-lock.json ├── package.json ├── src │ ├── css │ │ └── style.css │ ├── images │ │ ├── 01.jpg │ │ ├── 02.png │ │ ├── 03.jpg │ │ ├── 04.png │ │ ├── 05.png │ │ ├── 06.jpg │ │ ├── webpack.jpg │ │ └── webpack.svg │ ├── index.html │ └── js │ ├── index.js │ └── log.js └── webpack.config.js

重新開啟 /dist/index.html 後可以在瀏覽器控制檯看到如下輸出的資訊:

HTML 資源引入

HTML 中有一種常見的情況是:在模板中通過相對路徑引入圖片、指令碼等資源時,發現引入的資源都沒有被打包進去。

為什麼會發生這種情況呢?原來是 webpack 預設不會處理 html 中的資源引入。為了能使 HTML 能通過相對路徑引入資源,主要有 3 種解決的方案:

lodash template

現在專案中 /src/index.html 是作為 html-webpack-plugin 的模板,在模板中可以使用 lodash template 語法(以下簡稱模板語法)來插入內容。語法格式為: <%= value %>

比如在 src/index.html 的模板中插入圖片:

```html /src/index.html

```

``` css /src/css/style.css / 為了使頁面美觀,再新增一些樣式 / .panel6 { position: relative; overflow: hidden; background-color: #061927; }

.panel6 .item { position: relative; }

.panel6 .img { position: absolute; height: 100%; transform: scale(1); transition: transform 0.4s 0.6s; }

.panel6.open { flex: 2; }

.panel6.open .img { transform: scale(1.2); }

```

上例將通過 require() 函式引入圖片。webpack 引入圖片時預設是通過 ESModule 來引入的,因此解析的結果大致為 {default: module} 這種形式。因此後面還需要再加一個 default。這樣就能正確的引入資源啦。

靜態目錄

第二種就是新增一個靜態目錄 static(或者叫 public)。

HTML 預設不是引用不了原始碼目錄上的資源嗎?那我就直接將資源輸出到 dist 目錄上。模板引用資源時直接引入輸出後的檔案不就行啦?

copy-webpack-plugin 可以完成這種遷移的功能。它將從 form 處複製檔案/資料夾,複製到 to (預設是 webpack 的輸出目錄)中。現在來安裝它:

shell npm i -D copy-webpack-plugin

新增 static 目錄,並新增一些測試檔案:

diff . ├── package.json ├── src │ ├── css │ │ └── style.css │ ├── images │ │ ├── 01.jpg │ │ ├── 02.png │ │ ├── 03.jpg │ │ ├── 04.png │ │ ├── 05.png │ │ ├── 06.jpg │ │ ├── webpack.jpg │ │ └── webpack.svg │ ├── index.html │ ├── js │ │ ├── index.js │ │ └── log.js + │ └── static + │ └── images + │ ├── 06.jpg + │ ├── webpack.jpg + │ └── webpack.svg └── webpack.config.js

現在將 src/static/images 的所有檔案(不管程式碼裡有沒有引入這些檔案)都複製到 dist/img 中。

js /webpack.config.js // webpack.config.js { plugins: [ new CopyPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/static/images'), to: path.resolve(__dirname, 'dist/img') }, ], }), ], }

如果你不僅想要複製圖片還想要複製其他諸如 css 樣式表、js 指令碼甚至是 excel 檔案到輸出目錄的話。那可以考慮將 static 目錄與 dist 目錄進行合併,將 staticdist 下的目錄名保持一致。

比如將 static 的下 images 資料夾更名為圖片輸出目錄 img,這樣打包後會輸出到同一個目錄中:

``` js /webpack.config.js // webpack.config.js { plugins: [ new CopyPlugin({ patterns: [ // 如果只傳 string 的話,那這個 string 相當於 from // path.resolve(__dirname, 'src', 'static'),

    // to 預設是 `compiler.options.output`, 也就是 dist 目錄
    // {
    //   from: path.resolve(__dirname, 'src/static'),
    //   to: ''
    // },

    // 當前配置中與上面兩例等價
    {
      from: path.resolve(__dirname, 'src/static'),
      to: path.resolve(__dirname, 'dist')
    },
  ],
}),

], } ```

若指定檔案/資料夾不想複製到 dist 中,還可以使用 globOptions.ignore 來忽略:

js /webpack.config.js // webpack.config.js { plugins: [ new CopyPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/static'), to: path.resolve(__dirname, 'dist') globOptions: { ignore: ['/**/webpack.jpg', '/**/img/webpack.svg'], } }, ], }), ], }

重新修改模板中的圖片的引入的路徑,使其指向輸出目錄的 img:

``` html

VI

```

編譯後就能看到圖片正確被引用了。

html-loader

最後一種是安裝 html-loader,讓 webapck 可以處理 html 資源的引入。

shell npm install -D html-loader

js rules: [ { test: /\.html$/i, loader: 'html-loader', }, // 省略其他 rule... ]

配置 html-loader 後,HTML 訪問相對路徑的資源就由 html-loader 來進行引入。將模板中的路徑改為原始碼相對路徑:

``` html

VI

```

在實際編譯時,<img class="img" src="./images/06.jpg" alt="">src 的值會被轉為 require('./images/06.jpg'),通過 webpack 引入後再將編譯後的結果傳入圖片的 src 屬性中。

此時重新編譯後就可以正確引入了。但配置 html-loader 的方法會與方法二訪問靜態目錄資源有點衝突。配置 html-loader 後就不能通過 ./../ 這種相對路徑來訪問資輸出目錄的資源了。

如果我們配置了 html-loader 的同時又還想訪問靜態資源怎麼辦呢?這時可以通過根路徑 / 逐層來訪問,這樣 html-loader 就不會處理這種路徑:

``` html

VI

```

現在問題又來了,若我們通過根路徑來訪問資源的話,那就不能單純地開啟檔案來在瀏覽器檢視效果了。因為直接開啟檔案到瀏覽器上,是通過 file:// 協議開啟的。瀏覽器實際上訪問的路徑是檔案的絕對地址。

比如筆者開啟檔案後,瀏覽器位址列展示的 url 是: file:///Users/anran/project_my/webpack-example/getting-started-static-assets/dist/index.html。現在通過根路徑訪問資源,需要瀏覽器補全為完整的 URL,經過瀏覽器補全後絕對路徑是 file:///img/06.jpg。這樣路徑都是錯誤的自然就訪問不到想要的資源啦。

如果有寫過 SPA(單頁面應用) 專案的朋友應該很熟悉。將 SPA 專案打包後直接訪問 index.html 頁面是空白的,這種情況多半就是從根路徑引入資源失敗而引起的。

這個問題解決的辦法也很簡單,就是將編譯後的專案部署到伺服器上,直接通過伺服器進行訪問,問題就迎刃而解了。為什麼這樣就可以解決了呢?

比如筆者的網站域名是 anran758.github.io,現在將頁面部署到伺服器後,直接在瀏覽器訪問 http://anran758.github.io/,實際上訪問的是 /dist/index.html 檔案。html 通過相對路徑訪問/img/06.jpg,那補全後圖片的路徑就是 http://anran758.github.io/img/06.jpg。這樣自然就能訪問資源啦。

我們不妨通過 Node.js 起一個本地伺服器測試一下。在 /dist 同級目錄上新建一個 server.js 指令碼,新增如下程式碼:

``` js /server.js const express = require('express'); const config = require('./webpack.config');

const app = express(); const PORT = 8001;

// 設定靜態資源入口 app.use(express.static(config.output.path));

// 監聽埠 app.listen(PORT, (err) => { if (err) { console.log(err); return; }

console.log('Listening at http://localhost:' + PORT + '\n'); }) ```

上例指令碼程式碼是通過 express 快速搭建一個本地伺服器,將伺服器靜態資源入口設為 webpack.config.js 的輸出目錄(也就是 /dist),隨後啟動伺服器。

express 是基於 Node.js 的 web 框架,要使用它之前需要安裝依賴:

shell npm install -D express

package.json 中添加個快捷入口,並在終端執行該指令碼:

json { "scripts": { // 其他指令碼.. "test:prod": "node server.js" }, }

``` shell ➜ getting-started-static-assets git:(master) ✗ npm run test:prod

[email protected] test:prod /Users/anran/project_my/webpack-example/getting-started-static-assets node server.js

Server is running at http://localhost:8001 . Press Ctrl+C to stop. ```

開啟 http://localhost:8001 後就能看到圖片資源正確被引用了。

總結

好啦,現在 webpack 基礎篇也到了尾聲。我們對上述知識做一個簡單的小結:

webpack 是一個靜態模組打包工具,它本體雖然只支援處理 javascript 的模組,但可以通過 loader 讓 webpack 完成原本它不能處理的功能。

webpack 的提供外掛的功能,外掛可以針對某種需求做特定處理,比如自動給 html 插入資源。

除了靜態目錄的檔案外,我們發現 webpack 輸出的檔案都是有依賴關係的。為什麼會這麼說呢?仔細看看 webpack 處理的邏輯就能想清楚了:

webpack 從程式的入口 /src/js/index.js 開始處理,入口檔案引入了 style.css,而 style.css 內又引用了圖片資源。然後 HTML 再通過 webpack 外掛引入模板,再將這些資源插入模板中。這就是檔案的依賴關係,這些依賴關係最終會生成一個依賴圖(Dependency Graph)

想必看到這裡的各位對 webpack 都有了個比較清晰的概念了吧?當然這只是一個開始,後面還有一些高階的概念在本文中由於篇幅的限制無法一併理清。若對筆者 webpack 的筆記感興趣的話可以繼續關注此係列的更新,下一篇將圍繞開發環境進行梳理。

參考資料: