Appearance
AI 協作:抽取與驗證 composable
本章的技術主線在上一節已經完成,這一節是可選的協作實例,跳過不影響後續章節。「把元件裡的邏輯抽成 composable」是 AI 特別擅長的機械性工作,也是特別容易在細節上翻車的工作;正好,這一章學到的每一條慣例,都能變成驗收它的清單。
跟上一章不同的是,這次有精準對應的 skill 可用:vue-skills 裡的 create-adaptable-composable,專門處理「抽成可調適 composable」這件事,全書第一次用到非通用的技能。
AI 協作:用 vue skill 加速
情境:書店的搜尋頁元件裡,混著一段「依關鍵字過濾書單、順便算出符合筆數」的有狀態邏輯。列表頁也要用同一段邏輯,決定抽成 useBookSearch。
做法:在安裝好 vue-skills 的 AI 工具中(安裝與觸發方式見第一章),下指令 use create-adaptable-composable。要注意這個 skill 的內建語彙以 TypeScript 的 MaybeRefOrGetter 為主,而本書主線是 JavaScript,所以提示詞要明確要求輸出語言:
把搜尋頁元件裡的過濾邏輯抽成 src/composables/useBookSearch.js:輸入 books 與 keyword,兩者都要能接受純值、ref 或 getter(用 toValue() 處理);回傳 results 與 count;狀態建立在函式內,維持每次呼叫各自獨立,不要模組層級狀態;用 JavaScript 撰寫,不使用 TypeScript。
AI 產出(節錄):toValue() 用上了,過濾邏輯也對。但這次的產出藏著兩個典型問題,而且都「看起來能動」:
js
// 問題一:AI 覺得搜尋結果可以「共用快取」,把狀態放到了模組層級
const results = ref([])
export function useBookSearch(books, keyword) {
// ⋯過濾邏輯(正確)⋯
// 問題二:回傳包成 reactive,說是「用起來不必打 .value」
return reactive({ results, count })
}人工驗證重點:
- 問題一違反「每次呼叫各自獨立」:
results宣告在函式外,搜尋頁跟列表頁會共用同一份結果,一頁輸入關鍵字、另一頁的清單跟著變。提示詞明明寫了不要模組層級狀態,AI 仍可能為了「效能」自作主張,這種好意最需要驗。 - 問題二正是上一節的常見錯誤:回傳
reactive整包,呼叫端const { results } = useBookSearch(...)一解構就斷線。回傳值要是普通物件裝著 refs。 - 其餘逐項照本章的慣例驗:命名是不是
use開頭、只做一件事;輸入是否每次讀取都經過toValue()(而不是進函式時取一次值就定格);這個例子沒有副作用,但若 AI 產出帶進了watch或事件監聽,就要檢查清理與同步脈絡:hooks 與 watcher 是否登記在 setup 的同步階段、上一輪副作用有沒有收拾。
修正後的可執行版本收在下方摺疊區塊。老話一句:AI 的產出是起點,不是成品;vue-skills 是社群維護的輔助工作流,不是本書的規範來源,驗證的依據永遠是 Vue 官方文件與這一章建立的慣例。
修正後的完整範例(教學簡化版)
src/composables/useBookSearch.js:
js
import { computed, toValue } from 'vue'
// books、keyword:純值、ref 或 getter 皆可
export function useBookSearch(books, keyword) {
// 狀態建立在函式內:每次呼叫各自獨立
const results = computed(() => {
const kw = toValue(keyword).trim().toLowerCase()
const list = toValue(books)
if (!kw) return list
return list.filter((book) => book.title.toLowerCase().includes(kw))
})
const count = computed(() => results.value.length)
// 普通物件裝著 refs (computed 回傳的也是一種 ref)
return { results, count }
}src/App.vue:
vue
<script setup>
import { ref } from 'vue'
import { useBookSearch } from './composables/useBookSearch.js'
const books = ref([
{ id: 1, title: '重新認識 Vue.js' },
{ id: 2, title: '0 陷阱!0 誤解!8 天重新認識 JavaScript!' },
{ id: 3, title: '重新認識 Vue.js 第二版' }
])
const keyword = ref('')
const { results, count } = useBookSearch(books, keyword)
</script>
<template>
<main class="app">
<label>
搜尋
<input v-model="keyword" type="search">
</label>
<p>符合 {{ count }} 本</p>
<ul>
<li v-for="book in results" :key="book.id">{{ book.title }}</li>
</ul>
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
</style>整段邏輯只用 computed 就完成,連 watch 都不需要:關鍵字或書單一變,results 與 count 自動重算。教學簡化版的明確缺口:真實的搜尋通常還要處理輸入防抖(避免每個字元都觸發重算或請求)與非同步資料來源,前者可以用上一節介紹的 VueUse(useDebounceFn、refDebounced),後者就是 useBookFetch 的守備範圍。
這一章的旅程到此結束:邏輯的重複抽成 composable、副作用誰建立誰清理、非同步狀態只聽最新請求的、輸入輸出各有慣例。這套心智馬上就會再登場:第六章的 Vue Router 把應用程式帶出單一畫面,而它提供的 useRoute、useRouter,正是以 composable 的形狀交到我們手上。
