柚子快報(bào)激活碼778899分享:前端 Webpack學(xué)習(xí)記錄
柚子快報(bào)激活碼778899分享:前端 Webpack學(xué)習(xí)記錄
記錄學(xué)習(xí)筆記,歡迎指正
1.大型項(xiàng)目為什么需要打包
1.1 使用打包工具原因
編譯或轉(zhuǎn)譯文件:
項(xiàng)目中可能用到ES6語(yǔ)法,可能有瀏覽器不支持。需要打包工具將代碼編譯輸出為ES5語(yǔ)法的代碼。項(xiàng)目中可能使用Sass,Less等預(yù)處理器,瀏覽器本身并不支持。需要打包工具將代碼編譯輸出為CSS代碼。同編譯器的特點(diǎn),打包工具具有編譯器可以?xún)?yōu)化代碼。
處理模塊:
大型項(xiàng)目可以劃分為若干模塊,模塊的開(kāi)發(fā)需要依賴(lài)規(guī)范。例如UMD,AMD,CMD,ESM規(guī)范。瀏覽器默認(rèn)不支持除了ESM的規(guī)范,需要打包工具將項(xiàng)目打包成js文件,css文件后通過(guò)入口文件引入。
注意:編譯和轉(zhuǎn)譯的區(qū)別?
編譯是將高級(jí)程序語(yǔ)言的代碼轉(zhuǎn)為低級(jí)程序語(yǔ)言的代碼,例如匯編代碼,或者適合于某種虛擬機(jī)上運(yùn)行的代碼。轉(zhuǎn)譯是將高級(jí)程序語(yǔ)言的代碼轉(zhuǎn)為另一種高級(jí)程序語(yǔ)言的代碼。嚴(yán)格來(lái)說(shuō)babel應(yīng)該稱(chēng)之為轉(zhuǎn)譯器。
注意:webpack的能力包含編譯CSS,Typescript等文件嗎?
webpack本身功能比較局限只能編譯JavaScript和Json文件,將模塊進(jìn)行處理。編譯ES6,Sass,Less等需要webpack安裝其它的loader加載器。
注意:為什么使用ESM規(guī)范還需要打包?
避免頻繁網(wǎng)絡(luò)請(qǐng)求:
如果使用ESM規(guī)范但不打包,那么每使用一個(gè)模塊都要重新請(qǐng)求,頻繁請(qǐng)求會(huì)影響瀏覽器緩存效率,導(dǎo)致緩存命中率低。 如果使用ESM規(guī)范但打包,那么可以把模塊聚集在一個(gè)或屈指可數(shù)的幾個(gè)文件中,不會(huì)讓瀏覽器頻繁請(qǐng)求文件 提升兼容性:
大部分瀏覽器都支持ESM規(guī)范,但是考慮可能有低版本瀏覽器不支持,或支持度低。
2.包管理工具常識(shí)
2.1 開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境
開(kāi)發(fā)環(huán)境:
項(xiàng)目未打包時(shí)運(yùn)行的環(huán)境。依賴(lài)于開(kāi)發(fā)時(shí)依賴(lài)和生產(chǎn)時(shí)依賴(lài)的包。
生產(chǎn)時(shí)依賴(lài)的包可以流向開(kāi)發(fā)時(shí)依賴(lài),可以通過(guò)集合的包含關(guān)系來(lái)理解。如果想移動(dòng)生產(chǎn)時(shí)依賴(lài)的包到開(kāi)發(fā)時(shí)依賴(lài),直接執(zhí)行 npm i sylvester -D類(lèi)似的命令即可。 通常webpack配置全局變量process.env.NODE_ENV為development。
生產(chǎn)環(huán)境:
項(xiàng)目打包后運(yùn)行的環(huán)境。依賴(lài)于生產(chǎn)時(shí)依賴(lài)的包。
開(kāi)發(fā)時(shí)依賴(lài)的包不能流向生產(chǎn)時(shí)依賴(lài),可以通過(guò)集合的包含關(guān)系來(lái)理解。如果想移動(dòng)包,那么只能刪除開(kāi)發(fā)時(shí)依賴(lài)的指定包,之后再在生產(chǎn)時(shí)依賴(lài)中重新安裝。 通常webpack配置全局變量process.env.NODE_ENV為production。
舉例:
例如用于代理前綴的修改。通常為了便于做反向代理,會(huì)把所有JavaScript發(fā)起的請(qǐng)求的URL添加前綴做統(tǒng)一代理。如果不使用項(xiàng)目中統(tǒng)一封裝的網(wǎng)絡(luò)請(qǐng)求,例如使用window.location.href下載文件,并且生產(chǎn)環(huán)境下前后端都塞入tomcat,不存在跨域,那么可以通過(guò)process.env.NODE_ENV來(lái)判斷環(huán)境來(lái)決定是否添加前綴。
2.2 包的常用指令
npm install/add/i/ins…
空: 安裝開(kāi)發(fā)時(shí)依賴(lài)和生產(chǎn)時(shí)依賴(lài)的所有包。 某個(gè)包: 默認(rèn)使用–save命令安裝這個(gè)包。 –save或-S + 某個(gè)包: 安裝生產(chǎn)時(shí)依賴(lài),記錄到package.json的dependencies中。
舉例: react,ahooks,styled-components注意: 安裝包時(shí)默認(rèn)是–save指令是為了保證項(xiàng)目的正常運(yùn)行。如果默認(rèn)安裝到開(kāi)發(fā)時(shí)環(huán)境或全局環(huán)境,那么如果交付遠(yuǎn)程服務(wù)器打包時(shí),可能導(dǎo)致缺少必要的包。 –save-dev或-D + 某個(gè)包: 安裝開(kāi)發(fā)時(shí)依賴(lài),記錄到package.json的devDependencies中。
舉例: webpack,typescript,jest,eslint注意: 生產(chǎn)時(shí)依賴(lài)和開(kāi)發(fā)時(shí)依賴(lài)不存在交集,因此無(wú)法同時(shí)將某個(gè)包安裝到兩個(gè)環(huán)境中,即 npm install sylvester -D -S不會(huì)同時(shí)安裝到兩個(gè)環(huán)境。 –global或-g + 某個(gè)包: 安裝到全局環(huán)境。
注意:
項(xiàng)目之間的依賴(lài)最好是獨(dú)立管控的,不推薦全局安裝。如果項(xiàng)目要使用遠(yuǎn)程服務(wù)器打包(打包后交付測(cè)試或部署上線(xiàn))走自動(dòng)化流程,那么遠(yuǎn)程服務(wù)器打包時(shí)找不到你在本地全局安裝的包,因?yàn)槟悴粫?huì)把它記錄到package.json中 –no-save + 某個(gè)包: 安裝依賴(lài)。但不記錄到開(kāi)發(fā)時(shí)依賴(lài)和生產(chǎn)時(shí)依賴(lài)。
注意: 這通常用于測(cè)試某個(gè)第三方依賴(lài),并且你的同事暫時(shí)不會(huì)安裝它。不過(guò)這并不是妥當(dāng)?shù)姆椒?,因?yàn)槟憧赡芫帉?xiě)依賴(lài)于這個(gè)第三方依賴(lài)的代碼,如果你提交代碼,會(huì)導(dǎo)致其他人報(bào)錯(cuò)。推薦的方法是應(yīng)該新拉一個(gè)分支,或者新建一個(gè)項(xiàng)目來(lái)進(jìn)行測(cè)試。 –force或-f (+ 某個(gè)包): 強(qiáng)制安裝。 –production: 只安裝生產(chǎn)時(shí)依賴(lài)。
注意:
只是依據(jù)package.json安裝生產(chǎn)時(shí)依賴(lài),安裝新依賴(lài)時(shí)使用該命令將被忽視。因此這個(gè)命令的表現(xiàn)和 npm update的表現(xiàn)類(lèi)似,可以用來(lái)更新依賴(lài)。不論在什么環(huán)境下webpack打包都會(huì)依賴(lài)開(kāi)發(fā)時(shí)依賴(lài)和生產(chǎn)時(shí)依賴(lài),因此交付遠(yuǎn)程服務(wù)器打包時(shí)應(yīng)該使用 npm install而不是 npm install --production目前沒(méi)有只安裝開(kāi)發(fā)時(shí)依賴(lài)的指令,因?yàn)殚_(kāi)發(fā)環(huán)境依賴(lài)于開(kāi)發(fā)時(shí)依賴(lài)和生產(chǎn)時(shí)依賴(lài),只安裝開(kāi)發(fā)時(shí)依賴(lài)沒(méi)有任何意義。
**npm update **
空: 忽略開(kāi)發(fā)時(shí)依賴(lài)的更新,只更新生產(chǎn)時(shí)的依賴(lài)。其它與安裝時(shí)指令類(lèi)似,如果想指定也更新開(kāi)發(fā)時(shí)依賴(lài),可以添加特殊指令。
npm uninstall/rm
與安裝時(shí)指令類(lèi)似
2.3 包版本計(jì)算器
版本命名規(guī)范:
2.3.5-beta.3
major: 大版本minor: 小版本(可選)patch: 補(bǔ)丁版本(可選)prerelease_tag: 預(yù)發(fā)布標(biāo)簽(可選)
alpha: 內(nèi)測(cè)版本beta: 公測(cè)版本rc: 候選版本pre: 前面的三個(gè)數(shù)字類(lèi)型的版本不足以標(biāo)識(shí)修改時(shí)使用pre prerelease_version: 預(yù)發(fā)布版本(可選)
版本前綴含義:
*: 允許升級(jí)到任何版本。^: 不超過(guò)左邊非零數(shù)字
舉例:
^2.3.1表示 2.3.1 <= version < 3.0.0^0.3.1表示 0.3.1 <= version < 0.4.0^0.0.1表示 0.0.1 <= version < 0.0.2 ~: 允許補(bǔ)丁版本升級(jí)到任意發(fā)布的版本。
舉例:
~2.3.1表示 2.3.1 <= version < 2.4.0~2.3表示 2.3.0 <= version < 2.4.0~2表示 2.0.0 <= version < 2.1.0 >,<,>=,<=: 按符號(hào)字面意思表示版本允許升級(jí)的范圍。
模糊版本更新機(jī)制:
npm install忽略模糊版本
npm update更新到最新模糊版本
2.4 包執(zhí)行者
npx + 包 + 包對(duì)應(yīng)的指令
npx是包執(zhí)行者,在項(xiàng)目里npx會(huì)去node_modules/.bin中尋找包的執(zhí)行程序執(zhí)行。例如我沒(méi)有全局安裝webpack,只是在項(xiàng)目中用npm安裝,并且我想執(zhí)行webpack的指令,那么我可以使用npx。
舉例:
無(wú)webpack.config.js:npx webpack ./src/main.js --mode=development 有webpack.config.js:npx webpack
3.Webpack核心配置點(diǎn)
3.1 概述
entry(入口): 打包入口,從一個(gè)或多個(gè)入口打包
output(輸出): 打包文件輸出位置
loader(加載器): webpack本身只能編譯JavaScript處理模塊化。編譯CSS等其它資源需要借助loader。
plugins(插件): 擴(kuò)展webpack的功能。
devServer(服務(wù)器): 開(kāi)發(fā)模式下運(yùn)行項(xiàng)目并熱更新。
mode(模式): 開(kāi)發(fā)模式development和生產(chǎn)模式production。
3.2 Output
3.2.1 配置入口文件輸出位置
Node全局變量和方法:
__dirname:__dirname返回當(dāng)前文件所在文件夾的絕對(duì)路徑 __filename: __filename返回當(dāng)前文件的絕對(duì)路徑 path.resolve: 負(fù)責(zé)解析路徑。
將若干個(gè)表示相對(duì)路徑的參數(shù)拼接到當(dāng)前文件所在的文件夾的絕對(duì)路徑上如果有參數(shù)是絕對(duì)路徑,那么忽略最后一個(gè)絕對(duì)路徑前的參數(shù)
module.exports = {
output: {
path: path.resovle(__dirname, "dist")
filename: "static/js/main.js"
}
}
3.2.2 配置自動(dòng)清空上次打包效果
module.exports = {
output: {
clean: true
}
}
3.3 loader
3.3.1 配置樣式loader
解析順序: loader可以通過(guò)配置use屬性進(jìn)行鏈?zhǔn)秸{(diào)用,默認(rèn)規(guī)定鏈表頭是數(shù)組尾部,因此loader是從右向左解析。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /\.s[ac]ss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.styl$/,
use: ["style-loader", "css-loader", "stylus-loader"],
},
]
}
}
3.3.2 配置樣式兼容性loader
在package.json中配置需要兼容的瀏覽器,否則兼容性loader不會(huì)生效
// package.json中配置需要兼容的瀏覽器
{
"browserslist": {
"last 2 version" // 匹配的瀏覽器的最近的兩個(gè)版本
"> 1%" // 全球用戶(hù)使用率超過(guò)1%的瀏覽器
"not dead" // 不考慮官方聲明已經(jīng)不再維護(hù)的瀏覽器
}
}
配置css兼容性loader
樣式兼容性loader的位置: 在css-loader后面,在css預(yù)解析器loader的前面配置loader的方式: 在use中直接寫(xiě)loader名稱(chēng)表示使用默認(rèn)配置,使用對(duì)象方式寫(xiě)loader和options可以自定義配置
const getStyleLoader = (pre) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
// 在use中直接寫(xiě)loader的名稱(chēng)表示使用默認(rèn)配置,下面是通過(guò)options自定義配置,配置依據(jù)來(lái)源于官網(wǎng)
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
pre,
].filter(Boolean);
}
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader("less-loader")
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader("sass-loader")
},
{
test: /\.styl$/,
use: getStyleLoader("stylus-loader")
},
]
}
}
3.3.3 配置腳本兼容性loader
配置babel-loader??梢院蜆邮郊嫒菪詌oader類(lèi)比:
都是處理兼容性的loader都用到了自定義配置自定義配置都可以提出來(lái)形成獨(dú)立的配置文件
{
test: /\.js$/,
// 不處理的文件
exclude: /node_modules|bower_components/,
use: {
loader: "babel-loader",
// 也可以寫(xiě)?yīng)毩⑽募abel.config.js來(lái)配置下述內(nèi)容
options: {
presets: ["@babel/preset-env"],
},
},
}
3.3.4 配置資源loader
配置資源輸出路徑: generator中的filename指定了輸出的文件名或路徑。
有效配置: 只有設(shè)置了type為資源類(lèi)型 asset, asset/resource, ...的文件配置輸出路徑才有效。無(wú)效配置: 如果給css或者css預(yù)解析文件配置輸出路徑是無(wú)效的,這些loader的行為是把css輸出到j(luò)s文件中,因此不會(huì)輸出css文件,因此不會(huì)有文件在期望的輸出路徑生成。但是你可以借助plugin擴(kuò)展loader的能力來(lái)完成這個(gè)行為。 資源打包成DataUrl形式: 資源打包成DataUrl可以避免發(fā)請(qǐng)求。
DataUrl形式:
module.exports = {
modules: {
rules: [
// 圖片文件
{
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
type: 'asset',
parser: {
// 小于100KB的圖片解析為內(nèi)置資源dataUrl形式
dataUrlCondition: {
maxSize: 100 * 1024,
},
},
generator: {
// name: 文件名
// hash: 生成文件的唯一標(biāo)識(shí)
// 10: 限制hash長(zhǎng)度最多為10
// ext: 擴(kuò)展名
// query: 查詢(xún)參數(shù)
filename: 'images/[name][hash:10][ext][query]',
},
},
// 字體文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset',
generator: {
filename: 'fonts/[name][hash][ext]',
},
},
]
}
}
3.4 plugin
3.4.1 配置開(kāi)發(fā)規(guī)范plugin
webpack配置
const ESLintPlugin = require("eslint-webpack-plugin");
module.exports = {
plugins: [
new ESLintPlugin({
// eslint檢測(cè)的文件夾
context: path.resolve(__dirname, "../src"),
})
]
}
.eslintrc配置
注意: eslint配置文件.eslintrc.js一般放在根目錄下。配置完后立即生效,在開(kāi)發(fā)代碼時(shí)會(huì)自動(dòng)調(diào)用解析器檢查是否符合配置的規(guī)范。
配置說(shuō)明:
extends: 配置要繼承的eslint配置文件。
舉例: 下面的配置繼承于 eslint:recommended,之后又添加了部分自定義配置。也可以安裝 eslint-config-react-app來(lái)繼承于 react-app。 env: 配置代碼運(yùn)行的環(huán)境。
舉例: 下面配置node環(huán)境和browser環(huán)境,那么代碼可能會(huì)運(yùn)行在這兩個(gè)環(huán)境上,那么eslint在做檢查時(shí)要支持對(duì)應(yīng)環(huán)境上的全局變量。 parser: 配置eslint調(diào)用的解析器。默認(rèn)解析器是Espree。
舉例: 即使配置parserOptions,Espree也會(huì)判定在函數(shù)中使用import動(dòng)態(tài)加載模塊是錯(cuò)誤的。那么這時(shí)可以更換parser,例如babel-eslint或@typescript-eslint/parser parserOptions: 修改parse的配置。
舉例: Espree默認(rèn)檢查es5語(yǔ)法,項(xiàng)目如果用es6規(guī)范的代碼,那么需要指定ecmaVersion支持對(duì)應(yīng)版本的語(yǔ)法。sourceType指定文件的模塊系統(tǒng),是module模塊還是單獨(dú)的script腳本。 rules: 修改eslint的檢查規(guī)則。核心配置內(nèi)容。
舉例:
每個(gè)配置項(xiàng)都對(duì)應(yīng)著值 0, 1, 2和off, warn, error。表示命中該規(guī)則時(shí)“在文件中的表現(xiàn)”和“本地服務(wù)器上的表現(xiàn)”,例如文件中顯示黃色波浪線(xiàn),紅色波浪線(xiàn),不阻塞服務(wù)器運(yùn)行,但是輸出提示,阻塞服務(wù)器運(yùn)行。下面的配置表示使用var聲明變量時(shí)會(huì)報(bào)警告。聲明變量后未使用會(huì)報(bào)警告。 plugins: 擴(kuò)展eslint能力。
舉例: 配置插件使eslint可以檢查JSX。這樣擴(kuò)展了rules內(nèi)容,eslint本身的所有rules可以在官網(wǎng)上查到配置,安裝插件可以提供新的校驗(yàn)?zāi)芰托r?yàn)選項(xiàng)。例如安裝eslint-plugin-react,提供JSX和react框架風(fēng)格的相關(guān)檢查。
module.exports = {
extends: ["eslint:recommended"],
env: {
node: true,
browser: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module"
},
plugins: ["@typescript-eslint"],
rules: {
// 使用var做警告
"no-var": "warn",
// 未使用的變量做警告
"no-unused-vars": "warn",
},
};
3.4.2 配置文檔輸出plugin
配置輸出HTML的原因: 沒(méi)有配置前打包輸出沒(méi)有html文件,只是在public文件夾中的入口html文件中配置引入腳本的位置。如果打包后的腳本名稱(chēng)發(fā)生變化的話(huà)又需要重新配置引入比較麻煩。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [new HtmlWebpackPlugin({
// 不使用默認(rèn)位置,自定義輸出位置
template: path.resolve(__dirname, "../public/index.html")
})]
}
3.4.3 配置提取樣式plugin
提取CSS文件的原因: 如果使用style-loader來(lái)實(shí)現(xiàn)CSS-In-JS的方式,進(jìn)入頁(yè)面時(shí)會(huì)有閃動(dòng)效果。先解析js,解析完成后再通過(guò)style標(biāo)簽插入樣式,由無(wú)樣式的效果到有樣式的效果導(dǎo)致了頁(yè)面閃動(dòng)。注意事項(xiàng): 用插件的loader代替style-loader
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
modules: {
// 用MiniCssExtractPlugin.loader代替style-loader避免把CSS輸出到JS中
rules: [
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
},
{
test: /\.s[ac]ss$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
{
test: /\.styl$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "stylus-loader"],
},
]
}
plugins: [
new MiniCssExtractPlugin({
// 不用默認(rèn)輸出位置,指定輸出位置
filename: "static/css/main.css",
})
]
}
3.4.4 配置壓縮樣式plugin
是否要安裝壓縮其它資源的plugin: js壓縮在生產(chǎn)模式下打包webpack會(huì)默認(rèn)輸出壓縮后的js (使用的Terser),配置輸出html的插件并且在生產(chǎn)模式下打包webpack也會(huì)默認(rèn)輸出壓縮后的html,但是css不會(huì)默認(rèn)壓縮,需要安裝壓縮樣式的plugin
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
plugins: [new CssMinimizerPlugin()]
}
3.5 devServer
3.5.1 配置熱更新服務(wù)器
// 開(kāi)發(fā)服務(wù)器:不會(huì)輸出打包內(nèi)容到dist
devServer: {
host: "localhost",
port: "3000",
// 是否自動(dòng)打開(kāi)瀏覽器
open: true,
// 是否只局部重新構(gòu)建修改的文件,如果為false,那么已修改就會(huì)重新打包所有內(nèi)容(默認(rèn)值就是true)
hot: true
}
3.6 mode
3.6.1 開(kāi)發(fā)模式和生產(chǎn)模式
需要配置mode為development或production,這會(huì)影響打包結(jié)果是否壓縮
舉例: 網(wǎng)上說(shuō)可以在根目錄新建一個(gè)config文件夾來(lái)保存生產(chǎn)和開(kāi)發(fā)環(huán)境的webpack配置文件。個(gè)人覺(jué)得使用一份webpack配置文件即可,在內(nèi)部進(jìn)行環(huán)境判斷即可。 開(kāi)發(fā)模式不需要指定output的輸出目錄,開(kāi)發(fā)模式下一般采用webpack-dev-server服務(wù)器運(yùn)行,會(huì)將打包結(jié)果輸入內(nèi)存。
注意: 熱更新會(huì)導(dǎo)致頻繁打包來(lái)更新打包后的文件,webpack-dev-server將文件輸出到內(nèi)存可以提高IO讀寫(xiě)效率
3.7 內(nèi)部過(guò)程
3.7.1 Module
Module是模塊,指的是文件,不是webpack運(yùn)行時(shí)內(nèi)部過(guò)程的概念。
模塊支持性: webpack支持多種模塊系統(tǒng),例如ESM,AMD,CommonJS等。
模塊類(lèi)型:
模塊一定是文件。如果通過(guò)import從某個(gè)js文件中導(dǎo)入了一個(gè)函數(shù),那么函數(shù)不會(huì)被視為webpack模塊,函數(shù)所在的js文件將被視為模塊。 模塊不一定是js文件,在ESM系統(tǒng)中import可以導(dǎo)入各種資源,css文件,圖片等都可以被視為webpack模塊。
3.7.2 Chunk
Chunk定義:
Chunk是webpack打包的內(nèi)部過(guò)程中的概念,Chunk表示塊,是一種文件,產(chǎn)生在webpack的輸入到輸出過(guò)程中。
Chunk產(chǎn)生方式:
根據(jù)入口產(chǎn)生的依賴(lài)圖: 將文件系統(tǒng)看做圖,那么默認(rèn)情況下每個(gè)打包入口的所有可達(dá)節(jié)點(diǎn)的集合都會(huì)被裝載到一個(gè)chunk中。根據(jù)按需加載: 例如在ESM系統(tǒng)中每個(gè) import()導(dǎo)入的模塊都會(huì)被裝載到一個(gè)新chunk中。這么做的意圖是將按需加載的模塊放到不同chunk中,然后輸出不同的bundle,最后在使用某個(gè)按需加載的模塊時(shí)就會(huì)發(fā)請(qǐng)求新的bundle文件,以此實(shí)現(xiàn)按需加載。
注意: 這是一個(gè)理想情況,chunk和bundle不一定是一一對(duì)應(yīng)的,默認(rèn)情況下它們是一對(duì)一,如果有特殊配置,例如code split,那么會(huì)有不同的對(duì)應(yīng)關(guān)系。因此真正按需加載時(shí)你想請(qǐng)求模塊A,可能也會(huì)同時(shí)請(qǐng)求到模塊B和模塊C,因?yàn)槟憧赡軐?duì)配置了chunk組,使部分模塊打包到一個(gè)chunk中,最后使得按需加載的模塊都在一個(gè)bundle中。
舉例: chunk和bundle不一定是一一對(duì)應(yīng)的。下圖描述了一個(gè)chunk可以對(duì)應(yīng)多個(gè)最終輸出的bundle。如果使用了上文提到的提取樣式的插件就會(huì)有改效果。![外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳 根據(jù)代碼分割: 如果使用了下文提到的代碼分割來(lái)做優(yōu)化,那么將會(huì)按照配置規(guī)則來(lái)將模塊轉(zhuǎn)載到chunk中,可能導(dǎo)致一個(gè)模塊被裝到多個(gè)chunk的情況。
產(chǎn)生Chunk的時(shí)機(jī):
產(chǎn)生的chunk并不只是對(duì)代碼塊進(jìn)行組裝或拆分。產(chǎn)生chunk時(shí),loader和plugin的大部分工作已經(jīng)完成了。chunk到bundle的過(guò)程主要是optimization配置和部分plugin配置在發(fā)揮作用,例如MiniCssExtractPlugin將css提取成單獨(dú)文件。
注意: code split只是控制生成chunk的規(guī)則,并不控制chunk到bundle的生成規(guī)則。
3.7.3 Bundle
bundle是文件,表示webpack最終輸出的文件。bundle不是webpack運(yùn)行時(shí)的內(nèi)部概念。
3.7.4 Chunk和Bundle的對(duì)應(yīng)關(guān)系
參考Chunk中的描述
4.Webpack優(yōu)化
4.1 開(kāi)發(fā)體驗(yàn)優(yōu)化
使用 devtool中不同 source-map報(bào)錯(cuò)信息和源代碼進(jìn)行對(duì)應(yīng),否則報(bào)錯(cuò)信息會(huì)直接指向打包后的代碼。不同的 source-map記錄的報(bào)錯(cuò)信息精度不同,因此打包時(shí)編譯的速度會(huì)受到影響。
開(kāi)發(fā)模式: cheap-module-source-map
錯(cuò)誤信息精確到行打包速度快 生產(chǎn)模式: source-map
錯(cuò)誤信息精確到行和列打包速度比上面的慢
4.1.1 cheap-module-source-map
報(bào)錯(cuò)信息精確到行
下圖中顯然是args.reduce調(diào)用時(shí)出錯(cuò),但是波浪線(xiàn)從第二行開(kāi)頭開(kāi)始。不能精確到從args開(kāi)始。
報(bào)錯(cuò)信息精確到行不代表不會(huì)顯示列信息,下圖顯示了錯(cuò)誤在源代碼第二行第一列。只是這個(gè)列不夠精確,默認(rèn)就是第一列。
映射建立方式
打包后文件片段
對(duì)應(yīng)的源文件片段
打包后會(huì)生成json格式的.map映射文件描述映射關(guān)系。其中mappings描述了映射關(guān)系。指的是打包后文件到源文件的映射。
這里翻譯成js形式可以解讀為:
return args.reduce(function (sum, item) {映射到 args.reduce((sum, item) => (isNaN(item) ? sum : sum + item), 0)(); return isNaN(item) ? sum : sum + item;映射到 args.reduce((sum, item) => (isNaN(item) ? sum : sum + item), 0)(); }, 0)();映射到 args.reduce((sum, item) => (isNaN(item) ? sum : sum + item), 0)();
從編譯原理的角度理解:
詞法分析階段只記錄源代碼的行信息到詞素(token)上。行信息傳遞到語(yǔ)法分析階段生成的抽象語(yǔ)法樹(shù)上。依據(jù)抽象語(yǔ)法樹(shù)生成中間代碼時(shí),中間代碼的每個(gè)詞素都能記憶源代碼的行信息。并且保證中間代碼某一行的詞素都對(duì)應(yīng)源代碼的同一行,否則換行。編譯完成后根據(jù)輸出代碼每行詞素記憶的行信息,建立.map映射文件,方便代碼出錯(cuò)時(shí)快速定位到源文件。
4.1.2 source-map
報(bào)錯(cuò)信息精確到行和列
與source-map相比,波浪線(xiàn)可以精確到調(diào)用語(yǔ)句。控制臺(tái)報(bào)錯(cuò)信息可以精確到調(diào)用語(yǔ)句所在的列。
映射建立方式
和cheap-module-source-map一樣都會(huì)建立.map映射文件,效果和上面一樣。
翻譯一下就是建立了詞素到詞素的映射關(guān)系,詞素到詞素的映射細(xì)粒度比行到行的映射細(xì)粒度小很多,所以生成的map文件也會(huì)大一些。
為什么生產(chǎn)模式下不能用cheap-module-source-map?
如果生成模式下使用cheap-module-source-map會(huì)導(dǎo)致無(wú)法精確定位錯(cuò)誤信息到源文件,并且也不會(huì)生成.map映射文件。生產(chǎn)模式下js代碼是壓縮的,只有一行。從編譯原理的角度來(lái)講,即使代碼只有一行那也不影響我做映射,因?yàn)樵~法分析階段記錄的映射是精確到詞素(token)級(jí)別的。但是問(wèn)題出在cheap-module-source-map工具上,它只會(huì)根據(jù)詞素記錄的行信息來(lái)建立行到行映射,即建立細(xì)粒度更大更模糊的映射。壓縮后的代碼可能只有一行,所以映射只會(huì)有一條,所以無(wú)法精確定位。并且不論詞法分析階段記錄再多信息也沒(méi)用,因?yàn)樽詈笾粫?huì)取一小部分信息做模糊的映射。
4.2 提升打包構(gòu)建速度
4.2.1 模塊熱更新—HotModuleReplacement
配置devServer
設(shè)置hot屬性為true,當(dāng)文件修改時(shí)只局部重新構(gòu)建。設(shè)置為false時(shí),文件一旦修改就會(huì)對(duì)整個(gè)項(xiàng)目重新構(gòu)建。
hot屬性無(wú)法針對(duì)js文件。如果修改js可以局部重新構(gòu)建,那么可以考慮安裝vue-hot-loader或react-hot-laoder。
4.2.2 匹配唯一Loader一oneOf
配置loader
生產(chǎn)環(huán)境和開(kāi)發(fā)環(huán)境都適用。如果不配置oneOf,那么處理css文件時(shí)能命中第一個(gè)loader,但是命中完成后還會(huì)向后遍歷看是否能命中其它loader。如果文件很多時(shí),打包的時(shí)間復(fù)雜度的常數(shù)會(huì)很大。oneOf是一種優(yōu)化常數(shù)的方法。
rules: [
{
oneOf: [
{
test: /\.css$/,
use: getStyleLoader(),
},
{
test: /\.less$/,
// 類(lèi)比上述過(guò)程,那就是先把less轉(zhuǎn)為css-loader,然后重復(fù)上述過(guò)程
use: getStyleLoader("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoader("stylus-loader"),
},
// 激活內(nèi)置的loader(見(jiàn)文檔外部資源模塊)
{
test: /\.(png|jpe?g|gif|webp)$/,
// 大圖盡量不轉(zhuǎn)base64,base64增長(zhǎng)率太大。小圖片轉(zhuǎn)base64,base64增長(zhǎng)率比較小,可以接受。
type: "asset",
parser: {
dataUrlCondition: {
// 小于300KB的圖片會(huì)轉(zhuǎn)base64以節(jié)省請(qǐng)求數(shù)量。(300KB有點(diǎn)大,是因?yàn)楸镜貨](méi)有小圖)。
// 并且重新打包不會(huì)刪除之前資源,所以刪除dist后重新打包即可。
maxSize: 10 * 1024,
},
},
generator: {
// 輸出圖片名稱(chēng)(也可以是路徑)
// hash: 圖片名稱(chēng)(唯一的)。下面的是10表示只取前十位
// ext: 文件擴(kuò)展名
// query: url查詢(xún)參數(shù)
filename: "static/images/[hash:10][ext][query]",
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
generator: {
filename: "static/media/[hash:10][ext][query]",
},
},
// babel
{
test: /\.js$/,
// 不處理的文件
exclude: /node_modules|bower_components/,
use: {
loader: "babel-loader",
// 也可以寫(xiě)?yīng)毩⑽募abel.config.js
// options: {
// presets: ["@babel/preset-env"],
// },
},
},
],
},
],
4.2.3 包含和排除—Include/Exclude
一般只針對(duì)js文件做處理,例如babel和eslint。并且要注意include和exclude只能存在一個(gè)。
// babel
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 不處理的文件
exclude: /node_modules|bower_components/,
// 處理的文件(exclude和include只能使用一種)
// include: path.resolve(__dirname, '../src')
use: ["babel-loader"],
}
]
}
}
// eslint
module.exports = {
plugins: [
new ESLintPlugin({
// 檢測(cè)的文件夾
context: path.resolve(__dirname, "../src"),
// 不處理的文件
exclude: 'node_modules'
})
]
}
4.2.4 緩存—Cache
使用緩存原因
打包文件中往往都是js占比最大,并且每個(gè)js都會(huì)經(jīng)過(guò)Eslint插件和Babel加載器編譯。因此在重新構(gòu)建時(shí)可以緩存上次編譯結(jié)果,為重復(fù)構(gòu)建提速。
module.exports = {
module: {
rules: [
{
oneOf: [
// babel
{
test: /\.js$/,
exclude: /node_modules|bower_components/,
use: {
loader: "babel-loader",
options: {
// 開(kāi)啟babel緩存
cacheDirectory: true,
// 不壓縮緩存文件(壓縮會(huì)影響速度,壓縮只會(huì)減少緩存占用的存儲(chǔ)空間)
cacheCompression: false,
},
},
}
]
}
]
},
plugins: [
// eslint
new ESLintPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
// 開(kāi)啟緩存
cache: true,
// 緩存位置
cacheLocation: path.resolve(__dirname, "../node__modules/.cache/eslintcache")
})
]
}
4.2.5 多進(jìn)程—Thread
對(duì)JS處理的loader或plugin進(jìn)行多線(xiàn)程優(yōu)化
下面對(duì):(1)babel(2)eslint(3)terser(4)cssMinimizer做了多進(jìn)程優(yōu)化
注意: 關(guān)于優(yōu)化的插件可以放到optimization屬性的配置中。例如代碼壓縮,代碼分割,tree shaking等等。
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const threads = 4
module.exports = {
// babel
module: {
oneOf: [
{
test: /\.js$/,
exclude: /node_modules|bower_components/,
use: [
{
// 安裝thread-loaders,開(kāi)啟多進(jìn)程對(duì)babel做處理
loader: "thread-loader",
options: {
// 進(jìn)程數(shù)量
works: threads,
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
],
},
]
},
plugins: [
new ESLintPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/eslintcache"
),
// 開(kāi)啟多進(jìn)程
threads,
}),
],
optimization: {
minimizer: [
// 壓縮css
new CssMinimizerPlugin(),
// 壓縮js
new TerserWebpackPlugin({
// 開(kāi)啟多進(jìn)程對(duì)JavaScript壓縮優(yōu)化
parallel: threads,
}),
]
}
}
4.3 減少代碼體積
4.3.1 移除未使用代碼—Tree Shaking
tree shaking是webpack的默認(rèn)行為。如果一個(gè)模塊導(dǎo)出了若干內(nèi)容,但是其中有某些內(nèi)容沒(méi)有被其余模塊使用,那么不會(huì)被打包最后的文件。
舉例: 假設(shè)main.js是項(xiàng)目的入口文件,在其中引用了add函數(shù),但是從main開(kāi)始打包,沒(méi)有任何其余文件引用util中的mul函數(shù),那么最后只會(huì)有add函數(shù)被打進(jìn)包。
util.js
export const add = (a, b) => a + b
export const mul = (a, b) => a * b
main.js
import { add } from './util'
console.log(add(1, 2))
4.3.2 禁止引入的方法直接注入—babel
普通babel導(dǎo)致的問(wèn)題: 正常情況下,假設(shè)項(xiàng)目中多個(gè)文件引入了lodash的debounce做防抖,按之前的打包配置,babel會(huì)把debounce方法實(shí)現(xiàn)的代碼直接注入到每個(gè)引用它的模塊中,這導(dǎo)致了debounce的代碼重復(fù)出現(xiàn)了很多次。解決方案: 安裝插件@babel/plugin-transform-runtime在babel-loader中使用,可以把注入的行為轉(zhuǎn)為引用,實(shí)際上公共代碼就出現(xiàn)一次。注意(代碼復(fù)用針對(duì)對(duì)象問(wèn)題):這個(gè)babel插件只是針對(duì)輔助函數(shù),例如Object.assign,Promise等等。并不能處理我們自定義的函數(shù)或組件,復(fù)用這些自定義代碼塊需要通過(guò)配置splitchunk實(shí)現(xiàn)。
module.exports = {
module: {
oneOf: [
// babel
{
test: /\.js$/,
exclude: /node_modules|bower_components/,
use: [
{
loader: "thread-loader",
options: {
works: threads,
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
// 減少代碼體積
plugins: ["@babel/plugin-transform-runtime"],
},
},
],
}
]
}
}
4.3.3 壓縮圖片—Image Minimizer
可以配置Image Minimizer在打包時(shí)對(duì)圖片進(jìn)行壓縮。這個(gè)優(yōu)化操作配置比較麻煩。需要安裝該插件,image-minimizer-webpack-plugin,可以在webpack官網(wǎng)上查看相關(guān)配置。
4.4 優(yōu)化代碼運(yùn)行性能
4.4.1 代碼分割—Code Split
Code Split定義:
回顧一下上文提到的Chunk的概念,而Code Split主要用于控制生成Chunk,那么Code Split主要應(yīng)用于
多入口: 打包設(shè)置多個(gè)入口分割插件: 根據(jù)Code Split配置分割代碼生成新Chunk按需加載: 遇到按需加載直接分割代碼生成新Chunk。注(產(chǎn)生過(guò)多小chunk問(wèn)題):大型項(xiàng)目中路由很多,動(dòng)態(tài)加載的代碼很多,可能導(dǎo)致分割出很多小chunk,這通過(guò)splitchunk插件無(wú)法控制,需要使用webpack內(nèi)置的limitchunk插件控制數(shù)量
Code Split通信:
背景:
瀏覽器不能直接支持AMD,CMD等模塊系統(tǒng)。假如代碼分割導(dǎo)致打包后產(chǎn)生了A和B兩個(gè)js文件,并且A文件需要引用B文件的某些代碼塊,這會(huì)怎么實(shí)現(xiàn)?
原理:
查看打包后文件(例如要求輸出UMD格式)發(fā)現(xiàn)每個(gè)文件都是一個(gè)封閉的作用域,無(wú)法直接通信。那么A要引用B中代碼時(shí),先請(qǐng)求B的資源,請(qǐng)求完后放入頁(yè)面執(zhí)行B的腳本,B會(huì)把自己導(dǎo)出的內(nèi)容掛載到全局對(duì)象上。不同模塊引用代碼塊通過(guò)全局對(duì)象進(jìn)行通信。
Code Split默認(rèn)配置:
需要注意,在生產(chǎn)模式和開(kāi)發(fā)模式下默認(rèn)值會(huì)有些出入
字段取值默認(rèn)值含義chunks‘a(chǎn)sync’|‘a(chǎn)ll’| ‘initial’async代碼分割應(yīng)用的場(chǎng)景: 表示代碼分割默認(rèn)應(yīng)用場(chǎng)景。默認(rèn)為async,只對(duì)按需加載生效。all表示對(duì)所有模塊生效。initial表示對(duì)入口和按需加載生效。minSizenumber20000分割時(shí),chunk的最小大?。?表示當(dāng)前chunk超過(guò)該大小后會(huì)自動(dòng)分割出新chunk。單位是byte,默認(rèn)在20kb左右。minRemainingSizenumber0分割后,chunk的最小大小: 表示分割后不能生成過(guò)小的chunk。默認(rèn)為0,表示分割后的chunk大小不做限制。minChunksnumber1分割后,chunk的最小引用次數(shù): 表示分割后的chunk被其它c(diǎn)hunk引用的最小次數(shù)。默認(rèn)是1,該條件默認(rèn)生效,表示默認(rèn)可分割。注(無(wú)法復(fù)用問(wèn)題):如果SPA應(yīng)用只打出一個(gè)chunk,chunk中有很多復(fù)用代碼塊,如果設(shè)置minChunks大于1不會(huì)觸發(fā)公共代碼塊分割maxAsyncRequestsnumber30分割后,產(chǎn)生的chunk的最大數(shù)量: 指的是按需加載文件生成的chunk過(guò)大,需要進(jìn)一步拆分成多個(gè)chunk,限制進(jìn)一步拆分chunk的數(shù)量。maxInitialRequestsnumber30分割后,產(chǎn)生的chunk的最大數(shù)量: 指的是入口文件生成的chunk過(guò)大,需要進(jìn)一步拆分成多個(gè)chunk。顯然這對(duì)異步加載模塊無(wú)效,因?yàn)楫惒郊虞d模塊已經(jīng)分到另一個(gè)chunk里面了,使用的是maxAsyncRequests配置。enforceSizeThresholdnumber50000cacheGroups配置哪些代碼可以打包到一個(gè)組里
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
}
}
4.4.2 提取公共模塊—代碼分割—Code Split
回顧一下上文提到的Chunk的概念,Code Split主要用于控制生成Chunk,那么Code Split主要方式是
多入口: 打包設(shè)置多個(gè)入口分割插件: 根據(jù)Code Split配置分割代碼生成新Chunk按需加載: 遇到按需加載直接分割代碼生成新Chunk。
除此之外Code Split可以用于指定哪些代碼需要打包到一個(gè)Chunk中,可以借助這個(gè)能力來(lái)復(fù)用代碼塊(4.3.2中提到)。
module.exports = {
splitChunks: {
cacheGroups: {
// node_modules中代碼都打包到vendors塊中
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'all',
name: 'vendors'
},
// common中代碼都打包到common塊中
common: {
test: /[\\/]packages\/common[\\/]/,
priority: -10,
chunks: 'all',
name: 'common'
},
// sfa中代碼都打包到sfa塊中
sfa: {
test: /[\\/]packages\/sfa[\\/]/,
priority: -10,
chunks: 'all',
name: 'sfa'
},
// 其它代碼打包規(guī)則
// reuseExistingChunk: true 表示開(kāi)啟打包時(shí)代碼復(fù)用
default: {
// minChunks為1表示SPA中只要存在代碼復(fù)用就可以分割出去
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
},
},
}
4.4.3 緩存優(yōu)化—runtimeChunk
背景:
在代碼分割后,我們某次迭代可能只修改部分文件,這樣導(dǎo)致打包后的對(duì)應(yīng)chunk發(fā)生變化,并且文件hash值變化。但是實(shí)際不是這樣,這會(huì)導(dǎo)致入口chunk的緩存失效。
運(yùn)行時(shí):
導(dǎo)致這個(gè)問(wèn)題的原因是每個(gè)入口chunk中都會(huì)保存webpack運(yùn)行時(shí)的配置,比如模塊加載邏輯。如果代碼觸發(fā)了Code Split,那么這部分代碼將描述每個(gè)模塊對(duì)應(yīng)的hash值文件名。如果其中某個(gè)模塊的hash值改變就會(huì)導(dǎo)致這部分代碼刷新,從而導(dǎo)致入口chunk被修改,最后導(dǎo)致緩存無(wú)法復(fù)用。
解決方案:
配置runtimeChunk,指定從入口chunk提取出運(yùn)行時(shí)文件。
module.exports = {
optimization: {
runtimeChunk: {
name: (entryPoint) => `runtime~${entryPoint.name}.js`
}
}
}
代碼中有app和main兩個(gè)打包入口,配置runtimeChunk后生成了兩個(gè)runtime文件。這樣修改52,743,847這幾個(gè)chunk時(shí)就不會(huì)導(dǎo)致main或app的chunk緩存失效了。
優(yōu)點(diǎn):
多入口: 多入口時(shí)可以設(shè)置值runtimeChunk為single,把所有運(yùn)行時(shí)代碼提取到一個(gè)runtime文件中。如果多個(gè)入口中的依賴(lài)圖中存在相同節(jié)點(diǎn),那么將會(huì)復(fù)用運(yùn)行時(shí)配置,不需要在每個(gè)入口chunk中指明依賴(lài)圖中的chunk名稱(chēng),減少代碼量。緩存值: 不論是多入口還是單入口,抽取運(yùn)行時(shí)配置都可以避免修改chunk時(shí)使入口chunk的緩存失效。
柚子快報(bào)激活碼778899分享:前端 Webpack學(xué)習(xí)記錄
推薦鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。