Pinia学习
Vue3中 使用
安装
yarn add pinia
# 或者使用 npm
npm install pinia
使用
main.js中引入并注册
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
src下创建store文件夹-用于管理state
// 命名规范
// use + 逻辑名 || 业务名 + store
// useCounterStore
Store
创建
import { defineStore } from 'pinia'
// defineStore
// 参数一是一个id 参数二是配置项 state getters actions
export const useCounterStore = defineStore('counter', {
state: () => {
return {
count: 0
}
},
getters: {
doubleCount: () => state.count * 2
},
actions: {
increment () {
this.count++
},
decrement () {
this.count--
}
}
})
Store的 使用
<script setup>
import { useCounterStore } from './store/useCounterStore'
import { storeToRefs } from 'pinia'
// store 是一个用reactive 包裹的对象
// 这意味着不需要在getter 之后写.value,但是,就像setup 中的props 一样,我们不能对其进行解构:
const store = useCounterStore()
// 想结构后保持其响应式 就必须借用 storeToRefs()
const { count } = storeToRefs(store)
console.log('我创建的第一个仓库', store);
console.log('仓库结构', count); // ref包裹的对象
</script>
Store知识点
- defineStore 创建仓库
参数1 id 参数2 配置项
返回值 reactive包裹的响应式对象
引入 ==》 调用 ==》 结构
import { useCounterStore } from './store/useCounterStore'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count } = storeToRefs(store)
解构时 直接结构调用后的返回值(store)得到的是不具备响应式的数据
需要通过storeToRefs () 包裹 store 解构出来的数据才具备响应式 (ref)
State
创建
export const useCounterStore = defineStore("counter", {
state: () => {
return {
num: 0,
count:0
};
}
})
访问状态
默认情况下,您可以通过 store
实例访问状态来直接读取和写入状态:
<script setup>
import { useCounterStore } from './store/useCounterStore'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count } = storeToRefs(store)
console.log('我创建的第一个仓库', store);
console.log('仓库结构', count); // ref包裹的对象
重置状态
原理
调用的$patch
const $reset = isOptionsStore
? function $reset(this: _StoreWithState<Id, S, G, A>) {
const { state } = options as DefineStoreOptions<Id, S, G, A>
const newState = state ? state() : {}
// we use a patch to group all changes into one single subscription
this.$patch(($state) => {
assign($state, newState)
})
}
: /* istanbul ignore next */
DEV
? () => {
throw new Error(
🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().
)
}
: noop
你可以使用store中的 $reset 进行状态重置
const store = useCounterStore()
store.$reset()
使用选项API
// 仓库文件
import { defineStore } from "pinia";
// 第二个参数支持两种风格:options api 以及 composition api
export const useCounterStore = defineStore("counter", {
state: () => {
return {
num: 0,
};
}
})
组合API
import { defineStore } from "pinia";
import { reactive } from "vue";
export const useListStore = defineStore("list", () => {
const counterStore = useCounterStore();
// 组合 api 风格
// 创建仓库数据,类似于 state
const list = reactive({
items: [
{
text: "学习 Pinia",
isCompleted: true,
},
{
text: "打羽毛球",
isCompleted: false,
},
{
text: "玩游戏",
isCompleted: true,
},
],
counter: 100,
});
return {
list
}
})
非组合API
如果您不使用 Composition API,并且使用的是 computed
、methods
、...,则可以使用 mapState()
帮助器将状态属性映射为只读计算属性:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// 允许访问组件内部的 this.counter
// 与从 store.counter 读取相同
...mapState(useCounterStore, {
myOwnName: 'counter',
// 您还可以编写一个访问 store 的函数
double: store => store.counter * 2,
// 它可以正常读取“this”,但无法正常写入...
magicValue(store) {
return store.someGetter + this.counter + this.double
},
}),
},
}
可修改状态 mapWritableState
如果您希望能够写入这些状态属性(例如,如果您有一个表单),您可以使用 mapWritableState()
代替。 请注意,您不能传递类似于 mapState()
的函数:
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// 允许访问组件内的 this.counter 并允许设置它
// this.counter++
// 与从 store.counter 读取相同
...mapWritableState(useCounterStore, ['counter'])
// 与上面相同,但将其注册为 this.myOwnName
...mapWritableState(useCounterStore, {
myOwnName: 'counter',
}),
},
}
对于像数组这样的集合,您不需要
mapWritableState()
,除非您用cartItems = []
替换整个数组,mapState()
仍然允许您调用集合上的方法。
改变状态
方法一: 直接通过仓库实例更改
方法二: 通过$patch
// 1.
store.counter ++
// 2
store.$patch({
counter: store.counter + 1,
name: 'Abalam',
})
但是,使用这种语法应用某些突变非常困难或代价高昂:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合。 正因为如此,$patch
方法也接受一个函数来批量修改集合内部分对象的情况:
cartStore.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
替换state
您可以通过将其 $state
属性设置为新对象来替换 Store 的整个状态:
store.$state = { counter: 666, name: 'Paimon' }
您还可以通过更改 pinia
实例的 state
来替换应用程序的整个状态。 这在 SSR for hydration 期间使用。
pinia.state.value = {}
订阅状态
可以通过 store 的 $subscribe()
方法查看状态及其变化,类似于 Vuex 的 subscribe 方法。 与常规的 watch()
相比,使用 $subscribe()
的优点是 subscriptions 只会在 patches 之后触发一次(例如,当使用上面的函数版本时)。
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 与 cartStore.$id 相同
mutation.storeId // 'cart'
// 仅适用于 mutation.type === 'patch object'
mutation.payload // 补丁对象传递给 to cartStore.$patch()
// 每当它发生变化时,将整个状态持久化到本地存储
localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,state subscriptions 绑定到添加它们的组件(如果 store 位于组件的 setup()
中)。 意思是,当组件被卸载时,它们将被自动删除。 如果要在卸载组件后保留它们,请将 { detached: true }
作为第二个参数传递给 detach 当前组件的 state subscription:
export default {
setup() {
const someStore = useSomeStore()
// 此订阅将在组件卸载后保留
someStore.$subscribe(callback, { detached: true })
// ...
},
}
您可以在
pinia
实例上查看整个状态:watch( pinia.state, (state) => { // 每当它发生变化时,将整个状态持久化到本地存储 localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true } )
状态持久化
数据持久化的实现原理:
利用localStorage存储到本地
import { defineStore } from 'pinia'
const useStateStore = defineStore('state', {
state: () => {
return {
a: 1,
b: 2,
c: false
}
},
persist: true, // 持久化配置
})
export default useStateStore
// 安装 插件
// pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
Getters
本质:computed计算属性
getters语法和state一样
getters 中可以接收state作为参数
getters中如果要使用其他getters 不能继续使用箭头函数
使用其他store中的数据 直接在当前store引入即可
定义
export const useStore = defineStore('store', {
state:() =>{
a:1
}
getters:{
double:(state) =>{
return state.a +1
}
}
})
this
大多数时候,getter 只会依赖状态,但是,他们可能需要使用其他 getter。 正因为如此,我们可以在定义常规函数时通过
this
访问到 整个 store 的实例但是需要定义返回类型(在 TypeScript 中)。 这是由于 TypeScript 中的一个已知限制,并且不会影响使用箭头函数定义的 getter,也不会影响不使用 this 的 getter:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// 自动将返回类型推断为数字
doubleCount(state) {
return state.counter * 2
},
// 返回类型必须明确设置
doublePlusOne(): number {
return this.counter * 2 + 1
},
},
})
使用
直接在store实例上进行调用即可
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>
// 写法一
<script>
export default {
setup() {
const store = useStore()
return { store }
},
}
</script>
// 写法二
<script setup>
export default {
const store = useStore()
}
</script>
访问其他getter
当前getter要访问其他getter,可以借助普通函数中的this
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// 类型是自动推断的,因为我们没有使用 `this`
doubleCount: (state) => state.counter * 2,
// 这里需要我们自己添加类型(在 JS 中使用 JSDoc)。 我们还可以
// 使用它来记录 getter
/**
* 返回计数器值乘以二加一。
*
* @returns {number}
*/
doubleCountPlusOne() {
// 自动完成 ✨
return this.doubleCount + 1
},
},
})
参数传递
由于getter本质上也是计算属性,因此它本身不接收任何参数,但是我们可以通过返回函数的形式让其接收参数
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
实际使用
缺点:getter不再缓存
<script setup>
import {storeToRefs} from 'pinia'
export default {
const store = useStore()
const {getUserById} = storeToRefs(store)
}
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
Actions
Actions 相当于组件中的 methods。 它们可以使用
defineStore()
中的actions
属性定义,并且它们非常适合定义业务逻辑:
定义
与 getters 一样,操作可以通过
this
访问 whole store instance 并提供完整类型(和自动完成✨)支持。 与它们不同,actions 可以是异步的
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
使用 setup
<script setup>
const useStore = useStore()
// 像methods一样被调用即可
useStore.increment()
</script>
不适用setup
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
methods: {
// gives access to this.increment() inside the component
// same as calling from store.increment()
...mapActions(useCounterStore, ['increment'])
// same as above but registers it as this.myOwnName()
...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }),
},
}
访问其他store
直接在内部引入使用即可
mport { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
// ...
}),
actions: {
async fetchUserPreferences(preferences) {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
订阅
可以使用
store.$onAction()
订阅 action 及其结果。 传递给它的回调在 action 之前执行。after
处理 Promise 并允许您在 action 完成后执行函数。 以类似的方式,onError
允许您在处理中抛出错误。 这些对于在运行时跟踪错误很有用,类似于 Vue 文档中的这个提示。
const unsubscribe = someStore.$onAction(
({
name, // action 的名字
store, // store 实例
args, // 调用这个 action 的参数
after, // 在这个 action 执行完毕之后,执行这个函数
onError, // 在这个 action 抛出异常的时候,执行这个函数
}) => {
// 记录开始的时间变量
const startTime = Date.now()
// 这将在 `store` 上的操作执行之前触发
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// 如果 action 成功并且完全运行后,after 将触发。
// 它将等待任何返回的 promise
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// 如果 action 抛出或返回 Promise.reject ,onError 将触发
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动移除订阅
unsubscribe()
插件Plugins
插件仅适用于**在将
pinia
传递给应用程序后创建的 store **,否则将不会被应用。
作用
- 向 Store 添加新属性
- 定义 Store 时添加新选项
- 为 Store 添加新方法
- 包装现有方法
- 更改甚至取消操作
- 实现本地存储等副作用
- 仅适用于特定 Store
基本用法
import { createPinia } from 'pinia'
// 为安装此插件后创建的每个store添加一个名为 `secret` 的属性
// 这可能在不同的文件中
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// 将插件提供给 pinia
pinia.use(SecretPiniaPlugin)
// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'
参数
Pinia 插件是一个函数,可以选择返回要添加到 store 的属性。 它需要一个可选参数,一个 context:
export function myPiniaPlugin(context) {
context.pinia // 使用 `createPinia()` 创建的 pinia
context.app // 使用 `createApp()` 创建的当前应用程序(仅限 Vue 3)
context.store // 插件正在扩充的 store
context.options // 定义存储的选项对象传递给`defineStore()`
// ...
}
pinia.use(myPiniaPlugin)
扩充store
插件的任何属性 returned 都会被devtools自动跟踪,所以为了让
hello
在devtools中可见,如果你想调试它,请确保将它添加到store._customProperties
仅在开发模式 开发工具:
// 从上面的例子
pinia.use(({ store }) => {
store.hello = 'world'
// 确保您的打包器可以处理这个问题。 webpack 和 vite 应该默认这样做
if (process.env.NODE_ENV === 'development') {
// 添加您在 store 中设置的任何 keys
store._customProperties.add('hello')
}
})
请注意,每个 store 都使用
reactive
包装,自动展开任何 Ref (ref()
,computed()
, ...) 它包含了:这就是为什么您可以在没有
.value
的情况下访问所有计算属性以及它们是响应式的原因。
const sharedRef = ref('shared')
pinia.use(({ store }) => {
// 每个 store 都有自己的 `hello` 属性
store.hello = ref('secret')
// 它会自动展开
store.hello // 'secret'
// 所有 store 都共享 value `shared` 属性
store.shared = sharedRef
store.shared // 'shared'
})
添加新状态
请注意,插件中发生的状态更改或添加(包括调用
store.$patch()
)发生在存储处于活动状态之前,因此不会触发任何订阅。
- 在
store
上,因此您可以使用store.myState
访问它 - 在
store.$state
上,因此它可以在 devtools 中使用,并且在 SSR 期间被序列化。
onst globalSecret = ref('secret')
pinia.use(({ store }) => {
// `secret` 在所有 store 之间共享
store.$state.secret = globalSecret
store.secret = globalSecret
// 它会自动展开
store.secret // 'secret'
const hasError = ref(false)
store.$state.hasError = hasError
// 这个必须始终设置
store.hasError = toRef(store.$state, 'hasError')
// 在这种情况下,最好不要返回 `hasError`,因为它
// 将显示在 devtools 的 `state` 部分
// 无论如何,如果我们返回它,devtools 将显示它两次。
})
插件中使用订阅
pinia.use(({ store }) => {
store.$subscribe(() => {
// 在存储变化的时候执行
})
store.$onAction(() => {
// 在 action 的时候执行
})
})