用 <spec lang="md"> 標籤在 Vue 中舒服的使用 SFC 搭配 AI 開發

前言

許多開發者都會搭配著 AI 工具(Claude Code、Gemini、Copilot 等)來協助撰寫程式碼,而在 Vue 的單檔元件(Single File Component, SFC)中,常常會遇到一些困擾,所以這邊就來分享一個不同的做法,也就是自訂一個 <spec lang="md"> 標籤,來讓我們能夠在 SFC 中更舒服地使用 AI 工具來開發。

AI 開發模式

現在使用 AI 開發的模式基本上有這幾種:

  • SDD(Specification Driven Development,規格驅動開發):先撰寫規格文件,然後讓 AI 根據規格文件來產生程式碼。
  • BDD(Behavior Driven Development,行為驅動開發):先撰寫行為描述,然後讓 AI 根據行為描述來產生程式碼。
  • TDD(Test Driven Development,測試驅動開發):先撰寫測試案例,然後讓 AI 根據測試案例來產生程式碼。

其中 SDD 是最多人使用的模式,只要可以把規格描述清楚,那麼 AI 就能夠幫助我們產生出符合需求的程式碼,但這邊我們深入探討怎麼使用跟寫 SDD。

但問題來了,什麼叫做 「把規格描述清楚」 呢?

我認為這件事情很有趣,我們光描述自己的問題就有一定的難度,更不用說 AI 只會依照我們講的內容、資訊去產出程式碼了,更不用說沒有一個標準可以去衡量我們的描述是否足夠清楚、應該要包含哪些內容。

通常我們再請 AI 開始撰寫程式碼之前,都會請它先初步的專案分析,並產生一個專案規格文件,下一次我們開啟專案請 AI 撰寫程式碼時,就可以直接把這份規格文件貼給 AI,讓它根據這份文件來撰寫程式碼。

所以就有可能會變成有這些文件:

  • *.spec.md:專案規格文件
  • *.test.md:測試案例文件
  • *.guide.md:開發指南文件
  • *.task.md:任務清單文件
  • README.md:專案說明文件

等等之類的文件,其中可能還會把程式碼拆出來說明。

可想而知,開發文件會越來越多,而且我們還要不斷的更新這些文件,為了讓它們能夠跟程式碼/需求保持一致。

雖然你可能會認為「反正這些文件都是給 AI 用的,沒關係啦」,但事實上這些文件也會影響到我們開發的效率,因為我們也需要花時間去維護這些文件,你不可能不看這些文件就直接請 AI 幫你寫程式碼。

更不用說,AI 的 Context Window(上下文視窗)是有限的。

在 Vue SFC 中使用 <spec lang="md">

Vue 的 SFC 中,我們知道有 <template><script><style> 等標籤來分別撰寫模板、邏輯和樣式,但其實 Vue 也支援自訂區塊(Custom Blocks),我們可以利用這個特性來加入我們的規格文件,導入之後類似於這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref } from 'vue'

const message = ref('Hello World')
</script>

<template>
<div>{{ message }}</div>
</template>

<style scoped>
div {
color: blue;
}
</style>

<spec lang="md">
- 這是一個簡單的 Vue 元件,顯示一個藍色的 "Hello World" 訊息。
- 使用了 Vue 3 的 Composition API 來定義響應式變數 `message`。
- 樣式使用了 scoped,確保樣式只作用於此元件。
- `message` 的文字顏色設定為藍色。
</spec>

修改前

修改後

那該怎麼做到這件事情呢?其實官方文件中有提到自訂區塊的使用方式,可以參考這篇文章:SFC Custom Block Integrations

其中文章中,就有舉例 i18n 的自訂區塊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import vue from '@vitejs/plugin-vue'
import yaml from 'js-yaml'

const vueI18nPlugin = {
name: 'vue-i18n',
transform(code, id) {
// if .vue file don't have <i18n> block, just return
if (!/vue&type=i18n/.test(id)) {
return
}
// parse yaml
if (/\.ya?ml$/.test(id)) {
code = JSON.stringify(yaml.load(code.trim()))
}
// mount the value on the i18n property of the component instance
return `export default Comp => {
Comp.i18n = ${code}
}`
},
}

export default {
plugins: [vue(), vueI18nPlugin],
}

接著你只要這樣就可以使用

1
2
3
4
5
6
<template>Hello</template>

<i18n lang="yaml">
message: 'world'
fullWord: 'hello world'
</i18n>

基於這個特性,我們只需要將 Vite 插件稍微修改一下,就可以支援我們的 <spec lang="md"> 標籤了,底下提供了兩個版本的範例,一個是純 Vite 版本,另一個是 Nuxt 版本。

Vite 版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { fileURLToPath, URL } from 'node:url';

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
plugins: [
vue(),
{
name: 'vue-spec-plugin',
enforce: 'pre', // 在其他外掛程式之前執行
transform(_code, id) {
// 更精確的正則表達式匹配 .vue 檔案中的 spec 區塊
if (/\.vue\?vue&type=spec(?:&|$)/.test(id)) {
return {
code: 'export default {};',
map: null,
};
}
// 明確回傳 null 表示不處理此檔案
return null;
},
},
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});

而 Nuxt 版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default defineNuxtConfig({
vite: {
plugins: [
{
name: 'vue-spec-plugin',
enforce: 'pre', // 在其他外掛程式之前執行
transform(_code, id) {
// 更精確的正則表達式匹配 .vue 檔案中的 spec 區塊
if (/\.vue\?vue&type=spec(?:&|$)/.test(id)) {
return {
code: 'export default {};',
map: null,
};
}
// 明確回傳 null 表示不處理此檔案
return null;
},
},
],
},
});

這樣你就可以在 Vue SFC 中使用 <spec lang="md"> 區塊來撰寫規格跟需求。

那這樣有哪些好處呢?我認為有以下:

  • 集中管理:規格文件直接寫在元件檔案中,方便查看與維護,每個元件都有自己的規格,方便團隊協作與溝通。
  • 上下文一致:AI 在生成程式碼時,可以直接參考元件內的規格,減少誤解。
  • 減少文件數量:不需要額外維護多個規格文件,減少混亂。

但要注意這個方式只限於 .vue 檔案,如果是純 js/ts 檔案就無法使用這個方式,反而會推薦你用 JSDoc or TypeScript 的型別定義來描述會比較好。

最後這邊也提供範例程式碼給你,你可以試著玩看看:

參考文獻