likes
comments
collection
share

一文快速上手pinia,给你的知识树添砖加瓦

作者站长头像
站长
· 阅读数 62

Pinia和Vuex的对比

相同点:都是vue的全局状态管理器

pinia的优势

  1. 更简单的写法,代码更清晰简洁,支持 composition apioptions api 语法
  2. 更完善的 typescript 支持,无需创建自定义复杂的包装类型来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断
  3. 非常轻量,只有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() 定义的,可以接收一个参数或者两个参数

  1. useStore 可以是 useUser、useCart 之类的任何名字
  2. 第一个参数(必要)是应用程序中 store 的唯一id,Pinia 使用它来将 store 连接到 devtools
  3. 第二个参数是配置项,里面包含项目为,stategetteraction

建议:将返回的函数命名为 useXXX 是跨可组合项的约定,以使其符合你的使用习惯

src/store/index.ts

形式一:接收两个参数

import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  // 其他的配置项...
 })

形式二:接收一个参数

 export const useStore = defineStore({
   id:'main', 
   // 其他的配置项...
 })

不论是哪种创建形式,都必须为 Store 指定一个唯一 ID

管理State:

定义state中的数据

  1. state为一个函数,其中返回使用的数据
  2. 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 APIstoreToRefs 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 数据,所以它是只读的。

如果你要更新数据怎么办?

  1. 可以通过提前定义好的 Store Actions 方法进行更新。
  2. 在定义 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

它可以接受两个参数:

  1. 第一个入参是 callback 函数,必传
  2. 第二个入参是一些选项,可选
  3. 它还会返回一个函数,执行它可以用于移除当前订阅

添加订阅

$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通过 传入一个函数更改时,传递进来的荷载信息,只有 typepatch 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
评论
请登录