浮躁背后的本質(zhì)是自卑感
大家好,我是柒八九。
今天邮辽,我們繼續(xù)前端面試的知識(shí)點(diǎn)。我們來談?wù)勱P(guān)于前端工程化的相關(guān)知識(shí)點(diǎn)和具體的算法贸营。
該系列的文章吨述,大部分都是前面文章的知識(shí)點(diǎn)匯總,如果想具體了解相關(guān)內(nèi)容钞脂,請(qǐng)移步相關(guān)系列揣云,進(jìn)行探討。
如果冰啃,想了解該系列的文章邓夕,可以參考我們已經(jīng)發(fā)布的文章。如下是往期文章阎毅。
文章list
好了焚刚,天不早了,干點(diǎn)正事哇扇调。
[圖片上傳失敗...(image-fcf2df-1666320988527)]
你能所學(xué)到的知識(shí)點(diǎn)
- 常見腳手架
Source Map
推薦閱讀指數(shù) ????????Webpack
打包過程 推薦閱讀指數(shù) ??????- 微前端
Webpack Loader
vsPlugin
推薦閱讀指數(shù) ????????- Webpack 生命周期
- Webpack編譯階段提效 推薦閱讀指數(shù) ??????????
- Webpack打包階段提效 推薦閱讀指數(shù) ??????????
- Webpack 緩存優(yōu)化 推薦閱讀指數(shù) ??????????
概念介紹
腳手架
腳手架作為一種創(chuàng)建項(xiàng)目初始文件的工具被廣泛地應(yīng)用于新項(xiàng)目或者迭代初始階段
使用工具替代人工操作能夠避免人為失誤引起的低級(jí)錯(cuò)誤矿咕,同時(shí)結(jié)合整體前端工程化方案,快速生成功能模塊配置肃拜、自動(dòng)安裝依賴等痴腌,降低了時(shí)間成本。
常見腳手架工具
現(xiàn)在主流的前端腳手架工具有兩類:
名稱 | 模板框架 | 多選項(xiàng)生產(chǎn) | 支持自定義模板 | 特點(diǎn) |
---|---|---|---|---|
Create-React-App | React | 否 | 是 | React 官方維護(hù) |
Vue CLI | Vue | 是 | 是 | Vue官方維護(hù) |
-
CRA:
Facebook
官方提供的React
開發(fā)工具集,- 包含兩個(gè)基礎(chǔ)包
-
create-react-app
用于選擇腳手架創(chuàng)建項(xiàng)目 -
react-scripts
則作為所創(chuàng)建項(xiàng)目中的運(yùn)行時(shí)依賴包,提供了封裝后的項(xiàng)目啟動(dòng)燃领、編譯士聪、測(cè)試等基礎(chǔ)工具
- 自定義配置能力
-
react-app-rewired
:利用config-overrides.js
文件來對(duì)webpack
配置進(jìn)行擴(kuò)展 -
customize-cra
:利用react-app-rewired
的配置文件config-overrides.js
對(duì)webpack
配置進(jìn)行修改
-
Vue CLI: Vue CLI 由 Vue.js 官方維護(hù),其定位是 Vue.js 快速開發(fā)的完整系統(tǒng)猛蔽。完整的 Vue CLI 由三部分組成
- 作為全局命令的
@vue/cli
- 作為項(xiàng)目?jī)?nèi)集成工具的
@vue/cli-service
- 作為功能插件系統(tǒng)的
@vue/cli-plugin
- 作為全局命令的
腳手架模板
在實(shí)際開發(fā)中剥悟,我們可以通過創(chuàng)建腳手架對(duì)應(yīng)的模板對(duì)項(xiàng)目進(jìn)行定制化處理。
定制化模板可以彌補(bǔ)官方提供基礎(chǔ)工具集不滿足特定需求的場(chǎng)景曼库。
定制化有如下優(yōu)點(diǎn)(但有不僅限這些優(yōu)點(diǎn))
- 為項(xiàng)目引入新的通用特性
- 針對(duì)構(gòu)建環(huán)節(jié)的
webpack
配置優(yōu)化区岗,來提升開發(fā)環(huán)境的效率和生產(chǎn)環(huán)境的性能等 - 定制符合團(tuán)隊(duì)內(nèi)部規(guī)范的代碼檢測(cè)規(guī)則配置
- 定制單元測(cè)試等輔助工具模塊的配置項(xiàng)
- 定制符合團(tuán)隊(duì)內(nèi)部規(guī)范的目錄結(jié)構(gòu)與通用業(yè)務(wù)模塊
- 業(yè)務(wù)組件庫(kù)
- 輔助工具類
- 頁(yè)面模板
我們簡(jiǎn)單的為CRA
配置一個(gè)最簡(jiǎn)單的模板。
為 CRA 創(chuàng)建自定義模板
作為一個(gè)最簡(jiǎn)化的 CRA 模板毁枯,模板中包含如下必要文件
-
README.md
: 用于在npm
倉(cāng)庫(kù)中顯示的模板說明 -
package.json
: 用于描述模板本身的元信息慈缔, 例如名稱、運(yùn)行腳本种玛、依賴包名和版本等 -
template.json
:用于描述基于模板創(chuàng)建的項(xiàng)目中的package.json
信息 -
template
目錄: 用于復(fù)制到創(chuàng)建后的項(xiàng)目中藐鹤,gitignore
在復(fù)制后重命名為.gitignore
,public/index.html和src/index 為運(yùn)行 react-scripts 的必要文件
對(duì)應(yīng)的目錄結(jié)構(gòu)如下:
cra-template-[template-name]/
README.md (for npm)
template.json
package.json
template/
README.md (for projects created from this template)
gitignore
public/
index.html
src/
index.js (or index.tsx)
在使用時(shí)瓤檐,還是需要將模板通過 npm link
命令映射到全局依賴中或發(fā)布到 npm
倉(cāng)庫(kù)中。
然后執(zhí)行創(chuàng)建項(xiàng)目的命令
npx create-react-app [app-name] --template [template-name]
腳手架的功能和本質(zhì)
功能是創(chuàng)建項(xiàng)目初始文件,本質(zhì)是方案的封裝
SourceMap
Source Map
:映射轉(zhuǎn)換后的代碼與源代碼之間的關(guān)系
一段轉(zhuǎn)換后的代碼娱节,通過轉(zhuǎn)換過程中生成的 Source Map
文件就可以逆向解析得到對(duì)應(yīng)的源代碼挠蛉。
Source Map
是一個(gè) JSON
格式的文件,這個(gè) JSON
里面記錄的就是轉(zhuǎn)換后和轉(zhuǎn)換前代碼之間的映射關(guān)系肄满。
Source Map 的基本原理
在編譯器(Babel/SWC
)編譯處理的過程中谴古,在生成產(chǎn)物代碼的同時(shí),也生成產(chǎn)物代碼中被轉(zhuǎn)換的部分與源代碼中相應(yīng)部分的映射關(guān)系表。
有了完整的映射表稠歉,就可以通過 Chrome
控制臺(tái)中的"Enable Javascript source map"來實(shí)現(xiàn)調(diào)試時(shí)的顯示與定位源代碼功能掰担。
Source map的格式
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
這里額外對(duì)mappings
字段做一個(gè)介紹
這是一個(gè)很長(zhǎng)的字符串,它分成三層
- 第一層是行對(duì)應(yīng)轧抗,以分號(hào)(;)表示,每個(gè)分號(hào)對(duì)應(yīng)轉(zhuǎn)換后源碼的一行
- 第二層是位置對(duì)應(yīng)恩敌,以逗號(hào)(,)表示,每個(gè)逗號(hào)對(duì)應(yīng)轉(zhuǎn)換后源碼的一個(gè)位置
- 第三層是位置轉(zhuǎn)換,以
VLQ
編碼表示,代表該位置對(duì)應(yīng)的轉(zhuǎn)換前的源碼位置
mappings:"AAAAA,BBBBB;CCCCC"
:轉(zhuǎn)換后的源碼分成兩行横媚,第一行有兩個(gè)位置纠炮,第二行有一個(gè)位置。
一般我們會(huì)在轉(zhuǎn)換后的代碼中通過添加一行注釋的方式來去引入 Source Map 文件
對(duì)于同一個(gè)源文件灯蝴,根據(jù)不同的目標(biāo)恢口,可以生成不同效果的 Source Map
。在開發(fā)環(huán)境和生產(chǎn)環(huán)境下穷躁,對(duì)于 source map
功能的期望也有所不同耕肩。
- 在開發(fā)環(huán)境中,要求構(gòu)建速度快/質(zhì)量高/便于提升開發(fā)效率,而不關(guān)注生成文件的大小和訪問方式
- 在生產(chǎn)環(huán)境中问潭,需要考慮是否需要提供線上
Source Map
/生成的文件大小/訪問方式是否會(huì)對(duì)頁(yè)面性能造成影響,最后才考慮質(zhì)量和構(gòu)建速度
由于猿诸,現(xiàn)在在打包領(lǐng)域,Webpack
還是一個(gè)繞不過去的大山狡忙,所以梳虽,在了解到基礎(chǔ)的知識(shí)點(diǎn)后,需要將知識(shí)配合實(shí)際項(xiàng)目進(jìn)行分析和學(xué)習(xí)灾茁。
Source Map 處理插件
根據(jù)不同規(guī)則窜觉,實(shí)際上 Webpack
是從三種插件中選擇其一作為 source map
的處理插件。
EvalDevToolModulePlugin
EvalSourceMapDevToolPlugin
SourceMapDevToolPlugin
插件中有
eval
字段的北专,模塊產(chǎn)物是通過eval()
封裝的禀挫,只有SourceMapDevToolPlugin
(AKA:SMDP
)生成.map
文件
不同預(yù)設(shè)的效果總結(jié)
分別從質(zhì)量/構(gòu)建速度/包大小和生成方式三個(gè)角度來分析
質(zhì)量
生成的 source map
的質(zhì)量分為 5 個(gè)級(jí)別,對(duì)應(yīng)的調(diào)試便捷性依次降低拓颓。
[圖片上傳失敗...(image-9877bb-1666320988527)]
構(gòu)建速度
不同環(huán)境下關(guān)注的速度也不同
- 在開發(fā)環(huán)境下
- 一直開著
devServer
语婴,再次構(gòu)建的速度對(duì)我們的效率影響遠(yuǎn)大于初次構(gòu)建的速度
- 一直開著
- 在生產(chǎn)環(huán)境下
- 通常不會(huì)開啟再次構(gòu)建,因此相比再次構(gòu)建,初次構(gòu)建的速度更值得關(guān)注
包大小和生成方式
在開發(fā)環(huán)境下我們并不需要關(guān)注這些因素腻格。
開發(fā)環(huán)境下 Source Map 推薦預(yù)設(shè)
利用EvalSourceMapDevToolPlugin
對(duì)開發(fā)環(huán)境提速画拾。
在開發(fā)階段,調(diào)試的是開發(fā)的業(yè)務(wù)代碼部分菜职,而非依賴的第三方模塊部分,所以在生成 source map
的時(shí)候如果可以排除第三方模塊的部分,而只生成業(yè)務(wù)代碼的 source map
旗闽,無(wú)疑能進(jìn)一步提升構(gòu)建的速度酬核。
webpack.config.js
...
//devtool: 'eval-source-map',
devtool: false,
plugins: [
new webpack.EvalSourceMapDevToolPlugin({
exclude: /node_modules/,
module: true,
columns: false
})
],
...
將 devtool
設(shè)為 false
,就是丟棄webpack
或者CRA
的默認(rèn)配置,而是直接使用 EvalSourceMapDevToolPlugin
,通過傳入 module: true
和 column:false
,達(dá)到和預(yù)設(shè) eval-cheap-module-source-map
一樣的質(zhì)量适室。但是嫡意,我們傳入 exclude
參數(shù),排除第三方依賴包的 source map 生成捣辆。進(jìn)一步提升了構(gòu)建的速度蔬螟。
捕捉Source Map文件& 反編譯Source Map
在控制臺(tái)的網(wǎng)絡(luò)面板中通常看不到
source map
文件的請(qǐng)求汽畴,其原因是出于安全考慮Chrome
隱藏了source map
的請(qǐng)求
我們可以通過一些抓包工具進(jìn)行文件的抓取
-
Chrome
自帶的chrome://net-export/
whistle
WireShark
既然能通過抓包工具進(jìn)行Source Map
文件的捕獲旧巾,那么在前面我們就說了,Source Map:映射轉(zhuǎn)換后的代碼與源代碼之間的關(guān)系忍些,那么我們反其道而行鲁猩,我們是不是可以通過Source Map
來反向推出源代碼。
這里推薦一個(gè)庫(kù)shuji
罢坝±眨可以通過x.map
(x可以是js
也可以是css
),反編譯出源代碼。
Webpack 打包過程
本質(zhì)上嘁酿,
webpack
是一個(gè)現(xiàn)代JavaScript
應(yīng)用程序的靜態(tài)<span style="font-weight:800;color:#FFA500;font-size:18px">{模塊打包器| module bundler}</span>
當(dāng)webpack
處理應(yīng)用程序時(shí)隙券,它會(huì)遞歸地構(gòu)建一個(gè)<span style="font-weight:800;color:#FFA500;font-size:18px">{依賴關(guān)系圖| dependency graph}</span>,其中包含應(yīng)用程序需要的每個(gè)模塊闹司,然后將所有這些模塊打包成一個(gè)或多個(gè)bundle
娱仔。
打包流程
一切都從
entry
對(duì)象開始
模塊是文件的升級(jí)版。
模塊开仰,一旦創(chuàng)建和構(gòu)建拟枚,除了源代碼,還包含很多有意義的信息众弓,如:
- 使用的加載器
- 它的依賴關(guān)系
- 它的出口(如果有的話)
- 它的哈希值
同時(shí)entry
對(duì)象中的每一項(xiàng)都可以被認(rèn)為是模塊樹中的根模塊恩溅。模塊樹是因?yàn)楦K可能需要一些其他的模塊,這些模塊可能需要其他的模塊谓娃,等等脚乡。所有這些模塊樹都被儲(chǔ)存在 ModuleGraph
中。
webpack是建立在很多插件之上的
webpack的可擴(kuò)展性是通過hook
實(shí)現(xiàn)的。例如奶稠,你可以在 ModuleGraph
建立后俯艰,當(dāng)一個(gè)新的<span style="font-weight:800;color:#FFA500;font-size:18px">{資源|asset }</span>被生成時(shí),在模塊即將被建立前(運(yùn)行加載器和解析源代碼)锌订,添加自定義邏輯竹握。大多數(shù)時(shí)候,hook
是根據(jù)它們的目的分組的辆飘,對(duì)于任何定義好的目的啦辐,都有一個(gè)<span style="font-weight:800;color:#FFA500;font-size:18px">{插件| plugin}</span>。
- 對(duì)于
entry
對(duì)象中的每一項(xiàng)蜈项,都會(huì)有一個(gè)EntryPlugin
實(shí)例芹关,其中創(chuàng)建了一個(gè)EntryDependency
。- 這個(gè)
EntryDependency
保存了模塊的請(qǐng)求(即文件的路徑)紧卒,并且通過映射到一個(gè)模塊工廠侥衬,即NormalModuleFactory
,提供了一種使該請(qǐng)求有用的方法跑芳。- <span style="font-weight:800;color:#FFA500;font-size:18px">{模塊工廠|ModuleFactory}</span>知道如何從一個(gè)文件路徑中創(chuàng)建對(duì)webpack有用的實(shí)體轴总。
- <span style="font-weight:800;color:#FFA500;font-size:18px">{依賴關(guān)系|dependency }</span>對(duì)創(chuàng)建一個(gè)模塊至關(guān)重要,因?yàn)樗鼡碛兄匾男畔⒘觯热缒K的請(qǐng)求和如何處理該請(qǐng)求肘习。依賴關(guān)系有幾種類型,并不是所有的依賴關(guān)系都對(duì)創(chuàng)建一個(gè)新模塊有用坡倔。
- 從每個(gè)
EntryPlugin
實(shí)例漂佩,在新創(chuàng)建的EntryDependency
的幫助下,將創(chuàng)建一個(gè)模塊樹罪塔。模塊樹是建立在模塊和它們的依賴關(guān)系之上的投蝉,這些模塊也可以有依賴關(guān)系。
::: block-1
Module
模塊是一個(gè)文件的升級(jí)版征堪。
一個(gè)模塊瘩缆,一旦創(chuàng)建和構(gòu)建,除了源代碼佃蚜,還包含很多有意義的信息庸娱,如:
- 使用的加載器
- 它的依賴關(guān)系
- 它的出口(如果有的話)
- 它的哈希值
:::
::: block-1
Chunk
一個(gè)Chunk封裝了一個(gè)或多個(gè)模塊
一般情況下,entry
文件(一個(gè)entry
文件=entry
對(duì)象的一個(gè)項(xiàng)目)的數(shù)量與所產(chǎn)生的Chunk
的數(shù)量成正比谐算。
- 因?yàn)?code>entry對(duì)象可能只有一個(gè)項(xiàng)目熟尉,而結(jié)果塊的數(shù)量可能大于1。的確洲脂,對(duì)于每一個(gè)
entry
項(xiàng)目斤儿,在dist
目錄中都會(huì)有一個(gè)相應(yīng)的chunk
但也可能是隱式創(chuàng)建其他的
chunk
,例如在使用import()
函數(shù)時(shí)。但不管它是如何被創(chuàng)建的,每個(gè)chunk
在dist
目錄下都會(huì)有一個(gè)對(duì)應(yīng)的文件。
:::
::: block-1
ChunkGroup
一個(gè)ChunkGroup包含一個(gè)或多個(gè)chunks
一個(gè)ChunkGroup
可以是另一個(gè)ChunkGroup
的父或子孽水。
- 例如,當(dāng)使用動(dòng)態(tài)導(dǎo)入時(shí)堕油,每使用一個(gè)
import()
函數(shù),就會(huì)有一個(gè)ChunkGroup被創(chuàng)建肮之,它的父級(jí)是一個(gè)現(xiàn)有的ChunkGroup
馍迄,即包括使用import()
函數(shù)的文件(即模塊)的那個(gè)。
:::
微前端
微前端是一套用于組織大型前端應(yīng)用的指導(dǎo)規(guī)范局骤。是受后端微服務(wù)啟發(fā)而發(fā)展而來。
微前端試圖解決什么問題暴凑?
獨(dú)立的可構(gòu)建的前端資源允許團(tuán)隊(duì)擁有屬于自己的構(gòu)建流程峦甩。這避免了冗長(zhǎng)的構(gòu)建時(shí)間,并防止個(gè)別團(tuán)隊(duì)被其他成員的誤操作而影響項(xiàng)目構(gòu)建现喳。
獨(dú)立的可部署的前端資源允許解耦的這些團(tuán)隊(duì)定期將他們的代碼獨(dú)自發(fā)布到生產(chǎn)環(huán)境凯傲,而不需要復(fù)雜的部署順序。
這也意味著當(dāng)bug出現(xiàn)和事故發(fā)生時(shí)嗦篱,代碼可以自動(dòng)回滾冰单,而不會(huì)干擾到其他團(tuán)隊(duì)的項(xiàng)目開發(fā)。清晰的所有權(quán)在大型項(xiàng)目上是必要的灸促。在大型項(xiàng)目中诫欠,如果某個(gè)功能不被維護(hù),那幾乎就像它不存在一樣浴栽。因?yàn)闆]有人負(fù)責(zé)保持它的更新荒叼,它往往會(huì)變得難以維護(hù)。
微前端的潛在問題
-
潛在的性能隱患
- 在團(tuán)隊(duì)成員龐雜和項(xiàng)目應(yīng)用比較復(fù)雜的情況下典鸡,讓團(tuán)隊(duì)自由地選擇他們想使用的技術(shù)棧被廓,需要有一些重要的權(quán)衡。
- 用戶下載和運(yùn)行解決各種常見問題的代碼萝玷,如樣式設(shè)計(jì)嫁乘、獲取和改變數(shù)據(jù)、日期格式等球碉。以不同的方式蜓斧,用不同的工具和框架,重復(fù)多次汁尺。這是個(gè)很大的開銷法精。
-
困難的依賴性管理
- 即使通過同一個(gè)庫(kù)來解決功能共享的問題,但是,在規(guī)模比較大的情況下搂蜓,總是存在著該庫(kù)的不同版本需要保持更新的問題狼荞。特別是對(duì)于像高優(yōu)先級(jí)的更新。
- 解決方案帮碰,使用同一個(gè)打包工具相味,能夠做到優(yōu)化處理。
-
Tree-Shaking
-有了依賴關(guān)系后殉挽,可以通過依賴分析丰涉,去掉一些沒用的代碼 -
Code-Splitting
- 這些模塊根據(jù)功能和類型拆分到不同的分組(chunk)里,然后生成不同的文件斯碌,然后將變更頻繁和幾乎不動(dòng)的模塊劃分到不同的chunk
,并封裝到特定文件中一死,針對(duì)幾乎不會(huì)變更的資源和模塊,則可以利用瀏覽器緩存進(jìn)行資源的優(yōu)化處理
-
-
Lazy-Load
:生成的代碼傻唾,擁有自己的runtime
,這樣可以實(shí)現(xiàn)模塊的lazy load
投慈,也就是把Code-Splitting
分出來的 chunk,在運(yùn)行時(shí)動(dòng)態(tài)加載
服務(wù)的粒度劃分
Webpack Loader vs Plugin
-
loader
是文件加載器冠骄,能夠加載資源文件伪煤,并對(duì)這些文件進(jìn)行一些處理,諸如編譯凛辣、壓縮等抱既,最終一起打包到指定的文件中 -
plugin
賦予了webpack
各種靈活的功能,例如打包優(yōu)化扁誓、資源管理防泵、環(huán)境變量注入等,目的是解決 loader 無(wú)法實(shí)現(xiàn)的其他事
[圖片上傳失敗...(image-6e2e-1666320988527)]
兩者在運(yùn)行時(shí)機(jī)上的區(qū)別
-
loader
運(yùn)行在打包文件之前 -
plugins
在整個(gè)編譯周期都起作用
對(duì)于 loader
跋理,實(shí)質(zhì)是一個(gè)轉(zhuǎn)換器择克,將A文件進(jìn)行編譯形成B文件,操作的是文件前普,比如將A.scss
或A.less
轉(zhuǎn)變?yōu)?code>B.css肚邢,單純的文件轉(zhuǎn)換過程。
在 Webpack
運(yùn)行的生命周期中會(huì)廣播出許多事件拭卿,Plugin
可以監(jiān)聽這些事件骡湖,在合適的時(shí)機(jī)通過Webpack提供的 API改變輸出結(jié)果。
Webpack 生命周期
Webpack
工作流程中最核心的兩個(gè)模塊
Compiler
Compilation
它們都擴(kuò)展自 Tapable
類,用于實(shí)現(xiàn)工作流程中的生命周期劃分,以便在不同的生命周期節(jié)點(diǎn)上注冊(cè)和調(diào)用插件,其中所暴露出來的生命周期節(jié)點(diǎn)稱為Hook
(俗稱鉤子)峻厚。
Compiler Hooks
構(gòu)建器實(shí)例的生命周期可以分為 3 個(gè)階段
- 初始化階段
- 構(gòu)建過程階段
- 產(chǎn)物生成階段
初始化階段
-
environment
- 在創(chuàng)建完
compiler
實(shí)例且執(zhí)行了配置內(nèi)定義的插件的apply
方法后觸發(fā)
- 在創(chuàng)建完
-
afterEnvironment
- 在創(chuàng)建完
compiler
實(shí)例且執(zhí)行了配置內(nèi)定義的插件的apply
方法后觸發(fā)
- 在創(chuàng)建完
-
entryOption
- 執(zhí)行
EntryOptions
插件
- 執(zhí)行
afterPlugins
-
afterResolvers
- 解析了
resolver
配置后觸發(fā)
- 解析了
構(gòu)建過程階段
-
normalModuleFactory
- 在兩類模塊工廠創(chuàng)建后觸發(fā)
-
contextModuleFactory
- 在兩類模塊工廠創(chuàng)建后觸發(fā)
beforeRun
run
beforeCompile
compile
thisCompilation
-
make
- 最耗時(shí)
- 會(huì)執(zhí)行模塊編譯到優(yōu)化的完整過程
產(chǎn)物生成階段
-
shouldEmit响蕴、emit、assetEmitted惠桃、afterEmit
- 在構(gòu)建完成后浦夷,處理產(chǎn)物的過程中觸發(fā)
-
failed辖试、done
- 在達(dá)到最終結(jié)果狀態(tài)時(shí)觸發(fā)
Compilation Hooks
構(gòu)建過程實(shí)例的生命周期分為兩個(gè)階段:
- 構(gòu)建階段
- 優(yōu)化階段
Webpack編譯階段提效
真正影響整個(gè)構(gòu)建效率的是
Compilation
實(shí)例的處理過程
- 編譯模塊
- 優(yōu)化處理
要提升編譯階段的構(gòu)建效率,大致可以分為三個(gè)方向
- 減少執(zhí)行編譯的模塊
- 提升單個(gè)模塊構(gòu)建的速度
- 并行構(gòu)建以提升總體效率
優(yōu)化前的準(zhǔn)備工作
準(zhǔn)備基于時(shí)間的分析工具 - SMP
需要一類插件劈狐,來幫助我們統(tǒng)計(jì)項(xiàng)目構(gòu)建過程中在編譯階段的耗時(shí)情況
speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
});
準(zhǔn)備基于產(chǎn)物內(nèi)容的分析工具 - WBA
找出對(duì)產(chǎn)物包體積影響最大的包的構(gòu)成罐孝,從而找到那些冗余的、可以被優(yōu)化的依賴項(xiàng)肥缔。不僅能減小最后的包體積大小莲兢,也能提升構(gòu)建模塊時(shí)的效率
webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
編譯模塊階段所耗的時(shí)間是從單個(gè)入口點(diǎn)開始,編譯每個(gè)模塊的時(shí)間的總和
減少執(zhí)行編譯的模塊(4個(gè))
-
IgnorePlugin
(國(guó)際化包) - 按需引入類庫(kù)模塊 (工具類庫(kù))
DllPlugin
Externals
IgnorePlugin
有的依賴包续膳,除了項(xiàng)目所需的模塊內(nèi)容外改艇,還會(huì)附帶一些多余的模塊
[圖片上傳失敗...(image-fe58f0-1666320988527)]
Webpack 提供的 IgnorePlugin
,即可在構(gòu)建模塊時(shí)直接剔除那些需要被排除的模塊坟岔,從而提升構(gòu)建模塊的速度谒兄,并減少產(chǎn)物體積。
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
});
- resourceRegExp
- 指定需要剔除的文件(夾)
- contextRegExp (可選)
- 特定目錄
- 任何以 'moment' 結(jié)尾的目錄中匹配 './locale' 的任何
require
語(yǔ)句都將被忽略
除了 moment
包以外社付,其他一些帶有國(guó)際化模塊的依賴包舵变,都可以應(yīng)用這一優(yōu)化方式。
按需引入類庫(kù)模塊
減少執(zhí)行模塊的方式是按需引入,一般適用于工具類庫(kù)性質(zhì)的依賴包的優(yōu)化
[圖片上傳失敗...(image-f7e926-1666320988527)]
優(yōu)化處理
- 定向引入
- 效果最佳的方式是在導(dǎo)入聲明時(shí)只導(dǎo)入依賴包內(nèi)的特定模塊
- 使用插件
babel-plugin-lodash
-
babel-plugin-import
- 適用于
antd
,antd-mobil
,lodash
- 適用于
{
"plugins": [["import",{
"libraryName": "lodash",
"libraryDirectory": "",
"camel2DashComponentName": false, // default: true
}]]
}
注意點(diǎn)
Tree Shaking
瘦穆,這一特性也能減少產(chǎn)物包的體積,但是 Tree Shaking
需要相應(yīng)導(dǎo)入的依賴包使用 ES6
模塊化,而 lodash
還是基于 CommonJS
,需要替換為 lodash-es
才能生效
Tree Shaking
是在優(yōu)化階段生效,Tree Shaking
并不能減少模塊編譯階段的構(gòu)建時(shí)間。
DllPlugin
它的核心思想是將項(xiàng)目依賴的框架等模塊單獨(dú)構(gòu)建打包赊豌,與普通構(gòu)建流程區(qū)分開扛或。
事先把常用但又構(gòu)建時(shí)間長(zhǎng)的代碼提前打包好(例如 react
、react-dom
)碘饼,取個(gè)名字叫 dll
熙兔。后面再打包的時(shí)候就跳過原來的未打包代碼,直接用 dll
艾恼。這樣一來住涉,構(gòu)建時(shí)間就會(huì)縮短,提高 webpack
打包速度钠绍。
兩個(gè)配置文件
webpack.dll.config.js
module.exports = {
entry: {
vendor: ['react', 'react-dom'],
},
output: {
filename: '[name].dll.js',
path: path.join(__dirname, 'dll'),
publicPath: '/dll',
library: '[name]_dll',
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
name: '[name]_dll',
path: path.join(__dirname, 'dll' + '/[name]_manifest.json'),
}),
],
}
new webpack.DllPlugin
- 生成manifest.json
文件舆声,供DllReferencePlugin
指向依賴模塊位置
- 將公共模塊 react/react-dom
抽離到項(xiàng)目中dll文件下
webpack.app.config.js
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/vendor_manifest.json'),
}),
],
new webpack.DllReferencePlugin
- 引用
manifest.json
文件,尋找依賴模塊
webpack 4 有著比 dll 更好的打包性能,所以在最新版的cra中已經(jīng)將dll剔除柳爽。
Externals
Webpack 配置中的 externals
和 DllPlugin
解決的是同一類問題媳握。將依賴的框架等模塊從構(gòu)建過程中移除。
Externals
和 DllPlugin
區(qū)別
- 配置方面
-
externals
更簡(jiǎn)單 -
DllPlugin
需要獨(dú)立的配置文件
-
-
DllPlugin
包含了依賴包的獨(dú)立構(gòu)建流程磷脯,而externals
配置中不包含依賴框架的生成方式蛾找,通常使用已傳入 CDN 的依賴包 -
externals
配置的依賴包需要單獨(dú)指定依賴模塊的加載方式:全局對(duì)象、CommonJS
赵誓、AMD
等 - 在引用依賴包的子模塊時(shí)打毛,
DllPlugin
無(wú)須更改柿赊,而externals
則會(huì)將子模塊打入項(xiàng)目包中
使用范例
module.exports = {
//...
externals: [
{
// String
react: 'react',
// Object
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // indicates global variable
},
// [string]
subtract: ['./math', 'subtract'],
},
// Function
function ({ context, request }, callback) {
if (/^yourregex$/.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
},
// Regex
/^(jquery|\$)$/i,
],
};
提升單個(gè)模塊構(gòu)建的速度
在保持構(gòu)建模塊數(shù)量不變的情況下,提升單個(gè)模塊構(gòu)建的速度幻枉。
常用的方式有
include/exclude
noParse
Source Map
- TypeScript 編譯優(yōu)化
- Resolve
通過減少構(gòu)建單個(gè)模塊時(shí)的一些處理邏輯來提升速度
include/exclude
Webpack -loader
配置中的 include/exclude
碰声,是常用的優(yōu)化特定模塊構(gòu)建速度的方式之一
-
include
的用途是只對(duì)符合條件的模塊使用指定Loader
進(jìn)行轉(zhuǎn)換處理 -
exclude
則相反,不對(duì)特定條件的模塊使用該Loader
例如不使用 babel-loader 處理 node_modules 中的模塊
使用范例
module.exports = {
......
module: {
rules: [
{
test: /\.js$/,
include: /src/
exclude: /node_modules/,
use: ['babel-loader'],
},
],
},
}
注意點(diǎn)
- 通過
include/exclude
排除的模塊展辞,并非不進(jìn)行編譯奥邮,而是使用Webpack
默認(rèn)的 js 模塊編譯器進(jìn)行編譯 - 在一個(gè)
loader
中的include
與exclude
配置存在沖突的情況下,優(yōu)先使用exclude
的配置罗珍,而忽略沖突的include
部分的配置
noParse
Webpack 配置中的 module.noParse
則是在 include/exclude
的基礎(chǔ)上洽腺,進(jìn)一步省略了使用默認(rèn) js 模塊編譯器進(jìn)行編譯的時(shí)間
使用范例
module.exports = {
......
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
},
],
},
}
Source Map
對(duì)于生產(chǎn)環(huán)境的代碼構(gòu)建而言,會(huì)根據(jù)項(xiàng)目實(shí)際情況判斷是否開啟 Source Map
在開啟 Source Map
的情況下覆旱,優(yōu)先選擇與源文件分離的類型 --例如 "source-map"
TypeScript 編譯優(yōu)化
Webpack 中編譯 TS 有兩種方式
- 使用
ts-loader
- 使用
babel-loader
在使用 ts-loader
時(shí)蘸朋,由于 ts-loader
默認(rèn)在編譯前進(jìn)行類型檢查,因此編譯時(shí)間往往比較慢
通過加上配置項(xiàng) transpileOnly: true
扣唱,可以在編譯時(shí)忽略類型檢查
module.exports = {
......
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
],
},
}
babel-loader
則需要單獨(dú)安裝 @babel/preset-typescript
來支持編譯 TS,配合 ForkTsCheckerWebpackPlugin
使用類型檢查功能
module.exports = {
......
module: {
rules: [
{
test: /\.ts$/,
use: ['babel-loader'],
},
],
},
plugins: [
new TSCheckerPlugin({
typescript: {
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
],
}
Resolve
Webpack
中的 resolve
配置制定的是在構(gòu)建時(shí)指定查找模塊文件的規(guī)則
-
resolve.modules
- 指定查找模塊的目錄范圍
-
resolve.extensions
- 指定查找模塊的文件類型范圍
-
resolve.mainFields
- 指定查找模塊的
package.json
中主文件的屬性名
- 指定查找模塊的
-
resolve.symlinks
- 指定在查找模塊時(shí)是否處理軟連接
這些規(guī)則在處理每個(gè)模塊時(shí)都會(huì)有所應(yīng)用藕坯,因此盡管對(duì)小型項(xiàng)目的構(gòu)建速度來說影響不大,對(duì)于大型的模塊眾多的項(xiàng)目而言,使用默認(rèn)配置和增加了大量無(wú)效范圍后噪沙,構(gòu)建時(shí)長(zhǎng)的變化炼彪。
并行構(gòu)建以提升總體效率
并行構(gòu)建的方案早在 Webpack 2
時(shí)代已經(jīng)出現(xiàn),適用于大項(xiàng)目。
使用方式
- HappyPack
- thread-loader
- parallel-webpack
HappyPack 與 thread-loader
兩種工具的本質(zhì)作用相同正歼,都作用于模塊編譯的 Loader
上辐马,用于在特定 Loader 的編譯過程中。
開啟多進(jìn)程的方式加速編譯
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader',
’babel-loader‘
],
},
],
},
};
parallel-webpack
并發(fā)構(gòu)建的第二種場(chǎng)景是針對(duì)與多配置構(gòu)建局义。
Webpack
的配置文件可以是一個(gè)包含多個(gè)子配置對(duì)象的數(shù)組喜爷,在執(zhí)行這類多配置構(gòu)建時(shí),默認(rèn)串行執(zhí)行
var path = require('path');
module.exports = [
{
entry: './pageA.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'pageA.bundle.js'
}
},
{
entry: './pageB.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'pageB.bundle.js'
}
}];
通過 parallel-webpack
,就能實(shí)現(xiàn)相關(guān)配置的并行處理
"build:parallel": "parallel-webpack --config webpack.parallel.config.js"
Webpack打包階段提效
Webpack
構(gòu)建流程中的第二個(gè)階段萄唇,也就是從代碼優(yōu)化到生成產(chǎn)物階段的效率提升問題
優(yōu)化階段可以分為兩個(gè)不同的方向
- 針對(duì)某些任務(wù)
- 使用效率更高的工具或配置項(xiàng)
- 從而提升當(dāng)前任務(wù)的工作效率
- 提升特定任務(wù)的優(yōu)化效果
- 以減少傳遞給下一任務(wù)的數(shù)據(jù)量
- 從而提升后續(xù)環(huán)節(jié)的工作效率
以提升當(dāng)前任務(wù)工作效率為目標(biāo)的方案
一般在項(xiàng)目的優(yōu)化階段檩帐,主要耗時(shí)的任務(wù)有兩個(gè)
- 生成
ChunkAssets
- 即根據(jù)
Chunk
信息生成 Chunk 的產(chǎn)物代碼 - 主要在
Webpack
引擎內(nèi)部的模塊中處理- 優(yōu)化手段較少
- 主要集中在利用緩存方面
- 即根據(jù)
- 優(yōu)化 Assets
- 即壓縮 Chunk 產(chǎn)物代碼
面向 JS 的壓縮工具
Webpack 4 中內(nèi)置了 TerserWebpackPlugin
作為默認(rèn)的 JS 壓縮工具--基于 Terser
。之前的版本另萤,需要單獨(dú)引入湃密,早期主要使用的是 UglifyJSWebpackPlugin
-- 基于 UglifyJS
。兩者在壓縮效率與質(zhì)量方面差別不大四敞,但 Terser
整體上略勝一籌
Terser 和 UglifyJS 插件中的效率優(yōu)化
Terser
原本是 Fork
自 uglify-es
的項(xiàng)目勾缭,其絕大部分的 API 和參數(shù)都與 uglify-es 和 uglify-js@3 兼容。
以 Terser 為例來分析其中的優(yōu)化方向
npm install terser-webpack-plugin --save-dev
TerserWebpackPlugin
中目养,對(duì)執(zhí)行效率產(chǎn)生影響的配置主要分為 3 個(gè)方面
-
Cache
選項(xiàng)- 默認(rèn)開啟
- 使用緩存能夠極大程度上提升再次構(gòu)建時(shí)的工作效率
-
Parallel
選項(xiàng)- 默認(rèn)開啟
- 并發(fā)選項(xiàng)在大多數(shù)情況下能夠提升該插件的工作效率
- 適用大項(xiàng)目
-
terserOptions
選項(xiàng)- 即
Terser
工具中的minify
選項(xiàng)集合 - 主要看其中的
compress
和mangle
選項(xiàng) -
compress
參數(shù)的作用- 執(zhí)行特定的壓縮策略
- 例如省略變量賦值的語(yǔ)句俩由,從而將變量的值直接替換到引入變量的位置上,減小代碼體積
- 在需要對(duì)壓縮階段的效率進(jìn)行優(yōu)化的情況下癌蚁,可以優(yōu)先選擇設(shè)置該參數(shù)
-
mangle
參數(shù)的作用- 對(duì)源代碼中的變量與函數(shù)名稱進(jìn)行壓縮
- 當(dāng)compress參數(shù)為 false 時(shí)幻梯,壓縮階段的效率有明顯提升兜畸,同時(shí)對(duì)壓縮的質(zhì)量影響較小
- 即
案例使用
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
cache: false,
terserOptions: {
compress: false,
mangle: false,
},
}),
],
},
};
壓縮代碼是在 optimizeChunkAssets 階段
面向 CSS 的壓縮工具
CSS 同樣有3種壓縮工具可供選擇
-
OptimizeCSSAssetsPlugin
- CRA中使用
-
OptimizeCSSNanoPlugin
- vue-cli
-
CssMinimizerWebpackPlugin
- 2020 年
Webpack
社區(qū)新發(fā)布的 CSS 壓縮插件
- 2020 年
它們都基于 cssnano
實(shí)現(xiàn),壓縮質(zhì)量方面沒有什么差別。
在壓縮效率方面碘梢,最新發(fā)布的 MiniCssExtractPlugin
,它支持緩存和多進(jìn)程,默認(rèn)開啟多進(jìn)程咬摇。這是另外兩個(gè)工具不具備的。
MiniCssExtractPlugin
對(duì)于 CSS
文件的打包煞躬,一般我們會(huì)使用 style-loader
進(jìn)行處理肛鹏,這種處理方式最終的打包結(jié)果就是 CSS
代碼會(huì)內(nèi)嵌到 JS 代碼中
MiniCssExtractPlugin
是一個(gè)可以將 CSS
代碼從打包結(jié)果中提取出來的插件。
// ./webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過 style 標(biāo)簽注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
將這個(gè)插件添加到配置對(duì)象的 plugins
數(shù)組中,使用 MiniCssExtractPlugin
中提供的 loader
去替換掉 style-loader
,以此來捕獲到所有的樣式恩沛。打包過后在扰,樣式就會(huì)存放在獨(dú)立的文件中,直接通過 link
標(biāo)簽引入頁(yè)面
CssMinimizerWebpackPlugin (webpack 5)
使用了 MiniCssExtractPlugin
過后雷客,樣式就被提取到單獨(dú)的 CSS 文件中了,樣式文件并沒有被壓縮芒珠。Webpack
內(nèi)置的壓縮插件僅僅是針對(duì) JS 文件的壓縮,其他資源文件的壓縮都需要額外的插件搅裙。
// ./webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
文檔中的這個(gè)插件并不是配置在 plugins
數(shù)組中的皱卓,而是添加到了 optimization
對(duì)象中的 minimizer
屬性中。
如果我們配置到
plugins
屬性中部逮,那么這個(gè)插件在任何情況下都會(huì)工作,而配置到 minimizer 中娜汁,就只會(huì)在 minimize 特性開啟時(shí)才工作 ---optimization.minimize: true
原本可以自動(dòng)壓縮的 JS,現(xiàn)在卻不能壓縮了,因?yàn)樵O(shè)置了 minimizer
兄朋。Webpack
認(rèn)為我們需要使用自定義壓縮器插件存炮,那內(nèi)部的 JS 壓縮器就會(huì)被覆蓋掉。必須手動(dòng)再添加回來
內(nèi)置的 JS 壓縮插件叫作 terser-webpack-plugin
,手動(dòng)添加這個(gè)模塊到 minimizer
配置當(dāng)中蜈漓。
// ./webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin(),
new CssMinimizerPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
以提升后續(xù)環(huán)節(jié)工作效率為目標(biāo)的方案
通過對(duì)本環(huán)節(jié)的處理減少后續(xù)環(huán)節(jié)處理內(nèi)容,以便提升后續(xù)環(huán)節(jié)的工作效率
Code Splitting
Tree Shaking
-
Scope Hoisting
(作用域提升) sideEffects
Code Splitting(分塊打包)
Code Splitting--通過把項(xiàng)目中的資源模塊按照我們?cè)O(shè)計(jì)的規(guī)則打包到不同的 bundle
中
- 降低應(yīng)用的啟動(dòng)成本
- 提高響應(yīng)速度
Webpack 實(shí)現(xiàn)分包的方式主要有兩種
- 根據(jù)業(yè)務(wù)不同配置多個(gè)打包入口宫盔,輸出多個(gè)打包結(jié)果
- 結(jié)合
ES Modules
的動(dòng)態(tài)導(dǎo)入(Dynamic Imports
)特性融虽,按需加載模塊
多入口打包
多入口打包一般適用于傳統(tǒng)的多頁(yè)應(yīng)用程序,最常見的劃分規(guī)則就是:一個(gè)頁(yè)面對(duì)應(yīng)一個(gè)打包入口,對(duì)于不同頁(yè)面間公用的部分,再提取到公共的結(jié)果中
├── dist
├── src
│ ├── common
│ │ ├── fetch.js
│ │ └── global.css
│ ├── album.css
│ ├── album.html
│ ├── album.js
│ ├── index.css
│ ├── index.html
│ └── index.js
├── package.json
└── webpack.config.js
有兩個(gè)頁(yè)面灼芭,分別是 index
和 album
- index.js 負(fù)責(zé)實(shí)現(xiàn) index 頁(yè)面功能邏輯
- album.js 負(fù)責(zé)實(shí)現(xiàn) album 頁(yè)面功能邏輯
- global.css 是公用的樣式文件
- fetch.js 是一個(gè)公用的模塊有额,負(fù)責(zé)請(qǐng)求 API
配置文件
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名稱
},
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html'
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html'
})
]
}
-
一般
entry
屬性中只會(huì)配置一個(gè)打包入口。- 如果需要配置多個(gè)入口彼绷,可以把
entry
定義成一個(gè)對(duì)象巍佑。 -
entry
是定義為對(duì)象而不是數(shù)組,如果是數(shù)組的話就是把多個(gè)文件打包到一起寄悯,還是一個(gè)入口萤衰。 - 這個(gè)對(duì)象中一個(gè)屬性就是一個(gè)入口,屬性名稱就是這個(gè)入口的名稱猜旬,值就是這個(gè)入口對(duì)應(yīng)的文件路徑脆栋。
- 如果需要配置多個(gè)入口彼绷,可以把
輸出文件名
- 使用[name]
這種占位符來輸出動(dòng)態(tài)的文件名
-[name]
最終會(huì)被替換為入口的名稱通過
html-webpack-plugin
- 分別為index
和album
頁(yè)面生成了對(duì)應(yīng)的 HTML 文件
分包加載
輸出 HTML 的插件倦卖,默認(rèn)這個(gè)插件會(huì)自動(dòng)注入所有的打包結(jié)果。如果需要指定所使用的 bundle
,通過 HtmlWebpackPlugin
的 chunks
屬性來設(shè)置
每個(gè)打包入口都會(huì)形成一個(gè)獨(dú)立的 chunk(塊)
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名稱
},
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index'] // 指定使用 index.bundle.js
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album'] // 指定使用 album.bundle.js
})
]
}
提取公共模塊
需要把這些公共的模塊提取到一個(gè)單獨(dú)的 bundle 中
優(yōu)化配置中開啟 splitChunks
功能
// ./webpack.config.js
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名稱
},
optimization: {
splitChunks: {
// 自動(dòng)提取所有公共模塊到單獨(dú) bundle
chunks: 'all'
}
}
// ... 其他配置
}
將它設(shè)置為 all
椿争,表示所有公共模塊都可以被提取
動(dòng)態(tài)導(dǎo)入
Code Splitting
更常見的實(shí)現(xiàn)方式還是結(jié)合 ES Modules 的動(dòng)態(tài)導(dǎo)入特性,從而實(shí)現(xiàn)按需加載怕膛。
一般我們常說的按需加載指的是加載數(shù)據(jù)或者加載圖片,這里所說的按需加載秦踪,指的是在應(yīng)用運(yùn)行過程中褐捻,需要某個(gè)資源模塊時(shí),才去加載這個(gè)模塊椅邓。
- 極大地降低了應(yīng)用啟動(dòng)時(shí)需要加載的資源體積
- 提高了應(yīng)用的響應(yīng)速度
- 節(jié)省了帶寬和流量
Webpack
中支持使用動(dòng)態(tài)導(dǎo)入的方式實(shí)現(xiàn)模塊的按需加載柠逞,而且所有動(dòng)態(tài)導(dǎo)入的模塊都會(huì)被自動(dòng)提取到單獨(dú)的 bundle 中,從而實(shí)現(xiàn)分包
├── src
│ ├── album
│ │ ├── album.css
│ │ └── album.js
│ ├── common
│ │ ├── fetch.js
│ │ └── global.css
│ ├── posts
│ │ ├── posts.css
│ │ └── posts.js
│ ├── index.html
│ └── index.js
├── package.json
└── webpack.config.js
文章列表對(duì)應(yīng)的是這里的 posts
組件希坚,而相冊(cè)列表對(duì)應(yīng)的是 album
組件
在打包入口(index.js)中同時(shí)導(dǎo)入了這兩個(gè)模塊边苹,然后根據(jù)頁(yè)面錨點(diǎn)的變化決定顯示哪個(gè)組件
// ./src/index.js
// import posts from './posts/posts'
// import album from './album/album'
const update = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
import('./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
window.addEventListener('hashchange', update)
update()
為了動(dòng)態(tài)導(dǎo)入模塊,可以將 import
關(guān)鍵字作為函數(shù)調(diào)用裁僧。當(dāng)以這種方式使用時(shí)个束,import
函數(shù)返回一個(gè) Promise
對(duì)象.
- 在需要使用組件的地方通過
import
函數(shù)導(dǎo)入指定路徑 - 方法返回的是一個(gè)
Promise
-
Promise
的then
方法中能夠拿到模塊對(duì)象
由于這里的 posts 和 album 模塊是以默認(rèn)成員導(dǎo)出,需要解構(gòu)模塊對(duì)象中的 default,先拿到導(dǎo)出成員,然后再正常使用這個(gè)導(dǎo)出成員聊疲。
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
魔法注釋
默認(rèn)通過動(dòng)態(tài)導(dǎo)入產(chǎn)生的 bundle
文件茬底,它的 name
就是一個(gè)序號(hào)。如果需要給這些 bundle
命名的話,就可以使用 Webpack 所特有的魔法注釋去實(shí)現(xiàn)
import(/* webpackChunkName: 'posts' */'./posts/posts')
.then(({ default: posts }) => {
mainElement.appendChild(posts())
})
所謂魔法注釋,就是在 import
函數(shù)的形式參數(shù)位置获洲,添加一個(gè)行內(nèi)注釋,注釋有一個(gè)特定的格式---webpackChunkName:’xxx‘
,就可以給分包的 chunk
起名字
如果 chunkName
相同的話阱表,那相同的 chunkName
最終就會(huì)被打包到一起,借助這個(gè)特點(diǎn),就可以根據(jù)自己的實(shí)際情況贡珊,靈活組織動(dòng)態(tài)加載的模塊了最爬。
Tree Shaking
Tree-shaking
最早是 Rollup
中推出的一個(gè)特性,Webpack 從 2.0 過后開始支持這個(gè)特性门岔。使用 Webpack
生產(chǎn)模式打包的優(yōu)化過程中爱致,自動(dòng)開啟這個(gè)功能 --- npx webpack --mode=production
其他模式開啟 Tree Shaking
配置對(duì)象中添加一個(gè) optimization
屬性,該屬性用來集中配置 Webpack 內(nèi)置優(yōu)化功能寒随,它的值也是一個(gè)對(duì)象糠悯,在 optimization
對(duì)象中先開啟一個(gè) usedExports
選項(xiàng),表示在輸出結(jié)果中只導(dǎo)出外部使用了的成員
module.exports = {
// ... 其他配置項(xiàng)
optimization: {
// 模塊只導(dǎo)出被使用的成員
usedExports: true
}
}
對(duì)于未引用代碼,如果我們開啟壓縮代碼功能妻往,就可以自動(dòng)壓縮掉這些沒有用到的代碼.
module.exports = {
// ... 其他配置項(xiàng)
optimization: {
// 模塊只導(dǎo)出被使用的成員
usedExports: true,
// 壓縮輸出結(jié)果
minimize: true
}
}
Tree-shaking 的實(shí)現(xiàn)互艾,整個(gè)過程用到了 Webpack 的兩個(gè)優(yōu)化功能
-
usedExports
- 打包結(jié)果中只導(dǎo)出外部用到的成員
-
minimize
- 壓縮打包結(jié)果
把代碼看成一棵大樹
-
usedExports
的作用就是標(biāo)記樹上哪些是枯樹枝、枯樹葉 -
minimize
的作用就是負(fù)責(zé)把枯樹枝讯泣、枯樹葉搖下來
結(jié)合 babel-loader 的問題
Tree-shaking
實(shí)現(xiàn)的前提是 ES Modules
,最終交給 Webpack
打包的代碼纫普,必須是使用 ES Modules
的方式來組織的模塊化
Webpack 在打包所有的模塊代碼之前
- 先是將模塊根據(jù)配置交給不同的
Loader
處理 - 最后再將
Loader
處理的結(jié)果打包到一起
為了更好的兼容性,會(huì)選擇使用 babel-loader
去轉(zhuǎn)換我們?cè)创a中的一些 ECMAScript
的新特性,Babel
在轉(zhuǎn)換 JS 代碼時(shí)方淤,很有可能處理掉代碼中的 ES Modules
部分,把它們轉(zhuǎn)換成 CommonJS
的方式甲雅。
babel-loader
(低版本)
我們?yōu)?Babel
配置的都是一個(gè) preset
(預(yù)設(shè)插件集合)净宵,而不是某些具體的插件腕让。
目前市面上使用最多的 @babel/preset-env
,這個(gè)預(yù)設(shè)里面就有轉(zhuǎn)換 ES Modules
的插件悦昵。使用這個(gè)預(yù)設(shè)時(shí)肴茄,代碼中的 ES Modules
部分就會(huì)被轉(zhuǎn)換成 CommonJS
方式。Webpack 再去打包時(shí)但指,拿到的就是以 CommonJS
方式組織的代碼了寡痰,所以 Tree-shaking
不能生效
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
]
},
optimization: {
usedExports: true
}
}
最新版本(8.x)的 babel-loader
自動(dòng)幫我們關(guān)閉了對(duì) ES Modules 轉(zhuǎn)換的插件,經(jīng)過 babel-loader
處理后的代碼默認(rèn)仍然是 ES Modules
。那 Webpack
最終打包得到的還是 ES Modules
代碼棋凳。Tree-shaking
自然也就可以正常工作了
最新版本的 babel-loader
并不會(huì)導(dǎo)致 Tree-shaking
失效,確保babel-loader
能使用Tree-shaking
拦坠。最簡(jiǎn)單的辦法就是在配置中將 @babel/preset-env
的 modules
屬性設(shè)置為 false
。確保不會(huì)轉(zhuǎn)換 ES Modules
剩岳,也就確保了 Tree-shaking
的前提
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: 'false' }]
]
}
}
}
]
},
optimization: {
usedExports: true
}
}
Scope Hoisting (作用域提升)
Webpack 3.0 中添加的一個(gè)特性,使用 concatenateModules
選項(xiàng)繼續(xù)優(yōu)化輸出
普通打包只是將一個(gè)模塊最終放入一個(gè)單獨(dú)的函數(shù)中,如果模塊很多贞滨,就意味著在輸出結(jié)果中會(huì)有很多的模塊函數(shù)。concatenateModules
配置的作用,盡可能將所有模塊合并到一起輸出到一個(gè)函數(shù)中拍棕,既提升了運(yùn)行效率晓铆,又減少了代碼的體積。
module.exports = {
// ... 其他配置項(xiàng)
optimization: {
// 模塊只導(dǎo)出被使用的成員
usedExports: true,
// 盡可能合并每一個(gè)模塊到一個(gè)函數(shù)中
concatenateModules: true,
}
}
bundle.js
中就不再是一個(gè)模塊對(duì)應(yīng)一個(gè)函數(shù)了绰播,而是把所有的模塊都放到了一個(gè)函數(shù)中
sideEffects
Webpack 4 中新增了一個(gè) sideEffects
特性,允許通過配置標(biāo)識(shí)我們的代碼是否有副作用骄噪,從而提供更大的壓縮空間。
模塊的副作用指的就是模塊執(zhí)行的時(shí)候除了導(dǎo)出成員蠢箩,是否還做了其他的事情链蕊,特性一般只有去開發(fā)一個(gè) npm 模塊時(shí)才會(huì)用到。
Tree-shaking
只能移除沒有用到的代碼成員谬泌,而想要完整移除沒有用到的模塊滔韵,那就需要開啟 sideEffects
特性了,在 optimization
中開啟 sideEffects
特性
// ./webpack.config.js
module.exports = {
mode: 'none',
optimization: {
sideEffects: true
}
}
這個(gè)特性在 production
模式下同樣會(huì)自動(dòng)開啟
Webpack 緩存優(yōu)化
利用緩存數(shù)據(jù)來加速構(gòu)建過程的處理掌实。
在初次構(gòu)建的壓縮代碼過程中陪蜻,就將這一階段的結(jié)果寫入了緩存目錄(node_modules/.cache/插件名/
)中有緩存。
當(dāng)再次構(gòu)建進(jìn)行到壓縮代碼階段時(shí)潮峦,即可對(duì)比讀取已有緩存。
- 編譯階段的緩存優(yōu)化
- 優(yōu)化打包階段的緩存優(yōu)化
編譯階段的緩存優(yōu)化
編譯過程的耗時(shí)點(diǎn)主要在使用不同加載器(Loader)來編譯模塊的過程
Babel-loader
Babel-loader
是絕大部分項(xiàng)目中會(huì)使用到的 JS/JSX/TS
編譯器
與緩存相關(guān)的設(shè)置主要有
-
cacheDirectory
- 默認(rèn)為
false
勇婴,即不開啟緩存 - 當(dāng)值為
true
時(shí)開啟緩存并使用默認(rèn)緩存目錄./node_modules/.cache/babel-loader/
- 也可以指定其他路徑值作為緩存目錄
- 默認(rèn)為
-
cacheIdentifier
- 用于計(jì)算緩存標(biāo)識(shí)符
-
默認(rèn)使用
-
Babel
相關(guān)依賴包的版本 -
babelrc
配置文件的內(nèi)容 - 環(huán)境變量
- 與模塊內(nèi)容
-
- 一起參與計(jì)算緩存標(biāo)識(shí)符
-
cacheCompression
- 默認(rèn)為 true
- 將緩存內(nèi)容壓縮為 gz 包以減小緩存目錄的體積
- 在設(shè)為
false
的情況下將跳過壓縮和解壓的過程忱嘹,從而提升這一階段的速度
Cache-loader
在編譯過程中利用緩存的第二種方式是使用 --- Cache-loader
在使用時(shí),需要將 cache-loader
添加到對(duì)構(gòu)建效率影響較大的 Loader
(如 babel-loader 等)之前
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
},
],
}
使用 cache-loader
后耕渴,比使用 babel-loader
的開啟緩存選項(xiàng)后的構(gòu)建時(shí)間更短
主要原因是 babel-loader
中的緩存信息較少,而 cache-loader
中存儲(chǔ)的 Buffer 形式的數(shù)據(jù)處理效率更高拘悦。
優(yōu)化打包階段的緩存優(yōu)化
生成 ChunkAsset
時(shí)的緩存優(yōu)化
在 Webpack 4 中,生成 ChunkAsset
過程中的緩存優(yōu)化是受限制的:
- 只有在
watch
模式下 - 且配置中開啟
cache
時(shí)(development 模式下自動(dòng)開啟),才能在這一階段執(zhí)行緩存的邏輯
在 Webpack 4 中橱脸,緩存插件是基于內(nèi)存的,只有在 watch
模式下才能在內(nèi)存中獲取到相應(yīng)的緩存數(shù)據(jù)對(duì)象
代碼壓縮時(shí)的緩存優(yōu)化
對(duì)于 JS 的壓縮TerserWebpackPlugin
/UglifyJSPlugin
都是支持緩存設(shè)置的础米。
對(duì)于 CSS 的壓縮分苇,目前最新發(fā)布的 CSSMinimizerWebpackPlugin
支持且默認(rèn)開啟緩存,其他的插件如 OptimizeCSSAssetsPlugin
和 OptimizeCSSNanoPlugin
目前還不支持使用緩存
使用緩存注意點(diǎn)
如何最大程度地讓緩存命中,成為我們選擇緩存方案后首先要考慮的
緩存標(biāo)識(shí)符發(fā)生變化導(dǎo)致的緩存失效,支持緩存的 Loader
和插件中屁桑,會(huì)根據(jù)一些固定字段的值加上所處理的模塊或 Chunk 的數(shù)據(jù) hash 值來生成對(duì)應(yīng)緩存的標(biāo)識(shí)符医寿。一旦其中的值發(fā)生變化,對(duì)應(yīng)緩存標(biāo)識(shí)符就會(huì)發(fā)生改變蘑斧,意味著對(duì)應(yīng)工具中靖秩,所有之前的緩存都將失效。需要盡可能少地變更會(huì)影響到緩存標(biāo)識(shí)符生成的字段
優(yōu)化打包階段的緩存失效竖瘾,盡管在模塊編譯階段每個(gè)模塊是單獨(dú)執(zhí)行編譯的沟突。但是當(dāng)進(jìn)入到代碼壓縮環(huán)節(jié)時(shí),各模塊已經(jīng)被組織到了相關(guān)聯(lián)的 Chunk
中,N個(gè)模塊最后只生成了一個(gè) Chunk捕传。任何一個(gè)模塊發(fā)生變化都會(huì)導(dǎo)致整個(gè) Chunk
的內(nèi)容發(fā)生變化惠拭,而使之前保存的緩存失效。
優(yōu)化方案
盡可能地把那些不變的處理成本高昂的模塊打入單獨(dú)的 Chunk 中庸论,Webpack
中的分包配置——splitChunks
职辅。使用 splitChunks
優(yōu)化緩存利用率。
好處
- 合并通用依賴
- 提升構(gòu)建緩存利用率
- 提升資源訪問的緩存利用率
- 資源懶加載
CI/CD 中的緩存目錄問題
自動(dòng)化集成的系統(tǒng)中葡公,項(xiàng)目的構(gòu)建空間會(huì)在每次構(gòu)建執(zhí)行完畢后罐农,立即回收清理。在這種情況下催什,默認(rèn)的項(xiàng)目構(gòu)建緩存目錄(node_mo dules/.cache)將無(wú)法留存涵亏。導(dǎo)致即使項(xiàng)目中開啟了緩存設(shè)置,也無(wú)法享受緩存的便利性蒲凶,反而因?yàn)樾枰獙懭刖彺嫖募速M(fèi)額外的時(shí)間
如果需要使用緩存气筋,則需要根據(jù)對(duì)應(yīng)平臺(tái)的規(guī)范,將緩存設(shè)置到公共緩存目錄下
緩存的便利性本質(zhì)在于用磁盤空間換取構(gòu)建時(shí)間,需要考慮對(duì)緩存區(qū)域的定期清理
后記
分享是一種態(tài)度旋圆。
全文完宠默,既然看到這里了,如果覺得不錯(cuò)灵巧,隨手點(diǎn)個(gè)贊和“在看”吧搀矫。
[圖片上傳失敗...(image-fca16-1666320988527)]