Vue 檔案是如何被轉換並渲染到頁面的?

語言: CN / TW / HK
ead>

以前常常覺得,Vue 檔案(單檔案元件,Single File Component,SFC)的處理非常複雜,以至於很久一段時間,都不敢接觸它,直到我看了 @vite/plugin-vue 的原始碼,才發現,這個過程並沒有多複雜。因為 Vue 已經提供了 SFC 的編譯能力,我們只需要站在巨人的肩膀上,簡單地組合利用這些能力即可。

本文會用一個極其簡單的例子,來說明如何處理一個 Vue 檔案,並將其展示到頁面中。在這個過程中,介紹 Vue 提供的編譯能力,以及如何組合利用這些能力。

學完之後,你會明白 Vue 檔案是如何一步一步被轉換成 js 程式碼的,也能理解 viterollup 這些打包工具,是如何對 Vue 檔案進行打包的。

本文用到的專案,在該 Github 倉庫中,喜歡自己動手的同學,可以下載下來玩玩

一個簡單的例子

有一個 main.vue 檔案如下:

```html

```

接下來,我會一步一步帶大家手動處理這個 Vue 檔案,並將其展示到頁面中。

我們首先來了解一下,如果不使用 Vue 檔案,不進行編譯,要如何使用 Vue

在瀏覽器直接使用 Vue

這是 Vue 官方文件提供的一個例子

```html

Title

```

利用 script 標籤全域性載入 Vue,通過全域性變數 window.Vue 來獲取 Vue 模組。然後定義元件,建立 Vue 例項,並掛載到對應的 DOM。

頁面效果如下:

image-20220628203903756

上面的例子,是使用 js 來定義元件的。

那麼如果我們用 Vue SFC 來定義元件,就需要將 Vue 檔案,編譯成 js 物件形式的 Vue 元件物件(像上述例子一樣)

Vue 檔案主要由 3 部分組成:

  • script 指令碼
  • template 模板,可選
  • style 樣式,可選

要分別將這三部分,轉換成 js 並組合成一個 Vue 物件,瀏覽器才能正確的執行

如何編譯 Vue SFC?

Vue 提供了 @vue/compiler-sfc專門用於 Vue 檔案的預編譯。下面我會一步一步演示 @vue/compiler-sfc 的使用方法。

解析 Vue 檔案

在進行處理之前,首先要讀取到程式碼的字串

typescript import { readFile, writeFile } from "fs-extra"; const file = await readFile("./src/main.vue", "utf8");

然後用 @vue/compiler-sfc 提供的解析器,對程式碼進行解析

typescript import { parse } from "@vue/compiler-sfc"; const { descriptor, error } = parse(file);

這個是 Vue 檔案的內容

```html

```

下圖是 descriptor 的解析結果

image-20220628204228461

其實 parse 函式,就是把一個 Vue 檔案,分成 3 個部分:

  • template
  • script 塊和 scriptSetup
  • 多個style

這一步做的是解析,其實並沒有對程式碼進行編譯,可以看到,每個塊的 content 欄位,都是跟 Vue 檔案是相同的。

值得注意的是,script 包括 script 塊和 scriptSetup 塊,scriptSetup 塊在圖中沒有標註,是因為剛好我們的 Vue 檔案,沒有使用 script setup 的特性,因此它的值為空。

style 塊允許有多個,因為可以同時出現多個 style 標籤,而其他標籤只能有一個(scriptscript setup 能同時存在各一個)。

解析的目的,是將一個 Vue 檔案中的內容,拆分成不同的塊,然後分別對它們進行編譯

編譯 script

編譯 script 的目的有如下幾個:

  • 處理 script setup 的程式碼, script setup 的程式碼是不能直接執行的,需要進行轉換。
  • 合併 scriptscript setup 的程式碼。
  • 處理 CSS 變數注入

```typescript import { compileScript } from "@vue/compiler-sfc";

// 這個 id 是 scopeId,用於 css scope,保證唯一即可 const id = Date.now().toString(); const scopeId = data-v-${id};

// 編譯 script,因為可能有 script setup,還要進行 css 變數注入 const script = compileScript(descriptor, { id: scopeId }); ```

compileScript 返回結果如下:

image-20220627222759869

```typescript import { ref } from "vue";

export default { name: "Main", setup() { const message = ref("Main"); return { message, }; }, }; ```

可以看出編譯後的 script沒有變化,因為這裡的確不需要任何處理

如果有 script setup 或者 css 變數注入,編譯後的程式碼就會有變化,感興趣的可以看看 main-with-css-inject.vuemain-with-script-setup.vue 這兩個檔案的編譯結果。

編譯 template

編譯 template,目的是template 轉成 render 函式

```typescript import { compileTemplate } from "@vue/compiler-sfc";

// 編譯模板,轉換成 render 函式 const template = compileTemplate({ source: descriptor.template.content, filename: "main.vue", // 用於錯誤提示 id: scopeId, }); ```

compileTemplate 函式返回值如下:

image-20220627230039224

編譯後的 render 函式如下:

```javascript import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "message" }

export function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", _hoisted_1, _toDisplayString(_ctx.message), 1 / TEXT /)) } ```

這段程式碼,看起來好像一個函式都不認識。但其實,你只要把 _createElementBlock 當成 Vue.h 渲染函式來看,你就覺得非常熟悉了。

現在有了 scriptrender 函式,其實已經是可以把一個元件顯示到頁面上了,樣式可以先不管,我們先把元件渲染出來,然後再加上樣式

組合 script 和 render 函式

目前 scriptrender 函式,它們都是各自一個模組,而我們需要的是一個完整的 Vue 物件,即 render 函式需要作為 Vue 物件的一個屬性

可以採用以下這種方案:

```javascript // 將 script 儲存到 main.vue.script.js,拿到的是 Vue 物件 import script from '/src/main.vue.script.js'

// 將 render 函式儲存到 main.vue.template.js,拿到的是 render 函式 import { render } from '/src/main.vue.template.js'

// 將 style 函式儲存到 main.vue.style.js,import 之後就直接建立