Vue3学习第七课:(Vuex 替代方案)Pinia 状态管理 5 分钟上手

使用第一课中创建的工程

一、实验目标

  1. 掌握 Pinia 的核心概念:state(存储状态)、getters(计算属性)、actions(状态修改)

  2. 学会定义 Pinia Store 并在组件中使用

  3. 掌握 3 种状态修改方式(直接修改、$patchactions

  4. 配置 Pinia 持久化(基于 pinia-plugin-persistedstate),解决刷新丢失问题

  5. 理解 Pinia 跨组件共享的核心原理,能解决项目中的状态共享需求

二、实验步骤

实验结果截图:

登录前

登录后

2.1 安装依赖

bash 复制代码
# 安装 Pinia 核心库
npm install pinia
# 安装持久化插件(解决刷新状态丢失)
npm install pinia-plugin-persistedstate

2.2 初始化 Pinia(全局注册)

src/main.ts 配置:

javascript 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

// 1. 创建 Pinia 实例(TS 自动推断为 Pinia 类型)
const pinia = createPinia()
// 2. 安装持久化插件
pinia.use(piniaPluginPersistedstate)
// 3. 全局注册 Pinia(Vue 实例类型自动适配)
createApp(App).use(pinia).mount('#app')

2.3 定义 Pinia Store

src/stores/user.ts,聚焦登录核心字段和方法,TS 自动推断类型:

html 复制代码
import { defineStore } from 'pinia'

// 极简 Store:无冗余类型,TS 自动推断 + 忽略 persist 类型校验
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '', // 登录令牌
    userInfo: { id: 0, username: '', avatar: 'https://via.placeholder.com/40' }, // 用户信息
    isLogin: false // 登录状态
  }),

  getters: {
    // 简化用户名展示(未登录显示"游客")
    displayName: (state) => state.isLogin ? state.userInfo.username : '游客'
  },

  actions: {
    // 异步登录:模拟接口请求
    async login(username: string, password: string) {
      // 实际项目替换为 axios.post('/api/login', { username, password })
      const res = await new Promise<{ token: string; user: typeof this.userInfo }>((resolve) => {
        setTimeout(() => resolve({
          token: `token-${Date.now()}`,
          user: { id: Math.random() * 1000 | 0, username, avatar: 'https://via.placeholder.com/40' }
        }), 800)
      })

      // 直接修改状态(TS 自动校验类型)
      this.token = res.token
      this.userInfo = res.user
      this.isLogin = true
    },

    // 登出:一键重置
    logout() {
      this.$reset()
    },

    // 刷新用户信息(可选)
    async refreshUserInfo() {
      if (!this.token) return
      const res = await new Promise<typeof this.userInfo>((resolve) => {
        setTimeout(() => resolve({ ...this.userInfo, username: `${this.userInfo.username}-更新` }), 500)
      })
      this.userInfo = res
    }
  },

  // @ts-ignore:忽略类型校验,快速解决 persist 报错(简洁高效)
  persist: {
    key: 'user-state',
    storage: localStorage,
    paths: ['token', 'userInfo', 'isLogin'] // 只持久化核心字段
  }
})

2.4 组件中使用(简洁实战)

2.4.1 登录表单组件 src/components/LoginForm.vue

html 复制代码
<template>
  <div class="login-form" v-if="!userStore.isLogin">
    <h3>用户登录</h3>
    <input
      v-model="username"
      type="text"
      placeholder="输入用户名"
      @keyup.enter="handleLogin"
    />
    <input
      v-model="password"
      type="password"
      placeholder="输入密码"
      @keyup.enter="handleLogin"
    />
    <button @click="handleLogin" :disabled="loading">
      {{ loading ? '登录中...' : '登录' }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '../stores/user'

const userStore = useUserStore()
const username = ref('') // TS 自动推断 string 类型
const password = ref('')
const loading = ref(false)

// 处理登录
const handleLogin = async () => {
  if (!username.value || !password.value) return alert('请输入账号密码')
  
  loading.value = true
  try {
    // 调用 Store 的登录方法(TS 自动提示参数类型)
    await userStore.login(username.value, password.value)
    alert('登录成功!')
    // 登录成功后清空输入
    username.value = ''
    password.value = ''
  } catch (err) {
    alert('登录失败')
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 300px;
  margin: 20px auto;
}
input {
  padding: 8px;
  border: 1px solid #eee;
  border-radius: 4px;
}
button {
  padding: 8px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:disabled {
  background: #ccc;
  cursor: not-allowed;
}
</style>

2.4.2 用户信息展示组件 src/components/UserProfile.vue

html 复制代码
<template>
  <div class="user-profile" v-if="userStore.isLogin">
    <img :src="userStore.userInfo.avatar" alt="用户头像" />
    <div class="info">
      <h4>{{ userStore.displayName }}</h4>
      <p>用户ID: {{ userStore.userInfo.id }}</p>
      <p>Token: {{ userStore.token.slice(0, 10) }}...</p>
    </div>
    <button @click="userStore.logout">登出</button>
    <button @click="userStore.refreshUserInfo" style="margin-left: 8px;">
      刷新用户信息
    </button>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '../stores/user'

// 直接使用 Store,TS 自动提示所有状态和方法
const userStore = useUserStore()
</script>

<style scoped>
.user-profile {
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: 400px;
  margin: 20px auto;
  padding: 16px;
  border: 1px solid #eee;
  border-radius: 8px;
}
img {
  width: 40px;
  height: 40px;
  border-radius: 50%;
}
button {
  padding: 6px 12px;
  background: #f56c6c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:nth-child(4) {
  background: #409eff;
}
</style>

三、核心原理

3.1 TS 类型安全

  • 无需冗余接口,state 的返回值类型由 TS 自动推断,actions 的参数和返回值通过 TS 标注后,调用时自动校验类型;

  • 组件中使用 useUserStore() 时,TS 自动提示所有状态、getters、actions,避免拼写错误。

3.2 跨组件共享

  • Store 是单例模式,所有组件调用 useUserStore() 得到同一个实例,确保状态全局一致;

  • 基于 Vue3 的 reactive 实现响应式,状态变化时依赖组件自动重新渲染。

3.3 持久化原理

  • 插件监听 Store 的 $state 变化,当 tokenuserInfo 等字段修改时,序列化后存入 localStorage

  • 页面刷新时,Store 初始化阶段从 localStorage 读取数据,反序列化后合并到 state,恢复状态。

相关推荐
Java陈序员8 分钟前
一键部署!一款开源自托管的照片画廊神器!
vue.js·docker
AAA阿giao9 分钟前
从“拼字符串”到“魔法响应”:一场数据驱动页面的奇幻进化之旅
前端·javascript·vue.js
donecoding9 分钟前
解决 npm 发布 403 错误:全局配置 NPM Automation Token 完整指南
前端·javascript
小胖霞11 分钟前
vite+ts+monorepo从0搭建vue3组件库(三):开发一个组件
vue.js·前端框架·前端工程化
潜水豆11 分钟前
浅记录一下专家体系
前端
梨子同志12 分钟前
Node.js 事件循环(Event Loop)
前端
北慕阳12 分钟前
背诵-----------------------------
java·服务器·前端
JS_GGbond14 分钟前
Vue3 组件入门:像搭乐高一样玩转前端!
前端·vue.js
SakuraOnTheWay15 分钟前
拆解一个由 setTimeout 引发的“页面假死”悬案
前端·javascript
渔_15 分钟前
【已解决】uni-textarea 无法绑定 v-model / 数据不回显?换原生 textarea 一招搞定!
前端