Vue 状态管理

272 阅读8分钟
  • 举个简单的例子, 我们每个页面在访问后端数据的时候 都需要 token , 那么所有组件都依赖 token 这个数据, 我们就需要找个地方存起来,让其他组件都能访问到它

  • 通俗的讲,就是存储一些公用的东西,提供给各个组件使用,和服务器端的 session 功能也很类似

  • 那我们如何来实现这个共享存储呢?

共享内存

第一种方式最直接: 共享内存, 直接开辟一个变量,全局都能访问到就可以了, 类似于后端的全局变量:

前面我们在讲组建通信的时候就使用过:

  • 注入依赖
  • 组合式函数

全局注入

使用到 provide() 函数注入到根实例, 从而提供全局变量功能

<script setup>
import { RouterLink, RouterView } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue";

import { ref, provide } from "vue";
// 如果的变量可以是响应式的
const count = ref(0);
provide(/* 注入名 */ "count", /* 值 */ count);
</script>

通过 inject 获取父组件注入的变量:

<script setup>
import { inject } from "vue";

// 这里也可以获取默认值: inject(<变量名称>, <变量默认值>), 如果获取不到变量 就使用默认值
const count = inject("count");

const doClick = () => {
  count.value++;
};
</script>

<template>
  <button @click="doClick">You clicked me {{ count }} times.</button>
</template>

组合式函数

声明一个响应式模块,导出后,提供给所有组件使用

// store/login.js
import { reactive } from "vue";

export var login = reactive({ username: "", passwd: "" });

AboutView.vue

<template>
<input v-model="login.username" />
<input v-model="login.passwd" />
<button @click="Login()">登录</button>
</template>
<script setup>
import { login } from "../stores/login.js";

const Login = () => {
  console.log(login.username, login.passwd);
};

image.png HomeView.vue:切换页面时在控制台 打印 全局变量

切换后: image.png

总结

这种其实就是一个简单粗暴的 通过共享内存进行通信的方式, 好在其简单易懂,也许你会喜欢, 但是也有它的缺陷

因为这种方式使用的是内存, 所以页面关闭或者刷新就都没有, 想要就状态持久化 还需要存储

浏览器本地存储

这种方式就需要使用到浏览器的存储功能了, 它可供我们存储客户端临时信息 简称 Web Storage image.png

cookie

cookie 是有可以设置过期时间的, 同一个域下的页面都可以访问

cookie 在没有设置过期时间时,系统默认浏览器关闭时失效,只有设置了没到期的保存日期时,浏览器才会把 cookie 作为文件保存在本地上。当 expire 到期时,浏览器则会删除过期 cookie

注意:

  • 数据存放大小: 4k, 因为每次 http 请求都会携带 cookie
  • 浏览器关闭时, cookie会失效
  • 注意 cookie 可以支持 httpOnly , 这个时候前端 JS 是修改不了的(也看不到)
// 读取cookie, 注意读取出来的cookie是个字符串
document.cookie
'language=zh; Sub-System=develop; sidebarStatus=1; Current-Namespace=c16mhsddrei91m4ri0jg; Refresh-Token=paBuyTIfsX3BeKrXrCmD8khUla6x8y1g'
// 需要自己处理
document.cookie.split('; ')

// 直接赋值就添加了一个key-value
document.cookie = 'cookieKey=cookieValue'

// 当然cookie还有很多选项可以设置, 通过;隔开比如
document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";

// 修改cookie和设置cookie一样, 保证key相同就可以
document.cookie = 'cookieKey=cookieValue2'


// 删除cookie时,把expires 设置到过期的时间即可, 比如设置个2019年的时间
document.cookie = `cookieKey=;expires=Mon, 26 Aug 2019 12:00:00 UTC`

sessionStorage

存储的数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁, 因此 sessionStorage 不是一种持久化的本地存储,仅仅是会话级别的存储

那么,到底什么是一个会话?多个标签页之间的数据是否会共享呢?

我们可以验证下: 开启2个窗口, 直接通过浏览器修改sessionStorage 进行验证

通过验证我们可以知道 一个标签页 就表示一个会话, 当标签页关闭, 会话就清除, 不同标签页之间不共享数据

// 通过setItem设置key-value
sessionStorage.setItem('key1', 'value1')
sessionStorage['key2']= 'value2'
sessionStorage.key2= 'value2'

// 查询sessionStorage对象
sessionStorage
Storage {key2: 'value2', key1: 'value1', length: 2}

// 通过getItem获取key的值
sessionStorage.getItem('key1')
sessionStorage['key1']
sessionStorage.key1

// 修改
sessionStorage.key1 = 'value11'
sessionStorage['key1'] = 'value11'

// 删除key
sessionStorage.removeItem('key1')

// 清空storage
sessionStorage.clear()

localStorage

localStorage 生命周期是永久, 除非主动删除数据,否则数据是永远不会过期的

相同浏览器的不同页面间可以共享相同的 localStorage(页面属于相同域名和端口)

我们可以验证下: 开启2个窗口, 直接通过浏览器修改localStorage 进行验证

localStorage 的操作方法和 sessionStorage 完全一样:

// 通过setItem设置key-value
localStorage.setItem('key1', 'value1')
localStorage['key2']= 'value2'
localStorage.key2 = 'value2'

// 查询sessionStorage对象
localStorage
Storage {key2: 'value2', key1: 'value1', length: 2}

// 通过getItem获取key的值
localStorage.getItem('key1')
localStorage['key1']
localStorage.key1

// 修改
localStorage.key1 = 'value11'
localStorage['key1'] = 'value11'

// 删除key
localStorage.removeItem('key1')

// 清空storage
localStorage.clear()

Vueuse与本地存储

作为 Vue3 的标准库, vueuse 提供了很多实用的工具来实现了状态管理, 其中就有保护浏览器存储的组合式API函数:  VueUse

vueuse 中有一个高频使用的函数: useStorage, 就默认就是使用的 LocalStorage, 该函数已经将 LocalStroage 包装成了响应式了, 因此你可以把他理解为一个带有持久化机制的响应式对象

下面是 useStorage 的基础用法:

import { useStorage } from '@vueuse/core'

// bind object
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })

// bind boolean
const flag = useStorage('my-flag', true) // returns Ref<boolean>

// bind number
const count = useStorage('my-count', 0) // returns Ref<number>

// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>

// delete data from storage
state.value = null

我们集合前面讲到共享内存的通信方式, 把他们改造成, 结合本地存储基础, 从而避免刷新页面是状态丢失: 在 AboutView.vue 中进行修改

// import { login } from "../stores/login.js";  不导入之前的 全局变量包
import { useStorage } from "@vueuse/core";

const login = useStorage("login", { username: "", passwd: "" });

const Login = () => {
  console.log(login.value.username, login.value.passwd);
};

pinia

虽然我们的手动状态管理解决方案在简单的场景中已经足够了,但是在大规模的生产应用中还有很多其他事项需要考虑:

  • 更强的团队协作约定
  • 与 Vue DevTools 集成,包括时间轴、组件内部审查和时间旅行调试
  • 模块热更新 (HMR)
  • 服务端渲染支持

Pinia 就是一个实现了上述需求的状态管理库,由 Vue 核心团队维护,对 Vue 2 和 Vue 3 都可用

vuex与pinia

Pinia 最初正是为了探索 Vuex 的下一个版本而开发的, 因此整合了核心团队关于 Vuex 5 的许多想法。最终,我们意识到 Pinia 已经实现了我们想要在 Vuex 5 中提供的大部分内容,因此决定将其作为新的官方推荐

相比于 VuexPinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导

下面是使用组合式API定义的一个 store

import { defineStore } from 'pinia'

// 你可以任意命名`defineStore()`的返回值,并在其周围加上`use`和`Store`(例如:`useUserStore`、`useCartStore`、`useProductStore`)。
// 第一个参数是store在你的应用程序中的唯一ID
export const useStore = defineStore('main', {
  // other options...
})

基本概念

pinia遵循“单向数据流”这一概念, 流程如下:

  • 状态:驱动整个应用的数据源;
  • 视图:对状态的一种声明式映射, 视图层获取状态;
  • 交互:状态根据用户在视图中的输入而作出相应变更的可能方式, 通过 actions 来修改状态。

通过 defineStore 来声明一个状态管理模块, 从中也可以看出这3个核心概念:

export const useCounterStore = defineStore('counter', {
  // 状态数据
  state: () => ({ count: 0, name: 'Eduardo' }),
  // 获取状态数据
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  // 修改状态的数据
  actions: {
    increment() {
      this.count++
    },
  },
})

安装

当然你也可以选择手动安装:

yarn add pinia
# or with npm
npm install 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)
app.mount('#app')

当然更加简单的方法是 使用 cli 安装, 还能给我们生成实例

vue add pinia

定义Store

选项式风格: 见 stores/counter.js

pinia 很友好为我们提供了 Setup 风格, 这样整体代码可以保证统一的风格: 我们在 stores 文件夹内新建 user.js

import { defineStore } from "pinia";
import { ref } from "vue";

export const useUserStore = defineStore("user", () => {
  const username = ref("");
  const passwd = ref("");
  const userLogin = (user, pass) => {
    username.value = user;
    passwd.value = pass;
  };
  return { username, passwd, userLogin };
});

Setup 风格 Stores 中:

  • ref() 变为 state 数据
  • computed() 变为 getters
  • function() 变为 actions

使用Store

AboutView.vue 中替换上一个 login 通过组合式函数方式直接使用

<template>
  <input v-model="login.username" />
  <input type="password" v-model="login.passwd" />
  <button @click="Login()">登录</button>
</template>
<script setup>
// import { useStorage } from "@vueuse/core";
import { useUserStore } from "../stores/user";

const login = useUserStore();

const Login = () => {
  console.log(login.username, login.passwd);
};
</setup>

image.png

测试下 看看devtools中 vuex是否正常, 看看刷新后如何

持久化存储

上面的测试应该已经知道 pinia 的状态存储并不能持久化,存储在 Vuex 中的 store 里的数据,只要一刷新页面,数据就丢失了

那我们能不能叫 pinia 的存储修改为 localstorage 喃? 答案是可以的, 有个插件就完成了这个事儿: pinia-plugin-persist ,具体使用说明请参考: Pinia Plugin Persist

安装插件

npm i pinia-plugin-persist --save

安装好了后我们配置 vuex 使用该插件: main.js

// app.(creatPinia()) 原本是这个

const pinia = createPinia();
import piniaPersist from "pinia-plugin-persist";
pinia.use(piniaPersist);
app.use(pinia);

这样我们持久化的插件就安装上了, 但是默认我们的 state 并没有持久化, 可以测试下

使用插件

要开启 state 的持久化,在定义 store 的时候,传递一个参数:( user.js )

export const useUserStore = defineStore(
  "user",
  () => {
    const username = ref("");
    const passwd = ref("");
    const userLogin = (user, pass) => {
      username.value = user;
      passwd.value = pass;
    };
    return { username, passwd, userLogin };
  },
  { persist: { enabled: true } }
);

注意插件默认使用的是: sessionStorage , 当然我们也可以自定义存储策略

存储策略

依然是在 user.js

export const useUserStore = defineStore(
  "user",
  () => {
    const username = ref("");
    const passwd = ref("");
    const userLogin = (user, pass) => {
      username.value = user;
      passwd.value = pass;
    };
    return { username, passwd, userLogin };
  },
  {
    persist: {
      enabled: true,
      // username 和 passwd 用 localStorage 方式存储
      // 可同时用两种存储方式 (对于不同属性)
      strategies: [{ storage: localStorage, paths: ["username", "passwd"] }],
    },
  }
);

image.png