Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。
Pinia 起源于一次探索 Vuex 下一个迭代的实验,因此结合了 Vuex 5 核心团队讨论中的许多想法。最后,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分功能,所以决定将其作为新的推荐方案来代替 Vuex。
Vuex 3.x 只适配 Vue 2,而 Vuex 4.x 是适配 Vue 3 的,Pinia 可以同时支持 Vue2 和 Vue3。
1.1 安装
# 或者使用其它包管理工具,如 yarn pnpm
npm install pinia
创建一个 pinia 实例 (根 store) 并将其传递给应用:
// main.js
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
app.use(pinia)
return {
app,
}
}
注意事项:
createSSRApp
是配合 SSR 来使用的,其用法与createApp
相同,在这里 uni-app 为了做跨平台开发所采取的方式,其作用我们就按createApp
来理解即可。
1.2 Store
在深入研究核心概念之前,我们得知道 Store 是用 defineStore()
定义的,它支持两种语法法格式,分别是选项式 Store 和 组件式 Store,创建一个文件来演示它的使用:
- 选项式(Options) Store
// stores/counter.js
import { defineStore } from 'pinia'
// 选项式 Store
export const useCounterStore = defineStore('counter', {
state: () => {
return {
count: 0,
}
},
getters: {
double: (state) => {
return state.count * 2
},
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
},
})
在 pages/test/index
页面中通一个计数器来测试它的用法,下面新增的布局相关代码
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
// 测试网络请求
async function onButtonClick() {
// 省略前面小节代码
}
</script>
<template>
<view class="content">
<!-- 前面小节代码省略了 -->
<view class="counter">
<button class="button" type="primary">-</button>
<input class="input" type="text" />
<button class="button" type="primary">+</button>
</view>
</view>
</template>
<style lang="scss">
// 前面小节代码省略了
.counter {
display: flex;
margin-top: 30rpx;
}
.input {
flex: 1;
height: 96rpx;
text-align: center;
border: 2rpx solid #eee;
box-sizing: border-box;
}
.button {
width: 100rpx;
margin: 0;
&:first-child {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
&:last-child {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
}
</style>
接下来看如何使用 Pinia,在使用时要注意必须要调用定义好的 Store 才会真正创建 Store 实例,对应到下面的代码是必须要调用 useCounterStore
后才会创建 Store 实例,即 counterStore
。
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
// 测试网络请求
async function onButtonClick() {
// 省略前面小节代码
}
</script>
<template>
<view class="content">
<!-- 前面小节代码省略了 -->
<view class="counter">
<button class="button" type="primary">-</button>
<input class="input" type="text" />
<button class="button" type="primary">+</button>
</view>
</view>
</template>
Store 实例中定义的 state
、getters
、actions
可以直接应用到组件模板当中。
注意事项:
- 定义 Store 时建议(非必须)使用 use + 名称 + Store 格式命名,其中名称也会被当做 ID 出现在调试工具中
- 创建 Store 实例时,实例的名称建议用 名称 + Store 格式命名,避免引入多个 Store 时名称重复的问题
- 组件式(Setup)Store
组合式 Store 用法与选项式 Store 用法最直接的区别就是 defineStore
的第 2 个参数传入是一个函数,而选项式 Store 传入的是一个对象。
另一个区别是在组合式 API 中允许使用 Vue 的组合式函数,如 ref
、computed
、watch
等。
在组件式 Store 中:
ref()
就是state
属性
computed
就是getters
function()
就是actions
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 1. 选项式 Store
// 这里省略上一小节代码
// 2. 组合式 Store
export const useCounterStore = defineStore('counter', () => {
// 定义 state
const count = ref(0)
// 定义 getters
const double = computed(() => count.value * 2)
// 定义 actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 千万不要忘记这里要 return
return { count, double, increment, decrement }
})
对比发现组合式 Store 的用法与 Vue 组件的 setup 用法是一致的,咱们项目中会采用这种用法来开发。
1.3 State
State 实际上就是用来共享访问的数据,这些数据会涉及到访问、变更等操作,我们分别来学习 State 的相关操作。
- 访问,State 的数据是使用
reactive
创建的,直接通过 Store 实例属性的方式即可访问,这种访问方式也包括了getters
的访问。
<!-- pages/test/index.vue -->
<script setup>
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
// 像普通过 reactive 包装数据一样来访问
console.log(counterStore.count)
// getters 可以可采用相同的方式来访问
console.log(counterStore.double)
</script>
但是这里一定要注意是 reactive
的数据不允许解构,解构后的数据将会失去响应式,为了解决这个问题可以使用 Pinia 提供的工具函数 storeToRefs
2.变更
变更 State 的数据有两种方式,一种是直接赋值,另一种是调用 $patch 方法。
<!-- pages/test/index.vue -->
<script setup>
import { storeToRefs } from 'pinia'
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
let _count = 0
// 更新 state
function increment() {
// 直接等号赋值
// counterStore.count++
// 调用 $patch 方法
counterStore.$patch({
count: ++_count,
})
}
// 更新 state
function decrement() {
// 直接等号赋值
// counterStore.count--
// 调用 $patch 方法
counterStore.$patch({
count: --_count,
})
}
</script>
<template>
<view class="content">
<!-- 省略前面小节部分代码... -->
<view class="counter">
<button @click="decrement" class="button" type="primary">-</button>
<input class="input" :value="counterStore.count" type="text" />
<button @click="increment" class="button" type="primary">+</button>
</view>
<view class="state">
<text class="text">count: {{ count }}</text>
<text class="text">double: {{ double }}</text>
</view>
</view>
</template>
这两种方式都可以用来对 State 数据进行修改,在一次性需要更新多个数据时推荐使用 $patch
方法,单个数据更新时使用等号直接赋值。
1.4 持久化
Pinia 的数据是以全局的方式存储在内存中的,这会导致页面被刷新后数据丢失或重置,但实际开发中有的数据需要长时间的存储,即所谓的持久化,通常都是存入本地存储当中来实现的,在 Pinia 中通过插件来扩展持久化的功能。
- 安装
# 也可以使用其它包管理工具,如 yarn pnpm
npm i pinia-plugin-persistedstate
2.将插件添加 Pinia 实例上
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
// Pinia 持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
// 应用 Pinia 插件
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
return {
app,
}
}
3.将数据持久化存储,为 defineStore
传入第3个参数,第3个参数是对象类型
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 2. 组合式 Store
export const useCounterStore = defineStore(
'counter',
() => {
// 定义 state
const count = ref(0)
// 定义 getters
const double = computed(() => count.value * 2)
// 定义 actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 千万不要忘记这里要 return
return { count, double, increment, decrement }
},
{ persist: true }
)
当 count
数据发生改变后就会将数据存入本地存储当中了,但是这种方式有个弊端就是会将所有 State 数据持久化存储,这样会造成不必要的性能损耗,要解决这个问题也非常方便,通过 paths
来指定需要持久化存储的数据:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 2. 组合式 Store
export const useCounterStore = defineStore(
'counter',
() => {
// 定义 state
const count = ref(0)
// 定义 getters
const double = computed(() => count.value * 2)
// 定义 actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 千万不要忘记这里要 return
return { count, double, increment, decrement }
},
{
persist: {
paths: ['count'],
},
}
)
-
配置
以上的用法在一般的 Vue 项目中可以满足基本的开发需要了,但是在 uni-app 中时却需要做一些额外的配置,原因在于 uni-app 中本地存储使用的是 uni.setStorageSync
而插件中使用的是 localStorage.setItem
,为此需要我们自定义配置本地址存储的方法。
使用 createPersistedState
进行全局性配置
// main.js
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
// Pinia 持久化插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
// 应用 Pinia 插件
// pinia.use(piniaPluginPersistedstate)
pinia.use(
// 自定义 Pinia 插件
createPersistedState({
// 自定义本地存储的逻辑
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
})
)
app.use(pinia)
return {
app,
}
}
createPersistedState
传的参数中 storage
是用来自定义持久化存储方法的,其中 setItem
和 getItem
是内置固定的名称,在进行本地存储时插件内部会自动调用这两个方法,进而调用 uni.setStorageSync
将数据存入本地。
另外存入本地数据的名称默认为 Store 的名称,这个名称也允许自定义,使用 key
来指定:
// main.js
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
// Pinia 持久化插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
// 应用 Pinia 插件
// pinia.use(piniaPluginPersistedstate)
pinia.use(
// 自定义 Pinia 插件
createPersistedState({
// 自定义本地存数据的名称
key: (id) => `__persisted__${id}`,
// 自定义本地存储的逻辑
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
})
)
app.use(pinia)
return {
app,
}
}