jeecgboot vue Pinia 拆分01

Pinia入门

  • localStorage:永久本地存储(关闭浏览器还在),存 token 首选(你项目当前方案)
  • sessionStorage:浏览器标签关闭失效
  • Cookie :自动随请求带到后端、有过期时间,一般存少量鉴权字段,不做 Pinia 状态持久化首选

Pinia只存内存,刷新页面全部清空,官方没内置 localStorage/cookie 自动存储逻辑,必须二选一:

插件自动封装(主流:pinia-plugin-persistedstate),一行开启自动持久化,不用手动写任何存 localStorage 代码

java 复制代码
// user.ts只用加 persist:true,插件自动监听state变化→自动存localStorage,刷新自动回填token/userInfo
export const useUserStore = defineStore('app-user',{
  state:()=>({token:undefined,userInfo:null}),
  persist:true, // 开启自动持久化
  actions:{
    setToken(info){
      this.token = info 
      // 不用再写 setAuthCache!插件自动同步localStorage
    }
  }
})

Pinia 是 Vue 官方推荐的新一代状态管理库 ,替代 Vuex,核心优势:简洁、TS 友好、模块化、无需 mutations、支持组合式 API

npm install pinia

复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia()) // 注册 Pinia
app.mount('#app')

定义 Store(模块化)

java 复制代码
// 1. 导入定义 Store 的方法
import { defineStore } from 'pinia'

// 2. 定义 Store(唯一ID + 配置)
export const useUserStore = defineStore('user', {
  // 状态数据(等价于 Vuex state)
  state: () => ({
    name: '张三',
    age: 18,
    token: ''
  }),

  // 计算属性(等价于 Vuex getters)
  getters: {
    // 自动接收 state 作为参数
    doubleAge: (state) => state.age * 2,
    // 也可以用 this 访问整个 store
    getName() {
      return `我的名字:${this.name}`
    }
  },

  // 方法(等价于 Vuex actions + mutations,直接修改 state)
  actions: {
    updateName(name) {
      this.name = name // 直接修改,不需要 mutation
    },
    updateAge(age) {
      this.age = age
    },
    // 支持异步
    async login() {
      const res = await fetch('/api/login')
      const data = await res.json()
      this.token = data.token
    }
  }
})
html 复制代码
<template>
  <div>
    <p>名字:{{ userStore.name }}</p>
    <p>年龄:{{ userStore.age }}</p>
    <p>双倍年龄:{{ userStore.doubleAge }}</p>
    <button @click="userStore.updateName('李四')">修改名字</button>
  </div>
</template>

<script setup>
// 导入 Store
import { useUserStore } from '@/stores/user'

// 拿到 Store 实例
const userStore = useUserStore()
</script>

解构状态(保持响应式)

直接解构会丢失响应式,必须用 storeToRefs

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

// 正确:响应式解构
const { name, age, doubleAge } = storeToRefs(userStore)
// 方法直接解构
const { updateName } = userStore

小程序页面刷新 / 切换不会丢失状态,但冷启动会重置 必须做本地持久化 (如存入 uni.setStorageSync

npm install pinia-plugin-persistedstate

java 复制代码
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate' // 持久化
import App from './App.vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  
  pinia.use(persistedstate) // 注册持久化
  app.use(pinia)
  
  return { app }
}
java 复制代码
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null
  }),
  // 小程序必须加:持久化到本地存储
  persist: {
    // 存储 key
    key: 'user-store',
    // 使用 uniapp 存储 API
    storage: {
      getItem: uni.getStorageSync,
      setItem: uni.setStorageSync
    }
  }
})

项目开始

入口
复制代码
let app: Nullable<App<Element>> = null;
let store: Nullable<Pinia> = null;
复制代码
export function setupStore($app: App<Element>) {
  if (store == null) {
    store = createPinia();
  }
  app = $app;
  app.use(store);
}

类型很严格

项目中的 store 模块位于 src/store/modules/'

看userStore

存储用户登录后的 token(身份凭证)

存储 用户基本信息(头像、昵称等)

存储 角色权限列表(用于菜单 / 按钮权限控制)

提供 登录、设置 token 等方法

支持在组件外使用

java 复制代码
import { defineStore } from 'pinia';
import { store } from '/@/store';
///规定用户状态的数据结构
interface UserState {
  userInfo: Nullable<UserInfo>;
  token?: string;
  roleList: RoleEnum[];
  // ...
}
///创建 Store:defineStore
export const useUserStore = defineStore({
  id: 'app-user',  // 唯一标识
  state: (): UserState => ({
    // 用户信息
    userInfo: null,
    // token
    token: undefined,
    // 角色列表
    roleList: [],
    // 字典
    dictItems: null,
    // session过期时间
    sessionTimeout: false,
    // Last fetch time
    lastUpdateTime: 0,
    //租户id
    tenantid: '',
    // 分享租户ID
    // 用于分享页面所属租户与当前用户登录租户不一致的情况
    shareTenantId: null,
    //登录返回信息
    loginInfo: null,
  }),
///计算属性(读取数据的安全方式)
  getters: {
    getUserInfo(): UserInfo {
      return this.userInfo || {};
    },
    getToken(): string {
      return this.token || getAuthCache(TOKEN_KEY);
    },
    // ...
  },
///操作数据的方法
  actions: {
    setToken(info: string | undefined) {
      this.token = info;
      setAuthCache(TOKEN_KEY, info);
    },
    async login(params) {
      const data = await loginApi(params);
      this.setToken(data.token);
      return this.afterLoginAction();
    },
    // ...
  },
});

// 在非组件中使用
export function useUserStoreWithOut() {
  return useUserStore(store);
}

正常的 useUserStore() 只能在 Vue 组件 /setup 中使用

但像这些地方不能用

  • 路由守卫 router.beforeEach
  • axios 请求拦截器
  • 工具函数
  • 普通 ts 文件

所以必须提供一个提前注入了根 store 的方法。

使用

java 复制代码
import { useUserStore } from '/@/store/modules/user';

export default defineComponent({
  setup() {
    const userStore = useUserStore();
    
    // 访问状态
    console.log(userStore.token);
    console.log(userStore.getUserInfo);
    
    // 调用 action
    const handleLogin = async () => {
      await userStore.login({ username: 'admin', password: '123456' });
    };
    
    return { userStore, handleLogin };
  },
});

在非组件中使用

java 复制代码
import { useUserStoreWithOut } from '/@/store/modules/user';

// 路由守卫中使用
export function createPermissionGuard(router) {
  const userStore = useUserStoreWithOut();
  
  router.beforeEach(async (to, from, next) => {
    const token = userStore.getToken;
    if (!token) {
      next('/login');
      return;
    }
    // ...
  });
}

模板使用

java 复制代码
<template>
  <div>
    <span>用户名:{{ userStore.getUserInfo.name }}</span>
    <button @click="userStore.logout()">退出登录</button>
  </div>
</template>

<script setup>
import { useUserStore } from '/@/store/modules/user';
const userStore = useUserStore();
</script>
登录实现

<Button type="primary" size="large" block @click="handleLogin" :loading="loading">

{{ t('sys.login.loginButton') }}

</Button>

loginButton: '登录',

java 复制代码
// user.ts:151-161
async login(params) {
  const { goHome = true, mode, ...loginParams } = params;
  
  // 1️⃣ 调用后端登录接口
  const data = await loginApi(loginParams, mode);
  const { token, userInfo } = data;
  
  // 2️⃣ 保存 Token
  this.setToken(token);
  this.setTenant(userInfo.loginTenantId);
  
  // 3️⃣ 登录后处理
  return this.afterLoginAction(goHome, data);
}
java 复制代码
// user.ts:95-98
setToken(info: string | undefined) {
  this.token = info ? info : '';           // ① 保存到内存
  setAuthCache(TOKEN_KEY, info);           // ② 保存到 localStorage
}

// auth/index.ts:33-36
export function setAuthCache(key, value) {
  const fn = isLocal ? Persistent.setLocal : Persistent.setSession;
  return fn(key, value, true);  // 调用 localStorage.setItem()
}

/**
 * 静态方法:设置本地缓存(localStorage + 内存缓存 双层存储)
 * @param key - 缓存的 key(枚举类型,规范管理)
 * @param value - 要缓存的值
 * @param immediate - 是否立即同步到 localStorage,默认 false
 */
static setLocal(
  key: LocalKeys, 
  value: LocalStore[LocalKeys], 
  immediate = false
): void {
  // 1. 先把数据 存到【内存缓存 localMemory】(最快,页面读取优先走内存)
  // toRaw(value):解除响应式包裹(Pinia/Vue 的响应式对象不能直接存,必须转原生对象)
  localMemory.set(key, toRaw(value));

  // 2. 如果 immediate = true → 立即把【整个内存缓存】同步写入 localStorage
  // 不立即写:性能更好,等统一时机再批量存;立即写:关键数据(如token)立刻落地
  immediate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache);
}

后置处理

java 复制代码
// user.ts:179-222
async afterLoginAction(goHome?: boolean, data?: any) {
  if (!this.getToken) return null;
  
  // 获取详细用户信息
  const userInfo = await this.getUserInfoAction();
  
  // 保存登录信息
  await this.setLoginInfo({ ...data, isLogin: true });
  
  // 跳转到首页
  goHome && (await router.replace(userInfo?.homePath || PageEnum.BASE_HOME));
}

用户信息保持

java 复制代码
// user.ts:249-274
async getUserInfoAction(): Promise<UserInfo | null> {
  if (!this.getToken) return null;
  
  // 调用后端获取用户信息
  const { userInfo, sysAllDictItems } = await getUserInfo();
  
  if (userInfo) {
    // 保存角色列表
    const roleList = roles.map((item) => item.value) as RoleEnum[];
    this.setRoleList(roleList);
    
    // 保存用户信息
    this.setUserInfo(userInfo);  // ← 重点
  }
  
  // 保存字典数据
  if (sysAllDictItems) {
    this.setAllDictItems(sysAllDictItems);
  }
  
  return userInfo;
}

页面刷新

Vue 应用重新初始化 → Pinia 内存状态全部清空(token = undefined)

路由守卫 router.beforeEach 开始执行

判断当前页面是否需要登录权限

└──→ 调用 userStore.getToken 获取 token

└──→ getter 执行:return this.token || getAuthCache(TOKEN_KEY)

├── this.token → 刷新后为 undefined(内存丢失)

└── getAuthCache(TOKEN_KEY) → 从 localStorage 持久化缓存读取

成功获取到之前保存的有效 token

自动调用 userStore.setToken 将 token 重新存入 Pinia 内存

执行获取用户信息 action → 请求接口 /sys/user/info

获取用户信息、角色权限列表

更新 userInfo、roleList 到 Pinia + 本地缓存

登录状态完整恢复

路由守卫放行 → 正常进入页面

相关推荐
夜焱辰10 小时前
浏览器端 Agent 的文件版本管理:不用 Git,基于 OPFS + SQLite 自己造了一个
前端·人工智能
梦想的颜色10 小时前
TypeScript 完全指南(下):从类型体操到生产级配置
前端·javascript·typescript
Hi~晴天大圣11 小时前
npm使用介绍
前端·npm·node.js
888CC++12 小时前
如何在 C 语言中进行程序调试?
前端·javascript·算法
喵个咪12 小时前
基于 Taro 的 Headless CMS 多端前端架构:技术解析与二次开发导引
前端·react.js·taro
狂炫冰美式12 小时前
你还在古法PPT吗,试试HTML呢?免费编辑导出工具给 xdm 放这了
前端·后端·github
万少13 小时前
未来组织的分水岭不是员工数量,而是人才密度
前端·后端·面试
任磊abc13 小时前
nextjs16配置eslint+prettier
前端·eslint·nextjs·prettier
x***r15113 小时前
Another-Redis-Desktop-Manager.1.3.7安装步骤详解(附Redis可视化连接与Key管理教程)
前端·bootstrap·html