Pinia:Vue新一代的状态管理器

8,312 阅读5分钟

Pinia简介

简介

Pinia 是 Vue 新一代的轻量级状态管理库,相当于Vuex,也是Vue核心团队推荐的状态管理库。

并且实现了一些Vuex 5的RFCs,同时支持 Vue2Vue3

未来很有可能替代Vuex,比Vuex更容易上手。


名字来源

Pinia是西班牙语中菠萝一词最相似的英语发音:piña。菠萝实际上是一组单独的果实,它们结合在一起形成一个水果。与store类似,每个store都是单独存在的,但它们最终都是相互关联的。它也是一种美味的热带水果,原产于南美洲。


诞生

Pinia从2019年11月左右,开始尝试重新使用Composition API 设计Vue Store

Pinia 试图尽可能接近 Vuex 的理念,旨在测试 Vuex 下一次迭代的一个方案。目前,Vuex 5的open RFC,API与Pinia的API非常相似,这说明Pinia成功了。

注意,Pinia的作者(Eduardo),也是Vue.js核心团队的一员,积极参与 RouterVuex 等API的设计。设计 Pinia 的目的是想重新设计使用store的体验,同时保持Vue的容易理解的理念。将Pinia的API与Vuex保持尽可能的接近,因为它不断向前发展,使人们能够方便地迁移到Vuex,甚至在未来融合两个项目。

翻译自:pinia.vuejs.org/introductio…


特性

Pinia具有以下几点特性:

  • 直观,像定义components一样地定义 store
  • 完整的Typescript支持
  • 去除 mutations,只有 state,getters,actions
  • actions支持同步和异步
  • Vue Devtools支持Pinia,提供更好的开发体验
  • 能够构建多个 stores ,并实现自动地代码拆分
  • 极其轻量(1kb),甚至感觉不到它的存在

图解pinia

image.png

对比vuex

image.png

安装Pinia

下述demo使用vue3, 先用vite快速创建一个vue项目:

npm init vite@latest

安装pinia

npm install pinia

src/main.ts 文件,使用Vue.use()方法将pinia作为插件使用:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

createApp(App).use(createPinia()).mount('#app')

store定义

defineStore

Pinia是通过defineStore()这个方法来定义的,它的第一个参数为当前store的id(store名称),需要保证唯一。

创建store文件:common.ts

import { defineStore } from 'pinia'
export default defineStore('common', {
    // other options
});

useStore

通过 import 导入 javascript 模块的方式引入,引入后,直接使用变量接收即可。

<script setup lang="ts">
import useCommonStore from '../store/common';
// setup内不用导出,定义变量即可使用
const common = useCommonStore();
</script>

引入后,F12打开Vue Devtools查看,如下图所示:

可以看到定义的变量以及pinia定义的store

image.png


对比Vuex

从以上的Pinia定义store和使用store,可以看出,Pinia不同于Vuex

Vuex

  • Vuexstore需要一个主入口
  • 通过modules属性,拆分成不同的模块
  • 自动挂载在Vue实例上,通过this.$store去调用或者mapGetters等方法

Pinia

  • Piniastore不需要主入口
  • 直接创建不同的store文件即可实现多模块
  • 在使用时,直接通过javascript的模块导入,即可使用,可方便看到从哪个文件导入

State(数据)

state存储store的数据部分,Piniastate是一个返回不同data对象的函数,完整类型推断建议使用箭头函数。

非常类似于我们组件定义中的data项。


定义state

store/common.ts

export default defineStore('todo', {
    state: () => {
        return {
            name: '小花的store',
            list: [],
        }
    }
});

使用state

javascript中的模块导出的方式导出store数据,state中的数据均可通过变量.state数据名获取:

直接获取:

<script setup lang="ts">
import useCommonStore from '../store/common';

const common = useCommonStore();
</script>
<template>
    <div>
        {{ common.name }}
    </div>
</template>

解构获取:

store是一个reactive响应式对象,直接解构会使其失去响应式,类似setup中的props,为了既可解构又可保持其响应式,可使用storeToRefs,它将为每个reactive属性创建refs

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import useCommonStore from '../store/common';

const { name } = storeToRefs(useCommonStore());
</script>
<template>
    <div>
        {{ name }}
    </div>
</template>

修改state

可以通过以下三种方式进行修改state:

  1. 直接修改state:
<script setup lang="ts">
import useCommonStore from '../store/common';

const common = useCommonStore();

common.count++
</script>
  1. $patch以对象形式修改:
<script setup lang="ts">
import useCommonStore from '../store/common';

const common = useCommonStore();
......
common.$patch({
    name: '小花的store',
    list: [
        {
            name: '旺仔牛奶',
            stock: 100,
            price: 5
        },
        {
            name: '大辣片',
            stock: 1000,
            price: 5
        }
    ],
})
</script>

缺点: 如果只需修改state数据中的某一项,仍然需要将整个对象传给store。

  1. $patch接收函数:

接收一个函数做为参数,函数参数为state对象,可直接针对要修改的属性进行修改。

<script setup lang="ts">
import useCommonStore from '../store/common';

const common = useCommonStore();
...
common.$patch(state => {
    state.list[1].stock = 1000;
});
</script>

替换state

可以通过设置 store$state 属性为一个新对象,来替换 store 的整个state

// 重新设置$state的值为一个新的对象
import useCommonStore from '../store/common';

const common = useCommonStore();
......
common.$state = {
    name: '小花的store new',
    grade: 'A',
    list: [
        {
            name: '旺仔牛奶new',
            stock: 100,
            price: 5
        },
        {
            name: '大辣片new',
            stock: 100,
            price: 5
        }
    ],
}

重置state

可以通过 store$reset() 方法重置 state 的值为初始值,比如修改了name、库存等,可一键重置,将值初始化为初始状态的值。

import useCommonStore from '../store/common';

const common = useCommonStore();

common.$reset();

订阅state

通过store$subscribe()方法监听state及其变化,类似于Vuexsubscribe方法。与常规watch()相比,使用$subscribe()的优点是,在patch之后,subscribe只会触发一次。

import useCommonStore from '../store/common';

const common = useCommonStore();
......
// 监听整个store
common.$subscribe((mutation, state) => {
    console.log('mutation: ', mutation);
    console.log('state: ', state);
})

当我们触发页面上更改 store 的操作时,则会触发 subscribe 监听,监听函数有两个参数 mutationstate

mutation:包含3个参数

type:操作类型,'direct' | 'patch object' | 'patch function'

storeId:操作的store id

events:操作的事件详情,包括针对的数据、新值、旧值等

state:Proxy类型的对象

image.png


state订阅与组件分离

默认情况下,状态订阅绑定到添加它们的组件(如果store是在组件的setup()中)。也就是说,当卸载组件时,它们将自动删除。如果要在卸载组件后保留它们,可将{detached:true}作为第二个参数传递,以从当前组件分离state订阅。

export default {
  setup() {
    const someStore = useSomeStore()

    // this subscription will be kept after the component is unmounted
    someStore.$subscribe(callback, { detached: true })

    // ...
  },
}

pinia实例上监听整个state

watch(
  pinia.state,
  (state) => {
    // persist the whole state to the local storage whenever it changes
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

Getters(计算数据)

Getters 完全等同于 storestatecomputed values。可以使用defineStore() 中的 getters 属性定义它们。

接收state作为第一个参数,推荐使用箭头函数。

定义getters

export default defineStore('common', {
    // getters是一个对象
    getters: {
        doubleCount: (state) => state.count * 2,
        newList: (state) => {
            return state.list.filter(item => item.price > 5)
        }
    }
});

使用getters

<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
const { doubleCount, newList } = storeToRefs(useCommonStore());
</script>
<template>
    <div> {{ doubleCount }} </div>
    <div> {{ newList }} </div>
</template>

访问其他getters

与计算属性一样,可以组合多个 getters,通过this.去访问其他getters

export default defineStore('common', {
    // getters是一个对象
    getters: {
        doubleCount: (state) => state.count * 2,
        newList: (state) => {
            return state.list.filter(item => item.price > 5)
        },
        // 必须显示定义返回的类型,内部通过this访问store中的数据
        doubleCountPlus(): number {
            // autocompletion and typings for the whole store ✨
            return this.doubleCount * 2 + 1
        },
    }
});

给getters传递参数

getters只是一个计算属性,因此不可能向其传递任何参数。但是,可以从getters返回一个函数来接受任何参数。

export default defineStore('common', {
    getListById: state => {
       return (id: number) => state.list.find((item) => item.id === id);
    }
});
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
const { getListById } = storeToRefs(useCommonStore());
</script>
<template>
    <div> {{ getListById(2) }} </div>
</template>

注意:使用这种方式的时候,getters 不再被缓存,只是函数调用。


访问其他store的getters

访问其他 storegetters,可以直接引入其他 store 文件,在 getters 内部使用它。

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

在options API中使用

不在setup中使用:

使用 mapState 访问store中的数据。

import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'

export default {
  computed: {
    // gives access to this.doubleCounter inside the component
    // same as reading from store.doubleCounter
    ...mapState(useCounterStore, ['doubleCount'])
    // same as above but registers it as this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'doubleCounter',
      // you can also write a function that gets access to the store
      double: store => store.doubleCount,
    }),
  },
}

Actions(方法)

Actions 相当于组件中的方法,可以用 defineStore() 中的 actions 属性定义,非常适合定义业务逻辑。

定义actions

export const useStore = defineStore('common', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})

使用actions

同步的方式:

<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
</script>
<template>
    <button @click="common.increment()">触发actions</button>
</template>

异步的方式:

import { mande } from 'mande'

const api = mande('/api/users')

export const useUsers = defineStore('users', {
  state: () => ({
    userData: null,
    // ...
  }),

  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
        showTooltip(`Welcome back ${this.userData.name}!`)
      } catch (error) {
        showTooltip(error)
        // let the form component display the error
        return error
      }
    },
  },
})


访问其他store的actions

import { 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')
      }
    },
  },
})

在options Api中使用

不在setup中使用:

使用 mapActions 访问store中的数据。

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' }),
  },
}

订阅actions

使用store.$onAction()订阅actions,传递给它的回调函数在action之前执行,afteractions resolves之后执行,onErroractions抛出异常和错误的时候执行。

const unsubscribe = someStore.$onAction(
  ({
    name, // name of the action
    store, // store instance, same as `someStore`
    args, // array of parameters passed to the action
    after, // hook after the action returns or resolves
    onError, // hook if the action throws or rejects
  }) => {
    // a shared variable for this specific action call
    const startTime = Date.now()
    // this will trigger before an action on `store` is executed
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // this will trigger if the action succeeds and after it has fully run.
    // it waits for any returned promised
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // this will trigger if the action throws or returns a promise that rejects
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// manually remove the listener
unsubscribe()

$onAction 一般是在组件的 setup 建立,它会随着组件的 unmounted 而自动取消。如果你不想让它取消订阅,可以将第二个参数设置为 true

someStore.$onAction(callback, true)

Plugins(插件)

通过一些low level Api(底层API),可以对pinia store进行扩展:

  • 给 store 添加新的属性
  • 给 store 添加新的选项
  • 给 store 添加新的方法
  • 包装已经存在的方法
  • 修改或者删除 actions
  • 基于特定的 store 做扩展

Plugins 通过 pinia.use() 添加到 pinia 实例中。

store目录下创建pinia-plugin.ts,存储plugins相关:

import { PiniaPluginContext } from "pinia";
export default function piniaPlugin(context: PiniaPluginContext) {
    console.log('context:', context);
}

然后在main.ts中引入插件

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPlugin from './store/pinia-plugin'
import App from './App.vue'

const pinia = createPinia();
pinia.use(piniaPlugin);

createApp(App).use(pinia).mount('#app')

运行后,打印出来的context如下图:

image.png

context内容分为4部分:

  •  createApp() 中创建的 app 实例
  • defineStore 中的配置
  • createPinia() 中创建的 pinia 实例
  • 当前 store 对象

我们可以基于上面的context进行扩展。

总结

本文对pinia的诞生、安装、stategettersactionsplugins,都进行了详细介绍。

pinia去掉了mutation,支持直接导入使用,配合vue3 setup,使得store管理起来更加的方便。

如有不对之处,欢迎批评指正!也可以留言相互交流学习!

参考文档

  1. mp.weixin.qq.com/s/odou45XJt…
  2. juejin.cn/post/706513…