跳至内容

简介

Pinia 开始 作为一项实验,旨在重新设计 Vue 的 Store 在 Composition API 2019 年 11 月左右。从那时起,最初的原则保持不变,但 Pinia 适用于 Vue 2 和 Vue 3 并且不需要您使用 Composition API。API 对两者来说都是一样的,除了安装SSR,这些文档针对 Vue 3 并包含有关 Vue 2 的说明,只要有必要,就可以让 Vue 2 和 Vue 3 用户阅读!

我为什么要使用 Pinia?

Pinia 是 Vue 的一个 Store 库,它允许您在组件/页面之间共享状态。如果您熟悉 Composition API,您可能在想,您已经可以使用简单的 export const state = reactive({}) 来共享全局状态。这对于单页应用程序来说是正确的,但如果它是服务器端渲染的,则会使您的应用程序暴露于 安全漏洞。但即使在小型单页应用程序中,使用 Pinia 也会为您带来很多好处

  • 测试工具
  • 插件:使用插件扩展 Pinia 功能
  • 对 TypeScript 的适当支持或自动完成,适用于 JS 用户
  • 服务器端渲染支持
  • Devtools 支持
    • 跟踪 Action 和 Mutation 的时间线
    • Store 出现在使用它们的组件中
    • 时间旅行和更轻松的调试
  • 热模块替换
    • 修改您的 Store 而不重新加载页面
    • 在开发过程中保留任何现有状态

如果您仍然有疑问,请查看 官方 Mastering Pinia 课程。在开始时,我们将介绍如何构建我们自己的 defineStore() 函数,然后我们将转向官方的 Pinia API。

Vue Mastery Logo 从 Vue Mastery 获取 Pinia 速查表

基本示例

这就是使用 Pinia 在 API 方面的表现(请确保查看 入门 以获取完整说明)。您首先创建一个 Store

js
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // could also be defined as
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})

然后您在组件中使用

vue
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
</script>

<template>
  <!-- Access the state directly from the store -->
  <div>Current Count: {{ counter.count }}</div>
</template>

在 Playground 中试用

您甚至可以使用函数(类似于组件 setup())来定义 Store 以满足更高级的用例

js
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

在 Playground 中试用

如果您仍然不熟悉 setup() 和 Composition API,请不要担心,Pinia 也支持类似的 map helpers,就像 Vuex 一样。您以相同的方式定义 Store,但随后使用 mapStores()mapState()mapActions()

js
const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

const useUserStore = defineStore('user', {
  // ...
})

export default defineComponent({
  computed: {
    // other computed properties
    // ...
    // gives access to this.counterStore and this.userStore
    ...mapStores(useCounterStore, useUserStore),
    // gives read access to this.count and this.double
    ...mapState(useCounterStore, ['count', 'double']),
  },
  methods: {
    // gives access to this.increment()
    ...mapActions(useCounterStore, ['increment']),
  },
})

在 Playground 中试用

您将在核心概念中找到有关每个map helper 的更多信息。

官方课程

Pinia 的官方课程是 Mastering Pinia。由 Pinia 的作者撰写,涵盖从基础到高级主题,如插件、测试和服务器端渲染。它是开始使用 Pinia 并掌握它的最佳方式。

为什么是Pinia

Pinia(发音为 /piːnjʌ/,类似于英语中的“peenya”)是与piña(西班牙语中的菠萝)最接近的有效包名称。菠萝实际上是一组单独的花朵,它们结合在一起形成一个多果。类似于 Store,每个 Store 都是独立诞生的,但最终它们都连接在一起。它也是一种美味的热带水果,原产于南美洲。

更现实的示例

以下是用 Pinia 即使在 JavaScript 中也带有类型的 API 的更完整示例。对于某些人来说,这可能足以开始使用,而无需进一步阅读,但我们仍然建议您查看其余文档,甚至跳过此示例,并在阅读完所有核心概念后返回。

js
import { defineStore } from 'pinia'

export const useTodos = defineStore('todos', {
  state: () => ({
    /** @type {{ text: string, id: number, isFinished: boolean }[]} */
    todos: [],
    /** @type {'all' | 'finished' | 'unfinished'} */
    filter: 'all',
    // type will be automatically inferred to number
    nextId: 0,
  }),
  getters: {
    finishedTodos(state) {
      // autocompletion! ✨
      return state.todos.filter((todo) => todo.isFinished)
    },
    unfinishedTodos(state) {
      return state.todos.filter((todo) => !todo.isFinished)
    },
    /**
     * @returns {{ text: string, id: number, isFinished: boolean }[]}
     */
    filteredTodos(state) {
      if (this.filter === 'finished') {
        // call other getters with autocompletion ✨
        return this.finishedTodos
      } else if (this.filter === 'unfinished') {
        return this.unfinishedTodos
      }
      return this.todos
    },
  },
  actions: {
    // any amount of arguments, return a promise or not
    addTodo(text) {
      // you can directly mutate the state
      this.todos.push({ text, id: this.nextId++, isFinished: false })
    },
  },
})

在 Playground 中试用

与 Vuex 的比较

Pinia 最初是探索 Vuex 的下一个迭代可能是什么样的,它融合了核心团队对 Vuex 5 的许多讨论想法。最终,我们意识到 Pinia 已经实现了我们想要在 Vuex 5 中实现的大部分内容,并决定将其作为新的推荐方案。

与 Vuex 相比,Pinia 提供了更简单的 API,仪式更少,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。

RFC

最初,Pinia 没有经过任何 RFC 流程。我根据我在开发应用程序、阅读其他人的代码、为使用 Pinia 的客户工作以及在 Discord 上回答问题方面的经验测试了想法。这使我能够提供一个有效的解决方案,并适应各种情况和应用程序规模。我过去经常发布,并使库在保持其核心 API 不变的情况下不断发展。

现在,Pinia 已成为默认的状态管理解决方案,它与 Vue 生态系统中的其他核心库一样,也受制于相同的 RFC 流程,其 API 已进入稳定状态。

与 Vuex 3.x/4.x 的比较

Vuex 3.x 是 Vue 2 的 Vuex,而 Vuex 4.x 是 Vue 3 的 Vuex

Pinia API 与 Vuex ≤4 非常不同,主要体现在

  • mutation 不再存在。它们通常被认为是非常冗长的。它们最初带来了 devtools 集成,但这不再是一个问题。
  • 无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,API 的设计方式尽可能地利用 TS 类型推断。
  • 不再需要使用魔术字符串进行注入,导入函数,调用它们,享受自动完成!
  • 无需动态添加 Store,它们默认都是动态的,您甚至不会注意到。请注意,您仍然可以手动使用 Store 来注册它,无论何时您想要注册它,但由于它是自动的,因此您无需担心它。
  • 不再需要模块的嵌套结构。您仍然可以通过在另一个 Store 中导入和使用 Store 来隐式地嵌套 Store,但 Pinia 通过设计提供了一个扁平的结构,同时仍然能够在 Store 之间进行交叉组合。您甚至可以拥有 Store 的循环依赖关系
  • 没有命名空间模块。鉴于 Store 的扁平化架构,“命名空间”Store 本质上是它们定义的方式,可以说所有 Store 都是命名空间的。

有关如何将现有 Vuex ≤4 项目转换为使用 Pinia 的更详细说明,请参阅 从 Vuex 迁移指南