失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > pinia中文文档 指导文档中文翻译版 pinia指导中文翻译

pinia中文文档 指导文档中文翻译版 pinia指导中文翻译

时间:2021-12-21 01:17:13

相关推荐

pinia中文文档  指导文档中文翻译版   pinia指导中文翻译

Pinia 指导文档

中 文 翻 译 版

翻译者:jcLee95

Pinia 指导手册中文翻译地址(本文): /qq_28550263/article/details/120721160

Pinia API文档中文翻译地址(新):/qq_28550263/article/details/122601258

Pinia官网:https://pinia.esm.dev/

Vuex官网:/zh/

目录

1. Pinia介绍

1.1 为什么我应该使用 Pinia?1.2 基本例子1.3 为什么是Pinia1.4 一个更接近实际的例子1.5 与 Vuex 对比

2. 核心概念

2.1 定义一个 Store 2.1.1 使用store2.2 状态(State) 2.2.1 访问状态2.2.2 重置状态 2.2.2.1 使用 options API 2.2.3 改变状态2.2.4 更换状态2.2.5 订阅状态 2.3 访问器(Getters) 2.3.1 访问其他getter2.3.2 将参数传递给getter2.3.3 访问其他store getters2.3.4 使用 setup() 的用法2.3.5 使用 options API 的用法 2.4 动作(Actions) 2.4.1 访问其他 存储(store)中的 action2.4.2 使用setup()时的用法2.4.3 使用 options API 时的用法2.4.4 订阅action2.5 插件(Plugins) 2.5.1 介绍2.5.2 存储(store)拓展 2.5.2.1 添加新状态 2.5.3 添加新的外部属性2.5.4 在插件内部调用$subscribe2.5.5 添加新 options2.5.6 TypeScript 2.5.6.1 插件( plugins)类型注释2.5.6.2 存储属性(store properties)类型注释2.5.6.3 新状态(state)类型注释2.5.6.4 新选项(options)类型注释 2.5.7 Nuxt.js

3. 服务端渲染 (Server Side Rendering,SSR)

3.1 使用 setup() 之外的 store3.2 State hydration3.3 Nuxt.js 3.3.1 安装3.3.2 在setup()外使用store3.3.3 在stores中使用Nuxt context3.3.4 将 Pinia 和 Vuex 一起使用3.3.5 Typescript

4. 热模块替换 (Hot Module Replacement, HMR)

5. 测试 stores

5.1 Unit 测试一个 store5.2 Unit 测试模块5.3 E2E 测试

6. 不使用setup()时的用法

6.1 允许访问整个 store6.2 TypeScript

7. 组合Stores

7.1 嵌套 store7.2 Getters 共享7.3 动作(actions)共享

1. Pinia介绍

Pinia 起源于 11月左右,尝试重新定义Vue中Store 和 组合式 API 联合使用时应该时什么样子。 从那以后,最初的原则始终保持一致, 但是 Pinia 同时对 Vue 2 以及 Vue 3同时有效,并且并不要求你使用 API。 除了对于安装过程和服务端渲染(SSR),API 对两者来说都是一样的。本文档是针对 Vue 3 的,并在必要时带有有关Vue2的注释,以便使用Vue2和Vue3用户都可以阅读!

1.1 为什么我应该使用 Pinia?

Pinia 是一个 Vue 的存储库, 它能让你跨组件/页面共享状态.如果你熟悉组合式 API, 你大概会想到你已经能通过这样一个简单的例子来全局共享状态:export const state = reactive({}). 对于一个单页面应用(SPA, single page applications)来说的确如此,但是如果这是一个服务端渲染(SSR, server side rendered)应用时,将使你的应用暴露在安全漏洞之中。 但即使在小的 单页面应用(SPA, single page applications)中,你也可以通过使用Pinia获得很大便利:

Vue-Devtools 支持一个追踪动作、变化的时间轴出现在他们使用的组件中的存储Stores时间旅行和更简便的调试模块热替换在不要求重新加载页面条件下修改你的 stores当开发时保持所有存在的状态(state)插件: 使用插件扩展Pinia功能为JS用户提供适当的TypeScript支持与自动完成功能支持服务端渲染

1.2 基本例子

这就是使用pinia在API方面的样子 。你可以从创建store开始:

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

然后你在组件中使用它:

import {useCounterStore } from '@/stores/counter'export default {setup() {const counter = useCounterStore()counter.count++// 自动完成 ✨counter.$patch({count: counter.count + 1 })// 或者使用 action 来代替counter.increment();},}

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

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

如果你至今仍不熟悉setup()和组合式 API, 别慌, Pinia 也可以提供一组 类似于Vuex中的map helpers. 你用同样的方法来定义stores,但是接下来使用的是mapStores(),mapState(), 或者mapActions():

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的更多信息。

1.3 为什么是Pinia

Pinia 是最接近piña(西班牙语中的菠萝) 的单词,它是一个有效的包名。菠萝实际上是一组单个的花,它们结合在一起形成一个多重的果实。类似于stores,每一个都是单独诞生的,但最后都是连接在一起的。这也是原产于南美洲的美味热带水果。

1.4 一个更接近实际的例子

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

import {defineStore } from 'pinia'export const todos = defineStore('todos', {state: () => ({/** @type {{ text: string, id: number, isFinished: boolean }[]} */todos: [],/** @type {'all' | 'finished' | 'unfinished'} */filter: 'all',// type will be automatically inferred to numbernextId: 0,}),getters: {finishedTodos(state) {// 自动完成! ✨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') {// 用自动完成调用其他getters ✨return this.finishedTodos} else if (this.filter === 'unfinished') {return this.unfinishedTodos}return this.todos},},actions: {// 任何数量的 arguments,返回一个promise或否 addTodo(text) {// 你可以直接改变状态this.todos.push({text, id: this.nextId++, isFinished: false })},},})

1.5 与 Vuex 对比

Pinia 尝试尽可能地接近Vuex的设计哲学。它旨在测试Vuex下一次迭代的提案,它是成功的,因为我们目前有一个Vuex 5的开放RFC,其 API 与Pinia使用的非常相似。请注意,Eduardo是Pinia的作者,是Vue.js核心团队的一员,积极参与Vue Router和Vuex等API的设计。Eduardo个人对这个项目的意图是重新设计使用全局store的体验,同时保持Vue平易近人的理念。Eduardo将Pinia的API保持得和Vuex一样紧密,因为它一直在向前发展,使人们可以很容易地迁移到Vuex,甚至在将来融合两个项目(在Vuex下)。

1.5.1 RFCs

虽然Vuex通过RFC从社区收集尽可能多的反馈,但Pinia没有。Eduardo根据自己开发应用程序、阅读他人代码和回答Discord问题的经验来测试想法。这使Eduardo能够提供一个可行的、经常发布的解决方案,并使它在人们使用它的同时不断发展,如果必要的话,可以在主要版本中进行重大变更(在第一次稳定发布后不太可能有重大的重大变更)。

1.5.2 与 Vuex 3.x/4.x对比

Vuex 3.x 是基于 Vue 2 提供的Vuex, 而 Vuex 4.x 是 为Vue 3提供的Vuex。

Pinia API 与 Vuex ≤4有相当大的不同,即:

mutations(突变)不再存在。他们经常被认为极其 啰嗦. 他们最初带来了vue-devtools集成,但这不再是一个问题。不需要创建自定义的复杂包装器来支持TypeScript,一切都是类型化的,并且API的设计方式是尽可能利用TS类型推断。不再有魔术字符串注入,导入函数,调用它们,享受自动完成!不需要动态添加stores,默认都是动态的,你甚至不会注意到。请注意,您仍然可以随时手动使用stores来注册它,但是因为它是自动的,所以您不需要担心它。不再有modules的嵌套结构。你仍然可以通过在另一个store中导入和using一个store来隐式嵌套stores,但是Pinia提供了一种平面的设计结构,同时仍然支持store之间的交叉组合方式。没有namespaced模块。考虑到stores的平面结构(flat architecture),namespacing stores是定义store的固有方式,你可以说所有的stores都是namespacing

2. 核心概念

2.1 定义一个 Store

在深入研究核心概念之前,我们需要知道一个store(存储)是使用defineStore()定义的, 并且它需要一个唯一的名字,作为第一个参数传递:

import {defineStore } from 'pinia'// useStore 一切例如 useUser, useCart// 第一个参数是应用程序中store的唯一idexport const useStore = defineStore('main', {// 其他选项...})

这个name也称为id,它是必要的。Pinia使用它将store连接到devtools。命名返回的函数*use…*是跨成分的约定,使其用法习惯化。

2.1.1 使用store

我们定义一个store因为直到在setup()内部调用“useStore()”之前, store并不会被创建:

import {useStore } from '@/stores/counter'export default {setup() {const store = useStore()return {// 您可以返回整个存储实例,以便在模板中使用它store,}},}

你可以定义任意多的stores你应该在不同的文件中定义每个store以充分利用pinia(例如自动允许您的包进行代码拆分和TypeScript推理)。

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

一旦存储被实例化,您就可以直接在存储上访在“state”、“getters”和“actions”中定义的任何属性。我们将在接下来的几页中详细介绍这些内容,但自动完成功能会对您有所帮助。

请注意,store是一个用reactive包装的对象,这意味着没有必要在getters之后写入.value,但是像setup中的props一样,我们不能破坏它:

export default defineComponent({setup() {const store = useStore()// ❌ 这是行不通的,因为它破坏了响应性(reactivity)// 这和从`props`中破坏是一样的const {name, doubleCount } = storename // "eduardo"doubleCount // 2return {// will always be "eduardo"name,// will always be 2doubleCount,// this one will be reactivedoubleValue: computed(() => store.doubleCount),}},})

为了从store中提取属性,同时保持其反应性,您需要使用storeToRefs()。它将为任何响应性(reactivity)属性创建引用。当您只使用存储中(store)的状态,而不调用任何动作时,这很有用:

import {storeToRefs } from 'pinia'export default defineComponent({setup() {const store = useStore()// `name` 和 `doubleCount` 是 reactive refs// 这也将为plugins(插件)添加的属性创建引用,但跳过任何动作或非反应(非ref/reactive)属性const {name, doubleCount } = storeToRefs(store)return {name,doubleCount}},})

2.2 状态(State)

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

import {defineStore } from 'pinia'const useStore = defineStore('storeId', {// 推荐用于全类型推理的箭头函数state: () => {return {// 所有这些属性都将自动推断出它们的类型counter: 0,name: 'Eduardo',isAdmin: true,}},})

技巧:

如果您使用 Vue 2,您在state中创建的数据遵循与data 在 Vue 实例中相同的规则,即状态对象必须是普通的,并且您需要在向其添加新属性Vue.set()时调用

另请参阅:Vue#data

2.2.1 访问状态

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

const store = useStore()store.counter++

2.2.2 重置状态

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

const store = useStore()store.$reset()

2.2.2.1 使用 options API

如果您不使用 组合 API,而您正在使用computed,methods, …,则可以使用mapState()帮助器将状态属性映射为只读计算属性:

import {mapState } from 'pinia'export default {computed: {// 在组件内允许访问 this.counter // 与从 store.counter 读取一样...mapState(useStore, ['counter'])// 与上面一样但是将注册它为 this.myOwnName...mapState(useStore, {myOwnName: 'counter',// 你也可以写一个函数来访问 storedouble: store => store.counter * 2,// 它也能访问 `this` ,但是它不会正确地标注类型...magicValue(store) {return store.someGetter + this.counter + this.double},}),},}

可修改状态

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

import {mapWritableState } from 'pinia'export default {computed: {// 允许访问组件内部的 this.counter,并允许设置它// this.counter++// 从 store.counter 中读取也一样...mapWritableState(useStore, ['counter'])// 与上一样,但是将其注册为 this.myOwnName...mapWritableState(useStore, {myOwnName: 'counter',}),},}

技巧

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

2.2.3 更改 state

除了直接用store.counter++改变 store 之外,您还可以调用该$patch方法。它允许您对部分state对象同时应用多个更改:

store.$patch({counter: store.counter + 1,name: 'Abalam',})

但是,使用这种语法应用某些 突变 确实很难或成本很高:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合。正因为如此,此$patch方法还接受一个函数来对这种难以用patch 对象应用的突变进行分组:

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

这里的主要区别是$patch()允许您将多个更改分组到 devtools 中的一个条目中。请注意**,直接更改state$patch()出现在 devtools 中,**并且可以穿越时间(在 Vue 3 中还没有)。

2.2.4 更换 state

您可以通过将store$state属性设置为新对象来替换store的整个状态:

store.$state = {counter: 666, name: 'Paimon' }

您也可以通过更改替换您的应用程序的整体状态state中的pinia实例。这在SSR期间用于hydration。

pinia.state.value = {}

2.2.5 订阅状态

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

cartStore.$subscribe((mutation, state) => {// import { MutationType } from 'pinia'mutation.type // 'direct' | 'patch object' | 'patch function'// 与 cartStore.$id 一样mutation.storeId // 'cart'// 仅当 mutation.type === 'patch object' 时可用mutation.payload // patch object passed to cartStore.$patch()// 每当状态改变时,将整个状态保存到本地存储中localStorage.setItem('cart', JSON.stringify(state))})

默认情况下,状态订阅绑定到添加它们的组件(如果存储在组件的内部setup())。意思是,当组件被卸载时,它们将被自动删除。如果你想保持他们后成分是卸载,通过{ detached: true }作为第二个参数,以分离的状态订阅从当前组件:

export default {setup() {const someStore = useSomeStore()// 该 subscription 将在组件卸载后保留someStore.$subscribe(callback, {detached: true })// ...},}

技巧

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

watch(pinia.state,(state) => {// 每当状态改变时,将整个状态保存到本地存储中localStorage.setItem('piniaState', JSON.stringify(state))},{deep: true })

2.3 访问器(Getters)

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

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

大多数时候,getter 只会依赖状态,但是,他们可能需要使用其他 getter。因此,我们可以在定义常规函数时访问整个 store 实例,但在 TypeScript 中需要定义返回类型的类型。这是由于TypeScript中的一个已知限制,不影响用箭头函数定义的getter,也不影响不使用 This 的getter:

export const useStore = defineStore('main', {state: () => ({counter: 0,}),getters: {// 自动地推断返回值类型为一个数doubleCount(state) {return state.counter * 2},//返回值类型 **必须** 被明确地指定doublePlusOne(): number {// 为整个存储(store)自动完成和类型注释 ✨return this.counter * 2 + 1},},})

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

<template><p>Double count is {{ store.doubleCount }}</p></template><script>export default {setup() {const store = useStore()return {store }},}</script>

2.3.1 访问其他getter

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

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

2.3.2 将参数传递给getter

Getter只是在幕后计算的属性,因此不可能将任何参数传递给它们。但是您可以从getter返回一个函数以接受任何参数:

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

并在组件中使用:

<script>export default {setup() {const store = useStore()return {getUserById: store.getUserById }},}</script><template>User 2: {{ getUserById(2) }}</template>

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

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

2.3.3 访问其他store getters

要使用另一个store getters,可以直接在getters内部使用:

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

2.3.4 使用 setup() 的用法

你可以直接作为store的属性访问任意getter(与状态state属性完全相同):

export default {setup() {const store = useStore()store.counter = 3store.doubleCount // 6},}

2.3.5 使用 options API 的用法

您可以使用前一部分在 state 中使用的相同的mapState()函数来映射到 getter:

import {mapState } from 'pinia'export default {computed: {// 允许访问组件内部的 this.doubleCounter // 与从 store.doubleCounter 中读取一样...mapState(useStore, ['doubleCount'])// same as above but registers it as this.myOwnName...mapState(useStore, {myOwnName: 'doubleCounter',// 你也可以写一个函数以访问 storedouble: store => store.doubleCount,}),},}

2.4 动作(Actions)

动作(Actions)相当于组件中的**方法**。它们可以使用defineStore()的actions属性进行定义,并且非常适合定义业务逻辑:

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

就像getter一样,动作(Actions)可以通过this访问到整个存储实例,与完整的类型(和自动完成✨)的支持。与它们不同的是,actions可以是异步的(asynchronous),您可以await在它们内部进行任何 API 调用甚至其他操作!这是一个使用Mande的示例。请注意,您使用的库并不重要,只要您获得一个 Promise,您甚至可以使用本机fetch函数(仅限浏览器):

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)// 让 form 组件展示 errorreturn error}},},})

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

动作像方法一样被调用:

export default defineComponent({setup() {const main = useMainStore()// 作为 store 的方法来调用 actionmain.randomizeCounter()return {}},})

2.4.1 访问其他 存储(store)中的 action

要使用另一个store,您可以直接在action内部使用它:

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

2.4.2 使用setup()时的用法

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

export default {setup() {const store = useStore()store.randomizeCounter()},}

2.4.3 使用 options API 时的用法

如果您不使用组合 API,而您正在使用computed,methods, …,则可以使用mapActions()帮助器将操作属性映射为组件中的方法:

import {mapActions } from 'pinia'export default {methods: {// 组件内部允许访问 this.increment() // 就像从 store.increment() 调用一样...mapActions(useStore, ['increment'])// 与上面一样但是注册其为 this.myOwnName()...mapActions(useStore, {myOwnName: 'doubleCounter' }),},}

2.4.4 订阅action

可以用 观察action及其结果store.$onAction()。传递给它的回调在操作本身之前执行。after处理承诺并允许您更改操作的返回值。onError允许您阻止错误传播。这些对于在运行时跟踪错误很有用,类似于Vue 文档中的这个技巧。

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

const unsubscribe = someStore.$onAction(({name, // 动作(action)名store, // 存储实(store)例, 与 `someStore` 一样args, // 传入该动作的一组参数after, // 当 action 返回或者决定(resolves)后的钩子onError, // 当操作抛出或拒绝(rejects)时的钩子}) => {// 此特定操作调用的共享变量const startTime = Date.now()// 这将在执行`store action`之前触发console.log(`Start "${name}" with params [${args.join(', ')}].`)// 这将在`action` 成功(succeeds) 且完全运行后触发// it waits for any returned promisedafter((result) => {console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`)})// 这将在`action`抛出错误或者返回一个拒绝的(rejects)`Promise`是触发onError((error) => {console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`)})})// 手动移除监听器unsubscribe()

默认情况下,操作订阅绑定到添加它们的组件(如果商店位于组件的 内部setup())。意思是,当组件被卸载时,它们将被自动删除。如果你想保持他们后成分是卸载,通过true作为第二个参数到分离的action订阅*从当前组件:

export default {setup() {const someStore = useSomeStore()// this subscription will be kept after the component is unmountedsomeStore.$onAction(callback, true)// ...},}

2.5 插件(Plugins)

Pinia 存储(store)可以通过 低级API 完全扩展。以下是你可以执行的 action:

存储(store) 添加新属性;当定义存储时,添加新选项;向存储添加新方法;包装现有方法;更改甚至取消动作(action);执行如本地存储等副效果;仅适用于特定存储

插件将使用 添加到pinia 实例中。最简单的示例是通过返回对象向所有存储添加静态属性:pinia.use()

import {createPinia } from 'pinia'// 给每个安装该插件后创建的 存储(store) 添加一个命名为 `secret` 的属性// 这可能在不同的文件中function SecretPiniaPlugin() {return {secret: 'the cake is a lie' }}const pinia = createPinia()// 将插件交给 piniapinia.use(SecretPiniaPlugin)// 在另一个文件中const store = useStore()store.secret // 'the cake is a lie'

这对于添加全局对象(如routermodalToast管理器)非常有用。

2.5.1 介绍

Pinia 插件是一个函数,options 返回要添加到存储(store)中的属性。它需要一个 options 参数,一个上下文:

export function myPiniaPlugin(context) {context.pinia // 使用 `createPinia()` 创建 pinia context.app// 使用 `createApp()` 创建当前的 app (仅 Vue)context.store // 当前扩展插件的存储context.options // 传递给 `defineStore()` 的被定义存储的 options 对象 // ...}

然后,此函数传递pinia.use()的结果 给pinia

pinia.use(myPiniaPlugin)

插件只适用于pinia传递到应用程序后创建的存储(store)中,否则它们将不会被应用。

2.5.2 存储(store)拓展

您可以通过在插件中返回属性的对象来向每个存储添加属性:

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

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

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

插件返回的任何属性都会被 devtools 自动跟踪,因此为了在 devtools 中可见,请确保仅在要在 devtools 中调试它时才将其添加到 dev 模式下:hello``store._customProperties

// 从上面的例子pinia.use(({store }) => {store.hello = 'world'// 确保你的 bundler 能处理它。 默认情况下,webpack 和 vite 应该这样做if (process.env.NODE_ENV === 'development') {// 添加你在存储(store)中设置的任何键(key)store._customProperties.add('hello')}})

注意每个存储(store)都使用了reactive进行包装,自动地包装任意Ref (, , ...),包括了:ref()computed()

const sharedRef = ref('shared')pinia.use(({store }) => {// 每个 store 都有自己的 `hello` 属性store.hello = ref('secret')// 它会自动解包装store.hello // 'secret'// 所有的 stores 共享值 `shared` 属性store.shared = sharedRefstore.shared // 'shared'})

这就是您可以访问所有计算属性的原因,而无需访问它们,以及为什么它们是响应式的.value

2.5.2.1 添加新状态

如果要向存储添加新的状态属性或要在水化期间使用的属性,则必须在两个位置添加它

因为在store中,所以你可以访问它store.myStatestore.$state上,因此它可以在devtools中使用,并在SSR期间序列化

请注意,这允许您共享或属性:ref``computed

const globalSecret = ref('secret')pinia.use(({store }) => {// `secret` 被共享到所有的 stores 中store.$state.secret = globalSecretstore.secret = globalSecret// 它会自动解包store.secret // 'secret'const hasError = ref(false)store.$state.hasError = hasError// this one must always be setstore.hasError = toRef(store.$state, 'hasError')// in this case it's better not to return `hasError` since it// will be displayed in the `state` section in the devtools// anyway and if we return it, devtools will display it twice.})

警告

如果您使用的是Vue 2,Pinia 会受到与 Vue相同的reactivity警告。在创建新的状态属性时,您需要使用 from 和:set@vue/composition-apisecrethasError

import {set } from '@vue/composition-api'pinia.use(({store }) => {if (!store.$state.hasOwnProperty('hello')) {const secretRef = ref('secret')// If the data is meant to be used during SSR, you should// set it on the `$state` property so it is serialized and// picked up during hydrationset(store.$state, 'secret', secretRef)// set it directly on the store too so you can access it// both ways: `store.$state.secret` / `store.secret`set(store, 'secret', secretRef)store.secret // 'secret'}})

2.5.3 添加新的外部属性

在添加外部属性、来自其他库的类实例或只是非响应性内容时,在将对象传递到 pinia 之前,应先将其包装起来。下面是将路由器添加到每个存储(store)的示例:markRaw()

import {markRaw } from 'vue'// 基于你的 router 在哪进行调整import {router } from './router'pinia.use(({store }) => {store.router = markRaw(router)})

2.5.4 在插件内部调用$subscribe

您也可以在插件中使用store.KaTeX parse error: Expected 'EOF', got '#' at position 60: …epts/state.html#̲subscribing-to-…onAction:

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

2.5.5 添加新 options

在定义存储时可以创建新 options,以便以后从插件中使用它们。例如,您可以创建一个debounce选项来取消对任何 动作(action) 的debounce:

defineStore('search', {actions: {searchContacts() {// ...},},// 这在接下来将被一个插件读取debounce: {// debounce the action searchContacts by 300mssearchContacts: 300,},})

然后,插件可以读取该选项以包装动作(action)并替换原始动作:

// 使用任何 debounce 库import debounce from 'lodash/debunce'pinia.use(({options, store }) => {if (options.debounce) {// 我们正在用新的 动作(actions) 取代旧的动作return Object.keys(options.debounce).reduce((debouncedActions, action) => {debouncedActions[action] = debounce(store[action],options.debounce[action])return debouncedActions}, {})}})

请注意,使用安装程序语法时,自定义选项将作为第 3 个参数传递:

defineStore('search',() => {// ...},{// 这将接下来被一个插件读取debounce: {// debounce the action searchContacts by 300mssearchContacts: 300,},})

2.5.6 TypeScript

上面显示的所有内容都可以通过类型支持来完成,因此您永远不需要使用any@ts-ignore

2.5.6.1 插件( plugins)类型注释

Pinia插件可以按如下方式注释类型:

import {PiniaPluginContext } from 'pinia'export function myPiniaPlugin(context: PiniaPluginContext) {// ...}

2.5.6.2 存储属性(store properties)类型注释

向存储区添加新属性时,还应扩展PiniaCustomProperties接口。

import 'pinia'declare module 'pinia' {export interface PiniaCustomProperties {// 通过使用一个 setter 我们能允许 strings and refsset hello(value: string | Ref<string>)get hello(): string// 您也可以定义更简单的值simpleNumber: number}}

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

pinia.use(({store }) => {store.hello = 'Hola'store.hello = ref('Hola')store.number = Math.random()// @ts-expect-error: we haven't typed this correctlystore.number = ref(Math.random())})PiniaCustomProperties`是允许您引用存储的属性的泛型类型。想象一下下面的示例,其中我们复制初始选项为(这仅适用于选项存储):`$optionspinia.use(({options }) => ({$options: options }))

我们可以通过使用以下4种泛型类型来正确注释它的类型:PiniaCustomProperties

import 'pinia'declare module 'pinia' {export interface PiniaCustomProperties<Id, S, G, A> {$options: {id: Idstate?: () => Sgetters?: Gactions?: A}}}

技巧

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

S:StateG:GettersA:ActionsSS:Setup Store/Store

2.5.6.3 新状态(state)类型注释

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

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

2.5.6.4 新 options 类型注释

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

import 'pinia'declare module 'pinia' {export interface DefineStoreOptionsBase<S, Store> {// 允许为 ms 的任意动作(actions)定义一个数debounce?: Partial<Record<keyof StoreActions<Store>, number>>}}

提示

还有一个类型用于从 Store 类型中提取getter

您还可以通过分别扩展类型DefineStoreOptionsDefineSetupStoreOptions来扩展setup storesoption stores

2.5.7 Nuxt.js

当使用pinia与Nuxt一起使用时,您必须首先创建一个Nuxt插件。这将为您提供对实例的访问权限:pinia

// plugins/myPiniaPlugin.jsimport {PiniaPluginContext } from 'pinia'import {Plugin } from '@nuxt/types'function MyPiniaPlugin({store }: PiniaPluginContext) {store.$subscribe((mutation) => {// 相应到存储(store)的改变console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)})return {creationTime: new Date() }}const myPlugin: Plugin = ({pinia }) {pinia.use(MyPiniaPlugin);}export default myPlugin

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

3. 服务端渲染 (Server Side Rendering,SSR)

注意

如果你使用的是Nuxt.js, 则需要阅读these instructions

只要您在setup函数、gettersactions的顶部调用useStore()函数,使用Pinia创建stores,就可以进行SSR了:

export default defineComponent({setup() {// this works because pinia knows what application is running inside of// `setup()`const main = useMainStore()return {main }},})

3.1 使用 setup() 之外的 store

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

const pinia = createPinia()const app = createApp(App)app.use(router)app.use(pinia)router.beforeEach((to) => {// ✅ 这将确保正确的store用于当前运行的应用程序const main = useMainStore(pinia)if (to.meta.requiresAuth && !main.isLoggedIn) return '/login'})

Pinia 方便地将自己作为$pinia添加到您的应用程序中,这样您就可以在像serverPrefetch()这样的功能中使用它:

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

3.2 State hydration

To hydrate the initial state,你需要确保rootState包含在HTML中的某个地方以便Pinia稍后获取它。根据您使用的SSR,出于安全原因,你应跳过这个状态。我们推荐使用Nuxt.js使用的那一个 @nuxt/devalue :

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`上读取。// serialize, escape (如果用户可以更改状态的内容,这一点非常重要,几乎总是如此), // 并将其放在页面的某个地方,例如作为一个全局变量。devalue(pinia.state.value)

取决于您使用什么进行SSR,您将设置一个初始状态变量,该变量将在HTML中序列化。你也应该保护自己免受XSS袭击。 例如:使用vite-ssr 你可以使用transformStateoption 和@nuxt/devalue

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()函数之前,请确保hydrate pinia的状态。例如,如果我们将状态序列化为一个<script>标签,使其在客户端可以通过window.__pinia进行全局访问,我们可以这样写:

const pinia = createPinia()const app = createApp(App)app.use(pinia)// 必须由用户设置if (isClient) {pinia.state.value = JSON.parse(window.__pinia)}

3.3 Nuxt.js

将Pinia与Nuxt.js一起使用更容易,因为Nuxt在服务器端渲染时会处理很多事情。例如,你不需要关心序列化或XSS攻击

3.3.1 安装

确保安装@nuxtjs/composition-apipinia一起安装:

yarn add pinia @pinia/nuxt @nuxtjs/composition-api# or with npmnpm install pinia @pinia/nuxt @nuxtjs/composition-api

我们提供了一个module来为您处理一切,您只需要将它添加到nuxt.config.js文件中的buildModules中:

// nuxt.config.jsexport default {// ... other optionsbuildModules: [// https://composition-/getting-started/setup#quick-start'@nuxtjs/composition-api/module','@pinia/nuxt',],}

就这样,像往常一样使用你的store

3.3.2 在setup()外使用store

如果你打算在setup()外面定义一个 store ,必须通过pinia对象给useStore()。我们将它添加到上下文中,因此您可以在asyncData()fetch()中访问它:

import {useStore } from '~/stores/myStore'export default {asyncData({pinia }) {const store = useStore(pinia)},}

3.3.3 在stores中使用Nuxt context

您也可以通过使用注入的属性$nuxt在任何store中使用上下文(context) :

import {useUserStore } from '~/stores/userStore'defineStore('cart', {actions: {purchase() {const user = useUserStore()if (!user.isAuthenticated()) {this.$nuxt.redirect('/login')}},},})

3.3.4 将 Pinia 和 Vuex 一起使用

建议避免同时使用pinia和Vuex,但如果您需要同时使用两者,则需要告诉Pinia不要禁用它:

// nuxt.config.jsexport default {buildModules: ['@nuxtjs/composition-api/module',['@pinia/nuxt', {disableVuex: false }],],// ... other options}

3.3.5 Typescript

如果您正在使用 TypeScript 或拥有jsconfig.json,您还应该为context.pinia添加类型:

{"types": [// ..."@pinia/nuxt"]}

这也将确保您具有自动完成功能 😉 .

4. 热模块替换 (Hot Module Replacement, HMR)

Pinia支持Hot Module替换,因此您可以编辑您的 stores并在应用程序中直接与它们交互,而无需重新加载页面,从而允许您保持现有 state(状态)。添加甚至删除stateactionsgetters.

目前,只有Vite得到官方支持,但任何实现import.meta.hot规范的bundler都应该可以工作(例如 webpack似乎使用import.meta.webpackHot而不是import.meta.hot)。您需要在任何存储声明旁边添加这段代码。假设您有三个存储:auth.js,cart.js,和chat.js,在创建store definition后,您必须添加(并修改)此内容:

// auth.jsimport {defineStore, acceptHMRUpdate } from 'pinia'const useAuth = defineStore('auth', {// options...})// 在这种情况下,确保传递正确的store定义`useAuth`。if (import.meta.hot) {import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))}

5. 测试 stores

根据设计,Stores将在许多地方使用,这可能会使测试变得更加困难。幸运的是,情况并非一定如此。在测试store时,我们需要注意三件事:

pinia实例:Stores的工作不能没有它actions:大多数时候,它们包含了我们stores最复杂的逻辑。如果他们被default mocked 岂不是很好?插件(Plugins): 如果你依赖插件,你也必须安装它们进行测试

根据你测试的内容或方式,我们需要以不同的方式处理这三个问题:

单元测试 stores (组件外)使用stores单元测试组件端到端测试

5.1 Unit 测试一个 store

要对一个store进行单元测试,最重要的部分是创建一个pinia实例:

// counterStore.spec.tsimport {setActivePinia, createPinia } from 'pinia'import {useCounter } from '../src/stores/counter'describe('Counter Store', () => {beforeEach(() => {// 创建一个新的pinia并激活它,这样它就可以被任何`useStore()`调用自动拾取,而无需传递给它:// `useStore(pinia)`setActivePinia(createPinia())})it('increments', () => {const counter = useCounter()expect(counter.n).toBe(0)counter.increment()expect(counter.n).toBe(1)})it('increments by amount', () => {const counter = useCounter()counter.increment(10)expect(counter.n).toBe(10)})})

如果你有任何store插件,有一件重要的事情要知道:plugins won’t be used untilpiniais installed in an App. 这可以通过创建一个空的或假的应用程序来解决:

import {setActivePinia, createPinia } from 'pinia'import {createApp } from 'vue'import {somePlugin } from '../src/stores/plugin'// 与上面相同的代码...// 你不需要为每个测试创建一个应用程序const app = createApp({})beforeEach(() => {const pinia = createPinia().use(somePlugin)app.use(pinia)setActivePinia(pinia)})

5.2 Unit 测试模块

这可以通过createTestingPinia()实现。我还没有能够为此编写适当的文档,但可以通过自动完成和工具提示中显示的文档来发现它的用法。

首先安装 :@pinia/testing

npm i -D @pinia/testing

并确保在安装组件时在测试中 创建测试 pinia:

import {mount } from '@vue/test-utils'import {createTestingPinia } from '@pinia/testing'const wrapper = mount(Counter, {global: {plugins: [createTestingPinia()],},})const store = useSomeStore() // 使用测试pinia!// 状态(state)可以被直接操纵store.name = 'my new name'// 也可以通过 patch 来完成store.$patch({name: 'new name' })expect(store.name).toBe('new name')// 默认情况下,动作(actions)是存根化的,但是// 可以通过将一个 option 传递给`createTestingPinia()`来进行配置store.someAction()expect(store.someAction).toHaveBeenCalledTimes(1)expect(store.someAction).toHaveBeenLastCalledWith()

5.3 E2E 测试

说到 pinia,你不需要为e2e测试改变什么,这就是e2e测试的全部意义!您也许可以测试HTTP请求,但这远远超出了本指南的范围 😄.

6. 不使用setup()时的用法

即使你不适用组合式API,也可以使用Pinia(尽管如果您正在使用Vue 2,您仍然需要安装@vue/composition-api插件)。然而我们建议尝试一下组合式API并学习它,但现在可能还不是您和您的团队的时候,您可能正在迁移应用程序,或者有任何其他原因。有几个功能:

mapStoresmapStatemapWritableState⚠️ mapGetters (只是为了方便迁移,请改用mapState()mapActions

6.1 允许访问整个 store

如果您需要访问store中的几乎所有内容,映射store的每个属性可能太多了… 相反,您可以使用mapStores()访问整个store

import {mapStores } from 'pinia'// 给定两个具有以下id的storesconst useUserStore = defineStore('user', {// ...})const useCartStore = defineStore('cart', {// ...})export default {computed: {// 注意,我们没有传递数组,只是一个接一个地传递一个`store`,每个存储都可以作为其id + 'Store'来访问...mapStores(useCartStore, useUserStore),}),},methods: {async buyStuff() {// 在任何地方使用它们!if (this.userStore.isAuthenticated()) {await this.cartStore.buy()this.$router.push('/purchased')}},},}

默认情况下,Pinia将为每个store的id添加"Store"后缀。您可以通过调用setMapStoreSuffix()来自定义此行为:

import {createPinia, setMapStoreSuffix } from 'pinia'// 完全删除后缀: this.user, this.cartsetMapStoreSuffix('')// this.user_store, this.cart_store (没关系,我不会评论你)setMapStoreSuffix('_store')export const pinia = createPinia()

6.2 TypeScript

默认情况下, 所有 map helpers 都支持自动完成,你不需要做任何事情。 你可以调用setMapStoreSuffix()来更改"Store"后缀,您还需要将其添加到TS文件或您的global.d.ts文件中的某个位置。最方便的地方就是你调用setMapStoreSuffix()的相同位置:

import {createPinia, setMapStoreSuffix } from 'pinia'setMapStoreSuffix('') // 完全去掉后缀export const pinia = createPinia()declare module 'pinia' {export interface MapStoresCustomization {// 将其设置为与上面相同的值suffix: ''}}

警告

如果你使用一个 TypeScript 声明文件 (如global.d.ts),确保import 'pinia'在顶部以显示所有现有类型。

7. 组合Stores

组合stores就是让store互相使用,有一个规则需要遵守:

如果两个或者多个stores互用彼此,它们不能通过getter或action创建无限循环。他们都不能在他们的 setup 函数中直接读取彼此的状态:

const useA = defineStore('a', () => {const b = useB()// ❌ 这是不可行的,因为 b 也试图读取 a.nameb.namefunction doSomething() {// ✅ 在 computed 或者 actions 中读取 b 属性const bName = b.name// ...}return {name: ref('I am A'),}})const useB = defineStore('b', () => {const a = useA()// ❌ 这是不可行的,因为 a 也试图读取 a.namea.namefunction doSomething() {// ✅ 在 computed 或者 actions 中读取 b 属性const aName = a.name// ...}return {name: ref('I am B'),}})

7.1 嵌套 store

注意,如果一个store使用另一个store,则无需在单独的文件中创建新的store。您可以直接导入,把它想象成嵌套。

您可以在任何getteraction顶部调用useOtherStore()

import {useUserStore } from './user'export const cartStore = defineStore('cart', {getters: {// ... 其它 getterssummary(state) {const user = useUserStore()return `Hi ${user.name}, you have ${state.list.length} items in your cart. It costs ${state.price}.`},},actions: {purchase() {const user = useUserStore()return apiPurchase(user.id, this.list)},},})

7.2 Getters 共享

您可以简单地在getter内部调用useOtherStore()

import {defineStore } from 'pinia'import {useUserStore } from './user'export const useCartStore = defineStore('cart', {getters: {summary(state) {const user = useUserStore()return `Hi ${user.name}, you have ${state.list.length} items in your cart. It costs ${state.price}.`},},})

7.3 动作(actions)共享

同理也可用于actions的共享:

import {defineStore } from 'pinia'import {useUserStore } from './user'export const useCartStore = defineStore('cart', {actions: {async orderCart() {const user = useUserStore()try {await apiOrderCart(user.token, this.items)// another actionthis.emptyCart()} catch (err) {displayError(err)}},},})

如果觉得《pinia中文文档 指导文档中文翻译版 pinia指导中文翻译》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。