拥抱Pinia
Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇。
0.Pinia对比Vuex
Pinia API 与 Vuex ≤4 有很大不同,即:
- 没有mutations。mutations被认为是非常冗长的。最初带来了vue-devtool集成,但这不再是问题。
- 不再有模块的嵌套结构。您仍然可以通过在另一个store中导入和使用store来隐式嵌套store,但 Pinia 通过设计提供扁平结构,同时仍然支持store之间的交叉组合方式。您甚至可以拥有store的循环依赖关系。
- 更好的TypeScript支持。无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断。
- 不再需要注入、导入函数、调用它们,享受自动补全!
- 无需动态添加stores,默认情况下它们都是动态的,您甚至不会注意到。请注意,您仍然可以随时手动使用store来注册它,但因为它是自动的,所以您无需担心。
- 没有命名空间模块。鉴于store的扁平架构,“命名空间”store是其定义方式所固有的,您可以说所有stores都是命名空间的。
1. Pinia快速入门
1.1 安装
yarn add pinia
# or with npm
npm install pinia
如果您的应用程序使用Vue2,您还需要安装组合式API包: @vue/composition-api
如果您使用的是Vue CLI,您可以试试这个非官方插件。
1.2 初始化配置
Vue3 + Vite2:
1.在src/main.ts文件中
import { createApp } from 'vue'
// 1.安装后 导入
import { createPinia } from 'pinia'
import App from './App.vue'
// 2. 创建pinia实例
const pinia = createPinia()
const app = createApp(App)
// 3. use 挂载
app.use(pinia)
app.mount('#app')
2.在src/store/index.ts文件中
import { defineStore } from "pinia";
// 1.定义容器
// 参数1: 容器的ID,必须唯一,将来pinia会把所有容器挂载到根容器
// 参数2: 选项对象
export const useMainStore = defineStore("main", {
/**
* 类似于组件的data, 用来存储全局状态
* 1. 必须是函数,这样是为了在服务端渲染的时候避免交叉请求导致的数据状态污染
* 2. 必须是箭头函数,这样是为了更好的 TS 类型推导
* @returns 一个函数,调用得到容器实例
*/
state: () => ({
counter: 1,
name: 'coderY',
arr: [1,2,3]
}),
/**
* 类似于组件的computed,用来封装计算属性,有缓存的功能
*/
getters: {},
/**
* 类似于组件的methods,封装业务逻辑(同步,异步都可以),修改state
*/
actions: {},
});
2. State
类似于组件的data
,用来存储全局状态
2.1 访问State
方式1:数据量不多的情况下可以直接访问。
<template>
<div>{{ storeMain.counter }}</div>
<div>{{ storeMain.name }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
const storeMain = useMainStore();
</script>
<style lang="scss" scoped>
</style>
2.2 使用storeToRefs解构数据
方式2:数据量多的情况下,需要单独解构的话可以使用pinia提供的storeToRefs
<template>
<div>{{ counter }}</div>
<div>{{ name }}</div>
<div>{{ arr }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
import { storeToRefs } from 'pinia'
const storeMain = useMainStore();
// 如果不使用storeToRefs,解构出来的数据不是响应式的
// 这里用toRefs也可以,Pinia 内部也是用的 toRefs,只不过做了一层包装。
const { counter , name , arr } = storeToRefs(storeMain);
</script>
<style lang="scss" scoped>
</style>
2.3 修改State
<template>
<div>{{ storeMain.counter }}</div>
<div>{{ storeMain.name }}</div>
<div>{{ storeMain.arr }}</div>
<button @click="handleChangeState3"> 修改 </button>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
import { storeToRefs } from 'pinia'
const storeMain = useMainStore();
// 方式1:最简单的方式
// 修改一次,视图就更新一次,如果数据量大,造成的开销会很大,不建议
const handleChangeState1 = () => {
storeMain.counter++;
storeMain.name = 'test';
}
// 方式2: 如果要修改多个数据 $patch(对象)批量更新
// 一次性修改多个数据 建议使用$patch 在性能上有优化 一次性修改好数据,更新一次视图
const handleChangeState2 = () => {
storeMain.$patch({
// 这里不能写为 暂时不清楚原因 有知道的欢迎评论区做客
// counter: storeMain.counter++;
counter: storeMain.counter + 1,
name: storeMain.name = 'test',
arr: [ ...storeMain.arr , 4]
})
}
// 方式3: 如果要修改多个数据 $patch(函数) 批量更新
// 一次性修改多个数据 建议使用$patch 在性能上有优化 一次性修改好数据,更新一次视图
const handleChangeState3 = () => {
storeMain.$patch( (state) => {
state.counter++;
state.name = 'lisi'
state.arr.push(4);
})
}
</script>
<style lang="scss" scoped>
</style>
我们还可以通过 actions 去修改 state,action 里可以直接通过 this 访问,比较常用的做法。
src/store/index.ts
import { defineStore } from "pinia";
export const useMainStore = defineStore("main", {
state: () => {
return {
counter: 1,
name: "张三",
arr: [1, 2, 3],
};
},
getters: {},
actions: {
// actions : 不能使用箭头函数,因为箭头函数绑定外部this
changeState(num: number) {
this.counter += num;
this.name = "李四";
this.arr.push(4);
},
},
});
组件内调用actions
<template>
<div>{{ storeMain.counter }}</div>
<div>{{ storeMain.name }}</div>
<div>{{ storeMain.arr }}</div>
<button @click="handleChangeState4(10)"> 修改 </button>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
const storeMain = useMainStore();
// 方式4:actions
const handleChangeState4 = (num: number) => {
storeMain.changeState(num);
}
</script>
<style lang="scss" scoped>
</style>
3. getters
类似于组件的computed
,用来封装计算属性,有缓存的功能。
src/store/main.ts
import { defineStore } from "pinia";
export const useMainStore = defineStore("main", {
state: () => {
return {
counter: 1,
name: "张三",
arr: [1, 2, 3],
};
},
getters: {
// 函数接受一个可选参数 state 状态对象
counterDouble(state) {
console.log('counterDouble调用了');
return state.counter * 2;
}
// 细节1:如果在getters中使用了this (并且没有传递state可选参数) 则必须手动指定 返回值类型
// 否则ts会有报错提示
// counterDouble(): number {
// console.log('counterDouble调用了');
// return this.counter + 10;
//},
// 细节2:getters中使用this 参数写了state却没有使用 ts不会有报错提示
// counterDouble(state) {
// console.log('counterDouble调用了');
// return this.counter - 10;
// }
},
actions: {
// actions : 不能使用箭头函数,因为箭头函数绑定外部this
changeState(num: number) {
this.counter += num;
this.name = "李四";
this.arr.push(4);
},
},
});
组件内
<template>
<button @click="handleChangeState">修改数据</button>
<div>{{ storeMain.counter }}</div>
<div>{{ storeMain.name }}</div>
<div>{{ storeMain.arr }}</div>
<hr />
<!-- 调用三次getters -->
<div>双倍getters{{ storeMain.counterDouble }}</div>
<div>双倍getters{{ storeMain.counterDouble }}</div>
<div>双倍getters{{ storeMain.counterDouble }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
const storeMain = useMainStore();
const handleChangeState = () => {
storeMain.changeState(10)
}
</script>
<style lang="scss" scoped>
</style>
在模板中我们调用了三次getters中的函数,但是只打印了一次,不难看出,我们getters是有缓存功能的,数据发生变动的时候,才会去重新调用。
4. actions
类似于组件的methods
,用来封装业务逻辑(同步,异步都可以)。
处理同步、异步请求
src/api/test.ts
export const getData = async () => {
const res = await handleAsyncFns(1000)
return res
}
// 假设异步请求数据
async function handleAsyncFns(delay: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data...')
}, delay);
});
}
src/store/index.ts
import { defineStore } from "pinia";
import { getData } from '../api/test'
interface stateType {
counter: number;
name: string;
arr: number[];
data: unknown;
}
export const useMainStore = defineStore("main", {
state: (): stateType => ({
counter: 1,
name: "张三",
arr: [1, 2, 3],
data: 'none'
}),
getters: {},
actions: {
// 同步请求
changeState(num: number) {
this.counter += num;
this.name = "李四";
this.arr.push(4);
},
// 异步请求
async getApiData() {
const res = await getData()
this.data = res;
console.log(res)
}
},
});
组件内
<template>
<button @click="handleChangeState">修改数据</button>
<div>{{ storeMain.counter }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
const storeMain = useMainStore();
const handleChangeState = () => {
// 同步操作
storeMain.changeState(10)
// 异步操作 假设是网络请求 都可以在actions里面调用
storeMain.getApiData()
// 看看state里面的数据
console.log(storeMain.data)
}
</script>
<style lang="scss" scoped>
</style>
actions之间的相互调用
src/api/test.ts
export const getData = async () => {
const res = await handleAsyncFns(1000)
return res
}
// 假设异步请求数据
async function handleAsyncFns(delay: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data...')
}, delay);
});
}
src/store/base.ts
import { defineStore } from "pinia";
interface baseState {
baseCounter: number;
}
export const useBaseStore = defineStore({
id: "base",
state: (): baseState => ({
baseCounter: 1,
}),
getters: {},
//提供给其他actions 调用的
actions: {
handleUpdateState() {
setTimeout(() => {
this.baseCounter++;
}, 500);
},
},
});
src/store/index.ts
import { defineStore } from "pinia";
// 导入要调用的actions
import { useBaseStore } from './base'
import { getData } from '../api/test'
interface stateType {
counter: number;
name: string;
arr: number[];
data: unknown;
}
export const useMainStore = defineStore("main", {
state: (): stateType => ({
counter: 1,
name: "张三",
arr: [1, 2, 3],
data: 'none'
}),
getters: {},
actions: {
// 调用的方法
updataOtherActions() {
const useBaseMain = useBaseStore();
useBaseMain.handleUpdateState()
},
// 同步请求
changeState(num: number) {
this.counter += num;
this.name = "李四";
this.arr.push(4);
},
// 异步请求
async getApiData() {
const res = await getData()
this.data = res;
// 调用其他actions
this.updataOtherActions();
console.log(res)
}
},
});
组件内
<template>
<button @click="handleChangeState">修改数据</button>
<div>{{ storeMain.counter }}</div>
<hr />
<div>{{ baseMain.baseCounter }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
import { useBaseStore } from '../store/base'
const storeMain = useMainStore();
const baseMain = useBaseStore();
// 这里顺带调用了其他acitons
const handleChangeState = () => {
// 同步操作
storeMain.changeState(10)
// 异步操作 假设是网络请求 都可以在actions里面调用
storeMain.getApiData()
// 看看state里面的数据
console.log(storeMain.data)
}
</script>
<style lang="scss" scoped>
</style>
5. 数据持久化
5.1 pinia-plugin-persist插件安装
yarn add pinia-plugin-persist
# or with npm
npm install pinia-plugin-persist
5.2 pinia-plugin-persist插件使用
5.3 导入
src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// pinia 数据持久化插件的导入
import piniaPluginPersist from 'pinia-plugin-persist'
import App from './App.vue'
const pinia = createPinia()
// pinia 数据持久化插件的挂载
pinia.use(piniaPluginPersist)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
5.4 配置
在需要持久化的store配置即可,如果有多个store,则需要配置多次。
src/store/index.ts
import { defineStore } from "pinia";
interface stateType {
counter: number;
name: string;
}
export const useMainStore = defineStore("main", {
state: (): stateType => ({
counter: 1,
name: "张三"
}),
getters: {},
actions: {
changeState(num: number) {
this.counter += num;
this.name = "李四";
}
},
// 开启数据缓存
persist: {
enabled: true,
strategies: [
{
// 自定义 存储时的key
key: "indexKey",
// 默认是存储在 sessionStorage里面, 我们进行修改
storage: localStorage,
// 指定需要持久化的数据 , 不在指定数据的不会进行持久化
paths: ["counter"]
},
],
},
});
组件内
<template>
<button @click="handleChangeState">修改数据</button>
<div>{{ storeMain.counter }}</div>
<div>{{ storeMain.name }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '../store'
const storeMain = useMainStore();
const handleChangeState = () => {
storeMain.changeState(10)
}
</script>
<style lang="scss" scoped>
</style>