Vue3带来了一些令人兴奋的新特性,其中之一是Pinia,一个专为Vue3设计的状态管理库。在本文中,我们将深入了解Pinia的使用,包括安装、创建 store、使用状态、以及一些高级功能。在Vue2中,若要使用状态管理器,几乎是必须集成Vuex。首先要明确一点,Vuex在Vue3项目中仍然能够正常使用,是Vue项目中的主力军。然而,我们今天要探讨的是小巧而强大的Pinia。Pinia目前已成为Vue官方正式支持的状态库,不仅适用于Vue2,也同样适用于Vue3。你可以简单地将Pinia视为 Vuex5,是Vue3项目的首选。当然,许多公司或项目在从Vue2转向Vue3时,由于开发习惯了Vuex,因此在Vue3项目中继续使用Vuex的情况也并不罕见。选择使用Pinia还是Vuex取决于实际开发项目情况。

Pinia的概念(小菠萝)

Pinia是一个为Vue提供状态管理的库。它专注于提供简洁、强大、可扩展的状态管理解决方案。Pinia是Vue3官方支持的状态管理库,同时也兼容Vue2。Pinia的设计目标是在开发大型应用时提供更好的性能和开发体验。它采用了一些现代化的状态管理理念,并通过一些独特的特性使得开发者更容易组织和维护项目的状态管理。

Pinia中文官网地址:https://pinia.web3doc.top/

Pinia的优点

  • pinia符合直觉,易于学习。
  • pinia是轻量级状态管理工具,大小只有1KB.
  • pinia模块化设计,方便拆分。
  • pinia没有mutations,直接在actions中操作state,通过this.xxx访问响应的状态,尽管可以直接操作state,但是还是推荐在actions中操作,保证状态不被意外的改变。
  • storeaction被调度为常规的函数调用,而不是使用dispatch方法或者是MapAction辅助函数,这是在Vuex中很常见的。
  • 支持多个store
  • 支持Vue devtoolsSSRwebpack代码拆分。

Pinia的安装

在Vue3项目中,首先需要安装Pinia。使用以下命令:

1
npm install pinia

或者使用 yarn:

1
yarn add pinia

Pinia的使用

安装完Pinia后,接下来就是使用了。在使用的初始步骤中,我们需要在项目中引Pinia

Pinia导入

首先在main.js文件中引入

vue3写法:

1
import { createPinia } from 'pinia'

vue2写法:

1
import { PiniaVuePlugin } from 'pinia'

我们将继续使用Vue3来详细介绍Pinia的使用方法。在导入Pinia时,我们需要使用Vue的hook,并确保调用它。一旦调用完成,状态将以插件的形式存在于项目中,最后的步骤是在项目中使用这些状态。经过上述步骤的编写,我们成功地在Vue3项目中导入和配置了Pinia,为后续的状态管理操作做好了准备。

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const state = createPinia()
const app = createApp(App)
app.use(state)
app.mount('#app')

Pinia基本用法

创建index.js文件

首先,在根目录下创建一个 store 文件夹,这个结构对于熟悉Vuex的开发者来说应该很熟悉,毕竟Pinia的设计就是为了替代Vuex。创建完 store 文件夹后,在其中新建一个 JavaScriptTypeScript文件,命名为 index.jsindex.ts,为了方便大家直接理解,我这里创建的index.js文件。

编写index.js文件

defineStore有三个参数:第一个是state,第二个是getters,第三个是actions

  • state和之前我们vuex里面的写法是不一样的,在vuex里面呢,state是一个对象,但是在pinia中,state必须是一个箭头函数,然后在返回一个对象。
  • getters模块呢,类似于计算属性,是有缓存的,主要是帮助我们修饰一些值。
  • actions呢,类似于 methods,帮助我们做一些同步的或者是异步的操作,提交state之类的。

index.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineStore } from "pinia"
import { Names } from "./store_name"

export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是Jack',
age: 10,
}
},

getters: {

},

actions: {

}

})

store_name.js文件:

1
2
3
export const enum Names {
TEST = "TEST"
}

到这个地方为止,其实我们已经可以使用pinia了,我们写一个页面使用一下pinia的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
</script>
<style scoped>

</style>

修改Pinia的值

修改Pinia的值有多种方式,我们逐一介绍。实际上,修改Pinia中的值就是修改其状态(state)的值。在上个案例中,我们创建了一个状态(state),其中包含了两个值,一个是 name,另一个是 age。今天我们的任务是实现修改Pinia中状态的值。有许多常见的修改值的方式,我们将逐一进行讲解,一共有五种。

方式一:直接修改

这种方式简单粗暴,直接修改即可,在vuex里面是坚决不允许这样子直接操作state数据的,但是小菠萝是可以操作。比如,上个案例,我们来实现点击按钮的时候实现age1操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="changeAge">年龄+1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

const changeAge = () => {
userInfo.age++
}

</script>

方式二:$patch 函数修改

在我们实例化const userInfo = useInfoStore()这个state的时候,其实这个userInfo中,有一个方法,就是patch函数,它可以帮助我们批量修改。比如点击按钮,同时修改nameage的值,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';

// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

// 方式二: 通过 patch 函数批量修改 name 和 age
const change = () => {
userInfo.$patch({
age: 11,
name: 'jack'
})
}

</script>

方式三:$patch 函数修改

和方式二相同都是使用patch函数来实现修改,但是有区别,方式二是在patch函数中传入修改的对象值,但是这种方式传入的是一个函数,其作用可以进行逻辑操作,比如说判断之类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

// 方式三
const change = () => {
userInfo.$patch((state) => { // 这里传入的state就是pinia的state
state.age = 11
state.name = 'jack'
})
}

</script>

方式四:$state 方式

这种方式不是很常用,尽管它也可以修改state的值,但是这种方式有一个很大的弊端,它是直接替换掉state。比如上面的state里面有两个值,一个是name,一个是age,如果我们只想修改age的值,那么我们必须把agename的值都写上才可以,如果只写age ,那么name的值就没有了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

// 方式四:state
const change = () => {
userInfo.$state = {
name: 'jack',
age: 11
}
}

</script>

方式五: action 方式

这个方式我们需要借助actions来实现,所以说我们需要去store文件夹下的index.js文件中写一个action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineStore } from "pinia";
import { Names } from "./store_name";

export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是jack',
age: 10,
}
},

getters: {

},

actions: {
setAge () { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = 11
}
}

})

写完action就可以使用action的方式修改age的值了。只需要使用实例去调用刚才写的action函数就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改 age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

// 方式五
const change = () => {
userInfo.setAge()
}

</script>

action是可以传参的,修改一下index.js文件编写的action函数,让它接受一个参数再赋值。

1
2
3
4
5
 actions: {
setAge(num: number) { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = num
}
}

页面代码修改:

1
2
3
4
// 方式五
const change = () => {
userInfo.setAge(12)
}

pinia的解构

上面的案例,实例化const userInfo = useInfoStore()了以后呢,这个userInfo是可以继续解构操作。但是有个问题,就是解构后的数据,是不具备响应式的,即我们修改了state的值,页面数据不会跟着变化,因为解构后的不是响应式数据。官方提供了一个方法,可以把解构后的数据转换为响应式的数据。就是 storeToRefs,使用这个方法把我们解构的对象包裹一下就可以了,这个方法其实就是toRefs类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia中的数据: {{name}} --- {{age}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

// const { name, age } = userInfo // 对userInfo进行了解构,但是不具备响应式
const { name, age } = storeToRefs(userInfo) // 包裹一下结构对象,具备了响应式

</script>

Pinia 的 actions

actions 具备处理同步和异步操作的能力。同步操作相对较为简单,上面我们已经通过一个同步的 action 修改了 state

actions 异步

先模拟一个异步函数,比如常见的登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const User = {
name: '',
age: 0
}

const Login = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'admin',
age: 22
})
}, 2000)
})
}

然后我们在actions中就可以调用这个异步的操作了,在 actions 处理异步的时候呢,一般是与 asyncawait 连用。

index.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { defineStore } from "pinia"
import { Names } from "./store_name"

const User = {
name: '',
age: 0
}

const Login = ()=> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'admin',
age: 22
})
}, 2000)
})
}

export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是jack',
age: 10,
}
},

getters: {

},

actions: {
async setUser() {
const result = await Login()
this.name = result.name
this.age = result.age
}
}

})

login.vue页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>

<el-button @click="change">修改用户内容</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

let change = () => {
userInfo.setUser()
}

</script>

getter 函数

getters 类似于 vue 里面的计算属性,可以对已有的数据进行修饰,在pinia中有两种写法。

普通函数方式写法

1
2
3
4
5
6
getters: {
newName() {
return `这是getter修饰过的名称 ${this.name}`
}
}

相互调用

getter 也是可以像 actions 一样相互调用

1
2
3
4
5
6
7
8
9
 getters: {
newName() {
return `这是getter修饰过的名称 ${this.name} ,他的年纪是 ${this.getAge}`
},

getAge() {
return this.age
}
}

API 的使用

pinia 里有很多的 API,下面将介绍几种常见的API

$reset :重置到初始值

这个 $reset 可以将 state 的数据初始到初始值,比如有一个数据,点击按钮改变了,然后可以通过这个 API ,将数据恢复到初始状态值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>

<el-button @click="change">修改用户内容</el-button>

<p>getter: {{userInfo.newName}}</p>

<el-button @click="reset">$reset</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

let change = () => {
userInfo.setUser()
}

// 重置
let reset = () => {
userInfo.$reset()
}

</script>

$subscribe:监听 state 数据变化

$subscribe使用来监听的,监听 state 数据的变化,只要 state 里面的数据发生了变化,就会自动走这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<h3>pinia的使用详解</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia中的数据: {{userInfo.name}} --- {{userInfo.age}}</p>

<el-button @click="change">修改用户内容</el-button>

<p>getter: {{userInfo.newName}}</p>

<el-button @click="reset">$reset</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()

let change = () => {
userInfo.setUser()
}

// 重置
let reset = () => {
userInfo.$reset()
}

// 监听 state 的变化,返回一个工厂函数
userInfo.$subscribe((args, state) => {
console.log(args, state)
})

</script>

$onAction:一调用 actions 就触发

action一调用就会被触发,它里面有一个参数 args,这个argsactions 传进来的参数。

1
2
3
4
5
6
7
// $onAction
userInfo.$onAction((args) => {
args.after(() => {
console.log('after 回调')
})
console.log(args)
})

上面的案例只传了一个参数,就是一个工厂函数,其实他还有第二个参数true,传true的意思是当这个组件销毁了,这个onAction还可以继续保活。

1
2
3
4
5
6
7
// $onAction
userInfo.$onAction((args) => {
args.after(() => {
console.log('after 回调')
})
console.log(args)
}, true)

不止 $onAction 可以传第二个参数,$subscribe 也有第二个参数,只不过subscribe的参数是一个对象,对象里面设置的是detachedtrue效果和onAction 是一样的,当然还有其它的参数,和 watch 是类似的。

1
2
3
4
5
6
7
userInfo.$subscribe((args, state) => {
console.log(args, state)
}, {
detached: true,
deep: true,
flush: 'post'
})

总结

总体来说,Pinia 是一个为 Vue 开发者提供了更灵活、更强大的状态管理工具,尤其在大型应用的状态组织和维护方面具有优势。另外Pinia还有一些关键特点:

  1. Vue 3 官方支持: Pinia 是 Vue.js 团队官方支持的状态管理库,因此能够充分发挥与 Vue 3 的集成优势。

  2. 适用于 Vue 2 和 Vue 3: Pinia 不仅兼容 Vue 3,还可以在 Vue 2 项目中使用,这对于逐步迁移项目或在不同版本间共享代码的项目非常有用。

  3. Typescript 支持: Pinia 提供了完整的 TypeScript 支持,使得开发者在使用 TypeScript 编写代码时能够获得更好的类型提示和开发体验。

  4. 零依赖: Pinia 设计为零依赖,不依赖 Vuex 或其他状态管理库。这使得 Pinia 在体积和性能上都更加轻量。

  5. 响应式: Pinia 使用 Vue 3 的响应式系统,确保状态的变化能够正确地反映在视图上。

  6. 插件体系: Pinia 提供了插件体系,可以轻松扩展其功能。这使得开发者可以根据具体需求引入额外的功能或定制化行为。