1、定义Store
在进入核心概念之前,我们需要知道Store 是使用defineStore() 定义的,并且它需要一个唯一的名称,作为第一个参数传递:
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
})
这个名称(也称为id) 是必需的,Pania 使用它来将store 连接到devtools 。将返回的函数命名为use... 是可组合项之间的约定,以使其用法符合使用习惯。
1.1 使用Store
我们定义了一个Store ,因为只有在setup() 中调用了useStore() ,store 才会被创建:
import { useStore } from '@/stores/counter'
export default {
setup() {
const store = useStore()
return {
store,
}
},
}
您可以根据需要定义任意数量的store ,并且应该在不同的文件中定义每个store 以充分利用 Pinia (例如自动允许您的bundle 进行代码拆分和TypeScript 推理)。
如果您还没有使用setup 组件,您仍然可以将Pinia与辅助函数一起使用
一旦Store 被实例化,您就可以直接在store 上访问在state 、getters 和actions 中定义的任何属性。我们将在下一章中看到这些细节,自动补全功能也将帮助你。
请注意,Store 是一个用reactive 包装的对象,这意味着不需要在getter 后面写 .value ,但是,就像setup 中的props 一样,我们不能对它进行解构:
export default defineComponent({
setup() {
const store = useStore()
const { name, doubleCount } = store
name
doubleCount
return {
name,
doubleCount,
doubleValue: computed(() => store.doubleCount),
}
},
})
为了从store 中提取属性,同时保持其响应性,您需要使用storeToRefs() 。它将为任何响应性属性创建引用。当您仅使用store 中的state ,且不调用任何操作时,这很有用:
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
const store = useStore()
const { name, doubleCount } = storeToRefs(store)
return {
name,
doubleCount
}
},
})
2、State
大多数时候,state 是Store 的中心部分。人们通常从定义应用程序的state 开始。在Pinia 中,state 被定义为一个返回初始state 的函数。这保证了Pinia 在服务器端和客户端都能使用。
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
state: () => {
return {
counter: 0,
name: 'Eduardo',
isAdmin: true,
}
},
})
TIP 如果您使用Vue 2 ,您在state 中创建的数据应遵循与Vue 实例中data 相同的规则,即 state 对象必须是普通的,并且在向其添加新属性时需要调用Vue.set() 。另请参阅:Vue#data
2.1 访问State
默认情况下,你可以通过Store 实例直接读写state :
const store = useStore()
store.counter++
2.2 重置State
您可以通过调用store 上的$reset() 方法将state 重置为初始值:
const store = useStore()
store.$reset()
2.2.1 使用Options API
对于以下示例,您可以假设创建了以下store :
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counterStore', {
state: () => ({
counter: 0
})
})
2.2.2 使用setup()
虽然Composition API 并不适合所有人,但是setup() 钩子可以让Pinia 更容易在Options API 中使用。不需要额外的辅助函数!
import { useCounterStore } from '../stores/counterStore'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
tripleCounter() {
return counterStore.counter * 3
},
},
}
2.2.3 不使用setup()
如果您不使用Composition API ,而您使用的是computed , methods ,…,则你可以使用mapState() 辅助函数将状态属性映射为只读计算属性:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
...mapState(useCounterStore, ['counter']),
...mapState(useCounterStore, {
myOwnName: 'counter',
double: store => store.counter * 2,
magicValue(store) {
return store.someGetter + this.counter + this.double
},
}),
},
}
可修改的 State
如果您希望能够写入这些状态属性(例如,如果您有一个表单),您可以使用mapWritableState() 代替。请注意,您不能像mapState() 那样传递函数:
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
...mapWritableState(useCounterStore, ['counter']),
...mapWritableState(useCounterStore, {
myOwnName: 'counter',
}),
},
}
TIP 您不需要mapWritableState() 来处理像数组这样的集合,除非你用cartItems = [] 来替换整个数组,mapState() 仍然允许你在你的集合上调用方法。
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.4 替换 State
您可以通过将store 的$state 属性设置一个新对象来替换整个store 的状态:
store.$state = { counter: 666, name: 'Paimon' }
您还可以通过更改 pinia 实例的state 来替换应用程序的整个状态。这在SSR激活中使用。
pinia.state.value = {}
2.5 订阅 State
您可以通过store 的$subscribe() 方法查看状态及其变化,这与Vuex 的 subscribe 方法类似。与常规的watch() 相比,使用$subscribe() 的优势在于,订阅只会在patches 之后触发一次(例如,当使用上面的函数版本时)。
cartStore.$subscribe((mutation, state) => {
mutation.type
mutation.storeId
mutation.payload
localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,状态订阅被绑定到添加它们的组件上(如果store 在组件的setup() 中)。这意味着,当组件被卸载时,它们将被自动删除。如果你想在组件卸载后保留它们,传递{ detached: true } 作为第二个参数来从当前组件中分离状态订阅:
export default {
setup() {
const someStore = useSomeStore()
someStore.$subscribe(callback, { detached: true })
},
}
TIP 您可以查看`Pinia```实例上的整个状态:
watch(
pinia.state,
(state) => {
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
3、Getters
Getters 与store 状态的计算值完全相同。它们可以用defineStore() 中的getters 属性来定义。它们接收state 作为第一个参数,鼓励使用箭头函数:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
},
})
大多数时候,getters 只依赖于state ,但是,它们也可能需要使用其他的getters 。因此,当定义一个常规函数时,我们可以通过this 访问整个store 实例,但需要定义返回类型的类型(在Typescript 中)。这是由于TypeScript 中的一个已知限制,不会影响使用箭头函数定义的 getters ,也不会影响不使用this 的getters :
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount(state) {
return state.counter * 2
},
doublePlusOne(): number {
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>
3.1 访问其他 getters
与计算属性一样,您可以组合多个getters 。通过this 访问任何其他的getters 。即使您不使用TypeScript ,您也可以使用JSDoc提示IDE 输入的类型:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
doubleCountPlusOne() {
return this.doubleCount + 1
},
},
})
3.2 将参数传递给 getters
getters 只是后台的计算属性,因此不可能向它们传递任何参数。但是,您可以从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>
<p>User 2: {{ getUserById(2) }}</p>
</template>
请注意,执行此操作时,getters 不再被缓存,它们只是您调用的普通函数。但是,您可以在 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)
},
},
})
3.3 访问其他 Stores 的 getters
要使用其他store 的getters ,您可以直接在getter 内部使用它:
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
3.4 setup() 中的用法
您可以直接访问任何getter 作为store 的属性(完全和state 属性一样):
export default {
setup() {
const store = useStore()
store.counter = 3
store.doubleCount
},
}
3.5 Options API 中的用法
对于以下示例,您可以假设创建了以下store :
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counterStore', {
state: () => ({
counter: 0
}),
getters: {
doubleCounter() {
return this.counter * 2
}
}
})
3.5.1 使用 setup()
虽然Composition API 并不适合所有人,但是setup() 钩子可以让Pinia 更容易在Options API 中使用。不需要额外的辅助函数!
import { useCounterStore } from '../stores/counterStore'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return counterStore.doubleCounter * 2
},
},
}
3.5.2 不使用 setup()
您可以像前一节的state 一样使用mapState() 函数来映射到getters :
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
...mapState(useCounterStore, ['doubleCount']),
...mapState(useCounterStore, {
myOwnName: 'doubleCounter',
double: store => store.doubleCount,
}),
},
}
4、Actions
Actions 相当于组件中的 methods 。可以使用defineStore() 中的actions 属性来定义它们,并且它们非常适合定义业务逻辑:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
和getters一样,actions 通过this 来访问整个store 实例,还有完整的类型支持(和自动补全功能)。与它们不同的是,**actions **可以是异步的,您可以在它们内部进行任何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)
return error
}
},
},
})
您也可以完全自由地设置任何您想要的参数并返回任何东西。当调用actions 时,一切都会被自动推断出来!
actions 与methods 调用类似:
export default defineComponent({
setup() {
const main = useMainStore()
main.randomizeCounter()
return {}
},
})
4.1 访问其他 stores 的 actions
要使用另一个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')
}
},
},
})
4.2 setup() 中的用法
您可以直接调用任何action 作为store 的方法:
export default {
setup() {
const store = useStore()
store.randomizeCounter()
},
}
4.3 Options API 中的用法
对于以下示例,您可以假设创建了以下store :
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counterStore', {
state: () => ({
counter: 0
}),
actions: {
increment() {
this.counter++
}
}
})
4.3.1 使用 setup()
虽然Composition API 并不适合所有人,但setup() 钩子可以让Pinia 更容易在Options API 中使用。不需要额外的辅助函数!
import { useCounterStore } from '../stores/counterStore'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
methods: {
incrementAndPrint() {
counterStore.increment()
console.log('New Count:', counterStore.count)
},
},
}
4.3.2 不使用 setup()
如果您根本不想使用Composition API ,您可以使用mapActions() 辅助函数将actions 属性映射为组件中的methods :
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
methods: {
...mapActions(useCounterStore, ['increment']),
...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }),
},
}
4.4 订阅 actions
可以使用store.$onAction() 来观察actions 及其结果。传递给它的回调函数在action 本身之前执行。在处理promises 之后,允许您在action resolves 之后执行函数。类似地,onError 允许你在action 抛出或rejects 时执行函数。这些对于在运行时跟踪错误很有用,类似于Vue文档中的这个技巧。
下面是一个在运行actions 之前和resolve/reject 之后记录日志的示例。
const unsubscribe = someStore.$onAction(
({
name,
store,
args,
after,
onError,
}) => {
const startTime = Date.now()
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 订阅被绑定到添加它们的组件(如果store 在组件的setup() 中)。这就意味着,当组件被卸载时,它们将被自动删除。如果你想在组件卸载后保留它们,传true 作为第二个参数,以将操作订阅与当前组件分离:
export default {
setup() {
const someStore = useSomeStore()
someStore.$onAction(callback, true)
},
}
5、Plugins
由于低版本的API ,Pinia 的stores 可以完全扩展。下面是一些你可以做的事情:
- 向
stores 添加新的属性 - 在定义
stores 时添加新选项 - 向
stores 添加新方法 - 包装现有的方法
- 更改甚至取消操作
- 实现像本地存储这样的功能
- 只适用于特定的
stores
使用pinia.use() 将插件添加到pinia 实例中。最简单的例子是通过返回一个对象向所有stores 添加一个静态属性:
import { createPinia } from 'pinia'
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
pinia.use(SecretPiniaPlugin)
const store = useStore()
store.secret
这对于添加全局对象(如router 、modal 或toast 管理器)非常有用。
5.1 介绍
Pinia 的插件是一个函数,可以选择返回要添加到store 中的属性。它有一个可选参数 context :
export function myPiniaPlugin(context) {
context.pinia
context.app
context.store
context.options
}
然后将此函数传递给pinia 的pinia.use() :
pinia.use(myPiniaPlugin)
插件只应用于stores 被创建在pinia 传递给应用程序后 ,否则它们不会被应用。
5.2 扩展 Store
你可以通过在插件中返回一个属性对象来为每个store 添加属性:
pinia.use(() => ({ hello: 'world' }))
你也可以直接在store 中设置属性,如果可以的话,请返回版本,以便它们可以被devtools 自动跟踪:
pinia.use(({ store }) => {
store.hello = 'world'
})
插件返回的任何属性都将由devtools 自动追踪,因此为了hello 在devtools 中可见,请确保仅在开发模式中添加store._customProperties 属性,如果您想在devtools 中调试的话:
pinia.use(({ store }) => {
store.hello = 'world'
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('hello')
}
})
需要注意的是,每个store 都会使用reactive 包装,并且会自动解包它包含的任何Ref (ref() , computed() , …)等:
const sharedRef = ref('shared')
pinia.use(({ store }) => {
store.hello = ref('secret')
store.hello
store.shared = sharedRef
store.shared
})
这就是为什么您可以访问所有不带.value 计算属性它们是响应式的原因。
5.2.1 添加新状态
如果您想在激活过程中添加新的状态属性或属性到store ,您必须在两个地方添加它:
- 在
store 中,您可以通过store.myState 访问它 - 在
store.$state 中,它可以在devtools 中使用,并且在SSR 期间被序列化。
请注意,这允许您共享ref 或computed 属性:
const globalSecret = ref('secret')
pinia.use(({ store }) => {
store.$state.secret = globalSecret
store.secret = globalSecret
store.secret
const hasError = ref(false)
store.$state.hasError = hasError
store.hasError = toRef(store.$state, 'hasError')
})
请注意,在插件中发生的状态改变或添加(包括调用store.$patch() )发生在store 激活之前,因此不会触发任何订阅。
WARNING 如果您使用的是Vue 2 ,Pinia 将受到与Vue 相同的反应警告。当创建新的状态属性如 secret 和hasError 时,您需要使用来自@vue/composition-api 的set 方法。
import { set } from '@vue/composition-api'
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hello')) {
const secretRef = ref('secret')
set(store.$state, 'secret', secretRef)
set(store, 'secret', secretRef)
store.secret
}
})
5.3 添加新的外部属性
当添加外部属性,来自其他库的类实例或简单的非响应式对象时,应该在将对象传递给pinia 之前使用markRaw() 包装该对象。下面是一个将路由添加到所有store 的示例:
import { markRaw } from 'vue'
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})
5.4 在插件内部调用 $subscribe
您也可以在插件中使用store.$subscribe 和store.$onAction
pinia.use(({ store }) => {
store.$subscribe(() => {
})
store.$onAction(() => {
})
})
5.5 添加新选项
可以在定义stores 时创建新的选项,以便随后从插件中使用它们。例如,你可以创建一个debounce 选项,允许你对任何操作进行debounce :
defineStore('search', {
actions: {
searchContacts() {
},
},
debounce: {
searchContacts: 300,
},
})
插件可以读取该选项来包装actions 并替换原来的actions :
import debounce from 'lodash/debunce'
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
}, {})
}
})
请注意,使用setup 语法时,自定义选项作为第三个参数传入:
defineStore(
'search',
() => {
},
{
debounce: {
searchContacts: 300,
},
}
)
5.6 TypeScript
上面显示的所有内容都可以通过编写支持,因此您无需使用any 或@ts-ignore 。
5.6.1 编写插件
Pinia 插件可以按如下方式编写:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
}
5.6.2 编写新的store属性
当向stores 添加新属性时,您还应该扩展PiniaCustomProperties 接口。
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
set hello(value: string | Ref<string>)
get hello(): string
simpleNumber: number
}
}
然后可以安全地写入和读取:
pinia.use(({ store }) => {
store.hello = 'Hola'
store.hello = ref('Hola')
store.number = Math.random()
store.number = ref(Math.random())
})
PiniaCustomProperties 是一个泛型类型,允许您引用store 的属性。想象一下下面的示例,我们将初始选项复制为$options (这仅适用于option stores ):
pinia.use(({ options }) => ({ $options: options }))
我们可以通过使用PiniaCustomProperties 的4个泛型类型来正确地输入这个值:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}
TIP
在泛型中扩展类型时,它们的命名必须与源码中的完全相同。Id 不能命名为id 或I ,S 也不能命名为State 。以下是每个字母所代表的含义:
- S: State
- G: Getters
- A: Actions
- SS: Setup Store / Store
5.6.3 编写新的状态
当添加新的状态属性时(同时添加到store 和store.$state ),您需要将类型添加到PiniaCustomStateProperties 。与PiniaCustomProperties 不同的是,它只接收State 泛型:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}
5.6.4 编写新的创建选项
当为defineStore() 创建新选项时,您应该扩展DefineStoreOptionsBase 。与PiniaCustomProperties 不同的是,它只公开两种泛型:State 和Store 类型,允许您限制可以定义的类型。例如,你可以使用actions 的名称:
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}
提示 还有一个StoreGetters 类型用于从Store 类型中提取getters 。您还可以分别通过DefineStoreOptions 和DefineSetupStoreOptions 类型来扩展设置setup stores 或option stores 的选项。
5.7 Nuxt.js
当Nuxt 和Pinia 一起使用时,您必须先创建一个Nuxt 插件。这将使您可以访问该Pinia 实例:
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
return { creationTime: new Date() }
}
const myPlugin: Plugin = ({ pinia }) {
pinia.use(MyPiniaPlugin);
}
export default myPlugin
注意上面的例子使用的是TypeScript ,如果你使用的是.js 文件,你必须删除PiniaPluginContext 的类型注释和Plugin 的引入。
6、在组件之外使用 Store
Pinia stores 依赖于Pinia 实例在所有调用中共享相同的store 实例。大多数情况下,只需调用您的useStore() 函数,就可以开箱即用了。例如,在setup() 中,您不需要做任何其他事情。但是在组件之外使用的情况有点不同。在后台,useStore() 会注入到你应用程序的pinia 实例中。这意味着,如果pinia 实例不能被自动注入,你必须手动将它提供给useStore() 函数。根据所编写的应用程序的类型,可以采用不同的方法来解决这个问题。
6.1 单页应用程序
如果你不做SSR (服务端渲染),安装pinia 插件并app.use(pinia) 后,任何调用useStore() 方法将起作用:
import { useUserStore } from '@/stores/user'
import { createApp } from 'vue'
import App from './App.vue'
const userStore = useUserStore()
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
const userStore = useUserStore()
确保始终应用此方法的最简单方法是,通过将useStore() 的调用总是放置在安装pinia 之后运行的函数中,从而推迟对它们的调用。
让我们来看看这个在Vue Router 的导航守卫中使用store 的示例:
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) => {
const store = useStore()
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
6.2 SSR 应用
在处理服务器端渲染时,您必须将 pinia 实例传递给 useStore() 。 这可以防止 pinia 在不同的应用程序实例之间共享全局状态。
|