1. Pinia 与 Vuex 的对比
1.1. API 简洁性
Vuex:使用较为复杂的四大概念:State, Getters, Mutations, Actions。
Pinia:提供了更简单、更直观的 API,去掉了 Mutations,仅保留 State 和 Actions,减少了心智负担。
1.2. TypeScript 支持
Vuex:TypeScript 支持较为复杂,需要大量的类型定义和配置。
Pinia:内置了对 TypeScript 的良好支持,类型推断更加友好。
1.3. DevTools 支持
Vuex:DevTools 支持较好,可以方便地进行状态的调试和时间旅行。
Pinia: 同样支持 Vue DevTools,并提供了更好的插件机制,增强了调试能力。
1.4. 插件机制
Vuex:插件机制较为有限,扩展性不如 Pinia。
Pinia:提供了灵活的插件机制,可以方便地添加中间件、日志等功能。
1.5. 状态持久化
Vuex:需要手动配置插件实现状态持久化。
Pinia: 内置了状态持久化支持,通过简单配置即可实现。
2. Pinia 的设计理念
2.1. 单一状态树
Pinia 和 Vuex 一样,采用单一状态树的设计,将应用的所有状态集中管理。这种设计方式有助于维护状态的一致性和可预测性。
2.2. 响应式
Pinia 利用 Vue 3 的响应式系统,将状态声明为响应式对象,使得状态变化能够自动触发视图更新。
2.3. 模块化
Pinia 提供了模块化的状态管理方式,可以根据需要将状态划分为多个 store,使得状态管理更加清晰和可维护。
2.4. 插件友好
Pinia 提供了丰富的插件接口,可以方便地扩展状态管理的功能,如持久化、日志记录、性能监控等。
3. Pinia 核心概念
3.1. State
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象。
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});
也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想要暴露出去的属性和方法的对象,推荐选择这种写法。
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
function increment() {
count.value++;
}
return { count, increment };
});
可以这样理解:
在 Setup Store 中:
1. ref() 相当于 state 属性;
2. computed() 相当于 getters;
3. function() 相当于 actions;
使用 Store 也很简单:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 store
const store = useCounterStore()
</script>
3.2. Getter
跟 Vuex 的写法类似
// 示例文件路径:
// ./src/store/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 9,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
}
})
当然我们推荐用组合式的写法。
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return this.counterStore.doubleCount * 2
},
}
})
</script>
除此以外,你也可以使用 mapState 去解构内容到 getter。
import { mapState } from 'pinia'
import { useCounterStore } from './stores/counter'
export default {
computed: {
// 允许在组件中直接使用 this.doubleCount
...mapState(useCounterStore, ['doubleCount']),
// 重命名为 this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'doubleCount',
// 通过函数访问store
double: store => store.doubleCount
})
}
}
3.3. Action
Action 相当于组件中的 method,它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。
export const useCounterStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
randomizeCounter() {
this.count = Math.round(100 * Math.random());
},
},
});
类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注以及自动补全。不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action。下面是一个使用 Mande 的例子。请注意,你使用什么库并不重要,只要你得到的是一个 Promise,你甚至可以在浏览器中使用原生 fetch 函数。
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}`)
} catch (error) {
showTooltip(error)
// 让表单组件显示错误
return error
}
},
},
})
3.4. 使用
推荐直接用 composition api 的写法,来消费对应数据。
<!-- 使用选项式 API 的 Pinia 写法 -->
<script setup lang="ts">
// 始终注意 Pinia 定义的逻辑,依然是接近 composition API
import { useCounterStore } from "./store";
// 通过 useStore 获取 store 实例
const store = useCounterStore();
</script>
<template>
<div>
{{ store.count }}
<button @click="store.increment">+1</button>
<button @click="store.decrement">-1</button>
</div>
</template>
<style scoped></style>
4. Pinia 插件
Pinia 提供了灵活的插件机制,可以方便地定义自定义插件。下面是一个简单的日志插件示例:
import { PiniaPluginContext } from 'pinia';
function createLogger() {
return (context: PiniaPluginContext) => {
const store = context.store;
store.$onAction(({ name, args, after, onError }) => {
console.log(`Action ${name} was called with args: `, args);
after((result) => {
console.log(`Action ${name} ended with result: `, result);
});
onError((error) => {
console.error(`Action ${name} failed with error: `, error);
});
});
}
}
export default createLogger;
在 Pinia 实例中使用该插件:
import { createPlnia } from 'plnia';
import createLogger from './loggerPlugin';
const plnia = createPlnia();
plnia.use(createLogger);
app.use(plnia);
5. 最佳实践
尽可能拆分 composition api 的子逻辑,这样能够在更多的场景下进行复用。
const useDouble = (count: number) => {
return count * 2;
};
const useIncrement = (count: number) => {
return {
increment: () => {
count++;
},
};
};
const useDecrement = (count: number) => {
return {
decrement: () => {
count--;
},
};
};
// 第二种 composition api
export const useCounterStore = defineStore("counter", () => {
// state
const count = ref(0);
// getters
// const double = computed(() => count.value * 2);
const double = useDouble(count.value);
// actions
const increment = useIncrement(count.value);
// const increment = () => {
// count.value++;
// };
const decrement = useDecrement(count.value);
// const decrement = () => {
// count.value--;
// };
return {
count,
double,
increment,
decrement,
};
});