技術筆記
Vue 3 Pinia TypeScript

把 API 呼叫統一封裝進 services,Pinia store 才不會越養越肥

一開始的寫法

專案早期很多元件是這樣寫的:

// ArticleList.vue
const res = await axios.get(`/api/articles?page=${page.value}`);
articles.value = res.data.data;

Pinia store 裡也有幾乎一樣的邏輯,只是多包了 try/catch。結果同一支 API 被呼叫的地方,錯誤處理、loading 狀態、型別各寫一套,改一個欄位要改三個地方。

現在的分層

固定成三層,職責切乾淨:

  • services/articleService.ts:只負責打 API、回傳型別化的資料,不管畫面狀態。
  • stores/article.ts(Pinia):呼叫 service,管理 isLoadingerror、快取好的資料。
  • .vue 元件:只呼叫 store 的 action 跟讀 state,不知道 axios 的存在。
// services/articleService.ts
export async function fetchArticles(page: number): Promise<Article[]> {
  const { data } = await http.get<ArticleResponse>("/api/articles", {
    params: { page },
  });
  return data.data;
}
// stores/article.ts
export const useArticleStore = defineStore("article", () => {
  const articles = ref<Article[]>([]);
  const isLoading = ref(false);

  async function load(page: number) {
    isLoading.value = true;
    try {
      articles.value = await fetchArticles(page);
    } finally {
      isLoading.value = false;
    }
  }

  return { articles, isLoading, load };
});

換來的好處

換 API 版本或後端欄位改名的時候,只要動 services 那一層,store 跟元件完全 不用碰。單元測試也好寫很多,service 直接 mock 掉,store 的邏輯可以獨立測。 唯一的代價是一開始要多寫幾個檔案,小專案可能覺得多此一舉,但只要專案活過 半年,這筆帳一定划算。