pinia 基本概念
Pinia 是 Vue 的存储库,Pinia和Vuex一样都是是vue的全局状态管理器,它允许跨组件/页面共享状态。实际上,其实Pinia就是Vuex5,官网也说过,为了尊重原作者,所以取名 pinia,而没有取名 Vuex,所以大家可以直接将 pinia 比作为 Vue3 的 Vuex。
为什么用 pinia
- pinia中只有state、getter、action,抛弃了Vuex中的Mutation,Vuex中mutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。
- pinia中action支持同步和异步,Vuex不支持
- 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了
- 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。
- 体积非常小,只有1KB左右。
- pinia支持插件来扩展自身功能。
- 支持服务端渲染。
Pinia的函数
createPinia()
创建一个被应用所使用的 Pinia 实例。返回:Pinia
defineStore(id, options):
StoreDefinition。创建一个检索 store 实例的 useStore 函数。
- id:相当于为容器起一个名字。注意:这里的名字必须唯一,不能重复
- options:可以简单理解为一个配置对象,里边是对容器仓库的配置说明。当然这种说明是以对象的形式。
storeToRefs
用 store 的所有state、getters和 插件添加的(plugin-added)state、 属性(properties)创建一个引用对象。类似于toRefs(),但专门为 Pinia store 设计
- 参数:用以提取 refs 得store
辅助函数
mapStores()
通过生成要在组件的 computed 字段中展开的对象,允许使用没有组合式API (setup())的存储。它接受 store 定义的列表。
- 参数:要隐射到对象的存储列表
export default {computed: {// 其它计算属性...mapStores(useUserStore, useCartStore)},created() {this.userStore // id为"user"的存储this.cartStore // id为"cart"的存储}
}
mapState()
将 state 属性映射为只读的计算属性,虽然它可以通过这个访问组件实例,但它不会识别类型。
- useStore: 第一个参数,要隐射的存储
- keyMapper:第二个参数,状态 state 属性或访问器 getter 得对象
import { mapState } from 'pinia'
import { useCounterStore } from './store/counter.js'const counterComputed = computed(()=>{// 可以访问组件中的 this.count// 与从 store.count 中读取的数据相同...mapState(useCounterStore, ['count'])// 与上述相同,但将其注册为 this.myOwnName...mapState(useCounterStore, {myOwnName: 'count',double: store => store.count * 2,magicVlue(store) {return store.soneGetter + this.count + this.double}})
})
mapWritableState()
可修改 state 中的属性,但是不能像 mapState() 哪样传递一个函数。
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'export default {computed: {// 可以访问组件中的 this.count,并允许设置它。// this.count++// 与从 store.count 中读取的数据相同...mapWritableState(useCounterStore, ['count'])// 与上述相同,但将其注册为 this.myOwnName...mapWritableState(useCounterStore, {myOwnName: 'count',}),},
}
mapActions(useStore, keyMapper)
通过生成要在组件的 methods 字段中铺开的对象,允许直接使用存储(store)中的操作(action),而不使用合成API (setup())。对象的值是操作(action),而键是结果方法的名称。
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' }),},
}
安装
yarn add pinia
# 或者使用 npm
npm install pinia
在mian.js中,创建根存储
import { createPinia } from 'pinia'app.use(createPinia())
pinia 基础特性
state
- 默认情况下,通过 store 实例访问 state,可以直接读取和写入,如 @click='store.count++'
- 通过 store.$reset() 方法可以将 state 重置为初始值
- 除了直接通过 store 修改 state,还可以通过 store.$patch() 方法提交多个更改
- 可以通过 store.$subscribe() 订阅 state 得变化,在 patches 修改之后订阅只会触发一次,默认情况下,订阅绑定到添加他的组件,当组件卸载时,他们将自动删除,也可以配置将其保留。
getters
- Getters 属性的值是一个函数,接受 state 作为第一个参数,目的是鼓励使用箭头函数
- 非箭头函数会绑定 this,建议仅在需要获取整个 store 实例的场景使用,且需要显式定义函数返回类型
actions
- 与 Gettes 一样可以通过 this 访问整个 store 实例
- Actions 可以是异步的或同步的,不管怎样,都会返回一个 Promise
- Actions 可以自由的设置参数和返回的内容,一切将自动推断,不需要定义 TS 类型
- 与 State 一样,可以通过 store.$onAction() 订阅 Actions,回调将在执行前触发,并可以通过参数 after() 和 onError() 允许在Action 决议后和拒绝后执行函数。同样的,订阅绑定的是当前组件。
store
store实例相当于一个容器,里面存放的有类似于data,计算属性,方法之类的东西。通过defineStore()方法定义
store
在src下面创建一个store文件夹,再创建与之对应的js文件,比如user.js
defineStore:
- 第一个参数是你的应用中 Store 的唯一 ID(必传),Pinia 将用它来连接 store 和 devtools。
- 第二个参数可接受两类值:Setup 函数或 Option 对象。
option
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象
- state:是 store 中的 data
- getters:是 store 得计算属性 computed
- actions:是 store 的方法 methods
setup
option :
import { defineStore } from 'pinia'// useStore 可以是 useUser、useCart 之类的任何东西
// 第一个参数是应用程序中 store 的唯一 id
export const useUser = defineStore('user', {// other options...
})
setup 函数:
与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
- ref():对应 store 中的 state
- computed:对应 store 中的 getters
- function:对应 store 中的 actions
Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。
export const useCounterStore = defineStore('count',()=>{const count = ref(0)function increment(){count.value++}return { count, increment }
})
注:store 是一个用 reactive 包装的对象,这意味着不需要再 getters 后面下 .value ,就像 setup 中的 props 一样,不允许解构。
<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
使用 store
<script lang="ts" setup>
import { useUser } from "../stroe/user";const stroe = useUser();
console.log(stroe, "stroe");
</script>
添加 state
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
// 第二个参数是配置对象
export const useUser = defineStore('user', {// state是一个函数,返回一个对象state: () => {return {name: "anna",age: 16,};}
})
为了从 store 中提取属性时保持其响应性,需要使用 storeToRefs()。它将为每一个响应式属性创建引用,当只使用 store 得状态而不调用任何 action 时,非常有用。可以直接从 store 中解构 action ,因为他们也被绑定到 store 中。
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// name 和 age 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式(不是 ref 或 reactive )得属性
const { name, age } = storeToRefs(store)
// 作为 action 得 increment 可以直接解构
const { increment } = store
state
state 是 store 得核心。state 被定义为一个返回初始状态的函数,声明 state 建议使用箭头函数,会自动判断属性的类型。
typescript 写法:
const useStore = defineStore('user',{state: ()=>({userList: [] as UserInfo[],user: null as UserInfo | null})
})interface UserInfo{name: string,age: number
}
定义节后 state,并添加 state() 得返回值的类型
interface State{userList: UserInfo[],user: UserInfo | null
}interface UserInfo{name: string,age: number
}const useStore = defineStore('user',{state: State => {return {userList: [],user: null}}
})
读取 state
可以简单理解为一个配置对象,里边是对容器仓库的配置说明。当然这种说明是以对象的形式。默认情况下,可以直接访问 store 得实例 state属性,并对齐进行读写。
import { storeToRefs } from 'pinia'
const userStore = useUser()
// 如果直接解构出数据,这个数据不是响应式的。如果想要变成响应式的,需要调用storeToRefs方法
const { name, age } = storeToRefs(userStore)
修改 state
userStore.name = 'bob'
批量修改 state
除了 store.xxx 直接修改 store,还可以使用 $patch 方法,允许用一个 state 得补丁对象在同一时间更改多个属性。
// 可以用来修改单个属性
userStore.$patch({name: 'lily'
})// 这种回调函数的形式适合修改集合类的数据,比如数组
userStore.$patch((state) => {state.age = 18
})
两种变更 store 方法的主要区别是,$patch() 允许你将多个变更归入 devtools 的同一个条目中。同时请注意,直接修改 state,$patch() 也会出现在 devtools 中,而且可以进行 time travel (在 Vue 3 中还没有)。
事实上开发过程中为了满足工程扁平结构化规范,一般通过Action结合实现
重置 state
// 重置
userStore.$reset()
setup store
export const userCounterStore = defineStore('counter',()=>{const count = ref(0)function $reset(){count.value++}return { count, $reset }
})
直接替换整个 state (几乎不用)
// 替换
userStore.$state = { userName: 'tom', age: 11
}
订阅
类似于 vuex 得 subscribe,可以通过 store 得 $subscribe() 方法侦听 state 及其变化,比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次。
cartStore.$subscribe((mutation, state) => {// import { MutationTypr } from 'pinia'mutation.type // 'dorect' | 'patch object' | 'patch function'// 和 carStore.$id 一样mutation.storeId // 'cart'// 只有 mutation.type === 'patch onject'得情况下才可以用mutation.payload // 传递给 cartStore.$patch() 得补丁对象// 每当状态发生变化时,将整个 state 持久化到本地存储localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离:
const someStore = useSomeStore()
// 此订阅器几遍在组件卸载之后扔会被保留
someStore.$subscribe(callback, { detached: true })
setup store:
watch(pinia.state, state => {loaclStorage.setItem('piniaState',JSON.stringify(state))
})
getter
类似计算属性。用来监视或者说是计算状态的变化的,有缓存的功能。
推荐里面传递一个形参的箭头函数写法,不容易出错。如果使用非箭头函数,可以在函数中使用 this,但是在 getters 中使用形参 state 访问属性 typescript 无法识别。
- 为了避免使用 this,官方建议使用箭头函数
- 建议仅当需要获取整个 store 时使用 this,但必须显示的定义函数返回类型,typescript 不会自动判断
- 建议使用 this 得时候不要声明 state 形参
getters: {isAdult: (state) => {return state.age >= 18 ? "成年人" : "未成年";},isAdult1(){return this.age >= 18 ? "成年人" : "未成年";}
},
获取
// 直接获取
<div>{{userStore.isAdult}}</div>
调用本模块其他 getter
getters: {isAdult: (state) => {return state.age >= 18 ? '成年人' : '未成年'},msg: (state) => {// msg这个getter访问了自身的getter(isAdult)return state.userName + state.isAdult},msg1(){return this.userName + this.isAdult}}
getters 传参
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
getters: {isAdmin: (state) => {// 如果getter里面是返回的函数,那么它就可以传参数了return (name) => name === 'admin' ? '是管理员' : '不是管理员'}}
调用
// 直接获取
<div>{{userStore.isAdmin}}</div>
调用其他 store 里面的 getter
商品模块 good.js
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
// 第二个参数是配置对象
export const useGoods = defineStore('goods', {// state是一个函数,返回一个对象state: () => {return {goodsName: 'iphone'}},getters: {newIphone: (state) => {return state.goodsName + ' 14 pro'}}
})
想在 user 模块中的 getters 中去获取 goods 模块的 newIphone
import { useGoods } from './goods.js'
// ..........
getters: {info: (state) => {// 获取goods模块的storeconst goodsStore = useGoods()return state.userName + '买了' + goodsStore.newIphone}
}
actions
类似于 getters,actions 也可以通过 this 访问整个 store 实例。action 是可以异步的,可以在他们里面 await 调用任何 api,以及其他 action。注意action中需要定义普通函数,这样才有自己的this能取到state属性
基本使用
actions: {// 这里的方法要写成普通函数,因为里面需要通过this去访问state里面的数据changeNameAsync (newName) {setTimeout(() => {// actions里面可以访问statethis.userName = newName}, 1000)},async getList(params) {try {this.list = await api.post(params)} catch (err) {console.log('err:' + err.msg)}}}
Action 可以像函数或者通常意义上的方法一样被调用:
<templete><button @click='store.randomizeCounter()'/>
</templete><script setup>const store = useCounterStore()store.randomizeCounter()
</script>
访问其他 store 中的 action
import { defineStore } from 'pinia'
import { useAuthStore } from './auth-store'export const useSettingStore = defineStore('setting', {state: () => ({preference: null}),actions:{async fetchUserPreferences() {const auth = useAuthStore()if (auth.isAuthenticated) {this.preference = await fetchPreferences()} else {throw new Error('User must be authenticated')}}}
})
订阅 action
你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。
const unsubscribe = someStore.$onAction(({name, // action 名称store, // store 实例,类似 `someStore`args, // 传递给 action 的参数数组after, // 在 action 返回或解决后的钩子onError, // action 抛出或拒绝的钩子}) => {// 为这个特定的 action 调用提供一个共享变量const startTime = Date.now()// 这将在执行 "store "的 action 之前触发。console.log(`Start "${name}" with params [${args.join(', ')}].`)// 这将在 action 成功并完全运行后触发。// 它等待着任何返回的 promiseafter((result) => {console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`)})// 如果 action 抛出或返回一个拒绝的 promise,这将触发onError((error) => {console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`)})}
)// 手动删除监听器
unsubscribe()
模块化
在实际开发中,不可能把多个模块的数据都定义到一个store中,而是一个模块对应一个store,最后通过一个根store进行整合
建立两个 store
// 模块一
import { defineStore } from 'pinia'const useUserStore = defineStore('user', {state: () => {return {name: 'haha',age: 18,}},
})export default useUserStore// 模块二
import { defineStore } from 'pinia'const useCounterStore = defineStore('user', {state: () => {return {count: 1}},
})export default useUserStore
新建 store/index.js
import useUserStore from './user'
import useCounterStore from './counter'// 统一导出useStore方法
export default function useStore() {return {user: useUserStore(),counter: useCounterStore(),}
}
组件中使用
<script setup>
import { storeToRefs } from 'pinia'
import useStore from './store'
const { counter } = useStore()// 使用storeToRefs可以保证解构出来的数据也是响应式的
const { count } = storeToRefs(counter)
</script>
pinia 和 vuex
Pinia同样是一个Vue的状态管理工具,在Vuex的基础上提出了一些改进。与vuex相比,Pinia 最大的特点是:简便。
区别
- pinia 它没有 mutation,他只有 state, getters,action【同步、异步】使用他来修改state数据
- pinia 他默认也是存入内存中,如果需要使用本地存储,在配置上比vuex麻烦一点
- pinia 语法上比 vuex 更容易理解和使用,灵活
- pinia 得模块化设计,是通过构建多个存储模块,可以让程序自动拆分它们,pinia 没有 modules 配置,没一个独立的仓库都是 definStore 生成出来的
- pinia state 是一个对象返回一个对象和组件的 data 是一样的语法
- pinia 类型安全,与 TypeScript 一起使用时具有可靠的类型推断支持
- pinia 不再有 modules 的嵌套结构,没有命名空间模块
- Pinia 支持扩展,可以非常方便地通过本地存储,事物等进行扩展
- pinia 支持服务器端渲染
pinia 的优点
- 完整的 TypeScript 支持:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
- 极其轻巧(体积约 1KB)
- store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见
- 支持多个Store
- 支持 Vue devtools、SSR 和 webpack 代码拆分
pinia 的缺点
- 不支持时间旅行和编辑等调试功能
vuex 的优点
- 支持调试功能,如时间旅行和编辑
- 适用于大型、高复杂度的Vue.js项目
vuex 的缺点
- 从 Vue 3 开始,getter 的结果不会像计算属性那样缓存
- Vuex 4 有一些与类型安全相关的问题
何时使用Pinia,何时使用Vuex
由于Pinea是轻量级的,体积很小,它适合于中小型应用。它也适用于低复杂度的Vue.js项目,因为一些调试功能,如时间旅行和编辑仍然不被支持。
将 Vuex 用于中小型 Vue.js 项目是过度的,因为它重量级的,对性能降低有很大影响。因此,Vuex 适用于大规模、高复杂度的 Vue.js 项目。
pinia和vuex在vue2和vue3都可以使用,一般来说vue2使用vuex,vue3使用pinia。