Vue3全家桶之——Pinia状态管理

3,106 阅读4分钟

Pasted image 20230711100757.png

前言

Pinia简称小菠萝🍍,是一个专为Vue 3设计的现代化状态管理库,为Vue 3开发的,它提供了一种简单、可扩展和类型安全的方式来管理应用程序的状态。

Vue 2中的Vuex相比,Pinia提供了更好的TypeScript支持,具有更好的类型定义和类型推断,可在编译时捕获错误,提供更高的代码可靠性和开发体验。它是专为Vue 3设计的,充分利用了Vue 3的新特性,如Composition API,以提供更直接、自然和灵活的状态管理体验。Pinia的核心概念是Store,它类似于Vuex中的模块,用于管理应用程序的状,可以将相关的状态和逻辑组合到单个Store中,使代码更清晰、结构更有组织性。除此之外海提供了许多有用的特性和功能,例如模块化组织、状态持久化、插件扩展等。

总的来说,Pinia是一个功能强大而灵活的状态管理解决方案,适用于各种规模的Vue 3应用程序。它提供了现代化的特性和工具,帮助我们更好地组织、管理和扩展应用程序的状态,同时提供了更好的类型安全和开发体验。

安装

运行安装命令

npm install pinia

main.ts中引入

// main.ts
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')

初始化Store

新建stores文件,用于存放所有的store,然后创建index.ts

同过 defineStore() 定义一个store,它接受一个参数作为仓库名称,也就是Id。它返回一个函数,默认我们使用user开头的风格来接收。第二个参数为一个Setup函数或者Option对象。

import { defineStore } from 'pinia'

export const useUsersStore = defineStore('users', {
  // 其他配置...
  
})

Option Store

这种方式熟悉Vuex的很了解,传入一个带有 stateactionsgetters 属性的 Option 对象

export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 100
    }
  },

  getters: {
    getName: (state) => state.name + '🐔你好帅'
  },

  actions:{
    getUserInfo {
      ...
    }
  }
})

Option Store 中:

  • statestore 的数据 data
  • gettersstore 的计算属性 computed
  • actions 则是方法 methods

Setup Store

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

export const userUsersStore = defineStore('users', () => {
  const name = ref('inkun')
  function getInkun() {
    getInkun.value + '🐔你好帅'
  }

  return { name, getInkun }
})

Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

使用Store

定义一个store后,在组件里引入这个store然后就行使用,不需要像ref一样使用.value,可以直接修改访问。

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨

const store = useCounterStore()
</script>

State

state定义一个返回初始状态的函数,函数内返回一个对象,里面是需要定义的数据。

对于基础类型而言,[[../TypeScript|TypeScript]]可以自行推断出它们的数据类型,也可以接口,定义state函数返回值。

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

interface UserInfo {
  name: string
  age: number
}
export const userUsersStore = defineStore('users', {
  state: (): State => {
    return {
      userList: [],
      user: null
    }
  }
})

修改State

默认情况下可以直接通过store实例访问state,并且可以直接对其进行读写操作。

Vuex中,如果要对state进行修改必须要定一个mutation,通过mutation进行提交,太过于繁琐。

const store = useStore()

store.count++

变更

除了用 store.count++ 直接改变 store,还可以调用 $patch 方法。它允许你用一个 state 的补丁对象在同一时间更改多个属性:

store.$patch({
  count: store.count + 1,
  name: 'ff',
})

重置

可以通过调用 store$reset() 方法将 state 重置为初始值。

const store = useStore()

store.$reset()

监听

类似于 Vuexsubscribe 方法,可以通过 store $subscribe() 方法侦听 state 及其变化。

store.$subscribe((mutation, state) => {

  mutation.storeId // 'cart'

  console.log('state change', state)
  console.log('mutation', mutation.type)  // 'direct' | 'patch object' | 'patch function'
  console.log('mutation2', mutation.storeId) // 'users'
  // 只有 mutation.type === 'patch object'的情况下才可用
  // mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
  console.log('mutation3', mutation.payload)
}, {
  detached: true
})

默认情况下,state subscription 会被绑定到添加它们的组件上,当该组件被卸载时,它们将被自动删除。如果想在组件卸载后依旧保留它们,将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离,此时组件卸载后,订阅器还可以使用。

结构State

在使用state时是不允许直接从store中结构数据,这样会导致数据失去响应式和props一样。

解构出来的数据是可以正常访问,当数据修改时是不会发生任何变化。

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const {current, name} = useCounterStore() // 数据不会发生变化

function change() {
  store.current++
}
</script>

解决方案是通过storeToRefs将数据重新变回响应式。

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const store= useCounterStore() // 数据不会发生变化
const {name, current} = storeToRefs(store)
function change() {
  store.current = 1
  name.value = 'ff'
}
</script>

Getter

getter相当于计算属性,接收一个函数,函数参数为当前store里的state,也可以通过this去访问。

export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 100
    }
  },
	getUserName(state) {
		return state.name + '🐔你好帅'
	},
	getName(): string {
		return this.name + '🐔你实在太帅'
	}

})

然后就可以通过store实例访问getter

<template>
  {{ store.getUserName }}
  {{ store.getName }}
</template>

<script setup lang="ts">
import { userUsersStore } from './stores'

const store = userUsersStore()
</script>

访问其他Getter

通过this可以访问其他的getter

export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 100
    }
  },
	getUserName(state) {
		return '大家好,我是' + state.name 
	},
	getName(): string {
		return this.getUserName + '🐔你实在太帅'
	}

})

向Getter传递参数

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

export const userUsersStore = defineStore('users', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>

访问其他Store里的Getter

将要访问的store引入并实例就可以

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

action

action相当于method,和Vuex不同的是它异步同步都可以定义。

export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 100
    }
  },
  actions:{
    async getUserInfo {
      ...
    }
  }
})

getter一样,也可以通过this访问state数据


export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 100
    }
  },
  actions:{
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  }
})

在模版上也是和其他一样通过store直接访问。

<template>
  <button type="button" @click="getUserInfo">获取</button>
</template>

监听

可以通过store.$onAction()来监听 action 和它们的结果。第一个参数为回调函数,可以获取action 的一些信息,第二个参数如果想在组件卸载后依旧保留它们,将 true作为第二个参数传递给action` 订阅器。

它返回一个函数,可以在必要的时候调用函数,此时会删除订阅器取消监听。


<script setup lang="ts">
import { userUsersStore } from './stores'

const store = userUsersStore()

const unsubscribe = store.$onAction(({ name, store, args, after, onError }) => {})

// 取消监听
unsubscribe()

</script>

数据持久化

Vuex一样,都存在刷新后数据就会丢失,可以通过pinia-plugin-persistedstate插件来解决。

通过在将数据存储到本地storage中,避免数据刷新丢失。储存位子有两个一个是LocalStorageSessionStorage,具体看个人情况使用。

针对存储的位置,在使用的时候需要考虑项目是否真的要存储在某个位置,合理使用。不能说将用户头像、名称等信息存储在SessionStorage中,网站关闭后数据也还是会丢失。也不能说将IM聊天室消息、所有用户信息等数据存储在LocalStorage中,存储的大小也有限制,这是时候就要使用IndexDBweb SQL等方式。所以需要结合项目功能情况。合理选择存储,而不是一股脑的使用。

安装

npm i pinia-plugin-persistedstate

将插件添加到pinia实例上

// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

使用

在创建store时,设置persist: true

export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 1
    }
  },
  getters: {
    ...
  },
  actions: {
    ...
  },
  persist: true
})

设置完后可以在网页中看到数据存储在localStorage

Pasted image 20230710181703.png

配置persist

persist可以接收一个对象

export const userUsersStore = defineStore('users', {
  state: () => {
    return {
      name: 'inkun',
      current: 1
    }
  },

  persist: {
    key: 'my-custom-key',
    storage: sessionStorage,
    paths: ['current'],
    serializer: {
      deserialize: parse,
      serialize: stringify,
    },
    beforeRestore: (ctx) => {
      console.log(`即将恢复 '${ctx.store.$id}'`)
    },
     afterRestore: (ctx) => {
      console.log(`刚刚恢复完 '${ctx.store.$id}'`)
    },
  }
})
  • key: 用于引用 storage 中的数据,默认使用store中的Id
  • storage:数据存储位置,默认localStorage,可以该为sessionStorage
  • paths:指定state中哪些数据需要持久化
  • serializer:指定持久化时所使用的序列化方法,以及恢复 store 时的反序列化方法。
  • beforeRestore:该 hook 将在从 storage 中恢复数据之前触发,并且它可以访问整个 PiniaPluginContext,这可用于在恢复数据之前强制地执行特定的操作。
  • afterRestore:该 hook 将在从 storage 中恢复数据之后触发,并且它可以访问整个 PiniaPluginContext,这可用于在恢复数据之后强制地执行特定的操作。

全局配置

使用全局配置,就不用单独在每个store里面做配置,在使用pinia use的时候就可以通过createPersistedState函数设置。

// main.ts
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'

const pinia = createPinia()

pinia.use(
  createPersistedState({
    storage: sessionStorage,
    paths: ['current'],
  })
)

createPersistedState里的配置会将每个申明persist: truestore添加上配置,但是每个单独store里的配置将会覆盖调全局声明中的对应项

全局配置支持一下属性:

  • storage
  • serializer
  • beforeRestore
  • afterRestore

启用所有 Store 默认持久化

该配置将会使所有 store 持久化存储,且必须配置 persist: false 显式禁用持久化。

import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'

const pinia = createPinia()

pinia.use(
  createPersistedState({
    auto: true,
  })
)

Store多个持久化配置

在一些特殊情况下,每个store中的数据存储的位置不一样,可以将persist设置为接收多个配置形式。

import { defineStore } from 'pinia'

defineStore('store', {
  state: () => ({
    toLocal: '',
    toSession: '',
    toNowhere: '',
  }),
  persist: [
    {
      paths: ['toLocal'],
      storage: localStorage,
    },
    {
      paths: ['toSession'],
      storage: sessionStorage,
    },
  ],
})

强制恢复数据

每个 store 都有 $hydrate 方法来手动触发数据恢复。默认情况下,调用此方法还将触发 beforeRestoreafterRestore 钩子。但是可以通过配置方法来避免这两个钩子触发。

import { defineStore } from 'pinia'

const useStore = defineStore('store', {
  state: () => ({
    someData: '你好 Pinia',
  }),
})

调用 $hydrate 方法:

const store = useStore()

store.$hydrate({ runHooks: false })

这将从 storage 中获取数据并用它替换当前的 state。并且在上面的示例中,配置了runHooks: false,所以 beforeRestoreafterRestore 钩子函数不会被触发。

强制持久化

除了通过persist方式设置持久化,每个store都有$persist方法来手动触发持久化,这会强制将 store state 保存在已配置的 storage 中。

import { defineStore } from 'pinia'

const useStore = defineStore('store', {
  state: () => ({
    someData: '你好 Pinia',
  }),
})

// App.vue
const store = useStore()

store.$persist()

其他文章