掌握webpack(一)一張圖讓你明白webpack中output的filename、path、publicPath與主流外掛的關係

w4ngzhen發表於2022-12-31

webpack的核心概念,放到2022年相信很多的小夥伴都已經非常清楚了。但是,對於webpack配置中的output.path、output.filename以及output.publicPath,還有很多小夥伴還不理解。本文講圍繞output.filename、output.path與output.publicPath,講解它們的功能,並分析這些配置與webpack中常使用到的MiniCssExtractPlugin、HtmlWebpackPlugin等外掛的關係。

直接上總結圖

160-v3-path-filename-publicPath

基礎環境搭建

我們現在基於webpack搭建了一個前端專案,完成專案初始化,並安裝webpack三件套:

yarn init
yarn add -D webpack webpack-cli webpack-dev-server

安裝完成以後,我們在專案根目錄下建立一個webpack.config.js,一個極簡的配置如下:

const {resolve} = require('path');
module.exports = {
    mode: 'development',
    entry: {
        main: resolve(__dirname, 'src', 'index.js')
    },
    output: {
        filename: 'main.js',
        path: resolve(__dirname, 'dist')
    }
}

然後,在package.json中新增指令碼:

+ "scripts": {
+   "build": "webpack --config webpack.config.js"
+ },
  "devDependencies": {
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  }

接著,我們在src目錄下建立一個index.js檔案,這個js檔案的內容就是從dom上找到id為app的元素,並給其內部新增一個文字"hello, world"

document.getElementById('app').innerText = 'hello, world'

最後,我們執行webpack的構建過程:

yarn build

執行以後,就會在專案根目錄下的dist目錄下生成main.js。

010-base-config

注意:這裡並沒有配置關於js的解析,因為webpack預設就會處理js檔案。

引入HtmlWebpackPlugin

僅僅是生成目標js檔案,可能還不是我們期望的效果。對於一個專案來說,我們通常還希望有一個html來展示UI,並執行js程式碼,但是手工建立可能不能是一個好的方案。這裡,我們引入本專案的第一個外掛:HtmlWebpackPlugin

yarn add -D html-webpack-plugin

HtmlWebpackPlugin外掛基礎功能:

  1. 它會使用一個模板來生成一個html;
  2. 在生成的html中插入節點(譬如,js對應的script節點等)。

安裝好該外掛以後,在之前的webpack配置中,我們適當的修改:

  • 引用外掛,並new一個HtmlWebpackPlugin例項(不新增其他配置)
 const {resolve} = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
 module.exports = {
     mode: 'development',
     entry: {
@@ -7,5 +8,10 @@ module.exports = {
     output: {
         filename: 'main.js',
         path: resolve(__dirname, 'dist')
-    }
+    },
+    plugins: [
+        new HtmlWebpackPlugin({
+
+        })
+    ]
 }

讓我們再次執行構建指令碼後,我們會發現,dist目錄中,不僅僅生成了main.js,還生成一個index.html:

020-a-new-indexhtml

透過檢查這個index.html的內容可以看到,這個外掛不僅僅幫我們生成了一個html,還在這個html中的head節點中建立了一個script節點,並且src屬性填寫的是main.js。

030-check-new-indexhtml

此時,我們使用瀏覽器直接開啟這個index.html,儘管是在檔案系統,但瀏覽器還是可以透過script節點中的屬性`src="main.js",從index.html所在同級目錄中載入main.js。然而,執行起來有報錯:

040-raw-indexhtml-script

PS:這裡有同學可能會認為是script節點在body以前載入的,所以會報錯。但是實際不是這樣的,這裡script節點中有一個defer屬性,這個屬性表明,文件載入完畢以後才會執行main.js(MDN - defer),所以,我們不用擔心由於DOM未載入完就執行js程式碼而造成報錯。

這個地方的問題在於:我們的main.js中會執行查詢id為app的元素,但是實際生成的html是沒有這個元素的。

為了解決上述的問題,我們希望能夠自定義生成index.html。通常的做法就是:

  1. 在專案根目錄建立一個public目錄,在其中建立一個index.html(專案根目錄/public/index.html),內容如下(重點是body裡面新增了<div id="app"></div>):
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>這是一個模板HTML</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="app">
  </div>
</body>
</html>
  1. 然後,修改webpack配置中,關於HtmlWebpackPlugin的配置,配置外掛template引數,表明使用上述的建立的index.html:
     plugins: [
         new HtmlWebpackPlugin({
+            template: resolve(__dirname, 'public', 'index.html')
         })
     ]

我們再次執行構建,可以看到在dist目錄下的index.html是基於我們提供的模板生成:

050-indexhtml-by-custom

此時,我們再次開啟這個html,可以看到正確的處理後的結果:

060-right-handle

output.path與output.filename

讓我們回到關於output.path與output.filename上來。回顧我們的webpack配置:

  • output.filename:確定js最終生成的檔名

  • output.path:確定js所在的根路徑

js最終生成的路徑是:

output.path(絕對路徑) + output.filename(檔名,可以有相對路徑字首)

做一個簡單的實驗便可知,例如,我們修改配置如下,把output.filename改為"js/main.js",output.path改為'resolve(__diranme, "my-dist")'

     output: {
-        filename: 'main.js',
-        path: resolve(__dirname, 'dist')
+        filename: 'js/main.js',
+        path: resolve(__dirname, 'my-dist')
     },

重新經過構建以後,我們會看到my-dist目錄被建立,並且這個目錄下面還會建立js目錄,js目錄中會有main.js,正好匹配了output.path(專案根目錄/my-dist) + output.filename(js/main.js)

070-new-output-path-and-filename

但是,output.filename與output.path僅僅影響js的生成嗎?不然,讓我看看這兩個引數對於HtmlWebpackPlugin的關聯關係。

與HtmlWebpackPlugin的關聯

對於上述生成結果,我們會注意到,在webpack配置中的HtmlWebpackPlugin外掛部分,我們沒有編寫過任何關於index.html的生成路徑的配置,但這個index.html最終也生成到了"my-dist"目錄下(與output.path一致);此外,我們還可以發現,生成的index.html裡面的script節點的src屬性,是"js/mian.js"(與output.filename一致)。

我們可以整理一個圖,來描述相關配置與js構建、HtmlWebpackPlugin外掛的關聯關係:080-v1-path-filename

總結來說,output.path與output.filename不能單純只作為輸出js的配置,HtmlWebpackPlugin也會使用它們:

  • HtmlWebpackPlugin會使用output.path + 外掛本身的filename配置,作為html的生成路徑;
  • HtmlWebpackPlugin會使用output.filename作為生成的html中script節點src屬性的js路徑(特別注意:這裡還不準確,後續會補充修正!)。

讀者可以根據上述的表格,自己進行實驗驗證。

關於output.filename的注意點

對於output.filename,需要注意的是,不能是一個絕對路徑,譬如:"/js/main.js" or "/main.js",一旦配置成了絕對路徑,就會看到報錯:

configuration.output.filename: A relative path is expected. However, the provided value "/js/main.js" is an absolute path!
   Please use output.path to specify absolute path and output.filename for the file name.

你只能寫成:"js/main.js""./js/main.js"。然而,由於生成的html中script節點屬性src的值,來源於這個output.filename值,如果我們有需求,希望生成的src等於一個絕對路徑,譬如:src="/js/main.js",僅僅靠output.filename是不行的。於是乎,output.publicPath就登場了!

output.publicPath

首先,在webpack中,這個引數不配置的話,預設是空字串""。然後,我們需要糾正我們前面的一個結論

  • HtmlWebpackPlugin會使用output.filename作為生成的html中script節點src屬性的js路徑

實際上,script節點的src屬性的路徑,並不只是output.filename來決定的,而是由output.publicPath與output.filename共同決定:

src = output.publicPath(還有斜槓的特殊處理,後面講)+ output.filename

只是因為output.publicPath預設是空字串,所以我們前面生成出來的只是src="js/main.js"。這裡,我們可以做一個簡單的實驗,配置publicPath為"/",則生成的節點就會成為:<script src="/js/main.js">

090-publicPath-root

output.publicPath: "abc"(尾部沒有"/"),src="abc/js/main.js":

100-publicPath-abc

output.publicPath: "/abc"(尾部依然沒有"/"),src="/abc/js/main.js":

110-publicPath-root-abc

仔細觀察這幾種場景,就可以知道HtmlWebpackPlugin外掛,在生成html中的script標籤時候,其中的src屬性依賴output.filename以及output.publicPath,並且規則為:

  • publicPath為空白字串(預設),則src="${output.filename}";
  • publicPath非空且不以"/"結尾,則src="\({output.publicPath}/\){output.filename}"(補充了一個"/");
  • publicPath非空且以"/"結尾,則src="\({output.publicPath}\){output.filename}";

需要注意的是,謹記js檔案與html檔案的生成不會受到output.publicPath的影響,只跟output.path和filename(js是output.filename,html是HtmlWebpackPlugin的filename)相關。

於是乎,我們重新整理前面的關係圖,把output.publicPath配置引入:

120-v2-path-filename-publicPath

細心的讀者已經想到了,假如publicPath配置成了"/static/",影響了HtmlWebpackPlugin中的script節點的src屬性路徑;而js檔案實際生成路徑僅受到output.path+output.filename,勢必造成js訪問路徑不匹配的問題:

130-path-not-match.png

所以,日常對於webpack的配置一定要注意這種路徑問題,保持匹配,否則使用webpack-dev-server就會出現問題~

相信看到這裡,很多讀者對output中的path、filename以及publicPath能夠理解他們的效果了。接下來,我們舉一反三,引入常用的CSS打包工具MiniCssExtractPlugin也來分析一下。

引入MiniCssExtractPlugin

我們通常會有這樣的需求,一個前端專案打包的時候,希望能夠將專案依賴的css檔案最終抽離為一個或N個css檔案,並讓我們的前端html直接以link節點的形式載入。這個時候,我們一般使用MiniCssExtractPlugin來完成這個需求。當然,除了這個外掛以外,我們還需要一個最基礎的loader:css-loader

yarn add -D css-loader mini-css-extract-plugin

工程結構不會變化:

專案根目錄/
├─ package.json
├─ public
│    └─ index.html
├─ src
│    └─ index.js
└─ webpack.config.js

內容主要是新增了css-loader與mini-css-extract-plugin。

140-project-content

接下來,我們編寫一個簡單的css樣式檔案存放於src目錄下(src/my-style.css):

body {
    background-color: aqua;
}

#app {
    background-color: azure;
}

並修改index.js的程式碼,在index.js中引用它:

+import './my-style.css';
 document.getElementById('app').innerText = 'hello, world'

此時,如果我們不進行任何的配置,執行webpack打包,會看到報錯:

ERROR in ./src/my-style.css 1:5
Module parse failed: Unexpected token (1:5)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

核心問題在於,webpack無法處理index.js中關於.css的檔案(webpack預設值處理js檔案)。所以,需要我們配置專門處理css的規則:

+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
      ... ...
      plugins: [
         new HtmlWebpackPlugin({
             template: resolve(__dirname, 'public', 'index.html')
         }),
+        new MiniCssExtractPlugin({
+            filename: 'css/main.css'
+        })
     ],
+    module: {
+        rules: [{
+            test: /\.css/,
+            use: [MiniCssExtractPlugin.loader, 'css-loader']
+        }]
+    }
}

首先引入MiniCssExtractPlugin外掛;然後在plugins中,new出MiniCssExtractPlugin外掛例項,並傳入filename配置css/main.css;最後,配置module.rules中,新增對css的處理:

loader的執行順序是按照陣列從後向前的,所以use陣列最後是css-loader,然後才是MiniCssExtractPlugin提供的loader。webpack在構建過程,遇到引用css的場景,則先呼叫css-loader,對css檔案進行處理,然後呼叫MiniCssExtractPlugin提供的loader進行抽取

完成配置以後,我們再次啟動webpack的構建,會看到dist目錄下,又會產生一個css目錄,裡面存放的就是mian.js,並且,檢查index.html會發現這一次除了script標籤外,還插入了link標籤:

150-css-output

有的讀者可能已經能夠推斷出,這個link標籤的href路徑,也是根據output.publicPath+MiniCssExtractPlugin外掛的filename組合而來。這裡直接給出結論,就是這樣的。我們再次更新圖表,把匯出css樣式檔案的MiniCssExtractPlugin外掛與相關的配置關係也總結進去,得到如下最終版關係圖:

160-v3-path-filename-publicPath

關於關係圖的補充

透過關係圖,我們很容易知道,webpack中關於檔案生成最核心的配置就是output.path以及各種filename,js的生成、css的生成、html的生成都依賴了這套配置;

其次,與js相關的output.filename和與css相關的MiniCssExtractPlugin.filename配置都有兩個作用:

  1. js、css的生成檔案路徑;
  2. 被HtmlWebpackPlugin使用,以生成script節點和link節點中的資源路徑(當然這個過程還有output.publicPath的參與)。

最後,本文並沒有講到webpack-dev-server和上述配置的關係,這個會在本《掌握webpack》系列中單獨出一期。

相關文章