fix: abort機制

This commit is contained in:
skytek_xinliang
2026-06-01 14:50:47 +08:00
parent f61432ad8a
commit 915f3b7f2f
9 changed files with 70 additions and 16 deletions
+7
View File
@@ -21,3 +21,10 @@
## 資料流
store 可以呼叫 service module。component 不應繞過 store/composable 直接處理 token、session 或 HTTP hooks。
## 請求取消慣例
- 需要避免重複提交或快速切換造成的舊請求殘留時,在 store 層管理 `AbortController`
- 同一類請求使用固定 key(例如 `auth/login``menu/get-menu`),新請求前先取消舊請求。
- service module 只接收 `signal`,不管理 controller lifecycle。
- store 在 `finally` 清理該 key,在 `clear/logout` 清理全部 key。
+6 -6
View File
@@ -5,6 +5,7 @@ import { normalizeError } from '@/services/error'
import { authApi } from '@/services/modules/auth'
import { tokenService } from '@/services/token'
import { useMenuStore } from '@/stores/menu'
import { createRequestControllerManager } from '@/stores/request-controller'
const defaultLoginRequestFormat: LoginRequestFormat = 'formData'
@@ -56,22 +57,20 @@ export const useAuthStore = defineStore('auth', () => {
const token = tokenService.token
const loading = ref(false)
const error = ref<string | null>(null)
// 只針對 login 取消重複請求,避免競態與重複提交
const loginController = ref<AbortController | null>(null)
const requestControllerManager = createRequestControllerManager()
const isAuthenticated = computed(() => !!token.value)
const roles = computed(() => (user.value?.role ? [user.value.role] : []))
const login = async (payload: LoginPayload, options: LoginOptions = {}) => {
loginController.value?.abort()
loginController.value = new AbortController()
const signal = requestControllerManager.replace('auth/login')
loading.value = true
error.value = null
try {
const requestBody = createLoginRequestBody(payload)
const requestFormat = options.requestFormat ?? defaultLoginRequestFormat
const requestOptions = { signal: loginController.value.signal }
const requestOptions = { signal }
const { data } =
requestFormat === 'json'
? await authApi.loginWithJson(requestBody, requestOptions)
@@ -125,11 +124,12 @@ export const useAuthStore = defineStore('auth', () => {
throw normalizedError
} finally {
loading.value = false
loginController.value = null
requestControllerManager.clear('auth/login')
}
}
const logout = () => {
requestControllerManager.clearAll()
user.value = null
tokenService.clearToken()
useMenuStore().clear()
+9 -2
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { normalizeError } from '@/services/error'
import { menuApi, type MenuNode } from '@/services/modules/menu'
import { createRequestControllerManager } from '@/stores/request-controller'
export interface LayoutMenuItem {
title: string
@@ -17,6 +18,7 @@ export const useMenuStore = defineStore('menu', () => {
const isRail = ref(false)
const error = ref<string | null>(null)
const loading = ref(false)
const requestControllerManager = createRequestControllerManager()
const menuStorageKey = 'sk_playground_menu'
const favoriteStorageKey = 'sk_playground_favorite'
@@ -180,6 +182,7 @@ export const useMenuStore = defineStore('menu', () => {
})
const clear = () => {
requestControllerManager.clearAll()
menu.value = []
favorite.value = []
isRail.value = false
@@ -190,9 +193,10 @@ export const useMenuStore = defineStore('menu', () => {
}
const getMenu = async (id: string) => {
const signal = requestControllerManager.replace('menu/get-menu')
try {
loading.value = true
const res = await menuApi.getMenu({ userID: id })
const res = await menuApi.getMenu({ userID: id }, { signal })
menu.value = Array.isArray(res.data.data) ? (res.data.data as MenuNode[]) : []
} catch (error_) {
const normalizedError = normalizeError(error_)
@@ -202,13 +206,15 @@ export const useMenuStore = defineStore('menu', () => {
throw normalizedError
} finally {
loading.value = false
requestControllerManager.clear('menu/get-menu')
}
}
const getFavorite = async (id: string) => {
const signal = requestControllerManager.replace('menu/get-favorite')
try {
loading.value = true
const res = await menuApi.getFavorite({ userID: id })
const res = await menuApi.getFavorite({ userID: id }, { signal })
favorite.value = Array.isArray(res.data.data) ? (res.data.data as MenuNode[]) : []
} catch (error_) {
const normalizedError = normalizeError(error_)
@@ -218,6 +224,7 @@ export const useMenuStore = defineStore('menu', () => {
throw normalizedError
} finally {
loading.value = false
requestControllerManager.clear('menu/get-favorite')
}
}
+33
View File
@@ -0,0 +1,33 @@
export interface RequestControllerManager {
replace: (key: string) => AbortSignal
clear: (key: string) => void
clearAll: () => void
}
export function createRequestControllerManager(): RequestControllerManager {
const controllers = new Map<string, AbortController>()
const replace = (key: string): AbortSignal => {
controllers.get(key)?.abort()
const controller = new AbortController()
controllers.set(key, controller)
return controller.signal
}
const clear = (key: string) => {
controllers.delete(key)
}
const clearAll = () => {
controllers.forEach((controller) => {
controller.abort()
})
controllers.clear()
}
return {
replace,
clear,
clearAll,
}
}