1. Pinaia


Pinia是一个全新的Vue状态管理库,是Vuex的代替者,尤雨溪强势推荐。

如果你学过Vue2,那么你一定使用过Vuex。我们都知道Vuex在Vue2中主要充当状态管理的角色,所谓状态管理,简单来说就是一个存储数据的地方,存放在Vuex中的数据在各个组件中都能访问到,它是Vue生态中重要的组成部分。

在Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的。

官网解释:

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。
从上面官网的解释不难看出,pinia和Vuex的作用是一样的,它也充当的是一个存储数据的作用,存储在pinia的数据允许我们在各个组件中使用。

Pinia优势:

  • 支持Vue2和Vue3(Vue 版本大于 2.7)

  • 抛弃传统的 Mutation ,只有 state, getteraction ,简化状态管理库

  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适

  • 不需要嵌套模块,符合 Vue3 的 Composition api,让代码扁平化

  • 支持服务的渲染

1.1 Pinia基本使用

  • 初始化一个Vue3 + TS + Vite项目
1
npm init vite@latest my-vite-app --template vue-ts
  • 运行项目
1
2
3
4
5
// 安装依赖
npm i

// 运行项目
npm run dev
  • 安装Pinia
1
npm i pinia

安装完成后我们需要将pinia挂载到Vue应用中,也就是我们需要创建一个根存储传递给应用程序,简单来说就是创建一个存储数据的数据桶,放到应用程序中去。

修改main.ts,引入pinia提供的createPinia方法,创建根存储

1
2
3
4
5
6
7
8
9
10
11
// mian.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

1.2 创建store

store简单来说就是数据仓库的意思,我们数据都放在store里面。当然你也可以把它理解为一个公共组件,只不过该公共组件只存放数据,这些数据我们其它所有的组件都能够访问且可以修改。

我们需要使用pinia提供的defineStore()方法来创建一个store,该store用来存放我们需要全局使用的数据。

首先在项目src目录下新建store文件夹,用来存放我们创建的各种store,然后在该目录下新建user.ts文件,主要用来存放与user相关的store。

1
2
3
4
5
6
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
})

创建store很简单,调用pinia中的defineStore函数即可,该函数接收两个参数:

  • name:一个字符串,必传项,该store的唯一id。
  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。
    我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的。

1.3 使用store

前面我们创建了一个store,说白了就是创建了一个方法,那么我们的目的肯定是使用它,假如我们要在App.vue里面使用它,该如何使用呢?

1
2
3
4
5
6
7
8
9
// /src/App.vue
<script setup lang="ts">
import { useUsersStore } from '../src/store/index'

const store = useUsersStore()

console.log(store);

</script>

使用store很简单,直接引入我们声明的useUsersStore 方法即可,我们可以先看一下执行该方法输出的是什么:

image-20230630083853016

1.4 添加store

我们都知道store是用来存放公共数据的,那么数据具体存在在哪里呢?前面我们利用defineStore函数创建了一个store,该函数第二个参数是一个options配置项,我们需要存放的数据就放在options对象中的state属性内。

我们往store添加一些基本数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /src/store/index.ts
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
}
})

上段代码中我们给配置项添加了state属性,该属性就是用来存储数据的,我们往state中添加了4条数据。需要注意的是,state接收的是一个箭头函数返回的值,它不能直接接收一个对象。

1.5 操作store

我们往store存储数据的目的就是为了操作它,那么我们接下来就尝试操作state中的数据。

1.5.1 读取store

在App.vue中导入store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// /src/App.vue

<script setup lang="ts">
import { ref } from 'vue'
import { useUsersStore } from '../src/store/index'

const store = useUsersStore()

const msg = ref<string>(store.msg)
const name = ref<string>(store.name)
const age = ref<number>(store.age)
const title = ref<string>(store.title)
</script>

<template>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}
</template>

<style scoped></style>

效果:

image-20230630085130164

1.5.2 解构store

当store中的多个参数需要被使用到的时候,为了更简洁的使用这些变量,我们通常采用结构的方式一次性获取所有的变量名。

1
2
3
4
5
import { useUsersStore } from '../src/store/index'

const store = useUsersStore()

const { msg, name, age, title } = store

上段代码实现的效果与一个一个获取的效果一样,不过代码简洁了很多。但是ES传统方式解构取到的值,不具有响应性

==Pinia方法解构:storeToRefs==

1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
// Pinia中使用storeToRefs方法去解构
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)

</script>

1.5.3 多组件使用store

我们使用store的最重要的目的就是为了组件之间共享数据,那么接下来我们新建一个children.vue组件,在该组件内部也使用state数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /src/children.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)

</script>

<template>
<h1>这里是children组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}
</template>
  • 在页面中展示的效果与父组件一致

1.5.4 修改store数据

如果我们想要修改store中的数据,可以直接重新赋值即可,我们在App.vue里面添加一个按钮,点击按钮修改store中的某一个数据。

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
// /src/App.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'
import children from './children.vue'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)

const onchange = () => {
store.name = "李四"
}
</script>

<template>
<h1>这里是father组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}

<div>
<button @click="onchange">点我重生一次</button>
<button @click="fnchange">点我重生再一次</button>
</div>
<children />
</template>
  • 在子组件也可以添加这样一个按钮,点击就会改变页面的数据。

1.5.5 批量修改store数据

通过基础数据修改方式去修改多条数据也是可行的,但是在 pinia 官网中,已经明确表示$patch 的方式是经过优化的,会加快修改速度,对性能有很大好处,所以在进行多条数据修改的时候,更推荐使用 $patch

$patch 方法可以接受两个类型的参数,函数 和 对象

  • $patch + 对象
  • $patch + 函数: 通过函数方式去使用的时候,函数接受一个 state 的参数,state 就是 store 仓库中的 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
33
34
35
36
37
38
39
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'
import children from './children.vue'

const store = useUsersStore()
// 对象式
const { msg, name, age, title } = storeToRefs(store)
// 使用store.$patch方法
const onchange = () => {
store.$patch({
name: '王五',
age: 18,
title: '钻石王老五'
})
}
// 函数式
const fnchange = () => {
store.$patch((state) => {
state.age = 10
state.name = '张三' ? '王五' : '张三'
})
}
</script>

<template>
<h1>这里是father组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}

<div>
<button @click="onchange">点我重生有一次</button>
</div>
<children />
</template>
  • 提供action修改:
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
41
42
43
44
45
46
47
48
// /src/App.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'
import children from './children.vue'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)
// 对象式
const onchange = () => {
store.$patch({
name: '王五',
age: 18,
title: '钻石王老五'
})
}
// 函数式
const fnchange = () => {
store.$patch((state) => {
state.age = 10
state.name = '张三' ? '王五' : '张三'
})
}

// action 修改多个
const acchange = () => {
store.changeState()
}

</script>

<template>
<h1>这里是father组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}

<div>
<button @click="onchange">点我重生一次</button>
<button @click="fnchange">点我重生再一次</button>
<button @click="acchange">点我重生再再一次</button>
</div>
<children />
</template>
  • /src/store/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
},
getters: {},
actions: {
changeState() {
this.name = '王二狗',
this.age = 35,
this.title = '隔壁偷狗的'
}
}
})
  • 直接替换整个state:

    • pinia提供了方法让我们直接替换整个state对象,使用store的$state方法。
    1
    store.$state = { age: 45, name: '李四' }

1.5.6 重置state

有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。

此时,我们直接调用store的$reset()方法即可,继续使用我们的例子,添加一个重置按钮。

1
2
3
4
5
<button @click="reset">重置store</button>
// 重置store
const reset = () => {
store.$reset();
};

当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新。

1.6 getters属性

getters是defineStore参数配置项里面的另一个属性,前面我们讲了state属性。getter属性值是一个对象,该对象里面是各种各样的方法。大家可以把getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样。

当然我们这里的getter就是处理state数据。

1.6.1 添加getter

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
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
},
getters: {
getAddAge: (state) => {
return state.age + 10
}
},
actions: {
changeState() {
this.name = '王二狗',
this.age = 35,
this.title = '隔壁偷狗的'
}
}
})

上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个changeState方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据。

1.6.2 使用getter

在App.vue中

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
41
42
43
44
45
46
47
48
49
50
51
52
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'
import children from './children.vue'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)
// 对象式
const onchange = () => {
store.$patch({
name: '王五',
age: 18,
title: '钻石王老五'
})
}
// 函数式
const fnchange = () => {
store.$patch((state) => {
state.age = 10
state.name = '张三' ? '王五' : '张三'
})
}

// action 修改多个
const acchange = () => {
store.changeState()
}

// getter使用
const gtchange = () => {

}

</script>

<template>
<h1>这里是father组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}
新年龄:{{ store.getAddAge }}
<div>
<button @click="onchange">点我重生一次</button>
<button @click="fnchange">点我重生再一次</button>
<button @click="acchange">点我重生再再一次</button>
</div>
<children />
</template>

上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式。

当我们点击批量修改数据按钮时,页面上的新年龄字段也会跟着变化。

1.6.3 getter中调用其它getter

前面我们的getAddAge方法只是简单的使用了state方法,但是有时候我们需要在这一个getter方法中调用其它getter方法,这个时候如何调用呢?

其实很简单,我们可以直接在getter方法中调用this,this指向的便是store实例,所以理所当然的能够调用到其它getter。

1
2
3
4
5
6
7
8
9
10
// /src/store/index.ts

getters: {
getAddAge: (state) => {
return state.age + 100;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
}
},

上段代码中我们又定义了一个名为getNameAndAge的getter函数,在函数内部直接使用了this来获取state数据以及调用其它getter函数。

细心的小伙伴可能会发现我们这里没有使用箭头函数的形式,这是因为我们在函数内部使用了this,箭头函数的this指向问题相信大家都知道吧!所以这里我们没有采用箭头函数的形式。

那么在组件中调用的形式没什么变化,代码如下:

1
<p>调用其它getter:{{ store.getNameAndAge }}</p>

1.6.4 getter传参

既然getter函数做了一些计算或者处理,那么我们很可能会需要传递参数给getter函数,但是我们前面说getter函数就相当于store的计算属性,和vue的计算属性差不多,那么我们都知道Vue中计算属性是不能直接传递参数的,所以我们这里的getter函数如果要接受参数的话,也是需要做处理的。

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
// /src/store/index.ts
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
changeState() {
this.name = '王二狗',
this.age = 35,
this.title = '隔壁偷狗的'
}
}
})

上段代码中我们getter函数getAddAge接收了一个参数num,这种写法其实有点闭包的概念在里面了,相当于我们整体返回了一个新的函数,并且将state传入了新的函数。

接下来我们在组件中使用,方式很简单,代码如下:

1
2
3
// /src/App.vue

<p>新年龄:{{ store.getAddAge(1100) }}</p>

1.7 actions属性

前面我们提到的state和getters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。

那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。

actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法。

1.7.1 添加actions

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
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
changeState() {
this.name = '王二狗',
this.age = 35,
this.title = '隔壁偷狗的'
}
}
})

上段代码中我们定义了一个非常简单的actions方法,在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store。

1.7.2 使用actions

使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法。

1
2
3
const acchange = () => {
store.changeState()
}

1.8 store之间的相互调用

在 Pinia 中,可以在一个 storeimport 另外一个 store ,然后通过调用引入 store 方法的形式,获取引入 store 的状态

1.8.1 新建store

1
2
3
4
5
6
7
8
9
10
11
12
// /src/store/allan.ts
import { defineStore } from "pinia";

export const allanStore = defineStore('allan', {
state: () => {
return {
moveList: ['你的名字', '天气之子', '铃芽之旅']
}
},
getters: {},
actions: {}
})

1.8.2 原store引入新建store

  • getters里面定义一个方法,获取moveList
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
import { defineStore } from 'pinia'
import { allanStore } from './allan';

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
// 另一个Store引用,获取allanStore里面的moveList
getAllanStore(): string[] {
return allanStore().moveList
}
},
actions: {
changeState() {
this.name = '王二狗',
this.age = 35,
this.title = '隔壁偷狗的'
}
}
})
  • 在组件中使用
1
2
3
4
5
// /src/App.vue

<ul>
<li v-for="item in store.getAllanStore">{{ item }}</li>
</ul>

1.9 使用到的全部代码

  • /scr/store/index.ts
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
import { defineStore } from 'pinia'
import { allanStore } from './allan';

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
state: () => {
return {
msg: 'Hello world',
name: '张三',
age: 30,
title: '法外狂徒'
}
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
// 另一个Store引用,获取allanStore里面的moveList
getAllanStore(): string[] {
return allanStore().moveList
}
},
actions: {
changeState() {
this.name = '王二狗',
this.age = 35,
this.title = '隔壁偷狗的'
}
}
})
  • scr/store/allan.ts
1
2
3
4
5
6
7
8
9
10
11
import { defineStore } from "pinia";

export const allanStore = defineStore('allan', {
state: () => {
return {
moveList: ['你的名字', '天气之子', '铃芽之旅']
}
},
getters: {},
actions: {}
})
  • /scr/App.vue
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
41
42
43
44
45
46
47
48
49
50
51
52
53
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'
import children from './children.vue'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)
// 对象式
const onchange = () => {
store.$patch({
name: '王五',
age: 18,
title: '钻石王老五'
})
}
// 函数式
const fnchange = () => {
store.$patch((state) => {
state.age = 10
state.name = '张三' ? '王五' : '张三'
})
}

// action 修改多个
const acchange = () => {
store.changeState()
}


</script>

<template>
<h1>这里是father组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}
<p>新年龄:{{ store.getAddAge(1100) }}</p>
<ul>
<li v-for="item in store.getAllanStore">{{ item }}</li>
</ul>
<div>
<button @click="onchange">点我重生一次</button>
<button @click="fnchange">点我重生再一次</button>
<button @click="acchange">点我重生再再一次</button>
</div>
<children />
</template>

<style scoped></style>
  • /scr/children.vue
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
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUsersStore } from '../src/store/index'

const store = useUsersStore()

const { msg, name, age, title } = storeToRefs(store)

const onchange = () => {
store.name = "王二麻子"
}
</script>

<template>
<h1>这里是children组件</h1>
<div>
{{ msg }}
</div>
姓名:{{ name }}
年龄:{{ age }}
称号:{{ title }}

<div>
<button @click="onchange">点击改子名</button>
</div>
</template>

<style scoped></style>

1.10 Pinia总结

pinia的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌。其实我们更应该关注的是它的函数思想,大家有没有发现我们在Vue3中的所有东西似乎都可以用一个函数来表示,pinia也是延续了这种思想。

所以,大家理解这种组合式编程的思想更重要,pinia无非就是以下3个大点:

  • state
  • getters
  • actions
    当然,本篇文章只是讲解了基础使用部分,但是在实际工作中也能满足大部分需求了,如果还有兴趣学习pinia的其它特点,比如插件、订阅等等,可以移步官网:pinia官网