Nuxt 3 SSR Cookie首次加载undefined? 两种解决方案
2025-04-15 11:49:13
搞定 Nuxt 3 SSR 中 Cookie 首次加载 undefined 的问题
写 Nuxt 3 应用的时候,用 useCookie
处理 Cookie 是个挺方便的事儿。但有时候,尤其是在服务端渲染(SSR)的时候,可能会遇到一个头疼的问题:第一次加载页面,或者清了浏览器 Cookie 再访问,想从 Cookie 里拿个值,结果拿到的是 undefined
。后面刷新就好了,就很奇怪。
具体场景是这样:你想根据用户的偏好(比如语言)来设置一个 Cookie,fav_lang
。这个偏好的默认值,得从一个 API 接口那儿取。你希望在任何组件或 Composable 函数(特别是在 SSR 期间)访问这个 fav_lang
Cookie 时,它都应该有个正确的初始值,而不是 undefined
。试了在 Nuxt 插件里去获取 API 并设置 Cookie,但首次 SSR 访问时,其他地方读到的 fav_lang
还是 undefined
。
这篇就来捋一捋,为啥会这样,以及怎么解决在 Nuxt 3 SSR 场景下,确保 Cookie 在首次访问时也能读到正确的、从 API 获取的默认值。
为啥会这样?深挖原因
这事儿的关键在于 Nuxt 3 SSR 的运行机制和 useCookie
的工作方式。
- SSR 请求来了: 当用户的浏览器第一次请求页面(或者清了 Cookie 后请求),这个请求先打到服务器。
- Nuxt 开始渲染: 服务器上的 Nuxt 开始处理这个请求,准备渲染页面。这包括运行 Nuxt 插件、执行组件的
setup
函数(里面可能调用了 Composable)。 useCookie
执行: 当你的代码(无论在插件、组件还是 Composable 里)执行到useCookie('fav_lang')
时,它会尝试从当前进来的这个服务器请求里读取fav_lang
Cookie。- Cookie 不存在: 因为是首次访问或者 Cookie 被清了,请求头里根本没有
fav_lang
这个 Cookie。所以,useCookie
自然就返回了undefined
。 - 异步操作的“时差”: 如果你尝试在插件里通过
$fetch
去异步调用 API 获取默认值,然后再设置 Cookie (fav_lang.value = defaultValue
),这里有个时间差。$fetch
是个异步操作,它需要时间去请求 API 并等待响应。但在它完成之前,Nuxt 的渲染流程可能已经继续往下走了,其他的组件或 Composable 可能已经执行了它们自己的useCookie('fav_lang')
,这时它们读到的就是那个初始的undefined
。等到你的 API 调用成功并设置了 Cookie 值时,那些代码已经执行完了。这就是为啥第一次是undefined
,而之后刷新页面(Cookie 已经被设置上了)就正常了。 - 客户端激活 (Hydration): 服务器渲染完成后,HTML 发送到浏览器。浏览器加载 JS,Vue 开始“激活”过程,接管页面。这时如果再执行
useCookie
,它会读取浏览器端的 Cookie,或者沿用 SSR 时传递过来的状态,情况可能会变好,但 SSR 那一瞬间的问题已经发生了。
所以,核心矛盾点在于:需要在 SSR 期间、在所有需要该 Cookie 的地方读取它 之前,就完成 ①检查 Cookie 是否存在 -> ②如果不存在,则异步获取 API 数据 -> ③用获取到的数据设置 Cookie 默认值 这一整套操作。
解决方案
要解决这个问题,就得确保获取和设置默认 Cookie 的操作,能在 Nuxt 应用初始化和渲染的关键路径上,足够早且正确地完成。下面提供几种思路和具体操作方法。
方案一:利用 useCookie
的异步默认值工厂 (主要推荐)
Nuxt 3 的 useCookie
其实非常灵活,它的第二个参数可以传入一个选项对象,其中有个 default
属性。这个 default
属性不仅仅能接受一个普通的值,还能接受一个函数 ,甚至是一个异步函数 (async function) !
这简直就是为咱们这个场景量身定做的。当 useCookie
在读取 Cookie 时发现它不存在,它就会调用这个 default
函数来获取默认值。如果是异步函数,Nuxt 会正确地处理(await
)它,确保拿到结果后再继续。
原理:
useCookie(name, { default: async () => { ... } })
会在 Cookie 不存在时执行这个异步工厂函数。在 SSR 期间,Nuxt 会等待这个异步函数完成,拿到返回值作为 Cookie 的初始值。这样,后续所有(在同一个 SSR 请求处理流程中)调用 useCookie('fav_lang')
的地方,都能读到这个通过 API 获取并设置的默认值了。
步骤与代码:
假设你有一个 Composable ~/composables/useLanguages.js
负责获取语言列表和 API 调用:
// ~/composables/useLanguages.js (示例)
import { ref } from 'vue'
import { $fetch } from 'ofetch' // 或者 #app 里的 $fetch
export function UseLanguages() {
const languages_list = ref(null);
const languages_list_api = async () => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 150)); // 模拟网络延迟
// 实际项目中换成你的 $fetch 调用
// const data = await $fetch('/api/languages');
const data = [ // 假设这是 API 返回的数据
{ code: 'en', name: 'English', default_web: 0 },
{ code: 'zh-CN', name: 'Simplified Chinese', default_web: 1 },
{ code: 'ja', name: 'Japanese', default_web: 0 },
];
languages_list.value = data;
console.log('API fetched, languages list updated.');
};
return { languages_list, languages_list_api };
}
现在,直接在你的 Composable 或者需要用到 Cookie 的地方使用 useCookie
,并提供异步 default
工厂:
// 在某个 composable 或 setup 函数里
import { useCookie } from '#app'; // 从 #app 导入
import { UseLanguages } from '~/composables/useLanguages'; // 引入你的语言 Composable
// ... 可能在 setup() 或另一个 composable function 内部 ...
const favLangCookie = useCookie<string | null>('fav_lang', {
// ----- 这就是关键 👇 -----
async default() {
console.log('Cookie "fav_lang" not found, executing async default factory...');
// 这个 default 函数只在 Cookie 值是 undefined 时执行
// 并且我们最好只在服务端执行 API 调用来设置初始默认值
if (process.server) {
try {
const { languages_list_api, languages_list } = UseLanguages();
// 调用 API 获取语言列表
await languages_list_api();
// 从返回结果里找到默认语言
const defaultLang = languages_list.value?.find(lang => lang.default_web === 1);
if (defaultLang) {
console.log(`Default language found via API: ${defaultLang.code}. Setting it as default.`);
return defaultLang.code; // 返回找到的默认语言代码
} else {
console.log('No default language marked in API response.');
return 'en'; // 提供一个硬编码的后备默认值,避免返回 null/undefined
}
} catch (error) {
console.error('Error fetching default language in cookie factory:', error);
return 'en'; // 出错时也返回一个后备值
}
}
// 如果在客户端执行到这里(比如 hydration 期间检查),通常不需要重新 fetch
// Nuxt 的 SSR 数据传递机制会处理好
// 或者,如果 cookie 仍然缺失,可能返回一个客户端的默认值或 null
console.log('Running default factory on client, returning null (or client default).');
return null; // 在客户端,如果没有从服务端获取到值,可以返回 null 或客户端默认值
},
// ----- 异步 default 工厂结束 -----
// 其他 Cookie 配置,很重要!
path: '/', // 保证 Cookie 在全站可用
maxAge: 60 * 60 * 24 * 365, // Cookie 有效期,比如一年
sameSite: 'lax', // SameSite 策略,'lax' 或 'strict' 通常更安全
// secure: process.env.NODE_ENV === 'production', // 生产环境建议启用 Secure
// httpOnly: true, // 如果你的 JS 不需要直接读取它,建议设为 true,更安全
});
// 现在,favLangCookie.value 在 SSR 首次加载时,也会有值了(如果 API 成功返回的话)
console.log('Current fav_lang value:', favLangCookie.value);
// 你可以把 favLangCookie 暴露出去或者在当前作用域使用
// return { favLangCookie };
解释:
useCookie('fav_lang', { ... })
:定义我们要操作的 Cookie 名fav_lang
和配置项。async default()
:声明这是一个异步的默认值生成函数。if (process.server)
:我们通常只想在服务器端执行这个 API 调用来 首次 设置默认值。客户端的 Cookie 应该由服务器响应头里的Set-Cookie
或者后续的用户操作来设定。UseLanguages()
:调用你的 Composable 来获取 API 功能。await languages_list_api()
:执行异步 API 调用。Nuxt 在 SSR 期间会等待这个await
完成。find(lang => lang.default_web === 1)
:根据你的逻辑找到默认语言。return defaultLang.code
:返回找到的值。这个值会被useCookie
用作当前的初始值,并且useCookie
会自动帮你把这个值通过Set-Cookie
响应头发送给浏览器 ,这样浏览器也就存下了这个 Cookie。- 错误处理和后备值:添加
try...catch
并在出错或没找到默认语言时,返回一个可靠的后备值(比如 'en'),防止 Cookie 值意外变成null
或undefined
。 - Cookie 选项:
path
,maxAge
,sameSite
,secure
,httpOnly
这些选项对于 Cookie 的行为和安全至关重要,记得根据需求配置好。
优点:
- 逻辑内聚:获取默认值的逻辑和
useCookie
本身结合紧密。 - Nuxt 友好:利用了 Nuxt 提供的标准机制,写法比较优雅。
- 时机正确:Nuxt 会确保在 SSR 渲染需要用到 Cookie 值之前,等待这个异步默认值工厂执行完毕。
安全建议:
- 仔细设置 Cookie 的
path
,maxAge
,sameSite
,secure
(生产环境用true
) 和httpOnly
(如果 JS 不需要直接读,推荐true
) 属性,增强安全性。 - API 端点需要做好权限控制。
进阶使用技巧:
- 如果
UseLanguages
Composable 本身也需要在应用启动时做些初始化,确保它或者它的初始化逻辑(可能在某个插件里)能在useCookie
的default
工厂执行前准备好。不过,直接在default
工厂里调用 Composable 通常是没问题的。 - 对于非常复杂的初始化逻辑,可以考虑将获取默认值的逻辑封装成一个独立的异步函数,然后在
default
工厂里调用它。
方案二:使用 Nuxt Server Middleware (备选方案)
如果你希望把这种“请求预处理”的逻辑完全放到 Nuxt 的服务端处理流程的最前端,可以使用 Server Middleware。它在 Nitro 服务器处理请求时、在 Vue 应用开始渲染之前执行。
原理:
Server Middleware 可以拦截所有(或特定路径的)进入服务器的请求。在中间件里,你可以检查请求中是否带有 fav_lang
Cookie。如果没有,就调用 API 获取默认值,然后使用 h3
提供的 setCookie
辅助函数,将这个默认值设置到响应 (response) 头里。
注意: 这个方法主要是为后续的请求设置好 Cookie 。对于当前这次触发 Middleware 的请求,如果在它执行完毕之后、但在同一个请求响应周期内,Vue 应用内部的代码(比如某个 setup
函数里的 useCookie
)去读取 Cookie,它读到的可能仍然是请求(request)头里的原始状态(即 undefined
) ,而不是你在 Middleware 中刚设置到 响应 头里的值。
要解决这个问题,Middleware 在获取到默认值后,除了用 setCookie
写入响应头,还可以把这个值存入当前请求的上下文 event.context
中。然后,你的插件或者 useCookie
的 default
工厂(是的,可能还是要结合使用)可以优先从 event.context
读取这个预设的值。
这比方案一复杂,但提供了更早介入请求处理的机会。
步骤与代码:
-
创建 Server Middleware 文件:
在你的 Nuxt 项目根目录下创建
server/middleware/01.set-default-lang.ts
(文件名可以自定,数字前缀影响执行顺序)。// server/middleware/01.set-default-lang.ts import { defineEventHandler, getCookie, setCookie } from 'h3' import { $fetch } from 'ofetch' // Nitro 中可以直接用 ofetch // 假设 API 获取逻辑 (实际应更健壮) async function fetchDefaultLanguageCode(): Promise<string | null> { console.log('[Middleware] Fetching default language from API...'); try { // 替换成你的真实 API 地址和逻辑 // const languages = await $fetch('/api/languages'); await new Promise(resolve => setTimeout(resolve, 100)); // 模拟延迟 const languages = [ { code: 'en', name: 'English', default_web: 0 }, { code: 'zh-CN', name: 'Simplified Chinese', default_web: 1 }, ]; const defaultLang = languages.find(lang => lang.default_web === 1); if (defaultLang) { console.log(`[Middleware] API returned default: ${defaultLang.code}`); return defaultLang.code; } console.log('[Middleware] No default language found in API response.'); return 'en'; // 后备值 } catch (error) { console.error('[Middleware] Error fetching default language:', error); return 'en'; // 出错时的后备值 } } export default defineEventHandler(async (event) => { // 只处理页面请求,忽略 API 请求或其他资源请求(根据需要调整) const path = event.node.req.url || ''; if (!path.startsWith('/_nuxt/') && !path.startsWith('/api/') && (path === '/' || path.endsWith('.html') || !path.includes('.'))) { console.log(`[Middleware] Processing request for path: ${path}`); const favLang = getCookie(event, 'fav_lang'); console.log(`[Middleware] Existing 'fav_lang' cookie value: ${favLang}`); if (favLang === undefined || favLang === null || favLang === '') { console.log(`[Middleware] 'fav_lang' cookie not found or empty. Fetching default.`); const defaultLangCode = await fetchDefaultLanguageCode(); if (defaultLangCode) { console.log(`[Middleware] Setting 'fav_lang' cookie to: ${defaultLangCode}`); // 设置 Cookie 到响应头,为下次请求和浏览器准备 setCookie(event, 'fav_lang', defaultLangCode, { path: '/', maxAge: 60 * 60 * 24 * 365, // 一年 sameSite: 'lax', // secure: process.env.NODE_ENV === 'production', // httpOnly: true, }); // 【可选但推荐】将获取到的值存入当前请求上下文, // 供同一请求后续流程(如 useCookie 的 default 工厂)使用 event.context.fetchedDefaultLang = defaultLangCode; console.log(`[Middleware] Stored fetched value in event.context.fetchedDefaultLang`); } } else { console.log(`[Middleware] 'fav_lang' cookie already exists. No action needed.`); } } else { // console.log(`[Middleware] Skipping middleware for path: ${path}`); } })
-
(可选)在
useCookie
中利用event.context
:修改方案一中的
useCookie
,使其优先检查event.context
:// composable 或 setup 中 import { useCookie, useRequestEvent } from '#app'; const favLangCookie = useCookie<string | null>('fav_lang', { async default() { const event = useRequestEvent(); // 获取当前请求事件对象 (仅在 SSR 或 setup 中可用) // 优先检查 middleware 是否已在 context 中存了值 if (process.server && event && event.context.fetchedDefaultLang) { console.log('Using default language from event.context:', event.context.fetchedDefaultLang); return event.context.fetchedDefaultLang; } // 如果 context 没有,再执行原来的 API 调用逻辑 (或只返回后备值) console.log('Cookie or context value not found, proceeding with async default factory fetch (or fallback)...'); if (process.server) { // ... (原来的 API fetch 逻辑) ... // 保持这里的逻辑作为第二道防线,或处理 middleware 未覆盖的情况 } return 'en'; // 或者 null }, // ... 其他 cookie 选项 ... });
优点:
- 执行时机非常早,在 Vue 应用初始化之前。
- 职责分离,将请求的预处理逻辑放到 Middleware 层。
缺点:
- 逻辑分散:Cookie 的设置在 Middleware,读取和使用在 Vue 应用内部,可能还需要
event.context
作为桥梁,比方案一复杂。 - 潜在性能影响:Middleware 是阻塞性的,如果在里面做很慢的 API 调用,会延迟整个页面的响应。要确保 API 调用足够快或做好缓存。
- 对于解决“首次读取是 undefined”的问题,单纯使用 Middleware 设置响应头并不直接解决,还需要配合
event.context
或让useCookie
的default
逻辑感知到 Middleware 的结果。
安全建议:
- 同方案一,注意 Cookie 属性的配置。
- 保护好 Middleware 中调用的 API 端点。
- 避免在 Middleware 中执行过于耗时的操作。
进阶使用技巧:
- 可以创建多个 Middleware 文件,通过文件名前缀的数字来控制它们的执行顺序。
- 在
nitro.config.ts
中可以更精细地配置 Middleware 的应用范围和顺序。 - 对于全局共享的数据,除了
event.context
,还可以考虑使用 Nitro 的缓存或其他状态管理机制。
总的来说,对于“需要在 SSR 首次加载时,根据 API 结果为 useCookie
提供一个默认值”的场景,利用 useCookie
的异步 default
工厂(方案一)是最直接、最符合 Nuxt 3 设计理念、也通常是代码最简洁的解决方案 。它能确保在需要读取 Cookie 值的时候,异步获取默认值的操作已经完成,从而避免了首次访问得到 undefined
的问题。Server Middleware 作为一种备选方案,更适合做一些更通用的请求预处理,但解决这个特定问题时相对迂回一些。