Pinia学习

Pinia学习

前言

虽然已经在项目中使用过Pinia,现在决定好好系统学习一下

pinia是什么?

主要充当状态管理的角色,所谓状态管理,简单来说就是一个存储数据的地方,存放在Pinia中的数据在各个组件中都能访问到,它是Vue生态中重要的组成部分。实际上,pinia就是Vuex的升级版。

官网解释:

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。

Pinia的优点

  • Vue2和Vue3都支持,这就意味着同时使用Vue2和Vue3的小伙伴都能很快上手。
  • pinia中只有state、getter、action,抛弃了Vuex中的Mutation,Vuex中mutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。
  • pinia中action支持同步和异步,Vuex不支持
  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了
  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。
  • 体积非常小,只有1KB左右。
  • pinia支持插件来扩展自身功能。
  • 支持服务端渲染。

准备工作

首先新创建一个最新的Vue3 + TS + Vite项目

1
npm create vite@latest my-vite-app --template vue-ts

运行项目

1
2
npm install
npm run dev

Pinia基础使用

安装并且引入Pinia

安装

1
2
3
yarn add pinia
# 或者使用 npm
npm install pinia

安装完成后我们需要将pinia挂载到Vue应用中,在main.ts中引入Pinia,创建根存储

1
2
3
4
5
6
7
8
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const pinia = createPinia()

const app = createApp(App)
app.use(pinia)
app.mount('#app')

创建store

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

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

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

1
2
3
4
5
6
7
8
// /src/store/user.ts

import { defineStore } from 'pinia'

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

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

  • name:一个字符串,必传项,该store的唯一id。
  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。

我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的。

使用store

以在APP.vue中使用store为例

1
2
3
4
5
6
7
/src/App.vue

<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>

添加state

我们需要存放的数据就放在options对象中的state属性内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /src/store/user.ts

import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
state: () => {
return {
name: "zhangsan",
age: 18,
sex: "男"
}
}
})

需要注意的是,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
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>

<template>
<div>
<p>{{ name }}</p>
<p>{{ age }}</p>
<p>{{ sex }}</p>
</div>
</template>

<style scoped>

</style>

为了简洁代码,可以是使用解构的方式来获取值

1
2
3
4
5
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = store;
</script>

多个组件使用state

新建一个子组件child.vue,用同样的方式读取state,并展示到页面上。

可以发现,child组件和APP.vue组件几乎一样,就是很简单的使用了store中的数据。

修改state数据

如果我们想要修改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
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const {name, age,sex} = store
const changeName = () => {
store.name = "lisi"
console.log(store);
}
</script>

<template>
<div>
<p>{{ name }}</p>
<p>{{ age }}</p>
<p>{{ sex }}</p>
</div>
<button @click="changeName">点击我改变名字</button>
</template>

<style scoped>

</style>

点击按钮后我们通过控制台可以发现name确实被修改了,但是页面上却没有变化,这就说明name值不是响应式的。

我们需要使用pinia提供的storeToRefs函数,将state中的数据变为响应式的

1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useUsersStore} from "../src/store/user";
const store = useUsersStore();
const {name, age,sex} = storeToRefs(store)
const changeName = () => {
store.name = "lisi"
console.log(store);
}
</script>

这时我们再点击按钮,发现页面的值发生了改变。

重置state

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

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

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

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

批量修改state数据

如果我们一次性需要修改很多条数据的话,可以使用store的$patch方法

1
2
3
4
5
6
7
8
9
<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "李四",
age: 20,
sex: "女",
});
};

但是我们可以发现,我们采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举。

为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。

1
2
3
4
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})

或者也可以使用传入的state对象直接修改

1
2
3
4
store.$patch(state => {
state.name = "李四"
state.age = 20
})

使用上面的方法我们即批量更改了state的数据,又没有将所有的state字段列举出来。

直接替换整个state

pinia提供了方法让我们直接替换整个state对象,使用store的$state方法。

1
store.$state = { counter: 666, name: '张三' }

感觉这种场景使用较少

getter属性

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

添加getter

修改user.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
state: () => {
return {
name: "zhangsan",
age: 18,
sex: "男"
}
},
getters: {
getAddAge: (state) => {
return state.age + 10
}
}
})

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

使用getter

在APP.vue中使用

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<p>{{ name }}</p>
<p>{{ age }}</p>
<p>{{ sex }}</p>
<p>新年龄:{{ store.getAddAge }}</p>
</div>
<button @click="changeName">点击我改变名字</button>
<button @click="reset">点我重置</button>
<button @click="patchStore">批量修改数据</button>
</template>

我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式

getter中调用其它getter

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

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 {
name: "zhangsan",
age: 18,
sex: "男"
}
},
getters: {
getAddAge: (state) => {
return state.age + 10
},
getNameAndAge(): string {
return this.name + this.getAddAge //调用其它getter
}
}
})

因为箭头函数的this指向问题,此处没有使用箭头函数

getter传参

getter函数就相当于store的计算属性,和vue的计算属性差不多,那么我们都知道Vue中计算属性是不能直接传递参数的,所以我们这里的getter函数如果要接受参数的话,也是需要做处理的。

1
2
3
4
5
6
7
8
getters: {
getAddAge: (state) => {
return (num:number) => state.age + num
},
getNameAndAge(): string {
return this.name + this.getAddAge //调用其它getter
}
}

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

组件中使用

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

actions属性

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

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

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

// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
state: () => {
return {
name: "zhangsan",
age: 18,
sex: "男"
}
},
getters: {
getAddAge: (state) => {
return (num:number) => state.age + num
},
getNameAndAge(): string {
return this.name + this.getAddAge //调用其它getter
}
},
actions:{
saveName(name: string) {
this.name = name;
}
}
})

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

使用actions

直接在组件中调用该方法

1
2
3
4
5
  <button @click="saveName">点我也可以改变名字</button>
// 点击事件
const saveName = () => {
store.saveName('王五')
}

结语

本文主要介绍pinia的基本使用,想要了解更多内容,还是去官网吧

pinia的知识点还是比较少的,无非就是三个大点:

  • state
  • getter
  • actions

我们主要应该学习的是它的函数思想