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 之後就直接創建