淺析Vite本地構建原理

前端南玖發表於2024-06-20

前言

隨著Vue3的逐漸普及以及Vite的逐漸成熟,我們有必要來了解一下關於vite的本地構建原理。

對於webpack打包的核心流程是透過分析JS檔案中引用關係,透過遞迴得到整個專案的依賴關係,並且對於非JS型別的資源,透過呼叫對應的loader將其打包編譯生成JS 程式碼,最後再啟動開發伺服器。

瞭解到webpack的耗時主要花費在打包上,Vite選擇跳過打包,直接以 原生 ESM 方式提供原始碼,這樣豈不是可以非常快!

與webpack對比

Vite官網有兩張對比圖能夠非常直觀的對比兩者的區別。

這張圖代表的是基於打包器的構建方式(webpack就是其中之一),它在啟動服務之前,需要從入口開始掃描整個專案的依賴關係,然後基於依賴關係構建整個應用生成bundle,最後才會啟動開發伺服器。 這就是這類構建方式為什麼慢的原因,並且整個構建時間會隨著專案的變大變的越來越長!

這張圖代表的是基於ES Module的構建方式(比如:Vite),這張圖是不是能夠很直觀說明為什麼Vite會非常快,因為它上來就直接啟動開發伺服器,然後在瀏覽器請求原始碼時進行轉換並按需提供原始碼。根據情景動態匯入程式碼,即只在當前頁面上實際使用時才會被處理。

也就是它不需要掃描整個專案並且打包,不打包的話那它是如何讓瀏覽器拿到分散在專案中的各個模組呢?

這一切都要得益於瀏覽器支援ESM的模組化方案,當瀏覽器識別到模組內的 ESM 方式匯入的模組時,會自動去幫我們查詢對應的內容

這就是為什麼vite專案的模版檔案中的script標籤需要加上type=module,而webpack專案不需要。

<script type="module" src="/src/main.ts"></script>

vite快的原因

其實上面已經能夠說明vite為什麼會比webpack快了,但還有另外一個點在上圖中並沒有表現出來。

Vite會在一開始將專案中的所有模組分為原始碼依賴兩類

  • 原始碼指的是我們自己寫的程式碼,這類程式碼可能需要轉換(例如 JSX,CSS 或者 Vue/Svelte 元件),並且時常會被編輯。Vite 會以 原生 ESM 方式提供原始碼,同時並不是所有的原始碼都需要同時被載入(例如基於路由拆分的程式碼模組)。
  • 依賴大多為在開發時不會變動的純 JavaScript。一些較大的依賴(例如有上百個模組的元件庫)處理的代價也很高。依賴也通常會存在多種模組化格式(例如 ESM 或者 CommonJS)。Vite 將會使用 esbuild預構建依賴。esbuild 使用 Go 編寫,並且比以 JavaScript 編寫的打包器預構建依賴快 10-100 倍。

總結來說就是:基於ESM模組化方案 + 預構建

使用預構建的原因

Vite使用依賴預構建的原因主要有以下兩點:

  • 相容CommonJS與UMD:在開發階段中,Vite 的開發伺服器將所有程式碼視為原生 ES 模組。因此,Vite 必須先將以 CommonJS 或 UMD 形式提供的依賴項轉換為 ES 模組。
  • 效能:為了提高後續頁面的載入效能,Vite將那些具有許多內部模組的 ESM 依賴項轉換為單個模組。

可以來看個例子:

我們引入lodash-es工具包中的debounce方法,此時它理想狀態應該是隻發出一個請求

import  { debounce }  from 'lodash-es'

事實也是這樣

但這是預構建的功勞,如果我們對lodash-es關閉預構建呢?

vite配置檔案加上如下程式碼,再來試試:

// vite.config.js
optimizeDeps: {
    exclude: ['lodash-es']
  }

可以看到,此時發起了600多個請求,這是因為lodash-es 有超過 600 個內建模組!

vite透過將 lodash-es 預構建成單個模組,只需要發起一個HTTP請求!可以很大程度地提高載入效能

基本原理

跟著debug來一步一步看vite本地是如何工作的

首先從package.json出發,找到專案啟動命令:

可以看到,dev對應的命令直接就是vite,然後我們再找到node_modules下面的vite下面的bin資料夾下面的vite.js檔案,這就是vite執行的入口檔案。

這裡有一個start方法,從這打上斷點開始慢慢往下走,就能夠知道整個執行的基本原理

從上面我們知道,vite首先是會啟動一個本地服務,基於該服務對檔案的請求進行處理返回

接著往下走,我們可以看到有一個處理url的方法,此時執行棧裡面的address變數也能夠看到是127.0.0.1:5173,這就是我們等會要訪問的本地服務,當然現在瀏覽器還什麼也看不到,因為還沒開始處理/路由,該路由需要返回一個html檔案,也就是我們的模版檔案(專案基於Vue3)

繼續往下走,就可以看到有一個applyHtmlTransforms方法用來處理html檔案並返回,可以看到當前請求的原始路徑是/,返回的檔案是專案根目錄下的index.html檔案

裡面有一個指令碼檔案<script type="module" src="/src/main.ts"></script>,接下來就該請求並處理入口檔案main.ts

main.ts檔案如下:

import { createApp } from 'vue'
// import './style.css'
import  { debounce }  from 'lodash-es'

console.log('--lodash--', debounce)
import App from './App.vue'

createApp(App).mount('#app')

經過處理之後變成了:

它其實也沒做啥處理,只是把依賴的引用路徑處理成了預構建下的路徑(.vite/deps/),把原始碼的引用路徑處理成了絕對路徑。

🤔這裡是不是會好奇,瀏覽器不是不能識別處理vue檔案嗎,這個不需要處理嗎?(接著往下看!)

來看看此時瀏覽器中的載入順序是怎樣的吧:

整個檔案的載入順序是不是都對上了,注意看這個App.vue檔案,雖然是.vue結尾,但檔案型別依然是一個JavaScript檔案

App.vue經過編譯後檔案型別已經轉成JS了!

App.vue檔案內容如下:

<script setup lang="ts">
import { ref } from 'vue'
const userName = ref('前端南玖')
</script>

<template>
  <div class="user_name">{{ userName }}</div>
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

編譯後:

再接著往下走,看下style被編譯成了什麼內容:

最後整個頁面就可以渲染出來了!

總結

vite整體思路:啟動一個 connect 伺服器攔截由瀏覽器請求 ESM的請求。透過請求的路徑找到目錄下對應的檔案做一下編譯最終以 ESM的格式返回給瀏覽器。

對於node_modules下面的依賴,vite會使用esbuild進行預構建,主要是為了相容CommonJS與UMD,以及提高效能。

這樣完整走一遍,是不是對Vite的理解又更深一步了,它實際上就是“走一步看一步”,不像webpack上來就掃描整個專案進行打包編譯,所以vite的構建速度會比較快!

瞭解完vite工作原理,我們是不是可以來實現一個簡易的vite工具!