智能响应式网站建设南宁网站建设服务公司
前文vuex 过于详细,现在还是应该用pinia
看一眼下图基本明白它的流程了吧
使用步骤:
Pinia 使用步骤:以登录功能为例
Pinia 是 Vue.js 的新一代状态管理库,使用起来非常简单。下面我以 用户登录 功能为例,演示 Pinia 的基本使用步骤:
一、安装 Pinia
npm install pinia
二、创建并配置 Pinia 实例
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'const pinia = createPinia()
const app = createApp(App)app.use(pinia) // 注册 Pinia
app.mount('#app')
我们项目是这样创建的:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import ElementPlus from 'element-plus';
import 'element-plus/theme-chalk/index.css';
import App from './App.vue'
import Axios from 'axios'const app=createApp(App)
app.use(router)
app.use(createPinia())
app.use(ElementPlus)
app.mount('#app')
三、定义用户 Store(核心)
例子1:
// store/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'export const useUserStore = defineStore('user', () => {// 1. 状态:存储用户数据const user = ref(null) // 用户信息const isLoggedIn = ref(false) // 登录状态// 2. getters:计算属性(获取派生状态)const username = computed(() => user.value?.username || '')// 3. actions:修改状态的方法const login = async (credentials) => {try {// 模拟 API 请求const response = await fetch('/api/login', {method: 'POST',body: JSON.stringify(credentials)})const data = await response.json()// 更新状态user.value = data.userisLoggedIn.value = truereturn true // 登录成功} catch (error) {console.error('登录失败:', error)return false}}const logout = () => {user.value = nullisLoggedIn.value = false}return {user,isLoggedIn,username,login,logout}
})
-
Store 结构:
state
:用ref
定义响应式数据(如user
、isLoggedIn
)getters
:用computed
定义计算属性(如username
)actions
:定义修改状态的方法(如login
、logout
)
-
响应式原理:
- 当
user
或isLoggedIn
变化时,所有依赖它们的组件会自动更新。
- 当
Pinia 登录与登出逻辑
这段代码是 Pinia store 中处理用户认证的核心逻辑:
一、登录逻辑:login
方法
1. 整体功能
这是一个 异步方法,负责处理用户登录流程:
- 发送登录请求到后端 API
- 验证用户凭证
- 保存用户信息和登录状态
- 返回登录结果(成功 / 失败)
2. 参数与结构
const login = async (credentials) => { ... }
credentials
:用户输入的凭证,通常是{ username, password }
对象
const login = async (credentials) => {try {// 1. 发送 POST 请求到登录 APIconst response = await fetch('/api/login', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(credentials) // 将凭证转为 JSON 格式})// 2. 解析响应数据const data = await response.json()// 3. 更新状态user.value = data.user // 保存用户信息(如 id, username, token)isLoggedIn.value = true // 标记为已登录return true // 返回成功标志} catch (error) {console.error('登录失败:', error)return false // 返回失败标志}
}
4. 关键细节
-
API 请求:
- 使用
fetch
发送 POST 请求 - 需设置
Content-Type: application/json
头 - 用
JSON.stringify
将对象转为 JSON 字符串
- 使用
-
错误处理:
- 使用
try/catch
捕获网络错误或解析错误 - 可能的错误包括:网络超时、服务器错误、JSON 解析失败等
- 使用
-
状态更新:
user.value
存储完整用户信息(如{ id: 1, username: 'test', token: 'xxx' }
)isLoggedIn.value
作为登录状态标志,方便其他组件快速判断
二、登出逻辑:logout
方法
1. 整体功能
清除用户信息和登录状态,实现用户退出登录。
2. 核心步骤
const logout = () => {user.value = null // 清空用户信息isLoggedIn.value = false // 标记为未登录
}
3. 扩展应用
在实际项目中,可能还需要:
- 清除本地存储中的 token
- 调用后端的登出 API(如使 token 失效)
- 跳转到登录页或首页
可以这样扩展:
const logout = async () => {try {// 调用后端登出 APIawait fetch('/api/logout', { method: 'POST' })} catch (error) {console.warn('后端登出失败,但继续清除前端状态', error)} finally {// 清除状态user.value = nullisLoggedIn.value = false// 清除本地存储localStorage.removeItem('auth_token')// 跳转到登录页(需引入路由)router.push('/login')}
}
三、这个设计的优势
-
职责分离:
- 登录逻辑集中在 store 中,组件只需调用
login()
方法,无需关心具体实现
- 登录逻辑集中在 store 中,组件只需调用
-
响应式更新:
- 当
user
或isLoggedIn
变化时,所有依赖它们的组件会自动更新
- 当
-
可测试性:
- 可以轻松模拟 API 响应,测试登录成功 / 失败的场景
-
扩展性:
- 可以方便地添加额外功能(如 token 刷新、登录状态持久化)
四、常见问题及解决方案
问题 1:登录成功后页面未更新
原因:可能忘记在组件中使用响应式数据
解决方案:确保在组件中正确引入 store:
const userStore = useUserStore()
const { isLoggedIn } = storeToRefs(userStore) // 使用 storeToRefs 保持响应式
问题 2:跨页面刷新后登录状态丢失
解决方案:添加持久化插件(如 pinia-plugin-persistedstate
):
export const useUserStore = defineStore('user', {// ...原有代码
}, {persist: true // 自动持久化到 localStorage
})
问题 3:如何处理 token 过期?
解决方案:在请求拦截器中检测 token 状态,过期时自动刷新:
// 假设使用 axios
axios.interceptors.response.use(response => response,async error => {if (error.response.status === 401) { // token 过期const userStore = useUserStore()await userStore.refreshToken() // 刷新 tokenreturn axios.request(error.config) // 重试原请求}return Promise.reject(error)}
)
业务流程:
- 登录:验证凭证 → 保存用户信息 → 标记登录状态
- 登出:清除用户信息 → 标记未登录状态
我们遵循了 Pinia 的设计理念:将状态管理逻辑与组件分离,使代码更易维护和测试。
这里,const user = ref(null)
在初始状态下没有用户数据,但这正是 Pinia/Vue 中管理异步数据的常见方式。
一、为什么初始值是 null
?
-
表示 “未加载” 状态
在用户登录前或数据未从后端获取时,user
为空是合理的。此时组件可以显示 “加载中” 或 “请登录” 的提示。 -
避免空值错误
使用ref(null)
而非直接null
是为了保持响应式。当数据加载完成后,修改user.value
会触发所有依赖它的组件更新。 -
类型安全
在 TypeScript 中,ref(null)
可以明确类型(如ref<User | null>(null)
),避免运行时类型错误。
二、数据何时被填充?
当用户登录成功后,login
action 会更新 user
的值:
const login = async (credentials) => {try {const response = await fetch('/api/login')const data = await response.json()// 登录成功后,填充用户数据user.value = data.user // <-- 这里更新了 userisLoggedIn.value = truereturn true} catch (error) {return false}
}
三、如何在组件中安全使用?
虽然初始值是 null
,但可以通过 可选链操作符(?.) 或 计算属性 安全访问:
1. 直接使用可选链
<template><div v-if="userStore.user">欢迎,{{ userStore.user.username }}</div><div v-else>请先登录</div>
</template>
2. 使用计算属性提供默认值
const username = computed(() => user.value?.username || '未登录')// 在组件中使用
<p>{{ username }}</p> // 永远不会报错,要么显示用户名,要么显示“未登录”
第二个例子:我们例子项目的定义如下:
import { defineStore } from 'pinia'
import { ref ,computed } from 'vue'
import { postReq } from '../utils/api'//用户仓库 提供数据、计算属性、方法
export const useUserStore=defineStore("user",()=>{//ref = state //存放用户登录后的信息const userInfo=ref({id:"",token:"",isAuth:false,})const isLogin=ref(false)//computed() = getters//返回用户登录信息const getUserInfo = computed(()=>{console.log("getUserInfo:start")//pinia存储的用户信息和本地localStorageconsole.log(userInfo.value.isAuth);console.log(JSON.parse(localStorage.getItem("isAuth")));if(userInfo.value.isAuth==false&& JSON.parse(localStorage.getItem("isAuth")) ){console.log("getUserInfo:更新用户信息")setAuthenticated(JSON.parse(localStorage.getItem("user")))}console.log("getUserInfo:end")return userInfo;}) //function() = actions//设置登录状态,获取登录信息const setAuthenticated=(u)=>{console.log("setAuthenticated:start")userInfo.value=u;//同步localStorageconsole.log(u.useType)localStorage.setItem("user",JSON.stringify(u));localStorage.setItem("token",u.token);localStorage.setItem("isAuth",u.isAuth);console.log(`setAuthenticated:${userInfo.value.isAuth}`)console.log("setAuthenticated:end")}// const setLoginState(f)=>{// isLogin.value=f// }//刷新tokenconst refreshToken=()=>{// refreshTokenApi().then(response=>{// userStore.setAuthenticated({// token:response.data.access_token,// isAuth:true// })// })}//退出登录const logOut=()=>{console.log("logOut:start")userInfo.value.isAuth=falseuserInfo.value.token=''//LocalStorage:除非主动删除,否则会永久存储在浏览器中。localStorage.setItem("user",'');localStorage.setItem("token",'');localStorage.setItem("isAuth",false);//SessionStorage:只在当前所在窗口关闭前有效,窗口关闭后其存储数据也就会被自动清除。sessionStorage.clear() console.log("logOut:end")}return {userInfo,getUserInfo,setAuthenticated,logOut}
})
这是一个基于 Pinia 的用户认证模块,包含了用户状态管理和认证流程。下面我将详细说明其中的 actions(即修改状态的方法):
一、核心 Action 功能解析
1. setAuthenticated(u)
作用:设置用户认证状态并同步到本地存储
参数:u
(包含用户信息的对象,如 { id, token, isAuth }
)
const setAuthenticated = (u) => {console.log("setAuthenticated:start")userInfo.value = u; // 更新 Pinia 中的用户信息// 同步到 localStorage(持久化存储)localStorage.setItem("user", JSON.stringify(u));localStorage.setItem("token", u.token);localStorage.setItem("isAuth", u.isAuth);console.log(`setAuthenticated:${userInfo.value.isAuth}`)console.log("setAuthenticated:end")
}
关键逻辑:
- 将用户信息保存到 Pinia 状态
- 同时保存到
localStorage
,确保刷新页面后数据不丢失 - 通常在登录成功后调用,例如:
// 登录成功后 const userData = { id: 1, token: 'xxx', isAuth: true } setAuthenticated(userData)
2. logOut()
作用:用户退出登录,清除认证状态和本地存储
const logOut = () => {console.log("logOut:start")// 清除 Pinia 中的用户认证状态userInfo.value.isAuth = falseuserInfo.value.token = ''// 清除 localStorage 中的数据localStorage.setItem("user", '');localStorage.setItem("token", '');localStorage.setItem("isAuth", false);// 清除 sessionStorage(通常用于临时数据)sessionStorage.clear()console.log("logOut:end")
}
关键逻辑:
- 将
isAuth
标记为false
,表示未登录 - 清空 token 和用户信息
- 清除
localStorage
和sessionStorage
中的相关数据 - 通常在用户点击 "退出登录" 按钮时调用
3. refreshToken()
(未完成)
作用:刷新用户 token(用于保持会话,避免频繁登录)
const refreshToken = () => {// 未完成的实现// refreshTokenApi().then(response => {// userStore.setAuthenticated({// token: response.data.access_token,// isAuth: true// })// })
}
完整实现示例:
const refreshToken = async () => {try {// 调用后端刷新 token 的 APIconst response = await postReq('/api/refresh-token', {token: userInfo.value.token // 发送当前 token})// 更新用户信息(主要是新的 token)setAuthenticated({...userInfo.value,token: response.data.token,isAuth: true})return true} catch (error) {console.error('刷新 token 失败:', error)// 刷新失败时可以跳转到登录页logOut()return false}
}
二、这些 Actions 的协作流程
1. 登录流程:
- 组件调用登录 API(如
postReq('/login')
) - 登录成功后,获取用户信息和 token
- 调用
setAuthenticated
保存用户信息和 token
// 组件中的登录逻辑示例
async handleLogin() {const response = await postReq('/login', this.formData)if (response.success) {setAuthenticated(response.data.user) // 保存用户信息router.push('/dashboard') // 跳转到主页}
}
2. 登出流程:
- 组件调用
logOut()
action - 清除所有认证状态和存储数据
- 跳转到登录页
// 组件中的登出按钮
<button @click="logOut">退出登录</button>// 组件逻辑
import { useUserStore } from '@/store/user'setup() {const userStore = useUserStore()const logOut = () => {userStore.logOut()router.push('/login')}return { logOut }
}
3. 自动刷新 token:
当 API 请求返回 "token 过期" 错误时,自动调用 refreshToken
:
// API 拦截器示例(通常在 axios 中设置)
axios.interceptors.response.use(response => response,async error => {if (error.response.status === 401) { // token 过期const userStore = useUserStore()const refreshed = await userStore.refreshToken()if (refreshed) {// 刷新成功后,重试原请求return axios(error.config)} else {// 刷新失败,跳转到登录页router.push('/login')}}return Promise.reject(error)}
)
三、代码优化
- 统一存储操作:
可以创建工具函数封装localStorage
操作,避免重复代码:
// utils/storage.js
export const setStorage = (key, value) => {localStorage.setItem(key, JSON.stringify(value))
}export const getStorage = (key) => {return JSON.parse(localStorage.getItem(key))
}export const removeStorage = (key) => {localStorage.removeItem(key)
}
其他需要完善的地方:
-
完善
refreshToken
实现:
补充刷新 token 的具体逻辑,处理可能的错误。 -
添加类型定义(如果使用 TypeScript):
为userInfo
和相关方法添加类型,提高代码安全性。 -
错误处理:
在setAuthenticated
和logOut
中添加错误处理,确保操作安全。
流程:
- 登录:通过
setAuthenticated
保存用户信息 - 登出:通过
logOut
清除所有认证数据 - token 管理:通过
refreshToken
保持会话
它们的设计遵循了 Pinia 的最佳实践:
- 将状态修改逻辑集中在 actions 中
- 通过响应式机制自动更新依赖组件
- 结合本地存储实现数据持久化
Pinia Getter :计算派生状态
例子代码中,getters
部分定义了一个计算属性 username
,用于从用户状态中派生数据:
一、Getter 的本质:Vuex/Pinia 中的计算属性
在 Vuex/Pinia 中,getters 类似于 Vue 组件中的 computed 属性,主要用于:
- 简化复杂状态访问:避免在组件中重复编写复杂的状态获取逻辑
- 缓存计算结果:只有依赖的状态变化时才重新计算,提高性能
- 派生新状态:从现有状态计算出新的值(如过滤、转换格式等)
二、代码中的 Getter 分析
1. username
计算属性
const username = computed(() => user.value?.username || '')
作用:
- 安全地获取当前用户的用户名
- 当用户未登录(
user.value
为null
)时,返回空字符串''
- 当用户登录后,自动返回
user.value.username
的值
关键点:
- 使用 可选链操作符(?.) 避免
user.value
为null
时的错误 - 使用
|| ''
提供默认值,确保返回值类型始终为字符串 - 依赖于
user
状态的变化,当user
更新时自动重新计算
三、Getter 的使用场景
1. 在组件中直接使用
<template><div v-if="isLoggedIn">欢迎回来,{{ username }}!</div><div v-else>请先登录</div>
</template><script>
import { useUserStore } from '@/store/user'export default {setup() {const userStore = useUserStore()return {isLoggedIn: userStore.isLoggedIn,username: userStore.username // 直接使用 getter}}
}
</script>
2. 在其他 Getter 中复用
// 在同一个 store 中定义另一个 getter
const welcomeMessage = computed(() => {if (isLoggedIn.value) {return `欢迎回来,${username.value}!`} else {return '请登录以继续'}
})
四、Getter 的优势
-
代码复用:
多个组件可以共享同一个 getter,避免重复编写相同的状态处理逻辑。 -
安全性:
通过可选链和默认值,避免在状态未初始化时出现错误。例如:// 错误:直接访问可能为 null 的属性 console.log(user.value.username) // 当 user 为 null 时会报错// 安全:通过 getter 访问 console.log(username.value) // 始终返回字符串('' 或实际用户名)
-
性能优化:
计算结果会被缓存,只有依赖的状态(user
)变化时才会重新计算。
五、扩展:更多 Getter 示例
1. 复杂计算
// 计算用户全名(假设 user 有 firstName 和 lastName)
const fullName = computed(() => {return user.value ? `${user.value.firstName} ${user.value.lastName}` : '未登录用户'
})
2. 过滤列表
// 假设 store 中有一个 todos 数组
const completedTodos = computed(() => {return todos.value.filter(todo => todo.completed)
})
3. 带参数的 Getter
// 通过 ID 获取用户(返回一个函数)
const getUserById = computed(() => {return (id) => users.value.find(user => user.id === id)
})// 使用方式
const user = getUserById.value(123)
例子2中getter的使用:
getUserInfo
是唯一的 getter,它的设计很特别:不仅返回用户信息,还会在必要时从本地存储恢复状态。这是一个处理状态持久化的典型实现。
一、getUserInfo
的核心功能
const getUserInfo = computed(() => {console.log("getUserInfo:start")// 检查 Pinia 中的状态与 localStorage 是否不一致console.log(userInfo.value.isAuth);console.log(JSON.parse(localStorage.getItem("isAuth")));// 如果 Pinia 中未认证,但 localStorage 显示已认证if (userInfo.value.isAuth == false && JSON.parse(localStorage.getItem("isAuth"))) {console.log("getUserInfo:更新用户信息")setAuthenticated(JSON.parse(localStorage.getItem("user")))}console.log("getUserInfo:end")return userInfo; // 返回整个 userInfo 对象
})
核心逻辑:
-
状态同步检查:
比较userInfo.isAuth
(Pinia 中的状态)和localStorage.isAuth
(本地存储的状态)。 -
自动恢复状态:
如果发现 Pinia 中的状态丢失(如刷新页面后),但本地存储中仍有用户信息,则自动调用setAuthenticated
恢复状态。 -
返回完整用户信息:
无论是否需要恢复,最终都返回最新的userInfo
对象。
二、为什么需要这样设计?
1. 解决页面刷新后状态丢失的问题
Vuex/Pinia 的状态默认存储在内存中,页面刷新后会重置。而 localStorage
可以持久化保存数据。
这个 getter 的设计确保了:
- 用户刷新页面后,首次访问
getUserInfo
时会自动恢复之前的登录状态 - 组件可以始终获取到最新的用户信息,无需手动处理状态恢复
2. 保持状态一致性
通过在 getter 中添加同步逻辑,确保:
- 无论何时访问
getUserInfo
,得到的都是最新且一致的状态 - 避免了在每个组件中重复编写状态恢复逻辑,实现了逻辑复用
三、潜在问题与优化建议
1. 性能问题
每次访问 getUserInfo
都会读取 localStorage
,可能影响性能。
优化方案:
可以添加一个标志位,只在初始化时检查一次:
const isInitialized = ref(false)const getUserInfo = computed(() => {if (!isInitialized.value) {const localAuth = JSON.parse(localStorage.getItem("isAuth"))if (!userInfo.value.isAuth && localAuth) {setAuthenticated(JSON.parse(localStorage.getItem("user")))}isInitialized.value = true}return userInfo
})
2. 类型安全问题
直接从 localStorage
解析的数据可能不符合预期格式。
优化方案:
添加类型检查和默认值:
const storedUser = JSON.parse(localStorage.getItem("user") || "{}")
setAuthenticated({id: storedUser.id || "",token: storedUser.token || "",isAuth: storedUser.isAuth || false
})
3. 代码冗余问题
当前设计中,isLogin
状态与 userInfo.isAuth
重复。
这种在 getter 中添加副作用(如状态恢复)的做法并不常见,但在处理持久化状态时是一种有效的模式。如果使用 pinia-plugin-persistedstate
插件,这些逻辑可以被自动处理,代码会更简洁。
四、在组件中使用 Store
1. 登录表单组件
<!-- LoginForm.vue -->
<template><div><h2>登录</h2><input v-model="form.username" placeholder="用户名" /><input v-model="form.password" type="password" placeholder="密码" /><button @click="handleLogin">登录</button></div>
</template><script>
import { useUserStore } from '@/store/user'
import { ref } from 'vue'export default {setup() {const userStore = useUserStore()const form = ref({username: '',password: ''})const handleLogin = async () => {const success = await userStore.login(form.value)if (success) {console.log('登录成功')// 跳转到首页或其他操作} else {console.log('登录失败')}}return {form,handleLogin}}
}
</script>
2. 导航栏组件(显示登录状态)
<!-- Navbar.vue -->
<template><nav><div v-if="userStore.isLoggedIn">欢迎,{{ userStore.username }}<button @click="userStore.logout">退出</button></div><div v-else><button @click="goToLogin">登录</button></div></nav>
</template><script>
import { useUserStore } from '@/store/user'export default {setup() {const userStore = useUserStore()const goToLogin = () => {// 跳转到登录页}return {userStore,goToLogin}}
}
</script>
使用方式:
-
- 通过
useUserStore()
获取 store 实例 - 直接调用 actions 修改状态
- 通过 store 属性获取状态或计算属性
- 通过
六、扩展:添加持久化优化
如果需要在页面刷新后保留登录状态,可以添加 pinia-plugin-persistedstate
:
npm install pinia-plugin-persistedstate
修改 store/user.js
:
export const useUserStore = defineStore('user', () => {// ... 原有代码}, {// 启用持久化persist: {key: 'user-store',storage: localStorage,}
})
总结
Pinia 的使用非常直观:
- 定义 Store:用
defineStore
创建数据仓库 - 添加状态:用
ref
定义数据 - 添加计算属性:用
computed
定义派生数据 - 添加方法:用普通函数定义修改状态的逻辑
- 在组件中使用:通过
useStore
获取实例并调用属性和方法
相比 Vuex,Pinia 代码更简洁,类型支持更好,是 Vue 3 项目的首选状态管理方案。
下图从特定角度对比vuex 与pinia: