一直覺得,個人Webpack
就是複製粘貼的水平,而對Webpack
的知識真的很模糊,甚至是純小白。因此前段時間開始對Webpack
進行比較系統的學習。javascript
學習完成後,我抽空整理了筆記,前先後後也花了一週多。最後以爲能夠分享出來,讓對Webpack
還很模糊的朋友,能夠學習一下。css
固然,讀完本文,你會發現Webpack
還有更多更深的東西值得咱們去學習,所以這只是一個開始,從零開始。html
在學習webpack
以前,咱們須要先來捋一捋三個術語——module
、chunk
和bundle
。java
先看看webpack
官方對module
的解讀:node
Module
是離散功能塊,相比於完整程序提供了更小的接觸面。精心編寫的模塊提供了可靠的抽象和封裝界限,使得應用程序中每一個模塊都具備條理清楚的設計和明確的目的。jquery
其實簡單來講,module
模塊就是咱們編寫的代碼文件,好比JavaScript
文件、CSS
文件、Image
文件、Font
文件等等,它們都是屬於module
模塊。而module
模塊的一個特色,就是能夠被引入使用。webpack
一樣的先看看官方解讀:git
此
webpack
特定術語在內部用於管理捆綁過程。輸出束(bundle)由塊組成,其中有幾種類型(例如entry
和child
)。一般,塊直接與輸出束 (bundle
)相對應,可是,有些配置不會產生一對一的關係github
其實chunk
是webpack
打包過程的中間產物,webpack
會根據文件的引入關係生成chunk
,也就是說一個chunk
是由一個module
或多個module
組成的,這取決於有沒有引入其餘的module
。web
先看看官方解讀:
bundle
由許多不一樣的模塊生成,包含已經通過加載和編譯過程的源文件的最終版本。
bundle
實際上是webpack
的最終產物,一般來講,一個bundle
對應這一個chunk
。
其實module
、chunk
和bundle
能夠說是同一份代碼在不一樣轉換場景的不一樣名稱:
module
webpack
處理時時chunk
bundle
咱們經過一個小demo
來過一下,如今有一個項目,路徑以下:
src/
├── index.css
├── index.js
├── common.js
└── utils.js
複製代碼
而後咱們有兩個入口文件,一個是index.js
,一個是utils.js
,在index.js
中引入了index.css
和common.js
。而後經過webpack
打包出來了index.bundle.css
、index.bundle.js
和utils.bundle.js
。
好,介紹完背景後,咱們就能夠來分析一下module
、chunk
和bundle
。
首先,咱們編寫的代碼,就是module
,也就是說index.css
、common.js
、index.js
和utils.js
共四個module
文件。
其次,咱們有兩個入口文件,分別爲index.js
和utils.js
,而且它們最後是獨立打包成bundle
的,從而在webpack
打包過程當中就會造成兩個chunk
文件,而由index.js
造成chunk
還包含着index.js
引入的module
——common.js
和index.css
。
最後,咱們打包出來了index.bundle.css
、index.bundle.js
和uitls.bundle.js
,這三個也就是bundle
文件。
最後,咱們能夠總結一下三者之間的關係:一個budnle
對應着一個chunk
,一個chunk
對應着一個或多個module
。
接下來,咱們經過一步步實踐,來慢慢學習webpack
,這篇文章使用的是webpack5
。
首先,新建一個項目文件夾,而後初始化項目。
yarn init -y
複製代碼
而後安裝一下webpack
。當咱們使用webpack
時,還須要安裝webpack-cli
。
由於webpack
只是在開發環境纔會使用到,因此咱們只須要添加到devDependencies
便可。
# webpack -> 5.47.0, webpack-cli-> 4.7.2
yarn add webpack webpack-cli -D
複製代碼
而後再項目中新建src
路徑,再新建一個index.js
:
console.log("Hello OUDUIDUI");
複製代碼
而後執行npx webpack
,則執行webpack
打包。這時你的項目就會多一個dist
文件夾,而且在dist
文件夾中會看到一個main.js
,裏面的代碼跟index.js
同樣。
固然,咱們能夠在package.json
中編輯script
命令:
"scripts": {
"dev": "webpack"
}
複製代碼
而後執行yarn dev
,也能夠成功打包。
若是使用過webpack
的朋友應該知道,webpack
其實有一個配置文件——webpack.config.js
。
但爲何前面的初始化測試時,咱們沒有編輯配置文件卻能夠成功打包?這是由於webpack
會有一個默認配置,當它檢測到咱們沒有配置文件的時候,它默認會使用本身的默認配置。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
},
};
複製代碼
首先,咱們簡單來過一下這些默認配置叭。
entry
選項是用來配置入口文件的,它能夠是字符串、數組或者對象類型。webpack
默認只支持js
和json
文件做爲入口文件,所以若是引入其餘類型文件會保存。
output
選項是設置輸出配置,該選項必須是對象類型,不能是其它類型格式。在output
對象中,必填的兩個選項就是導出路徑path
和導出bundle
文件名稱filename
。其中path
選項必須爲絕對路徑。
entry
和output
的配置,對於不一樣的應用場景的配置也會有所不一樣。
咱們最廣泛的就是單個入口文件,而後打包成單個bundle
文件。這種應用場景下,entry
可使用字符串的形式,則跟默認配置文件相似:
entry: './src/index.js'
複製代碼
當咱們的項目須要有多個入口文件,但只須要一個輸出bundle
的時候,這時候entry
可使用數組的形式:
entry: ['./src/index_1.js', './src/index_2.js']
複製代碼
注意:此時其實只有一個chunk
當咱們的項目同時多個入口文件,而且它們須要單獨打包,也就是意味着會有多個bundle
文件輸出,此時咱們的entry
須要使用對象形式,而且對象key
對應的對應chunk
的名稱。
entry: {
index: "./src/index.js", // chunkName爲index
main: "./src/main.js" // chunkName爲main
}
複製代碼
此時,咱們的output.filename
也不能寫死了,這時候webpack
提供了一個佔位符[name]
給咱們使用,它會自動替換爲對應的chunkName
。
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // [name]佔位符會自動替換爲chunkName
},
複製代碼
根據上面的配置,最後會打包出index.js
和main.js
。
在單入口單輸出的應用場景下,entry
也可使用對象的形式,從而來自定義chunkName
,而後output.filename
也使用[name]
佔位符來自動匹配。固然也可使用數組,可是不太大必要。
當entry
使用數組或字符串的時候,chunkName
默認爲main
,所以若是output.filename
使用[name]
佔位符的時候,會自動替換爲main
。
在前面的打包測試的時候,命令行都會報一個警告:
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.
複製代碼
這是由於webpack
須要咱們配置mode
選項。
wepack給咱們提供了三個選項,即none
、development
和production
,而默認就是production
。
三者的區別呢,在於webpack
自帶的代碼壓縮和優化插件使用。
none
:不使用任何默認優化選項;
development
:指的是開發環境,會默認開啓一些有利於開發調試的選項,好比NamedModulesPlugin
和NamedChunksPlugin
,分別是給module
和chunk
命名的,而默認是一個數組,對應的chunkName
也只是下標,不利於開發調試;
production
:指的是生產環境,則會開啓代碼壓縮和代碼性能優化的插件,從而打包出來的文件也相對none
和development
小不少。
當咱們設置mode以後,咱們能夠在
process.env.NODE_ENV
獲取到當前的環境
所以咱們能夠在配置文件上文件上配置mode
:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
// 開啓source-map
devtool: "source-map"
};
複製代碼
webpack
也給咱們提供了另外一種方式,就是在命令行中配置,也就是加上--mode
:
// package.json
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
複製代碼
聊完mode
後,說到開發調試,不難想起的就是sourceMap
。而咱們能夠在配置文件中,使用devtool
開啓它。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
// 開啓source-map
devtool: "source-map"
};
複製代碼
打包後,你的dist
中就會多了一個main.js.map
文件。
固然,官方不止提供這麼一個選項,具體的能夠去官網看看,這裏就說其餘幾個比較經常使用的選項。
none
:不會生成sourceMap
;
eval
:每一個模塊都會使用eval()
執行,不建議生成環境中使用;
cheap-source-map
:生成sourceMap
,可是沒有列映射,則只會提醒是在代碼的第幾行,不會提示到第幾列;
inline-source-map
:會生成sourceMap
,但不會生成map
文件,而是將sourceMap
放在打包文件中。
前面咱們有提到過,就是webpack
的入口文件只能接收JavaScript
文件和JSON
文件。
但咱們一般項目還會有其餘類型的文件,好比html
、css
、圖片、字體等等,這時候咱們就須要用到第三方loader
來幫助webpack
來解析這些文件。理論上只要有相應的loader
,就能夠處理任何類型的文件。
在
webpack
官網其實提供了不少loader
,已經能知足咱們平常使用,固然咱們也能夠去github
找找別人寫的loader
或者本身手寫loader
來使用。
而對於loader
的配置,是寫着module
選項裏面的。module
選項是一個對象,它裏面有一個rules
屬性,是一個數組,在裏面咱們能夠配置多個匹配規則。
而匹配規則是一個對象,會有test
屬性和use
屬性,test
屬性通常是正則表達式,用來識別文件類型,而use
屬性是一個數組,裏面用來存放對該文件類型使用的loader
。
module: {
rules: [
{
test: /\.css$/, // 識別css文件
use: ['style-loader', 'css-loader'] // 對css文件使用的三個loader
}
]
}
複製代碼
對於use
數組的順序是有要求的,webpack
會根據自後向前的規則去執行loader
。也就是說,上面的例子webpack
會先執行css-loader
,再執行style-loader
。
其次,當咱們須要對對應loader
提供配置的時候,咱們能夠選用對象寫法:
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
// loader名稱
loader: 'css-loader',
// loader選項
options: {
...
}
}
]
}
]
}
複製代碼
在後面咱們根據實際應用場景再講講module
的使用。
webpack
還提供了一個plugins
選項,讓咱們可使用一些第三方插件,所以咱們可使用第三方插件來實現打包優化、資源管理、注入環境變量等任務。
一樣的,
webpack
官方也提供了不少plugin
。
plugins
選項是一個數組,裏面能夠放入多個plugin
插件。
plugins: [
new htmlWebpackPlugin(),
new CleanWebpackPlugin(),
new miniCssExtractPlugin(),
new TxtWebpackPlugin()
]
複製代碼
而對於plugins
數組對排序位置是沒有要求,由於在plugin
的實現中,webpack
會經過打包過程的生命週期鉤子,所以在插件邏輯中就已經設置好須要在哪一個生命週期執行哪些任務。
當咱們是Web
項目的時候,咱們必然會存在html
文件去實現頁面。
而對於其餘類型的文件,好比css
、圖片、文件等等,咱們是能夠經過引入入口js
文件,而後經過loader
進行解析打包。而對於html
文件,咱們不可能將其引入到入口文件而後解析打包,反而咱們還須要將打包出來的bundle
文件引入html
文件去使用,
所以,其實咱們須要實現的操做只有兩個,一個是複製一份html
文件到打包路徑下,另外一個就是將打包出來的bundle
文件自動引入到html
文件中去。
這時候咱們須要使用一個插件來實現這些功能——html-webpack-plugin
。
# 5.3.2
yarn add html-webpack-plugin -D
複製代碼
安裝插件後,咱們先在src
文件下新建一下index.html
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Demo</title>
</head>
<body>
<div>Hello World</div>
</body>
</html>
複製代碼
這裏面咱們暫時不須要引入任何模塊。
接下來配置一下webpack
。通常plugin
插件都是一個類,而咱們須要在plugins
選項中須要建立一個插件實例。
對於htmlWebpackPlugin
插件,咱們須要傳入一些配置:html
模板地址template
和打包出來的文件名filename
。
const path = require('path');
// 引入htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
// 使用htmlWebpackPlugin插件
new htmlWebpackPlugin({
// 指定html模板
template: './src/index.html',
// 自定義打包的文件名
filename: 'index.html'
})
]
};
複製代碼
接下來執行一下打包,就會發現dist
文件下會生成一個index.html
。打開會發現,webpack
會自動將bundle
文件引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Demo</title>
<script defer src="main.js"></script></head>
<body>
<div>Hello World</div>
</body>
</html>
複製代碼
若是咱們有多個chunk
的時候,咱們能夠指定該html
要引入哪些chunk
。在htmlWebpackPlugin
配置中有一個chunks
選項,是一個數組,你只須要加入你想引入的chunkName
便可。
const path = require('path');
// 引入htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
main: './src/main.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
new htmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ["index"] // 只引入index chunk
})
]
};
複製代碼
打包完成後,dist
文件下會出現index.html
、index.js
和main.js
,可是index.html
只會引入index.js
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="index.js"></script></head>
<body>
HelloWorld!
</body>
</html>
複製代碼
若是咱們須要實現多頁面的話,只須要再new
一個htmlWebpackPlugin
實例便可,這裏就再也不多說。
在每次打包前,咱們其實都須要去清空一下打包路徑的文件。
若是文件重名的話,webpack
還會自動覆蓋,可是實際中咱們都會在打包文件名稱中加入哈希值,所以清空的操做不得不實現。
這時候咱們須要使用一個插件——clean-webpack-plugin
。
yarn add clean-webpack-plugin -D
複製代碼
而後只需引入到配置文件且在plugins
配置就可使用了。
const path = require('path');
// 引入CleanWebpackPlugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
publicPath: ''
},
plugins: [
// 使用CleanWebpackPlugin
new CleanWebpackPlugin(),
]
};
複製代碼
有些狀況下,咱們不須要徹底清空打包路徑,這時候咱們可使用到一個選項,叫cleanOnceBeforeBuildPatterns
,它是一個數組,默認是[**/*]
,也就是清理output.path
路徑下全部東西。而你能夠在裏面輸入只想刪除的文件,同時咱們能夠輸入不想刪除的文件,只須要在前面加上一個!
。
須要注意的是,
cleanOnceBeforeBuildPatterns
這個選項是能夠刪除打包路徑下以外的文件,只須要你配上絕對路徑的話。所以CleanWebpackPlugin
還提供了一個選項供測試——dry
,默認是爲false
,當你設置爲true
後,它就不會真正的執行刪除,而是隻會在命令行打印出被刪除的文件,這樣子更好的避免測試過程當中誤刪。
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
publicPath: ''
},
plugins: [
new CleanWebpackPlugin({
// dry: true // 打開可測試,不會真正執行刪除動做
cleanOnceBeforeBuildPatterns: [
'**/*', // 刪除dist路徑下全部文件
`!package.json`, // 不刪除dist/package.json文件
],
}),
]
};
複製代碼
當咱們使用webpack
的時候,不只僅只是用於打包文件,大部分咱們還會依賴webpack
來搭建本地服務,同時利用其熱更新的功能,讓咱們更好的開發和調試代碼。
接下來咱們來安裝一下webpack-dev-server
:
# 版本爲3.11.2
yarn add webpack-dev-server -D
複製代碼
而後執行下列代碼開啓服務:
npx webpack serve
複製代碼
或者在package.json配置一下:
"scripts": {
"serve": "webpack serve --mode development"
}
複製代碼
而後經過yarn serve
運行。
這時,webpack會默認開啓http://localhost:8080/
服務(具體看大家運行返回的代碼),而該服務指向的是dist/index.html
。
但你會發現,你的dist
中實際上是沒有任何文件的,這是由於webpack
將實時編譯後的文件都保存到了內存當中。
其實webpack
自帶提供了--watch
命令,能夠實現動態監聽文件的改變並實時打包,輸出新的打包文件。
但這種方案存在着幾個缺點,一就是每次你一修改代碼,webpack就會所有文件進行從新打包,這時候每次更新打包的速度就會慢了不少;其次,這樣的監聽方式作不到熱更新,即每次你修改代碼後,webpack從新編譯打包後,你就得手動刷新瀏覽器,才能看到最新的頁面結果。
而webpack-dev-server
,卻有效了彌補這兩個問題。它的實現,是使用了express
啓動了一個http
服務器,來伺候資源文件。而後這個http
服務器和客戶端使用了websocket
通信協議,當原始文件做出改動後,webpack-dev-server
就會實時編譯,而後將最後編譯文件實時渲染到頁面上。
在webpack.config.js
中,有一個devServer
選項是用來配置webpack-dev-server
,這裏簡單講幾個比較經常使用的配置。
咱們能夠經過port來設置服務器端口號。
module.exports = {
...
// 配置webpack-dev-server
devServer: {
port: 8888, // 自定義端口號
},
};
複製代碼
在devServer
中有一個open
選項,默認是爲false
,當你設置爲true
的時候,你每次運行webpack-dev-server
就會自動幫你打開瀏覽器。
module.exports = {
...
// 配置webpack-dev-server
devServer: {
open: true, // 自動打開瀏覽器窗口
},
};
複製代碼
這個選項是用來設置本地開發的跨域代理的,關於跨域的知識就很少在這說了,這裏就說說如何去配置。
proxy
的值必須是一個對象,在裏面咱們能夠配置一個或多個跨域代理。最簡單的配置寫法就是地址配上api
地址。
module.exports = {
...
devServer: {
// 跨域代理
proxy: {
'/api': 'http://localhost:3000'
},
},
};
複製代碼
這時候,當你請求/api/users
的時候,就會代理到http://localhost:3000/api/users
。
若是你不須要傳遞/api
的話,你就須要使用對象的寫法,從而增長一些配置選項:
module.exports = {
//...
devServer: {
// 跨域代理
proxy: {
'/api': {
target: 'http://localhost:3000', // 代理地址
pathRewrite: { '^/api': '' }, // 重寫路徑
},
},
},
};
複製代碼
這時候,當你請求/api/users
的時候,就會代理到http://localhost:3000/users
。
在proxy中的選項,還有兩個比較經常使用的,一個就是changeOrigin
,默認狀況下,代理時會保留主機頭的來源,當咱們將其設置爲true
能夠覆蓋這種行爲;還有一個是secure
選項,當你的接口使用了https
的時候,須要將其設置爲false
。
module.exports = {
//...
devServer: {
// 跨域代理
proxy: {
'/api': {
target: 'http://localhost:3000', // 代理地址
pathRewrite: { '^/api': '' }, // 重寫路徑
secure: false, // 使用https
changeOrigin: true // 覆蓋主機源
},
},
},
};
複製代碼
接下來說講關於webpack
對css
的解析處理叭。
在前面的例子也能看到,咱們解析css
須要用到的loader
有css-loader
和style-loader
。css-loader
主要用來解析css
文件,而style-loader
是將css
渲染到DOM
節點上。
首先咱們來安裝一下:
# css-loader -> 6.2.0; style-loader -> 3.2.1
yarn add css-loader style-loader -D
複製代碼
而後咱們新建一個css
文件。
/* style.css */
body {
background: #222;
color: #fff;
}
複製代碼
而後在index.js
引入一下:
import "./style.css";
複製代碼
緊接着咱們配置一下webpack
:
module.exports = {
...
module: {
rules: [
{
test: /\.css$/, // 識別css文件
use: ['style-loader', 'css-loader'] // 先使用css-loader,再使用style-loader
}
]
},
...
};
複製代碼
這時候咱們打包一下,會發現dist
路徑下只有main.js
和index.html
。但打開一下index.html
會發現css
是生效的。
這是由於style-loader
是將css
代碼插入到了main.js
當中去了。
若是咱們不想將css
代碼放進js
中,而是直接導出一份css
文件的話,就得使用另外一個插件——mini-css-extract-plugin
。
# 2.1.0
yarn add mini-css-extract-plugin -D
複製代碼
而後將其引入到配置文件,而且在plugins
引入。
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...
plugins: [
// 使用miniCssExtractPlugin插件
new miniCssExtractPlugin({
filename: "[name].css" // 設置導出css名稱,[name]佔位符對應chunkName
})
]
};
複製代碼
緊接着,咱們還須要更改一下loader
,咱們再也不使用style-loader
,而是使用miniCssExtractPlugin
提供的loader
。
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
// 使用miniCssExtractPlugin.loader替換style-loader
use: [miniCssExtractPlugin.loader,'css-loader']
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: "[name].css"
})
]
};
複製代碼
接下來打包一下,dist
路徑下就會多出一個main.css
文件,而且在index.html
中也會自動引入。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="main.js"></script><link href="main.css" rel="stylesheet"></head>
<body>
HelloWorld!
</body>
</html>
複製代碼
當咱們使用一下css
新特性的時候,可能須要考慮到瀏覽器兼容的問題,這時候可能須要對一些css
屬性添加瀏覽器前綴。而這類工做,其實能夠交給webpack
去實現。準確來講,是交給postcss
去實現。
postcss
對於css
猶如babel
對於JavaScript
,它專一於對轉換css
,好比添加前綴兼容、壓縮css
代碼等等。
首先咱們須要先安裝一下postcss
和post-css-loader
。
# postcss -> 8.3.6,postcss-loader -> 6.1.1
yarn add postcss postcss-loader -D
複製代碼
接下來,咱們在webpack
配置文件先引入postcss-loader
,它的順序是在css-loader
以前執行的。
rules: [
{
test: /\.css$/,
// 引入postcss-loader
use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
]
複製代碼
接下來配置postcss
的工做,就不在webpack
的配置文件裏面了。postcss
自身也是有配置文件的,咱們須要在項目根路徑下新建一個postcss,config.js
。而後裏面也有一個配置項,爲plugins
。
module.exports = {
plugins: []
}
複製代碼
這也意味着,postcss
自身也支持不少第三方插件使用。
如今咱們想實現的添加前綴的功能,須要安裝的插件叫autoprefixer
。
# 1.22.10
yarn add autoprefixer -D
複製代碼
而後咱們只須要引入到postcss
的配置文件中,而且它裏面會有一個配置選項,叫overrideBrowserslist
,是用來填寫適用瀏覽器的版本。
module.exports = {
plugins: [
// 將css編譯爲適應於多版本瀏覽器
require('autoprefixer')({
// 覆蓋瀏覽器版本
// last 2 versions: 兼容各個瀏覽器最新的兩個版本
// > 1%: 瀏覽器全球使用佔有率大於1%
overrideBrowserslist: ['last 2 versions', '> 1%']
})
]
}
複製代碼
關於overrideBrowserslist
的選項填寫,咱們能夠去參考一下browserslist,這裏就很少講。
固然,咱們其實能夠在package.json
中填寫兼容瀏覽器信息,或者使用browserslist
配置文件.browserslistrc
來填寫,這樣子若是咱們之後使用其餘插件也須要考慮到兼容瀏覽器的時候,就能夠統一用到,好比說babel
。
// package.json 文件
{
...
"browserslist": ['last 2 versions', '> 1%']
}
複製代碼
# .browserslsetrc 文件
last 2 versions
> 1%
複製代碼
但若是你多個地方都配置的話,overrideBrowserslist
的優先級是最高的。
接下來,咱們修改一下style.css
,使用一下比較新的特性。
body {
display: flex;
}
複製代碼
而後打包一下,看看打包出來後的main.css
。
body {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
複製代碼
當咱們須要壓縮css
代碼的時候,可使用postcss
另外一個插件——cssnano
。
# 5.0.7
yarn add cssnano -D
複製代碼
而後仍是在postcss
配置文件中引入:
module.exports = {
plugins: [
... ,
require('cssnano')
]
}
複製代碼
打包一下,看看main.css
。
body{display:-webkit-box;display:-ms-flexbox;display:flex}
複製代碼
在如今咱們實際開發中,咱們會更多使用Sass
、Less
或者stylus
這類css
預處理器。而其實html
是沒法直接解析這類文件的,所以咱們須要使用對應的loader
將其轉換成css
。
接下來,我就以sass
爲例,來說一下如何使用webpack
解析sass
。
首先咱們須要安裝一下sass
和sass-loader
。
# sass -> 1.36.0, sass-loader -> 12.1.0
yarn add sass sass-loader -D
複製代碼
而後咱們在module
加上sass
的匹配規則,sass-loader
的執行順序應該是排第一,咱們須要先將其轉換成css
,而後才能執行後續的操做。
rules: [
...
{
test: /\.(scss|sass)$/,
use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']
}
]
複製代碼
而後咱們在項目中新建一個style.scss
。
$color-white: #fff;
$color-black: #222;
body {
background: $color-black;
div {
color: $color-white;
}
}
複製代碼
而後在index.js
引入。
import "./style.css";
import "./style.scss";
複製代碼
而後執行打包,再看看打包出來的main.css
,scss
文件內容被解析到裏面,同時若是咱們引入多個css
或css
預處理器文件的話,miniCssExtractPlugin
也會將其打包成一個bundle
文件裏面。
body{display:-webkit-box;display:-ms-flexbox;display:flex}
body{background:#222}body div{color:#fff}
複製代碼
當咱們使用了圖片、視頻或字體等等其餘靜態資源的話,咱們須要用到url-loader
和file-loader
。
# url-loader -> 4.1.1; file-loader -> 6.2.0
yarn add url-loader file-loader -D
複製代碼
首先咱們在項目中引入一張圖片,而後在引入到index.js
中。
import pic from "./image.png";
const img = new Image();
img.src= pic;
document.querySelector('body').append(img);
複製代碼
而後我先使用url-loader
。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: ['url-loader']
}
]
}
};
複製代碼
而後執行一下打包。
你會發現,dist
路徑下沒有圖片文件,可是你打開頁面是能夠看到圖片的,且經過調試工具,咱們能夠看到其實url-loader
默認會將靜態資源轉成base64
。
固然,url-loader
選項有提供一個屬性,叫limit
,就是咱們能夠設置一個文件大小閾值,當文件大小超過這個值的時候,url-loader
就不會轉成base64
,而是直接打包成文件。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: [{
loader: 'url-loader',
options: {
name: '[name].[ext]', // 使用佔位符設置導出名稱
limit: 1024 * 10 // 設置轉成base64閾值,大於10k不轉成base64
}
}]
}
]
}
};
複製代碼
這時候咱們再打包一下,dist
文件夾下就會出現了圖片文件。
而file-loader
其實跟url-loader
差很少,但它默認就是導出文件,而不會導出base64
的。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: ['file-loader']
}
]
}
};
複製代碼
打包一下,會發現dist
文件夾下依舊會打包成一個圖片文件,可是它的名稱會被改爲哈希值,咱們能夠經過options
選項來設置導出的名稱。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]', // 使用佔位符設置導出名稱
}
}]
}
]
}
};
複製代碼
而對於視頻文件、字體文件,也是用相同的方法,只不過是修改test
。
module.exports = {
...
module: {
rules: [
// 圖片
{
test: /\.(png|je?pg|gif|webp)$/,
use: {
loader: 'url-loader',
options: {
esModule: false,
name: '[name].[ext]',
limit: 1024 * 10
}
}
},
// 字體
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 1024 * 10
}
}
},
// 媒體文件
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 1024 * 10
}
}
}
]
}
};
複製代碼
但如今有個問題,就是若是直接在index.html
引入圖片的話,能夠順利打包嗎?
答案是不會的,咱們能夠測試一下。首先將圖片引入index.html
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<img src="./image.png">
</body>
</html>
複製代碼
而後執行打包後,打包出來的index.html
照樣是<img src="./image.png">
,可是咱們並無解析和打包出來image.png
出來。
這時候咱們須要藉助另外一個插件——html-withimg-loader
。
# 0.1.16
yarn add html-withimg-loader -D
複製代碼
而後咱們再添加一條rules
。
{ test: /\.html$/,loader: 'html-withimg-loader' }
複製代碼
這時候打包成功後,dist
文件成功將圖片打包出來了,可是打開頁面的時候,圖片仍是展現不出來。而後經過調試工具看的話,會發現
<img src="{"default":"image.png"}">
複製代碼
這是由於html-loader
使用的是commonjs
進行解析的,而url-loader
默認是使用esmodule
解析的。所以咱們須要設置一下url-loader
。
{
test: /\.(png|je?pg|gif|webp)$/,
use: {
loader: 'url-loader',
options: {
esModule: false, // 不適用esmodule解析
name: '[name].[ext]',
limit: 1024 * 10
}
}
}
複製代碼
這時候從新打包一下,頁面就能成功展現圖片了。
在webpack5
中,新添了一個資源模塊,它容許使用資源文件(字體,圖標等)而無需配置額外 loader
,具體的內容你們能夠看看文檔,這裏簡單講一下如何操做。
前面的例子,咱們對靜態資源都使用了url-loader
或者file-loader
,而在webpack5
,咱們甚至能夠須要手動去安裝和使用這兩個loader
,而直接設置一個type
屬性。
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
type: "asset/resource",
}
複製代碼
而後打包測試後,靜態文件都會直接打包成文件並自動引入,效果跟file-loader
一致。
type
值提供了四個選項:
asset/resource
: 發送一個單獨的文件並導出 URL。以前經過使用 file-loader
實現。asset/inline
: 導出一個資源的 data URI。以前經過使用 url-loader
實現。asset/source
:**導出資源的源代碼。以前經過使用 raw-loader
實現。asset
: 在導出一個 data URI 和發送一個單獨的文件之間自動選擇。以前經過使用 url-loader
,而且配置資源體積限制實現。同時,咱們能夠在output
設置輸出bundle
靜態文件的名稱:
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
// 設置靜態bundle文件的名稱
assetModuleFilename: '[name][ext]'
}
複製代碼
不只僅css
須要轉義,JavaScript
也要爲了兼容多瀏覽器進行轉義,所以咱們須要用到babel
。
# 8.2.2
yarn add babel-loader -D
複製代碼
同時,咱們須要使用babel
中用於JavaScript
兼容的插件:
# @babel/preset-env -> 7.14.9; @babel/core -> 7.14.8; @core-js -> 3.16.0
yarn add @babel/preset-env @babel/core core-js -D
複製代碼
接下來,咱們須要配置一下webpack
的配置文件。
{
test: /\.js$/,
use: ['babel-loader']
}
複製代碼
而後咱們須要配置一下babel
。固然咱們能夠直接在webpack.config.js
裏面配置,可是babel
一樣也提供了配置文件.babelrc
,所以咱們就直接在這邊進行配置。
在根路徑新建一個.babelrc
。
{
"presets": [
[
"@babel/preset-env",
{
// 瀏覽器版本
"targets": {
"edge": "17",
"chrome": "67"
},
// 配置corejs版本,但須要額外安裝corejs
"corejs": 3,
// 加載狀況
// entry: 須要在入口文件進入@babel/polyfill,而後babel根據使用狀況按需載入
// usage: 無需引入,自動按需加載
// false: 入口文件引入,所有載入
"useBuiltIns": "usage"
}
]
]
}
複製代碼
接下來,咱們來測試一下,先修改一下index.js
。
new Promise(resolve => {
resolve('HelloWorld')
}).then(res => {
console.log(res);
})
複製代碼
而後執行yarn build
進行打包。
在使用babel
以前,打包出來的main.js
以下。
!function(){"use strict";new Promise((o=>{o("HelloWorld")})).then((o=>{console.log(o)}))}();
複製代碼
上面打包代碼是直接使用了Promise
,而沒有考慮到低版本瀏覽器的兼容。而後咱們打開babel
後,執行一下打包命令,會發現代碼多出了不少。
而在打包代碼中,能夠看到webpack
使用了polyfill
實現promise
類,而後再去調用,從而兼容了低版本瀏覽器沒有promise
屬性問題。
在目前咱們的測試代碼中,咱們的src
文件夾以下:
├── src
│ ├── Alata-Regular.ttf
│ ├── image.png
│ ├── index.html
│ ├── index.js
│ ├── style.css
│ └── style.scss
複製代碼
而正常項目的話,咱們會使用文件夾將其分好類,這並不難,咱們先簡單歸類一下。
├── src
│ ├── index.html
│ ├── js
│ │ └── index.js
│ ├── static
│ │ └── image.png
│ │ └── Alata-Regular.ttf
│ └── style
│ ├── style.css
│ └── style.scss
複製代碼
接下來,咱們須要打包出來的文件也是歸類好的,這裏就不太複雜,直接用一個assets
文件夾將全部靜態文件放進去,而後index.html
放外面。
├── dist
│ ├── assets
│ │ ├── Alata-Regular.ttf
│ │ ├── image.png
│ │ ├── main.css
│ │ └── main.js
│ └── index.html
複製代碼
這裏先補充一下style.css
引入字體的代碼:
@font-face {
font-family: "test-font";
src: url("../static/Alata-Regular.ttf") format('truetype')
}
body {
display: flex;
font-family: "test-font";
}
複製代碼
首先,咱們先將打包出來的JavaScript
文件放入assets
文件夾下,咱們只須要修改output.filename
便可。
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name].js'
}
複製代碼
其次,咱們將打包出來的css
文件也放入assets
路徑下,由於咱們打包css
是使用miniCssExtractPlugin
,所以咱們只須要配置一下miniCssExtractPlugin
的filename
便可:
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name].css"
})
]
複製代碼
最後就是靜態資源了,這裏咱們使用靜態模塊方案,因此直接修改output.assetModuleFilename
便可:
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name].js',
assetModuleFilename: 'assets/[name][ext]'
},
複製代碼
這時候打包一下,預覽一下頁面,發現都正常引入和使用。
一般,咱們打包文件的文件名都須要帶上一個哈希值,這會給咱們的好處就是避免緩存。
webpack
也提供了三種哈希值的策略,接下來咱們一一來看看:
爲了更好的比較三者之間的區別,這邊先調整一下項目和配置。
// index.js
import pic from "../static/image.png";
const img = new Image();
img.src = pic;
document.querySelector('body').append(img);
// main.js
import "../style/style.scss";
import "../style/style.css";
console.log('Hello World')
// webpack.config.js
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
複製代碼
hash
策略,是以項目爲單位的,也就是說,只要項目一個文件發生改動,首先打包後該文件對應的bundle
文件名會改變,其次全部js
文件和css
文件的文件名也會改變。
咱們先經過一個例子來看看:
首先咱們須要在全部設置filename
的地方加入[hash]
佔位符,同時咱們也能夠設置哈希值的長度,只需加上冒號和長度值便可,好比[hash:6]
。
module.exports = {
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name]-[hash:6].js',
assetModuleFilename: 'assets/[name]-[hash:6][ext]'
},
module: {
...
},
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name]-[hash:6].css"
}),
]
};
複製代碼
這時候打包一下,看看打包文件:
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-7fa71a.js
│ ├── main-7fa71a.css
│ └── main-7fa71a.js
└── index.html
複製代碼
而後我隨便改一下style.css
,再從新打包一下。
這時候會發現index.js
、main.js
、main.css
的文件名都會發生改變,但靜態文件並不會發生變化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-4b2329.js
│ ├── main-4b2329.css
│ └── main-4b2329.js
└── index.html
複製代碼
而後咱們從新找一張圖片,覆蓋一下image.png
,而後從新打包。
這時候,index.js
、main.js
、main.css
的文件名依舊會發生改變,同時image.png
也發生了變化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-46acaa.js
│ ├── main-46acaa.css
│ └── main-46acaa.js
└── index.html
複製代碼
經過上面的例子,咱們能夠簡單總結出:
js
、css
打包文件的文件名都會發生變化,儘管來自多個chunk
。js
、css
打包文件的文件名也都會發生變化。而chunkhash
策略的話,是以chunk
爲單位的,若是一個文件發生變化,只有那條chunk
相關的文件的打包文件文件名纔會發生變化。
咱們依舊經過例子看看:
首先咱們先將配置文件都改爲chunkhash
。這裏注意的是chunkhash
不適用於靜態文件,所以靜態文件依舊使用hash
。
module.exports = {
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name]-[chunkhash:6].js',
assetModuleFilename: 'assets/[name]-[hash:6][ext]'
},
module: {
...
},
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name]-[chunkhash:6].css"
}),
]
};
複製代碼
先打包一次:
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-6be98e.js
│ ├── main-a15a74.css
│ └── main-a15a74.js
└── index.html
複製代碼
而後咱們首先修改一下style.css
,打包一下,會發現main.css
和main.js
都發生了變化,而index.js
不是一個chunk
的,所以不會發生變化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-6be98e.js
│ ├── main-88f8ea.css
│ └── main-88f8ea.js
└── index.html
複製代碼
一樣,咱們再覆蓋一下image.png
,再打包一下。
這時候image.png
當然會發生變化,而後index.js
也發生了變化,由於它們是一個chunk
的,而main.css
和main.js
就不會發生變化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-89dfd4.js
│ ├── main-88f8ea.css
│ └── main-88f8ea.js
└── index.html
複製代碼
簡單總結一下:
chunk
的js
、css
打包文件的文件名都會發生變化。chunk
的js
、css
打包文件的文件名也都會發生變化。最後一個就是contenthash
策略, 它是以自身內容爲單位的,所以當一個文件發生變化的時候,首先它自己的打包文件的名稱會發生變化,其次,引入它的文件的打包文件也會發生變化。
慣例來個實驗:
咱們將因此哈希佔位符改爲contenthash
。
module.exports = {
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name]-[contenthash:6].js',
assetModuleFilename: 'assets/[name]-[contenthash:6][ext]'
},
module: {
...
},
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name]-[contenthash:6].css"
}),
]
};
複製代碼
而後先打包一下。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-1e2b37.js
│ ├── main-02a4b4.css
│ └── main-c437b0.js
└── index.html
複製代碼
首先咱們先修改一下圖片吧,找一張新圖覆蓋一下image.png
,而後打包一下。
首先image.png
的名稱必定會發生變化,由於它改動了。其次index.js
也會發生變化,這是由於它引入了image.png
,而image.png
的名稱發生變化,所以它代碼中引入的名稱也得發生變化,所以index.js
的名稱也會發生變化。
而main.js
和main.css
由於沒有引用image.png
,所以不會發生變化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-e241d6.js
│ ├── main-02a4b4.css
│ └── main-c437b0.js
└── index.html
複製代碼
接下來,咱們來修改一下main.js
,而後打包一下。
咱們會發現,只有main.js
的打包文件會發生變化,而處於同個chunk
的main.css
卻不會發生變化,這是由於main.css
沒有引用main.js
。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-e241d6.js
│ ├── main-02a4b4.css
│ └── main-d1f8ed.js
└── index.html
複製代碼
如今能夠簡單總結一下:
一般咱們項目都會有開發環境和生產環境。
前面咱們也看到了webpack
提供了一個mode
選項,但咱們開發中不太可能說開發的時候mode
設置爲development
,而後等到要打包才設置爲production
。固然,前面咱們也說了,咱們能夠經過命令--mode
來對應匹配mode
選項。
但若是開發環境和生產環境的webpack
配置差別不只僅只有mode
選項的話,咱們可能須要考慮多份打包配置了。
咱們默認的webpack
配置文件名爲webpack.config.js
,而webpack
執行的時候,也默認會找該配置文件。
但若是咱們不使用該文件名,而改爲webpack.conf.js
的話,webpack
正常執行是會使用默認配置的,所以咱們須要使用一個--config
選項,來指定配置文件。
webpack --config webpack.conf.js
複製代碼
所以,咱們就能夠分別配置一個開發環境的配置webpack.dev.js
和生成環境的配置webpack.prod.js
,而後經過指令進行執行不一樣配置文件:
// package.json
"scripts": {
"dev": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
}
複製代碼
若是說,你不想建立那麼多配置文件的話,咱們也能夠只只用webpack.config.js
來實現多份打包配置。
按照前面說的使用--mode
配置mode
選項,其實咱們能夠在webpack.config.js
中拿到這個變量,所以咱們就能夠經過這個變量去返回不一樣的配置文件。
// argv.mode能夠獲取到配置的mode選項
module.exports = (env, argv) => {
if (argv.mode === 'development') {
// 返回開發環境的配置選項
return { ... }
}else if (argv.mode === 'production') {
// 返回生產環境的配置選項
return { ... }
}
};
複製代碼
mode
選項和devtool
選項前面已經有講到關於mode
選項和devtool
選項,而不一樣選項打包的速度也會有所不一樣,所以按照你的實際需求進行配置,有須要用到才生成,沒須要用到就能省就省。
在配置文件中,其實有一個resovle.alias
選項,它能夠建立import
和reuquire
別名,來確保模塊引入變得更簡單,同時webpack
在打包的時候也能更快的找到引入文件。
// webpack.config.js
const path = require('path');
module.exports = {
...
resolve: {
alias: {
// 配置style路徑的別名
style: path.resolve(__dirname, 'src/style/')
},
}
};
複製代碼
// 使用
import "style/style.scss";
import "style/style.css";
複製代碼
當咱們使用loader
的時候,咱們能夠配置include
來指定只解析該路徑下的對應文件,同時咱們能夠配置exclude
來指定不解析該路徑下的對應文件。
const path = require('path');
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
include: [path.resolve(__dirname, 'src')] // 只解析src路徑下的css
}
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/ // 不解析node_modules路徑下的js
}
]
}
};
複製代碼
咱們能夠在module.noParse
選項中,只配置不須要解析的文件。一般咱們會忽略一些大型插件從而來提升構建性能。
module.exports = {
...
module: {
noParse: /jquery|lodash/,
},
};
複製代碼
在webpack
構建過程當中,其實大部分消耗時間都是用到loader
解析上面,一方面是由於轉換文件數據量很大,另外一方面是由於JavaScript
單線程特性的緣由,所以須要一個個去處理,而不能併發操做。
而咱們可使用HappyPack
,將這部分任務分解到多個子進程中去進行並行處理,子進程處理完成後把結果發送到主進程中去,從而減小總的構建時間。
# 5.0.1
yarn add happypack -D
複製代碼
// webpack.config.js
const HappyPack = require("happypack");
const os = require("os");
const HappyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'happypack/loader?id=happyBabelLoader'
}]
}
]
},
plugins: [
new HappyPack({
id: 'happyBabelLoader', // 與loader對應的id標識
// 用法跟loader配置同樣
loaders: [
{loader: 'babel-loader', options: {}}
],
threadPool: HappyThreadPool // 共享進程池
})
]
};
複製代碼
起碼有聊到,當mode
爲production
的時候,webpack
打包會開啓代碼壓縮插件,同時webpack
也有提供一個optimization
選項,讓咱們可使用本身喜歡的插件去覆蓋原生插件。
所以,咱們可使用webpack-parallel-uglify-plugin
來覆蓋原生代碼壓縮插件,它的一個優勢就是能夠並行執行。
# 2.0.0
yarn add webpack-parallel-uglify-plugin -D
複製代碼
// webpack.config.js
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin")
module.exports = {
...
optimization: {
minimizer: [
new ParallelUglifyPlugin({
// 緩存路徑
cacheDir: '.cache/',
// 壓縮配置
uglifyJS: {
output: {
comments: false,
beautify: false
},
compress: {
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
})
]
}
};
複製代碼
咱們每次執行構建都會把全部的文件都從新編譯一邊,若是咱們能夠將這些重複動做緩存下來的話,對下一步的構建速度會有很大的幫助。
如今大部分的loader
都提供了緩存選項,但並不是全部的loader
都有,所以咱們最好本身去配置一下全局的緩存動做。
在Webpack5
以前,咱們都使用了cache-loader
,而在webpack5
中,官方提供了一個cache
選項給咱們帶來持久性緩存。
// 開發環境
module.exports = {
cache: {
type: 'memory' // 默認配置
}
}
// 生產環境
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
}
複製代碼
咱們可使用webpack-bundle-analyzer
插件來幫助咱們分析打包文件,它會將打包後的內容束展現爲方便交互的直觀樹狀圖,讓咱們知道咱們所構建包中真正引入的內容。
# 4.4.2
yarn add webpack-bundle-analyzer -D
複製代碼
// webpack.config.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
module.exports = {
...
plugins: [
new BundleAnalyzerPlugin()
]
};
複製代碼
而後咱們打包後,webpack
會自動打開一個頁面,顯示咱們打包文件的狀況,經過打包報告能夠很直觀的知道哪些依賴包大,則能夠作作針對性的修改。
若是不想每次運行都打開網頁的話,咱們能夠先將數據保存起來,而後要看的時候再執行新的命令去查看。
// webpack.config.js
new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true
})
複製代碼
// package.json
"scripts": {
"analyzer": "webpack-bundle-analyzer --port 3000 ./dist/stats.json"
},
複製代碼
在webpack
官網,它給提出了幾個loader
的編寫原則:
loader
只作一件事情;webpack
會按照順序鏈式去調用每一個loader
;webpack
定製的設計規則和結構,輸入和輸入均爲字符串,每一個loader
徹底獨立,即插即用。同時webpack
還給咱們提供了loader API
,所以咱們可使用this
去獲取須要用到的API
,但也是由於如此,咱們loader
的實現就不能使用箭頭函數了。
今天,咱們來簡單手寫一下sass-loader
、css-loader
和style-loader
,而它們也有各自的單一功能:
sass-loader
:用來解析sass
和scss
代碼;css-loader
:用來解析css
代碼;style-loader
:將css
代碼插入到js
中。首先,咱們先建立一個myLoders
文件夾,而後建立三個loader
文件。
├── myLoaders
│ ├── ou-css-loader.js
│ ├── ou-sass-loader.js
│ └── ou-style-loader.js
複製代碼
而後咱們須要在webpack
引入,而且須要配置一下resolveLoader
選項,由於webpack
默認只會去node_modules
搜索loader
。
module.exports = {
...
resolveLoader: {
// 添加loader查詢路徑
modules: ['node_modules', './myLoaders']
},
module: {
rules: [{
test: /\.(scss|sass)$/,
// 使用本身的loader
use: ['ou-style-loader','ou-css-loader','ou-sass-loader']
}]
}
};
複製代碼
首先咱們先來實現ou-sass-loader
。
loader
的本質就是一個函數,而咱們能夠在函數的第一個參數獲取到對應文件的代碼,咱們能夠先打印一下來看看。
// ou-sass-loader.js
module.exports = function(source) {
console.log(source);
}
複製代碼
而後執行打包後,咱們能夠看到咱們的scss
文件中的代碼。
所以,咱們可使用sass
插件來進行解析scss
代碼,sass
有一個render
函數能夠去解析。
// ou-sass-loader.js
const sass = require('sass');
module.exports = function(source) {
// 使用render函數進行解析scss代碼
sass.render({data: source}, (err, result) => {
console.log(result);
});
}
複製代碼
咱們在執行一下打包,會發現result
是一個對象,而裏面的css
就是咱們所須要的,所以咱們須要將其返回出去。
這裏
css
是Buffer
,咱們須要去解析它,可是解析它是css-loader
的工做,而不是sass-loader
的工做。
{
css: <Buffer 62 6f 64 79 20 7b 0a 20 20 62 61 63 6b 67 72 6f 75 6e 64 3a 20 23 32 32 32 3b 0a 7d 0a 62 6f 64 79 20 64 69 76 20 7b 0a 20 20 63 6f 6c 6f 72 3a 20 23 ... 6 more bytes>,
map: null,
stats: {
entry: 'data',
start: 1628131813793,
end: 1628131813830,
duration: 37,
includedFiles: [ [Symbol($ti)]: [Rti] ]
}
}
複製代碼
但這裏是一個異步操做,咱們不能直接return
回去,而是須要使用到webpack
提供的一個API
——this.async
,它自己是一個函數,而後會返回一個callback()
讓咱們能夠返回異步的結果。
// ou-sass-loader.js
const sass = require('sass');
module.exports = function(source) {
// 獲取callback函數
const callback = this.async();
sass.render({data: source}, (err, result) => {
// 將結果返回
if (err) return callback(err);
callback(null, result.css);
});
}
複製代碼
這時候,咱們ou-sass-loader
就實現了,接下來咱們來實現ou-css-loader
。
它其實任務很簡單,就是將ou-sass-loader
返回的css
解析爲字符串就能夠了。
// ou-css-loader.js
module.exports = function(source) {
return JSON.stringify(source)
}
複製代碼
最後就是ou-style-loader
,它的任務就是建立一個style
標籤,而後將ou-css-loader
返回的數據插進去,而且將style
標籤放置到head
標籤裏面去。
// ou-style-loader.js
module.exports = function(sources) {
return ` const tag = document.createElement("style"); tag.innerHTML = ${sources}; document.head.appendChild(tag) `
}
複製代碼
這時咱們簡易版的sass-loader
、css-laoder
和style-laoder
就實現了,咱們能夠執行一下打包命令,檢驗頁面是否是有對應的樣式效果。
在webpack
運行過程當中,會存在一個生命週期,而在生命週期中webpack
會廣播出許多事情,而在plugin
中是能夠監聽到這些事件,所以plugin
是能夠實如今合適的時機經過Webpack
提供的API
去實現一些動做。
正常狀況下,一個plugin
是一個類,而且裏面會有一個apply
函數,而在apply
函數中會接收到一個compiler
參數,裏面包含了關於webpack
環境全部的配置信息。
module.exports = class MyPlugin {
apply (compiler) {}
}
複製代碼
在compiler
中會暴露不少生命週期鉤子函數,具體的能夠查看文檔。咱們能夠經過如下方式去訪問鉤子函數。
compiler.hooks.someHook.tap(...)
複製代碼
在tap
方法中,接收兩個參數,一個是該plugin
的名稱,一個是回調函數,而在回調函數中,又會接收到一個compilation
參數。
module.exports = class MyPlugin {
apply (compiler) {
compiler.hooks.compile.tap("MyPlugin", (compilation) => {
console.log(compilation)
})
}
}
複製代碼
compilation
對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當運行webpack
開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation
,從而生成一組新的編譯資源。compilation
對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。
compliation
也暴露了許多的鉤子,具體的話能夠去看看文檔。
接下來,簡單實現一下一個plugin
,打包後生成一個txt
文件,裏面會打印出每一個bundle
的大小。
module.exports = class MyPlugin {
apply(compiler) {
// 生成資源到 output 目錄以前
compiler.hooks.emit.tap("MyPlugin", (compilation) => {
let str = ''
for (let filename in compilation.assets) {
// 獲取文件名稱和文件大小
str += `${filename} -> ${compilation.assets[filename]['size']() / 1000}KB\n`
}
// 新建fileSize.txt
compilation.assets['fileSize.txt'] = {
// 內容
source: function () {
return str
}
}
})
}
}
複製代碼
緊接着,咱們將其引入到webpack.config.js
,並在plugins
中建立實例。
const MyPlugin = require("./myPlugins/my-plugin")
module.exports = {
...
plugins: [
new MyPlugin()
]
};
複製代碼
而後打包後,dist
文件中會生成一個fileSize.txt
文件。
assets/Alata-Regular-e83420.ttf -> 96.208KB
assets/image-f3f2ec.png -> 207.392KB
index.html -> 0.364KB
assets/index-41f0e2.css -> 0.177KB
assets/index-acc2f5.js -> 1.298KB
複製代碼
喜歡的朋友能夠點個
Star
哦~
首先咱們先初始化咱們的項目文件。
先新建一個src
路徑,而後建立三個js
文件——index.js
、a.js
、b.js
。
// index.js
import {msg} from "./a.js";
console.log(msg);
// a.js
import {something} from "./b.js";
export const msg = `Hello ${something}`;
// b.js
export const something = 'World';
複製代碼
而後咱們能夠先安裝webpack
,而後測試一下打包出來的bundle
文件有什麼特色。
這裏就很少說了,直接看
bundle
文件(默認配置,mode
爲development
)
打包後,咱們能夠看到bundle
文件有不少內容,但也有一大半註釋。
其實咱們只須要看兩個地方,一個是__webpack_modules__
變量。咱們能夠看到它是一個對象,而後key
值爲module
路徑,而value
值是執行module
代碼的函數。
var __webpack_modules__ = ({
"./src/a.js": (() => eval( ... )),
"./src/b.js": (() => eval( ... )),
"./src/index.js": (() => eval( ... ))
})
複製代碼
其次,咱們能看到一個函數,叫__webpack_require__
,它接收一個moduleId
的參數。然而咱們能夠在最後看到了這個函數的調用,就會發現其實moduleId
就是__webpack_modules__
的key
值,也就是module
的路徑。
var __webpack_exports__ = __webpack_require__("./src/index.js");
複製代碼
到這裏,咱們就能夠大概捋清楚webpack
打包的一個邏輯了。
webpack
是直接拿到js
文件的代碼,即字符串。而後經過eval()
函數執行代碼;webpack
會從入口文件開始,不斷遞歸遍歷引入模塊,而後保持在一個對象裏面,key
值爲moduleId
,即模塊路徑,而value
是模塊的相關代碼。webpack
會將代碼轉換爲commonJS
,即便用require
去引入模塊,同時它自身會去封裝一個require
函數,去執行入口文件代碼。話很少說,咱們開始來手寫代碼。
首先咱們能夠先初始化webpack
配置文件——webpack.config.js
。
const path = require("path");
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'index.js'
}
}
複製代碼
其次,咱們新建一個lib
文件夾,而後建立一個webpack.js
,用來手寫咱們的mini-webpack
。
咱們能夠先初始化一下,Webpack
是一個類,其次構建函數會接受配置文件,其次會有一個run
函數,是webpack
的運行函數。
module.exports = class Webpack {
/** * 構造函數,獲取webpack配置 * @param {*} options */
constructor(options) {}
/** * webpack運行函數 */
run() {
console.log('開始執行Webpack!')
}
}
複製代碼
而後咱們須要一個執行文件,即在根路徑建立一個debugger.js
。
const webpack = require('./lib/webpack');
const options = require('./webpack.config');
new webpack(options).run();
複製代碼
緊接着咱們執行一下該文件。
node debugger.js
複製代碼
這時候命令行就會打印出開始執行Webpack!
。
咱們能夠開始手寫mini-webpack
了。
首先,在構造函數中,咱們須要保存一下配置信息。
constructor(options) {
const {entry, output} = options;
this.entry = entry; // 入口文件
this.output = output; // 導出配置
}
複製代碼
在執行的第一步,咱們須要來解析一下入口文件,所以咱們用一個parseModules
來實現這個功能。
module.exports = class Webpack {
constructor(options) {
...
}
run() {
// 解析模塊
this.parseModules(this.entry);
}
/** * 模塊解析 * @param {*} file */
parseModules(file) {}
}
複製代碼
在parseModules
中,咱們須要作兩件事情:分析模塊信息、遞歸遍歷引入模塊。
咱們一步一步來實現。首先,封裝一個getModuleInfo
函數,來分析模塊信息。
parseModules(file) {
// 分析模塊
this.getModuleInfo(file);
}
/** * 分析模塊 * @param {*} file * @returns Object */
getModuleInfo(file) {}
複製代碼
首先,咱們接收到的file
其實就是入口文件的相對路徑,即./src/index.js
。所以咱們能夠先用node
自帶的fs
模塊來讀取文件內容。
getModuleInfo(file) {
// 讀取文件
const body = fs.readFileSync(file, "utf-8");
}
複製代碼
讀取到內容後,咱們就要來分析一下文件內容了,這時候就須要用到了AST語法樹
了。
抽象語法樹 (
Abstract Syntax Tree
),簡稱AST
,它是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。演示地址:astexplorer.net/
這裏咱們用到的時候babel
的parse
插件,經過它來將JavaScript
轉成AST
。
# 7.14.8
yarn add @babel/parser -D
複製代碼
const fs = require("fs");
const parser = require("@babel/parser");
module.exports = class Webpack {
...
getModuleInfo(file) {
// 讀取文件
const body = fs.readFileSync(file, "utf-8");
// 轉化爲AST語法樹
const ast = parser.parse(body, {
sourceType: 'module' // 表示咱們解析的是ES模塊
})
}
}
複製代碼
緊接着,咱們還須要使用@babel/traverse
來遍歷AST
,從而來識別該文件有沒有引入其餘模塊,有的話就將其記錄下來。
# 7.14.8
yarn add @babel/traverse -D
複製代碼
const traverse = require("@babel/traverse").default;
複製代碼
traverse
接受兩個參數,第一個是ast
語法樹,第二個是一個對象,在對象中咱們能夠設置觀察者函數,而且能夠針對語法樹中的特定節點類型。
好比咱們此次只須要找到引入模塊的語句,對應的節點類型爲ImportDeclaration
,咱們就能夠設置對應的ImportDeclaration
函數,並在參數值獲取到節點信息。
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
module.exports = class Webpack {
...
getModuleInfo(file) {
// 讀取文件
const body = fs.readFileSync(file, "utf-8");
// 轉化爲AST語法樹
const ast = parser.parse(body, {
sourceType: 'module' // 表示咱們解析的是ES模塊
})
traverse(ast, {
// visitor函數
ImportDeclaration({node}) {
console.log(node);
}
})
}
}
複製代碼
咱們執行一下,能夠打印出import {msg} from "./a.js"
的語法樹。
所以,咱們須要將其路徑收集起來。
// 依賴收集
const deps = {};
traverse(ast, {
// visitor函數
ImportDeclaration({node}) {
// 入口文件路徑
const dirname = path.dirname(file);
// 引入文件路徑
const absPath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
}
})
複製代碼
此時的deps
就是{ './a.js': './src/a.js' }
,之因此要保存它相對項目根路徑的相對路徑,是爲了後面更好的去拿到它的文件內容。
收集完依賴後,咱們須要將AST
轉回JavaScript
代碼,而且將其轉成ES5
語法。這時候咱們就會用到@babel/core
和@babel/preset-env
。
# @babel/core -> 7.14.8, @babel/preset-env -> 7.14.8
yarn add @babel/core @babel/preset-env -D
複製代碼
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
module.exports = class Webpack {
...
getModuleInfo(file) {
// 讀取文件
const body = fs.readFileSync(file, "utf-8");
// 轉化爲AST語法樹
const ast = parser.parse(body, {
sourceType: 'module' // 表示咱們解析的是ES模塊
})
// 依賴收集
const deps = {};
traverse(ast, {
// visitor函數
ImportDeclaration({node}) {
// 入口文件路徑
const dirname = path.dirname(file);
// 引入文件路徑
const absPath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
}
})
// ES6轉成ES5
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
})
}
}
複製代碼
這時候咱們能夠打印一下code
,會發現它再也不是ESModule
的引入方式了,而是使用了CommonJS
引入方式。
"use strict";
var _a = require("./a.js");
console.log(_a.msg);
複製代碼
最終,getModuleInfo
會返回一個對象,對象裏面包含着解析文件的路徑,該文件的依賴對象以及文件代碼。
parseModules(file) {
// 分析模塊
const entry = this.getModuleInfo(file);
}
getModuleInfo(file) {
...
return {
file, // 文件路徑
deps, // 依賴對象
code // 代碼
};
}
複製代碼
但咱們分析完入口文件後,咱們就須要進行遞歸遍歷,去分析引入模塊。
首先,咱們須要新建一個數組,保存一下全部的分析結果。其次,咱們來實現一下getDeps
函數,來遞歸遍歷引入模塊。
parseModules(file) {
// 分析模塊
const entry = this.getModuleInfo(file);
const temp = [entry];
// 遞歸遍歷,獲取引入模塊代碼
this.getDeps(temp, entry)
}
/** * 獲取依賴 * @param {*} temp * @param {*} module */
getDeps(temp, {deps}) {}
複製代碼
在getDeps
中,咱們能夠經過第二個參數獲取到依賴對象,其次經過遍歷這個對象,一一執行一下getModuleInfo
函數,獲取各個依賴模塊的解析內容,並保存到temp
。
最後,再自調用一下getDeps
,傳入引入模塊內容,繼續遞歸遍歷。
getDeps(temp, {deps}) {
// 遍歷依賴
Object.keys(deps).forEach(key => {
// 獲取依賴模塊代碼
const child = this.getModuleInfo(deps[key]);
temp.push(child);
// 遞歸遍歷
this.getDeps(temp, child);
})
}
複製代碼
這裏還須要進行查重,好比在多個文件都引入了b.js
的話,temp
數組就會保存多個b.js
的內容對象,所以咱們能夠先查重一下,若是temp
對象沒有該模塊,咱們再執行後面的操做。
getDeps(temp, {deps}) {
Object.keys(deps).forEach(key => {
// 去重
if (!temp.some(m => m.file === deps[key])) {
const child = this.getModuleInfo(deps[key]);
temp.push(child);
this.getDeps(temp, child);
}
})
}
複製代碼
這時候,咱們模塊解析的操做已經完成了差很少了。
最後咱們最須要將temp
數組,轉換成對象,即跟__webpack_modules__
相似,以路徑爲key
名,而後value
爲對應的內容信息。
parseModules(file) {
const entry = this.getModuleInfo(file);
const temp = [entry];
this.getDeps(temp, entry)
// 將temp轉成對象
const depsGraph = {};
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
return depsGraph;
}
複製代碼
這時候,咱們在run()
函數保存一下解析結果,就完成了第一步操做了。
run() {
// 解析模塊
this.depsGraph = this.parseModules(this.entry);
}
複製代碼
下一步就是執行打包操做了,咱們先封裝一個bundle
函數。
run() {
// 解析模塊
this.depsGraph = this.parseModules(this.entry);
// 打包
this.bundle()
}
/** * 生成bundle文件 */
bundle() { }
複製代碼
首先咱們先把簡單的部分完成了,就是生成打包文件。
咱們要用到fs
模塊,先識別打包路徑存不存在,不存在的話新建一個目錄,其次就寫入bundle
文件。
bundle() {
const content = `console.log('Hello World')`;
// 生成bundle文件
!fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
const filePath = path.join(this.output.path, this.output.filename);
fs.writeFileSync(filePath, content);
}
複製代碼
這時運行一下打包命令,項目裏就會出現一個dist
文件夾,裏面會有一個index.js
。
console.log('Hello World')
複製代碼
接下來咱們就得來實現bundle
文件的內容。
首先它是一個匿名函數只執行的方式,而後它接收一個參數__webpack_modules__
,即咱們前面解析文件的結果。
(function(__webpack_modules__){
...
})(this.depsGraph)
複製代碼
其次,咱們須要是實現一下__webpack_require__
函數,它接收一個moduleId
參數,即路徑參數。
而後咱們還須要去調用一下__webpack_require__
,並傳入入口文件路徑。
(function(__webpack_modules__){
function __webpack_require__(moduleId) {
...
}
__webpack_require__(this.entry)
})(this.depsGraph)
複製代碼
前面咱們又看到,babel
將代碼轉義成commonJS
,所以咱們須要來實現一下require
函數,由於JavaScript
自己不具有。
require
函數的實質就是返回引入文件的內容。
同時,咱們還須要新建一個exports
對象,這樣子模塊導出的內容就能夠保存到裏面去了,最後也須要將其返回出去。
(function(__webpack_modules__){
function __webpack_require__(moduleId) {
// 實現require方法
function require(relPath) {
return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
}
// 保存導出模塊
var exports = {};
return exports
}
__webpack_require__(this.entry)
})(this.depsGraph)
複製代碼
最後,就只須要來執行一下入口文件的代碼便可。
這裏仍是使用一個匿名函數並自調用。
(function(__webpack_modules__){
function __webpack_require__(moduleId) {
// 實現require方法
function require(relPath) {
return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
}
// 保存導出模塊
var exports = {};
// 調用函數
(function (require,exports,code) {
eval(code)
})(require,exports,__webpack_modules__[moduleId].code)
return exports
}
__webpack_require__(this.entry)
})(this.depsGraph)
複製代碼
這時候咱們再將這段代碼,換到content
變量中去。
bundle() {
const content = ` (function (__webpack_modules__) { function __webpack_require__(moduleId) { function require(relPath) { return __webpack_require__(__webpack_modules__[moduleId].deps[relPath]) } var exports = {}; (function (require,exports,code) { eval(code) })(require,exports,__webpack_modules__[moduleId].code) return exports } __webpack_require__('${this.entry}') })(${JSON.stringify(this.depsGraph)}) `;
// 生成bundle文件
!fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
const filePath = path.join(this.output.path, this.output.filename);
fs.writeFileSync(filePath, content);
}
複製代碼
而後執行打包,就能夠看到完整的打包內容了。
(function (__webpack_modules__) {
function __webpack_require__(moduleId) {
function require(relPath) {
return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
}
var exports = {};
(function (require,exports,code) {
eval(code)
})(require,exports,__webpack_modules__[moduleId].code)
return exports
}
__webpack_require__('./src/index.js')
})({"./src/index.js":{"deps":{"./a.js":"./src/a.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log(_a.msg);"},"./src/a.js":{"deps":{"./b.js":"./src/b.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.msg = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar msg = \"Hello \".concat(_b.something);\nexports.msg = msg;"},"./src/b.js":{"deps":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.something = void 0;\nvar something = 'World';\nexports.something = something;"}})
複製代碼
最後,咱們執行一下,看看能不能打印出Hello World
。
node ./dist/index.js
複製代碼