导读
组件之间原本不能实现数据共享,需要借助其他技术,比如pina
在Vue2的时候使用Vuex
但到了Vue3,官方推荐pina
假设我们有一个需求:在组件 p1 里更新了数据,主页组件也同步更新显示

npm install pinia
在 main.ts 中引入
import { createPinia } from 'pinia'
// ...
createApp(A6).use(antdv).use(router).use(createPinia()).mount('#app')
再新建 store 目录来管理共享数据,下面是 /src/store/UserInfo.ts
import axios from '../api/request'
import { defineStore } from "pinia"
import { UserInfoDto } from '../model/Model8080'
export const useUserInfo = defineStore('userInfo', {
state: () => {
return { username: '', name: '', sex: '' }
},
actions: {
async get(username: string) {
const resp = await axios.get(`/api/info/${username}`)
Object.assign(this, resp.data.data)
},
async update(dto: UserInfoDto) {
await axios.post('/api/info', dto)
Object.assign(this, dto)
}
}
})
定义了 useUserInfo 函数,用来获取共享数据,它可能用于多个组件
state 定义数据格式
actions 定义操作数据的方法
get 方法用来获取用户信息
update 方法用来修改用户信息
由于 useRequest 必须放在 setup 函数内,这里简化起见,直接使用了 axios
获取用户信息
<template>
<div class="a6main">
<a-layout>
<a-layout-header>
<span>{{serverUsername}} 【{{userInfo.name}} - {{userInfo.sex}}】</span>
</a-layout-header>
<a-layout>
<!-- ... -->
</a-layout>
</a-layout>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import AIcon from '../components/AIcon3' // jsx icon 组件
import { serverMenus, serverUsername } from '../router/a6router'
import { useUserInfo } from '../store/UserInfo'
const userInfo = useUserInfo()
onMounted(()=>{
userInfo.get(serverUsername.value)
})
</script>
修改用户信息
<template>
<div class="a6p1">
<h3>修改用户信息</h3>
<hr>
<a-form>
<a-form-item label="用户名">
<a-input readonly v-model:value="dto.username"></a-input>
</a-form-item>
<a-form-item label="姓名" v-bind="validateInfos.name">
<a-input v-model:value="dto.name"></a-input>
</a-form-item>
<a-form-item label="性别">
<a-radio-group v-model:value="dto.sex">
<a-radio-button value="男">男</a-radio-button>
<a-radio-button value="女">女</a-radio-button>
</a-radio-group>
</a-form-item>
</a-form>
<a-button type="primary" @click="onClick">确定</a-button>
</div>
</template>
<script setup lang="ts">
import { Form } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { UserInfoDto } from '../model/Model8080'
import { useUserInfo } from '../store/UserInfo';
const dto = ref<UserInfoDto>({ username: '', name: '', sex: '' })
const userInfo = useUserInfo()
onMounted(()=>{
Object.assign(dto.value, userInfo)
})
const rules = ref({
name: [
{required: true, message:'姓名必填'}
]
})
const { validateInfos, validate } = Form.useForm(dto, rules)
async function onClick() {
try {
await validate()
await userInfo.update(dto.value)
} catch (e) {
console.error(e)
}
}
</script>
首先回顾一下vue3的解构赋值是否是响应式的,取决于解构赋值的类型和方式。
那么回到pina,它使用的响应式系统和Vue3用的不是同一套,所以:
const { name, age } = useUsersStore(),那么解构赋值后会失去响应式,因为访问解构赋值的变量时绕过了对象的get方法,无法触发依赖收集和更新。storeToRefs函数,它会将响应式对象转换为普通对象,但是每个属性都是一个ref,可以保持响应性。例如const { name, age } = storeToRefs(useUsersStore())。So,我个人建议自己尽量少使用解构赋值,感觉意义不大事还多
在 getters 中,state 参数是被 pinia 库自动注入的,所以可以直接在方法中使用。而在 actions 中,如果不显式声明参数类型,那么参数类型会被推断为 any,这样可能会导致一些类型错误。因此,为了确保参数类型的正确性,需要显式声明 actions 中的参数类型。
为什么pinia官方在actions 中不把state 参数自动注入?
在 Pinia 的 actions 中,参数是通过解构传递的,这与 Vue 3 中的 Composables 有关。当你从一个组件或者 store 中引入一个 Composable 时,你只需要解构这个 Composable,这个 Composable 就会自动得到它所需要的数据。类似地,在 actions 中,你需要通过解构 context 来访问 state,getters,commit,dispatch 等。这是为了确保在 actions 中只能访问到需要的状态和操作,而不是暴露所有状态和操作,从而提高了应用程序的安全性和可维护性。
当你在 actions 中访问 state,你实际上是通过 context 参数来访问 state。如果你不想解构 context 参数,可以在 setup 函数中使用 useStore hook 来访问 state。但是在 actions 中,直接访问 state 是被 Pinia 设计为不被支持的。
import { defineStore } from 'pinia'
import { User } from './type'
// 创建 User Store
export const useUserStore = defineStore({
id: "user",
state: (): User => ({
id: 0,
name: "",
isLoggedIn: false,
}),
// 在 `getters` 中,`state` 参数是被 `pinia` 库自动注入的,所以可以直接在方法中使用。
getters: {
getUserId(state): number {
return state.id;
},
getUserName(state): string {
return state.name;
},
isLoggedIn(state): boolean {
return state.isLoggedIn;
},
},
// 而在 `actions` 中,如果不显式声明参数类型,那么参数类型会被推断为 `any`,这样可能会导致一些类型错误。因此,为了确保参数类型的正确性,需要显式声明 `actions` 中的参数类型。
actions: {
// Pinia 官方推荐的写法
login({ state }: { state: User }, { id, name }: { id: number; name: string }): void {
state.id = id;
state.name = name;
state.isLoggedIn = true;
},
// 也可以使用 this.$state 访问 state 中的数据,这样可以避免在 actions 中传递 state 对象。在Pinia中,state属性是store实例的一个对象。为了避免state对象的属性与store实例的方法/属性名称冲突,Pinia会将state属性作为store实例的一个私有属性,通过$state属性来访问。所以在store实例的方法中,使用this.$state来访问state对象的属性,以区分store实例的方法和state对象的属性。
logout(): void {
this.$state.id = 0;
this.$state.name = "";
this.$state.isLoggedIn = false;
},
},
});
Pinia 存储的数据是存储在内存中的,因此在关闭网页或浏览器时,数据也会随之消失。这意味着,Pinia 并不适合用于需要永久存储数据的场景,比如用户的个人资料等。对于这种情况,可以考虑使用浏览器的本地存储方案,比如 localStorage 或者 IndexedDB 等。
解决方案
可以使用Pinia插件 pinia-plugin-persist 来实现将store的数据持久化到浏览器的 sessionStorage 或 localStorage 中,这样在页面刷新或关闭后,数据也可以被保留下来。
你可以按照以下步骤使用 pinia-plugin-persist:
安装插件:
npm install pinia-plugin-persist
导入并使用插件:
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(createPersistedState())
在使用插件时,你可以传递一些选项来自定义插件的行为。例如,你可以指定要使用的持久化存储类型(默认为 sessionStorage),以及要持久化的 store。
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(
createPersistedState({
storage: localStorage, // 指定要使用的持久化存储类型
paths: ['user'], // 指定要持久化的 store
})
)
注意:使用 pinia-plugin-persist 会将 store 中的数据保存到浏览器中,因此你需要仔细考虑要保存哪些数据,以避免安全问题。同时,存储在 sessionStorage 和 localStorage 中的数据会在浏览器关闭或过期后被删除。
最近,有个作业还让我们用Vuex,明明教的是Vue3但明显感觉老师老喜欢选项式那一套。自从有了
<script setup>语法糖之后我认为Vue3才是Vue3,可是呢,老师谈都没谈。虽然我是用Pinia做的但在帮别人改作业的时候却学了Vuex的语法。最后不得不感叹Vuex真的是很low的东西,Pinia真的是清晰方便!
不管怎么说,两者实现状态管理的基本思路还是一致的,首先都得先定义要管理的状态吧?区别只是究竟是交给Pinia还是交给Vuex管理罢了。
主要记住state(data对象)、getter(计算属性)、action(操作函数)即可。
但其实除了上述三者外还有Mutation,它是用于修改状态的函数,它是同步执行的。通过提交一个mutation,你可以在Pinia和Vuex中变更状态。Mutation应该是纯粹的操作,用于修改state的值。而action则是异步的,可以认为使多个mutation组成,所以我们完全可以只用action因为无论是简单的state状态修改还是复杂的操作他都能搞定。
defineStore函数来定义状态存储,并在存储对象中使用state字段来定义状态。createStore函数来创建一个状态管理实例,在实例中使用state字段来定义状态。import { defineStore } from 'pinia';
export const useStore = defineStore('store', {
// :State 是存储应用程序数据的地方。它类似于组件的 data 对象,但是可以被多个组件访问和共享。State 在 store 中定义,并且可以通过 getters 和 actions 进行访问和修改。
state: () => ({
cart: [], // 购物车列表(id、菜名、单价、数量)
}),
// Getter 是用于从 state 中派生出衍生数据的函数。它可以将 state 的一部分或全部进行计算、转换或筛选,并返回一个新的值。Getter 可以被认为是 store 的计算属性,可以在组件中使用。
getters: {
// 获取结果栏的最终统计总数和合计金额
getTotal(state) {
const total = {
totalPrice: 0,
totalNum: 0,
};
for (const item of state.cart) {
total.totalPrice += item.price * item.quantity;
total.totalNum += item.quantity;
}
return total;
},
},
// Action 是用于处理业务逻辑、触发状态变更的函数。它可以包含异步操作、调用 API、提交 mutations 等。Action 接收一个上下文对象(context),可以通过它来访问和操作 state、调用其他 actions,甚至提交 mutations。
actions: {
// 添加商品到购物车
addToCart({ food, quantity }) {
const cartItem = this.$state.cart.find((item) => item.id === food.id);
if (cartItem) {
// 购物车已存在
cartItem.quantity += quantity;
} else {
// 购物车不存在
this.$state.cart.push({ ...food, quantity });
}
},
// 更新购物车单项商品数量(加减按钮通用)
updateCartItem(item) {
const cartItem = this.$state.cart.find((i) => i.id === item.id);
if (cartItem) {
cartItem.quantity = item.quantity;
}
},
// 从购物车中移除指定商品
removeFromCart(item) {
const index = this.$state.cart.findIndex((i) => i.id === item.id);
// 如果存在则移除
if (index !== -1) {
this.$state.cart.splice(index, 1);
}
},
},
});
import { createStore } from 'vuex';
const store = createStore({
state() {
return {
cart: [], // 购物车列表(id、菜名、单价、数量)
};
},
getters: {
// 获取结果栏的最终统计总数和合计金额
getTotal(state) {
const total = {
totalPrice: 0,
totalNum: 0,
};
for (const item of state.cart) {
total.totalPrice += item.price * item.quantity;
total.totalNum += item.quantity;
}
return total;
},
},
mutations: {
addToCart(state, { food, quantity }) {
const cartItem = state.cart.find((item) => item.id === food.id);
if (cartItem) {
// 购物车已存在,+1
cartItem.quantity += quantity;
} else {
// 购物车中没有则添加
state.cart.push({ ...food, quantity });
}
},
//商品的增加和减少都会使得cart变化,调用该方法以更新。则不用添加两个具体方法增加和减少
updateCartItem(state, item) {
const cartItem = state.cart.find((i) => i.id === item.id);
if (cartItem) {
cartItem.quantity = item.quantity;
}
},
removeFromCart(state, item) {
const index = state.cart.findIndex((i) => i.id === item.id);
// 如果存在则移除
if (index !== -1) {
state.cart.splice(index, 1);
}
},
},
actions: {
addToCart({ commit }, payload) {
commit('addToCart', payload);
},
updateCartItem({ commit }, payload) {
commit('updateCartItem', payload);
},
removeFromCart({ commit }, payload) {
commit('removeFromCart', payload);
},
},
});
export default store;
访问和调用方式才是重点,这是他们的差异所在(至少从语法上来看,Pinia比Vuex方便的多)
Pinia采用了函数对象调用的方式。这意味着在Pinia中,你可以直接通过调用函数来触发状态的变更。例如,对于一个名为counter的状态,你可以直接调用该函数来增加计数器的值:
counter.increment()
这种调用方式更加直观和直观,因为你可以像调用普通函数一样使用状态的变更操作。
而在Vuex中,通常采用字符串触发的方式。你需要通过提交(commit)一个字符串类型的mutation或者分发(dispatch)一个字符串类型的action来触发状态的变更。例如,对于一个名为counter的模块,你需要使用字符串来指定需要触发的mutation或action:
store.commit('counter/increment')
store.dispatch('counter/increment')
这种方式相对于Pinia的函数对象调用来说,更加显式地表明了你想要触发的具体变更操作。
这些不同的调用方式是由于Pinia和Vuex在设计理念和API风格上的差异所导致的。Pinia倾向于使用函数对象调用,更加贴近普通函数的使用方式;而Vuex则采用了字符串触发,更加明确地指定了需要触发的变更操作。但在语法上尤其定义了多个状态模块的情况下这一点代码将变得非常冗余
使用Pinia时,我们通过useStore()函数创建一个store实例,然后可以直接访问store的状态和调用actions。我们可以在setup()函数中获取状态,定义方法,并在模板中使用它们。
要注意的是useStore }是从"../store/index"中导入的一个状态函数对象。区别于Vuex库的useStore(),这一点要非常清除!
<script setup>
import { reactive } from "vue";
import { useStore } from "../store/index";
import { ElMessage } from "element-plus";
const store = useStore();
const foodList = reactive([
{ id: 1, name: "油焖鸡腿", src: "/img/food (1).jpg", price: 38 },
...
]);
// 加入购物车
function handleAdd(index) {
const food = foodList[index];
const cartItem = store.cart.find((item) => item.id === food.id);
if (cartItem) {
cartItem.quantity++;
} else {
store.addToCart({ food, quantity: 1 });
}
ElMessage({
message: `${food.name}成功加入了购物车!`,
type: "success",
});
}
</script>
从Vuex库导入useStore(),通过库的useStore()得到的是一个Vuex全局的状态管理对象;如果Vuex中只定义了一个状态那么可以直接通过state、getter和借助dispatch等方法对Action等直接访问,如下所示。但如果定义了多个状态模块的话就要更麻烦些了,。
<script setup>
import { reactive } from "vue";
import { useStore } from "vuex";
import { ElMessage } from "element-plus";
const store = useStore();
const foodList = reactive([
{ id: 1, name: "白灼菜心", src: require('@/assets/1.png'), price: 9 },
...
]);
// 加入购物车
function handleAdd(index) {
const food = foodList[index];
const cartItem = store.state.cart.find((item) => item.id === food.id);
if (cartItem) {
cartItem.quantity++;
} else {
store.dispatch('addToCart',{ food, quantity: 1 });
}
ElMessage({
message: `${food.name}成功加入了购物车!`,
type: "success",
});
}
</script>
如果要定义多个状态模块,可以使用createStore函数的modules选项。下面是一个在Vue 3中使用最新语法定义多个状态模块的示例:
import { createApp } from 'vue';
import { createStore } from 'vuex';
const moduleA = {
state() {
return {
// 状态属性
};
},
mutations: {
// 状态变更
},
actions: {
// 异步操作
},
getters: {
// 计算属性
}
};
const moduleB = {
state() {
return {
// 状态属性
};
},
mutations: {
// 状态变更
},
actions: {
// 异步操作
},
getters: {
// 计算属性
}
};
const store = createStore({
modules: {
moduleA,
moduleB
}
});
const app = createApp(App);
app.use(store);
app.mount('#app');
在上述示例中,我们定义了两个模块moduleA和moduleB,并将它们作为modules选项的属性添加到createStore函数中。每个模块都具有自己的state、mutations、actions和getters。
要在组件中访问和调用不同的模块,可以使用useStore函数来获取Vuex Store的实例,并使用模块名称作为命名空间来访问和调用模块中的状态、变更、动作和计算属性。例如:
<template>
<div>
<p>Module A Count: {{ moduleACount }}</p>
<p>Module B Count: {{ moduleBCount }}</p>
<button @click="incrementModuleA">Increment Module A</button>
<button @click="decrementModuleB">Decrement Module B</button>
</div>
</template>
<script>
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const moduleACount = computed(() => store.state.moduleA.count);
const moduleBCount = computed(() => store.state.moduleB.count);
const incrementModuleA = () => {
store.commit('moduleA/increment');
};
const decrementModuleB = () => {
store.dispatch('moduleB/decrement');
};
return {
moduleACount,
moduleBCount,
incrementModuleA,
decrementModuleB
};
}
};
</script>
在上述例子中,我们通过访问store.state.moduleA.count和store.state.moduleB.count来获取模块moduleA和moduleB的状态属性。使用store.commit来触发moduleA模块中的变更,使用store.dispatch来调度moduleB模块中的动作。
Pinia和Vuex是用于Vue应用程序状态管理的两个库,它们之间存在一些主要区别:
API设计:Pinia是在Vue 3的Composition API基础上构建的全新状态管理库,而Vuex是基于Vue 2的选项API设计的。Pinia利用了Composition API的优势,提供了更简洁、灵活和直观的API,使开发人员可以更好地组织和管理状态。
类型支持:Pinia提供了对TypeScript的原生支持,包括类型推断、类型安全的状态访问和代码提示。它充分利用了Vue 3的类型系统,使得在使用TypeScript时能够更轻松地编写类型安全的状态管理代码。Vuex在Vue 2中的类型支持相对较弱,需要借助插件或其他工具来实现类型检查。
性能优化:Pinia在内部实现上进行了一些性能优化,例如使用响应式缓存,将状态和订阅关联起来,以便在状态变更时只更新相关的订阅者。这种优化可以提高大型应用程序的性能,并减少不必要的更新。Vuex在性能方面相对较慢,特别是对于大型应用程序,需要额外的优化措施来提高性能。
生态系统和迁移:由于Vuex是Vue生态系统中广泛使用的状态管理库,因此具有更广泛的插件和工具支持。它也有大量的文档和社区资源可供参考。然而,Pinia作为相对较新的库,其生态系统和资源相对较小。迁移现有的Vue 2应用程序使用Vuex到Vue 3和Pinia可能需要一些努力和调整。
综上所述,Pinia在Vue 3中提供了更现代、类型安全和性能优化的状态管理解决方案,而Vuex则是在Vue 2中广泛使用的传统状态管理库,具有更丰富的生态系统和插件支持。选择使用Pinia还是Vuex取决于项目需求、团队技术栈和个人偏好。
我的话:用新不用旧,用易不用难