webpack的核心概念,放到2022年相信很多的小夥伴都已經非常清楚了。但是,對於webpack配置中的output.path、output.filename以及output.publicPath,還有很多小夥伴還不理解。本文講圍繞output.filename、output.path與output.publicPath,講解它們的功能,並分析這些配置與webpack中常使用到的MiniCssExtractPlugin、HtmlWebpackPlugin等外掛的關係。
直接上總結圖
基礎環境搭建
我們現在基於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。
注意:這裡並沒有配置關於js的解析,因為webpack預設就會處理js檔案。
引入HtmlWebpackPlugin
僅僅是生成目標js檔案,可能還不是我們期望的效果。對於一個專案來說,我們通常還希望有一個html來展示UI,並執行js程式碼,但是手工建立可能不能是一個好的方案。這裡,我們引入本專案的第一個外掛:HtmlWebpackPlugin。
yarn add -D html-webpack-plugin
HtmlWebpackPlugin外掛基礎功能:
- 它會使用一個模板來生成一個html;
- 在生成的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:
透過檢查這個index.html的內容可以看到,這個外掛不僅僅幫我們生成了一個html,還在這個html中的head節點中建立了一個script節點,並且src屬性填寫的是main.js。
此時,我們使用瀏覽器直接開啟這個index.html,儘管是在檔案系統,但瀏覽器還是可以透過script節點中的屬性`src="main.js",從index.html所在同級目錄中載入main.js。然而,執行起來有報錯:
PS:這裡有同學可能會認為是script節點在body以前載入的,所以會報錯。但是實際不是這樣的,這裡script節點中有一個
defer
屬性,這個屬性表明,文件載入完畢以後才會執行main.js(MDN - defer),所以,我們不用擔心由於DOM未載入完就執行js程式碼而造成報錯。
這個地方的問題在於:我們的main.js中會執行查詢id為app的元素,但是實際生成的html是沒有這個元素的。
為了解決上述的問題,我們希望能夠自定義生成index.html。通常的做法就是:
- 在專案根目錄建立一個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>
- 然後,修改webpack配置中,關於HtmlWebpackPlugin的配置,配置外掛
template
引數,表明使用上述的建立的index.html:
plugins: [
new HtmlWebpackPlugin({
+ template: resolve(__dirname, 'public', 'index.html')
})
]
我們再次執行構建,可以看到在dist目錄下的index.html是基於我們提供的模板生成:
此時,我們再次開啟這個html,可以看到正確的處理後的結果:
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)
。
但是,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外掛的關聯關係:
總結來說,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">
output.publicPath: "abc"(尾部沒有"/"),src="abc/js/main.js":
output.publicPath: "/abc"(尾部依然沒有"/"),src="/abc/js/main.js":
仔細觀察這幾種場景,就可以知道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配置引入:
細心的讀者已經想到了,假如publicPath配置成了"/static/",影響了HtmlWebpackPlugin中的script節點的src屬性路徑;而js檔案實際生成路徑僅受到output.path+output.filename,勢必造成js訪問路徑不匹配的問題:
所以,日常對於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。
接下來,我們編寫一個簡單的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標籤:
有的讀者可能已經能夠推斷出,這個link標籤的href路徑,也是根據output.publicPath+MiniCssExtractPlugin外掛的filename組合而來。這裡直接給出結論,就是這樣的。我們再次更新圖表,把匯出css樣式檔案的MiniCssExtractPlugin外掛與相關的配置關係也總結進去,得到如下最終版關係圖:
關於關係圖的補充
透過關係圖,我們很容易知道,webpack中關於檔案生成最核心的配置就是output.path以及各種filename,js的生成、css的生成、html的生成都依賴了這套配置;
其次,與js相關的output.filename和與css相關的MiniCssExtractPlugin.filename配置都有兩個作用:
- js、css的生成檔案路徑;
- 被HtmlWebpackPlugin使用,以生成script節點和link節點中的資源路徑(當然這個過程還有output.publicPath的參與)。
最後,本文並沒有講到webpack-dev-server和上述配置的關係,這個會在本《掌握webpack》系列中單獨出一期。