Stories

Detail Return Return

淺析Vite本地構建原理 - Stories Detail

前言

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

對於webpack打包的核心流程是通過分析JS文件中引用關係,通過遞歸得到整個項目的依賴關係,並且對於非JS類型的資源,通過調用對應的loader將其打包編譯生成JS 代碼,最後再啓動開發服務器。

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

與webpack對比

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

1.bundle.png

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

2.esm.png

這張圖代表的是基於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'

事實也是這樣

3.png

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

vite配置文件加上如下代碼,再來試試:

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

4.png

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

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

基本原理

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

首先從package.json出發,找到項目啓動命令:

5.png

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

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

6.png

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

7.png

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

8.png

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

9.png

裏面有一個腳本文件<script type="module" src="/src/main.ts"></script>,接下來就該請求並處理入口文件main.ts

10.png

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

經過處理之後變成了:

11.png

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

🤔這裏是不是會好奇,瀏覽器不是不能識別處理vue文件嗎,這個不需要處理嗎?(接着往下看!)

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

12.png

整個文件的加載順序是不是都對上了,注意看這個App.vue文件,雖然是.vue結尾,但文件類型依然是一個JavaScript文件

13.png

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>

編譯後:

14.png

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

15.png

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

16.png

總結

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

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

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

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

user avatar grewer Avatar beverly0 Avatar yuan_life Avatar fangtangxiansheng Avatar
Favorites 4 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.