使用第一课中创建的工程
一、实验目标
-
掌握 Pinia 的核心概念:
state(存储状态)、getters(计算属性)、actions(状态修改) -
学会定义 Pinia Store 并在组件中使用
-
掌握 3 种状态修改方式(直接修改、
$patch、actions) -
配置 Pinia 持久化(基于
pinia-plugin-persistedstate),解决刷新丢失问题 -
理解 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变化,当token、userInfo等字段修改时,序列化后存入localStorage; -
页面刷新时,Store 初始化阶段从
localStorage读取数据,反序列化后合并到state,恢复状态。