基于 pinia 封装持久化插件(ts版)

2,078 阅读4分钟

1. 为什么需要piana

vuex的问题

  1. TS支持度不好

  2. 架构过于繁琐

    state -> mutation -> action getters

    1. state -> action(同步 + 异步) - getters
    2. 天然支持模块化 modules

替代方案 - piana

官方文档Pinia

git 地址 github.com/vuejs/pinia

2.介绍

  • Vue2 和Vue3 都支持

    • 除了初始化安装和SSR配置之外,两者的API 都是相同
    • 官方文档中主要针对vue3进行说明,必要的时候会提供vue2 的注释
  • 支持Vue DevTools

    • 跟踪 actions mutatlons 的时间线
    • 在使用容器的组件就可以观察到容器本身
    • 支持 time travel 更容易的调试功能
    • 在vue2 中Pinla 使用的是Vuex现有接口,所以不能与Vuex一起使用
    • 但是针对 Vue3 中调试工具支持还不够完美,比如还没有,time-travel 调试功能
  • 模块热更新

    • 无需重新加载页面即可修改你的容器
    • 热更新的时候保持任何现有状态
  • 支持插件扩展 Pinia 功能

  • 相比 Vuex 有更好完美的TypeSrcipt 支持

  • 支持服务器渲染

3.Vuex与pinia的区别

pinia 就是更好的vuex ,推荐在项目中直接使用它,尤其是使用了TypeScript 的项目

  • Vuex 中有四个核心概念 State Getters Mutations Actions

  • Pinia 中有三个核心概念 State Gettes Mutatlons Actions

    • 没有 mutations . mutations 被认为是非常冗长的,最初带来了devtoods集成,但这不再是问题

    • 不再 有模块的嵌套结构,仍然可以通过在另一个store中导入和使用store来隐式嵌套store

      但 Pinia 通过设计提供扁平结构,同时仍然支持 store 之间的交叉组合方式。你甚至可以拥有store 的循环依赖关系

    • 更好支持 typeScript ,无需创建自定义的复杂包装器来支持,TypeScipt ,所有的内容都类型化,并且所有的Api的设计方式尽可能地利用TS类型判断

    • 不再需要注入,导入函数 ,调用它们享受自动补全

    • 无需动态添加 stores,可随手使用store来注册,因为是自动的,所以不用担心

    • 没有命名空间模块,鉴于store的扁平架构,'命名空间’ store是其定义方式所固有的,可以说所有stores都是命名空间的

4. 安装和配置

1)安装pinia插件

提示:如果应用程序使用Vue2,需要安装组合式api包 @vue/composition-api

yarn add pinia 
​
npm install pinia

2)注册pinia插件 - main.js

import { createPinia } from 'pinia'
app.use(createPinia()) //挂载到 Vue 根实例

5.使用

5.1创建store

store/counter.js

state
  • 创建store,命名规则: useXxxxStore
import { defineStore } from 'pinia'
// 参数1:容器ID 必须唯一,将来pinia 会把所有的容器挂载到根容器
// 参数2:对象,可以提供state actions getters
// 返回值:一个函数,调用得到容器实例
const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {},
  actions: {},
})
export default useCounterStore
getters

类似于组件computed,用来封装计算属性,有缓存的功能

  • 箭头函数不能使用this. 修改值时请使用 state
  • 类似于computed 数据修饰并且有缓存
    getters:{
       newCurrent ():number | string {
           return ++this.current + this.newName
       },
       newName ():string {
           return `$-${this.name}`
       },
      //使用箭头函数时,使用state修改值 
       newPrice:(state)=>  `$${state.user.price}`
    }
actions
  • 在pinia中没有mutations,只有actions
  • 不管是同步还是异步的代码,都在actions中完成
  • 注意:不能使用箭头函数定义 actions,因为箭头函数绑定外部的this
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
  state: () => {
    // 定义响应式数据
    return {
      count: 1,
      foo:100,
      arr:[1,2,3]
    }
  },
 //类似计算属性,具有缓存功能 
  getters:{
 // 函数接受一个可选参数state状态对象
    counl10(state){
      return state.count +10  
    },
 //如果getters中使用了 this则必须手动指定返回值的类型,否则类型推导不出3
    coun11():number{
      return this.count +10
    }
  },
//注意:不能使用箭头函数定义 action,因为箭头函数绑定外部的this                                    
  actions: {
    //对应$patch的数据修改--方法四
    changeSltate(num:number){
       this.count+=num
        this.foo='hello'
        this.arr.push(4)
      //this.$path({})
      // this.$path(state=>{})
    }  
   }
  }
})
​
export default useCounterStore

5.2 组件使用 app.vue

storeToRefs (响应式)

解构失去响应式情况,可用pinia 的storeToRefs

  • 使用storeToRefs可以保证解构出来的
  • storeToRefs:只能解构数据,不能解构函数
<script setup lang='ts'>
import {storeToRefs} from "pinia"
import useCounterStore from './store/counter'
const counterStore = useCounterStore() // count increment// 解构会失去响应式 
//const {count,foo}=counterStore//解决办法就是使用 storeToRefs
// 把解构出来的数据做 ref 响应代理
const {count,foo}= storeToRefs(counterStore) 
console.log(count.value)
</script>
​
<template>
  <!-- 使用store中的响应式数据 -->
 <p>{{ count }} </p>
 <p> {{ foo }} </p>   
</template>
​
$patch (批量修改)

批量处理 要修改的数据

<script setup lang='ts'>
import {storeToRefs} from "pinia"
import useCounterStore from './store/counter'
  
const counterStore = useCounterStore() // count increment
const {count,foo}= storeToRefs(counterStore) 
​
//修改数据
const handleChangeStae=()=>{
  // 方式一 最简单的方式
    counterStore.count++
     counterStore.foo='hello'
  //方式二:如果需要修改多个数据,建议使用$patch 批量更新
   counterStore.$patch({
      count: counterStore.count++,
      foo:'hello',
      arr:[...counterStore.arr,4]
   })
  //  方式三,$patch 一个函数 
  counterStore.$patch(state=>{
    state.count++
    state.foo="hello"
    state.arr.push(4)
  })
 // 方式四 逻辑比较多的时候可以封闭到 actions (推荐使用)
  counterStore.changeState(10)
}
​
</script>
​
<template>
  <!-- 使用store中的响应式数据 -->
 <p>{{ count }} </p>
 <p> {{ foo }} </p>   
 <p> {{ counterStore.arr }} </p>  
</template>
​
$reset(重置)

重置 store 至它的初始值

state: () => ({
     user: <Result>{},
     name: "default",
     current:1
    })
<script setup lang='ts'>
import useCounterStore from './store/counter'
const counterStore = useCounterStore() // count increment
 const reset=()=>{
    counterStore.$reset()//重置state数据
 }
</script>
​
<template>
  <!-- 使用store中的响应式数据 -->
<button @click='reset'></button>
 <p>{{ count }} </p>
 <p> {{ foo }} </p>   
</template>
​
$subscribe(监听state)

监听 state值的变化则调用 $subscribe

  • 如果你的组件卸载之后还想继续调用请设置第二个参数 detached
Test.$subscribe((args,state)=>{
  //args==>object包含新值与旧值 
  // state==>实例
   console.log(args,state);
},{
  detached:true
})
$onAction(监听)

只要有actions被调用就会走这个函数

  • 如果你的组件卸载之后还想继续调用请设置第二个参数 true
Test.$onAction((args)=>{
   console.log(args);
   
},true)

5.3 pinia的模块化

  • 在复杂项目中,不可能把多个模块的数据都定义到一个store中
  • 一般来说会一个模块对应一个store
  • 最后通过一个根store进行整合
  1. 新建 store/user.js文件

    // 模块一:存放数量
    import { defineStore } from 'pinia'
    const useCounterStore = defineStore('couter', {
      state: () => {
        return {
          count: 11,
          money: 99999999
        }
      }
    })
    export default useCounterStore
    
  2. 新建 store/couter.js

    import { defineStore } from 'pinia'
    // 参数1:容器ID 必须唯一,将来pinia 会把所有的容器挂载到根容器
    // 参数2:对象,可以提供state actions getters
    // 返回值:一个函数,调用得到容器实例
    const useCounterStore = defineStore('counter', {
      state: () => {
        return {
          count: 0,
        }
      },
      getters: {},
      actions: {},
    })
    export default useCounterStore
    
  3. 新建store/index.js

    // 统一管理所有的 pinia 模板
    import useCounterStore from "./couter";
    import useUserStore from "./user";
    ​
    // 暴露一个函数
    export default function useStore() {
      return {
        // 管理两个模块
        counter: useCounterStore(),
        user: useUserStore()
      }
    }
    
  4. 在组件中使用

    <script setup>
    import useStore from './store/index.js'
    // 使用 counter, user 中的数据
    const { counter, user } = useStore()
    </script>
    ​
    <template>
      <h1>pinia 中的模块化</h1>
      <h3>使用 counter 中的数据</h3>
      <div>counter.count:{{ counter.count }}</div>
      <div>counter.money:{{ counter.money }}</div>
      <h3>使用 user 中的数据 </h3>
      <div>user.name: {{ user.name }}</div>
      <div>user.age: {{ user.age }}</div>
    </template>
    ​
    

6持久化 pinia插件

创建 store/piniaMain.ts

import type {PiniaPluginContext } from 'pinia'
import { toRaw } from 'vue'
const __piniaKey = '__PINIAKEY__'
type OptPinia={
    key?:string
    paths?:string[]
}
​
//取值
const getStorage = (key: string) => {
    return (localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {})
}
//存储
const setStorage = (key: string, value: any): void => {
    localStorage.setItem(key, JSON.stringify(value))
}
​
const piniaPlugin = (options: OptPinia)=>{
    return (context: PiniaPluginContext)=>{
        const { store } = context;
        const data = getStorage(`${options?.key ?? __piniaKey}-${store.$id}`)
        store.$subscribe((args, state)=>{
            let arrPaths = options.paths
            if (!arrPaths?.includes(args.storeId))return
            setStorage(`${options?.key ?? __piniaKey}-${store.$id}`, toRaw(store.$state))
        },{
            detached:true
          })
​
        return{
            ...store.$state,
            ...data
        }
    }
}
​
​
​
export default piniaPlugin

创建store/piniaStore.ts

import piniaPlugin from "@/store/piniaMain"import { createPinia } from 'pinia'
const store=createPinia()

export default store.use(piniaPlugin({
    key: "pinia", //存储的前缀key
    paths: ['user', 'cart']//存储state中的那些数据
}))

main.ts 注册

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPlugin from "@/store/piniaStore"
​
app.use(store)
​
const app = createApp(App)
​