在現代前端開發中,有效管理 API 請求和異步數據是至關重要的。本文將介紹如何結合 Pinia、@vueuse/core 的 createFetch、Zod 和 TypeScript,創建一個強大而靈活的數據管理系統。我們將在 Day 15 的基礎上,進一步優化我們的用戶管理系統,實現更高效的數據獲取和狀態管理。
首先,讓我們使用 @vueuse/core 的 createFetch 來重構我們的 API 請求。這將為我們提供更多的靈活性和功能,如自動取消請求等。
修改 src/composables/useUserApi.ts
:
import { createFetch } from '@vueuse/core'
import { CommonHttpStatusCode, userSchema, UserSchema } from "../schemas/user"
const useFetch = createFetch({
baseUrl: 'http://api.example.com',
options: {
async beforeFetch({ options }) {
// 這裡可以加入認證邏輯
return { options }
},
},
fetchOptions: {
mode: 'cors',
},
})
export const useUserApi = () => {
const createUserApi = async (user: UserSchema): Promise<UserSchema> => {
const requestValidator = userSchema.safeParse(user)
if (!requestValidator.success) {
console.error(requestValidator.error)
throw new TypeError('request zod type error')
}
const { data, error } = await useFetch('/createUser', {
method: 'POST',
body: JSON.stringify(user),
}).json()
if (error.value) {
throw new Error('API request failed')
}
const responseValidator = userSchema.safeParse(data.value)
if (!responseValidator.success) {
console.error(responseValidator.error)
throw new TypeError('response zod type error')
}
return responseValidator.data
}
return {
createUserApi,
}
}
export type UseUserApi = typeof useUserApi
備註補充
: 以上 beforeFetch
認證邏輯的擴展,基本上大部分的複雜邏輯實現可以參考 Day 13 的文章。
這裡簡單表示,防止文章內文實現過於複雜,關於驗證方面
可以參考 Day 13 的 responseSchema 的驗證,如果是 header 授權相關的,有很多範例,筆者僅展示 Bearer Token
的展示,事實上關於授權相關是個水很深的主題,且嚴謹地遵守 RFC 規範,未來有機會可以寫另外一個 30 天,感謝🙏。
下一步,我們將更新 Pinia store 以支持取消請求。我們將使用 AbortController 來實現這一功能。
修改 src/stores/useUserStore.ts
:
import { acceptHMRUpdate } from 'pinia'
import { definePrivateState } from './privateState'
import { LoadingStatus, UserSchema } from '../schemas/user'
import { useUserApi } from '../composables/useUserApi'
import { useLoadingStore } from './useLoadingStore'
import { ref } from 'vue'
export interface UserStoreState {
user: UserSchema | null
error: string | null
}
export const useUserStore = definePrivateState('useUserStore', (): UserStoreState => {
return {
user: null,
error: null
}
}, privateState => {
const { createUserApi } = useUserApi()
const loadingStore = useLoadingStore()
const { addLoadingStatus, isLoadingStatusExist, removeLoadingStatus, isTypeError } = loadingStore
const abortController = ref<AbortController | null>(null)
const createUser = async (user: UserSchema): Promise<boolean> => {
if (isLoadingStatusExist(LoadingStatus.CreateUser)) return false
addLoadingStatus(LoadingStatus.CreateUser)
// 如果有正在進行的請求,先取消它
if (abortController.value) {
abortController.value.abort()
}
// 創建新的 AbortController
abortController.value = new AbortController()
try {
const createdUser = await createUserApi(user)
privateState.user = createdUser
return true
} catch (error) {
if (isTypeError(error)) {
// 如果是型別錯誤在這裡做一些處理
privateState.error = '資料格式錯誤'
} else if (error instanceof Error) {
privateState.error = error.message
} else {
privateState.error = '未知錯誤'
}
return false
} finally {
removeLoadingStatus(LoadingStatus.CreateUser)
abortController.value = null
}
}
const cancelCurrentRequest = () => {
if (abortController.value) {
abortController.value.abort()
abortController.value = null
removeLoadingStatus(LoadingStatus.CreateUser)
}
}
return {
createUser,
cancelCurrentRequest
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}
現在,讓我們更新我們的 Vue 組件以使用新的 store 功能,包括取消請求的能力。
修改 src/components/UserForm.vue
:
<script setup lang="ts">
import CustomInput from '../components/CustomInput.vue'
import { useCreateUserForm } from '../composables/useCreateUserForm'
import { useUserStore } from '../stores/useUserStore'
import { onBeforeUnmount } from 'vue'
const userStore = useUserStore()
const { createUser, cancelCurrentRequest } = userStore
const {
userName,
email,
age,
formSubmit,
isSubmittingDisabled,
errors
} = useCreateUserForm(async (values) => {
const isApiResponseSuccess = await createUser(values)
if (isApiResponseSuccess) {
alert('送出成功')
}
return isApiResponseSuccess
})
// 在組件卸載前取消正在進行的請求
onBeforeUnmount(() => {
cancelCurrentRequest()
})
</script>
<template>
<!-- 表單內容與之前相同 -->
</template>
為了未來能夠輕鬆進行測試,我們可以創建一個 mock 數據文件和一個簡單的 mock API 服務。
創建 src/mocks/userMockData.ts
:
import { UserSchema } from '../schemas/user'
export const mockUsers: UserSchema[] = [
{ userName: 'JohnDoe', email: 'john@example.com', age: 30 },
{ userName: 'JaneSmith', email: 'jane@example.com', age: 25 },
]
export const createMockUser = (user: UserSchema): Promise<UserSchema> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...user, id: Math.random().toString(36).substr(2, 9) })
}, 1000)
})
}
為了方便在開發和測試環境中切換between真實 API 和 mock API,我們可以創建一個環境變量和一個工具函數。
創建 src/utils/apiConfig.ts
:
export const isUseMockApi = import.meta.env.VITE_USE_MOCK_API === 'true'
export const getApiBaseUrl = () => {
return isUseMockApi ? '/mock-api' : 'http://api.example.com'
}
然後更新 src/composables/useUserApi.ts
:
import { createFetch } from '@vueuse/core'
import { CommonHttpStatusCode, userSchema, UserSchema } from "../schemas/user"
import { getApiBaseUrl, isUseMockApi } from '../utils/apiConfig'
import { createMockUser } from '../mocks/userMockData'
const useFetch = createFetch({
baseUrl: getApiBaseUrl(),
options: {
async beforeFetch({ options }) {
// 這裡可以加入認證邏輯
return { options }
},
},
fetchOptions: {
mode: 'cors',
},
})
export const useUserApi = () => {
const createUserApi = async (user: UserSchema): Promise<UserSchema> => {
const requestValidator = userSchema.safeParse(user)
if (!requestValidator.success) {
console.error(requestValidator.error)
throw new TypeError('request zod type error')
}
if (isUseMockApi) {
return await createMockUser(user)
}
const { data, error } = await useFetch('/createUser', {
method: 'POST',
body: JSON.stringify(user),
}).json()
if (error.value) {
throw new Error('API request failed')
}
const responseValidator = userSchema.safeParse(data.value)
if (!responseValidator.success) {
console.error(responseValidator.error)
throw new TypeError('response zod type error')
}
return responseValidator.data
}
return {
createUserApi,
}
}
export type UseUserApi = typeof useUserApi
在這個 Day 16 的實作中,我們成功地將 Day 15 的基礎上進行了多項改進:
這些改進不僅提高了我們應用的可靠性和效能,還為未來的開發和測試奠定了堅實的基礎。通過結合 Pinia、@vueuse/core、Zod 和 TypeScript,我們創建了一個強大而靈活的數據管理系統,能夠有效地處理異步操作和錯誤情況。
在未來的文章中,我們將進一步探討如何利用這個基礎來實現更複雜的功能,如數據緩存、離線支持等高級特性。同時,我們也將深入研究如何編寫單元測試,以確保我們的數據管理邏輯的正確性和穩定性。