我正在使用 Nuxt 3 进行服务器端渲染。对于 REST 请求,我使用 Axios。
当用户登录时,刷新令牌 cookie 会保存在浏览器中。然后,对于任何 REST 端点的所有请求,标头中都会包含访问令牌。如果未找到访问令牌,则使用刷新令牌请求一个访问令牌。如果访问令牌已过期,则会请求新的访问令牌,并再次发送失败的请求。所有这些都是由 Axios 拦截器处理的。
现在,我遇到的问题是,如果刷新令牌不再有效,则根本不可能创建新的访问令牌。因此,访问令牌端点返回代码 401。但是当我尝试在 Axios 拦截器中捕获该 401 并导航到登录页面时,它只会在客户端执行此操作。
为了调试该问题,我创建了这个简单的页面。它仍然使用axios,但是没有拦截器。但基本问题似乎是相同的,即服务器端导航不能从承诺链发生。
<template>
<div>
{{ data }}
{{ error }}
</div>
</template>
<script setup lang="ts">
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
return userService.getSelf().catch(() => {
try {
console.log('handling error');
navigateTo('/login');
console.log('navigated to /login');
} catch (ex) {
console.log(ex);
}
})
});
</script>
服务器端日志是这样写的:
handling error
WARN [nuxt] useAsyncData should return a value that is not null or undefined or the request may be duplicated on the client side.
[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables.
很明显,navigateTo 失败并出现异常,因为它从未记录“导航到/登录”。结果,页面被渲染。您基本上无法再访问的页面。
客户端日志这样说:
GET http://127.0.0.1:8080/users/self 401 (Unauthorized)
restricted.vue:13 handling error
restricted.vue:15 navigated to /login
因此,在渲染 /restricted 页面并将请求再次提交到客户端的 REST 端点之后,导航到 /login 成功,并且您将被重定向到 /login
现在,如果我简单地检查数据对象是否已设置,如果没有,则导航到 /login,一切正常。它将针对 /restricted 请求返回状态 302,而不先渲染该页面。正如我所希望的那样。该代码如下所示:
<template>
<div>
{{ data }}
{{ error }}
</div>
</template>
<script setup lang="ts">
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
return userService.getSelf()
});
if (!data.value) {
navigateTo('/login')
}
</script>
当然,这不是处理它的好方法,因为刷新令牌可能在任何给定时刻、在任何 100 个页面上以及在任何 100 个不同的 REST 端点调用期间变得无效。我需要的是对这一切进行某种集中处理。我错过了什么?有没有更好的方法来处理这样的事情?
https://github.com/nuxt/nuxt/discussions/16715#discussioncomment-6564177):
// create useCustomFetch to replace useFetch
import { withQuery } from 'ufo'
import { defu } from 'defu'
import type { FetchError } from 'ofetch'
import type { AvailableRouterMethod, NitroFetchRequest } from 'nitropack'
import type { AsyncData, UseFetchOptions, FetchResult } from 'nuxt/app'
type PickFrom<T, K extends Array<string>> = T extends Array<any> ? T : T extends Record<string, any> ? keyof T extends K[number] ? T : K[number] extends never ? T : Pick<T, K[number]> : T;
type KeysOf<T> = Array<T extends T ? (keyof T extends string ? keyof T : never) : never>;
export function useCustomFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void
? 'get' extends AvailableRouterMethod<ReqT>
? 'get'
: AvailableRouterMethod<ReqT>
: AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
>(
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null> {
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const { token, logout } = useUserSession()
return useFetch(
request,
defu(
<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>>{
baseURL: config.api.baseUrl,
headers: {
authorization: `bearer ${token.value}`,
},
onRequestError: (error) => {
if (error.response?.status === 401) {
logout()
router.push(withQuery('/auth', { redirect: route.fullPath }))
}
},
},
opts,
),
)
}
您可以修改它以匹配您自己的业务逻辑。请记住,在整个应用程序中,您应该使用此自定义可组合项而不是默认的 useFetch。