前端工程化_知識(shí)點(diǎn)精講

浮躁背后的本質(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

  1. CSS重點(diǎn)概念精講
  2. JS_基礎(chǔ)知識(shí)點(diǎn)精講
  3. 網(wǎng)絡(luò)通信_(tái)知識(shí)點(diǎn)精講
  4. JS_手寫實(shí)現(xiàn)

好了焚刚,天不早了,干點(diǎn)正事哇扇调。
[圖片上傳失敗...(image-fcf2df-1666320988527)]

你能所學(xué)到的知識(shí)點(diǎn)

  1. 常見腳手架
  2. Source Map 推薦閱讀指數(shù) ????????
  3. Webpack 打包過程 推薦閱讀指數(shù) ??????
  4. 微前端
  5. Webpack Loader vs Plugin 推薦閱讀指數(shù) ????????
  6. Webpack 生命周期
  7. Webpack編譯階段提效 推薦閱讀指數(shù) ??????????
  8. Webpack打包階段提效 推薦閱讀指數(shù) ??????????
  9. 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ǔ)包
    1. create-react-app 用于選擇腳手架創(chuàng)建項(xiàng)目
    2. react-scripts 則作為所創(chuàng)建項(xiàng)目中的運(yùn)行時(shí)依賴包,提供了封裝后的項(xiàng)目啟動(dòng)燃领、編譯士聪、測(cè)試等基礎(chǔ)工具
    • 自定義配置能力
    1. react-app-rewired:利用config-overrides.js 文件來對(duì) webpack 配置進(jìn)行擴(kuò)展
    2. 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 由三部分組成
    1. 作為全局命令的 @vue/cli
    2. 作為項(xiàng)目?jī)?nèi)集成工具的 @vue/cli-service
    3. 作為功能插件系統(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)的字符串,它分成三層

  1. 第一層是行對(duì)應(yīng)轧抗,以分號(hào)(;)表示,每個(gè)分號(hào)對(duì)應(yīng)轉(zhuǎn)換后源碼的一行
  2. 第二層是位置對(duì)應(yīng)恩敌,以逗號(hào)(,)表示,每個(gè)逗號(hào)對(duì)應(yīng)轉(zhuǎn)換后源碼的一個(gè)位置
  3. 第三層是位置轉(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 的處理插件。

  1. EvalDevToolModulePlugin
  2. EvalSourceMapDevToolPlugin
  3. 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: truecolumn: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è)chunkdist目錄下都會(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ù)。


微前端的潛在問題

  1. 潛在的性能隱患

    • 在團(tuán)隊(duì)成員龐雜和項(xiàng)目應(yīng)用比較復(fù)雜的情況下典鸡,讓團(tuán)隊(duì)自由地選擇他們想使用的技術(shù)棧被廓,需要有一些重要的權(quán)衡。
    • 用戶下載和運(yùn)行解決各種常見問題的代碼萝玷,如樣式設(shè)計(jì)嫁乘、獲取和改變數(shù)據(jù)、日期格式等球碉。以不同的方式蜓斧,用不同的工具和框架,重復(fù)多次汁尺。這是個(gè)很大的開銷法精。
  2. 困難的依賴性管理

    • 即使通過同一個(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)加載
  3. 服務(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.scssA.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è)模塊

  1. Compiler
  2. Compilation

它們都擴(kuò)展自 Tapable 類,用于實(shí)現(xiàn)工作流程中的生命周期劃分,以便在不同的生命周期節(jié)點(diǎn)上注冊(cè)和調(diào)用插件,其中所暴露出來的生命周期節(jié)點(diǎn)稱為Hook(俗稱鉤子)峻厚。

Compiler Hooks

構(gòu)建器實(shí)例的生命周期可以分為 3 個(gè)階段

  1. 初始化階段
  2. 構(gòu)建過程階段
  3. 產(chǎn)物生成階段

初始化階段

  1. environment
    • 在創(chuàng)建完 compiler 實(shí)例且執(zhí)行了配置內(nèi)定義的插件的 apply 方法后觸發(fā)
  2. afterEnvironment
    • 在創(chuàng)建完 compiler 實(shí)例且執(zhí)行了配置內(nèi)定義的插件的 apply 方法后觸發(fā)
  3. entryOption
    • 執(zhí)行 EntryOptions 插件
  4. afterPlugins
  5. afterResolvers
    • 解析了 resolver 配置后觸發(fā)

構(gòu)建過程階段

  1. normalModuleFactory
    • 在兩類模塊工廠創(chuàng)建后觸發(fā)
  2. contextModuleFactory
    • 在兩類模塊工廠創(chuàng)建后觸發(fā)
  3. beforeRun
  4. run
  5. beforeCompile
  6. compile
  7. thisCompilation
  8. make
    • 最耗時(shí)
    • 會(huì)執(zhí)行模塊編譯到優(yōu)化的完整過程

產(chǎn)物生成階段

  1. shouldEmit响蕴、emit、assetEmitted惠桃、afterEmit
    • 在構(gòu)建完成后浦夷,處理產(chǎn)物的過程中觸發(fā)
  2. failed辖试、done
    • 在達(dá)到最終結(jié)果狀態(tài)時(shí)觸發(fā)

Compilation Hooks

構(gòu)建過程實(shí)例的生命周期分為兩個(gè)階段:

  1. 構(gòu)建階段
  2. 優(yōu)化階段

Webpack編譯階段提效

真正影響整個(gè)構(gòu)建效率的是 Compilation 實(shí)例的處理過程

  1. 編譯模塊
  2. 優(yōu)化處理

提升編譯階段的構(gòu)建效率,大致可以分為三個(gè)方向

  1. 減少執(zhí)行編譯的模塊
  2. 提升單個(gè)模塊構(gòu)建的速度
  3. 并行構(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è))

  1. IgnorePlugin (國(guó)際化包)
  2. 按需引入類庫(kù)模塊 (工具類庫(kù))
  3. DllPlugin
  4. 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$/,
});
  1. resourceRegExp
    • 指定需要剔除的文件(夾)
  2. contextRegExp (可選)
    • 特定目錄
  • 任何以 'moment' 結(jié)尾的目錄中匹配 './locale' 的任何 require 語(yǔ)句都將被忽略

除了 moment 包以外社付,其他一些帶有國(guó)際化模塊的依賴包舵变,都可以應(yīng)用這一優(yōu)化方式。

按需引入類庫(kù)模塊

減少執(zhí)行模塊的方式是按需引入,一般適用于工具類庫(kù)性質(zhì)的依賴包的優(yōu)化

[圖片上傳失敗...(image-f7e926-1666320988527)]

優(yōu)化處理

  1. 定向引入
    • 效果最佳的方式是在導(dǎo)入聲明時(shí)只導(dǎo)入依賴包內(nèi)的特定模塊
  2. 使用插件
    • 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)的代碼提前打包好(例如 reactreact-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 配置中的 externalsDllPlugin 解決的是同一類問題媳握。將依賴的框架等模塊從構(gòu)建過程中移除

ExternalsDllPlugin 區(qū)別

  1. 配置方面
    • externals 更簡(jiǎn)單
    • DllPlugin 需要獨(dú)立的配置文件
  2. DllPlugin 包含了依賴包的獨(dú)立構(gòu)建流程磷脯,而 externals 配置中不包含依賴框架的生成方式蛾找,通常使用已傳入 CDN 的依賴包
  3. externals 配置的依賴包需要單獨(dú)指定依賴模塊的加載方式:全局對(duì)象、CommonJS赵誓、AMD
  4. 在引用依賴包的子模塊時(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)建的速度幻枉。

常用的方式有

  1. include/exclude
  2. noParse
  3. Source Map
  4. TypeScript 編譯優(yōu)化
  5. 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 中的 includeexclude 配置存在沖突的情況下,優(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 有兩種方式

  1. 使用 ts-loader
  2. 使用 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ī)則

  1. resolve.modules
    • 指定查找模塊的目錄范圍
  2. resolve.extensions
    • 指定查找模塊的文件類型范圍
  3. resolve.mainFields
    • 指定查找模塊的 package.json 中主文件的屬性名
  4. 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)目。
使用方式

  1. HappyPack
  2. thread-loader
  3. 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è)不同的方向

  1. 針對(duì)某些任務(wù)
    • 使用效率更高的工具或配置項(xiàng)
    • 從而提升當(dāng)前任務(wù)的工作效率
  2. 提升特定任務(wù)的優(yōu)化效果
    • 減少傳遞給下一任務(wù)的數(shù)據(jù)量
    • 從而提升后續(xù)環(huán)節(jié)的工作效率

以提升當(dāng)前任務(wù)工作效率為目標(biāo)的方案

一般在項(xiàng)目的優(yōu)化階段檩帐,主要耗時(shí)的任務(wù)有兩個(gè)

  1. 生成 ChunkAssets
    • 即根據(jù) Chunk 信息生成 Chunk 的產(chǎn)物代碼
    • 主要在 Webpack 引擎內(nèi)部的模塊中處理
      • 優(yōu)化手段較少
      • 主要集中在利用緩存方面
  2. 優(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 原本是 Forkuglify-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è)方面

  1. Cache 選項(xiàng)
    • 默認(rèn)開啟
    • 使用緩存能夠極大程度上提升再次構(gòu)建時(shí)的工作效率
  2. Parallel 選項(xiàng)
    • 默認(rèn)開啟
    • 并發(fā)選項(xiàng)在大多數(shù)情況下能夠提升該插件的工作效率
    • 適用大項(xiàng)目
  3. terserOptions 選項(xiàng)
    • Terser 工具中的 minify 選項(xiàng)集合
    • 主要看其中的compressmangle選項(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種壓縮工具可供選擇

  1. OptimizeCSSAssetsPlugin
    • CRA中使用
  2. OptimizeCSSNanoPlugin
    • vue-cli
  3. CssMinimizerWebpackPlugin
    • 2020 年 Webpack 社區(qū)新發(fā)布的 CSS 壓縮插件

它們都基于 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é)的工作效率

  1. Code Splitting
  2. Tree Shaking
  3. Scope Hoisting (作用域提升)
  4. sideEffects

Code Splitting(分塊打包)

Code Splitting--通過把項(xiàng)目中的資源模塊按照我們?cè)O(shè)計(jì)的規(guī)則打包到不同的 bundle

  • 降低應(yīng)用的啟動(dòng)成本
  • 提高響應(yīng)速度

Webpack 實(shí)現(xiàn)分包的方式主要有兩種

  1. 根據(jù)業(yè)務(wù)不同配置多個(gè)打包入口宫盔,輸出多個(gè)打包結(jié)果
  2. 結(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è)面灼芭,分別是 indexalbum

  • 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)的文件路徑脆栋。
  • 輸出文件名
    - 使用 [name] 這種占位符來輸出動(dòng)態(tài)的文件名
    - [name] 最終會(huì)被替換為入口的名稱

  • 通過 html-webpack-plugin
    - 分別為 indexalbum 頁(yè)面生成了對(duì)應(yīng)的 HTML 文件

分包加載

輸出 HTML 的插件倦卖,默認(rèn)這個(gè)插件會(huì)自動(dòng)注入所有的打包結(jié)果。如果需要指定所使用的 bundle,通過 HtmlWebpackPluginchunks 屬性來設(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
  • Promisethen 方法中能夠拿到模塊對(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)化功能

  1. usedExports
    • 打包結(jié)果中只導(dǎo)出外部用到的成員
  2. 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-envmodules 屬性設(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ì)比讀取已有緩存。

  1. 編譯階段的緩存優(yōu)化
  2. 優(yōu)化打包階段的緩存優(yōu)化

編譯階段的緩存優(yōu)化

編譯過程的耗時(shí)點(diǎn)主要在使用不同加載器(Loader)來編譯模塊的過程

Babel-loader

Babel-loader 是絕大部分項(xiàng)目中會(huì)使用到的 JS/JSX/TS 編譯器

與緩存相關(guān)的設(shè)置主要有

  1. cacheDirectory
    • 默認(rèn)為 false勇婴,即不開啟緩存
    • 當(dāng)值為 true 時(shí)開啟緩存并使用默認(rèn)緩存目錄
      • ./node_modules/.cache/babel-loader/
    • 也可以指定其他路徑值作為緩存目錄
  2. cacheIdentifier
    • 用于計(jì)算緩存標(biāo)識(shí)符
    • 默認(rèn)使用
      • Babel 相關(guān)依賴包的版本
      • babelrc 配置文件的內(nèi)容
      • 環(huán)境變量
      • 與模塊內(nèi)容
    • 一起參與計(jì)算緩存標(biāo)識(shí)符
  3. 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)開啟緩存,其他的插件如 OptimizeCSSAssetsPluginOptimizeCSSNanoPlugin 目前還不支持使用緩存

使用緩存注意點(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)化緩存利用率

好處

  1. 合并通用依賴
  2. 提升構(gòu)建緩存利用率
  3. 提升資源訪問的緩存利用率
  4. 資源懶加載

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)]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市刻肄,隨后出現(xiàn)的幾起案子瓤球,更是在濱河造成了極大的恐慌,老刑警劉巖敏弃,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卦羡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)绿饵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門欠肾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拟赊,你說我怎么就攤上這事刺桃。” “怎么了要门?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵虏肾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我欢搜,道長(zhǎng)封豪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任炒瘟,我火速辦了婚禮吹埠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疮装。我一直安慰自己缘琅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布廓推。 她就那樣靜靜地躺著刷袍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪樊展。 梳的紋絲不亂的頭發(fā)上呻纹,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音专缠,去河邊找鬼雷酪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛涝婉,可吹牛的內(nèi)容都是我干的哥力。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼墩弯,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼吩跋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起渔工,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锌钮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后涨缚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體轧粟,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年脓魏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了兰吟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡茂翔,死狀恐怖混蔼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情珊燎,我是刑警寧澤惭嚣,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站悔政,受9級(jí)特大地震影響晚吞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谋国,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一槽地、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芦瘾,春花似錦捌蚊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至祷愉,卻和暖如春窗宦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谣辞。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工迫摔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泥从。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓句占,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親躯嫉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纱烘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容