跳至內容

WebConf 2025 延伸閱讀:Vue.js 與 AI 協作的開發新思維

JSDC 2025 之後,今天在 WebConf 2025 又講了一場「AI 只懂 React?Vue.js 也能 Vibe Coding!」。

事實上,演講標題裡的 React 只是個引子,我知道你們想看什麼,但不是你們想的那樣 XD

這場演講真正想跟大家聊的,是 AI 協作時代下開發思維的轉變。 而最近在社群竄起的 SDD(Spec-Driven Development)剛好跟我一直以來的理念相當合拍,於是就有了這場延續 JSDC 的分享。

WebConf 當天雖然有將近 50 分鐘,但要把 SDD 從概念講到落地還是有點趕,很多實作細節只能快速帶過就跳下一張投影片, 趁著剛講完記憶還熱,我想補充一些沒講到的重點,並且把簡報重點整理一下,方便回顧。

廢話不多說,先上簡報:

在新視窗開啟簡報 ↗

本講演の <spec> のコンセプトは、@ykoizumi0903 さんの記事「Vue SFC のカスタムブロックと AI 駆動開発の相性が良い理由」から着想を得ました。

記事では、Spec-Driven Development の二つの課題として「仕様の言語化の難しさ」と「仕様とコードの分離による AI コンテキストの喪失」を指摘されています。Vue SFC の Custom Block を活用することで、仕様と実装を同一ファイルに配置し、AI が常に完全なコンテキストを参照できる点は、まさに目から鱗でした。

私はこの基盤の上に、いくつかの方向性を拡張しました:SDD の4ステップ閉ループフロー、逆同期(Distillation)の概念、レイヤー戦略(コア層/ビジネス層/プレゼンテーション層)、そして既存の CI/CD フローとの統合方法です。

この重要な洞察を共有してくださった ykoizumi0903 さんに心より感謝申し上げます。おかげで Vue コミュニティに具体的かつ実践可能な AI 協働のアプローチが生まれました。


這場演講的 <spec> 概念,最初是受到 @ykoizumi0903 的文章 Vue SFC のカスタムブロックと AI 駆動開発の相性が良い理由 啟發。

他在文中指出 Spec-Driven Development 的兩大痛點:規格難以語言化,以及規格與程式碼分離導致 AI 上下文遺失。而 Vue SFC 的 Custom Block 恰好能解決這個問題,將規格與實作放在同一個檔案,確保 AI 永遠能讀到完整的上下文。這個洞見讓我眼睛一亮。

我在這個基礎上延伸了幾個方向:SDD 的四步閉環流程、反向同步(Distillation)的概念、分層策略(核心層/業務層/展示層),以及與現有 CI/CD 流程的整合方式。 感謝 @ykoizumi0903 提出這個關鍵洞見,讓 Vue 社群有了一個具體可行的 AI 協作方案。

這篇文章我想補充以下幾個重點:

  • <spec> 的環境設定與常見問題
  • 有 Spec vs 無 Spec 的實際差異
  • SDD 落地時的限制與風險(對,任何方法論都有坑)
  • 與現有 CI/CD 流程的整合方式
  • 團隊導入的漸進式策略
  • 會眾 FAQ(設計師 PM 參與、同步機制、跨元件狀態管理等)

環境設定完整版

在開始使用 <spec> 之前,需要完成以下設定。

1. Vite Plugin

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

function ignoreSpecBlock() {
  return {
    name: 'ignore-spec-block',
    transform(code: string, id: string) {
      if (id.includes('?vue&type=spec')) {
        return { code: 'export default {}' }
      }
    }
  }
}

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

這個 Plugin 的作用是:當 Vite 遇到 <spec> 區塊時,直接回傳空物件。Production build 不會包含任何 spec 內容,實現零執行成本。

2. TypeScript 宣告

typescript
// src/shims-spec.d.ts
declare module 'vue' {
  interface SFCCustomBlocksOptions {
    spec?: string
  }
}

export {}

加上這個宣告,Volar 就不會對 <spec> 報紅字警告。

注意最後的 export {}:TypeScript 的 .d.ts 檔案如果沒有任何 importexport,會被視為 script(全域腳本);加上 export {} 後才會被視為 module,此時 declare module 'vue' 才能正確作為 module augmentation 擴充 Vue 的型別定義。少了這行,vue-tsc 可能會報錯。

感謝 ShunnNet 留言指正!

3. IDE 設定(VS Code + Vue - Official Extension,原 Volar)

<spec> 標籤加上 lang="md",可以獲得 Markdown 語法高亮:

vue
<spec lang="md">
# UserCard

## Props
- user: { name: string, role: 'admin' | 'user' }
</spec>

4. ESLint 設定(視情況加入)

通常 ESLint 會自動忽略未知的 Custom Block,不需要額外設定。

但如果你安裝了某些嚴格的 Parser 或 Markdown 檢查 Plugin 導致報錯,可以針對性調整。注意:vue/comment-directive 規則主要處理 <!-- eslint-disable --> 等註解,關閉它可能導致行內 ESLint 忽略功能失效。

json
{
  "overrides": [
    {
      "files": ["*.vue"],
      "rules": {
        // 僅在遇到解析錯誤時才考慮加入
        // "vue/comment-directive": "off"
      }
    }
  ]
}

建議先確認錯誤來源,再決定是否需要此設定。


無 Spec vs 有 Spec:AI 產出的差異

讓 AI 實際寫一個 UserCard 元件,看看有沒有 Spec 的差異。

情境:建立一個 UserCard 元件

需求:顯示使用者卡片,admin 要有特殊標示。

無 Spec 的 Prompt:

幫我寫一個 UserCard 元件,顯示使用者名稱和角色,
admin 要有金色邊框。

AI 產出(可能結果):

vue
<template>
  <div :class="cardClass">
    <h3>{{ user.name }}</h3>
    <span>{{ user.role }}</span>
  </div>
</template>

<script setup>
const props = defineProps(['user'])
const cardClass = computed(() =>
  props.user.role === 'admin' ? 'border-yellow-500' : ''
)
</script>

問題:

  • border-yellow-500 而非設計稿的 amber-400
  • 沒有點擊事件
  • Props 沒有型別定義
  • 每次生成可能不同

有 Spec 的元件:

vue
<spec lang="md">
# UserCard

## Props
- user: { name: string, role: 'admin' | 'user' }

## Behavior
- role='admin' → 金色邊框 (amber-400)
- role='user' → 預設邊框 (gray-200)

## Interaction
- 點擊卡片觸發 `select` 事件,payload: user.id
</spec>

<script setup lang="ts">
// AI 根據 Spec 生成...
</script>

AI 產出(穩定結果):

vue
<script setup lang="ts">
interface User {
  id: string
  name: string
  role: 'admin' | 'user'
}

const props = defineProps<{ user: User }>()
const emit = defineEmits<{ select: [id: string] }>()

const borderClass = computed(() =>
  props.user.role === 'admin'
    ? 'border-amber-400'
    : 'border-gray-200'
)
</script>

<template>
  <div
    :class="['border-2', 'rounded-lg', 'p-4', borderClass]"
    @click="emit('select', user.id)"
  >
    <h3>{{ user.name }}</h3>
    <span>{{ user.role }}</span>
  </div>
</template>

差異比較

項目無 Spec有 Spec
一致性每次可能不同穩定產出
型別安全可能缺少明確定義
Design TokenAI 自己猜指定顏色
事件定義可能遺漏明確列出
可維護性改需求要重新描述改 Spec 即可

為什麼不用 JSDoc 或註解就好?

可能有人會問:「為什麼不直接用 JSDoc 或程式碼註解來寫 Spec 就好?為什麼要用 <spec> Custom Block?」 這是一個好問題。讓我們比較一下兩者的差異:

比較項目JSDoc / 註解<spec> Custom Block
結構化程度純文字,散落在程式碼各處Markdown 結構(## Props, ## Behavior)
可定位性AI 需要在 // 註解中猜測哪些是規格AI 精準定位 <spec> 標籤
意圖明確性可能是實作註解,也可能是說明標籤本身表示「這是規格」
IDE 支援無特殊處理可加 lang="md" 獲得語法高亮

核心差異:在 Vue SFC 中,<spec> 提供了一個「明確標示為規格」的獨立區塊,AI 不需要猜測哪些註解是規格、哪些只是實作說明。

更重要的是 Co-location(空間群聚) 的價值:我們利用 Vue SFC 的特性,將 Spec 與 Code 放在同一個檔案,確保 AI 在處理該元件時,必定能讀取到這份「最高權重的指令」。 這比依賴外部文檔(容易過期、容易被 AI 忽略)或散落在各處的註解更可靠,同時也更易於人工後續維護。

當然,JSDoc 在非 SFC 的場景(如 Store、工具函式、composables)仍然是很好的選擇。 這裡我認為重點不在 「JSDoc vs <spec>」 的兩者對立,而是不同層級下怎麼選用最適合的方式。

LLM 對結構化的 Markdown 遵循能力較高。就像交接工作時,給同事一份條列式 SOP,比在程式碼裡到處寫註解更有效率。


Spec 寫作準則

該寫什麼

類別說明範例
Props 資料契約輸入的資料結構與型別user: { name: string, role: 'admin' | 'user' }
Behavior 行為規則條件與結果的對應role='admin' → 金色邊框 (amber-400)
Interaction 互動定義使用者操作與事件點擊觸發 select 事件,payload 為 user.id
視覺規範 Design Token顏色、間距等設計值amber-400gap-4rounded-lg

不該寫什麼

類別錯誤範例為什麼不該寫
程式庫選擇使用 axios.get 獲取資料屬於專案層(CLAUDE.md)
實作細節使用 ref<boolean>(false) 儲存 isOpen換實作方式就要改
佈局細節使用 flex justify-center 置中這是 CSS 實作
錯誤處理機制用 try-catch 包住 API 呼叫可能與全域處理衝突

判斷原則

兩個問題幫你判斷 Spec 粒度:

  1. 「換一種實作方式,這段描述需要改嗎?」 → 要改就是太細了
  2. 「AI 看完這段,還需要問我問題嗎?」 → 會問就是太粗了

Design Token 保留的重要性

反向同步時,很多人會把「看起來像實作」的東西都移除,結果把 amber-400 也移除了。

這是錯誤的。

Design Token 不是實作細節,是視覺規範。它們是 AI 保持 UI 一致性的關鍵。

markdown
# 錯誤:移除了 Design Token
## Behavior
- admin 使用者顯示金色邊框

# 正確:保留 Design Token
## Behavior
- role='admin' → 金色邊框 (amber-400)

只寫「金色邊框」,AI 可能生成 border-yellow-500border-amber-300、甚至 border-[#ffd700]。寫明 amber-400,AI 就知道用這個特定顏色。


SDD 的限制與風險

任何方法論都有適用範圍。不然就變成邪教了(笑)。誠實說明這些限制,才能幫你做正確決策。

已知限制

限制緩解方式
團隊需要時間適應「先寫 Spec」漸進式導入,從新元件開始
Spec 寫太詳細會變成負擔遵守「只寫意圖」原則
跨元件情境粒度難定義以「獨立可測試的單位」為粒度
不同 AI 工具處理不一致使用規則檔同步機制

風險警示

Spec 與 Code 衝突

案例:Spec 寫「用 try-catch 並 alert」,但專案引入了 Axios Interceptor。AI 照 Spec 生成 try-catch,錯誤訊息跳兩次。

解法:Spec 不寫實作機制,只寫意圖(「處理異常,遵循全域規範」)。

測試過度生成

讓 AI 同時生成 Code 和 Test,可能產生大量「為測試而測試」的案例。

解法:在 Spec 中標註「需要測試的關鍵行為」,使用分層策略。

過度依賴 Spec

團隊花太多時間寫 Spec,反而減緩開發。

解法:記住目標是「讓 AI 寫出可控的程式碼」,不是「寫完美的文件」。Spec 夠用就好。


與現有開發流程整合

CI/CD Pipeline

[開發] Spec(人工)→ AI CodeGen → AI TestGen → 本地驗證
[提交] git commit → Pre-commit Hook(檢查 Spec 是否更新)
[CI]   PR 建立 → Vitest → Lint → Type Check
[Review] 先看 <spec> 變更 → 再看 Code → 確認 Test 覆蓋

PR Review Checklist

在 PR Template 加入:

markdown
## SDD Checklist
- [ ] 新增元件是否已加上 `<spec>`
- [ ] 修改行為時 `<spec>` 是否已同步?
- [ ] 測試是否覆蓋 `<spec>` 定義的關鍵行為?

Git Hooks 自動提醒(Node.js 腳本範例)

將此腳本存為 scripts/check-spec.js,並在 .husky/pre-commit 中以 node scripts/check-spec.js 執行:

javascript
// scripts/check-spec.js
const { execSync } = require('child_process')

const changedFiles = execSync('git diff --cached --name-only')
  .toString().split('\n')

changedFiles
  .filter(f => f.endsWith('.vue'))
  .forEach(file => {
    const diff = execSync(`git diff --cached ${file}`).toString()
    const hasCodeChanges = diff.includes('<script') || diff.includes('<template')
    const hasSpecChanges = diff.includes('<spec')

    if (hasCodeChanges && !hasSpecChanges) {
      console.warn(`⚠️ ${file}: 修改了程式碼但沒更新 <spec>`)
    }
  })

注意:此腳本較為嚴格。在純重構(Refactoring)不改變行為的情況下,可能會出現誤報。建議團隊約定在 commit message 加入特定 tag(如 [refactor])來略過此檢查,或將腳本改為僅輸出提醒而非阻擋 commit。


反向同步(Distillation)實戰指南

當你修改了程式碼但忘記更新 Spec,或者接手一個沒有 Spec 的舊元件,就需要「反向同步」:從程式碼萃取出 Spec。 這裡的重點是抽象出「意圖」,而非「實作細節」。

實際操作步驟

Step 1:給 AI 的 Prompt

請閱讀這個元件的實作,萃取出 <spec> 區塊。

規則:
1. 只寫「意圖」,不寫「實作方式」
2. 保留 Design Token(如 amber-400)
3. 使用 Props / Behavior / Interaction 三段式結構
4. 移除任何程式庫相關描述(如 axios、pinia)
5. 如果程式碼中包含可疑邏輯或潛在 Bug,請標註為 (待確認),不要直接視為正規行為

Step 2:AI 產出的初稿

假設元件程式碼是:

vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useUserStore } from '@/stores/user'

interface Props {
  userId: string
  showBadge?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showBadge: true
})

const emit = defineEmits<{
  click: [userId: string]
  delete: [userId: string]
}>()

const userStore = useUserStore()
const user = computed(() => userStore.getUserById(props.userId))
const isAdmin = computed(() => user.value?.role === 'admin')
</script>

<template>
  <div
    class="p-4 rounded-lg border-2"
    :class="isAdmin ? 'border-amber-400 bg-amber-50' : 'border-gray-200'"
    @click="emit('click', userId)"
  >
    <span v-if="showBadge && isAdmin" class="badge-admin">Admin</span>
    <h3>{{ user?.name }}</h3>
    <button @click.stop="emit('delete', userId)">刪除</button>
  </div>
</template>

AI 萃取結果:

markdown
# UserCard

## Props
- userId: string - 使用者 ID
- showBadge: boolean (default: true) - 是否顯示角色徽章

## Behavior
- 根據 userId 從 store 取得使用者資料
- role='admin' → 金色邊框 (amber-400) + 淺金背景 (amber-50)
- role='admin' 且 showBadge=true → 顯示 Admin 徽章
- 其他角色 → 灰色邊框 (gray-200)

## Interaction
- 點擊卡片 → 觸發 `click` 事件,payload: userId
- 點擊刪除按鈕 → 觸發 `delete` 事件,payload: userId(不冒泡)

Step 3:人工審查與調整

AI 萃取的 Spec 需要人工確認:

  • Design Token 有保留
  • 事件定義完整
  • 「根據 userId 從 store 取得」可能太實作導向,改成「根據 userId 顯示對應使用者」

反向同步能自動化嗎?

短答案:半自動化可行,全自動化風險高。

可自動化的部分

  1. 偵測變更:Git Hook 可以偵測「程式碼改了但 Spec 沒改」
  2. 生成初稿:AI 可以從程式碼萃取 Spec 草稿
  3. 格式檢查:確認 Spec 包含必要區塊(Props / Behavior / Interaction)

不建議自動化的部分

  1. 直接覆蓋 Spec:AI 萃取可能包含實作細節,需要人工過濾
  2. 語意判斷:「這是意圖還是實作?」需要人類判斷
  3. 業務邏輯確認:AI 不知道某個行為是 Bug 還是 Feature

建議的半自動化流程

bash
# .husky/pre-commit(提醒模式)
# 注意:這是概念範例,實際使用請根據專案現況調整
#!/bin/sh

changed_vue_files=$(git diff --cached --name-only | grep '\.vue$')

for file in $changed_vue_files; do
  # 檢查是否有 <spec> 區塊
  if ! grep -q '<spec' "$file"; then
    echo "⚠️  $file 沒有 <spec> 區塊"
    continue
  fi

  # 檢查 spec 是否有更新
  diff=$(git diff --cached "$file")
  has_code_change=$(echo "$diff" | grep -E '<script|<template')
  has_spec_change=$(echo "$diff" | grep '<spec')

  if [ -n "$has_code_change" ] && [ -z "$has_spec_change" ]; then
    echo "⚠️  $file: 程式碼有變更但 <spec> 沒更新"
    echo "   執行 'pnpm spec:sync $file' 生成建議"
  fi
done
javascript
// scripts/spec-sync.js(生成建議,不自動覆蓋)
import Anthropic from '@anthropic-ai/sdk'
import fs from 'fs'

const client = new Anthropic()

async function generateSpecSuggestion(filePath) {
  const content = fs.readFileSync(filePath, 'utf-8')

  const message = await client.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    messages: [{
      role: 'user',
      content: `請從這個 Vue 元件萃取 <spec>,只寫意圖不寫實作:\n\n${content}`
    }]
  })

  console.log('=== 建議的 Spec ===')
  console.log(message.content[0].text)
  console.log('\n請人工審查後貼入元件')
}

為什麼不建議全自動化?

反向同步的核心價值在於「思考這段程式碼的意圖是什麼」。

如果完全自動化:

  • Spec 會變成「程式碼的另一種描述」而非「意圖的規格」
  • 失去人類審查,錯誤的行為會被記錄成「正確的 Spec」
  • Bug 會被當成 Feature 寫進 Spec

最佳實踐:自動化偵測 + AI 生成建議 + 人工審查確認。

記住,Spec 是給 AI 的「工作指令」,不是給版控的「程式碼描述」。讓機器做機器擅長的事,人做人擅長的事。


決策樹:該用 SDD 嗎?

不是每個元件都需要 Spec。不然光寫 Spec 就飽了。這裡提供一個簡單的決策流程:

這個元件需要寫 <spec> 嗎?

├─ 跟「錢」有關?(金流、付款、訂單)
│   └─ YES → Spec + Test

├─ 會被多處重用?
│   └─ YES → Spec Only

├─ 有複雜的條件邏輯?
│   └─ YES → Spec Only

├─ 需要長期維護?
│   └─ YES → Spec Only

└─ 以上皆否 → 直接 Vibe Coding

元件分類參考

元件類型策略Test理由
PaymentFormSpec + TestYes金流,錯誤代價高
LoginFormSpec + TestYes權限,安全性重要
UserCardSpec OnlyOptional可重用,但非核心
DataTableSpec OnlyOptional複雜互動
PageHeaderVibe CodingNo純展示
AboutPageVibe CodingNo一次性頁面

團隊導入策略

說真的,最難的不是技術,是說服團隊。「又來一個新東西要學?」這種反應很正常。

漸進式導入時程

Phase 1(第 1-2 週)
└─ 只在「新元件」使用 SDD
└─ 指定 1-2 位 Champion 先試水溫

Phase 2(第 3-4 週)
└─ 重構舊元件時順便加 Spec
└─ 建立 Spec 範本,降低撰寫門檻

Phase 3(第 5 週起)
└─ PR Review 開始檢查 Spec
└─ 每週五花 30 分鐘,AI 批次檢查同步狀態

Spec 範本

vue
<spec>
# [元件名稱]

## Props
- prop1: type - 說明

## Behavior
- 條件 → 結果 (design-token)

## Interaction
- 動作觸發 `event-name` 事件,payload: { ... }
</spec>

會眾 FAQ

演講結束後收到不少問題,這裡整理幾個比較常見的,順便補充一些在簡報裡來不及講的內容。

Q1: 設計師和 PM 需要參與 Spec 撰寫嗎?

還是純粹是工程師的工作?

我的看法是:Spec 的撰寫責任在工程師,但內容來源不只是工程師

設計師和 PM 不需要學會寫 <spec> 語法,但他們提供的資訊會直接影響 Spec 的品質:

角色貢獻範例
設計師Design Token、視覺規範「這個按鈕用 amber-400,不是 yellow-500」
PM業務邏輯、Edge Cases「admin 可以刪除,但要二次確認」
工程師整合成結構化 Spec把上述資訊寫成 Props / Behavior / Interaction

實務上的做法是:工程師寫完 Spec 初稿後,拉設計師和 PM 快速 Review 一下「這是你們要的嗎?」。這個過程通常 5 分鐘就結束,但可以避免後面來回修改。

如果你的團隊設計師或 PM 比較積極,也可以讓他們直接在 Spec 上面用註解補充,工程師再整理成正式格式。重點是「資訊要流通」,誰來寫只是執行細節。


Q2: Spec 跟 Code 不同步怎麼辦?有沒有強制機制?

講者提到「趕時間直接改 Code 忘記更新 Spec」是現實,有沒有強制機制防止這種情況?

有,但我建議「提醒優先,阻擋其次」。

文章前面有提到 Git Hook 的做法,這裡補充幾個層級:

Level 1:提醒(推薦先從這裡開始)

bash
# pre-commit hook 只 warn,不阻擋
echo "⚠️ 這些 .vue 檔案改了程式碼但沒更新 <spec>,請確認是否需要同步"

好處是不會打斷開發節奏,讓工程師自己判斷「這次改動是否影響行為」。純重構、改變數名稱這種不改行為的修改,本來就不需要更新 Spec。

Level 2:PR Review 檢查

在 PR Template 加入 Checklist:

markdown
- [ ] 行為有變更時,`<spec>` 已同步更新

這是人工檢查,但有紀錄可追蹤。

Level 3:CI 阻擋(謹慎使用)

可以在 CI 裡跑一個 script 檢查「有改 <script><template> 但沒改 <spec>」的檔案,然後 fail 掉。但這會有誤報(重構不改行為的情況),所以要搭配 escape hatch,例如在 commit message 加 [no-spec] 可以跳過檢查。

我的建議

從 Level 1 開始,觀察一兩週看看漏掉的頻率。如果團隊自律性夠高,Level 1 就夠了;如果經常漏,再升級到 Level 2。Level 3 通常只有在「這個元件出錯代價很高」(例如金流相關)才需要。

話說,如果工程師連提醒都無視,那問題可能不在工具上(笑)


Q3: AI 反覆修正會不會比自己寫還慢?什麼時候該放棄?

如果 AI 生成的結果需要反覆修正,會不會比自己寫還慢?

會,而且這是真實會發生的情況。

我自己的判斷標準是「三次原則」:第一次 AI 產出不對,先調整 Prompt 或 Spec;第二次還是不對,檢查是不是 Context 不夠,Rules 檔案有沒有相關規則;如果第三次還是不對,就直接手寫吧。

三次修正的時間,通常已經超過自己寫的時間了。繼續跟 AI 糾纏下去只會更浪費時間。

什麼情況 AI 特別容易卡住?

情境原因建議
複雜的狀態邏輯AI 難以追蹤多層狀態變化自己寫核心邏輯,AI 寫周邊
專案特有的 PatternAI 沒見過你們的寫法先在 Rules 放範例
Edge Case 處理AI 傾向 Happy Path手動補充邊界條件
效能敏感的程式碼AI 不知道你的效能瓶頸自己寫,AI 只做 Review

換個角度想

「AI 生成 → 修正 → 再生成」這個過程,其實也是在幫你釐清需求。有時候 AI 寫錯,反而讓你發現「原來我自己也沒想清楚這邊要怎麼處理」。

所以即使最後選擇手寫,這個過程也不算完全浪費。只是心情會有點煩躁


Q4: 跨元件的複雜互動怎麼處理?

簡報展示的是單一元件的 Spec,如果是跨多個元件的狀態管理(例如 Pinia),Spec 要怎麼寫?

這是個好問題,因為 <spec> 的設計確實是以「單一元件」為單位。跨元件的情況需要用不同的方式處理。

我的分層策略

層級放哪裡內容
專案層CLAUDE.mdStore 的整體架構、命名規範
模組層Store 檔案的 JSDoc這個 Store 的職責、對外 API
元件層<spec>這個元件如何使用 Store

實際範例

typescript
// stores/cart.ts

/**
 * @module CartStore
 *
 * ## 職責
 * - 管理購物車商品列表
 * - 計算總價(含折扣邏輯)
 * - 與後端同步購物車狀態
 *
 * ## 對外 API
 * - items: 商品列表(唯讀)
 * - totalPrice: 總價(含折扣)
 * - addItem(product, quantity): 加入商品
 * - removeItem(productId): 移除商品
 * - checkout(): 結帳,回傳 orderId
 *
 * ## 狀態流
 * 1. addItem → 更新 items → 觸發 totalPrice 重算
 * 2. checkout → 呼叫 API → 清空 items → 回傳 orderId
 */
export const useCartStore = defineStore('cart', () => {
  // ...
})

然後在元件的 <spec> 裡這樣寫:

vue
<spec lang="md">
# CartButton

## Dependencies
- 使用 CartStore 的 items 和 addItem

## Behavior
- 點擊 → 呼叫 CartStore.addItem(product, 1)
- items.length > 0 → 顯示紅點數字
</spec>

為什麼不把所有東西都寫在元件 Spec 裡?

因為會重複。如果 10 個元件都用到 CartStore,你不會想在 10 個地方都寫一次 Store 的完整規格。

元件的 Spec 只需要說明「這個元件怎麼用這個 Store」,Store 本身的規格放在 Store 檔案的 JSDoc 或獨立文件。


Q5: 團隊成員技術水平不一,怎麼確保 Spec 品質一致?

講者提到 Spec 寫太細或太粗都有問題,有沒有具體的 review checklist?

有,這是我目前實驗中整理出來的 Spec Review Checklist,供參考:

Spec Review Checklist

markdown
## 結構完整性
- [ ] 有 Props 區塊(如果元件接收 props)
- [ ] 有 Behavior 區塊(描述條件與結果)
- [ ] 有 Interaction 區塊(如果有使用者互動)

## 粒度檢查
- [ ] 沒有寫到「用什麼函式/API」(太細)
- [ ] 沒有寫到「怎麼實作」(太細)
- [ ] 有寫到「Design Token」(視覺規範要保留)
- [ ] 看完 Spec 後,AI 不需要再問問題(太粗的話會需要)

## 品質檢查
- [ ] 使用「條件 → 結果」格式描述行為
- [ ] 事件有說明 payload 結構
- [ ] 沒有模稜兩可的描述(如「適當處理」「必要時」)

實務上怎麼推動?

先從範本開始,前面文章有提供 Spec 範本,讓團隊成員照著填空就好。前幾次一起 Review,資深成員帶著 Junior 一起看,解釋為什麼這樣寫。然後建立 Bad Examples,收集「寫太細」和「寫太粗」的反例,讓大家知道邊界在哪。

一個簡單的判斷口訣

「換一種寫法,Spec 要改嗎?」

  • 要改 → 太細了,刪掉
  • 不用改 → 可以保留

比如「使用 ref<boolean>(false)」要改成「使用 reactive」的話 Spec 就要改,所以這句太細了。 但「admin 顯示金色邊框 (amber-400)」換成用 CSS Variable 實作,Spec 也不用改,所以可以保留。


Q6: 既有專案怎麼辦?

我們現有的 Vue 專案沒有 <spec> 區塊,要從哪裡開始導入?全面重寫還是漸進式?

絕對不要全面重寫除非你想讓團隊集體崩潰

漸進式導入的策略:

Phase 1:只對新元件使用

從今天開始,所有新建立的元件都加上 <spec>。舊的不動。

這是最低成本的起步方式,團隊可以先熟悉 Spec 的寫法,累積經驗。

Phase 2:修 Bug 時順便加

當你要修某個舊元件的 Bug,先花 5 分鐘讓 AI 幫你反向同步(Distillation)出 Spec 初稿,人工審核後加進去。

這樣 Spec 會隨著日常維護慢慢覆蓋到舊程式碼,而且是從「最常被改動的元件」開始,ROI 最高。

Phase 3:重點元件優先

如果想更主動一點,可以列出專案裡的「核心元件」,像是被很多地方引用的共用元件、跟金流或權限相關的元件、經常出 Bug 的元件,這些優先加上 Spec + Test。

實際時程建議

週數目標
1-2 週新元件 100% 有 Spec,指定 1-2 人當 Champion
3-4 週建立 Spec 範本和 Review Checklist,開始在 PR Review 檢查
5-8 週逐步對核心元件補 Spec(每週 2-3 個)
之後維持習慣,隨日常維護自然覆蓋

千萬不要做的事

不要一次性把所有元件都補上 Spec,浪費時間而且品質不會好。也不要強制要求兩週內全部完成,團隊會反彈。更不要沒有 Champion 就開始推,你需要有人負責回答問題、做示範。

心態調整

Spec 覆蓋率不是目標,「讓 AI 產出可控」才是目標

如果某個元件本來就很穩定、很少改動,沒有 Spec 也不會怎樣。把精力放在「會被頻繁改動」和「出錯代價高」的元件上。


結語

寫這篇文章的過程中,我一直在想一件事:SDD 的核心價值到底是什麼?

不是讓 AI 寫出更多程式碼,而是讓我們用更少的時間產出更可控的結果

Spec 本質上是一種「意圖的結晶」。當你把腦中模糊的需求寫成結構化的 Spec,其實就是在強迫自己想清楚「我到底要什麼」。這個過程本身就有價值,就算不用 AI,這份 Spec 也能幫助團隊溝通、幫助 Code Review、幫助未來的自己回想當初的設計意圖。

說到底,Spec 的本質是溝通。它不只是寫給 AI 看的提示詞,也是寫給人類接手者看的規格書。即使哪天不用 AI 了,良好的規格描述本來就是高品質程式碼的一部分。

我在演講的時候也提到,工程師天生就討厭寫文件,也討厭別人不寫文件。然後自己不看文件,也討厭別人不看文件。 這個部分正好由 AI 來幫忙承擔。只要我們願意花點時間「先寫 Spec」,AI 就能幫我們把這些 Spec 轉化成程式碼和測試,減少人類撰寫的負擔。

雖然流程增加了「撰寫規格」的成本,本質上是用「前期思考時間」來換取「後期 Debug 時間」。 這筆交易是否划算,我認為取決於團隊的專案複雜度與團隊的成員素質。

對於快速迭代的原型期專案,這可能是負擔;但對於需要長期維護的核心業務,這筆投資通常是值得的。

所以與其說 SDD 是一種「AI 開發方法論」,不如說它是一種思考方式的轉變:從「我要寫什麼程式碼」變成「我要表達什麼意圖」。

SDD 某種程度上是針對「當前 AI 能力限制」所設計的工程解法。隨著 AI 持續進步,未來或許不需要這麼明確的結構化 Spec。但 Spec-first 的思維方式本身有獨立價值,它迫使我們在動手之前先想清楚意圖,這個習慣不會因為工具進步而過時。

這個轉變不容易,但值得嘗試。


相關資源

如有問題或建議,歡迎留言討論!

💬 留言討論

Released under the MIT License.