fix: abort機制
This commit is contained in:
@@ -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
@@ -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
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user