Vue 最佳實踐
本文件定義 ITO 前端團隊的 Vue 開發規範。假設你已熟悉 Vue Composition API,這裡說明的是團隊在多種合理做法中的選擇,而非基礎語法教學。
基礎規範
永遠使用 script setup
在組件中一律使用 <script setup> 語法,不使用傳統 Composition API 或 Options API。
原因
- 語法更簡潔,無需
export default和手動return - 編譯時優化,執行效能更好
- 更好的 TypeScript 支援與型別推斷
✅ Good
<script setup lang="ts">
import { ref } from 'vue';
const msg = ref('Hello world!');
</script>❌ Bad
<!-- 傳統 Composition API,需手動 return -->
<script>
import { ref } from 'vue';
export default {
setup() {
const msg = ref('Hello world!');
return { msg };
},
};
</script><!-- Options API -->
<script>
export default {
data() {
return { msg: 'Hello world!' };
},
methods: {},
};
</script>永遠使用 ref,而非 reactive
統一使用 ref 處理所有響應式狀態,包括原始型別和物件型別,避免使用 reactive。
原因
ref同時適用於原始型別與物件型別,統一使用能減少心智負擔.value的存取方式能明確辨識該變數為響應式狀態- 避免
reactive解構後失去響應性的陷阱
✅ Good
<script setup lang="ts">
import { ref } from 'vue';
const email = ref('');
const user = ref({ name: '小明', age: 16 });
// .value 明確標示這是響應式狀態
function updateUser() {
user.value = { name: '小華', age: 18 };
}
</script>❌ Bad
<script setup lang="ts">
import { reactive } from 'vue';
// reactive 解構後會失去響應性
const user = reactive({ name: '小明', age: 16 });
const { name } = user; // name 不再是響應式
</script>偏好 Immutable 更新,State 保持原子化
雖然 Vue 的響應式系統是 mutable,但團隊偏好 immutable 的更新方式,State 應保持扁平結構。
原因
- Immutable 更新讓資料流更清晰,易於追蹤變化
- 扁平的 State 結構降低複雜度,避免深層巢狀更新的麻煩
- 若有效能考量,可使用
shallowRef僅追蹤.value的變化
✅ Good
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
// 原子化 state(單層物件 OK)
const user = ref({
name: '小明',
age: 16,
address: '台北市',
});
// immutable 更新陣列
const orderList = ref<Order[]>([]);
function addOrder(order: Order) {
orderList.value = [...orderList.value, order];
}
function updateOrder(id: number, data: Partial<Order>) {
orderList.value = orderList.value.map(order =>
order.id === id ? { ...order, ...data } : order,
);
}
function removeOrder(id: number) {
orderList.value = orderList.value.filter(order => order.id !== id);
}
// 效能考量時使用 shallowRef
const largeList = shallowRef<Order[]>([]);
function setLargeList(newList: Order[]) {
largeList.value = newList; // 整個替換才會觸發更新
}
</script>❌ Bad
<script setup lang="ts">
import { ref } from 'vue';
// 過深的巢狀物件
const user = ref({
profile: {
name: '小明',
address: {
city: '台北',
district: '信義區',
},
},
});
// mutable 更新,難以追蹤變化
const orderList = ref<Order[]>([]);
orderList.value.push(newOrder);
orderList.value[0].status = 'completed';
</script>使用 getter-only computed
computed 應保持純粹,僅用於衍生狀態的計算,不應包含 setter。
原因
- 保持 computed 的純函式特性,易於理解和測試
- 避免隱式副作用,資料流更清晰
- 需要可寫入的響應式資料時,使用
ref搭配函式處理更明確
✅ Good
<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('小明');
const lastName = ref('王');
// getter-only computed
const fullName = computed(() => `${firstName.value}${lastName.value}`);
function updateName(first: string, last: string) {
firstName.value = first;
lastName.value = last;
}
</script>❌ Bad
<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('小');
const lastName = ref('明');
// 帶有 setter 的 computed,產生隱式副作用
const fullName = computed({
get: () => `${firstName.value}${lastName.value}`,
set: (val: string) => {
const [first, ...rest] = val;
firstName.value = first;
lastName.value = rest.join('');
},
});
</script>Template 避免複雜邏輯
Template 應保持簡潔,複雜的邏輯應抽到 computed 或函式中。
原因
- 提升可讀性,讓 template 專注於呈現結構
- 提升可測試性,邏輯可以獨立測試
- 邏輯可複用,避免在多處重複相同的計算
✅ Good
<script setup lang="ts">
import { ref, computed } from 'vue';
const orderList = ref<Order[]>([]);
const activeOrders = computed(() =>
orderList.value.filter(order => order.status === 'active'),
);
const sortedActiveOrders = computed(() =>
[...activeOrders.value].sort((a, b) => a.createdAt - b.createdAt),
);
const totalAmount = computed(() =>
activeOrders.value.reduce((sum, order) => sum + order.amount, 0),
);
</script>
<template>
<ul>
<li v-for="order in sortedActiveOrders" :key="order.id">
{{ order.productName }}
</li>
</ul>
<p>總金額:{{ totalAmount }}</p>
</template>❌ Bad
<template>
<ul>
<li
v-for="order in orderList
.filter(o => o.status === 'active')
.sort((a, b) => a.createdAt - b.createdAt)"
:key="order.id"
>
{{ order.productName }}
</li>
</ul>
<p>
總金額:{{
orderList
.filter(o => o.status === 'active')
.reduce((sum, o) => sum + o.amount, 0)
}}
</p>
</template>優先使用事件處理,而非 watch
優先使用明確的事件處理函式,保留 watch 給真正需要監聽資料變化的場景。
原因
- 事件處理的執行流程更明確,易於追蹤和 debug
watch在跨組件時難以追蹤觸發來源- 保留
watch給必要場景(如 route 參數變化、外部資料源同步)
✅ Good
<script setup lang="ts">
import { ref } from 'vue';
const selectedProductId = ref(0);
// 明確的事件驅動流程
async function onSelectProduct(productId: number) {
selectedProductId.value = productId;
await fetchProductDetails(productId);
}
</script>
<template>
<button @click="onSelectProduct(123)">查看商品</button>
</template>❌ Bad
<script setup lang="ts">
import { ref, watch } from 'vue';
const selectedProductId = ref(0);
// 難以追蹤誰修改了 selectedProductId
watch(selectedProductId, async productId => {
await fetchProductDetails(productId);
});
</script>
<template>
<button @click="selectedProductId = 123">查看商品</button>
</template>組件設計
組件命名統一使用 PascalCase
無論是檔案名稱或在 template 中使用,皆採用 PascalCase 命名組件。
原因
- 與 HTML 原生標籤明確區分,一眼就能辨識是自訂組件
- 與 JavaScript 類別/建構函式的命名慣例一致
- 統一的命名風格提升程式碼可讀性
✅ Good
<script setup lang="ts">
import UserProfile from '@/components/UserProfile.vue';
import ProductCard from '@/components/ProductCard.vue';
</script>
<template>
<UserProfile :user="currentUser" />
<ProductCard :product="selectedProduct" @add-to-cart="handleAddToCart" />
</template>❌ Bad
<script setup lang="ts">
import UserProfile from '@/components/user-profile.vue';
import ProductCard from '@/components/product-card.vue';
</script>
<template>
<user-profile :user="currentUser" />
<product-card :product="selectedProduct" @add-to-cart="handleAddToCart" />
</template>Script 內程式碼順序
依照以下原則組織 <script setup> 內的程式碼:
- Props / Emits / Model — 組件介面定義(最上方)
- 依功能分組 — 相關的狀態、computed、函式放在一起
- 生命週期 Hooks —
onMounted、onUnmounted等(最下方)
每個功能區塊內部建議順序:狀態 → computed → 函式
原因
- 組件介面定義在最上方,快速了解組件的輸入輸出
- 依功能分組讓相關邏輯集中,提升可讀性和可維護性
- 生命週期 hooks 放最下方,避免干擾主要邏輯閱讀
✅ Good
<script setup lang="ts">
// 1. 組件介面定義
const props = defineProps<{
userId: number;
readonly?: boolean;
}>();
const emits = defineEmits<{
save: [data: UserData];
cancel: [];
}>();
// 2. 功能區塊:使用者資料管理
const { user, loading, fetchUser } = useUser();
const formData = ref({ name: '', email: '' });
const isDirty = computed(() => formData.value.name !== user.value?.name);
function resetForm() {
formData.value = {
name: user.value?.name ?? '',
email: user.value?.email ?? '',
};
}
function handleSave() {
emit('save', formData.value);
}
// 2. 功能區塊:權限檢查
const authStore = useAuthStore();
const canEdit = computed(
() => !props.readonly && authStore.hasPermission('edit'),
);
// 3. 生命週期 hooks
onMounted(() => {
fetchUser(props.userId);
});
</script>Props 與 Emits 定義
使用 TypeScript 型別語法定義 Props 和 Emits,不使用 runtime 宣告。
原因
- TypeScript 型別語法提供更好的型別推斷和 IDE 支援
- 程式碼更簡潔,型別定義更直觀
- 與 TypeScript 生態系統整合更緊密
✅ Good
<script setup lang="ts">
// Props 定義
const props = withDefaults(
defineProps<{
title: string;
status?: 'active' | 'inactive';
items?: string[];
}>(),
{
status: 'active',
items: () => [],
},
);
// Emits 定義
const emits = defineEmits<{
updateValue: [value: string];
submit: [data: FormData];
}>();
</script>❌ Bad
<script setup lang="ts">
// Runtime 宣告,型別推斷較弱
const props = defineProps({
title: String,
status: {
type: String,
default: 'active',
},
items: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['updateValue', 'submit']);
</script>不要解構 Props
一律使用 props.xxx 存取 props,不解構 props。
原因
- 明確來源,
props.xxx讓我們清楚知道該值來自父組件 - 避免誤用,在 Vue 3.4 及更早版本中解構會失去響應性
- 跨版本相容,避免依賴編譯器的隱式轉換
✅ Good
<script setup lang="ts">
const props = defineProps<{
userId: number;
userName: string;
}>();
// 使用 props.xxx
function greet() {
console.log(`Hello, ${props.userName}`);
}
watchEffect(() => {
console.log(props.userId);
});
</script>❌ Bad
<script setup lang="ts">
const props = defineProps<{
userId: number;
userName: string;
}>();
// 解構 props
const { userId, userName } = props;
function greet() {
console.log(`Hello, ${userName}`); // 不清楚來源
}
watchEffect(() => {
console.log(userId); // Vue 3.4 及以前版本會失去響應性
});
</script>事件命名規範
事件名稱統一使用 kebab-case,無論是在 emit 或 template 中。
原因
- HTML 屬性不區分大小寫,
kebab-case避免混淆 - 與 HTML 原生事件命名慣例一致(如
@click、@input) - 統一風格提升程式碼可讀性
✅ Good
<script setup lang="ts">
const emits = defineEmits<{
updateValue: [value: string];
addToCart: [productId: number];
}>();
// 使用 kebab-case
emit('update-value', value);
emit('add-to-cart', productId);
</script>
<template>
<ProductCard @add-to-cart="handleAddToCart" />
</template>❌ Bad
<script setup lang="ts">
const emits = defineEmits<{
updateValue: [value: string];
addToCart: [productId: number];
}>();
// 使用 camelCase
emit('updateValue', value);
emit('addToCart', productId);
</script>
<template>
<ProductCard @addToCart="handleAddToCart" />
</template>狀態管理
使用 Pinia 管理共享狀態
使用 Pinia 集中管理跨組件的共享狀態,避免使用 provide / inject。
原因
- 集中於 store 統一管理,比起分散在各組件更易於維護與追蹤
- Pinia 是 Vue 官方推薦的新一代狀態管理方案
- 若專案無法引入 Pinia,可考慮 VueUse 的 createGlobalState
✅ Good
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth';
import { storeToRefs } from 'pinia';
// 使用 Pinia 管理共享狀態
const authStore = useAuthStore();
const { user, isAuthenticated } = storeToRefs(authStore);
function handleLogout() {
authStore.logout();
}
</script>❌ Bad
<!-- Parent Component -->
<script setup lang="ts">
import { ref, provide } from 'vue';
// 使用 provide / inject 難以追蹤狀態來源
const user = ref(null);
provide('user', user);
</script>
<!-- Child Component -->
<script setup lang="ts">
import { inject } from 'vue';
const user = inject('user'); // 不清楚 user 從哪裡來
</script>避免使用 provide / inject
provide / inject 難以追蹤狀態來源,應改用 Pinia 或 composables。
原因
- 難以追蹤資料流向,不清楚狀態從哪個組件提供
- 缺乏型別安全,inject 的值可能為 undefined
- Pinia 或 composables 提供更好的可維護性和開發體驗
✅ Good
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth';
import { storeToRefs } from 'pinia';
// 使用 Pinia,狀態來源明確
const authStore = useAuthStore();
const { token } = storeToRefs(authStore);
</script>❌ Bad
<!-- Parent Component -->
<script setup lang="ts">
import { ref, provide } from 'vue';
const token = ref(null);
provide('token', token); // 難以追蹤誰使用了這個 token
</script>
<!-- Child Component -->
<script setup lang="ts">
import { inject } from 'vue';
const token = inject('token'); // 不清楚 token 從哪裡來
</script>Pinia 只使用 Setup Store
Pinia Store 統一使用 Setup Store 語法,不使用 Option Store。
原因
- 語法與 Composition API 一致,能無縫整合 composables
- 提供更好的 TypeScript 型別推斷
- 邏輯組織更靈活,可依功能分組而非強制分類為 state / getters / actions
✅ Good
// Setup Store
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([]);
const totalAmount = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
);
function addItem(item: CartItem) {
items.value = [...items.value, item];
}
return { items, totalAmount, addItem };
});❌ Bad
// Option Store
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
}),
getters: {
totalAmount: state =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
},
actions: {
addItem(item) {
this.items.push(item);
},
},
});Composables
定義與分類
Composable 是使用 Vue Composition API 封裝的邏輯函式,我們將其分為通用型和組件專屬型兩類。
原因
- 明確區分可複用的通用邏輯與專案特定邏輯
- 通用型 composable 可跨專案複用,提升開發效率
- 組件專屬型 composable 幫助簡化複雜組件邏輯
| 類型 | 用途 | 命名範例 |
|---|---|---|
| 通用型 | 單一功能、可跨專案複用 | useDebounce、useLocalStorage |
| 組件專屬型 | 組件邏輯過於複雜,抽離以提升可讀性,不考慮複用 | useOrderFormLogic、useUserProfileState |
兩種類型皆統一放置於 /composables 目錄下。
Composable vs Pure Function
依據是否使用 Vue API 區分 Composable 和 Pure Function,並放置於不同目錄。
原因
- 清楚區分依賴 Vue 的邏輯和純函式邏輯
- Composable 可使用 Vue 的響應式系統和生命週期
- Pure Function 更容易測試和跨框架複用
✅ Good
// /utils/format.ts - Pure function,不依賴 Vue
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
}).format(value);
}
// /composables/useCounter.ts - Composable,使用 Vue API
import { ref, computed } from 'vue';
export function useCounter(initial = 0) {
const count = ref(initial);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubled, increment };
}❌ Bad
// /composables/format.ts - Pure function 不應放在 composables
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
}).format(value);
}
// /utils/useCounter.ts - Composable 不應放在 utils
import { ref } from 'vue';
export function useCounter() {
const count = ref(0);
return { count };
}命名規範
Composable 函式名稱統一使用 use 前綴。
原因
use前綴是 Vue 社群的慣例,清楚表達這是一個 composable- 與一般函式明確區分,一眼就能辨識
- 與 React Hooks 命名慣例一致,降低學習成本
✅ Good
export function useUserList() {
const users = ref<User[]>([]);
async function fetchUsers() {
users.value = await api.getUsers();
}
return { users, fetchUsers };
}
export function useFormValidation() {
const errors = ref<Record<string, string>>({});
function validate(data: FormData) {
// 驗證邏輯
}
return { errors, validate };
}❌ Bad
// 缺少 use 前綴
export function getUserList() {
/* ... */
}
export function formValidation() {
/* ... */
}參數偏好傳入 Raw Value
Composable 的函式參數偏好接收原始值,而非直接傳入 ref 或 getter。
原因
- 呼叫端更明確,清楚知道何時觸發操作
- Composable 邏輯更單純,不需處理 ref 或 getter 的轉換
- 避免隱式的 watch 行為,執行時機更可控
✅ Good
export function useSearch() {
const results = ref<Product[]>([]);
const loading = ref(false);
async function search(keyword: string) {
loading.value = true;
results.value = await api.searchProducts(keyword);
loading.value = false;
}
return { results, loading, search };
}
// 使用時
const keyword = ref('');
const { results, loading, search } = useSearch();
async function onSearch() {
await search(keyword.value); // 明確傳入當下的值
}❌ Bad
// 直接傳入 ref,呼叫端不知何時觸發
import type { MaybeRefOrGetter } from 'vue';
import { toValue, watch } from 'vue';
export function useSearch(keyword: MaybeRefOrGetter<string>) {
const results = ref<Product[]>([]);
watch(
() => toValue(keyword),
async val => {
results.value = await api.searchProducts(val);
},
{ immediate: true },
);
return { results };
}優先使用物件的形式回傳
Composable 回傳值統一使用物件形式,方便呼叫端按需解構。
原因
- 呼叫端可按需解構,不需要的值可以不取
- 語意明確,一眼就能看出每個值的用途
- 方便擴充,新增回傳值不會影響現有程式碼
✅ Good
export function useOnline() {
const isOnline = ref(navigator.onLine);
window.addEventListener('online', () => (isOnline.value = true));
window.addEventListener('offline', () => (isOnline.value = false));
return { isOnline };
}
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function execute() {
loading.value = true;
error.value = null;
try {
data.value = await fetch(url).then(r => r.json());
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
return { data, error, loading, execute };
}❌ Bad
// 回傳陣列,呼叫端需要記住順序
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function execute() {
// ...
}
return [data, error, loading, execute]; // 順序難記憶
}
// 使用時
const [data, error, loading, execute] = useFetch('/api/users');Composable 專注邏輯,UI 留在組件
Composable 負責狀態與業務邏輯,不處理 UI 相關的樣式、DOM 操作或 template 渲染。
原因
- 職責分離,Composable 專注於邏輯,組件專注於呈現
- 提升可測試性,邏輯可以獨立測試而不需要渲染組件
- 提升可複用性,相同邏輯可以在不同 UI 呈現中複用
✅ Good
<!-- UI 邏輯留在組件 -->
<script setup lang="ts">
import { useOrderForm } from '@/composables/useOrderForm';
const { formData, errors, submit, isSubmitting } = useOrderForm();
</script>
<template>
<form @submit.prevent="submit">
<input
v-model="formData.productName"
:class="{ error: errors.productName }"
/>
<span v-if="errors.productName" class="error-text">
{{ errors.productName }}
</span>
<button :disabled="isSubmitting">送出訂單</button>
</form>
</template>// /composables/useOrderForm.ts
// 專注於邏輯,不涉及 UI
export function useOrderForm() {
const formData = ref({ productName: '', quantity: 1 });
const errors = ref<Record<string, string>>({});
const isSubmitting = ref(false);
async function validate() {
errors.value = {};
if (!formData.value.productName) {
errors.value.productName = '請輸入商品名稱';
}
return Object.keys(errors.value).length === 0;
}
async function submit() {
const isValid = await validate();
if (!isValid) return;
isSubmitting.value = true;
await api.createOrder(formData.value);
isSubmitting.value = false;
}
return { formData, errors, isSubmitting, submit };
}❌ Bad
// /composables/useOrderForm.ts
// 在 composable 中處理 UI 邏輯
export function useOrderForm() {
const formData = ref({ productName: '', quantity: 1 });
async function submit() {
// ❌ 在 composable 中操作 DOM
document.querySelector('.submit-btn')?.classList.add('loading');
await api.createOrder(formData.value);
// ❌ 在 composable 中顯示 UI 提示
alert('訂單已送出');
}
return { formData, submit };
}使用 Named Export
避免使用 export default,具名匯出有利於 tree-shaking 與 IDE 自動補全
// Good ✅ 具名匯出
export function useCounter() {
/* ... */
}
// Bad ❌ 預設匯出
export default function useCounter() {
/* ... */
}Spec 區塊
什麼是 Spec 區塊?
<spec> 是 Vue SFC 的自訂區塊,使用 Markdown 格式將元件規格與實作程式碼共存於同一檔案中。
- 描述 WHAT(元件應該是什麼),而非 HOW(如何實作)
- 「如果換一種實作方式,這個描述需要更新嗎?」
- 如果「需要」→ 過於詳細,移除實作細節
- 「AI 讀完這個 Spec 後,還需要額外說明嗎?」
- 如果「需要」→ 過於模糊,補充條件邏輯
Spec 區塊結構
永遠使用這四個段落,不多不少
<spec lang="md">
# {ComponentName}
## Props
[資料結構與型別定義]
## Behavior
[條件邏輯 → 視覺結果]
## Interaction
[使用者操作 → 事件定義]
</spec>{ComponentName} 段落
顯示這個 component 的名稱
Props 段落
使用 TypeScript 型別語法定義資料結構:
## Props
- user: { name: string, role: 'admin' | 'user', avatar: string, id: number }多個 props 範例:
## Props
- status: 'active' | 'inactive' | 'pending'
- label: string
- count: number
- onClick: () => voidBehavior 段落
使用 條件 → 結果 格式,搭配 Design Token 描述視覺規格:
## Behavior
role='admin' → 顯示編輯按鈕、金色邊框 (amber-400)
role='user' → 隱藏編輯按鈕、灰色邊框 (gray-200)關鍵規則:
- 使用 Design Token(
amber-400、green-500),不使用 hex 色碼或顏色名稱 - 使用箭頭格式(
→),不使用冗長的「當...時」句式 - 只描述視覺結果,不包含 pixel 值或實作細節
- 禁止框架 API 術語(如
primary、default、large) - 禁止顏色名稱(如「綠色」、「紅色」),必須使用 Design Token
❌ 錯誤範例(冗長、使用顏色名稱):
## Behavior
- 當 `role === 'admin'` 時:
- 顯示編輯按鈕 (primary 樣式)
- 卡片邊框為綠色,寬度 2px✅ 正確範例(簡潔、使用 Design Token):
## Behavior
role='admin' → 顯示編輯按鈕、金色邊框 (amber-400)
role='user' → 隱藏編輯按鈕、灰色邊框 (gray-200)
status='active' → 背景 (green-500)、文字 (white)、圖示 ✓
status='inactive' → 背景 (red-500)、文字 (white)、圖示 ✗Interaction 段落
定義使用者操作與事件,包含 payload 結構:
## Interaction
點擊編輯按鈕 → emit('edit', user.id)
點擊刪除按鈕 → emit('delete', { id: user.id, name: user.name })何時撰寫或更新 Spec
在以下情況應撰寫或更新 <spec> 區塊:
- ✅ 建立新的 Vue 元件
- ✅ 修改現有元件的行為
- ✅ 變更 Props、Events 或視覺外觀
- ✅ 元件經常被修改、高風險或大量重複使用
常見錯誤
| 錯誤寫法 | 問題 | 正確做法 |
|---|---|---|
| 章節名稱使用「事件」、「樣式」、「功能」 | 章節名稱必須精確 | 使用 Props、Behavior、Interaction |
| Props 使用條列式加註解 | 格式錯誤 | 單行 TypeScript 型別語法 |
| 使用顏色名稱(「綠色」、「紅色」) | 缺乏語意 | 使用 Design Token(green-500、red-500) |
使用 hex 色碼(#fbbf24) | 實作細節 | 使用 Design Token(amber-400) |
包含 CSS class 名稱(btn-primary) | 實作細節 | 只描述視覺結果 |
包含 pixel 值(2px、寬度 1px) | 實作細節 | 只描述語意層級 |
使用框架 API 術語(primary、large) | 實作細節 | 描述視覺或行為結果 |
| 冗長的「當...時」句式 | 可讀性差 | 使用箭頭格式(→) |