日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
前端構(gòu)建效率優(yōu)化之路

項目背景

我們的系統(tǒng)(一個 ToB 的 Web 單頁應(yīng)用)經(jīng)過多年的迭代,目前已經(jīng)累積有大幾十萬行的業(yè)務(wù)代碼,30+ 路由模塊,整體的代碼量和復(fù)雜度還是比較高的。

成都創(chuàng)新互聯(lián)公司自2013年創(chuàng)立以來,先為雨花等服務(wù)建站,雨花等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為雨花企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。

項目整體是基于 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ā)布、回滾等一系列操作的準(zhǔn)確性、及時性

本文,就將詳細(xì)介紹整個我們項目,在隨著項目體量不斷增大的過程中,對整體的打包構(gòu)建效率的優(yōu)化之路。

瓶頸分析

再更具體一點,我們的項目最初是基于 vue-cli 4,當(dāng)時其基于的是 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ā)階段而言,在沒有任何緩存的前提下,單次冷啟動整個項目的時間達(dá)到了驚人的 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ā)階段,我們的核心目標(biāo)是在保有項目所有功能的前提下,盡可能提高構(gòu)建速度,保證開發(fā)時的效率,所以對于 Live 才需要的一些功能,譬如代碼混淆壓縮、圖片壓縮等功能是可以不開啟的,并且在開發(fā)階段,我們需要熱更新。

  1. ?基于生產(chǎn)階段 npm run build 的優(yōu)化。

而在生產(chǎn)打包階段,盡管構(gòu)建速度也非常重要,但是一些在開發(fā)時可有可無的功能必須加上,譬如代碼壓縮、圖片壓縮。因此,生產(chǎn)構(gòu)建的目標(biāo)是在于保證最終項目打包體積盡可能小,所需要的相關(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 上。

本文不會詳細(xì)描述 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. 多進(jìn)程
  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. 多進(jìn)程
  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 為模塊提供中間緩存,緩存默認(rèn)存放的路徑是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 左右,提升的幅度非常的夸張,當(dāng)然,這個也因項目而異,但是整體而言,在不同項目中實測發(fā)現(xiàn)它都能比較大的提升開發(fā)時二次編譯的效率。

設(shè)置 babel-loader 的 cacheDirectory 以及 DLL

另外,在緩存方面我們的嘗試有:

  1. 設(shè)置 babel-loader 的 cacheDirectory
  2. DLL

但是整體收效都不太大,可以簡單講講。

打開 babel-loader 的 cacheDirectory 的配置,當(dāng)有設(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)剔除了這個功能。

多進(jìn)程

基于 NodeJS 單線程的特性,當(dāng)有多個任務(wù)同時存在,它們也只能排隊串行執(zhí)行。

而如今大多數(shù) CPU 都是多核的,因此我們可以借助一些工具,充分釋放 CPU 在多核并發(fā)方面的優(yōu)勢,利用多核優(yōu)勢,多進(jìn)程同時處理任務(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 可利用多進(jìn)程對文件進(jìn)行打包, 將任務(wù)分解給多個子進(jìn)程去并行執(zhí)行,子進(jìn)程處理完后,再把結(jié)果發(fā)送給主進(jìn)程,達(dá)到并行打包的效、HappyPack 并是所有的 loader 都支持, 比如 vue-loader 就不支持。

可以通過 Loader Compatibility List 來查看支持的 loaders。需要注意的是,創(chuàng)建子進(jìn)程和主進(jìn)程之間的通信是有開銷的,當(dāng)你的 loader 很慢的時候,可以加上 happypack。否則,可能會編譯的更慢。

當(dāng)然,由于 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ù)準(zhǔn)備編譯。
  2. run:開始編譯。
  3. make:從入口開始分析依賴,對依賴模塊進(jìn)行 build。
  4. before-resolve:對位置模塊進(jìn)行解析。
  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ā)而言,極大概率只是基于這整個大項目的某一小個模塊進(jìn)行開發(fā)即可。

所以,如果我們可以在收集依賴的時候,跳過我們本次不需要的模塊,或者可以自行選擇,只構(gòu)建必要的模塊,那么整體的構(gòu)建時間就可以大大減少。

這也就是我們要做的 -- 分模塊構(gòu)建。

什么意思呢?舉個栗子,假設(shè)我們的項目一共有 6 個大的路由模塊 A、B、C、D、E、F,當(dāng)新需求只需要在 A 模塊范圍內(nèi)進(jìn)行優(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) 語句,然后遞歸的去收集依賴進(jìn)行打包構(gòu)建。

我們要做的,就是通過增加一些配置,簡單改造下我們的現(xiàn)有代碼,使得 Webpack 在初始化遍歷整個路由模塊收集依賴的時候,可以跳過我們不需要的模塊。

再說得詳細(xì)點,假設(shè)我們的路由大致代碼如下:

import Vue from 'vue';
import VueRouter, { Route } from 'vue-router';
// 1. 定義路由組件.
// 這里簡化下模型,實際項目中肯定是一個一個的大路由模塊,從其他文件導(dǎo)入
const moduleA = { template: '
AAAA
' }
const moduleB = { template: '
BBBB
' }
const moduleC = { template: '
CCCC
' }
const moduleD = { template: '
DDDD
' }
const moduleE = { template: '
EEEE
' }
const moduleF = { template: '
FFFF
' }
// 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 插件進(jìn)行文件替換。

最終選擇了使用 NormalModuleReplacementPlugin 插件進(jìn)行文件替換的方式,原因在于它對整個項目的侵入性非常小,只需要添加前置腳本及修改 Webpack 配置,無需改變?nèi)魏温酚晌募a??偨Y(jié)而言,該方案的兩點優(yōu)勢在于:

  1. 無需改動上層代碼
  2. 通過生成臨時路由文件的方式,替換原路由文件,對項目無任何影響

使用 NormalModuleReplacementPlugin 生成新的路由配置文件

利用 NormalModuleReplacementPlugin 插件,可以不修改原來的路由配置文件,在編譯階段根據(jù)配置生成一個新的路由配置文件然后去使用它,這樣做的好處在于對整個源碼沒有侵入性。

NormalModuleReplacementPlugin 插件的作用在于,將目標(biāo)源文件的內(nèi)容替換為我們自己的內(nèi)容。

我們簡單修改 Webpack 配置,如果當(dāng)前是開發(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: '請選擇啟動的模塊, 點擊上下鍵選擇, 按空格鍵確認(rèn)(可以多選), 回車運行。注意: 直接敲擊回車會全量編譯, 速度較慢。',
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 = [];
/* 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)ド尚碌?nbsp;routesConfig 文件。

這樣,我們就實現(xiàn)了分模塊構(gòu)建,按需進(jìn)行依賴收集。以我們的項目為例,我們的整個項目大概有 20 個不同的模塊,幾十萬行代碼:

構(gòu)建模塊數(shù)

耗時

冷啟動全量構(gòu)建 20 個模塊

4.5MIN

冷啟動只構(gòu)建 1 個模塊

18s

有緩存狀態(tài)下二次構(gòu)建 1 個模塊

4.5s

實際效果大致如下,無需啟動所有模塊,只啟動我們選中的模塊進(jìn)行對應(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 文件支持,還搞定了熱更新,而且熱更新的速度不會隨著模塊增多而變慢。

當(dāng)然,由于 Vite 本身特性的限制,目前只適用于在開發(fā)階段替代 Webpack。

我們都知道 Vite 非???,它主要快在什么地方?

  1. 項目冷啟動更快。
  2. 熱更新更快。

那么是什么讓它這么快?

Webpack 與 Vite 冷啟動的區(qū)別

我們先來看看 Webpack 與 Vite 的在構(gòu)建上的區(qū)別。下圖是 Webpack 的遍歷遞歸收集依賴的過程:

上文我們也講了,Webpack 啟動時,從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進(jìn)行編譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理。

這一過程是非常非常耗時的,再看看 Vite:

Vite 通過在一開始將應(yīng)用中的模塊區(qū)分為 依賴 和 源碼 兩類,改進(jìn)了開發(fā)服務(wù)器啟動時間。它快的核心在于兩點:

  1. 使用 Go 語言的依賴預(yù)構(gòu)建:Vite 將會使用 esbuild 進(jìn)行預(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ù)頁面加載性能。如果不編譯,每個依賴包里面都可能含有多個其他的依賴,每個引入的依賴都會又一個請求,請求多了耗時就多。
  1. 按需編譯返回:Vite 以 原生 ESM 方式提供源碼。這實際上是讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進(jìn)行轉(zhuǎn)換并按需提供源碼。根據(jù)情景動態(tài)導(dǎo)入代碼,即只在當(dāng)前屏幕上實際使用時才會被處理。

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 ,當(dāng)收到服務(wù)器的更新指令的時候,就去更新文件的變化。
  • bundle.js:構(gòu)建輸出的文件。

Webpack 熱更新的大致原理是,文件經(jīng)過 Webpack-complier 編譯好后傳輸給 HMR Server,HMR Server 知道哪個資源 (模塊) 發(fā)生了改變,并通知 HMR Runtime 有哪些變化,HMR Runtime 就會更新我們的代碼,這樣瀏覽器就會更新并且不需要刷新。

而 Webpack 熱更新機制主要耗時點在于,Webpack 的熱更新會以當(dāng)前修改的文件為入口重新 build 打包,所有涉及到的依賴也都會被重新加載一次。

而  Vite 號稱 熱更新的速度不會隨著模塊增多而變慢。它的主要優(yōu)化點在哪呢?

Vite 實現(xiàn)熱更新的方式與 Webpack 大同小異,也通過創(chuàng)建 WebSocket 建立瀏覽器與服務(wù)器建立通信,通過監(jiān)聽文件的改變向客戶端發(fā)出消息,客戶端對應(yīng)不同的文件進(jìn)行不同的操作的更新。

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ā)更細(xì)粒度的更新(目前只有 Vue 和 JS,Vue 文件又包含了 template、script、style 的改動),做到只更新必須的文件,而不是全量進(jìn)行更新。在些事件分別是:

  • connected: WebSocket 連接成功。
  • vue-reload: Vue 組件重新加載(當(dāng)修改了 script 里的內(nèi)容時)。
  • vue-rerender: Vue 組件重新渲染(當(dāng)修改了 template 里的內(nèi)容時)。
  • style-update: 樣式更新。
  • style-remove: 樣式移除。
  • js-update: js 文件更新。
  • full-reload: fallback 機制,網(wǎng)頁重刷新。

本文不會在 Vite 原理上做太多深入,感興趣的可以通過官方文檔了解更多 -- Vite 官方文檔 -- 為什么選 Vite。

基于 Vite 的改造,相當(dāng)于在開發(fā)階段替換掉 Webpack,下文主要講講我們在替換過程中遇到的一些問題。

基于 Vue-cli 4 的 Vue2 項目改造,大致只需要:

  1. 安裝 Vite。
  2. 配置 index.html(Vite 解析