在简单的应用中,我们可以通过组件之间的“传参”来传递数据。但随着应用变大,会出现以下痛点:
- “套娃”传参(Prop Drilling): 爷爷组件的数据要传给孙子,必须经过爸爸组件,即使爸爸根本不需要这份数据。
- 跨页面同步: 用户在“设置页”改了头像,返回“首页”时,头像得立即更新。
- SSR(服务端渲染)的特殊性:数据脱水与注水: 服务器算好了数据(脱水),得原封不动传给浏览器(注水)。如果没有统一的状态管理,浏览器会重新计算,导致页面闪烁(水合错误)。
- 内存隔离: 传统的全局变量在服务器上是“共享”的。如果不使用 Nuxt 提供的状态工具,A 用户的信息可能会被 B 用户看到。
什么是 useState?
useState是 Nuxt 专门为SSR(服务端渲染) 环境设计的响应式状态管理钩子。
- 它能干什么?
- 在服务器和浏览器之间共享状态。
- 防止状态污染:确保每个用户的请求都有独立的状态,不会把 A 用户的登录信息发给 B 用户。
- 解决水合不匹配 (Hydration Mismatch):确保服务器生成的 HTML 和浏览器渲染的结果完全一致,避免页面闪烁或报错。
- 适用场景:
- 跨组件同步简单的数据(如:用户头像、弹窗开关、当前主题)。
- 替代普通的
ref(),处理那些在 SSR 阶段就需要确定的随机数或时间戳。
水合不匹配
<script setup lang="ts">
// ❌ 错误做法:直接在 SSR 环境下用 ref 配合随机逻辑
const randomNumber = ref(Math.random())
</script>
<template>
<div>
<h2>当前随机数:{{ randomNumber }}</h2>
</div>
</template>
为什么会翻车?
- 服务器端:运行
Math.random()得到了0.123,生成的 HTML 源码是<h2>0.123</h2>。 - 浏览器端:代码下载后重新运行,
Math.random()变成了0.456。 - 水合冲突:浏览器发现 HTML 里的文字是
0.123,但内存里的响应式数据是0.456。
基础用法
基本语法
const state = useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>
其中key是唯一标识符。Nuxt 靠这个 Key 来确保服务器的数据能精准地“投喂”给客户端对应的变量。如果 Key 重复,不同组件会共享同一个状态。
init是可选的,表示初始化函数。仅在状态尚未创建时执行。它必须返回一个初始值。
init** 函数只在“第一次”被需要时运行。**
- 如果服务器已经运行过
init并把值传给了浏览器,浏览器在执行同一行代码时,会直接跳过init** 函数**,直接从NUXT_DATA里拿值。
- 基础初始化
这种写法最标准,适合定义各种响应式变量。
const color = useState('color', () => 'red')
- 只获取不初始化(跨组件通信)
如果你在app.vue里已经初始化了某个状态,在子组件里可以省略第二个参数,直接通过 Key 获取。
// 假设 'user' 已经在别处初始化过了
const user = useState('user')
- 带泛型(显式定义类型)
在 TypeScript 环境下,为了让代码提示更友好,建议加上泛型。
interface User {
name: string
age: number
}
const user = useState<User>('user_info', () => ({
name: '枫枫',
age: 18
}))
单个组件内共享
在组件内定义一个 key,确保数据在两端同步。
<script setup lang="ts">
// 'counter' 是唯一键,防止数据混淆
const counter = useState('counter', () => Math.round(Math.random() * 100))
</script>
<template>
<div>
<p>初始化随机数(SSR 安全):{{ counter }}</p>
<button @click="counter++">增加</button>
</div>
</template>
跨组件共享(全局状态)
利用 Nuxt 的composables目录实现自动导入,达到类似 Vuex/Pinia 的效果。
定义状态:app/composables/useStates.ts
// 定义一个全局的用户状态
export const useUser = () => useState('user', () => ({
nickname: '枫枫',
isLogin: false
}))
组件中使用:
<script setup lang="ts">
const user = useUser() // 自动导入,无需 import
const login = () => {
user.value.isLogin = true
user.value.nickname = '枫枫知道'
}
</script>
<template>
<div>
<div v-if="user.isLogin">欢迎回来:{{ user.nickname }}</div>
<button v-else @click="login">点击登录</button>
</div>
</template>
扩展——在 Nuxt 中使用 Pinia
当你的项目很复杂时,useState的管理成本会变高,这时我们需要Pinia。
直接使用原生 Pinia 在 Nuxt 中会导致单例模式下的内存泄漏和状态污染。@pinia/nuxt 模块会自动为每个 SSR 请求创建独立的 Store 实例。
- 安装模块
npm install @pinia/nuxt
- 注册模块:nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
- 编写 Store:
app/stores/auth.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: 'xxxx',
userInfo: {
name: "fengfeng",
addr: "长沙"
}
}),
actions: {
async fetchUser() {
// 在这里可以写异步登录逻辑
// Nuxt 会在服务器端运行此逻辑并把结果同步给客户端
console.log("fetchUser")
// 如果在action里面请求数据,需要使用$fetch
return $fetch("/api/user")
}
},
getters: {
name: (state) => {
return state.userInfo.name
}
}
})
组件里面使用
<script setup lang="ts">
const authStore = useAuthStore() // 直接用就好,不需要导入
async function getUser(){
const data = await authStore.fetchUser()
console.log("data", data)
}
</script>
<template>
<div>pinia使用</div>
<div>userInfo: {{ authStore.userInfo}}</div>
<div>getter: {{ authStore.name}}</div>
<div> <button @click="getUser">点我</button></div>
</template>
扩展——useCookie
useCookie 是 Nuxt 提供的 SSR 友好的 Cookie 管理工具。
- 它能干什么?
- 在浏览器和服务器之间同步 Cookie。
- 持久化存储:即使用户刷新页面或关闭浏览器,数据依然存在。
- SSR 安全:在服务器端(Server Side)渲染时,它能自动读取请求头里的 Cookie;在客户端(Client Side)又能通过 JS 读写。
- 使用场景:
- 存储用户的Token(登录凭证)。
- 存储用户的个性化配置(如:语言设置、深色/浅色模式、侧边栏折叠状态)。
基础读写
<script setup lang="ts">
// 创建一个名为 'counter' 的 cookie,默认值 0
const counter = useCookie('counter', { default: () => 0 })
const add = () => counter.value++
</script>
<template>
<div>
<h1>计数器(刷新不重置):{{ counter }}</h1>
<button @click="add">+1</button>
<button @click="counter = 0">重置</button>
</div>
</template>
与 useState 联动
// app/composables/useAuth.ts
export const useToken = () => {
// 1. 定义一个 cookie
const tokenCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 7, // 设置有效期 7 天
path: '/'
})
// 2. 用 useState 包装,确保全应用响应式同步
const token = useState('token', () => tokenCookie.value)
// 3. 监听 token 变化,自动同步回 cookie
watch(token, (newToken) => {
tokenCookie.value = newToken
})
return token
}
useCookie的配置项
| 配置项 | 作用 | 推荐值 |
|---|---|---|
maxAge |
Cookie 有效期(秒) | 60 * 60 * 24 * 7 (一周) |
expires |
到期具体时间 | Date 对象 |
httpOnly |
是否禁止 JS 访问 | 存储 Token 时建议后端设置,前端useCookie 无法直接设置 true |
secure |
是否仅在 HTTPS 下传输 | 生产环境建议为true |
watch |
自动监听并同步状态 | 默认为true或'shallow' |
在 Nuxt 开发中,我们通常根据复杂程度选择不同的管理方式:
| 层次 | 技术工具 | 使用场景 |
|---|---|---|
| 局部状态 | ref()/reactive() |
仅在单个.vue 文件内部使用(如:输入框内容)。 |
| 简单全局状态 | useState() |
跨页面/组件共享,且需要兼容 SSR(如:当前用户信息、主题开关)。 |
| 复杂全局状态 | Pinia | 大型项目、有复杂的异步逻辑、需要 DevTools 调试(如:购物车系统、权限控制)。 |
| 持久化状态 | useCookie() |
刷新页面或关闭浏览器后仍需保留的数据(如:登录 Token)。 |