1 vite
1.1 介绍
1.2 功能
2 pinia1
2.1 Store:定义
01.方式一:选项式
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
02.方式二:组合式
// src/stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
})
03.使用 Store
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
04.从 Store 解构
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
2.2 State:类似data
01.方式一:JS写法
// src/stores/exampleStore.js
import { defineStore } from 'pinia'
// 定义 store
export const useExampleStore = defineStore('exampleStore', {
state: () => ({
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: false,
}),
})
02.方式二:TS写法
// src/stores/exampleStore.ts
// @ts-ignore
import { defineStore } from 'pinia'
interface UserInfo {
name: string
age: number
}
interface State {
userList: UserInfo[]
user: UserInfo | null
}
// 定义 store
export const useExampleStore = defineStore('exampleStore', {
state: (): State => ({
userList: [],
user: null,
}),
})
03.访问state
const store = useStore()
store.count++
默认情况下,你可以通过 store 实例访问 state,直接对其进行读写。
注意,新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。例如:如果 secondCount 没有在 state() 中定义,我们无法执行 store.secondCount = 2。
04.重置state
const store = useStore()
store.$reset()
05.变更state
方式一:
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
方式二:
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
06.替换state
你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 patch 它。
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })
---------------------------------------------------------------------------------------------------------
你也可以通过变更 pinia 实例的 state 来设置整个应用的初始 state。这常用于 SSR 中的激活过程。
pinia.state.value = {}
07.订阅state
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 // 传递给 cartStore.$patch() 的补丁对象。
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
})
---------------------------------------------------------------------------------------------------------
类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe() 方法侦听 state 及其变化。
比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次 (例如,当使用上面的函数版本时)。
2.3 Getter:类似computed
01.定义Getter
Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。
a.JS写法
推荐使用箭头函数,并且它将接收 state 作为第一个参数:
// src/stores/counterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne: (state) => state.count * 2 + 1,
},
})
b.TS写法
// src/stores/counterStore.js
// @ts-ignore
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注
return this.doubleCount + 1
},
},
})
02.访问其他getter
与计算属性一样,你也可以组合多个 getter。通过 this,你可以访问到其他任何 getter。
在这种情况下,你需要为这个 getter 指定一个返回值的类型。
a.JS写法
// 你可以在 JavaScript 中使用 JSDoc (https://jsdoc.app/tags-returns.html)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne() {
return this.doubleCount + 1
},
},
})
b.TS写法
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
})
03.向getter传递参数
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
---------------------------------------------------------------------------------------------------------
如果需要向 getter 传递参数,可以将 getter 设计为返回一个函数:
export const useUserListStore = defineStore('userList', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
---------------------------------------------------------------------------------------------------------
在组件中使用:
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
---------------------------------------------------------------------------------------------------------
请注意,当你这样做时,getter 将不再被缓存。它们只是一个被你调用的函数。
不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好
export const useUserListStore = defineStore('userList', {
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId) => activeUsers.find((user) => user.id === userId)
},
},
})
04.访问其他 store 的 getter
如果需要在一个 store 的 getter 中访问另一个 store 的 getter,可以直接调用另一个 store
---------------------------------------------------------------------------------------------------------
// src/stores/otherStore.js
import { defineStore } from 'pinia'
export const useOtherStore = defineStore('other', {
state: () => ({
data: 42,
}),
})
---------------------------------------------------------------------------------------------------------
// src/stores/mainStore.js
import { defineStore } from 'pinia'
import { useOtherStore } from './otherStore'
export const useMainStore = defineStore('main', {
state: () => ({
localData: 10,
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
05.使用 setup() 时的用法
作为 store 的一个属性,你可以直接访问任何 getter(与 state 属性完全一样):
---------------------------------------------------------------------------------------------------------
<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount // 6
</script>
06.使用选项式 API 的用法
a.创建Stores
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
},
})
b.使用 setup()
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return this.counterStore.doubleCount * 2
},
},
})
</script>
c.不使用 setup()
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',
// 你也可以写一个函数来获得对 store 的访问权
double: (store) => store.doubleCount,
}),
},
}
2.4 Action:类似methods
01.定义Action
Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。
类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。
不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action!
a.定义
// src/stores/counterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
async fetchData() {
try {
const response = await fetch('https://api.example.com/data')
const data = await response.json()
this.count = data.value
} catch (error) {
console.error('Failed to fetch data:', error)
}
},
},
})
b.使用:Action 可以像函数或者通常意义上的方法一样被调用
<!-- src/components/CounterComponent.vue -->
<template>
<div>
<p>Count: {{ store.count }}</p>
<button @click="increment">Increment</button>
<button @click="randomizeCounter">Randomize</button>
<button @click="fetchData">Fetch Data</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counterStore'
const store = useCounterStore()
// 调用 store 的 action
function increment() {
store.increment()
}
function randomizeCounter() {
store.randomizeCounter()
}
function fetchData() {
store.fetchData()
}
</script>
02.访问其他 store 的 action
如果一个 action 需要调用另一个 store 的 action,可以直接在 action 内部调用另一个 store
---------------------------------------------------------------------------------------------------------
// src/stores/authStore.js
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
isAuthenticated: false,
}),
actions: {
async login() {
// 登录逻辑
this.isAuthenticated = true
},
},
})
---------------------------------------------------------------------------------------------------------
// src/stores/settingsStore.js
import { defineStore } from 'pinia'
import { useAuthStore } from './authStore'
export const useSettingsStore = defineStore('settings', {
state: () => ({
preferences: null,
}),
actions: {
async fetchUserPreferences() {
const authStore = useAuthStore()
if (authStore.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
03.使用选项式 API 的用法
a.创建Stores
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
},
})
b.使用 setup()
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
methods: {
incrementAndPrint() {
this.counterStore.increment()
console.log('New Count:', this.counterStore.count)
},
},
})
</script>
c.不使用 setup()
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' }),
},
}
04.订阅 action
你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。
after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。
同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。
这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。
// 在某个组件或文件中
import { useCounterStore } from './counterStore'
const counterStore = useCounterStore()
const unsubscribe = counterStore.$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()
---------------------------------------------------------------------------------------------------------
在组件卸载后保留订阅
如果希望即使在组件卸载后仍保留订阅,可以将 true 作为第二个参数传递给 $onAction:
<script setup>
import { useCounterStore } from '@/stores/counterStore'
const counterStore = useCounterStore()
counterStore.$onAction((context) => {
// 处理 action 相关的逻辑
}, true) // 即使组件卸载后也保留订阅
</script>
2.5 Plugin:插件
01.为 Store 添加新的属性
你可以通过插件为每个 store 添加新的属性。以下示例中,我们为每个 store 添加一个 secret 属性:
import { createPinia } from 'pinia'
// 定义插件
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// 使用插件
pinia.use(SecretPiniaPlugin)
// 在 store 中使用新增的属性
const store = useStore()
console.log(store.secret) // 'the cake is a lie'
02.扩展 Store 的 State 和 Methods
你可以通过插件直接在 store 上添加新的 state 或 methods:
import { ref } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(({ store }) => {
// 添加新的 state
if (!store.$state.hasOwnProperty('hasError')) {
store.$state.hasError = ref(false)
}
store.hasError = toRef(store.$state, 'hasError')
// 添加新的方法
store.resetError = () => {
store.hasError = false
}
})
const useErrorStore = defineStore('error', {
state: () => ({
hasError: false,
}),
actions: {
triggerError() {
this.hasError = true
}
}
})
const store = useErrorStore()
store.triggerError()
console.log(store.hasError) // true
store.resetError()
console.log(store.hasError) // false
03.包装现有的方法
你可以包装现有的 methods 以添加额外的功能,如防抖动:
import debounce from 'lodash/debounce'
import { createPinia } from 'pinia'
const pinia = createPinia()
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
}, {})
}
})
const useSearchStore = defineStore('search', {
actions: {
searchContacts() {
// 执行搜索
}
},
debounce: {
searchContacts: 300,
}
})
const store = useSearchStore()
store.searchContacts() // 此方法会被防抖动处理
04.添加外部属性(如路由器)
将外部属性如路由器添加到 store 中:
import { markRaw } from 'vue'
import { createPinia } from 'pinia'
import { router } from './router' // 引入路由器实例
const pinia = createPinia()
pinia.use(({ store }) => {
store.router = markRaw(router)
})
const useStore = defineStore('store', {})
const store = useStore()
console.log(store.router) // 使用 router 实例
05.订阅 Store 的变更和 Actions
在插件中使用 store.$subscribe 和 store.$onAction 来监听 store 的变更:
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(({ store }) => {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
store.$onAction(({ name, after, onError }) => {
after((result) => {
console.log(`Action "${name}" resolved with ${result}`)
})
onError((error) => {
console.error(`Action "${name}" failed with ${error}`)
})
})
})
const useStore = defineStore('store', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
const store = useStore()
store.increment() // 触发监听器
06.在 Nuxt.js 中使用 Pinia 插件
在 Nuxt.js 中,你需要创建一个插件并将其注册到 Pinia:
// plugins/myPiniaPlugin.ts
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
3 pinia2
3.1 对比
01.pinia
a.说明
Pinia 起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。
从那时起,我们就倾向于同时支持 Vue 2 和 Vue 3,并且不强制要求开发者使用组合式 API,我们的初心至今没有改变。
除了安装和 SSR 两章之外,其余章节中提到的 API 均支持 Vue 2 和 Vue 3。
虽然本文档主要是面向 Vue 3 的用户,但在必要时会标注出 Vue 2 的内容,因此 Vue 2 和 Vue 3 的用户都可以阅读本文档。
b.为什么取名 Pinia?
Pinia (发音为 /piːnjʌ/,类似英文中的 “peenya”) 是最接近有效包名 piña (西班牙语中的 pineapple,即“菠萝”) 的词。
菠萝花实际上是一组各自独立的花朵,它们结合在一起,由此形成一个多重的水果。 与 Store 类似,每一个都是独立诞生的,
但最终它们都是相互联系的。 它(菠萝)也是一种原产于南美洲的美味热带水果。
02.为什么使用pinia
a.说明
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。如果你熟悉组合式 API 的话,
你可能会认为可以通过一行简单的 export const state = reactive({}) 来共享一个全局状态。
对于单页应用来说确实可以,但如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。
b.说明
而如果使用 Pinia,即使在小型单页应用中,你也可以获得如下功能:
测试工具集
插件:可通过插件扩展 Pinia 功能
为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。
支持服务端渲染
c.Devtools 支持
追踪 actions、mutations 的时间线
在组件中展示它们所用到的 Store
让调试更容易的 Time travel
d.热更新
不必重载页面即可修改 Store
开发时可保持当前的 State
03.vuex、pinia对比
a.结论
Vuex 和 Pinia 一样,天生就是用来做“跨组件”和 SPA 内的“跨页面”状态共享的。这是它们共同的核心价值。
b.Pinia
API 层面:Pinia 废除了 Mutations,并通过扁平化的 Store 设计取代了 Vuex 的 modules,代码更简洁、直观
类型安全:Pinia 提供开箱即用的完美 TypeScript 支持,解决了 Vuex 的最大痛点
开发范式:Pinia 深度拥抱 Vue 3 的组合式 API,在 <script setup> 中的开发体验远超 Vuex
轻量与高效:Pinia 的体积更小,性能也更好
c.跨组件、跨页面
在应用内部,通过路由 (Vue Router) 从一个视图切换到另一个视图(例如,从 /home 页面跳转到 /profile 页面),
应用本身并未重新加载,只是组件在不断地被挂载和卸载。
-----------------------------------------------------------------------------------------------------
核心机制:
无论是 Vuex 还是 Pinia,它们都会在你的 Vue 应用根实例上创建一个全局唯一的、响应式的 Store 实例。
全局单例:这个 Store 实例独立于任何组件之外,存活于整个应用的生命周期中。
组件通信:任何组件都可以读取这个 Store 中的状态。当一个组件修改了 Store 中的状态时,所有依赖这个状态的其他组件都会自动接收到更新。
路由切换:当你从 /home 切换到 /profile 时,Home 组件被卸载,Profile 组件被挂载。但那个全局的 Store 实例始终存在,没有被销毁。因此,Profile 组件可以无缝地访问到之前 Home 组件可能设置或修改过的状态。
d.对比 Vuex 3.x/4.x
Vuex 3.x 只适配 Vue 2,而 Vuex 4.x 是适配 Vue 3 的。
-----------------------------------------------------------------------------------------------------
Pinia API 与 Vuex(<=4) 也有很多不同,即:
mutation 已被弃用。它们经常被认为是极其冗余的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。
无需要创建自定义的复杂包装器来支持 TypeScript,一切都可标注类型,API 的设计方式是尽可能地利用 TS 类型推理。
无过多的魔法字符串注入,只需要导入函数并调用它们,然后享受自动补全的乐趣就好。
无需要动态添加 Store,它们默认都是动态的,甚至你可能都不会注意到这点。注意,你仍然可以在任何时候手动使用一个 Store 来注册它,但因为它是自动的,所以你不需要担心它。
不再有嵌套结构的模块。你仍然可以通过导入和使用另一个 Store 来隐含地嵌套 stores 空间。虽然 Pinia 从设计上提供的是一个扁平的结构,但仍然能够在 Store 之间进行交叉组合。你甚至可以让 Stores 有循环依赖关系。
不再有可命名的模块。考虑到 Store 的扁平架构,Store 的命名取决于它们的定义方式,你甚至可以说所有 Store 都应该命名。
3.2 使用:选项式
01.state:类似data
a.说明
state 属性必须是一个**函数**,它返回 Store 的初始状态对象
b.代码
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state 必须是一个返回对象的函数,以防止在服务端渲染 (SSR) 场景下多实例共享状态。
state: () => ({
count: 0,
userName: 'Guest',
}),
})
c.为什么 `state` 必须是函数?
这与 Vue 组件中 `data` 必须是函数的原因相同。
如果 `state` 是一个普通对象,那么在应用的整个生命周期中,所有 Store 实例将共享同一个状态对象,
这在 SSR 场景下会导致交叉请求状态污染。函数形式确保了每个 Store 实例都拥有自己独立的状态副本。
02.getters:类似computed
a.说明
getters 用于派生计算状态,完全等同于 Vue 组件中的 `computed` 属性。
Getter 会接收 `state` 作为其第一个参数。
Getter 之间可以通过 `this` 互相调用。
所有 Getter 都会被缓存,只有当其依赖的状态发生变化时才会重新计算。
b.代码
getters: {
// 接收 state 作为第一个参数
doubleCount: (state) => state.count * 2,
// 也可以通过 this 访问 store 实例,从而调用其他 getter
doubleCountPlusOne() {
return this.doubleCount + 1
},
// 结合多个 state
welcomeMessage: (state) => `Welcome, ${state.userName}! Your score is ${state.count}.`,
},
03.action:类似methods
a.说明
actions 相当于组件中的 `methods`,用于定义业务逻辑和修改状态。
Actions 可以是异步的 (`async/await`)。
在 Action 内部,通过 `this` 关键字可以访问整个 Store 实例,从而读取 `state`、调用其他 `getters` 或 `actions`。
修改 `state` 时,可以直接通过 `this.propertyName = ...` 进行修改。对于批量修改,建议使用 `$patch` 方法以优化性能。
b.代码
actions: {
// 同步 action
increment() {
this.count++
},
// 异步 action
async fetchAndSetUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
const userData = await response.json()
// 使用 $patch 进行批量、高效的状态更新
this.$patch({
count: userData.score,
userName: userData.name,
})
} catch (error) {
console.error('Failed to fetch user:', error)
}
},
// 调用其他 action
async incrementAndFetch(userId) {
this.increment() // 调用同一个 store 的另一个 action
await this.fetchAndSetUser(userId)
}
}
04.完整示例
a.代码
// stores/counter.options.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counterOptions', {
state: () => ({
count: 0,
userName: 'Guest',
}),
getters: {
doubleCount: (state) => state.count * 2,
welcomeMessage: (state) => `Welcome, ${state.userName}!`,
},
actions: {
increment(amount = 1) {
this.count += amount
},
reset() {
this.count = 0
},
async fetchNewCount() {
const response = await new Promise(resolve => setTimeout(() => resolve({ newCount: 100 }), 1000));
this.count = response.newCount;
}
},
})
b.优点
结构清晰: state, getters, actions 的划分非常明确,强制代码组织
易于上手: 对于有 Vuex 或 Vue Options API 背景的开发者几乎没有学习成本
心智模型简单: 概念与 Vuex 高度一致
c.缺点
类型推断略显繁琐: 虽然 Pinia 对 TypeScript 支持很好,但在 Options Store 中,this 的类型有时需要手动注解才能获得完美的类型提示。
灵活性较低: 所有逻辑必须归类到三个属性中,不利于复杂逻辑的抽离和复用。
3.3 使用:组合式
01.核心映射关系
a.说明
在 Setup Store 中,我们不再需要 `state`, `getters`, `actions` 这些固定的属性名,而是直接使用 Vue 的响应式 API 来构建。
b.state -> ref() & reactive()
a.说明
Store 的状态被定义为使用 `ref()`、`reactive()` 或 `shallowRef()` 创建的响应式变量。
b.示例
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const useUserStore = defineStore('userSetup', () => {
// ref() 用于定义原始类型或简单对象的响应式状态
const userId = ref(null)
const lastLogin = ref(new Date())
// reactive() 用于定义复杂对象的响应式状态
const profile = reactive({
name: 'Anonymous',
email: '',
isAdmin: false,
})
// ...
})
c.getters -> computed()
a.说明
派生状态(Getters)通过 Vue 的 `computed()` 函数创建。
b.示例
import { computed } from 'vue'
// computed() 创建一个只读的响应式引用,其行为与 getter 完全一致
const welcomeMessage = computed(() => `Hello, ${profile.name}!`)
const permissions = computed(() => {
return profile.isAdmin ? ['read', 'write', 'delete'] : ['read']
})
// ...
d.actions -> function()
a.说明
业务逻辑(Actions)就是简单的函数。这些函数可以直接访问和修改由 `ref` 或 `reactive` 定义的状态。
b.示例
// 普通函数作为 action
function logout() {
userId.value = null
profile.name = 'Anonymous'
profile.email = ''
profile.isAdmin = false
}
// 异步函数作为 action
async function login(email, password) {
try {
const userData = await api.login(email, password)
userId.value = userData.id
profile.name = userData.name
profile.email = userData.email
profile.isAdmin = userData.isAdmin
lastLogin.value = new Date()
return true
} catch (error) {
console.error('Login failed:', error)
logout() // 可以直接调用其他 action
return false
}
}
02.关键部分:return语句
a.说明
与组件的 setup 函数不同,Store 的 setup 函数必须返回一个对象,该对象包含了所有需要向外部暴露的状态、计算属性和方法。
b.代码
// 将所有需要暴露的 state, getters, actions 返回
return {
// State
userId,
lastLogin,
profile,
// Getters
welcomeMessage,
permissions,
// Actions
login,
logout,
}
03.高级用法:Store内的组合式能力
a.说明
Setup Store 最大的优势在于可以充分利用组合式 API 的所有能力。
b.使用watch()
a.说明
你可以在 Store 内部监听状态变化并执行副作用
b.代码
import { watch } from 'vue'
// 监听 profile.name 的变化,并将其持久化到 localStorage
watch(
() => profile.name,
(newName) => {
localStorage.setItem('userName', newName)
}
)
c.逻辑复用
a.说明
你可以将复杂的、可复用的逻辑抽离成独立的 composable 函数,并在多个 Store 或组件中使用
b.代码
// composables/useAuth.js
export function useAuth() {
// ... 复杂的认证逻辑
return { ... }
}
// stores/user.js
export const useUserStore = defineStore('user', () => {
// 在 store 内部使用 composable
const { user, login, logout } = useAuth()
// ...
return { user, login, logout }
})
04.优缺点分析
a.优点
与组合式 API 统一: 与 Vue 3 的 <script setup> 拥有完全一致的开发体验。
极致的类型推断: TypeScript 支持近乎完美,无需额外配置,自动推断所有返回值的类型。
高度灵活: 可以自由组织代码,使用 watch、provide/inject 以及外部 composable,轻松应对复杂场景。
更好的代码组织: 相关联的状态、计算属性和方法可以就近放置,而非强制分离。
b.缺点
结构约束较弱: 对于习惯了严格分层的开发者,可能会觉得过于自由。
需要理解响应式原理: 必须熟悉 ref, reactive, computed 等 Vue 核心响应式 API。
3.4 各种环境使用
01.在 Vue 组件外使用 Pinia Store
即使在组件外,你仍然可以访问和操作 Pinia store。
首先,你需要在应用的入口文件中创建和配置 Pinia 实例,然后在其他 JavaScript 文件中使用它。
a.创建和配置 Pinia 实例
在你的应用入口文件(例如 main.js 或 main.ts)中,你应该创建 Pinia 实例并将其传递给 Vue 应用
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
b.在组件外使用 Store
在组件外使用 store,你需要先导入定义的 store,然后通过 useStore 函数来访问它。以下是一个示例:
a.stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
b.services/someService.js
import { useCounterStore } from '../stores/counter'
export function performAction() {
const counterStore = useCounterStore()
counterStore.increment()
console.log('Count after increment:', counterStore.count)
}
c.总结
在 someService.js 文件中,我们直接导入并使用了 useCounterStore。这样,即使在 Vue 组件之外,我们也能操作 store。
02.在 Vue 3 的 Composition API 中使用 Store
你可以在 Vue 3 的 Composition API 中使用 Pinia store,虽然这通常发生在组件内部,但也可以在其他 JS 文件中使用:
a.stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'John Doe'
}),
actions: {
setName(newName) {
this.name = newName
}
}
})
b.utils/userUtils.js
import { useUserStore } from '../stores/user'
export function updateUserName(newName) {
const userStore = useUserStore()
userStore.setName(newName)
console.log('User name updated:', userStore.name)
}
03.在非组件环境中使用 Store(例如在 Node.js 环境中)
在某些非浏览器环境(如 Node.js)中使用 Pinia,首先确保你的环境支持 ES 模块和 Pinia。以下是一个简单的示例:
// server.js
import { createPinia } from 'pinia'
import { useCounterStore } from './stores/counter'
// 创建 Pinia 实例
const pinia = createPinia()
// 使用 store
const counterStore = useCounterStore()
counterStore.increment()
console.log('Count:', counterStore.count)
04.在 Vue 3 中的异步操作中使用 Store
在处理异步操作时,你可能需要在 Promise 或异步函数中使用 store:
// api/userApi.js
import { useUserStore } from '../stores/user'
export async function fetchUserData() {
const userStore = useUserStore()
try {
const response = await fetch('/api/user')
const data = await response.json()
userStore.setName(data.name)
console.log('User name from API:', userStore.name)
} catch (error) {
console.error('Failed to fetch user data:', error)
}
}
05.使用 Store 进行测试
在测试环境中使用 Pinia store,可以模拟 store 状态或方法:
// tests/userStore.test.js
import { createPinia } from 'pinia'
import { useUserStore } from '../stores/user'
const pinia = createPinia()
const userStore = useUserStore()
userStore.setName('Test User')
test('User store name should be Test User', () => {
expect(userStore.name).toBe('Test User')
})