Pinia简介

Pinia介绍#

Pinia 开始 是一项实验,目的是在 2019 年 11 月左右使用 Composition API 重新设计 Vue Stroe的外观。从那时起,最初的原则保持不变,但 Pinia 适用于两种 Vue 2 和 Vue 3 并且不要求您使用组合 API。 除了 installationSSR 之外,两者的 API 是相同的,并且这些文档针对 Vue 3 并在必要时提供有关 Vue 2 的注释,以便 Vue 2 和 Vue 3 用户可以阅读!

为什么要使用 Pinia?#

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

  • 开发工具支持
    • 跟踪动作、突变的时间线
    • Store出现在使用它们的组件中
    • 时间旅行和更容易的调试
  • 热模块更换
    • 在不重新加载页面的情况下修改您的Store
    • 在开发时保持任何现有状态
  • 插件:使用插件扩展 Pinia 功能
  • 为 JS 用户提供适当的 TypeScript 支持或 autocompletion
  • 服务器端渲染支持

基本示例#

这就是使用 Pinia 在 API 方面的样子(请务必查看 Getting Started 以获取完整说明)。 您首先创建一个store:

js

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

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // 也可以定义为  => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})

然后你在一个组件中 使用 它:

js

import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counter = useCounterStore()

    counter.count++
    // 带自动补全 ✨
    counter.$patch({ count: counter.count + 1 })
    // 或使用动作代替
    counter.increment()
  },
}

你甚至可以使用一个函数(类似于一个组件setup())来为更高级的用例定义一个Store:

js

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

  return { count, increment }
})

如果您仍然不了解 setup() 和 Composition API,别担心,Pinia 还支持一组类似的 map helpers like Vuex。 您以相同的方式定义存储,然后使用 mapStores()mapState()mapActions()

js {22,24,28}

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

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

export default {
  computed: {
    // 其他计算属性
    // ...
    // 提供对 this.counterStore 和 this.userStore 的访问权限
    ...mapStores(useCounterStore, useUserStore),
    // 授予对 this.count 和 this.double 的读取权限
    ...mapState(useCounterStore, ['count', 'double']),
  },
  methods: {
    // 允许访问 this.increment()
    ...mapActions(useCounterStore, ['increment']),
  },
}

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

Pinia 名称来意#

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

一个更现实的例子#

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

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 会自动推断为 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') {
        // 使用自动完成调用其他 getter ✨
        return this.finishedTodos
      } else if (this.filter === 'unfinished') {
        return this.unfinishedTodos
      }
      return this.todos
    },
  },
  actions: {
    // 任何数量的参数,是否返回一个承诺
    addTodo(text) {
      // 你可以直接改变状态
      this.todos.push({ text, id: this.nextId++, isFinished: false })
    },
  },
})

与 Vuex 的比较#

Pinia 最初是为了探索 Vuex 的下一次迭代可能会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容,并决定实现它 取而代之的是新的建议。

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

RFCs#

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

现在 Pinia 已经成为默认的状态管理解决方案,它和 Vue 生态系统中的其他核心库一样遵循 RFC 流程,其 API 也进入了稳定状态。

Comparison with Vuex 3.x/4.x#

Vuex 3.x is Vuex for Vue 2 while Vuex 4.x is for Vue 3

Pinia API 与 Vuex ≤4 有很大不同,即:

  • mutations 不再存在。 他们通常被认为是**_极其_冗长**。 他们最初带来了 devtools 集成,但这不再是问题。
  • 无需创建自定义复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断。
  • 不再需要注入、导入函数、调用函数、享受自动完成功能!
  • 无需动态添加仓库,默认情况下它们都是动态的,您甚至都不会注意到。请注意,您仍然可以随时手动使用仓库进行注册,但因为它是自动的,您无需担心。
  • 不再有_modules_的嵌套结构。您仍然可以通过在另一个仓库中导入和_使用_ 来隐式嵌套仓库,但 Pinia 通过设计提供平面结构,同时仍然支持仓库之间的交叉组合方式。 您甚至可以拥有store的循环依赖关系
  • 没有_命名空间模块_。鉴于仓库的扁平架构,“命名空间”仓库是其定义方式所固有的,您可以说所有仓库都是命名空间的。

快速入门#

安装方式#

用你最喜欢的包管理器安装 pinia

bash

yarn add pinia
# or with npm
npm install pinia

TIP

如果您的应用使用的是 Vue <2.7,您还需要安装组合 API:@vue/composition-api。 如果您使用 Nuxt,则应遵循 这些说明

如果你使用的是 Vue CLI,你可以试试这个 非官方插件。 创建一个 pinia 实例(根存储)并将其作为插件传递给应用程序:

js

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

const pinia = createPinia()
const app = createApp(App)

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

如果您使用的是 Vue 2,您还需要安装一个插件并将创建的 pinia 注入应用程序的根目录:

js

import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
  el: '#app',
  // other options...
  // ...
  // 请注意,可以在同一页面上的多个 Vue 应用程序中使用相同的 `pinia` 实例
  pinia,
})

这也将添加 devtools 支持。 在 Vue 3 中,仍然不支持时间旅行和编辑等一些功能,因为 vue-devtools 尚未公开必要的 API,但 devtools 具有更多功能,并且整体开发人员体验要优越得多。 在 Vue 2 中,Pinia 使用 Vuex 的现有接口(因此不能与它一起使用)。

什么是仓库?#

一个仓库(如 Pinia)是一个实体,它持有未绑定到您的组件树的状态和业务逻辑。 换句话说,它托管全局状态。 它有点像一个始终存在并且每个人都可以读取和写入的组件。 它有三个概念stategettersactions 并且可以安全地假设这些概念等同于组件中的datacomputedmethods

我什么时候应该使用仓库#

存储应该包含可以在整个应用程序中访问的数据。 这包括在许多地方使用的数据,例如 导航栏中显示的用户信息,以及需要通过页面保留的数据,例如 一个非常复杂的多步骤表格。

另一方面,您应该避免在存储中包含可以托管在组件中的本地数据,例如 页面本地元素的可见性。

并非所有应用程序都需要访问全局状态,但如果您需要一个,Pania 将使您的生活更轻松。

核心概念

定义仓库#

在深入了解核心概念之前,我们需要知道仓库是使用 defineStore() 定义的,并且它需要一个唯一名称,作为第一个参数传递:

js

import { defineStore } from 'pinia'
// 你可以为`defineStore()`的返回值命名任何你想要的名字,
// 但最好使用仓库的名称并用`use`和`Store`包围它
//(例如`useUserStore`、`useCartStore`、`useProductStore` )

// 第一个参数是应用程序中仓库的唯一 id
export const useStore = defineStore('main', {
  // other options...
})

这个 name,也称为 id,是必需的,Pinia 使用它来将仓库连接到 devtools。 将返回的函数命名为 use… 是跨可组合项的约定,以使其用法惯用。

defineStore() 的第二个参数接受两个不同的值:Setup 函数或 Options 对象。

Option Stores#

与 Vue 的 Options API 类似,我们也可以传递带有 stateactionsgetters 属性的 Options 对象。

js

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

您可以将state视为仓库的data,将getters视为仓库的computed属性,将actions视为methods

Option stores应该感觉直观且易于上手。

Setup Stores#

还有另一种可能的语法来定义仓库。 类似于 Vue Composition API 的 setup function,我们可以传入一个定义响应式属性和方法的函数,并返回一个带有属性和 我们要公开的方法。

js

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

Setup Stores 中:

  • ref() 成为 state 的属性
  • computed() 变成 getters
  • function() 变成 actions

设置仓库比 Options Stores 带来更多的灵活性,因为您可以在仓库内创建观察者并自由使用任何 composable。 但是,请记住,使用 SSR 时,使用可组合项会变得更加复杂。

我应该选择什么语法?#

Vue 的 Composition API 和 Options API 一样,选择您觉得最舒服的一个。 如果您不确定,请先尝试 Option Stores

使用仓库#

我们正在_定义_一个 store,因为在 setup() 内部调用 use...Store() 之前不会创建 store:

js

import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const store = useCounterStore()

    return {
      // 您可以返回整个仓库实例以在模板中使用它
      store,
    }
  },
}

TIP

如果您还没有使用 setup 组件,您仍然可以将 Pinia 与 map helpers 一起使用

您可以根据需要定义任意数量的仓库,并且您应该在不同的文件中定义每个仓库以充分利用 Pinia(例如自动允许您的捆绑器进行代码拆分并提供 TypeScript 推理)。

实例化仓库后,您可以直接在仓库上访问在stategettersactions中定义的任何属性。 我们将在接下来的几页中详细介绍这些内容,但自动完成功能将为您提供帮助。

请注意,store 是一个用 reactive 包裹的对象,这意味着不需要在 getter 之后写 .value 但是,就像 setup 中的 props 一样,我们不能解构它

js

export default defineComponent({
  setup() {
    const store = useCounterStore()
    // ❌ 这不会工作,因为它破坏了反应性
    // 这和从 `props` 解构是一样的
    const { name, doubleCount } = store

    name // "Eduardo"
    doubleCount // 0

    setTimeout(() => {
      store.increment()
    }, 1000)

    return {
      name, // 将永远是 "Eduardo"
      doubleCount, // 将永远是 0
      doubleNumber: store.doubleCount, //也将永远是0
      doubleValue: computed(() => store.doubleCount), // ✅ 这个是响应式的
    }
  },
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

为了从仓库中提取属性同时保持其反应性,您需要使用storeToRefs() 。 它将为每个反应属性创建引用。 当您仅使用仓库中的状态而不调用任何操作时,这很有用。 请注意,您可以直接从仓库中解构操作,因为它们也绑定到仓库本身:

js

import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    const store = useCounterStore()
     // `name` 和 `doubleCount` 是响应式引用
     // 这也将为插件添加的属性创建引用
     // 但跳过任何动作或非反应性(非参考/反应性)属性
    const { name, doubleCount } = storeToRefs(store)
    // 可以提取增量动作
    const { increment } = store

    return {
      name,
      doubleCount,
      increment,
    }
  },
})

State#

大多数时候,州是仓库的中心部分。 人们通常从定义代表他们的应用程序的状态开始。 在 Pinia 中,状态被定义为返回初始状态的函数。 这允许 Pinia 在服务器端和客户端工作。

js

import { defineStore } from 'pinia'

export const useStore = defineStore('storeId', {
  // 推荐用于完整类型推断的箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断其类型
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
})

TIP

如果您使用的是 Vue 2,您在 state 中创建的数据遵循与 Vue 实例中的 data 相同的规则,即 state 对象必须是普通的,并且您需要在以下情况下调用 Vue.set() *为其添加新的**属性。 另请参阅:Vue#data

TypeScript#

为了使您的状态与 TS 兼容,您不需要做太多事情:确保 strict,或者至少,noImplicitThis ,已启用,Pania 将自动推断您的状态类型! 但是,在某些情况下,您应该帮助它进行一些铸造:

ts

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      // 对于最初为空的列表
      userList: [] as UserInfo[],
      // 对于尚未加载的数据
      user: null as UserInfo | null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

如果您愿意,可以使用接口定义状态并输入 state() 的返回值:

ts

interface State {
  userList: UserInfo[]
  user: UserInfo | null
}

export const useUserStore = defineStore('user', {
  state: (): State => {
    return {
      userList: [],
      user: null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

访问state#

默认情况下,您可以通过 store 实例访问状态来直接读取和写入状态:

js

const store = useStore()

store.count++

注意你不能添加一个新的状态属性如果你没有在state()中定义它,它必须包含初始状态。 例如:如果 state() 中没有定义 secondCount,我们就不能执行 store.secondCount = 2

重置状态#

您可以通过调用 store 上的 $reset() 方法将状态 reset 到其初始值:

js

const store = useStore()

store.$reset()

使用选项式 API#

对于以下示例,您可以假设已创建以下仓库:

js

// Example File Path:
// ./src/stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
})

如果您不使用 Composition API,并且使用的是 computedmethods、…,则可以使用 mapState() 帮助器将状态属性映射为只读计算属性:

js

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

export default {
  computed: {
    // 提供对组件内部 this.count 的访问
    // 与从 store.count 读取相同
    ...mapState(useCounterStore, ['count'])
    // 与上面相同,但将其注册为 this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'count',
      // 您还可以编写一个访问仓库的函数
      double: store => store.count * 2,
      // 它可以访问“this”,但不能正确输入...
      magicValue(store) {
        return store.someGetter + this.count + this.double
      },
    }),
  },
}
可修改状态#

如果您希望能够写入这些状态属性(例如,如果您有一个表单),您可以使用 mapWritableState() 代替。 请注意,您不能传递类似于 mapState() 的函数:

js

import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // 允许访问组件内部的 this.count 并允许设置它
    // this.count++ 与从 store.count 中读取相同
    ...mapWritableState(useCounterStore, ['count'])
    // 与上面相同,但将其注册为 this.myOwnName
    ...mapWritableState(useCounterStore, {
      myOwnName: 'count',
    }),
  },
}

TIP

对于像数组这样的集合,您不需要 mapWritableState(),除非您用 cartItems = [] 替换整个数组,mapState() 仍然允许您调用集合上的方法。

改变状态#

除了直接用 store.count++ 修改 store,你还可以调用 $patch 方法。 它允许您使用部分“状态”对象同时应用多个更改:

js

store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

但是,使用这种语法应用某些突变非常困难或代价高昂:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合。 正因为如此,$patch 方法也接受一个函数来分组这种难以用补丁对象应用的突变:

js

store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

这里的主要区别是$patch() 允许您将多个更改分组到开发工具中的一个条目中。 注意两者,state 和 $patch() 的直接更改都出现在 devtools 中,并且可以进行时间旅行(在 Vue 3 中还没有)。

替换状态#

不能完全替换 store 的状态,因为这会破坏反应性。 但是,您可以_patch it_:

js

// 这实际上并没有取代`$state`
store.$state = { count: 24 }
// 它在内部调用`$patch()`:
store.$patch({ count: 24 })

您还可以通过更改 pinia 实例的 state设置整个应用程序的初始状态。 这在 SSR 水合 期间使用。

js

pinia.state.value = {}

1

订阅状态#

可以通过 store 的 $subscribe() 方法查看状态及其变化,类似于 Vuex 的 subscribe 方法。 与常规的 watch() 相比,使用 $subscribe() 的优点是 subscriptions 只会在 patches 之后触发一次(例如,当使用上面的函数版本时)。

js

cartStore.$subscribe((mutation, state) => {
  // 从'pinia'导入{ MutationType }
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // 与 cartStore.$id 相同
  mutation.storeId // 'cart'
  // 仅适用于 mutation.type === 'patch object'
  mutation.payload // 传递给 cartStore.$patch() 的补丁对象

  // 每当它发生变化时,将整个状态持久化到本地存储
  localStorage.setItem('cart', JSON.stringify(state))
})

默认情况下,state subscriptions 绑定到添加它们的组件(如果仓库位于组件的 setup() 中)。 意思是,当组件被卸载时,它们将被自动删除。 如果您还想在组件卸载后保留它们,请将 { detached: true } 作为第二个参数传递给 detach 当前组件的 state subscription

js

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

    // 即使在卸载组件后,此订阅仍将保留
    someStore.$subscribe(callback, { detached: true })

    // ...
  },
}

TIP

您可以在 pinia 实例上查看整个状态:

js

watch(
  pinia.state,
  (state) => {
    // 每当它发生变化时,将整个状态持久化到本地存储
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

Getters#

Getter 完全等同于 Store 状态的 计算值。 它们可以用 defineStore() 中的 getters 属性定义。 他们接收“状态”作为第一个参数以鼓励箭头函数的使用:

js

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
})

大多数时候,getter 只会依赖状态,但是,他们可能需要使用其他 getter。 正因为如此,我们可以在定义常规函数时通过 this 访问到_whole store instance_ 但是需要定义返回类型的类型(在 TypeScript 中)。 这是由于 TypeScript 中的一个已知限制,并且不会影响使用箭头函数定义的 getter,也不会影响不使用 this 的 getter

ts

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // 自动将返回类型推断为数字
    doubleCount(state) {
      return state.count * 2
    },
    // 返回类型 **必须** 明确设置
    doublePlusOne(): number {
      // 整个仓库的自动完成和填写 ✨
      return this.doubleCount + 1
    },
  },
})

然后你可以直接在 store 实例上访问 getter:

vue

<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>

<script>
export default {
  setup() {
    const store = useCounterStore()

    return { store }
  },
}
</script>

访问其他 getter#

与计算属性一样,您可以组合多个 getter。 通过 this 访问任何其他 getter。 即使您不使用 TypeScript,您也可以使用 JSDoc 提示您的 IDE 类型:

js

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // 类型是自动推断的,因为我们没有使用 `this`
    doubleCount: (state) => state.count * 2,
    // 这里我们需要自己添加类型(在 JS 中使用 JSDoc)。
    // 我们也可以使用它来记录 getter
    /**
     * 返回计数值乘以二加一。
     *
     * @returns {number}
     */
    doubleCountPlusOne() {
      // autocompletion ✨
      return this.doubleCount + 1
    },
  },
})

将参数传递给 getter#

Getters 只是幕后的 computed 属性,因此无法向它们传递任何参数。 但是,您可以从 getter 返回一个函数以接受任何参数:

js

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

并在组件中使用:

vue

<script>
export default {
  setup() {
    const store = useStore()

    return { getUserById: store.getUserById }
  },
}
</script>

<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>

请注意,在执行此操作时,getter 不再缓存,它们只是您调用的函数。 但是,您可以在 getter 本身内部缓存一些结果,这并不常见,但应该证明性能更高:

js

export const useStore = defineStore('main', {
  getters: {
    getActiveUserById(state) {
      const activeUsers = state.users.filter((user) => user.active)
      return (userId) => activeUsers.find((user) => user.id === userId)
    },
  },
})

访问其他仓库的吸气剂#

要使用其他存储 getter,您可以直接在 better 内部使用它:

js

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

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

setup() 一起使用#

您可以直接访问任何 getter 作为 store 的属性(与 state 属性完全一样):

js

export default {
  setup() {
    const store = useCounterStore()

    store.count = 3
    store.doubleCount // 6
  },
}

使用选项式 API#

对于以下示例,您可以假设已创建以下仓库:

js

// Example File Path:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },
})

使用 setup()#

虽然 Composition API 并不适合所有人,但 setup() 钩子可以使在 Options API 中使用 Pinia 更容易。 不需要额外的地图辅助功能!

js

import { useCounterStore } from '../stores/counter'

export default {
  setup() {
    const counterStore = useCounterStore()

    return { counterStore }
  },
  computed: {
    quadrupleCounter() {
      return this.counterStore.doubleCount * 2
    },
  },
}

Without setup()#

您可以使用 previous section of state 中使用的相同 mapState() 函数映射到 getter:

js

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

export default {
  computed: {
    // 允许访问组件内的 this.doubleCount
    // 与从 store.doubleCount 中读取相同
    ...mapState(useCounterStore, ['doubleCount']),
    // 与上面相同,但将其注册为 this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'doubleCount',
      // 您还可以编写一个访问仓库的函数
      double: (store) => store.doubleCount,
    }),
  },
}

Actions#

Actions 相当于组件中的 methods。 它们可以使用 defineStore() 中的 actions 属性定义,并且它们非常适合定义业务逻辑

js

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    // 因为我们依赖 `this`,所以我们不能使用箭头函数
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})

getters 一样,操作可以通过 this 访问 whole store instance 并提供完整类型(和自动完成✨)支持与 getter 不同,actions 可以是异步的,您可以在任何 API 调用甚至其他操作的操作中await! 这是使用 Mande 的示例。 请注意,只要您获得“Promise”,您使用的库并不重要,您甚至可以使用本机的“fetch”函数(仅限浏览器):

js

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)
        // 让表单组件显示错误
        return error
      }
    },
  },
})

你也可以完全自由地设置你想要的任何参数并返回任何东西。 调用动作时,一切都会自动推断!

Actions 像方法一样被调用:

js

export default defineComponent({
  setup() {
    const store = useCounterStore()
    // 将操作作为仓库的方法调用
    store.randomizeCounter()

    return {}
  },
})

访问其他仓库的 Actions#

要使用另一个仓库,您可以直接在 Action 内部使用它:

js

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

Usage with setup()#

您可以直接调用任何操作作为 store 的方法:

js

export default {
  setup() {
    const store = useCounterStore()

    store.randomizeCounter()
  },
}

使用选项式 API#

对于以下示例,您可以假设已创建以下仓库:

js

// Example File Path:
// ./src/stores/counter.js

import { defineStore } from 'pinia',

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

With setup()#

虽然 Composition API 并不适合所有人,但 setup() 钩子可以使在 Options API 中使用 Pinia 更容易。 不需要额外的地图辅助功能!

js

import { useCounterStore } from '../stores/counter'

export default {
  setup() {
    const counterStore = useCounterStore()

    return { counterStore }
  },
  methods: {
    incrementAndPrint() {
      this.counterStore.increment()
      console.log('New Count:', this.counterStore.count)
    },
  },
}

Without setup()#

如果您根本不想使用 Composition API,可以使用 mapActions() 帮助器将操作属性映射为组件中的方法:

js

import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  methods: {
    // 允许访问组件内部的 this.increment()
    // 与从 store.increment() 调用相同
    ...mapActions(useCounterStore, ['increment'])
    // 与上面相同,但将其注册为 this.myOwnName()
    ...mapActions(useCounterStore, { myOwnName: 'increment' }),
  },
}

订阅action#

可以使用 store.$onAction() 观察动作及其结果。 传递给它的回调在操作本身之前执行。 after 处理承诺并允许您在操作解决后执行函数。 以类似的方式,onError 允许您在操作抛出或拒绝时执行函数。 这些对于在运行时跟踪错误很有用,类似于 Vue 文档中的这个提示

这是一个在运行操作之前和它们解决/拒绝之后记录的示例。

js

const unsubscribe = someStore.$onAction(
  ({
    name, // 动作名称
    store, // 存储实例,与 `someStore` 相同
    args, // 传递给动作的参数数组
    after, // 动作返回或解决后的钩子
    onError, // 如果动作抛出或拒绝,则挂钩
  }) => {
    // 此特定操作调用的共享变量
    const startTime = Date.now()
    // 这将在 `store` 上的操作执行之前触发
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 如果操作成功并且完全运行后,这将触发。
    // 它等待任何返回的承诺
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果动作抛出或返回拒绝的承诺,这将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手动移除监听器
unsubscribe()

默认情况下,action subscriptions 绑定到添加它们的组件(如果仓库位于组件的 setup() 内)。 意思是,当组件被卸载时,它们将被自动删除。 如果您还想在组件卸载后保留它们,请将 true 作为第二个参数传递给当前组件的 detach action subscription

js

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

    // 即使在卸载组件后,此订阅仍将保留
    someStore.$onAction(callback, true)

    // ...
  },
}

最后更新: Plugins#

由于低级 API,Pania 仓库可以完全扩展。 以下是您可以执行的操作列表:

  • 向仓库添加新属性
  • 定义仓库时添加新选项
  • 为仓库添加新方法
  • 包装现有方法
  • 更改甚至取消操作
  • 实现类似 Local Storage 的副作用
  • 适用于特定仓库

使用 pinia.use() 将插件添加到 pinia 实例中。 最简单的例子是通过返回一个对象为所有仓库添加一个静态属性:

js

import { createPinia } from 'pinia'

// 为安装此插件后创建的每个仓库添加一个名为 `secret` 的属性
// 这可能在不同的文件中
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
// 将插件提供给 pinia
pinia.use(SecretPiniaPlugin)

// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'

这对于添加全局对象(如路由器、模式或 toast 管理器)很有用。

介绍#

Pinia 插件是一个函数,可以选择返回要添加到仓库的属性。 它需要一个可选参数,一个 context

js

export function myPiniaPlugin(context) {
  context.pinia // 使用 `createPinia()` 创建的 pinia
  context.app // 使用 `createApp()` 创建的当前应用程序(仅限 Vue 3)
  context.store // 插件正在扩充的仓库
  context.options // 定义存储的选项对象传递给`defineStore()`
  // ...
}

然后使用 pinia.use() 将此函数传递给 pinia

js

pinia.use(myPiniaPlugin)

插件仅适用于在将pinia传递给应用程序后创建的仓库,否则将不会被应用。

扩充仓库#

您可以通过简单地在插件中返回它们的对象来为每个仓库添加属性:

js

pinia.use(() => ({ hello: 'world' }))

您也可以直接在 store 上设置属性,但如果可能,请使用返回版本,以便 devtools 可以自动跟踪它们

js

pinia.use(({ store }) => {
  store.hello = 'world'
})

插件的任何属性_returned_都会被devtools自动跟踪,所以为了让hello在devtools中可见,如果你想调试它,请确保将它添加到store._customProperties仅在开发模式 开发工具:

js

// 从上面的例子
pinia.use(({ store }) => {
  store.hello = 'world'
  // 确保您的捆绑器处理此问题。 webpack 和 vite 应该默认这样做
  if (process.env.NODE_ENV === 'development') {
    // 添加您在仓库中设置的任何密钥
    store._customProperties.add('hello')
  }
})

请注意,每个 store 都使用 reactive 包装,自动展开任何 Ref (ref(), computed() , …) 它包含:

js

const sharedRef = ref('shared')
pinia.use(({ store }) => {
  // 每个仓库都有自己的 `hello` 属性
  store.hello = ref('secret')
  // 它会自动展开
  store.hello // 'secret'

  // 所有仓库都共享 value `shared` 属性
  store.shared = sharedRef
  store.shared // 'shared'
})

js

import { toRef, ref } from 'vue'

pinia.use(({ store }) => {
  // 为了正确处理 SSR,我们需要确保我们没有覆盖现有值
  if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
    // hasError 在插件中定义,因此每个仓库都有自己的状态属性
    const hasError = ref(false)
    // 在 $state 上设置变量,允许在 SSR 期间对其进行序列化
    store.$state.hasError = hasError
  }
  // 我们需要将 ref 从 state 传输到 store,这样
  // 两个访问:store.hasError 和 store.$state.hasError 都可以工作并共享同一个变量
  // See https://vuejs.org/api/reactivity-utilities.html#toref
  store.hasError = toRef(store.$state, 'hasError')

  // 在这种情况下,最好不要返回 `hasError`,因为无论如何它都会
  // 显示在 devtools 的 `state` 部分中,如果我们返回它,devtools 会显示两次。
})

请注意,插件中发生的状态更改或添加(包括调用store.$patch())发生在存储处于活动状态之前,因此不会触发任何订阅

WARNING

如果您使用 Vue 2,Pinia 会受到与 Vue 一样的相同的响应式警告。 在创建像 secrethasError 这样的新状态属性时,您需要使用 Vue.set() (Vue 2.7) 或set()(from @vue/composition-api for Vue <2.7):

js

import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
  if (!Object.prototype.hasOwnProperty(store.$state, 'hello')) {
    const secretRef = ref('secret')
    // 如果数据打算在 SSR 期间使用,您应该在 `$state` 
    // 属性上设置它,以便在水合期间对其进行序列化和拾取
    set(store.$state, 'secret', secretRef)
  }
  // 也可以直接在仓库中设置它,这样您就可以通过两种方式
  // 访问它:`store.$state.secret` / `store.secret`
  set(store, 'secret', toRef(store.$state, 'secret'))
  store.secret // 'secret'
})

添加新的外部属性#

当添加外部属性、来自其他库的类实例或仅仅是非反应性的东西时,您应该在将对象传递给 pinia 之前使用 markRaw() 包装对象。 这是一个将路由器添加到每个仓库的示例:

js

import { markRaw } from 'vue'
// 根据您的路由器所在的位置进行调整
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
})

在插件中调用 $subscribe#

您也可以在插件中使用 store.KaTeX parse error: Expected 'EOF', got '#' at position 57: …epts/state.html#̲%E8%AE%A2%E9%98…onAction

ts

pinia.use(({ store }) => {
  store.$subscribe(() => {
    // react to store changes
  })
  store.$onAction(() => {
    // react to store actions
  })
})

添加新选项#

可以在定义仓库时创建新选项,以便以后从插件中使用它们。 例如,您可以创建一个 debounce 选项,允许您对任何操作进行去抖动:

js

defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // 稍后将由插件读取
  debounce: {
    // 将动作 searchContacts 去抖动 300ms
    searchContacts: 300,
  },
})

然后插件可以读取该选项以包装操作并替换原始操作:

js

// 使用任何去抖库
import debounce from 'lodash/debounce'

pinia.use(({ options, store }) => {
  if (options.debounce) {
    // 我们正在用新的行动覆盖这些行动
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      )
      return debouncedActions
    }, {})
  }
})

请注意,使用设置语法时,自定义选项作为第三个参数传递:

js

defineStore(
  'search',
  () => {
    // ...
  },
  {
    // 稍后将由插件读取
    debounce: {
      // 将动作 searchContacts 去抖动 300ms
      searchContacts: 300,
    },
  }
)

TypeScript#

上面显示的所有内容都可以通过键入支持来完成,因此您无需使用 any@ts-ignore

Typing plugins#

Pinia 插件可以按如下方式键入:

ts

import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {
  // ...
}

键入新的仓库属性#

向仓库添加新属性时,您还应该扩展 PiniaCustomProperties 接口。

ts

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomProperties {
    // 通过使用 setter,我们可以同时允许字符串和引用
    set hello(value: string | Ref<string>)
    get hello(): string

    // 你也可以定义更简单的值
    simpleNumber: number
  }
}

然后可以安全地写入和读取它:

ts

pinia.use(({ store }) => {
  store.hello = 'Hola'
  store.hello = ref('Hola')

  store.simpleNumber = Math.random()
  // @ts-expect-error: 我们没有正确输入
  store.simpleNumber = ref(Math.random())
})

PiniaCustomProperties 是一种通用类型,允许您引用仓库的属性。 想象以下示例,我们将初始选项复制为$options(这仅适用于选项式API):

ts

pinia.use(({ options }) => ({ $options: options }))

1

我们可以使用 4 种通用类型的 PiniaCustomProperties 来正确输入:

ts

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomProperties<Id, S, G, A> {
    $options: {
      id: Id
      state?: () => S
      getters?: G
      actions?: A
    }
  }
}

TIP

在泛型中扩展类型时,它们的命名必须与源代码中的完全相同Id不能命名为idIS不能命名为State。 以下是每个字母所代表的含义:

  • S:State
  • G:Getters
  • A:Actions
  • SS:Setup Store / Store

输入新状态#

当添加新的状态属性(storestore.$state)时,您需要将类型添加到 PiniaCustomStateProperties。 与 PiniaCustomProperties 不同,它只接收 State 泛型:

ts

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomStateProperties<S> {
    hello: string
  }
}

键入新的创建选项#

在为 defineStore() 创建新选项时,您应该扩展 DefineStoreOptionsBase。 与 PiniaCustomProperties 不同,它只公开了两个泛型:State 和 Store 类型,允许您限制可以定义的内容。 例如,您可以使用操作的名称:

ts

import 'pinia'

declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    // 允许为任何操作定义毫秒数
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  }
}

TIP

还有一个 StoreGetters 类型可以从 Store 类型中提取 getters。 您还可以通过分别扩展 DefineStoreOptionsDefineSetupStoreOptions 类型来扩展 setup storesoption stores only 的选项。

Nuxt.js#

在 Nuxt 旁边使用 pinia 时,您必须先创建一个 Nuxt 插件 . 这将使您可以访问 pinia 实例:

ts

// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'

function MyPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation) => {
    // react to store changes
    console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
  })

  // Note this has to be typed if you are using TS
  return { creationTime: new Date() }
}

export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(MyPiniaPlugin)
})

请注意,上面的示例使用的是 TypeScript,如果您使用的是 .js 文件,则必须删除类型注释 PiniaPluginContextPlugin 以及它们的导入。

Nuxt.js 2#

如果您使用的是 Nuxt.js 2,则类型略有不同:

ts

// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'

function MyPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation) => {
    // react to store changes
    console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
  })

  // 请注意,如果您使用的是 TS,则必须输入
  return { creationTime: new Date() }
}

const myPlugin: Plugin = ({ $pinia }) => {
  $pinia.use(MyPiniaPlugin)
}

export default myPlugin

最后更新: 在组件之外使用Store#

Pinia 仓库依靠 pinia 实例在所有调用中共享同一个仓库实例。 大多数情况下,只需调用您的“useStore()”函数即可开箱即用。 例如,在 setup() 中,您无需执行任何其他操作。 但在组件之外,情况有些不同。 在幕后,useStore() injects 你给你的apppinia 实例。 这意味着如果 pinia 实例无法自动注入,您必须手动将其提供给 useStore() 函数。 您可以根据您正在编写的应用程序的类型以不同的方式解决这个问题。

单页应用程序#

如果您没有进行任何 SSR(服务器端渲染),则在使用 app.use(pinia) 安装 pinia 插件后,任何useStore() 调用都将起作用:

js

import { useUserStore } from '@/stores/user'
import { createApp } from 'vue'
import App from './App.vue'

// ❌  失败,因为它是在创建 pinia 之前调用的
const userStore = useUserStore()

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// ✅ 有效,因为 pinia 实例现在处于活动状态
const userStore = useUserStore()

确保始终应用此功能的最简单方法是延迟调用 useStore(),方法是将它们放在安装 pinia 后始终运行的函数中。

让我们看一下这个使用 Vue Router 的导航守卫内部的仓库的例子:

js

import { createRouter } from 'vue-router'
const router = createRouter({
  // ...
})

// ❌ 根据导入的顺序,这将失败
const store = useStore()

router.beforeEach((to, from, next) => {
  // 我们想在这里使用仓库
  if (store.isLoggedIn) next()
  else next('/login')
})

router.beforeEach((to) => {
  // ✅ 这将起作用,因为路由器在安装路由器后开始导航,并且也将安装 pinia
  const store = useStore()

  if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})

务器端渲染 (SSR)

TIP

如果您使用的是 Nuxt.js, 您需要改为阅读 这些说明

只要您在 setup 函数、gettersactions 的顶部调用 useStore() 函数,使用 Pinia 创建仓库应该可以立即用于 SSR:

js

export default defineComponent({
  setup() {
    // 这是有效的,因为 pinia 知道什么应用程序在 `setup()` 中运行
    const main = useMainStore()
    return { main }
  },
})

setup() 之外使用Store#

如果您需要在其他地方使用仓库,则需要将 pinia 实例 已传递给应用程序 传递给 useStore() 函数调用:

js

const pinia = createPinia()
const app = createApp(App)

app.use(router)
app.use(pinia)

router.beforeEach((to) => {
  // ✅ 这将确保正确的仓库用于当前正在运行的应用程序
  const main = useMainStore(pinia)

  if (to.meta.requiresAuth && !main.isLoggedIn) return '/login'
})

Pinia 可以方便地将自身作为 $pinia 添加到您的应用程序中,以便您可以在诸如 serverPrefetch() 之类的函数中使用它:

js

export default {
  serverPrefetch() {
    const store = useStore(this.$pinia)
  },
}

状态补水(hydration)#

为了水合初始状态,您需要确保 rootState 包含在 HTML 中的某个位置,以便 Pinia 稍后获取它。 根据您用于 SSR 的内容,出于安全原因,您应该转义状态。 我们建议使用 Nuxt.js 使用的 @nuxt/devalue

js

import devalue from '@nuxt/devalue'
import { createPinia } from 'pinia'
// 检索 rootState 服务器端
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)

// 渲染页面后,根状态被构建,可以直接在 `pinia.state.value` 上读取。

// 序列化、转义(如果状态的内容可以由用户更改,这非常重要,这几乎总是如此)
// 并将其放置在页面上的某个位置,例如,作为全局变量。

devalue(pinia.state.value)

根据您用于 SSR 的内容,您将设置一个将在 HTML 中序列化的 initial state 变量。 您还应该保护自己免受 XSS 攻击。 例如,使用 vite-ssr 你可以使用 transformState 选项@nuxt/devalue

js

import devalue from '@nuxt/devalue'

export default viteSSR(
  App,
  {
    routes,
    transformState(state) {
      return import.meta.env.SSR ? devalue(state) : state
    },
  },
  ({ initialState }) => {
    // ...
    if (import.meta.env.SSR) {
      // 这将被字符串化并设置为 window.__INITIAL_STATE__
      initialState.pinia = pinia.state.value
    } else {
      // 在客户端,我们恢复状态
      pinia.state.value = initialState.pinia
    }
  }
)

您可以根据需要使用 其他替代方法@nuxt/devalue,例如 如果你可以使用 JSON.stringify()/JSON.parse() 序列化和解析你的状态,你可以大大提高你的性能

使此策略适应您的环境。 确保在客户端调用任何 useStore() 函数之前对 pinia 的状态进行水合。 例如,如果我们将状态序列化为 <script> 标记,以便通过 window.__pinia 在客户端全局访问它,我们可以这样写:

js

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// 必须由用户设置
if (isClient) {
  pinia.state.value = JSON.parse(window.__pinia)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值