新的一年了,还在忍受Vuex4糟糕的类型体验?🙅,我们要把 Vuex 换成 Pinia 了💪;Pinia ,Vue.js 官方开发,下一代的 Vuex,简化的概念,更好的 TypeScript 支持。快来一起体验吧~~
简介
Pinia 是一个 Vue 的存储库, Pinia最初是一个实验,它的作者是Vue.js核心团队的成员,本来是为了测试Vuex5提案而出现的,在2019年11月左右重新设计的Vue状态管理以适用于 Composition API,它不但支持Vue3也支持Vue2的options API,也是下一代的轻量级状态管理库。
我们知道在不同版本的Vue中,所需要使用的状态管理库Vuex版本也是不一样的,不然就会有错误发生。
vue2.x 我们使用--》 vuex 3.x版本
vue3.x 我们使用--》vuex 4.x版本
然鹅鹅🤷♀️,我们的Pinia就不会有这个限制啦~~ 它不但支持Vue3也支持Vue2,且不一定要与 Composition API 一起使用,API 的使用方式在两者中也是保持一致的。★,°:.☆( ̄▽ ̄)/$: .°★ 。🌹🌹🌹
核心概念
- State: 用于存放数据,有点儿类似
data
的概念; - Getters: 用于获取数据,有点儿类似
computed
的概念; - Actions: 用于修改数据,有点儿类似
methods
的概念; - Plugins: Pinia 插件。
❓为啥说Pinia是下一代的轻量级状态管理库
我们先来看看官方对官方对 Vuex5 的 Rfcs 提案如下图:github.com/vuejs/rfcs/…
而这些与不pinia的功能类似么?而且在这个提案下方的评论中 也可以看到
为啥用Pinia
在前边也介绍了Pinia
是vue的官方核心开发成员开发的,天然对vue是支持的,设计也非常接近vuex5的提案,甚至vuex5
的部分灵感都是来自于pinia,那究竟它到底有哪些优势呢?类比vuex 4.x
好在哪呢?
那么接下来我们来看看vuex 4.x的缺点:
- 1、改变一个
state
的值,如果是同步更新需要mutations
,异步的时候需要在actions
- 2、给Vuex的state添加typescript,需要自定义复杂的类型来支持ts
- 3、把vue想中的state分成多个部分,就需要用到 module
- 4、从vue3开始。
getters
的结果不会像计算属性那样缓存了 - 5、vuex4.X有一些与类型安全相关的问题
没有命名空间的模块,或者说所有的store都是命名空间
优势
- 完整的
TypeScript
支持;天生具备完美的类型推断 - 支持两种语法创建 Store:
Options Api
和Composition Api
; - 类比
vuex 4.x
,Pinia
删除mutations
;统一在 actions 中操作 state,通过this.xx 访问相应状态虽然可以直接操作 Store,但还是推荐在 actions 中操作,保证状态不被意外改变 - Store中的Actions 配置项可以执行同步或异步方法,且 action 被调用的是为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数
- 可以构建多个store,打包管理会自动拆分
- 模块化的设计,便于拆分状态,能很好支持代码分割;
- 没有嵌套的模块,只有 Store 的概念,可以声明多个Store,Pinia在设计上提供了一个扁平的结构,仍然能够在存储空间之间进行交叉组合,
- 无需动态添加stores,默认都是动态的;
- 没有命名空间的模块
namespaced模块
,或者说所有的store都是命名空间 - Pinia 可以自由扩展 插件功能官方文档 Plugins
- 极轻, 仅有 1 KB
- 支持Vue DevTools, SSR和Webpack 代码拆分
- 无论是在 Vue2 中,还是在 Vue3 中均可以使用 Pinia,且不一定要与 Composition API 一起使用,API 的使用方式在两者中也是保持一致的。
Pinia与Vuex代码分割机制
Vuex的代码分割: 打包时,vuex会把3个store合并打包,当首页用到Vuex时,这个包会引入到首页一起打包,最后输出1个js chunk。这样的问题是,其实首页只需要其中1个store,但其他2个无关的store也被打包进来,造成资源浪费。
Pinia的代码分割: 打包时,Pinia会检查引用依赖,当首页用到main store,打包只会把用到的store和页面合并输出1个js chunk,其他2个store不耦合在其中。Pinia能做到这点,是因为它的设计就是store分离的,解决了项目的耦合问题。
使用pinia会让你真香的!!!!
快速上手
安装
我们可以在已有的vue项目中直接安装pinia使用状态管理,与已经安装的vuex不会有依赖冲突,在这我们就新建一个项目来一步一步的看看吧,这边举例子的是vue3的项目,这样可以更好的结合ts来看看吧~
使用vite创建一个vue-ts的项目,(因为pinia是专门用于vue的所以要选用vue,最后用于ts因为更香,只用js不能更加体会pinia的好用呀)
关于vite创建前端框架项目的详解可以查看之前的文章:前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(一)
第一步:使用vite创建一个vue-ts的项目
npm init @vitejs/app <project-name> --template
第二步:安装状态管理依赖pinia
yarn add pinia@next
# or with npm
npm install pinia@next
// 该版本与Vue 3兼容,如果你正在寻找与Vue 2.x兼容的版本,请查看v1分支。
初始化配置
在入口文件main.ts中 创建一个 pinia(根存储)并将其传递给应用程序:
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
// 创建Pinia实例
const pinia = createPinia()
// 实例化 Vue
createApp(App)
.use(pinia) // 挂载到Vue根实例上
.mount('#app') // 挂载在真实 DOM
在上面的代码中,你将Pinia添加到Vue.js项目中,我们可以看到里边可以结构一个方法 createPinia
在挂载到Vue根实例上, 这样你就可以在你的代码中使用Pinia的全局对象。
使用
按照以往惯例我们会在项目src下,创建一个store来专门存放状态管理,,然后在用到该数据的地方进行导入,我们使用pinia也是如此。
基本使用及解析
步骤:
- 1.定义容器
- 2.使用容器中的state
- 3.修改state
- 4.容器中的action的state
1.定义容器
在文件中声明store
创建src/store/index.ts 用来存放状态管理
第一种模式Store
类似 vuex 的方式来构建 state
import { defineStore } from "pinia";
// 参数1:容器的ID,必须唯一 将来pinia会把所有的容易挂载到根容器
// 参数2:选项对象
// 返回值是一个函数 调用得到 对外部暴露一个 use 方法,该方法会导出我们定义的 state
export const useMainStore = defineStore("main", {
/**
* 类似于组件的data 用来存储全局状态
*/
state: () => {
return {
countPinia: 100
};
},
// 也可以这样定义
// state: () => ({ countPinia: 100 }),
getters: {
double () {
// getter 中的 this 指向👉 state
return this.countPinia * 2
},
// 如果使用箭头函数会导致 this 指向有问题
// 可以在函数的第一个参数中拿到 state
double2: (state) => {
return state.count * 2
}
},
// actions 用来修改 state
actions: {
increment() {
// action 中的 this 指向👉 state
this.countPinia++
},
});
也可以写成一下方式
export const useMainStore = defineStore({
id: 'main',
state: () => {
return {
countPinia: 100
}
},
...
})
第二种方式模式
使用
function
的形式来创建 store,有点类似于 Vue3 中的setup
import { ref, computed } from "vue"
import { defineStore } from "pinia"
// 对外部暴露一个 use 方法,该方法会导出我们定义的 state
const useMainStore = defineStore('main', function () {
const countPinia = ref(0)
const double = computed(() => countPinia.value * 2)
function increment() {
countPinia.value++
}
return {
countPinia, double, increment
}
})
export default useMainStore
👨🎓解析代码:
Pinia通过defineStore函数来创建一个store,它接收一个id用来标识store,以及store选项
为了创建一个store,你用一个包含创建一个基本store所需的states、actions和getters的对象来调用 defineStore
方法。
这个方法返回一个函数, 对外部暴露这个方法,该方法会导出我们定义的 state;
defineStore
方法 有两个参数
- 参数1:容器的ID,必须唯一 将来pinia会把所有的容易挂载到根容器
- 参数2:选项对象
2.使用容器中的state
上边pinia优势中的时候说过,Pinia 提供了两种方式来使用 store,
Options Api
和Composition Api
中都完美支持Vue2 与 Vue3 最大的区别: Vue2 使用
Options API
而 Vue3 使用的Composition API
使用options API模式定义,这种方式和vue2的组件模型形式类似,也是对vue2技术栈开发者较为友好的编程模式。
还有就是 Composition Api,我们这边使用setup模式定义,符合Vue3 setup的编程模式,让结构更加扁平化,个人推荐推荐使用这种方式。
Options Api方式使用
在
Options Api
中,可直接使用官方提供的mapActions
和mapState
方法,导出 store 中的 state、getter、action,其用法与 Vuex 基本一致,很容易上手。
import { mapActions, mapState } from 'pinia'
import { useMainStore } from '@/store'
export default {
name: 'HelloWorld',
computed: {
...mapState(useMainStore, ['countPinia', 'double'])
},
methods: {
...mapActions(useMainStore, ['increment'])
}
Composition Api模式使用
<script setup lang="ts">
import { useMainStore } from './store'
const useMain = useMainStore()
const { countPinia} = useMain
const clickEdit = ()=>{
useMain.increment()
}
</script>
<template>
<h2>{{ useMain }}</h2>
<h2>{{ useMain.countPinia }}</h2>
<p>{{useMain.double}}</p>
<button @click="clickEdit">修改数据</button>
</template>
也可以结合 computed 获取。
const countPiniaComputed = computed(() => useMainStore.countPinia)
👨🎓解析代码:
- 1、
const { countPinia} = useMain
这种方式的解构,是有一些问题的,数据不能够实现响应式,那需要怎么做呢?这时候可以用 pinia 的 storeToRefs。如下
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useMainStore } from './store'
const useMain = useMainStore()
const { countPinia } = storeToRefs(useMain)
</script>
<template>
<h2>{{ useMain }}</h2>
<h2>{{ useMain.countPinia }}</h2>
<p>{{countPinia}}</p>
</template>
useMain
是一个用reactive
包装的对象,这意味着没有必要在getters
之后写入.value
,但是如果想结构这种方式就是破坏了响应性类似从props
中一样。所以我们要使用pinia中的storeToRefs
来包裹,就可以啦~~
因为pinia其实就是把state数据都做了 reactive 处理了~~
ps:这个时候其实我们可以体会到因为pinia的支持ts,所以会有很好的提示,可以快速的开发,相当优秀;如果我们想要这些提示,那么需要改写vuex 4.x 方法,就需要手动声明类型
- 2、Pinia在setup模式下的调用机制是先install再调用。
- 3、可以定义任意多的
stores
,在真正的useMainStore()
执行之前,store并不会被创建,一旦存储被实例化,您就可以直接在存储上访在“state”、“getters”和“actions”中定义的任何属性。 - 4、如果我们使用 Vue 2的项目中使用pinia,在
state
中创建的数据遵循与 data 在 Vue 实例中 相同的规则,即 状态对象必须是普通的,并且您需要在向其添加新属性Vue.set()时调用。
3.修改state
Composition Api
中,不管是 state 还是 getter 都需要通过computed
方法来监听变化,这和Options Api
中,需要放到computed
对象中的道理一样。
Options Api
中拿到的 state 值是可以直接进行修改操作的,当然还是建议写一个 action 来操作 state 值,方便后期维护。
方式一:最简单的方式:
Options Api
中拿到的 state 值是可以直接进行修改操作的,
// 组件中
useMain.countPinia++
方式二: $path 批量更新1
如果需要修改多个数据,建议使用 path是有性能的优化
// 组件中
useMain.$patch({
countPinia: useMain.countPinia+1
})
方式三:$patch 使用一个函数
也是批量更新 (好处,相比较方法二,在处理一些复杂的数据时,如数组、对象等更简单)
// 组件中
useMain.$patch(state=>{
state.countPinia++
})
方式四:封装到action
逻辑比较多的时候,可以封装到action(最常用的) actions 去修改 state,action 里可以直接通过 this 访问。
// 组件中
useMain.changeState()
src/store/index.ts 中在action添加方法
//src/store/index.ts
import { defineStore } from "pinia";
// 参数1:容器的ID,必须唯一 将来pinia会把所有的容易挂载到根容器
// 参数2:选项对象
// 返回值是一个函数 调用得到 对外部暴露一个 use 方法,该方法会导出我们定义的 state
export const useMainStore = defineStore("main", {
/**
* 类似于组件的data 用来存储全局状态
*/
state: () => {
return {
countPinia: 100
};
},
/**
* 类似组件的computed 用于封装计算属性,有缓存的功能
*/
getters: {
},
/**
* 类似于组件的methods 封装有误逻辑 修改 state
*/
actions: {
changeState() {
// this.countPinia++
this.$patch(state => {
state.countPinia++;
});
}
}
});
👨🎓解析代码注意:
不能使用箭头函数定义action 因为使用箭头函数 函数内部的this指向就是外部
良好的编程习惯:state的改变交给action去处理
核心概念详解
State
在 Pinia 中,
State状态
被定义为返回初始状态的函数。类似于组件的data 用来存储全局状态
options api中定义的时候
1.必须是函数:这样是为了在服务器渲染的时候避免交叉请求导致的数据状态污染
2.推荐用箭头函数,这是为了更好的 TS 类型推导
import { defineStore } from 'pinia'
const useMainStore = defineStore('main', {
// 推荐用于全类型推理的箭头函数
state: () => {
return {
// 所有这些属性都将自动推断出它们的类型
counter: 0,
name: 'lee'
}
},
})
技巧:
如果您使用 Vue 2,您在
state
中创建的数据遵循与 data 在 Vue 实例中 相同的规则,即 状态对象必须是普通的,并且您需要在向其添加新属性Vue.set()时调用。另请参阅: Vue#data。
Composition Api模式定义
使用setup模式定义
import { ref } from 'vue';
import { defineStore } from 'pinia';
// 使用setup模式定义
export const useCounterStoreForSetup = defineStore('counter', () => {
const count = ref<number>(1);
return { count};
});
1、访问state
引入创建的store容器,调用后通过实例访问状态来直接读取和写入状态
<script setup lang="ts">
import { useMainStore,useCounterStoreForSetup } from './store'
const useMain = useMainStore()
const useCounterForSetup = useCounterStoreForSetup()
</script>
<template>
<h2>{{ useMain.countPinia++ }}</h2>
<hr>
<p>{{useCounterForSetup.count++}}</p>
</template>
2、重置state
可以通过调用store 上的方法将状态重置为其初始值
$reset()
:
<script setup lang="ts">
import { useMainStore,useCounterStoreForSetup } from './store'
const useMain = useMainStore()
const useCounterForSetup = useCounterStoreForSetup()
useMain.$reset()
</script>
3、修改
具体可看上边的 使用中的修改state
- 直接修改
- 通过调用
$patch
方法允许您对部分state
对象批量更改 - 或者在action方法中修改
- 可以通过将
store
的$state
属性设置为新对象来替换store
的整个状态;useMain.$state = { countPinia: 666 }
- 可以通过更改替换您的应用程序的整体状态
state
中的pinia
实例.pinia.state.value = {}
4、订阅状态
可以通过
$subscribe()
store的方法观察状态及其变化,类似于 Vuex 的 subscribe 方法。
$subscribe()
与常规相比使用的优点watch()
是订阅只会在patch
后触发一次(例如,使用上面的函数版本时)。
useMain.$subscribe((mutation, state)=>{
localStorage.setItem('useMain-state', JSON.stringify(state))
})
👨🎓解析代码注意:
当useMain中的state改变时,本地缓存中就会被存储这些useMain的整个state
默认情况下,状态订阅绑定到添加它们的组件(如果存储在组件的内部setup()
)。意思是,当组件被卸载时,它们将被自动删除。如果你想保持他们后成分是卸载,通过{ detached: true }
作为第二个参数,以分离的状态订阅从当前组件:
useMain.$subscribe((mutation, state)=>{
localStorage.setItem('useMain-state', JSON.stringify(state))
}, { detached: true })
ps: 可以查看pinia
实例上的整个状态:
watch(
pinia.state,
(state) => {
// 每当状态改变时,将整个状态保存到本地存储中
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
Getters
类似组件的computed 用于封装计算属性,有缓存的功能,getters的第一个参数是state。
1、声明一个getters 中的方法:
/**
* 类似组件的computed 用于封装计算属性,有缓存的功能
*/
getters: {
/**
*
* @param state 函数接受一个可选参数:state 状态对象
* 不使用state也可以使用 this
* 如果使用了this 这必须手动制定函数的返回值的类型 否则类型推导不出来
* @returns
*/
getCount(state){
return state.countPinia +10
},
//返回值类型 **必须** 被明确地指定
getCount2(): number {
// 为整个存储(store)自动完成和类型注释
return this.countPinia +10
},
},
👨🎓解析代码注意:
getters中的函数接受一个可选参数:state 状态对象
- 不使用state也可以使用 this
- 如果使用了this 这必须手动制定函数的返回值的类型 否则类型推导不出来
2、在组件中使用与state相同
<template>
<!-- state 中 -->
<p>{{useMain.countPinia}}</p>
<!-- getters 中 -->
<p>{{useMain.getCount}}</p>
</template>
** 3、传递参数:利用高阶函数传递参数**
/**
* 类似组件的computed 用于封装计算属性,有缓存的功能
*/
getters: {
/**
*
* @param state 函数接受一个可选参数:state 状态对象
* 不使用state也可以使用 this
* 如果使用了this 这必须手动制定函数的返回值的类型 否则类型推导不出来
* @returns
*/
getCount(state){
return state.countPinia +10
},
getCount2(state):Number{
return (num)=> {
return state.countPinia + num
}
}
},
组件中使用
<p>{{useMain.getCount2(3)}}</p>
4、访问其他store中的getters
例如我们声明了两个store
export const useMainStore = defineStore({
id: "main",
state: ()=>({
return {countMain: 2}
}),
getters: {
getCountMain(state){
return state.countMain + 2
}
}
})
export const useStore = defineStore({
id: "user",
state: ()=>({
return { countUser: 2}
}),
getters: {
addMain(state){
const store = useMainStore()
return state.countUser + store.countMain
}
}
})
5、在vue3组件中使用Composition Api
<script setup lang="ts">
import { storeToRefs,mapState } from 'pinia'
import { useMainStore } from '@/store'
const store = useStore()
const { countPinia,getCountMain } = storeToRefs(store)
</script>
<template>
<p>{{countPinia}}</p>
<p>{getCountMain}}</p>
</template>
6、vue2中 options API 的用法
import { mapState } from 'pinia'
import { useMainStore } from '@/store'
export default {
computed: {
// 允许访问组件内部的 this.doubleCounter
// 与从 store.doubleCounter 中读取一样
//...mapState(useMainStore, ['getCountMain']),
// same as above but registers it as this.myOwnName
...mapState(useMainStore, {
myOwnName: 'doubleCounter',
// 你也可以写一个函数以访问 store
double: store => store.doubleCount,
}),
},
}
Action
用于修改数据,有点儿类似
methods
的概念;它们可以使用defineStore()的
actions
属性进行定义,异步和同步方法都可以使用,项目中的业务逻辑中方法可以用在这(这里与 Vuex 有极大的不同,Pinia 仅提供了一种方法来定义如何更改状态的规则,放弃 mutations 只依靠 Actions)
import { defineStore } from "pinia";
// 1.定义容器
// 参数1:容器的ID,必须唯一 将来pinia会把所有的容易挂载到根容器
// 参数2:选项对象
// 返回值是一个函数 调用得到
export const useMainStore = defineStore("main", {
/**
* 类似于组件的data 用来存储全局状态
* 1.必须是函数:这样是为了在服务器渲染的时候避免交叉请求导致的数据状态污染
* 2. 必须还是箭头函数,这是为了更好的 TS 类型推导
*/
state: () => {
return {
countPinia: 100,
title: "pinia",
arr: [1, 2],
};
},
/**
* 类似于组件的methods 封装有误逻辑 修改 state
*/
actions: {
// 注意:不能使用箭头函数定义action 因为使用箭头函数 函数内部的this指向就是外部
changeState() {
// this.countPinia++
// this.title = "在action中修改title"
// this.arr.push(3)
this.$patch(state => {
state.countPinia++;
state.title = "修改";
state.arr.push(3);
});
},
changeState2(num:number){
this.countPinia += num;
}
},
});
👨🎓解析代码注意:
actions中的方法,可以通过 this
访问到整个存储实例,所以这个时候就不能使用箭头函数,因为箭头函数内部的this指向就是外部。
使用在Composition Api模式中使用
<script setup lang="ts">
import { storeToRefs,mapState } from 'pinia'
import { useMainStore } from '@/store'
const store = useStore()
const { countPinia } = storeToRefs(store)
const clickEdit =()=>{
store.changeState()
}
</script>
<template>
<p>{{countPinia}}</p>
<button @click="clickEdit">修改数据</button>
</template>
options API 时的用法
import { mapActions } from 'pinia'
import { useMainStore } from '@/store'
export default {
methods: {
// 组件内部允许访问 this.changeState()
// 就像从 store.changeState() 调用一样
...mapActions(useMainStore, ['changeState']),
// 与上面一样但是注册其为 this.myOwnName()
...mapActions(useMainStore, { myOwnName: 'changeState' }),
},
}
调用其他的action
export const useAppStore = defineStore({
id: 'app',
actions: {
setData(data) {
console.log(data)
return data
}
}
})
export const useMainStore = defineStore({
id: 'main',
actions: {
setData(data) {
const appStore = useAppStore()
return appStore.setData(data)
}
}
})
订阅actions
$onAction() 详情可以:pinia.vuejs.org/core-concep…
插件
Pinia 存储(store)可以通过 低级API 完全扩展。以下是你可以执行的 action:
- 向
存储
(store) 添加新属性; - 当定义
存储
时,添加新选项; - 向
存储
添加新方法; - 包装现有方法;
- 更改甚至取消
动作
(action); - 执行如本地存储等副效果;
- 仅适用于特定
存储
。
详情可以看:pinia.vuejs.org/core-concep…
持久化存储
插件 pinia-plugin-persist 可以辅助实现数据持久化功能。
安装
npm i pinia-plugin-persist --save
使用
第一步:在src/store/index.ts
import { createPinia } from 'pinia'
import type { App } from "vue";
import piniaPluginPersist from 'pinia-plugin-persist'
export function setupStore(app: App<Element>) {
const pinia = createPinia();
pinia.use(piniaPluginPersist);
app.use(pinia);
}
第二步:修改在入口main.ts的初始化配置pinia
import { createApp } from 'vue'
import App from './App.vue'
import { setupStore } from "@/store";
const app = createApp(App)
setupStore(app)
app.mount('#app')
第三步:创建相应的store
在src/store/app.ts
import { defineStore } from "pinia";
export const useAppStore = defineStore("app",{
state: () => {
return {
name: "app store state: name",
};
},
actions: {
changeName() {
this.$patch((state: { name: string; }) => {
state.name = "修改";
});
},
},
// 开启数据缓存
persist: {
enabled: true,
},
});
第四步:使用
<script setup lang="ts">
import {useAppStore} from '@/store/app'
const appStore = useAppStore();
const clickEdit = ()=>{
appStore.changeName()
}
</script>
<template>
<button @click="clickEdit">修改数据</button>
<hr>
<div>{{appStore.name}}</div>
</template>
数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 key。
问:如果不想使用store中的id当做存储的可以了怎么办呢?
你可以自定义这个key,persist中有一个strategies 里自定义 key 值,并将存放位置由 sessionStorage 改为 localStorage。
persist: {
enabled: true,
strategies: [
{
key: 'my_store_app',
storage: localStorage,
}
]
}
问:如果不想存储左右的状态state,只存储部分呢?
//src/store/app.ts
import { defineStore } from "pinia";
export const useAppStore = defineStore("app",{
state: () => {
return {
name: "app store state: name",
title:'标题',
sub:'pinia',
content:'本文是针对pinia的知识的分享。。。'
};
},
actions: {
changeName() {
this.$patch((state: { name: string; }) => {
state.name = "修改";
});
},
},
// 开启数据缓存
persist: {
enabled: true,
strategies: [
{
key: 'my_store_app',
storage: localStorage,
paths: ['name', 'sub']
}
]
},
});
感谢
到此,本篇文章先告一段落,如果有什么错误或不足,欢迎评论区指正!感谢您的阅读~~