一文快速上手pinia,给你的知识树添砖加瓦
Pinia和Vuex的对比
相同点:都是vue的全局状态管理器
pinia的优势
- 更简单的写法,代码更清晰简洁,支持
composition api
和options api
语法 - 更完善的 typescript 支持,无需创建自定义复杂的包装类型来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断
- 非常轻量,只有1kb的大小
安装
官方文档:pinia.web3doc.top/core-concep…
yarn add pinia
// 或者使用 npm
npm install pinia
挂载pinia
src/main.ts
import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')
查看你的 package.json ,看看里面的 dependencies
是否成功加入了 Pinia 和它的版本号(下方是示例代码,以实际安装的最新版本号为准)
{
"dependencies": {
"pinia": "^2.0.11",
},
}
然后打开 src/main.ts
文件,添加下面那两行有注释的新代码:
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 Pinia
import App from '@/App.vue'
createApp(App)
.use(createPinia()) // 启用 Pinia
.mount('#app')
pinia的三个概念
- state →类似vue2中的data
- getter → 类似computed
- actions → 类似vuex中的mutaiton和action的结合体
基础结构及基础使用
src/store/index.ts
import { defineStore } from 'pinia' // 导入定义store的方法
export const useStore = defineStore('main', {
state: () => { return {// 要定义的数据} },
getters: { // 要定义的getters的方法},
actions: { // 要定义的actions的方法},
})
vue组件中使用
<template>
// 无论是state中的属性,getter或者action中的方法,都可以用 store.XXX 进行使用
<h1>{{ store.name }}</h1>
</template>
<script lang="ts">
import { useStore } from './store'
export default defineComponent({
setup() {
const store = useStore()
return { store }
},
})
</script>
创建Store
创建Store
Store 是使用 defineStore()
定义的,可以接收一个参数或者两个参数
- useStore 可以是 useUser、useCart 之类的任何名字
- 第一个参数(必要)是应用程序中 store 的唯一id,Pinia 使用它来将 store 连接到 devtools
- 第二个参数是配置项,里面包含项目为,
state
,getter
,action
建议:将返回的函数命名为 useXXX 是跨可组合项的约定,以使其符合你的使用习惯
src/store/index.ts
形式一:接收两个参数
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
// 其他的配置项...
})
形式二:接收一个参数
export const useStore = defineStore({
id:'main',
// 其他的配置项...
})
不论是哪种创建形式,都必须为 Store 指定一个唯一 ID
管理State:
定义state中的数据
- state为一个函数,其中返回使用的数据
- state中的属性都将自动推断其类型
建议:state它是通过一个箭头函数的形式来返回数据,并且能够正确的帮你推导 TypeScript 类型
src/store/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
name: 'Eduardo',
}
},
// 如果不显式return,则需要用 圆括号 括起来,于是上面的写法可以变成下面这个样子
state: () => ({
name: 'Eduardo',
}),
})
可能有同学会问: Vuex 可以用一个对象来定义 state 的数据, Pinia 可以吗? 答案是:不可以! state 的类型必须是
state?: (() => {}) | undefined
,要么不配置(就是 undefined ),要么只能是个箭头函数。
手动指定state中的数据类型
虽然 Pinia 会帮你推导 TypeScript 的数据类型,但可能不够用
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
name: 'Eduardo',
stringArr:[] // 预期数组为一个string类型的数组,但此时会被推断成 never[]
}
}
})
此时可以通过类型断言 as
断言数组类型
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
// 以下两种方法都可以
stringArr:[] as string[]
stringArr: <string[]>[]
}
}
})
获取和更新state
使用store的实例
用法上和 Vuex 很相似,但有一点区别是,数据直接是挂在 store
上的,而不是 store.state
上面
所以,可以直接通过store.name
获取到state中的name数据
import { defineComponent } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
const store = useStore() // 定义一个变量拿到实例
console.log(store.name) // 直接通过实例来获取数据
return {
store, // 这种方式你需要把整个 store 给到 template 去渲染数据
}
},
})
一些复杂数据写起来会比较长,推荐使用 computed API
和 storeToRefs API
等方式来获取
注意:不能直接通过 ES6 解构的方式( e.g.
const { message } = store
),那样会破坏数据的响应性。
更新state中的数据
const store = useStore()
store.name = '张三1'
使用computed API
下面这段代码是在 Vue 组件里导入我们的 Store ,并通过计算数据 computed
拿到里面的 message
数据传给 template 使用
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
// 像 useRouter 那样定义一个变量拿到实例
const store = useStore()
// 通过计算拿到里面的数据
const message = computed(() => store.message)
console.log('message', message.value)
// 传给 template 使用
return {
message,
}
},
})
</script>
这里的定义的 message
变量是一个只有 getter ,没有 setter 的 ComputedRef 数据,所以它是只读的。
如果你要更新数据怎么办?
- 可以通过提前定义好的 Store Actions 方法进行更新。
- 在定义 computed 变量的时候,配置好
setter
的行为:
// 其他代码和上一个例子一样,这里省略...
// 修改:定义 computed 变量的时候配置 getter 和 setter
const message = computed({
// getter 还是返回数据的值
get: () => store.message,
// 配置 setter 来定义赋值后的行为
set(newVal) {
store.message = newVal
},
})
// 此时不再抛出 Write operation failed: computed value is readonly 的警告
message.value = 'New Message.'
// store 上的数据已成功变成了 New Message.
console.log(store.message)
使用storeToRefs() API
问题:使用解构赋值从store中提取属性会失去响应式
解决:使用storeToRefs()
进行包裹
组件中:
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
// 包裹后的 name ,doubleCount 即为响应式
const { name, doubleCount } = storeToRefs(store)
return { name, doubleCount }
},
})
使用 toRefs API
storeToRefs() API 本身的设计就是类似于 toRefs
,所以你也可以直接用 toRefs 把 state 上的数据转成 ref 变量。
// 注意 toRefs 是 vue 的 API ,不是 Pinia
import { defineComponent, toRefs } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
const store = useStore()
// 跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理
const { message } = toRefs(store)
console.log('message', message.value)
return {
message,
}
},
})
像上面这样,对 store 执行 toRefs 会把 store 上面的 getters 、 actions 也一起提取,如果你只需要提取 state 上的数据,可以这样做
// 只传入 store.$state
const { message } = toRefs(store.$state)
使用 toRef API
toRef 是 toRefs 的兄弟 API ,一个是只转换一个字段,一个是转换所有字段,所以它也可以用来转换 state 数据变成 ref 变量。
// 注意 toRef 是 vue 的 API ,不是 Pinia
import { defineComponent, toRef } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
const store = useStore()
// 遵循 toRef 的用法即可
const message = toRef(store, 'message')
console.log('message', message.value)
return {
message,
}
},
})
批量更新state
问题:如果想一次更新多个数据
解决:通过$patch
进行批量更新,此时可以传入两种方式
批量修改:传入一个对象
const store = useStore()
store.$patch({
count: store.count + 1,
name: '张三2',
age: 50,
arr:[...store.arr , 4 ] // 数组则可以使用展开运算符
})
批量修改:传入一个函数
const store = useStore()
// state 是 useStore 中的state,此时就可以修改
store.$patch( state =>{
state.age = 1000
state.arr.push(5) // 此时可使用方法对数组进行操作
})
注意:传进去的函数只能是同步函数,不可以是异步函数!
全量更新state
简单的说就是把整个state进行替换了
const store = useStore()
store.$state({ // 使用 $state 进行替换
counter: 666,
name: 'Abalam',
})
注意:该操作不会使 state 失去响应性。
重置state
调用 $reset()
方法将状态 重置 到其初始值:
const store = useStore()
store.$reset()
订阅state
和 Vuex 一样, Pinia 也提供了一个用于订阅 state 的 $subscribe
API
订阅 API 的 TS 类型
在了解这个 API 的使用之前,先看一下它的 TS 类型定义
$subscribe(
callback: SubscriptionCallback<S>,
options?: { detached?: boolean } & WatchOptions
): () => void
它可以接受两个参数:
- 第一个入参是 callback 函数,必传
- 第二个入参是一些选项,可选
- 它还会返回一个函数,执行它可以用于移除当前订阅
添加订阅
$subscribe
API 的功能类似于 watch ,但它只会在 state 被更新的时候才触发一次,并且在组件被卸载时删除
它可以接受两个参数,第一个参数是必传的 callback 函数,一般情况下默认用这个方式即可
// 你可以在 state 出现变化时,更新本地持久化存储的数据
store.$subscribe((mutation, state) => {
localStorage.setItem('store', JSON.stringify(state))
})
callback 里面有 2 个入参
入参 | 作用 |
---|---|
mutation | 本次事件的一些信息 |
state | 当前实例的 state |
其中 mutation 包含了以下数据
字段 | 值 |
---|---|
storeId | 发布本次订阅通知的 Pinia 实例的唯一 ID(由 创建 Store 时指定) |
type | 有 3 个值:返回 direct 代表 直接更改数据;返回 patch object 代表是通过 传入一个对象更改;返回 patch function 则代表是通过 传入一个函数更改 |
events | 触发本次订阅通知的事件列表 |
payload | 通过 传入一个函数更改时,传递进来的荷载信息,只有 type 为 patch object 时才有 |
如果你不希望组件被卸载时删除订阅,可以传递第二个参数 options 用以保留订阅状态,传入一个对象
可以简单指定为 { detached: true }
store.$subscribe((mutation, state) => {
// ...
}, { detached: true })
移除订阅
默认情况下,组件被卸载时订阅也会被一并移除,但如果你之前启用了 detached
选项,就需要手动取消了
在启用 $subscribe
API 之后,会有一个函数作为返回值,这个函数可以用来取消该订阅。
用法非常简单,做一下简单了解即可
// 定义一个退订变量,它是一个函数
const unsubscribe = store.$subscribe((mutation, state) => {
// ...
}, { detached: true })
// 在合适的时期调用它,可以取消这个订阅
unsubscribe()
管理getters
给store添加getter
类似计算属性,通过函数来返回计算后的值
在 Pinia ,只能使用箭头函数,通过入参的 state
来拿到当前实例的数据
src/store/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('storeId', {
state: () => {
return {
name: 'Eduardo',
}
},
// ---------------------------我是分割线 0.0---------------------------
getters: {
getAdd: (state) => {
return state.age + 10
},
},
})
注意:
1、getters是一个对象
2、默认接收一个state参数,就是上面的state ,state是可选的
在getter中引用其他的getter
问题:有时候可能需要引用另一个getter的值返回数据,如何解决?
解决:此时将箭头函数转换为普通函数,函数内部通过 this
来调用当前 Store 上的数据和方法
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
getters: {
fullMessage: (state) => `The message is "${state.name}".`,
// 这个 getter 返回了另外一个 getter 的结果
emojiMessage(): string {
return `🎉🎉🎉 ${this.fullMessage}`
},
},
})
注意: 如果只是写JS,则在箭头函数中引用不会报错,但是如果是TS,Vscode则会抛出错误
给getter传递数据
getter 本身是不支持参数的,但和 Vuex 一样,支持返回一个具备入参的函数,用来满足需求。
定义一个getter
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => ({
name: 'Eduardo',
}),
getters: {
// 定义一个接收入参的函数作为返回值
signedMessage: (state) => {
return (name1: string) => `${name1} say: "The message is ${state.name}".`
},
},
})
调用
const signedMessage = store.signedMessage('Petter')
console.log('signedMessage', signedMessage)
// Petter say: "The message is Hello World".
这种情况下,这个 getter 只是调用的函数的作用,不再有缓存
// 通过变量定义一个值
const signedMessage = store.signedMessage('Petter')
console.log('signedMessage', signedMessage)
// Petter say: "The message is Hello World".
// 2s 后改变 message
setTimeout(() => {
store.name= 'New Message'
// signedMessage 不会变
console.log('signedMessage', signedMessage)
// Petter say: "The message is Hello World".
// 必须这样再次执行才能拿到更新后的值
console.log('signedMessage', store.signedMessage('Petter'))
// Petter say: "The message is New Message".
}, 2000)
获取和更新getter
getter 和 state 都属于数据管理,读取和赋值的方法是一样的
管理action
给store添加action
类似 methods 方法,Pinia 只需要用 actions
就可以解决各种数据操作,无需像 Vuex 一样区分为 mutations / actions
两大类
- action是一个对象
- state和getters 主要都是数据层面的,
action
处理逻辑 - this 指向的是当前的实例,下面的例子中
this
指向的就是useStore
注意:不要使用箭头函数定义action ,容易出现this指向问题
src/store/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('storeId', {
state: () => {
return {
name: 'Eduardo',
}
},
// ---------------------------我是分割线 0.0---------------------------
getters: {},
// ---------------------------我是分割线 0.0---------------------------
actions: {
// 同步更新 name 值
updateNameSync(newName: string): string {
this.name = newName
return '同步修改state中的name值完成'
},
// 异步更新 name 值
async updateName(newName: string) {
return new Promise((resolve) => {
setTimeout(() => {
// 这里的 this 是当前的 Store 实例
this.name = newName
resolve('异步修改完成')
}, 3000)
})
},
},
})
调用action
在 Pinia 像普通的函数一样使用即可,不需要和 Vuex 一样执行 commit 或者 dispatch,
export default defineComponent({
setup() {
const store = useStore()
// 立即执行
store.updateMessageSync('同步')
// 3s 后执行
store.updateMessage('异步')
},
})
添加多个 Store
目录结构建议
建议统一存放在 src/stores
下面管理,根据业务需要进行命名
src
└─stores
│ # 入口文件
├─index.ts
│ # 多个 store
├─user.ts
├─game.ts
└─news.ts
index.ts
export * from './user'
export * from './game'
export * from './news'
导入只需要
import { useUserStore } from '@/stores'
在 Vue 组件 / TS 文件里使用
这里我以一个比较简单的业务场景举例,希望能够方便的理解如何同时使用多个 Store
假设目前有一个 userStore
是管理当前登录用户信息, gameStore
是管理游戏的信息,而 “个人中心” 这个页面需要展示 “用户信息” ,以及 “该用户绑定的游戏信息”,那么就可以这样
import { defineComponent, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
// 这里导入你要用到的 Store
import { useUserStore, useGameStore } from '@/stores'
import type { GameItem } from '@/types'
export default defineComponent({
setup() {
// 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
const userStore = useUserStore()
const { userId, userName } = storeToRefs(userStore)
// 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
const gameStore = useGameStore()
const gameList = ref<GameItem[]>([])
onMounted(async () => {
gameList.value = await gameStore.queryGameList(userId.value)
})
return {
userId,
userName,
gameList,
}
},
})
再次提醒,切记每个 Store 的 ID 必须不同,如果 ID 重复,在同一个 Vue 组件 / TS 文件里定义 Store 实例变量的时候,会以先定义的为有效值,后续定义的会和前面一样
如果先定义了 userStore :
// 假设两个 Store 的 ID 一样
const userStore = useUserStore() // 是想要的 Store
const gameStore = useGameStore() // 得到的依然是 userStore 的那个 Store
如果先定义了 gameStore :
// 假设两个 Store 的 ID 一样
const gameStore = useGameStore() // 是想要的 Store
const userStore = useUserStore() // 得到的依然是 gameStore 的那个 Store
Store 之间互相引用
如果在定义一个 Store 的时候,要引用另外一个 Store 的数据,也是很简单,我们回到那个 message 的例子,我们添加一个 getter ,它会返回一句问候语欢迎用户
// src/stores/message.ts
import { defineStore } from 'pinia'
// 导入用户信息的 Store 并启用它
import { useUserStore } from './user'
const userStore = useUserStore()
export const useMessageStore = defineStore('message', {
state: () => ({
message: 'Hello World',
}),
getters: {
// 这里我们就可以直接引用 userStore 上面的数据了
greeting: () => `Welcome, ${userStore.userName}!`,
},
})
假设现在 userName
是 Petter ,那么你会得到一句对 Petter 的问候
const messageStore = useMessageStore()
console.log(messageStore.greeting) // Welcome, Petter!
专属插件的使用
如何查找插件
插件有统一的命名格式 pinia-plugin-*
,所以你可以在 npmjs 上搜索这个关键词来查询目前有哪些插件已发布
点击查询: pinia-plugin - npmjs
如何使用插件
- 这里以 pinia-plugin-persistedstateopen in new window 为例,这是一个让数据持久化存储的 Pinia 插件
- 插件也是独立的 npm 包,需要先安装,再激活,然后才能使用。
- 激活方法会涉及到 Pinia 的初始化过程调整,这里不局限于某一个插件,通用的插件用法如下(请留意代码注释):
// src/main.ts
import { createApp } from 'vue'
import App from '@/App.vue'
import { createPinia } from 'pinia' // 导入 Pinia
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入 Pinia 插件
const pinia = createPinia() // 初始化 Pinia
pinia.use(piniaPluginPersistedstate) // 激活 Pinia 插件
createApp(App)
.use(pinia) // 启用 Pinia ,这一次是包含了插件的 Pinia 实例
.mount('#app')
使用前
Pinia 默认在页面刷新时会丢失当前变更的数据,没有在本地做持久化记录
// 其他代码省略
const store = useMessageStore()
// 假设初始值是 Hello World
setTimeout(() => {
// 2s 后变成 Hello World!
store.message = store.message + '!'
}, 2000)
// 页面刷新后又变回了 Hello World
使用后
按照 persistedstate 插件的文档说明,我们在其中一个 Store 启用它,只需要添加一个 persist: true
的选项即可开启:
// src/stores/message.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
const userStore = useUserStore()
export const useMessageStore = defineStore('message', {
state: () => ({
message: 'Hello World',
}),
getters: {
greeting: () => `Welcome, ${userStore.userName}`,
},
// 这是按照插件的文档,在实例上启用了该插件,这个选项是插件特有的
persist: true,
})
回到我们的页面,现在这个 Store 具备了持久化记忆的功能了,它会从 localStorage 读取原来的数据作为初始值,每一次变化后也会将其写入 localStorage 进行记忆存储
// 其他代码省略
const store = useMessageStore()
// 假设初始值是 Hello World
setTimeout(() => {
// 2s 后变成 Hello World!
store.message = store.message + '!'
}, 2000)
// 页面刷新后变成了 Hello World!!
// 再次刷新后变成了 Hello World!!!
// 再次刷新后变成了 Hello World!!!!
完整使用案例
src/store/index.ts
import { defineStore } from 'pinia' // 导入定义store的方法
export const useStore = defineStore('main', {
state: () => {
return {
name: '张三',
age: 11,
sex: '男',
count: 0,
arr: [1, 2, 3],
}
// 也可以定义为
// state: () => ({ count: 0 })
},
getters: {
getAdd: (state) => { return state.age + 10 },
// 如果不使用可选参数state,可以用this,但此时需要手动对指定返回参数类型,
getAdd1(): number { return this.age + 10 },
},
actions: {
// this 指的是当前的实例,就是useCounterStore
addAction(num: number) {
this.count += num // 传递的参数
this.age = 500
this.arr.push(6)
// 或者使用
// this.$patch()
// this.$patch((state) => {})
},
},
})
组件中使用实践
1、官方中使用:
<template>
<h1>{{ name }}</h1>
<h1>{{ age }}</h1>
<h1>{{ sex }}</h1>
<h1>{{ count }}</h1>
<h1>{{ arr }}</h1>
<h1>{{ getAdd }}</h1>
<button @click="changeState">修改</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { storeToRefs } from 'pinia' // 如果直接解构,则会失去响应式,需要导入storeToRefs
import { useStore } from './store' // 导入store暴露出的函数
export default defineComponent({
setup() {
// 实例化
const store = useStore()
// 解构需要使用 storeToRefs 进行包裹,否则会失去响应式的特性
const { name, age, sex, count, arr, getAdd } = storeToRefs(store)
const changeState = () => {
// 调用 action 中的方法
store.addAction(10)
}
return {
changeState, name, age, sex, count, arr, getAdd
}
},
})
</script>
2、为了解决解构过程中出现需要再次赋值导出的问题,可以使用ES6写法进行优化,
<template>
<h1>{{ name }}</h1>
<h1>{{ age }}</h1>
<h1>{{ sex }}</h1>
<h1>{{ count }}</h1>
<h1>{{ arr }}</h1>
<h1>{{ getAdd }}</h1>
<button @click="changeState">修改</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { storeToRefs } from 'pinia' // 如果直接解构,则会失去响应式,需要导入storeToRefs
import { useStore } from './store' // 导入store
export default defineComponent({
setup() {
const store = useStore()
const changeState = () => {
store.addAction(10)
}
return {
changeState,
// 通过 展开运算符 + storeToRefs 实现直接导出,无需解构
...storeToRefs(useStore()),
}
},
})
</script>
转载自:https://juejin.cn/post/7293826641363697703