Pinia,一个替代Vue.js的轻量级状态管理库

5,978 阅读8分钟

Pinia homepage screenshot

为你的Vue.js应用程序创建一个储存器的另一个选择是Pinea。在谈到Vue的储存器时,Pinea无疑是新出现的。它是由Vue核心团队成员Eduardo San Martin Morote发起的,第一次提交到repo是在2019年11月18日。Pinia拥有一个直观的API和许多你所期望的Vue.js轻量级状态管理库的功能。

Pinia同时支持Vue 2和Vue 3,但我在下面展示的例子都是使用Pinia 2(目前处于alpha阶段)的Vue 3。Vue 2和Vue 3的Pinia有一些细微的差别,所以请查看Pinia官方文档以了解更多信息

安装与设置

要安装Pinia,你可以使用npm或yarn。

yarn add [email protected]
# or with npm
npm install [email protected]

安装完成后,你需要从Pinea库中导入createPinia ,并在你的Vue应用中用use() 方法将其添加为Vue插件。

// main.js
import { createPinia } from 'pinia'
//...
createApp(App)
.use(createPinia())
.mount('#app')

现在你已经准备好开始制作商店了!

商店的定义

Pinia中的储存器与Vuex中的储存器的处理方式有些不同。在Vuex中,整个应用程序都有一个主要的存储器(尽管你可以根据需要把它分成几个模块)。另一方面,Pinia是开箱即用的模块化。没有单一的主存储空间,而是创建不同的存储空间,并为它们命名,这对你的应用程序是有意义的。例如,我们可以创建一个登录用户的存储器。

// store/loggedInUser.js
import { defineStore } from 'pinia'

export const useLoggedInUserStore = defineStore({
  // id is required so that Pinia can connect the store to the devtools
  id: 'loggedInUser',
  state: () =>({}),
  getters: {},
  actions:{}
})

要访问loggedInUserStore,我们必须在我们想使用它的组件中导入它,并在setup函数中调用useLoggedInUserStore() ,将其设置为我们选择的变量(在本例中为user)。

//AppComponent.vue
<script>
import { useLoggedInUserStore } from "@/store/loggedInUser";
export default{
  setup(){
    const user = useLoggedInUserStore()
  }
}
</script>

这与Vuex大相径庭,Vuex让商店实例在你的应用程序实例上自动可用。然而,Pinia的方式也让开发者和你的IDE更清楚地知道商店的来源,因为它是一个标准的Javascript模块导入。

如果你不喜欢使用组合API和设置函数,你仍然可以使用Pinea的一些辅助函数。我们将在下面详细介绍这些功能。

状态

设置好商店后,是时候定义一些状态了。为了做到这一点,我们在存储器中设置了一个状态属性,该函数返回一个持有不同状态值的对象。这与我们在组件中定义数据的方式非常相似。事实上,唯一的区别是属性名称:state vs data。

//store/loggedInUser.js
//...
state: ()=>({ name: 'John Doe', email: '[email protected]', username: 'jd123'})

现在,为了从组件中访问loggedInUserStore的状态,我们只需直接引用我们之前创建的用户常量上的状态属性。完全没有必要从存储中嵌套到一个状态对象,如果你问我,结果是相当优雅的。

// AppComponent.vue
<template>
  <h1>Hello, my name is {{user.name}}</h1>
</template>
<script>
setup(){
  const user = useLoggedInUserStore()
  // acces it directly in setup
  // don't have to use user.state.name
  user.name // John Doe
  // or return it to access it in template
  return {user}
},
</script>

有一点很重要,如果你喜欢破坏,你可能会看一下上面的代码,然后想,"我可以让它变得更甜!"然而,这在user 上是行不通的,因为它将导致状态失去反应性。

❌ const {name, email} = useLoggedInUserStore()

最后,如果你对Object API比较熟悉,并且不喜欢使用setup函数,你可以使用Pinea的mapState函数来访问商店的状态。与Vuex不同的是,作为第一个参数,你必须告诉mapState函数要从哪个商店映射,然后作为第二个参数,要映射哪些状态属性。

//AppComponent.vue
<template>
  <h1>Hello, my name is {{name}}</h1>
</template>
<script>
import {mapState} from 'pinia'
export default{
  computed:{
    ...mapState(useLoggedInUserStore, ['name'])
  }
}
</script>

在Pinea中定义状态感觉非常熟悉,因为我们都习惯于在我们的组件上定义数据,并且在商店中直接访问它也感觉很直观。虽然手动将你的商店导入到你需要的组件中可能需要一点时间来适应,但它实际上提供了很好的透明度,感觉就像正常的Javascript。

获取器

Pinia中的获取器与Vuex中的获取器以及组件中的计算属性的作用相同。从Vuex的获取器转移到Pinia的获取器并不是一个很大的思维跳跃。除了Pinea中的getters,你可以通过两种不同的方式访问状态,它们看起来基本相同。

访问getter的第一种方式是通过 "this "关键字。这适用于传统的函数声明和方法速记。然而,由于箭头函数处理 "this "关键字范围的方式,它对箭头函数不起作用。

import { defineStore } from 'pinia'

export const usePostsStore = defineStore({
  id: 'PostsStore',
  state: ()=>({ posts: ['post 1', 'post 2', 'post 3', 'post 4'] }),
  getters:{

    // traditional function
    postsCount: function(){
      return this.posts.length
    },
    // method shorthand
    postsCount(){
      return this.posts.length
    },

    // cannot access state using "this" with an arrow functionpostsCount: ()=> this.posts.length
  }
})

Learn Vue.js 3 With Vue School

访问getter的第二种方式是通过getter函数的状态参数。这是为了鼓励使用箭头函数来实现简短、精确的获取器。

import { defineStore } from 'pinia'

export const usePostsStore = defineStore({
  //...
  getters:{
    // arrow function
    postsCount: state => state.posts.length,
  }
})

另外,Pinia并不像Vuex那样通过第二个函数参数来暴露其他getter,而是通过 "this "关键字。

import { defineStore } from 'pinia'

export const usePostsStore = defineStore({
  //...
  getters:{
    // use "this" to access other getters (no arrow functions)
    postsCountMessage(){ return `${this.postsCount} posts available` }
  }
})

一旦定义了getters,它们就可以在setup函数中作为存储实例的属性被访问,就像状态属性一样,不需要在getters对象下访问。

// FeedComponent.vue
<template>
  <p>{{postsCount}} posts available</p>
</template>

<script>
import { usePostsStore } from "@/store/PostsStore";

export default {
  setup() {
    const PostsStore = usePostsStore();
    return { postsCount: PostsStore.postsCount };
  }
};
</script>

如果你喜欢选项API而不是组合API,你可以使用Pinea提供的mapState帮助器。请注意,这个助手被称为mapState,而不是mapGetters,因为Pinia以完全相同的方式访问这两者。

<template>
  <p>{{postsCount}} posts available</p>
</template>

<script>
import { mapState } from 'pinia'
import { usePostsStore } from "@/store/PostsStore";

export default {
  computed:{
    ...mapState(usePostsStore, ['postsCount'])
  }
};
</script>

行动

与Vuex不同,Pinia只提供了一个单一的途径来制定定义如何改变状态的规则。放弃了突变,Pinia只依赖于动作。从Vuex的工作来看,这是一个鲜明的对比,但也是一个令人耳目一新的工作方式。不仅没有需要建立突变的模板代码,而且动作也不是通过带有魔法字符串的调度方法来调用,而是像其他方法一样(是的,如果你的代码编辑器支持自动完成/提示功能,它将与Pinea动作一起使用!)。

这里有一些更多的信息和最佳实践,当它涉及到Pinea的动作时。

行动。

  • 可以通过组件或其他动作来调用
  • 可以从其他商店行动中调用
  • 可以直接在商店实例上调用(不需要 "调度 "方法
  • 可以是异步的或同步的
  • 可以有任意多的参数
  • 可以包含关于如何改变状态的逻辑
  • 可以直接用this.propertyName 来改变状态属性,或者使用$patch方法将多个状态变化组合在一起,以便在Vue devtools的时间轴上进行单一输入
import { defineStore } from 'pinia'

export const usePostsStore = defineStore({
  id: 'PostsStore',
  state: ()=>({ 
    posts: ['post 1', 'post 2', 'post 3', 'post 4'],
    user: { postsCount: 2 },
    errors: []
  }),
  getters:{
    postsCount: state => state.posts.length,
  },
  actions:{
    async insertPost(post){
      // contains logic for altering differenct pieces of state
      try{
        await doAjaxRequest(post)

        // directly alter the state via the action and 
    // change multiple pieces of state
        this.posts.push(post)
        this.user.postsCount++

    // OR alternatavley use .$patch to group change of posts and user.postsCount in devtools timeline
    this.$patch((state) => {
          state.posts.push(post)
          state.user.postsCount++
        })
      }catch(error){
        this.errors.push(error)
      }

    }
  }
})

一旦你定义了一个商店的动作,它们就可以像状态和getters一样在setup方法中被访问。

// PostEditorComponent.vue
<template>
  <input type="text" v-model="post" />
  <button @click="insertPost(post)">Save</button>
</template>
<script>
import { usePostsStore } from '@/store/PostsStore';
export default{
  data(){
    return { post: '' }
  }, 
  setup(){
    const PostsStore = usePostsStore()
    return { insertPost: PostsStore.insertPost }
  },
}
</script>

再说一次,如果你喜欢选项API,你可以跳过设置函数,使用Pinia的辅助函数mapActions 来访问存储器的动作。

// PostEditorComponent.vue
<template>
  <input type="text" v-model="post" />
  <button @click="insertPost(post)">Save</button>
</template>
<script>
import {mapActions} from 'pinia'
import { usePostsStore } from '@/store/PostsStore';
export default{
  data(){
    return { post: '' }
  }, 
  methods:{
    ...mapActions(usePostsStore, ['insertPost'])
  }
}
</script>

在模块中进行组织

如前所述,Pinia本质上是模块化的。没有单一的根存储器。Pinia文档称你创建的所有不同的存储实例为 "存储",但你也可以把它们看作Vuex的模块。它们的作用是一样的,都是基于领域逻辑来组织你的存储器。

当你需要从一个商店访问任何东西时,你所要做的就是导入它并使用它。这一理念适用于两个组件的设置方法,以及其他商店。我不会深究例子(你可以在官方文档中看到关于从另一个商店访问一个商店的获取器从另一个商店访问一个商店的动作的例子),但只需说,它是非常直观的。它的行为就像普通的Javascript模块。

Vue开发工具

就开发工具而言,Pinia在这方面做得很好。在Vue 2中,Pinia支持在Vuex标签中查看状态,甚至可以进行时间旅行。我承认时间旅行的标签没有你在Vuex中使用突变时那么好,但我认为没有办法绕过它,因为你直接改变了状态而没有调用一个命名的突变。在devtools中,你看到的不是突变的名字,而是一个标签,告诉你这个变化来自哪个存储器,以及它是一个补丁还是一个 "到位 "的变化(也就是说,直接在this.propName )。

Pinia devtools screenshot

不过对我来说,这是一个值得做的权衡。我阅读和编写代码的频率远远高于我在开发工具中使用时间旅行。我更愿意跳过那些突变的模板,写出像PostsStore.insertPost(post) ,可以自动完成的代码。

至于Vue 3,在2021年5月写这篇文章时,Pinia只支持在devtools中查看状态,不支持时间旅行功能。然而,这实际上比Vuex为Vue 3提供的更多,因为最新的devtools中还完全不支持它。

值得注意的功能

为了总结Pinia,让我们快速回顾一下它最值得注意的特点,以帮助你决定实施最适合你和你的项目的存储器。

  • 由Vue.js核心团队成员负责维护
  • 感觉更像普通的老式javascript,导入模块,以方法形式调用动作,直接访问商店的状态,等等。
  • 摒弃了对突变模板的需求
  • 与Vue Devtools集成

总结

虽然是Vue生态系统的新成员,但Pinea被证明是一个有前途的商店解决方案,具有直观的API。请记住,它仍然处于开发的早期阶段,没有像Vuex那样经过时间的考验,也没有像整个社区那样有很好的文档。因此,它可能不适合大规模、长期运行的项目,但可能正是你的下一个小规模或一次性的Vue 3项目所需要的。而且,当它达到稳定版本时,它当然有可能满足你所有的项目需求。

尽管Vuex和Pinia都有其优点和缺点,但还有更多的东西要做。在下一课中,我们将看看Vue.observable() ,如何作为你的Vue 2项目的主卷库。

Learn Vue.js 3 With Vue School