nuxt状态管理
在简单的应用中,我们可以通过组件之间的“传参”来传递数据。但随着应用变大,会出现以下痛点: “套娃”传参(Prop Drilling): 爷爷组件的数据要传给孙子,必须经过爸爸组件,即使爸爸根本不

nuxt状态管理

发布时间:2026-04-21 (51分钟前)

在简单的应用中,我们可以通过组件之间的“传参”来传递数据。但随着应用变大,会出现以下痛点:

  • “套娃”传参(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>

为什么会翻车?

  1. 服务器端:运行Math.random()得到了0.123,生成的 HTML 源码是<h2>0.123</h2>
  2. 浏览器端:代码下载后重新运行,Math.random()变成了0.456
  3. 水合冲突:浏览器发现 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里拿值。
  1. 基础初始化

这种写法最标准,适合定义各种响应式变量。

const color = useState('color', () => 'red')
  1. 只获取不初始化(跨组件通信)

如果你在app.vue里已经初始化了某个状态,在子组件里可以省略第二个参数,直接通过 Key 获取。

// 假设 'user' 已经在别处初始化过了
const user = useState('user')
  1. 带泛型(显式定义类型)

在 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 实例。

  1. 安装模块
npm install @pinia/nuxt
  1. 注册模块:nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
})
  1. 编写 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)。