概念看懂了,一到真实项目还是不知道怎么下手?这篇用一个最典型的后台场景——登录态管理,把 Pinia 的三个核心用法串起来:store 存登录态、持久化保证刷新不丢、路由守卫做权限拦截。改完你就知道真实项目里 store 是怎么组织、怎么跟路由配合的。
前置:Pinia 的 state / getters / actions、Options 与 Setup 两种写法、插件机制,见 Pinia 状态管理详解;路由守卫语法见 Vue Router 路由详解。本文基于 Pinia 4 + Vue 3。
一、需求拆解:登录态为什么这么麻烦
BinMaker 的后台系统有一个很常见的需求:登录一次,刷新页面不掉线;没登录的人访问任何页面,都弹回登录页。拆开看其实三个问题:
| 问题 | 解决方案 |
|---|---|
| 登录状态(token、用户信息)放哪 | 一个 user store,全局共享 |
| 刷新页面为什么就丢了 | store 在内存里,刷新就清空 → 持久化到 localStorage |
| 怎么拦住没登录的人 | 路由守卫读 store,没 token 就重定向到 /login |
二、user store:登录态的「唯一真相」
先定义 store。token、用户信息、权限点都放这里,任何组件都能读:
// stores/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
token: '', // 登录凭证
profile: null, // 用户信息 { name, roles }
}),
getters: {
isLoggedIn: (state) => !!state.token, // 有没有 token,就是「登没登录」
isAdmin: (state) => state.profile?.roles?.includes('admin'),
},
actions: {
async login(username, password) {
// 调登录接口(fetch 换成你的 axios 实例)
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
const data = await res.json();
this.token = data.token; // 存 token
this.profile = data.profile; // 存用户信息
},
async fetchProfile() {
const res = await fetch('/api/profile', {
headers: { Authorization: `Bearer ${this.token}` },
});
this.profile = await res.json();
},
logout() {
this.token = '';
this.profile = null;
},
},
});组件里用起来非常简单:登录页调 userStore.login(...),退出登录调 userStore.logout(),侧边栏判断 userStore.isAdmin 决定显不显示管理菜单——数据变了,所有组件自动跟着变(这就是 Vue 快速入门 说的数据驱动视图)。
三、持久化:刷新不丢
store 是内存对象,刷新页面就没了——所以刷新后 token 要能从 localStorage 恢复。两种做法:
方案 A:官方插件(一行配置)
// 安装插件
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); // 必须先 use,再 use(app)// store 里声明要持久化
export const useUserStore = defineStore('user', {
state: () => ({ token: '', profile: null }),
persist: {
key: 'eam-user', // 存 localStorage 的 key
pick: ['token'], // 只存 token,profile 刷新后重新拉
},
// ...actions
});方案 B:手写插件(不装依赖)
Pinia 插件机制(详解篇讲过)就是干这个的——一个插件给所有 store 加持久化:
// plugins/persist.js —— 手写 localStorage 持久化
export function persistPlugin({ store }) {
const key = `store-${store.$id}`;
// 创建时从 localStorage 恢复
const saved = localStorage.getItem(key);
if (saved) store.$patch(JSON.parse(saved));
// 每次 state 变化自动存;flush:'sync' 同步写入,页面关闭瞬间不丢最后一次更新
store.$subscribe(
(_mutation, state) => {
localStorage.setItem(key, JSON.stringify(state));
},
{ flush: 'sync' }
);
}注册方式和方案 A 一样:pinia.use(persistPlugin)——实测确认:pinia.use() 注册的插件,在 app.use(pinia) 安装之前创建的 store 不会执行(只进待安装队列),所以 main.js 里的顺序必须是「先 pinia.use(插件) → 再 app.use(pinia) → 之后创建的 store 插件才生效」。这也解释了为什么有的项目里持久化「偶尔生效偶尔不生效」——store 在插件生效前被提前创建了。
pick 白名单是必须有的意识:token 应该持久化,但 profile(用户信息)刷新后应该重新调接口拉——因为用户权限可能已经变了,用旧缓存反而出错。这也是方案 A 里 pick: ['token'] 的意义。
四、路由守卫:没登录就拦
路由守卫里读 store,判断放行还是重定向:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '../stores/user';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('../views/Login.vue') },
{
path: '/assets',
component: () => import('../views/AssetList.vue'),
meta: { requiresAuth: true }, // 标一个「需要登录」
},
],
});
router.beforeEach((to) => {
const user = useUserStore(); // 守卫里直接拿 store
if (to.meta.requiresAuth && !user.isLoggedIn) {
return { path: '/login', query: { redirect: to.fullPath } }; // 拦回登录页,记住来源
}
if (to.path === '/login' && user.isLoggedIn) {
return { path: '/' }; // 已登录的人不用再看登录页
}
});
export default router;守卫逻辑就两条规则,配合 meta.requiresAuth 标记按需加到页面上:
| 场景 | 结果 |
|---|---|
| 未登录访问需要登录的页面 | 重定向 /login,带 redirect 参数(登录后跳回来) |
| 已登录访问 /login | 重定向首页 |
| 其余 | 放行 |
⚠️ 踩坑点:守卫里 useStore 报错。如果
useUserStore()在 Pinia 还没安装时就调用,会抛getActivePinia was called with no active Pinia。解决:main.js 里先createPinia()并app.use(pinia),再挂 router(或路由守卫在导航触发时才执行,此时 Pinia 早已装好)。
五、完整链路:登录 → 刷新 → 再访问
把三块串起来,整个生命周期是这样的:
- 登录:登录页 →
user.login()调接口 → token/profile 进 store → 持久化插件写入 localStorage → 跳转首页 - 刷新:内存清空 → store 重建时从 localStorage 恢复 token → 守卫读
isLoggedIn放行 → 页面再调fetchProfile()拉最新用户信息 - 退出:
user.logout()清空 store → 持久化同步清掉 localStorage → 守卫拦截后续访问
main.js 的组装顺序(顺序错了就踩上面那个坑):
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); // ① 先装持久化插件
app.use(pinia); // ② 再装 pinia
app.use(router); // ③ 最后挂路由(守卫里就能安全 useStore)
app.mount('#app');总结
| 需求 | 手法 |
|---|---|
| 登录态共享 | user store(token / profile / isLoggedIn / login / logout) |
| 刷新不丢 | 持久化插件,pick 白名单只存 token |
| 权限拦截 | 路由守卫读 store + meta.requiresAuth 标记 |
| 组装顺序 | 持久化插件 → pinia → router |
进阶方向:动态权限(登录后按角色注册路由)、多 store 协作(user + permission + app 各司其职)、action 里组合多个 store——这些在 Pinia 状态管理详解 的「store 之间怎么通信」和 Vue Router 路由详解 的「动态路由」有对应展开。
链式导航:Pinia 概念与插件机制基础见 Pinia 状态管理详解;路由守卫完整语法(beforeEach、redirect 参数、meta 字段)见 Vue Router 路由详解;store 里 ref 解构保响应性的坑见 Pinia 状态管理详解 的 storeToRefs 小节;这套登录流程的模板语法基础在 Vue 快速入门。
