99网
您的当前位置:首页基于 Vue3 学习状态管理器:pinia

基于 Vue3 学习状态管理器:pinia

来源:99网
pinia 基本概念

Pinia 是 Vue 的存储库,Pinia和Vuex一样都是是vue的全局状态管理器,它允许跨组件/页面共享状态。实际上,其实Pinia就是Vuex5,官网也说过,为了尊重原作者,所以取名 pinia,而没有取名 Vuex,所以大家可以直接将 pinia 比作为 Vue3 的 Vuex。

为什么用 pinia
  • pinia中只有state、getter、action,抛弃了Vuex中的Mutation,Vuex中mutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。
  • pinia中action支持同步和异步,Vuex不支持
  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了
  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是的,互相不影响。
  • 体积非常小,只有1KB左右。
  • pinia支持插件来扩展自身功能。
  • 支持服务端渲染。

Pinia的函数
createPinia()

创建一个被应用所使用的 Pinia 实例。返回:Pinia

defineStore(id, options):

StoreDefinition。创建一个检索 store 实例的 useStore 函数。

  • id:相当于为容器起一个名字。注意:这里的名字必须唯一,不能重复
  • options:可以简单理解为一个配置对象,里边是对容器仓库的配置说明。当然这种说明是以对象的形式。
storeToRefs

用 store 的所有state、getters和 插件添加的(plugin-added)state、 属性(properties)创建一个引用对象。类似于toRefs(),但专门为 Pinia store 设计

  • 参数:用以提取 refs 得store
辅助函数
mapStores()

通过生成要在组件的 computed 字段中展开的对象,允许使用没有组合式API (setup())的存储。它接受 store 定义的列表。

  • 参数:要隐射到对象的存储列表
export default {
 computed: {
   // 其它计算属性
   ...mapStores(useUserStore, useCartStore)
 },
 
 created() {
   this.userStore // id为"user"的存储
   this.cartStore // id为"cart"的存储
 }
}
mapState()

将 state 属性映射为只读的计算属性,虽然它可以通过这个访问组件实例,但它不会识别类型。

  • useStore: 第一个参数,要隐射的存储
  • keyMapper:第二个参数,状态 state 属性或访问器 getter 得对象
import { mapState } from 'pinia'
import { useCounterStore } from './store/counter.js'

const counterComputed = computed(()=>{
  // 可以访问组件中的 this.count
  // 与从 store.count 中读取的数据相同
  ...mapState(useCounterStore, ['count'])
  // 与上述相同,但将其注册为 this.myOwnName
  ...mapState(useCounterStore, {
    myOwnName: 'count',
    double: store => store.count * 2,
    magicVlue(store) {
      return store.soneGetter + this.count + this.double
    }
  })
})
mapWritableState()

可修改 state 中的属性,但是不能像 mapState() 哪样传递一个函数。

import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // 可以访问组件中的 this.count,并允许设置它。
    // this.count++
    // 与从 store.count 中读取的数据相同
    ...mapWritableState(useCounterStore, ['count'])
    // 与上述相同,但将其注册为 this.myOwnName
    ...mapWritableState(useCounterStore, {
      myOwnName: 'count',
    }),
  },
}
mapActions(useStore, keyMapper)

通过生成要在组件的 methods 字段中铺开的对象,允许直接使用存储(store)中的操作(action),而不使用合成API (setup())。对象的值是操作(action),而键是结果方法的名称。

import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  methods: {
    // 访问组件内的 this.increment()
    // 与从 store.increment() 调用相同
    ...mapActions(useCounterStore, ['increment'])
    // 与上述相同,但将其注册为this.myOwnName()
    ...mapActions(useCounterStore, { myOwnName: 'increment' }),
  },
}
安装
yarn add pinia
# 或者使用 npm
npm install pinia

在mian.js中,创建根存储

import { createPinia } from 'pinia'

app.use(createPinia())
pinia 基础特性
state
  • 默认情况下,通过 store 实例访问 state,可以直接读取和写入,如 @click='store.count++'
  • 通过 store.$reset() 方法可以将 state 重置为初始值
  • 除了直接通过 store 修改 state,还可以通过 store.$patch() 方法提交多个更改
  • 可以通过 store.$subscribe() 订阅 state 得变化,在 patches 修改之后订阅只会触发一次,默认情况下,订阅绑定到添加他的组件,当组件卸载时,他们将自动删除,也可以配置将其保留。
getters
  • Getters 属性的值是一个函数,接受 state 作为第一个参数,目的是鼓励使用箭头函数
  • 非箭头函数会绑定 this,建议仅在需要获取整个 store 实例的场景使用,且需要显式定义函数返回类型
actions
  • 与 Gettes 一样可以通过 this 访问整个 store 实例
  • Actions 可以是异步的或同步的,不管怎样,都会返回一个 Promise
  • Actions 可以自由的设置参数和返回的内容,一切将自动推断,不需要定义 TS 类型
  • 与 State 一样,可以通过 store.$onAction() 订阅 Actions,回调将在执行前触发,并可以通过参数 after() 和 onError() 允许在Action 决议后和拒绝后执行函数。同样的,订阅绑定的是当前组件。
store

store实例相当于一个容器,里面存放的有类似于data,计算属性,方法之类的东西。通过defineStore()方法定义

store

在src下面创建一个store文件夹,再创建与之对应的js文件,比如user.js

defineStore:

  • 第一个参数是你的应用中 Store 的唯一 ID(必传),Pinia 将用它来连接 store 和 devtools。
  • 第二个参数可接受两类值:Setup 函数或 Option 对象。
option

与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象

  • state:是 store 中的 data
  • getters:是 store 得计算属性 computed
  • actions:是 store 的方法 methods
setup

option :

import { defineStore } from 'pinia'

// useStore 可以是 useUser、useCart 之类的任何东西
// 第一个参数是应用程序中 store 的唯一 id
export const useUser = defineStore('user', {
  // other options...
})

setup 函数:

与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

  • ref():对应 store 中的 state
  • computed:对应 store 中的 getters
  • function:对应 store 中的 actions

Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

export const useCounterStore = defineStore('count',()=>{
  const count = ref(0)
  function increment(){
    count.value++
  }
  return { count, increment }
})

注:store 是一个用 reactive 包装的对象,这意味着不需要再 getters 后面下 .value ,就像 setup 中的 props 一样,不允许解构。

<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
  store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
使用 store
<script lang="ts" setup>
import { useUser } from "../stroe/user";

const stroe = useUser();
console.log(stroe, "stroe");
</script>
添加 state
import { defineStore } from 'pinia'
​
// 第一个参数是应用程序中 store 的唯一 id
// 第二个参数是配置对象
export const useUser = defineStore('user', {
  // state是一个函数,返回一个对象
  state: () => {
    return {
      name: "anna",
      age: 16,
    };
  }
})

为了从 store 中提取属性时保持其响应性,需要使用 storeToRefs()。它将为每一个响应式属性创建引用,当只使用 store 得状态而不调用任何 action 时,非常有用。可以直接从 store 中解构 action ,因为他们也被绑定到 store 中。

import { storeToRefs } from 'pinia'
const store = useCounterStore()
// name 和 age 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式(不是 ref 或 reactive )得属性
const { name, age } = storeToRefs(store)
// 作为 action 得 increment 可以直接解构
const { increment } = store
state

state 是 store 得核心。state 被定义为一个返回初始状态的函数,声明 state 建议使用箭头函数,会自动判断属性的类型。

typescript 写法:

const useStore = defineStore('user',{
  state: ()=>({
    userList: [] as UserInfo[],
    user: null as UserInfo | null
  })
})

interface UserInfo{
  name: string,
  age: number
}

定义节后 state,并添加 state() 得返回值的类型

interface State{
  userList: UserInfo[],
    user: UserInfo | null
}

interface UserInfo{
  name: string,
  age: number
}

const useStore = defineStore('user',{
  state: State => {
    return {
      userList: [],
      user: null
    }
  }
})
读取 state

可以简单理解为一个配置对象,里边是对容器仓库的配置说明。当然这种说明是以对象的形式。默认情况下,可以直接访问 store 得实例 state属性,并对齐进行读写。

import { storeToRefs } from 'pinia'
const userStore = useUser()
// 如果直接解构出数据,这个数据不是响应式的。如果想要变成响应式的,需要调用storeToRefs方法
const { name, age } = storeToRefs(userStore)
修改 state
userStore.name = 'bob'
批量修改 state

除了 store.xxx 直接修改 store,还可以使用 $patch 方法,允许用一个 state 得补丁对象在同一时间更改多个属性。

// 可以用来修改单个属性
userStore.$patch({
  name: 'lily'
})
    
// 这种回调函数的形式适合修改集合类的数据,比如数组
userStore.$patch((state) => {
  state.age = 18
})

两种变更 store 方法的主要区别是,$patch() 允许你将多个变更归入 devtools 的同一个条目中。同时请注意,直接修改 state,$patch() 也会出现在 devtools 中,而且可以进行 time travel (在 Vue 3 中还没有)。

事实上开发过程中为了满足工程扁平结构化规范,一般通过Action结合实现

重置 state
// 重置
userStore.$reset()
setup store
export const userCounterStore = defineStore('counter',()=>{
  const count = ref(0)
  function $reset(){
    count.value++
  }
  return { count, $reset }
})
直接替换整个 state (几乎不用)
// 替换
userStore.$state = { 
  userName: 'tom', 
  age: 11 
}
订阅

类似于 vuex 得 subscribe,可以通过 store 得 $subscribe() 方法侦听 state 及其变化,比起普通的 watch(),使用 $subscribe() 的好处是 subscriptionspatch 后只触发一次。

cartStore.$subscribe((mutation, state) => {
  // import { MutationTypr } from 'pinia'
  mutation.type // 'dorect' | 'patch object' | 'patch function'
  // 和 carStore.$id 一样
  mutation.storeId // 'cart'
  // 只有 mutation.type === 'patch onject'得情况下才可以用
  mutation.payload  // 传递给 cartStore.$patch() 得补丁对象

  // 每当状态发生变化时,将整个 state 持久化到本地存储
  localStorage.setItem('cart', JSON.stringify(state))
})

默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离

const someStore = useSomeStore()
// 此订阅器几遍在组件卸载之后扔会被保留
someStore.$subscribe(callback, { detached: true })

setup store:

watch(pinia.state, state => {
  loaclStorage.setItem('piniaState',JSON.stringify(state))
})
getter

类似计算属性。用来监视或者说是计算状态的变化的,有缓存的功能。

推荐里面传递一个形参的箭头函数写法,不容易出错。如果使用非箭头函数,可以在函数中使用 this,但是在 getters 中使用形参 state 访问属性 typescript 无法识别。

  • 为了避免使用 this,官方建议使用箭头函数
  • 建议仅当需要获取整个 store 时使用 this,但必须显示的定义函数返回类型,typescript 不会自动判断
  • 建议使用 this 得时候不要声明 state 形参
getters: {
    isAdult: (state) => {
      return state.age >= 18 ? "成年人" : "未成年";
    },
    isAdult1(){
      return this.age >= 18 ? "成年人" : "未成年";
    }
},
获取
// 直接获取
<div>{{userStore.isAdult}}</div>
调用本模块其他 getter
  getters: {
    isAdult: (state) => {
      return state.age >= 18 ? '成年人' : '未成年'
    },
    msg: (state) => {
      // msg这个getter访问了自身的getter(isAdult)
      return state.userName + state.isAdult
    },
    msg1(){
      return this.userName + this.isAdult
    }
  }
getters 传参

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:

  getters: {
    isAdmin: (state) => {
      // 如果getter里面是返回的函数,那么它就可以传参数了
      return (name) => name === 'admin' ? '是管理员' : '不是管理员'
    }
  }
调用
// 直接获取
<div>{{userStore.isAdmin}}</div>
调用其他 store 里面的 getter

商品模块 good.js

import { defineStore } from 'pinia'
​
// 第一个参数是应用程序中 store 的唯一 id
// 第二个参数是配置对象
export const useGoods = defineStore('goods', {
  // state是一个函数,返回一个对象
  state: () => {
    return {
      goodsName: 'iphone'
    }
  },
  getters: {
    newIphone: (state) => {
      return state.goodsName + ' 14 pro'
    }
  }
})

想在 user 模块中的 getters 中去获取 goods 模块的 newIphone

import { useGoods } from './goods.js'
// ..........
getters: {
    info: (state) => {
      // 获取goods模块的store
      const goodsStore = useGoods()
      return state.userName + '买了' + goodsStore.newIphone
    }
}
actions

类似于 getters,actions 也可以通过 this 访问整个 store 实例。action 是可以异步的,可以在他们里面 await 调用任何 api,以及其他 action。注意action中需要定义普通函数,这样才有自己的this能取到state属性

基本使用
  actions: {
    // 这里的方法要写成普通函数,因为里面需要通过this去访问state里面的数据
    changeNameAsync (newName) {
      setTimeout(() => {
        // actions里面可以访问state
        this.userName = newName
      }, 1000)
    },
    async getList(params) {
      try {
        this.list = await api.post(params)
      } catch (err) {
        console.log('err:' + err.msg)
      }
    }
  }

Action 可以像函数或者通常意义上的方法一样被调用:

<templete>
  <button @click='store.randomizeCounter()'/>
</templete>

<script setup>
  const store = useCounterStore()
  store.randomizeCounter()
</script>
访问其他 store 中的 action
import { defineStore } from 'pinia'
import { useAuthStore } from './auth-store'

export const useSettingStore = defineStore('setting', {
  state: () => ({
    preference: null
  }),
  actions:{
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preference = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    }
  }
})
订阅 action

你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。

const unsubscribe = someStore.$onAction(
  ({
    name, // action 名称
    store, // store 实例,类似 `someStore`
    args, // 传递给 action 的参数数组
    after, // 在 action 返回或解决后的钩子
    onError, // action 抛出或拒绝的钩子
  }) => {
    // 为这个特定的 action 调用提供一个共享变量
    const startTime = Date.now()
    // 这将在执行 "store "的 action 之前触发。
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 这将在 action 成功并完全运行后触发。
    // 它等待着任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果 action 抛出或返回一个拒绝的 promise,这将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手动删除
unsubscribe()
模块化

在实际开发中,不可能把多个模块的数据都定义到一个store中,而是一个模块对应一个store,最后通过一个根store进行整合

建立两个 store
// 模块一
import { defineStore } from 'pinia'
 
const useUserStore = defineStore('user', {
  state: () => {
    return {
      name: 'haha',
      age: 18,
    }
  },
})
 
export default useUserStore
 
// 模块二
import { defineStore } from 'pinia'
 
const useCounterStore = defineStore('user', {
  state: () => {
    return {
      count: 1
    }
  },
})
 
export default useUserStore
新建 store/index.js
import useUserStore from './user'
import useCounterStore from './counter'
 
// 统一导出useStore方法
export default function useStore() {
  return {
    user: useUserStore(),
    counter: useCounterStore(),
  }
}
组件中使用
<script setup>
import { storeToRefs } from 'pinia'
import useStore from './store'
const { counter } = useStore()
 
// 使用storeToRefs可以保证解构出来的数据也是响应式的
const { count } = storeToRefs(counter)
</script>
pinia 和 vuex

Pinia同样是一个Vue的状态管理工具,在Vuex的基础上提出了一些改进。与vuex相比,Pinia 最大的特点是:简便。

区别
  • pinia 它没有 mutation,他只有 state, getters,action【同步、异步】使用他来修改state数据
  • pinia 他默认也是存入内存中,如果需要使用本地存储,在配置上比vuex麻烦一点
  • pinia 语法上比 vuex 更容易理解和使用,灵活
  • pinia 得模块化设计,是通过构建多个存储模块,可以让程序自动拆分它们,pinia 没有 modules 配置,没一个的仓库都是 definStore 生成出来的
  • pinia state 是一个对象返回一个对象和组件的 data 是一样的语法
  • pinia 类型安全,与 TypeScript 一起使用时具有可靠的类型推断支持
  • pinia 不再有 modules 的嵌套结构,没有命名空间模块
  • Pinia 支持扩展,可以非常方便地通过本地存储,事物等进行扩展
  • pinia 支持服务器端渲染
pinia 的优点
  • 完整的 TypeScript 支持:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
  • 极其轻巧(体积约 1KB)
  • store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见
  • 支持多个Store
  • 支持 Vue devtools、SSR 和 webpack 代码拆分
pinia 的缺点
  • 不支持时间旅行和编辑等调试功能
vuex 的优点
  • 支持调试功能,如时间旅行和编辑
  • 适用于大型、高复杂度的Vue.js项目
vuex 的缺点
  • 从 Vue 3 开始,getter 的结果不会像计算属性那样缓存
  • Vuex 4 有一些与类型安全相关的问题
何时使用Pinia,何时使用Vuex

由于Pinea是轻量级的,体积很小,它适合于中小型应用。它也适用于低复杂度的Vue.js项目,因为一些调试功能,如时间旅行和编辑仍然不被支持。

将 Vuex 用于中小型 Vue.js 项目是过度的,因为它重量级的,对性能降低有很大影响。因此,Vuex 适用于大规模、高复杂度的 Vue.js 项目。

pinia和vuex在vue2和vue3都可以使用,一般来说vue2使用vuex,vue3使用pinia。

因篇幅问题不能全部显示,请点此查看更多更全内容