pinia 新一代的vue状态管理
0.前言
本来vue全家桶系列本来打算写个vuex的教程的,但是现在有了新的pinia,咱们就来学习新的pinia
现在正好pinia还没有中文的官方文档,我就试着翻译一下
我现在开始的是日期是2021年10月29日,
我安装的pinia的版本是2.0.0
下面就正式开始学习pinia了
1.简介
pinia在2019年11月开始时候是一个实验项目,目的就是重新设计一个与组合API匹配的vue状态存储。基本原则和原来还是一样的,pinia同时支持vue2和vue3,比不要求你必须使用组合API。不管是使用vue2或者vue3,pinia的API是相同的,文档是基于vue3写的,同时在需要的地方也标注了vue2的用法,所以不管你是使用vue2还是vue3开发,都能够通过这个文档来学习。
你为什么应该使用pinia
pinia是一个vue的状态存储库,你可以使用它来存储、共享一些跨组件或者页面的数据。如果你对组合API很熟,你也许会想到你可以使用组合API来做一个简单的全局共享状态存储,像这样
export const state = reactive({})
对于spa来说确实可以这么用,如果是服务端渲染(ssr)那么这样会给你的项目带来安全风险。
但是,就算是spa,你使用pinia也可以有很多优势:
dev tools支持
有跟踪action和mutation的时间轴
按需导入状态存储
可进行时光旅行调试,而且调试更方便
热更新
不刷新页面更新你的状态
保持在开发中已有的状态
插件:使用插件来扩展pinia的功能
更好的ts支持和代码自动补全
支持ssr
基础示例
下面就是使用pinia的一个例子(一定要看后面的文档,从头学习)。这样你就创建了一个状态存储。
// stores/counter.js
import { 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的功能。你可以用上面的方式定义你的store,但是使用时用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的更多使用方法。
为什么名字是pinia
pinia(在英语中发音类似/peenya/)是和pina(西班牙语的pineapple)最近接的词(pineapple是菠萝的意思,译者注)。刚好别人没有使用过这个包名。菠萝实际上是好多独立的花聚合在一起形成了一个水果。这和状态管理类似,每一个状态都是独立的,但是最后要把它们聚合在一起。菠萝本身是一种原产自南美的很好吃的热带水果。
一个更完整的例子
下面是一个更完整的pinia的例子。对于一些人来说不用看更深入的文档,直接看这个例子就可以学会pinia了,但是我们还是推荐看完整个文档,甚至可以跳过不看这个例子,等你看完核心概念再返回来这个例子。
import { defineStore } from 'pinia'
export const todos = defineStore('todos', {
state: () => ({
/** @type {{ text: string, id: number, isFinished: boolean }[]} */
todos: [],
/** @type {'all' | 'finished' | 'unfinished'} */
filter: 'all',
// 累心会被自动推断为number类型
nextId: 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') {
// 调用其他的getter,同样会有代码自动补全 ✨
return this.finishedTodos
} else if (this.filter === 'unfinished') {
return this.unfinishedTodos
}
return this.todos
},
},
actions: {
// 传入任意参数,可以返回promise,也可以不返回
addTodo(text) {
// 可以直接更改状态
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})
与vuex对比
pinia尽量和vuex的理念保持一致。我们设计它的目的就是为下一代vuex做一个试验,pinia很成功。所以我们设计的vuex5的API和pinia很类似,现在已经有RFC了。我(Eduardo),pinia的作者,也是vue核心团队的一名成员,在Router和vuex中都有我设计的API。我做这个项目的个人目的就是为了重新设计一个符合vue理念的全局状态管理。我让pinia的API尽量接近vuex,这样以后pinia的用户转到vuex的时候更简单,甚至这两个项目以后可能会合并成为一个(Vuex)。
RFC
vuex是通过RFC在社区得到很多反馈的,pinia并不是这样。我是基于我的开发经验、阅读别人的代码、与使用pinia的人交流和在Discord上回答问题来设计pinia的。这样,我可以使pinia善于处理各种情况、适用于大小项目。我经常更新,并且在保持API不变的同时,pinia的内部代码不断地提升。
与vuex 3.x/4.x对比
vuex 3.x对应的是vue2,vuex 4.x对应的是vue3
与vue4之前的版本相比,pinia的API是有很多不同的,即:
- 去掉了mutation。因为好多人认为mutation是多余的。以前它方便devtools集成,现在这不是个问题了。
- 不用在写复杂的ts类型包装,所有的都是有类型的,API设计的都是尽量符合ts的类型推断
- 不再使用一个莫名其妙的字符串了,只需要导入一个函数,调用他们就行了,同时还有代码自动补全
- 不需要动态添加store了,因为它们现在本来就是动态。如果你想,你随时可以手动去写一个store。
- 没有复杂的嵌套模块了。你仍然可以在一个store中导入其他的store来实现嵌套模块,但是pinia还是推荐使用一个扁平的结构。但是即使你使用循环依赖也没关系。
- 不再需要命名空间了。因为现在store本来就是扁平结构了。你也可以理解为所有的store本来就有命名空间了。
开始
安装
你可以使用你喜欢的包管理工具安装pinia
yarn add pinia
# 或者使用npm
npm install pinia
提示
如果你使用的是vue2,你要安装@vue/composition-api,如果你使用的是nuxt,你需要看这里
如果你使用vue cli,你可以试一试这个非官方插件
创建一个pinia(根store),并且把它传给app:
import { createPinia } from 'pinia'
app.use(createPinia())
如果你使用的是vue2,你需要安装一个插件,并且把创建的pinia传给根app:
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
el: '#app',
// 其他选项...
// ...
// 注意,一个pinia实例可以在同一个页面的多个vue app中共用
pinia,
})
这样可以通过devtools调试了。在vue3中,有些特性,比如时间旅行调试和编辑还不支持,因为devtools还没有开发相关的API,但是devtools还有很多其他特性,开发体验会很好。在vue2中,pinia用的是vuex的现成的接口。
什么是store(状态存储)?
像pinia这样的状态存储就是要保存一些状态和一些业务逻辑,并且要与你的组件树解耦。换句话说,它要保存全局数据。就好像有一个公共数据组件,其他的组件都可以从它那里读取数据和更改数据。它有三个核心概念,state、getters和actions。就好比是组件中的data、computed和methods。
什么情况下你需要用store?
你的应用中的全局数据需要保存在store中。在很多地方你都要使用这些数据,比如说,用户信息需要在导航栏中显示,也需要在个人中心显示。还有些数据,需要暂存起来,比如一个需要分好几页填写的表单。
另一方面,一些只有在一个页面用的局部数据就不要放到全局的store中,比如一个页面上某个弹窗显示不显示,就没有必要放在store中了。
不是所有的应用都需要全局状态管理,但是如果你需要使用,pinia会是一个不错的选择。
核心概念
定义store
在开始学习核心概念之前,我们需要知道store是通过defineStore()方法定义的,它的第一个参数就是一个唯一的名字:
import { defineStore } from 'pinia'
// useStore 可以定义为其他的名字,比如 useUser, useCart
//第一个参数是store的名字,在整个app中它必须是唯一的
export const useStore = defineStore('main', {
// other options...
})
名字,可以说是id,它是必填的,pinia就是用名字来连接store和devtools的。使用useXXX名字defineStore返回的函数是一个通用习惯。
使用store
我们上面只是定义了store,在setup函数中调用了useStore()时,才会创建store:
import { useStore } from '@/stores/counter'
export default {
setup() {
const store = useStore()
return {
// 你可以返回store这个对象,然后就可以在template中使用了
store,
}
},
}
你想定义多少个store都可以,不过你最好给每一个store新建一个文件(这样在打包时可以更好的进行代码分割)。
如果你不使用setup函数,你可以使用map方式
在store实例化以后,你就可以获取到store中定义的state、getters和actions了。这些我们后面会学习,并且会有代码自动补全。
记着store是一个reactive响应式的对象,所以不用写.value。像setup中props一样,我们不可以解构它:
export default defineComponent({
setup() {
const store = useStore()
// ❌ 这样不可以,因为会失去响应性
// 和解构props是一样的`
const { name, doubleCount } = store
name // "eduardo"
doubleCount // 2
return {
// 一直是 "eduardo"
name,
// 一直是 2
doubleCount,
// 这个值是响应式的
doubleValue: computed(() => store.doubleCount),
}
},
})
为了让解构的值还保持响应式,你需要用到storeToRefs()方法。它会给响应式的数据创建ref。如果你只使用store中的stata不调用action,这么写很简单:
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
const store = useStore()
// `name` 和 `doubleCount` 是响应式的
// 插件增加的属性也会创建ref
// 但是会自动跳过action或者不是响应性的属性
const { name, doubleCount } = storeToRefs(store)
return {
name,
doubleCount
}
},
})
state
大多数时候,state是store的中心。大家一般都是从定义state开始写store的。在pinia中,是调用一个函数来返回初始的state。这样pinia既可以在客户端运行,就可以在服务端运行。
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
// 推荐使用箭头函数
state: () => {
return {
// 这些属性都会自动推断类型
counter: 0,
name: 'Eduardo',
isAdmin: true,
}
},
})
提示
如果你使用的是vue2,在初始化state的方式和vue组件中data的方式一样,比如,state对象一定是扁平的,而且如果你要给它加新的属性的话,需要调用Vue.set()
获取state
默认情况下,你可以在store实例上直接获取或者修改state:
const store = useStore()
store.counter++
重置state
你可以调用$reset()方法来把state恢复为初始值:
const store = useStore()
store.$reset()
选项API示例
如果你不使用组合API,而使用computed、methods。。。你可以使用mapState(),获取state的值:
import { mapState } from 'pinia'
export default {
computed: {
// 在组件中可以是用this.counter获取
// 和使用store.counter获取一样
...mapState(useStore, ['counter'])
// 和上面一样,不过使用了别名this.myOwnName获取
...mapState(useStore, {
myOwnName: 'counter',
// 你也可以写一个方法
double: store => store.counter * 2,
// 可以访问this指针,但是不能自动推断类型了
magicValue(store) {
return store.someGetter + this.counter + this.double
},
}),
},
}
可修改的state
如果你想修改state里面的属性(比如在你的表单中),你可以使用mapWritableState()。注意不能像使用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()时,你可以调用数组的方法。
改变state
除了直接修改store里的值store.counter++,你也可以是用$patch方法。你可以同时修改多个值:
store.$patch({
counter: store.counter + 1,
name: 'Abalam',
})
但是,这么写有时不方便,有时太消耗性能,比如说你要修改一个数组时,还得新建一个数组。出于这个原因,$patch方可可以接收一个函数作为参数,来简化改变数组的写法:
cartStore.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
这么写还有一个好处,就是这一组改变在devtools中查看时,是一次改变。注意,直接改变state和使用$patch方法,都可以在devtools里面查看,都可以实现时间旅行(在vue3中暂时不能).
替换state
你可以通过store的$state属性,整个替换state对象:
store.$state = { counter: 666, name: 'Paimon' }
你也可以使用pinia实例的state属性替换你的应用的全局state。可以在ssr中使用:
pinia.state.value = {}
订阅state的改变
你可以用$subscribe()来侦听state的改变,和vuex的subscribe
方法类似。
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia' 改变触发的类型
mutation.type // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to cartStore.$patch()
// 侦听到state变化时,把state存在localStorage中
localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,state侦听会和组件绑定在一起(如果store是在组件的setup中)。这意味着,当组件卸载时,侦听会自动被移除。如果你需要在组件被卸载时,侦听仍然保持,需要给$subscribe()方法传递第二个参数true:
export default {
setup() {
const someStore = useSomeStore()
// 组件卸载后,侦听也会有
someStore.$subscribe(callback, true)
// ...
},
}
提示
你可以在pinia实例上侦听整个state
watch(
pinia.state,
(state) => {
// 在state改变时,保存在localStorage中
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
Getters
getters就相当于的state的计算属性。可以在defineStore()方法中的getters属性中定义它们。getter的第一个参数就是state,可以是用箭头函数来定义:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
},
})
大多数情况下,getter只依赖于state的值,但是,有时也会依赖于其他getter。所以,在getter中可以用this指针访问store对象,这是要使用普通的function来定义getter,如果使用ts,记得定义返回值的类型。
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// 可以自动推断返回值类型是数字
doubleCount(state) {
return state.counter * 2
},
// 这是必须指定返回值类型(ts)
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>
在getter中访问其他getter
就想计算属性一样,getter也可以通过this指针访问其他的getter。即使你不使用ts,你也可以写JSDoc,这样你的idea就可以知道数据的类型了:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// 因为没有用this,可以正确推断类型
doubleCount: (state) => state.counter * 2,
// 也可以写JSDoc来说明类型
/**
* 返回的值是counter乘以2再加1
*
* @returns {number}
*/
doubleCountPlusOne() {
// 会有代码自动补全 ✨
return this.doubleCount + 1
},
},
})
给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)
},
},
})
获取其他store实例中的getter
你可以在getter中直接获取其他store中的getter:
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
在setup中使用
就行state一样,你可以通过store直接获取getter:
export default {
setup() {
const store = useStore()
store.counter = 3
store.doubleCount // 6
},
}
在选项API中使用
你可以像之前学习过的使用mapState()方法,获取getter:
import { mapState } from 'pinia'
export default {
computed: {
// 在这个组件中可以这样访问this.doubleCounter
...mapState(useStore, ['doubleCount'])
// 使用别名 this.myOwnName
...mapState(useStore, {
myOwnName: 'doubleCounter',
// 也可以写一个function,参数可以获取到store对象
double: store => store.doubleCount,
}),
},
}
actions
action相当于组件中的methods。可以在defineStore()方法中定义action。我们应该在业务逻辑定义在action中:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
就行getter一样,action可以使用this指针访问整个store实例,并且有类型推断和代码补全。不同的是,action支持异步,你可以在action内部去调用后台接口,甚至调用其他action。下面是一个使用Mande调用接口的例子。注意,你用哪个库没关系,只要返回的是一个Promise,你甚至可以直接使用原生的fetch:
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
data: userData,
// ...
}),
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
}
},
},
})
你可以随意的给action定义参数和返回值。调用action时,所有值都会正确地推断类型。
调用action就像调用methods一样:
export default defineComponent({
setup() {
const main = useMainStore()
// 调用action就像调用methods一样
main.randomizeCounter()
return {}
},
})
在action中访问其他的store
可以在action中直接使用其他的store:
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')
}
},
},
})
在setup中使用
你可以直接调用action,就行调用一个方法一样:
export default {
setup() {
const store = useStore()
store.randomizeCounter()
},
}
在选项API中使用
如果你不使用组合API,而使用computed、methods。。。你可以使用在methods中使用mapAction(),这样就可以调用action了:
import { mapActions } from 'pinia'
export default {
methods: {
// 可以在组件中使用this.increment() 调用
// 和使用store实例调用是相同的 store.increment()
...mapActions(useStore, ['increment'])
// 使用别名 this.myOwnName()
...mapActions(useStore, { myOwnName: 'doubleCounter' }),
},
}
订阅action
可以是用store.$onAction()方法侦听、订阅action的调用与结果。在action调用以前就会调用这个回调。你可以在after回调中改变action的返回结果。在onError回调中你可以处理错误。这样你就可以在运行时追踪错误了,跟vue中类似。
下面是一个例子,在action调用前和调用后输出了一些内容:
const unsubscribe = someStore.$onAction(
({
name, // action的名字
store, // store的实例
args, // action的参数数组
after, //action调用完成后 return或者resolved
onError, // 抛出错误或者reject
}) => {
// 在这里可以定义一些这几回调都可以访问的公有变量
const startTime = Date.now()
// 在action执行前,会执行这里
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// action调用成功会执行after
// 它会等待promise完成
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// 发送错误或者promise reject时调用
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动移除侦听
unsubscribe()
默认情况下,actions的侦听是在组件初始化是加上的(如果store是在一个组件的setup中使用)。这意味着组件卸载时,侦听会自动被移除。如果你想要组件卸载时,不移除侦听,在组件中调用$onAction时加上第二个参数true:
export default {
setup() {
const someStore = useSomeStore()
// this subscription will be kept after the component is unmounted
someStore.$onAction(callback, true)
// ...
},
}
插件
归功于pinia的low level API,pinia可以支持全面的拓展。下面是你可以扩展的:
- 给store增加新属性
- 在定义store时增加新选项
- 给store增加新方法
- 包装现有的方法
- 改变、甚至取消action
- 实现其他功能,比如本地存储
- 给特定store添加功能
使用pinia.use()给pinia实例添加插件。下面是一个最简单的例子,给所有store添加一个静态的属性,这个属性返回一个对象:
import { createPinia } from 'pinia'
// 在插件被使用后,给所有的store增加了一个secret的属性
// 这段代码可以放在一个单独的文件里
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// 使用use方法添加插件
pinia.use(SecretPiniaPlugin)
// 在另外一个文件里定义store
const store = useStore()
store.secret // 'the cake is a lie'
这个方式很有用,可以添加全局的路由、模态框或者toast。
介绍
pinia的插件就是一个方法,它返回的对象会添加在store对象上。它有一个参数context,作为选项:
export function myPiniaPlugin(context) {
context.pinia // 通过`createPinia()`创建的pinia实例
context.app // `createApp()` 创建的vueApp的实例(Vue 3 )
context.store // store的实例
context.options // 调用`defineStore()`时的选项
// ...
}
然后把这个方法传递给pinia.use():
pinia.use(myPiniaPlugin)
只有在pinia实例传给vue app时,插件才会应用在store中,其他情况它们不会起作用。
增强store
在插件里返回一个对象,然后每个store都会增加这个属性:
pinia.use(() => ({ hello: 'world' }))
你也可以在store对象上直接添加属性,但是还是尽量使用返回对象的方式,因为这样可以被devtools自动追踪:
pinia.use(({ store }) => {
store.hello = 'world'
})
插件返回的任何属性都可以被devtools自动追踪,这样在devtools中就可以看到‘hello’属性了。
如果是直接在store上定义新属性,一定要调用store._customProperties,这样才能在devtools中调试这个属性:
pinia.use(({ store }) => {
store.hello = 'world'
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('secret')
}
})
注意,store都是被reactive包装的,它会自动解包任何的ref类型(ref(), computed(), ...):
const sharedRef = ref('shared')
pinia.use(({ store }) => {
// 每个store都有独立的hello属性
store.hello = ref('secret')
// 自动解包
store.hello // 'secret'
// 所有的的store都会有shared这个属性
store.shared = sharedRef
store.shared // 'shared'
})
这就是为什么你可以不使用.value访问所有的属性,并且他们是reactive。
增加新的state
如果你要给store增加新的state,你有两种方式:
- 你可以使用store.myState的方式
- 使用store.$state的方式,可以在devtools和ssr中生效
记着,这样你可以共享一个ref或者computed属性:
const globalSecret = ref('secret')
pinia.use(({ store }) => {
// `secret` 属性在所有的store中共享
store.$state.secret = globalSecret
store.secret = globalSecret
// 自动解包
store.secret // 'secret'
const hasError = ref(false)
store.$state.hasError = hasError
// 必须这么写
store.hasError = toRef(store.$state, 'hasError')
// 这时最好不要return ‘hasError’,因为这样在devtools中会显示两次
})
警告
如果你使用的是vue2,你需要使用@vue/composition-api的set方法来添加新的属性:
import { set } from '@vue/composition-api'
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hello')) {
const secretRef = ref('secret')
// 如果你使用ssr,需要在$state上定义它
set(store.$state, 'secret', secretRef)
// 在store对象上直接设置
// 两种方式: `store.$state.secret` / `store.secret`
set(store, 'secret', secretRef)
store.secret // 'secret'
}
})
添加外部属性
当在给pinia添加外部属性时,比如其他库的实例对象,或者不是reactive的属性,你需要用markRaw()方法先包裹它们,然后再传给pinia。下面是吧router对象添加给所有的store:
import { markRaw } from 'vue'
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})
在插件内部调用$subscribe
你可以在插件里面调用store.onAction方法:
pinia.use(({ store }) => {
store.$subscribe(() => {
// store变化时调用
})
store.$onAction(() => {
// action触发时调用
})
})
添加新的选项
可以在插件中给定义store时增加新的选项。例如,你可以新增一个debounce选项,这样你可以在action调用时实现防抖:
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// 这个属性之后会在插件里用到
debounce: {
// 给searchContacts这个action加了300毫秒防抖
searchContacts: 300,
},
})
插件可以读取选项,包装action,并且替换原来那个:
// 导入一个debounce方法
import debounce from 'lodash/debunce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// 我们新的action会替换原来的
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})
注意,在使用setup语法时,自定义的选项时通过第三个参数传的:
defineStore(
'search',
() => {
// ...
},
{
// 这个值会在插件中使用
debounce: {
// 给searchContacts这个action加了300毫秒防抖
searchContacts: 300,
},
}
)
TypeScript
上面这些代码都可以使用ts来写,所以你就不需要使用any或者@ts-ignore了。
类型化插件
类型化插件可以这么写:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}
类型化新的store属性
给store添加属性时,需要使用PiniaCustomProperties接口:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
// 在setter中,允许string类型和ref类型
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()
// @ts-expect-error: 这个类型不正确
store.number = ref(Math.random())
})
PiniaCustomProperties允许你可以拿到store的属性。拿下面的例子来说,我们把原来的options复制一份,叫做$options:
pinia.use(({ options }) => ({ $options: options }))
我们可以使用PiniaCustomProperties让它们有正确的类型:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}
提示
当拓展这些基本类型时,它们必须和源码中的命名一致。Id不能命名为id或者I,S不能被命名为State。下面是这些简写对应的单词:
- S: State
- G: Getters
- A: Actions
- SS: Setup Store / Store
类型化新的state
当添加新的state属性属性时(不管是store还是store.$state),你需要在PiniaCustomStateProperties中添加。和PiniaCustomProperties不同,它只接受State类型:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}
类型化新的选项
在给defineStore()增加新的选项时,你需要使用DefineStoreOptionsBase。和PiniaCustomProperties不同,它只有两个类型:State类型和Store类型,用来让你去限制都可以定义哪些内容。例如,你可以使用action的名字:
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// 可以为任何的action定义一个数字
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}
提示
getter也有一个对应的StoreGetters。你也可以使用DefineStoreOptions、DefineSetupStoreOptions来拓展store的选项。
Nuxt.js
在Nuxt中使用pinia时,你需要使用Nuxt plugin。这样你就可以获取pinia实例了:
// plugins/myPiniaPlugin.js
import { 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
注意,上面使用的是ts,如果你使用的是js,你要把PiniaPluginContext和Plugin的类型声明去掉。
在组件之外使用store
pinia依靠的是pinia实例来共享同一个store实例。大多数情况下,你可以使用useStore()方法来获取store实例。
例如,在setup函数中,你不需要再做别的了。但是如果是在组件之外,会有一些不同。其实,useStore()会被自动注入你的app的pinia实例。这意味着,如果pinia实例不能被自动注入时,你必须手动把它传给useStore()。你可以用多种方式解决这个问题,这取决于你在做的应用是什么类型的。
单页应用
如果你不使用ssr,在你调用app.use(pinia)后,你就可以直接使用useStore()方法了。
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()
最简单的方式就是,在确保pinia实例已经有了之后,app.use(pinia)调用后,再去调用useStore()。
我们看一个使用vue router导航守卫的例子:
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
// ❌取决于pinia和router导入顺序的先后
const store = useStore()
router.beforeEach((to, from, next) => {
// 我们想在这里使用store
if (store.isLoggedIn) next()
else next('/login')
})
router.beforeEach((to) => {
// ✅ 成功,因为router是在router被使用后才喀什导航的,现在pinia肯定也被使用了
const store = useStore()
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
服务端渲染的应用
在使用srr时,你一定要把pinia实例传给useStore()。这样防止里pinia在不同的应用实例中共享同一个全局数据了。后面有完整ssr中使用pinia的例子。
ssr
Cookbook
结束语
最后两部分ssr和高级内容我就不翻译了,我估计用途也不大,而且对于ssr我也不太熟。
相信看完官方文档好多小伙伴还是很蒙的,其实官方文档并不适合入门学习
首先大部分官方文档并不是安装由易到难的顺序编写的,因为它要讲一个模块时,要把这个模块所有内容尽量讲到;其次官方文档是按模块划分内容的,为了是方便查阅,并不适合按这个顺序学习;还有就是我有的地方也不太熟悉,比如ts部分我只是学过一些内容。
所以我打算后面自己来写比较适合从零入门的教程,目的是可以由简入难的学习,还有就是结合实际工作中会遇到的场景。
这篇翻译到这里就结束了,完结撒花。