全局状态的管理(Pinia)-学习整理

1,900 阅读5分钟

Pinia是官网推荐Vue3项目中作为全局状态管理的新工具。

官方推出的全局状态管理工具目前有Vuex和Pinia,Pinia的设计更贴近Vue3组合式API的用法。

从 2022-02-07 在 Vue 3 被设置为默认版本开始, Pinia 已正式被官方推荐作为全局状态管理的工具。

官网:pinia.vuejs.org/ 笔记整理-主要参考:vue3.chengpeiquan.com/

安装和启动

  • npm install pinia

    • package.json中可查看到Pinia和版本号
    • 在main.js中添加/多出两行代码
    •   import {createPinia} from 'pinia'//导入Pinia
    •   .use(createPinia())//启用Pinia

状态树的结构

作用Vue ComponentVuexPinia
数据管理datastatestate
数据计算computedgettersgetters
行为方法methodsmutations/actionsactions

补充说明:在行为方法部分去掉了 mutations (同步操作)和 actions (异步操作)的区分,更接近组件的结构,入门成本会更低一些。

创建Store(Pinia核心)

  • 特点:Store通过defineStore创建
  • 管理方案:在src下创建stores,并添加index.ts

    • 直接在index.ts中创建Store
    • 通过index.ts引入其他.ts文件,在其他.ts文件中创建Store
  • 入参形式,必须为Store指定一个唯一ID

    • 函数命名规范:以use开头(e.g. useUserStore\useGameStore)
    • 默认导出:是使用export const不是export default
    • //形式1:接收两个参数
      import {defineStore} from 'pinia'
      export const useStore = defineStore('main',{
          //Store选项
      })
      
      //形式2:接收一个参数
      import {defineStore} from 'pinia'
      export const useStore = defineStore({
          id:'main',
          //Store选项
      })
      

管理State

给Store添加state

  • 特点:通过箭头函数形式state:()=>({})state:()=>{return{}}返回数据
// src/stores/index.ts
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  // 写法1:不显式 return
  state: () => ({
    message: 'Hello World',
  }),
  
  // 写法2:显式 return
  state: () => {
    return {
      message: 'Hello World',
    }
  },
  // ...
})

手动指定类型

  • 特点:可通过as<>指定类型
  • 例子:[] as string[]<string[]>[]
// ...
export const useStore = defineStore('main', {
  state: () => {
    return {
      message: 'Hello World',
      // 通过 as 关键字指定 TS 类型
      randomMessages: [] as string[],
      // ...
      // 通过 as 关键字指定 TS 类型
      randomMessages: [] as string[],
      //Pinia 会帮推导成 never[]
      randomMessages: [],

    }
  },
  // ...
})

获取和更新state

  • Pinia数据是挂在store上(store.message),Vuex是在store.state上(store.state.message)
import { defineComponent,toRefs,toRef } from 'vue'
import { useStore } from '@/stores'
import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    // 像 useRouter 那样定义一个变量拿到实例
    const store = useStore()

    // 获取方式1:直接通过实例来获取数据(不推荐)
    console.log(store.message)
    //获取方式2:通过计算属性computed拿值传给template(message1)使用(only read)
    const message1=computed(()=>store.message)
    
    //更新数据方式1:定义computed变量是配置getter和setter
    const message = computed({
        get:()=>store.message,
        set(newVal){
            store.message=newVal
        }
    })
    message.value='new value'
    console.log(store.message)//new value
    
    //更新数据方式2,获取方式3:使用storeToRefs API(转换为ref变量)
    //通过storeToRefs拿到响应性的message
    const {message}=storeToRefs(store)
    console.log(message.value)
    message.value='new message-value'
    
    //更新数据方式3,获取方式4:使用Vue的toRefs或toRef API——Vue API
    const message = toRefs(store)//转换所有字段为ref变量
    const message = toRef(store,'message')//转换一个字段为ref变量
    
    //action方法(Pinia所有操作都在action,不区分同步和异步,Vuex更新数据需通过mutation提交 并且异步操作需通过action触发mutation)
    
    
    
    // 这种方式需要把整个 store 给到 template 去渲染数据
    return {
      store,
      message1,
      message
    }
  },
})

批量更新state(修改多个state数据)——$patch(Pinia API)

  • 写法:store.$patch(对象/函数)
// 继续用前面的数据,这里会打印出修改前的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"Hello World","randomMessages":[]}

/**
 * 注意这里,传入了一个对象
 */
store.$patch({
  message: 'New Message',
  randomMessages: ['msg1', 'msg2', 'msg3'],
})
/**
 * 注意这里,这次是传入了一个函数
 */
store.$patch((state) => {
  state.message = 'New Message'
  // 数组改成用追加的方式,而不是重新赋值
  for (let i = 0; i < 3; i++) {
    state.randomMessages.push(`msg${i + 1}`)
  }
})

// 这里会打印出修改后的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

全量更新state——store.$state

  • 特点:store.$state属性本身可以直接赋值,赋值时必须遵循state原有的数据和对应的类型。
  • 例子:更新数据(state还是会保持响应性)
store.$state = {
  message: 'New Message',
  randomMessages: ['msg1', 'msg2', 'msg3'],
}

重置state——$reset

  • 特点:用于重置整个state树为初始数据
  • 补充:可以和setTimeout结合使用
// 修改数据
store.message = 'New Message'
console.log(store.message) // 输出 New Message

// 3s 后重置状态
setTimeout(() => {
  store.$reset()
  console.log(store.message) // 输出最开始的 Hello World
}, 3000)

订阅state——$subscribe

  • 特点:类似于watch,它只会在state被更新时触发一次,组件被卸载时删除

    • $subscribe接受两个参数
    • 第一个参数是callback函数,必传;第二个入参时一些选项,可选
// $subscribe 部分的 TS 类型
// ...
$subscribe(
  callback: SubscriptionCallback<S>,
  options?: { detached?: boolean } & WatchOptions
): () => void
// ...
  • 添加订阅

    • 接收的第一个参数是必传的callback函数

    • callback有两个入参:mutation(本次事件的一些信息)、state(当前实例的state)

    • mutation包含以下数据

      • storeId:发布本次订阅通知的Pinia实例的唯一ID(创建Store时指定)
      • type:有3个值,返回direct代表直接更改数据;返回patch object代表时通过传入一个对象更改;返回patch function则代表是通过传入一个函数更改
      • events:触发本次订阅通知的事件列表
      • payload:通过传入一个函数更改时,传递进来的荷载信息,只有type为patch object时有
    • 补充:若希望组件被卸载时,不删除订阅,可传递第2个参数options(也可以搭配 watch API 的选项一起用)

// 可以在 state 出现变化时,更新本地持久化存储的数据
store.$subscribe(
    (mutation, state) => {
      localStorage.setItem('store', JSON.stringify(state))
    },
    {
    detached: true 
    }

)
  • 删除订阅

    • 使用场景:detached为true,需要手动卸载订阅unsubscribe()
    • 特点:与watch API 的机制非常相似, 它也是返回 一个取消监听的函数 用于移除指定的 watch
// 定义一个退订变量,它是一个函数
const unsubscribe = store.$subscribe(
  (mutation, state) => {
    // ...
  },
  { detached: true }
)

// 在合适的时期调用它,可以取消这个订阅
unsubscribe()

管理getters

  • 特点:Pinia的getters用于计算数据

给Store添加getter

  • 添加普通的getter

    • 特点:通过入参的state拿当前数据(Pinia官方推荐箭头函数)
    • // src/stores/index.ts
      import { defineStore } from 'pinia'
      
      export const useStore = defineStore('main', {
        state: () => ({
          message: 'Hello World',
        }),
        // 定义一个 fullMessage 的计算数据
        getters: {
          fullMessage : ( state ) =>  `The message is " ${state.message} ".` ,
        },
        // ...
      })
      
  • 添加引用getter的getter

    • 特点:

      • 引用另一个getter的值来返回数据,不能用箭头函数,需定义成普通函数
      • 普通函数内容通过this调用当前Store上的数据和方法
      • 普通函数的TS返回类型要显式标注
export const useStore = defineStore('main', {
  state: () => ({
    message: 'Hello World',
  }),
  getters: {
    fullMessage: (state) => `The message is "${state.message}".`,
    // 这个 getter 返回了另外一个 getter 的结果
    emojiMessage(): string {
      return `🎉🎉🎉 ${this.fullMessage}`
    },
  },
})
  • 给getter传递参数

    • 特点:本身不支持参数(与Vuex一样),支持返回一个具备入参的函数

    • import { defineStore } from 'pinia'
      
      export const useStore = defineStore('main', {
        state: () => ({
          message: 'Hello World',
        }),
        getters: {
          // 定义一个接收入参的函数作为返回值
          signedMessage: (state) => {
            return (name: string) => `${name} say: "The message is ${state.message}".`
          },
        },
        
        
        //调用时
          const signedMessage = store.signedMessage('Petter')
          console.log('signedMessage', signedMessage)
          // Petter say: "The message is Hello World".
        
      })
      
    • 注意点:

      • 这个getter只是调用的函数作用,不再有缓存
      • 通过变量定义这个getter数据,该变量只是普通变量,不具备响应性
      • // 通过变量定义一个值
        const signedMessage = store.signedMessage('Petter')
        console.log('signedMessage', signedMessage)
        // Petter say: "The message is Hello World".
        
        // 2s 后改变 message
        setTimeout(() => {
          store.message = '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都属于数据管理,读取和赋值是一样的

管理actions

  • 特点:Pinia只需要用actions就可解决数据操作;Vuex需要区分mutation/actions

给Store添加action(src/store/index.ts)

  • 特点:

    • 在actions中若访问当前实例的state或getter,通过this操作
    • this是指当前的Store实例
    • actions方法中有其他函数调用实例,需写成箭头函数(来提升this)
// src/stores/index.ts
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => ({
    message: 'Hello World',
  }),
  actions: {
    // 异步更新 message
    async updateMessage(newMessage: string): Promise<string> {
      return new Promise((resolve) => {
        setTimeout(() => {
          // 这里的 this 是当前的 Store 实例
          this.message = newMessage
          resolve('Async done.')
        }, 3000)
      })
    },
    // 同步更新 message
    updateMessageSync(newMessage: string): string {
      // 这里的 this 是当前的 Store 实例
      this.message = newMessage
      return 'Sync done.'
    },
  },
})

调用action

  • 特点:像调用普通函数一样(不需和Vuex一样执行commit或dispatch)
export default defineComponent({
  setup() {
    const store = useStore()
    const { message } = storeToRefs(store)

    // 立即执行
    console.log(store.updateMessageSync('New message by sync.'))

    // 3s 后执行
    store.updateMessage('New message by async.').then((res) => console.log(res))

    return {
      message,
    }
  },
})

添加多个Store(用于维护不同需求模块的数据状态)

目录结构及使用

目录机构推荐

src
└─stores
  │ # 入口文件
  ├─index.ts
  │ # 多个 store
  ├─user.ts
  ├─game.ts
  └─news.ts

以index.ts作为统一的入口文件,index.ts中内容可如下

export * from './user'
export * from './game'
export * from './news'

index.ts中其他文件写法,如./user

// src/stores/user.ts
export const useUserStore = defineStore('user', {
  // ...
})

使用时

import { useUserStore } from '@/stores'

在Vue组件/TS文件中使用

  • 注意点:切记每个 Store 的 ID 必须不同,如果 ID 重复,会以先定义的为有效值,后续定义的会和前面一样
  • 场景:目前有一个 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之间互相引用

// 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}!`,
  },
})
const messageStore = useMessageStore()
console.log(messageStore.greeting) // Welcome, Petter!

Pinia专属插件的使用

查找插件

插件的命名格式:pinia-plugin-*

官网:www.npmjs.com/search?q=pi…

pinia-plugin-persistedstate

  • 是一个让数据持久化存储的 Pinia 插件
  • 插件用法

    • 安装:npm i pinia-plugin-persistedstate
    • 激活(在main.ts)
    • // 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')
      
    • 在一个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,
      })