項目背景
我們的系統(tǒng)(一個 ToB 的 Web 單頁應(yīng)用)經(jīng)過多年的迭代亮蛔,目前已經(jīng)累積有大幾十萬行的業(yè)務(wù)代碼,30+ 路由模塊擎厢,整體的代碼量和復(fù)雜度還是比較高的究流。
項目整體是基于 Vue + TypeScirpt,而構(gòu)建工具动遭,由于最早項目是經(jīng)由 vue-cli 初始化而來芬探,所以自然而然使用的是 Webpack。
我們知道厘惦,隨著項目體量越來越大偷仿,我們在開發(fā)階段將項目跑起來,也就是通過 npm run serve 的單次冷啟動時間宵蕉,以及在項目發(fā)布時候的 npm run build 的耗時都會越來越久酝静。
因此,打包構(gòu)建優(yōu)化也是伴隨項目的成長需要持續(xù)不斷去做的事情羡玛。在早期别智,項目體量比較小的時,構(gòu)建優(yōu)化的效果可能還不太明顯稼稿,而隨著項目體量的增大薄榛,構(gòu)建耗時逐漸增加,如何盡可能的降低構(gòu)建時間让歼,則顯得越來越重要:
1敞恋、大項目通常是團隊內(nèi)多人協(xié)同開發(fā),單次開發(fā)時的冷啟動時間的降低是越,乘上人數(shù)及天數(shù)耳舅,經(jīng)年累月節(jié)省下來的時間非常可觀倚评,能較大程度的提升開發(fā)效率浦徊、提升開發(fā)體驗
2、大項目的發(fā)布構(gòu)建的效率提升天梧,能更好的保證項目發(fā)布盔性、回滾等一系列操作的準確性、及時性
本文呢岗,就將詳細介紹整個我們項目冕香,在隨著項目體量不斷增大的過程中,對整體的打包構(gòu)建效率的優(yōu)化之路悉尾。
瓶頸分析
再更具體一點构眯,我們的項目最初是基于 vue-cli 4,當時其基于的是 webpack4 版本猜丹。如無特殊說明射窒,下文的一些配置會基于 webpack4 展開。
工欲善其事必先利其器老赤,解決問題前需要分析問題轮洋,要優(yōu)化構(gòu)建速度制市,首先得分析出 Webpack 構(gòu)建編譯我們的項目過程中抬旺,耗時所在,側(cè)重點分布祥楣。
這里开财,我們使用的是 SMP 插件,統(tǒng)計各模塊耗時數(shù)據(jù)误褪。
speed-measure-webpack-plugin 是一款統(tǒng)計 webpack 打包時間的插件责鳍,不僅可以分析總的打包時間,還能分析各階段loader 的耗時兽间,并且可以輸出一個文件用于永久化存儲數(shù)據(jù)历葛。
// 安裝
npm install --save-dev speed-measure-webpack-plugin
// 使用方式
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
config.plugins.push(smp());
開發(fā)階段構(gòu)建耗時
對于 npm run serve,也就是開發(fā)階段而言嘀略,在沒有任何緩存的前提下恤溶,單次冷啟動整個項目的時間達到了驚人的 4 min。
生產(chǎn)階段構(gòu)建耗時
而對于 npm run build帜羊,也就是實際線上生產(chǎn)環(huán)境的構(gòu)建咒程,看看總體的耗時:
因此,對于構(gòu)建效率的優(yōu)化可謂是勢在必行讼育。首先帐姻,我們需要明確稠集,優(yōu)化分為兩個方向:
1、基于開發(fā)階段 npm run serve 的優(yōu)化
在開發(fā)階段饥瓷,我們的核心目標是在保有項目所有功能的前提下剥纷,盡可能提高構(gòu)建速度,保證開發(fā)時的效率呢铆,所以對于 Live 才需要的一些功能筷畦,譬如代碼混淆壓縮、圖片壓縮等功能是可以不開啟的刺洒,并且在開發(fā)階段鳖宾,我們需要熱更新。
2逆航、基于生產(chǎn)階段 npm run build 的優(yōu)化
而在生產(chǎn)打包階段鼎文,盡管構(gòu)建速度也非常重要,但是一些在開發(fā)時可有可無的功能必須加上因俐,譬如代碼壓縮拇惋、圖片壓縮。因此抹剩,生產(chǎn)構(gòu)建的目標是在于保證最終項目打包體積盡可能小撑帖,所需要的相關(guān)功能盡可能完善的前提下,同時保有較快的構(gòu)建速度澳眷。
兩者的目的不盡相同胡嘿,因此一些構(gòu)建優(yōu)化手段可能僅在其中一個環(huán)節(jié)有效。
基于上述的一些分析钳踊,本文將從如下幾個方面探討對構(gòu)建效率優(yōu)化的探索:
1衷敌、基于 Webpack 的一些常見傳統(tǒng)優(yōu)化方式
2、分模塊構(gòu)建
3拓瞪、基于 Vite 的構(gòu)建工具切換
4缴罗、基于 Es-build 插件的構(gòu)建效率優(yōu)化
為什么這么慢?
那么祭埂,為什么隨著項目的增大面氓,構(gòu)建的效率變得越來越慢了呢?
從上面兩張截圖不難看出蛆橡,對于我們這樣一個單頁應(yīng)用舌界,構(gòu)建過程中的大部分時間都消耗在編譯 JavaScript 文件及 CSS 文件的各類 Loader 上。
本文不會詳細描述 Webpack 的構(gòu)建原理航罗,我們只需要大致知道禀横,Webpack 的構(gòu)建流程,主要時間花費在遞歸遍歷各個入口文件粥血,并基于入口文件不斷尋找依賴逐個編譯再遞歸處理的過程柏锄,每次遞歸都需要經(jīng)歷 String->AST->String 的流程酿箭,然后通過不同的 loader 處理一些字符串或者執(zhí)行一些 JavaScript 腳本,由于 NodeJS 單線程的特性以及語言本身的效率限制趾娃,Webpack 構(gòu)建慢一直成為它飽受詬病的原因缭嫡。
因此,基于上述 Webpack 構(gòu)建的流程及提到的一些問題抬闷,整體的優(yōu)化方向就變成了:
1妇蛀、緩存
2、多進程
3笤成、尋路優(yōu)化
4评架、抽離拆分
5、構(gòu)建工具替換
基于 Webpack 的傳統(tǒng)優(yōu)化方式
上面也說了炕泳,構(gòu)建過程中的大部分時間都消耗在遞歸地去編譯 JavaScript 及 CSS 的各類 Loader 上纵诞,并且會受限于 NodeJS 單線程的特性以及語言本身的效率限制。
如果不替換掉 Webpack 本身培遵,語言本身(NodeJS)的執(zhí)行效率是沒法優(yōu)化的浙芙,只能在其他幾個點做文章。
因此在最早期籽腕,我們所做的都是一些比較常規(guī)的優(yōu)化手段嗡呼,這里簡單介紹最為核心的幾個:
1、緩存
2皇耗、多進程
3南窗、尋址優(yōu)化
緩存優(yōu)化
其實對于 vue-cli 4 而言,已經(jīng)內(nèi)置了一些緩存操作廊宪,譬如上圖可見到 loader 的過程中矾瘾,有使用 cache-loader,所以我們并不需要再次添加到項目之中箭启。
- cache-loader: 在一些性能開銷較大的 loader 之前添加 cache-loader,以便將結(jié)果緩存到磁盤里
那還有沒有一些其他的緩存操作呢用上的呢蛉迹?我們使用了一個 HardSourceWebpackPlugin 傅寡。
HardSourceWebpackPlugin
- HardSourceWebpackPlugin: HardSourceWebpackPlugin 為模塊提供中間緩存,緩存默認存放的路徑是 node_modules/.cache/hard-source北救,配置了 HardSourceWebpackPlugin 之后荐操,首次構(gòu)建時間并沒有太大的變化,但是第二次開始珍策,構(gòu)建時間將會大大的加快托启。
首先安裝依賴:
npm install hard-source-webpack-plugin -D
修改 vue.config.js 配置文件:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
...
configureWebpack: (config) => {
// ...
config.plugins.push(new HardSourceWebpackPlugin());
},
...
}
配置了 HardSourceWebpackPlugin 的首次構(gòu)建時間,和預(yù)期的一樣攘宙,并沒有太大的變化屯耸,但是第二次構(gòu)建從平均 4min 左右降到了平均 20s 左右拐迁,提升的幅度非常的夸張,當然疗绣,這個也因項目而異线召,但是整體而言,在不同項目中實測發(fā)現(xiàn)它都能比較大的提升開發(fā)時二次編譯的效率多矮。
設(shè)置 babel-loader 的 cacheDirectory 以及 DLL
另外缓淹,在緩存方面我們的嘗試有:
1、設(shè)置 babel-loader 的 cacheDirectory
2塔逃、DLL
但是整體收效都不太大讯壶,可以簡單講講。
打開 babel-loader 的 cacheDirectory 的配置湾盗,當有設(shè)置時鹏溯,指定的目錄將用來緩存 loader 的執(zhí)行結(jié)果。之后的 webpack 構(gòu)建淹仑,將會嘗試讀取緩存丙挽,來避免在每次執(zhí)行時,可能產(chǎn)生的匀借、高性能消耗的 Babel 重新編譯過程颜阐。實際的操作步驟,你可以看看 Webpack - babel-loader吓肋。
那么 DLL 又是什么呢凳怨?
DLL 文件為動態(tài)鏈接庫,在一個動態(tài)鏈接庫中可以包含給其他模塊調(diào)用的函數(shù)和數(shù)據(jù)是鬼。
為什么要用 DLL肤舞?
原因在于包含大量復(fù)用模塊的動態(tài)鏈接庫只需要編譯一次,在之后的構(gòu)建過程中被動態(tài)鏈接庫包含的模塊將不會在重新編譯均蜜,而是直接使用動態(tài)鏈接庫中的代碼李剖。
由于動態(tài)鏈接庫中大多數(shù)包含的是常用的第三方模塊,例如 Vue囤耳、React篙顺、React-dom熄守,只要不升級這些模塊的版本踊谋,動態(tài)鏈接庫就不用重新編譯滴劲。
DLL 的配置非常繁瑣劣针,并且最終收效甚微偶妖,我們在過程中借助了 autodll-webpack-plugin窍奋,感興趣的可以自行嘗試主巍。值得一提的是您市,Vue-cli 已經(jīng)剔除了這個功能观挎。
多進程
基于 NodeJS 單線程的特性琴儿,當有多個任務(wù)同時存在段化,它們也只能排隊串行執(zhí)行。
而如今大多數(shù) CPU 都是多核的凤类,因此我們可以借助一些工具穗泵,充分釋放 CPU 在多核并發(fā)方面的優(yōu)勢,利用多核優(yōu)勢谜疤,多進程同時處理任務(wù)佃延。
從上圖中可以看到,Vue CLi4 中夷磕,其實已經(jīng)內(nèi)置了 thread-loader履肃。
- thread-loader: 把 thread-loader 放置在其它 loader 之前,那么放置在這個 loader 之后的 loader 就會在一個單獨的 worker 池中運行坐桩。這樣做的好處是把原本需要串行執(zhí)行的任務(wù)并行執(zhí)行尺棋。
那么,除了 thread-loader绵跷,還有哪些可以考慮的方案呢膘螟?
HappyPack
HappyPack 與 thread-loader 類似。
HappyPack 可利用多進程對文件進行打包, 將任務(wù)分解給多個子進程去并行執(zhí)行碾局,子進程處理完后荆残,再把結(jié)果發(fā)送給主進程,達到并行打包的效果净当、HappyPack 并不是所有的 loader 都支持, 比如 vue-loader 就不支持内斯。
可以通過 Loader Compatibility List 來查看支持的 loaders。需要注意的是像啼,創(chuàng)建子進程和主進程之間的通信是有開銷的俘闯,當你的 loader 很慢的時候,可以加上 happypack忽冻。否則真朗,可能會編譯的更慢。
當然甚颂,由于 HappyPack 作者對 JavaScript 的興趣逐步丟失蜜猾,維護變少,webpack4 及之后都更推薦使用 thread-loader振诬。因此,這里沒有實際結(jié)論給出衍菱。
上一次 HappyPack 更新已經(jīng)是 3 年前
尋址優(yōu)化
對于尋址優(yōu)化赶么,總體而言提升并不是很大。
它的核心即在于脊串,合理設(shè)置 loader 的 exclude 和 include 屬性辫呻。
- 通過配置 loader 的 exclude 選項清钥,告訴對應(yīng)的 loader 可以忽略某個目錄
- 通過配置 loader 的 include 選項,告訴 loader 只需要處理指定的目錄放闺,loader 處理的文件越少祟昭,執(zhí)行速度就會更快
這肯定是有用的優(yōu)化手段,只是對于一些大型項目而言怖侦,這類優(yōu)化對整體構(gòu)建時間的優(yōu)化不會特別明顯篡悟。
分模塊構(gòu)建
在上述的一些常規(guī)優(yōu)化完成后。整體效果仍舊不是特別明顯匾寝,因此搬葬,我們開始思考一些其它方向。
我們再來看看 Webpack 構(gòu)建的整體流程:
上圖是大致的 webpack 構(gòu)建流程艳悔,簡單介紹一下:
1急凰、entry-option:讀取 webpack 配置,調(diào)用 new Compile(config) 函數(shù)準備編譯
2猜年、run:開始編譯
3抡锈、make:從入口開始分析依賴,對依賴模塊進行 build
4乔外、before-resolve:對位置模塊進行解析
5床三、build-module:開始構(gòu)建模塊
6、normal-module-loader:生成 AST 樹
7袁稽、program:遍歷 AST 樹勿璃,遇到 require 語句收集依賴
8、seal:build 完成開始優(yōu)化
9推汽、emit:輸出 dist 目錄
隨著項目體量地不斷增大补疑,耗時大頭消耗在第 7 步,遞歸遍歷 AST歹撒,解析 require莲组,如此反復(fù)直到遍歷完整個項目。
而有意思的是暖夭,對于單次單個開發(fā)而言锹杈,極大概率只是基于這整個大項目的某一小個模塊進行開發(fā)即可。
所以迈着,如果我們可以在收集依賴的時候竭望,跳過我們本次不需要的模塊,或者可以自行選擇裕菠,只構(gòu)建必要的模塊咬清,那么整體的構(gòu)建時間就可以大大減少。
這也就是我們要做的 -- 分模塊構(gòu)建。
什么意思呢旧烧?舉個栗子影钉,假設(shè)我們的項目一共有 6 個大的路由模塊 A、B掘剪、C平委、D、E夺谁、F廉赔,當新需求只需要在 A 模塊范圍內(nèi)進行優(yōu)化新增,那么我們在開發(fā)階段啟動整個項目的時候予权,可以跳過 B昂勉、C、D扫腺、E岗照、F 這 5 個模塊,只構(gòu)建 A 模塊即可:
假設(shè)原本每個模塊的構(gòu)建平均耗時 3s笆环,原本 18s 的整體冷啟動構(gòu)建耗時就能下降到 3s攒至。
分模塊構(gòu)建打包的原理
Webpack 是靜態(tài)編譯打包的,Webpack 在收集依賴時會去分析代碼中的 require(import 會被 bebel 編譯成 require) 語句躁劣,然后遞歸的去收集依賴進行打包構(gòu)建迫吐。
我們要做的,就是通過增加一些配置账忘,簡單改造下我們的現(xiàn)有代碼志膀,使得 Webpack 在初始化遍歷整個路由模塊收集依賴的時候,可以跳過我們不需要的模塊鳖擒。
再說得詳細點溉浙,假設(shè)我們的路由大致代碼如下:
import Vue from 'vue';
import VueRouter, { Route } from 'vue-router';
// 1. 定義路由組件.
// 這里簡化下模型,實際項目中肯定是一個一個的大路由模塊蒋荚,從其他文件導(dǎo)入
const moduleA = { template: '<div>AAAA</div>' }
const moduleB = { template: '<div>BBBB</div>' }
const moduleC = { template: '<div>CCCC</div>' }
const moduleD = { template: '<div>DDDD</div>' }
const moduleE = { template: '<div>EEEE</div>' }
const moduleF = { template: '<div>FFFF</div>' }
// 2. 定義一些路由
// 每個路由都需要映射到一個組件戳稽。
// 我們后面再討論嵌套路由。
const routesConfig = [
{ path: '/A', component: moduleA },
{ path: '/B', component: moduleB },
{ path: '/C', component: moduleC },
{ path: '/D', component: moduleD },
{ path: '/E', component: moduleE },
{ path: '/F', component: moduleF }
]
const router = new VueRouter({
mode: 'history',
routes: routesConfig,
});
// 讓路由生效 ...
const app = Vue.createApp({})
app.use(router)
我們要做的期升,就是每次啟動項目時惊奇,可以通過一個前置命令行腳本,收集本次需要啟動的模塊播赁,按需生成需要的 routesConfig 即可颂郎。
我們嘗試了:
1、IgnorePlugin 插件
2容为、webpack-virtual-modules 配合 require.context
3祖秒、NormalModuleReplacementPlugin 插件進行文件替換
最終選擇了使用 NormalModuleReplacementPlugin 插件進行文件替換的方式诞吱,原因在于它對整個項目的侵入性非常小舟奠,只需要添加前置腳本及修改 Webpack 配置竭缝,無需改變?nèi)魏温酚晌募a≌犹保總結(jié)而言抬纸,該方案的兩點優(yōu)勢在于:
1、無需改動上層代碼
2耿戚、通過生成臨時路由文件的方式湿故,替換原路由文件,對項目無任何影響
使用 NormalModuleReplacementPlugin 生成新的路由配置文件
利用 NormalModuleReplacementPlugin 插件膜蛔,可以不修改原來的路由配置文件坛猪,在編譯階段根據(jù)配置生成一個新的路由配置文件然后去使用它,這樣做的好處在于對整個源碼沒有侵入性皂股。
NormalModuleReplacementPlugin 插件的作用在于墅茉,將目標源文件的內(nèi)容替換為我們自己的內(nèi)容。
我們簡單修改 Webpack 配置呜呐,如果當前是開發(fā)環(huán)境就斤,利用該插件,將原本的 config.ts 文件蘑辑,替換為另外一份洋机,代碼如下:
// vue.config.js
if (process.env.NODE_ENV === 'development') {
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/src\/router\/config.ts/,
'../../dev.routerConfig.ts'
)
)
}
上面的代碼功能是將實際使用的 config.ts 替換為自定義配置的 dev.routerConfig.ts 文件,那么 dev.routerConfig.ts 文件的內(nèi)容又是如何產(chǎn)生的呢洋魂,其實就是借助了 inquirer 與 EJS 模板引擎绷旗,通過一個交互式的命令行問答,選取需要的模塊副砍,基于選擇的內(nèi)容衔肢,動態(tài)的生成新的 dev.routerConfig.ts 代碼,這里直接上代碼址晕。
改造一下我們的啟動腳本膀懈,在執(zhí)行 vue-cli-service serve 前,先跑一段我們的前置腳本:
{
// ...
"scripts": {
- "dev": "vue-cli-service serve",
+ "dev": "node ./script/dev-server.js && vue-cli-service serve",
},
// ...
}
而 dev-server.js 所需要做的事谨垃,就是通過 inquirer 實現(xiàn)一個交互式命令启搂,用戶選擇本次需要啟動的模塊列表,通過 ejs 生成一份新的 dev.routerConfig.ts 文件刘陶。
// dev-server.js
const ejs = require('ejs');
const fs = require('fs');
const child_process = require('child_process');
const inquirer = require('inquirer');
const path = require('path');
const moduleConfig = [
'moduleA',
'moduleB',
'moduleC',
// 實際業(yè)務(wù)中的所有模塊
]
//選中的模塊
const chooseModules = [
'home'
]
function deelRouteName(name) {
const index = name.search(/[A-Z]/g);
const preRoute = '' + path.resolve(__dirname, '../src/router/modules/') + '/';
if (![0, -1].includes(index)) {
return preRoute + (name.slice(0, index) + '-' + name.slice(index)).toLowerCase();
}
return preRoute + name.toLowerCase();;
}
function init() {
let entryDir = process.argv.slice(2);
entryDir = [...new Set(entryDir)];
if (entryDir && entryDir.length > 0) {
for(const item of entryDir){
if(moduleConfig.includes(item)){
chooseModules.push(item);
}
}
console.log('output: ', chooseModules);
runDEV();
} else {
promptModule();
}
}
const getContenTemplate = async () => {
const html = await ejs.renderFile(path.resolve(__dirname, 'router.config.template.ejs'), { chooseModules, deelRouteName }, {async: true});
fs.writeFileSync(path.resolve(__dirname, '../dev.routerConfig.ts'), html);
};
function promptModule() {
inquirer.prompt({
type: 'checkbox',
name: 'modules',
message: '請選擇啟動的模塊, 點擊上下鍵選擇, 按空格鍵確認(可以多選), 回車運行胳赌。注意: 直接敲擊回車會全量編譯, 速度較慢。',
pageSize: 15,
choices: moduleConfig.map((item) => {
return {
name: item,
value: item,
}
})
}).then((answers) => {
if(answers.modules.length===0){
chooseModules.push(...moduleConfig)
}else{
chooseModules.push(...answers.modules)
}
runDEV();
});
}
init();
模板代碼的簡單示意:
// 模板代碼示意匙隔,router.config.template.ejs
import { RouteConfig } from 'vue-router';
<% chooseModules.forEach(function(item){%>
import <%=item %> from '<%=deelRouteName(item) %>';
<% }) %>
let routesConfig: Array<RouteConfig> = [];
/* eslint-disable */
routesConfig = [
<% chooseModules.forEach(function(item){%>
<%=item %>,
<% }) %>
]
export default routesConfig;
dev-server.js 的核心在于啟動一個 inquirer 交互命令行服務(wù)疑苫,讓用戶選擇需要構(gòu)建的模塊,類似于這樣:
模板代碼示意 router.config.template.ejs 是 EJS 模板文件,chooseModules 是我們在終端輸入時捍掺,獲取到的用戶選擇的模塊集合數(shù)組撼短,根據(jù)這個列表,我們?nèi)ド尚碌?routesConfig 文件挺勿。
這樣曲横,我們就實現(xiàn)了分模塊構(gòu)建,按需進行依賴收集不瓶。以我們的項目為例禾嫉,我們的整個項目大概有 20 個不同的模塊,幾十萬行代碼:
構(gòu)建模塊數(shù)
- 冷啟動全量構(gòu)建 20 個模塊 4.5MIN
- 冷啟動只構(gòu)建 1 個模塊 18s
-
有緩存狀態(tài)下二次構(gòu)建 1 個模塊 4.5s
實際效果大致如下蚊丐,無需啟動所有模塊熙参,只啟動我們選中的模塊進行對應(yīng)的開發(fā)即可:
這樣,如果單次開發(fā)只涉及固定的模塊麦备,單次項目冷啟動的時間孽椰,可以從原本的 4min+ 下降到 18s 左右,而有緩存狀態(tài)下二次構(gòu)建 1 個模塊泥兰,僅僅需要 4.5s弄屡,屬于一個比較大的提升。
受限于 Webpack 所使用的語言的性能瓶頸鞋诗,要追求更快的構(gòu)建性能膀捷,我們不可避免的需要把目光放在其他構(gòu)建工具上。這里削彬,我們的目光聚焦在了 Vite 與 esbuild 上全庸。
使用 Vite 優(yōu)化開發(fā)時構(gòu)建
Vite,一個基于瀏覽器原生 ES 模塊的開發(fā)服務(wù)器融痛。利用瀏覽器去解析 imports壶笼,在服務(wù)器端按需編譯返回,完全跳過了打包這個概念雁刷,服務(wù)器隨起隨用覆劈。同時不僅有 Vue 文件支持,還搞定了熱更新沛励,而且熱更新的速度不會隨著模塊增多而變慢责语。
當然,由于 Vite 本身特性的限制目派,目前只適用于在開發(fā)階段替代 Webpack坤候。
我們都知道 Vite 非常快企蹭,它主要快在什么地方白筹?
1智末、項目冷啟動更快
2、熱更新更快
那么是什么讓它這么快徒河?
Webpack 與 Vite 冷啟動的區(qū)別
我們先來看看 Webpack 與 Vite 的在構(gòu)建上的區(qū)別系馆。下圖是 Webpack 的遍歷遞歸收集依賴的過程:
上文我們也講了,Webpack 啟動時虚青,從入口文件出發(fā)它呀,調(diào)用所有配置的 Loader 對模塊進行編譯,再找出該模塊依賴的模塊棒厘,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理。
這一過程是非常非常耗時的下隧,再看看 Vite:
Vite 通過在一開始將應(yīng)用中的模塊區(qū)分為 依賴 和 源碼 兩類奢人,改進了開發(fā)服務(wù)器啟動時間。它快的核心在于兩點:
1淆院、使用 Go 語言的依賴預(yù)構(gòu)建:Vite 將會使用 esbuild 進行預(yù)構(gòu)建依賴何乎。esbuild 使用 Go 編寫,并且比以 JavaScript 編寫的打包器預(yù)構(gòu)建依賴快 10-100 倍土辩。依賴預(yù)構(gòu)建主要做了什么呢支救?
- 開發(fā)階段中,Vite 的開發(fā)服務(wù)器將所有代碼視為原生 ES 模塊拷淘。因此各墨,Vite 必須先將作為 CommonJS 或 UMD 發(fā)布的依賴項轉(zhuǎn)換為 ESM
- Vite 將有許多內(nèi)部模塊的 ESM 依賴關(guān)系轉(zhuǎn)換為單個模塊,以提高后續(xù)頁面加載性能启涯。如果不編譯贬堵,每個依賴包里面都可能含有多個其他的依賴,每個引入的依賴都會又一個請求结洼,請求多了耗時就多
2黎做、 按需編譯返回:Vite 以 原生 ESM 方式提供源碼。這實際上是讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進行轉(zhuǎn)換并按需提供源碼松忍。根據(jù)情景動態(tài)導(dǎo)入代碼蒸殿,即只在當前屏幕上實際使用時才會被處理。
Webpack 與 Vite 熱更新的區(qū)別
使用 Vite 的另外一個大的好處在于鸣峭,它的熱更新也是非常迅速的宏所。
我們首先來看看 Webpack 的熱更新機制:
一些名詞解釋:
- Webpack-complier:Webpack 的編譯器,將 Javascript 編譯成 bundle(就是最終的輸出文件)
- HMR Server:將熱更新的文件輸出給 HMR Runtime
- Bunble Server:提供文件在瀏覽器訪問叽掘,也就是我們平時能夠正常通過 localhost 訪問我們本地網(wǎng)站的原因
- HMR Runtime:開啟了熱更新的話楣铁,在打包階段會被注入到瀏覽器中的 bundle.js,這樣 bundle.js 就可以跟服務(wù)器建立連接更扁,通常是使用 Websocket 盖腕,當收到服務(wù)器的更新指令的時候赫冬,就去更新文件的變化
- bundle.js:構(gòu)建輸出的文件
Webpack 熱更新的大致原理是,文件經(jīng)過 Webpack-complier 編譯好后傳輸給 HMR Server溃列,HMR Server 知道哪個資源 (模塊) 發(fā)生了改變劲厌,并通知 HMR Runtime 有哪些變化,HMR Runtime 就會更新我們的代碼听隐,這樣瀏覽器就會更新并且不需要刷新补鼻。
而 Webpack 熱更新機制主要耗時點在于,Webpack 的熱更新會以當前修改的文件為入口重新 build 打包雅任,所有涉及到的依賴也都會被重新加載一次风范。
而 Vite 號稱 熱更新的速度不會隨著模塊增多而變慢。它的主要優(yōu)化點在哪呢沪么?
Vite 實現(xiàn)熱更新的方式與 Webpack 大同小異硼婿,也通過創(chuàng)建 WebSocket 建立瀏覽器與服務(wù)器建立通信,通過監(jiān)聽文件的改變向客戶端發(fā)出消息禽车,客戶端對應(yīng)不同的文件進行不同的操作的更新寇漫。
Vite 通過 chokidar 來監(jiān)聽文件系統(tǒng)的變更,只用對發(fā)生變更的模塊重新加載殉摔,只需要精確的使相關(guān)模塊與其臨近的 HMR 邊界連接失效即可州胳,這樣 HMR 更新速度就不會因為應(yīng)用體積的增加而變慢而 Webpack 還要經(jīng)歷一次打包構(gòu)建。所以 HMR 場景下逸月,Vite 表現(xiàn)也要好于 Webpack栓撞。
通過不同的消息觸發(fā)一些事件。做到瀏覽器端的即時熱模塊更換(熱更新)彻采。通過不同事件腐缤,觸發(fā)更細粒度的更新(目前只有 Vue 和 JS,Vue 文件又包含了 template肛响、script岭粤、style 的改動),做到只更新必須的文件特笋,而不是全量進行更新剃浇。這些事件分別是:
- connected: WebSocket 連接成功
- vue-reload: Vue 組件重新加載(當修改了 script 里的內(nèi)容時)
- vue-rerender: Vue 組件重新渲染(當修改了 template 里的內(nèi)容時)
- style-update: 樣式更新
- style-remove: 樣式移除
- js-update: js 文件更新
- full-reload: fallback 機制,網(wǎng)頁重刷新
本文不會在 Vite 原理上做太多深入猎物,感興趣的可以通過官方文檔了解更多 -- Vite 官方文檔 -- 為什么選 Vite
基于 Vite 的改造虎囚,相當于在開發(fā)階段替換掉 Webpack,下文主要講講我們在替換過程中遇到的一些問題蔫磨。
基于 Vue-cli 4 的 Vue2 項目改造淘讥,大致只需要:
1、安裝 Vite
2堤如、配置 index.html(Vite 解析 <script type="module" src="..."> 標簽指向源碼)
3蒲列、配置 vite.config.js
4窒朋、package.json 的 scripts 模塊下增加啟動命令 "vite": "vite"
當以命令行方式運行 npm run vite時,Vite 會自動解析項目根目錄下名為 vite.config.js 的文件蝗岖,讀取相應(yīng)配置侥猩。而對于 vite.config.js 的配置,整體而言比較簡單:
1抵赢、Vite 提供了對 .scss, .sass, .less, 和 .stylus 文件的內(nèi)置支持
2欺劳、天然的對 TS 的支持,開箱即用
3铅鲤、基于 Vue2 的項目支持划提,可能不同的項目會遇到不同的問題,根據(jù)報錯逐步調(diào)試即可彩匕,譬如通過一些官方插件兼容 .tsx腔剂、.jsx
當然,對于項目的源碼驼仪,可能需要一定的改造,下面是我們遇到的一些小問題:
1袜漩、tsx 中使用裝飾器導(dǎo)致的編譯問題绪爸,我們通過魔改了 @vitejs/plugin-vue-jsx,使其支持 Vue2 下的 jsx
2宙攻、由于 Vite 僅支持 ESM 語法奠货,需要將代碼中的模塊引入方式由 require 改為 import
3、Sass 預(yù)處理器無法正確解析樣式中的 /deep/座掘,可使用 ::v-deep 替換
4递惋、其他一些小問題,譬如 Webpack 環(huán)境變量的兼容溢陪,SVG iCON 的兼容
對于需要修改到源碼的地方萍虽,我們的做法是既保證能讓 Vite 進行適配,同時讓該改動不會影響到原本 Webpack 的構(gòu)建形真,以便在關(guān)鍵時刻或者后續(xù)迭代能切回 Webpack