当前位置: 首页 > news >正文

Day 4 登录页及路由 (二) -- Vue状态管理

状态管理

之前的实现中,判断登录状态用了伪实现,实际当中,应该是以缓存中的数据为依据来进行的。这就涉及到了应用程序中的状态管理。在Vue中,状态管理之前是Vuex,现在则是推荐使用Pinia,在脚手架项目创建过程中,也提示了是否使用。

通过Pinia官方文档,可以看到核心概念Store的定义:

Store (如 Pinia) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。

而关于何时使用也有一个指南:

一个 Store 应该包含可以在整个应用中访问的数据。这包括在许多地方使用的数据,例如显示在导航栏中的用户信息,以及需要通过页面保存的数据,例如一个非常复杂的多步骤表单。另一方面,你应该避免在 Store 中引入那些原本可以在组件中保存的本地数据,例如,一个元素在页面中的可见性。

 对于登录状态,显然就是一个需要在各个地方都可以访问的数据。

一般而言,用户登录后,会把一些基础信息,比如用户名,token,角色,权限等缓存起来,用于全局使用。

实现

而按照官方文档推荐,定义一个interface用于类型推断:

src\stores\interfaces\index.ts 

import type { Ref } from 'vue'/*** 用户信息*/
export interface UserInfo {/*** 名字*/name: string
}/*** 登录用户状态*/
export interface UserState {/*** 令牌*/token: Ref<string>/*** 用户信息*/userInfo: Ref<UserInfo>/*** 设置令牌* @param token 令牌   */setToken: (token: string) => void/*** 设置用户信息* @param userInfo 用户信息*/setUserInfo: (userInfo: UserInfo) => void
}

然后,定义CurrentUserStore:

src\stores\currentUser.ts

import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { UserInfo, UserState } from '@/stores/interfaces'export const useCurrentUserStore = defineStore('currentUser',(): UserState => {const token: Ref<string> = ref('')const userInfo: Ref<UserInfo> = ref({ name: '' })function setToken(value: string) {token.value = value}function setUserInfo(value: UserInfo) {userInfo.value = value}return { token, userInfo, setToken, setUserInfo }},{persist: true}
)

这里面在官方文档中defineStore的第二个参数其实有一个选项式和组合式的选择,个人觉得组合式相对精简,当然,这也是因为框架做了不少工作。

关于store的定义,额外需要说明的就是第三个参数,里面多了一个 persist:true。这个实际上用到了Pinia的插件——pinia-plugin-persistedstate。很有意思的是,Pinia算是Vue的插件,而它本身也有一个插件体系。

要使用pinia-plugin-persistedstate,首先需要安装包

npm install pinia-plugin-persistedstate

然后修改main.ts,使用之:

src\main.ts

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'const app = createApp(App)const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)app.use(pinia)

至此,Store已经定义好了,应该可以使用了,不过在使用之前,还是先写一个单元测试来验证一下。之前的脚手架创建时,就已经选择了Vitest作为单元测试框架,因此,在src\component下已经创建了一个_test_文件夹,单元测试代码就写在这里。具体如下:

src\components\__tests__\currentUser.spec.ts

import { describe, beforeEach, it, expect } from 'vitest'
import { createApp } from 'vue'
import type {UserState, UserInfo} from '@/stores/interfaces'
import { setActivePinia, createPinia,storeToRefs  } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { useCurrentUserStore } from '@/stores/currentUser'describe('Current User Store', () => {const app = createApp({})  const testToken = 'TestToken 1234'beforeEach(() => {// 创建一个新 pinia,并使其处于激活状态,这样它就会被任何 useStore() 调用自动接收// 而不需要手动传递:// 在 pinia 被安装在一个应用之后,插件才会被使用const pinia = createPinia().use(piniaPluginPersistedstate)app.use(pinia)setActivePinia(pinia)})it('setToken', () => {const currentUser = useCurrentUserStore()expect(currentUser.token).toBe('')currentUser.setToken(testToken)expect(currentUser.token).toBe(testToken)})it('setUserInfo', ()=>{const currentUser = useCurrentUserStore()        const {userInfo} = storeToRefs(currentUser)// const {userInfo} = currentUserexpect(currentUser.userInfo.name).toBe('')    expect(userInfo.value.name).toBe('')// expect(userInfo.name).toBe('')const name = 'test user'currentUser.setUserInfo({name:name})expect(currentUser.userInfo.name).toBe(name)expect(userInfo.value.name).toBe(name)// expect(userInfo.name).toBe(name)  // 这里会失败,因为userInfo 解包成非响应式引用})
})

参照Pinia官方文档关于测试的描述,使用beforeEach来构造测试环境。另外,注意在 setUserInfo这个case中,特意演示了 storeToRefs的用法,这样解包才能保证token、userInfo依然是响应式的,注释掉的部分说明了直接解包的话,数据变更不会反映到直接解包的变量上。

OK,前戏已经差不多可以了,现在可以进入主题,修改router中的validateLoginState方法了。

因为是前端判断,token的其它校验就留给后端,前端仅判断token是否为空就可以了,代码就变得很简单:

src\router\index.ts

/*** 校验登录是否有效* @returns True: 登录状态有效 False:登录状态无效*/
const validateLoginState = () => {const currentUser = useCurrentUserStore()return (currentUser.token != '') 
}

有了校验,就需要有地方可以设置token,修改一下LoginView,加一个FakeLogin:

src\views\login\LoginView.vue

<template>Logged On ? <el-text class="mx-1" type="info">{{ isLogon }}</el-text><RouterLink to="/">Go to Home</RouterLink><el-button type="primary" @click="fakeLogin">Fake Login</el-button>
</template>
<script setup lang="ts">
import { useCurrentUserStore } from '@/stores/currentUser'
const currentUser = useCurrentUserStore()
const isLogon = computed(() => currentUser.token.length > 0)
function fakeLogin(event: Event) {currentUser.setToken("faked login token")
}
</script>

样式比较丑,先体验效果:

这个时候,试着点击Go to Home按钮,页面不会动,因为还未登录,在DevTools中查看Pinia如下,token为空字符串:

点击Fake Login 按钮,模拟登录后,状态改变:

页面也有变化(这里红框中的部分也是体验了一下响应式,用了一个computed,这样登录状态会根据token自动变化,后续登陆后的页面框架也可以用这样的方式来处理token失效、退出事件)

再在HomeIndex.vue中,添加一个LogOff按钮,整个过程就完整了。logoff方法就做了两个动作,清除token,重定向到登录页。

src\views\home\HomeIndex.vue

<template>Home<el-button type="primary" @click="logoff">Log Off</el-button>
</template>
<script setup lang="ts">
import { useCurrentUserStore } from '@/stores/currentUser'import { useRouter } from "vue-router"
import { LOGIN_URL } from "@/config"const currentUser = useCurrentUserStore()
const router = useRouter()
/*** 推出登录*/
function logoff(event: Event) {// 清除tokencurrentUser.setToken('')// 重定向到登录页router.replace(LOGIN_URL);
}</script>

效果如下,一如既往的丑:

至此,基于Pinia的登录状态管理已经完成了。

回顾

回过头来再看,Pinia本身的概念不复杂,state,getter,action,三个核心概念构成了store整体。从后端开发的角度来看,感觉会很眼熟,Repository + DTO,或者当成一个领域对象来看,有数据,有查询,有操作。那么是不是可以把这个模式扩大到整个应用程序呢,比如getter直接调用api获取后端数据,action也是直接调用api来实现数据操作。从道理上来讲应该是可以的,但是那就是把store当成了领域层,localStorage作为缓存了。而在前端是否需要这么复杂的架构,还是需要仔细考量的。我感觉如果不是特别复杂的应用上,没有必要使用这种方式来开发。毕竟前端是运行在浏览器中,性能还是比较重要的。即使有各种优化措施,这种框架带来的概念上的简化也并没有太多的优势,因为本身大部分情况下,前端页面还没有这么复杂。

相关文章:

  • 边缘计算技术的崭新篇章:赋能未来智能系统
  • 在Spring boot中 使用JWT和过滤器实现登录认证
  • 【年终特惠】全流程HEC-RAS 1D/2D水动力与水环境模拟技术案例实践及拓展应用
  • 9.Python3-注释
  • 20年经典传承 | 性能圣典!火焰图发明者Brendan Gregg“神作”
  • Linux cp命令:复制文件和目录
  • 【git】git使用教程
  • [架构之路-245/创业之路-76]:目标系统 - 纵向分层 - 企业信息化的呈现形态:常见企业信息化软件系统 - 企业资源管理计划ERP
  • 第16章_变量、流程控制与游标
  • CCS3列表和超链接样式
  • jenkins工具系列 —— 删除Jenkins JOB后清理workspace
  • SurfaceFliger与Vsync信号如何建立链接?
  • Docker 部署spring-boot项目(超详细 包括Docker详解、Docker常用指令整理等)
  • 十月听书笔记
  • 表格识别软件:科技革新引领行业先锋,颠覆性发展前景广阔
  • JavaScript 如何正确处理 Unicode 编码问题!
  • Angular2开发踩坑系列-生产环境编译
  • Docker 笔记(2):Dockerfile
  • emacs初体验
  • HTTP请求重发
  • JavaScript学习总结——原型
  • SpringBoot几种定时任务的实现方式
  • Web设计流程优化:网页效果图设计新思路
  • 二维平面内的碰撞检测【一】
  • 前端性能优化——回流与重绘
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 学习笔记:对象,原型和继承(1)
  • 进程与线程(三)——进程/线程间通信
  • !!Dom4j 学习笔记
  • ###C语言程序设计-----C语言学习(3)#
  • #我与Java虚拟机的故事#连载13:有这本书就够了
  • (4) PIVOT 和 UPIVOT 的使用
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (剑指Offer)面试题34:丑数
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • (转)Unity3DUnity3D在android下调试
  • (状压dp)uva 10817 Headmaster's Headache
  • **PyTorch月学习计划 - 第一周;第6-7天: 自动梯度(Autograd)**
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • . NET自动找可写目录
  • .net 7 上传文件踩坑
  • .NET Core WebAPI中使用swagger版本控制,添加注释
  • .net实现头像缩放截取功能 -----转载自accp教程网
  • // an array of int
  • /usr/bin/python: can't decompress data; zlib not available 的异常处理
  • @Autowired和@Resource装配
  • @transaction 提交事务_【读源码】剖析TCCTransaction事务提交实现细节
  • [ Linux ] Linux信号概述 信号的产生
  • []串口通信 零星笔记
  • [android] 天气app布局练习
  • [Android]RecyclerView添加HeaderView出现宽度问题
  • [AUTOSAR][诊断管理][ECU][$37] 请求退出传输。终止数据传输的(上传/下载)
  • [C++] Windows中字符串函数的种类
  • [Firefly-Linux] RK3568修改控制台DEBUG为普通串口UART