目录
状态管理技术选型
1. Pinia概述
前端项目需要使用到状态管理框架,目前社区流行的有Vuex和Pinia两个框架。经过对比后,我们选择Pinia作为我们的状态管理库。下面将分点说明Pinia特点。
-
背景
Pinia是根据Vue3而设计的,相比于Vuex来说历史包袱更少,它吸收了Vuex下个版本的很多核心概念以及项目开发经验而产生。
-
框架量级
Pinia是轻量级的,体积很小。
-
开发体验
Pinia有完整的 TypeScript 支持,与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易。
在使用上可以使用贴近Vuex的API风格来编写代码,也可以使用Vue3组合式API的风格来编写代码。
Store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见。
-
社区和生态系统
Pinia目前是Vue.js生态系统中增长最快的状态管理库之一,社区正在快速增长。然而Vuex毕竟是Vue.js核心团队推荐的状态管理库,拥有庞大的社区,核心团队成员做出了重大贡献。 Stack Overflow 上很容易找到 Vuex 错误的解决方案。
-
Pinia能力评估
接下来是评估Pinia是否能满足我们项目对于状态管理库的要求。
-
跨组件/页面数据共享(支持)
-
热更新(支持)
-
插件机制(支持)
-
Devtools(支持)
-
数据持久化(支持,可通过插件机制实现)
2. Vuex的应用
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
2.1 Vuex 的使用
-
安装Vuex
yarn add vuex@next --save
# 或者使用 npm
npm install vuex@next --save
-
安装Vuex 后在main.ts入口文件注册Vuex
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index';
import store from './vuex/store'; // 引入store对象
const app = createApp(App);
app.use(router);
app.use(store); // 把store对象添加到vue实例上
app.mount('#app');
-
src文件夹下新建store文件夹,然后在store文件夹下新建store.ts文件
import { Store, createStore } from 'vuex';
// 配置vue+ts的项目里面使用vuex
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number;
list: string[];
msg: string;
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>;
}
}
const store = createStore({
// 数据
state() {
return {
count: 1,
list: ['马总', '刘总'],
msg: '你好vue',
};
},
// 方法 Mutations里面的函数必须是同步操作
mutations: {
// 方法的作用修改state中的变量,state是必须默认参数
increment(state: any) {
state.count++;
},
// 增加一个带参数的mutations方法
setCount(state: any, num: number) {
state.count = num;
},
setMsg(state: any, msg: string) {
state.msg = msg;
},
},
// 计算属性
getters: {
// 获取修饰后的name,第一个参数state为必要参数,必须写在形参上
reverseMsg(state: any) {
return state.msg.split('').reverse().join('');
},
num(state: any) {
return state.count + 10;
},
},
// 执行mutations里面的方法 异步操作放在actions
actions: {
// 默认第一个参数是context,其值是复制的一份store
increment(context) {
// 执行mutations里面的increment
context.commit('increment');
},
// action就是去提交mutation的,什么异步操作都在action中消化了,最后再去提交mutation的
// 直接将context结构掉,解构出commit,下面就可以直接调用了
setMsg({ commit }, msg: string) {
// 执行mutations里面的increment
setTimeout(() => {
commit('setMsg', msg);
}, 1000);
},
},
});
// vuex的模块
// import storeModule1 from './storeModule1';
// import storeModule2 from './storeModule2';
// const store = createStore({
// modules: {
// storeModule1: storeModule1,
// storeModule2: storeModule2,
// },
// });
export default store;
2.2 测试组件
新建test.vue的测试组件,使用Vuex
<template>
<div>
vuex测试组件
<br />
<br />
vuex--{{ count }}
<br />
<br />
<button @click="increment">执行vuex里面的方法,改变count</button>
<br />
<br />
<button @click="setCount">执行vuex里面的方法,进行传值</button>
<br />
<br />
<hr />
<ul>
<li v-for="(item, index) in $store.state.list" :key="index">{{ item }}</li>
</ul>
<hr />
<br />
<br />
获取getters的数据--{{ reverseMsg }}
<br />
<br />
获取getters的数据--{{ num }}
<br />
<br />
<button @click="setMsg">改变state里面的数据</button>
<br />
<br />
<hr />
触发actions里面的方法
<br />
<br />
<button @click="$store.dispatch('increment')">actions increment</button>
<br />
<br />
<button @click="setActionMsg">actions setMsg</button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { mapGetters, mapState, useStore } from 'vuex';
// import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default defineComponent({
// 组合式api写法
setup() {
const store = useStore();
return {
count: computed(() => {
return store.state.count;
}),
increment: () => {
// 触发vuex中mutation的方法
store.commit('increment');
},
setCount: () => {
// 触发vuex中mutation的方法 进行传值
store.commit('setCount', 15);
},
reverseMsg: computed(() => {
return store.getters.reverseMsg;
}),
num: computed(() => {
return store.getters.num;
}),
setMsg: (): void => {
store.commit('setMsg', '你好,我是mutations改变后的数据');
},
setActionMsg: () => {
store.dispatch('setMsg', '你好,我是actions改变后的数据');
},
};
},
// 选项式api写法
// computed: {
// // ...mapState(['count', 'list']), // 经过解构后,自动添加到计算属性中,此时就可以直接像访问计算属性一样访问它
// // ...mapState({ // 赋别名接收对象,不是数组
// // num: (state: any) => state.count,
// // myList: (state: any) => state.list,
// // }),
// // ...mapGetters(['reverseMsg', 'num']),
// // ...mapState({
// // reverseMsg: 'reverseMsg',
// // num: 'num',
// // }),
// count(): number {
// // 使用vuex中state的数据
// return this.$store.state.count;
// },
// reverseMsg(): string {
// return this.$store.getters.reverseMsg;
// },
// num(): number {
// return this.$store.getters.num;
// },
// },
// methods: {
// //...mapMutations(['increment']),
// //...mapActions(['setMsg']),
// increment(): void {
// // 触发vuex中mutation的方法
// this.$store.commit('increment');
// this.increment();
// },
// setCount() {
// // 触发vuex中mutation的方法 进行传值
// this.$store.commit('setCount', 15);
// },
// setMsg() {
// this.$store.commit('setMsg', '你好我是改变后的数据');
// },
// setActionMsg(): void {
// this.$store.dispatch('setMsg', '你好我是改变后的数据');
// },
// // async setActionMsg(): void {
// // await this.setMsg('setMsg', '你好我是改变后的数据');
// // },
// },
});
</script>
<style lang="scss" scoped></style>
3. Pinia 的应用
Pinia 是 Vue 的状态管理库,它允许您跨组件/页面共享状态。
3.1 Pinia 的使用
-
安装Pinia
yarn add pinia#
#或者使用 npm
npm install pinia
-
安装pinia后在main.ts入口文件注册pinia
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index';
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(router);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');
-
src文件夹下新建store文件夹,然后在store文件夹下新建mian.ts文件
import { defineStore } from 'pinia';
type User = {
name: string;
age?: number;
sex?: string;
msg?: string;
};
const login = (user: User): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => {
user.msg = '登陆成功';
resolve(user);
}, 2000);
});
};
export const useMainStore = defineStore({
id: 'mian',
state: () => ({
name: '超级管理员',
count: 0,
user: <User>{},
}),
// computed 计算属性
getters: {
nameLength: state => state.name.length,
newName(): string {
return `$-${this.name}--${this.nameLength}`;
},
},
// methods 可以做同步、异步 提交state
actions: {
setCount(count: number) {
this.count = count;
},
setUser(user: User) {
this.user = user;
},
async setUserLogin(user: User) {
const result = await login(user);
this.user = result;
this.setSex('女');
},
setSex(sex: string) {
this.user.sex = sex;
},
},
});
3.2 Pinia 的持久化
-
在store文件夹下封装一个Pinia 插件,文件名为piniaPlugin.ts
import { PiniaPluginContext } from 'pinia';
import { toRaw } from 'vue';
type Options = {
key?: string;
};
const _piniaKey_ = 'pinia';
const setStorage = (key: string, value: any) => {
localStorage.setItem(key, JSON.stringify(value));
};
const getStorage = (key: string) => {
return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {};
};
export const piniaPlugin = (options: Options) => {
return (context: PiniaPluginContext) => {
const { store } = context;
const data = getStorage(`${options?.key ?? _piniaKey_}-${store.$id}`);
store.$subscribe(() => {
setStorage(`${options?.key ?? _piniaKey_}-${store.$id}`, toRaw(store.$state));
});
return {
...data,
};
};
};
-
在main.ts入口文件注册Pinia 持久化插件
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index';
import { createPinia } from 'pinia';
import { piniaPlugin } from './store/piniaPlugin';
const app = createApp(App);
app.use(router);
const pinia = createPinia();
pinia.use(piniaPlugin({ key: 'pinia' }));
app.use(pinia);
app.mount('#app');
3.3 测试组件
新建test.vue的测试组件,使用Pinia
<template>
<div>
<p>state</p>
用户名:{{ mainStore.name }}--- count:{{ mainStore.count }}
</div>
<hr />
<div>
<p>getters</p>
nameLength:{{ mainStore.nameLength }}
<br />
newName:{{ mainStore.newName }}
</div>
<hr />
<div>
<p>修改store中的变量</p>
<button @click="change1">第一种方式</button>
<button @click="change2">第二种方式</button>
<button @click="change3">第三种方式</button>
<button @click="change4">第四种方式</button>
<button @click="change5">第五种方式</button>
</div>
<hr />
<div>
<p>pinia解构不具有响应性解决办法</p>
用户名:{{ name }}--- count:{{ count }}
</div>
<hr />
<div>
<p>actions</p>
用户名:{{ mainStore.user }}
<button @click="changeUser">同步 --- 修改用户</button>
<button @click="userLogin">异步 --- 用户登陆</button>
</div>
<hr />
<div>
<p>reset</p>
<button @click="reset">reset --- 恢复state初始值</button>
</div>
<hr />
</template>
<script setup lang="ts">
import { useMainStore } from '@/store/mian';
import { storeToRefs } from 'pinia';
const mainStore = useMainStore();
// 修改store中的变量
const change1 = () => {
mainStore.count++;
};
const change2 = () => {
mainStore.$patch({
name: '修改后名称变化,nameLength也改变了',
count: 2,
});
};
const change3 = () => {
mainStore.$patch(state => {
state.name = '修改后名称变化,nameLength也改变了';
state.count = 3;
});
};
const change4 = () => {
mainStore.$state = {
name: '修改后名称变化,nameLength也改变了',
count: 4,
user: { name: '李四' },
};
};
const change5 = () => {
mainStore.setCount(5);
};
// pinia解构不具有响应性解决办法
// const { name, count } = mainStore;
const { name, count } = storeToRefs(mainStore);
// actions
const changeUser = () => {
mainStore.setUser({ name: '张三' });
};
const userLogin = () => {
mainStore.setUserLogin({ name: '王五', age: 40 });
};
// reset
const reset = () => {
mainStore.$reset();
};
// state的值变化触发subscribe
mainStore.$subscribe(
(args, state) => {
console.log('========', args);
console.log('========', state);
},
{ detached: true } // 组件销毁仍然进行监听,还有deep,flush属性
);
// actions变化触发onAction
mainStore.$onAction(args => {
args.after(() => {
console.log('========', 'after');
});
console.log('========', args);
}, true); // 组件销毁仍然进行监听
</script>
<style lang="scss" scoped>
button {
margin: 10px;
}
</style>