Vue 檔案是如何被轉換並渲染到頁面的?
ead>以前常常覺得,Vue 檔案(單檔案元件,Single File Component,SFC)的處理非常複雜,以至於很久一段時間,都不敢接觸它,直到我看了 @vite/plugin-vue
的原始碼,才發現,這個過程並沒有多複雜。因為 Vue 已經提供了 SFC 的編譯能力,我們只需要站在巨人的肩膀上,簡單地組合利用這些能力即可。
本文會用一個極其簡單的例子,來說明如何處理一個 Vue 檔案,並將其展示到頁面中。在這個過程中,介紹 Vue 提供的編譯能力,以及如何組合利用這些能力。
學完之後,你會明白 Vue 檔案是如何一步一步被轉換成 js 程式碼的,也能理解 vite
、rollup
這些打包工具,是如何對 Vue 檔案進行打包的。
本文用到的專案,在該 Github 倉庫中,喜歡自己動手的同學,可以下載下來玩玩
一個簡單的例子
有一個 main.vue 檔案如下:
```html
```
接下來,我會一步一步帶大家手動處理這個 Vue 檔案,並將其展示到頁面中。
我們首先來了解一下,如果不使用 Vue 檔案,不進行編譯,要如何使用 Vue
在瀏覽器直接使用 Vue
這是 Vue 官方文件提供的一個例子
```html
```
利用 script
標籤全域性載入 Vue,通過全域性變數 window.Vue
來獲取 Vue 模組。然後定義元件,建立 Vue 例項,並掛載到對應的 DOM。
頁面效果如下:
上面的例子,是使用 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
的解析結果
其實 parse
函式,就是把一個 Vue 檔案,分成 3 個部分:
template
塊script
塊和scriptSetup
塊- 多個
style
塊
這一步做的是解析,其實並沒有對程式碼進行編譯,可以看到,每個塊的 content
欄位,都是跟 Vue 檔案是相同的。
值得注意的是,script
包括 script
塊和 scriptSetup
塊,scriptSetup
塊在圖中沒有標註,是因為剛好我們的 Vue 檔案,沒有使用 script setup
的特性,因此它的值為空。
style
塊允許有多個,因為可以同時出現多個 style
標籤,而其他標籤只能有一個(script
和 script setup
能同時存在各一個)。
解析的目的,是將一個 Vue 檔案中的內容,拆分成不同的塊,然後分別對它們進行編譯
編譯 script
編譯 script
的目的有如下幾個:
- 處理
script setup
的程式碼,script setup
的程式碼是不能直接執行的,需要進行轉換。 - 合併
script
和script 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
返回結果如下:
```typescript import { ref } from "vue";
export default { name: "Main", setup() { const message = ref("Main"); return { message, }; }, }; ```
可以看出編譯後的 script
沒有變化,因為這裡的確不需要任何處理。
如果有 script setup
或者 css
變數注入,編譯後的程式碼就會有變化,感興趣的可以看看 main-with-css-inject.vue
或 main-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
函式返回值如下:
編譯後的 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
渲染函式來看,你就覺得非常熟悉了。
現在有了 script
和 render
函式,其實已經是可以把一個元件顯示到頁面上了,樣式可以先不管,我們先把元件渲染出來,然後再加上樣式
組合 script 和 render 函式
目前 script
和 render
函式,它們都是各自一個模組,而我們需要的是一個完整的 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 之後就直接建立