文章目录
- Pinia 状态管理
- 一、 Pinia 安装与使用
- 1.1 安装
- 1.2 注册 pinia 实例到全局
- 1.3 创建一个 Store
- 1.4 组件内使用 Store
- 二、Pinia 核心概念展开学习
- Store 的定义和使用
- 2.1 State
- 2.2 Getter
- 2.3 Action
- 附:
- 1. 什么是魔法字符串?
Pinia 状态管理
一、 Pinia 安装与使用
Pinia(发音为 /piːnjʌ/
) 是一个拥有 “组合式 API” 的 Vue 专属状态管理库,它允许跨组件或页面共享状态。
Pinia 相对比 Vuex 3.x/4.x 发生了哪些变化?
- mutation 已经被弃用,状态操作更直接
- 设计上提供一个扁平架构,“不再有可命名模块和嵌套结构模块”
- API 设计上尽可能地利用 TS 类型推导
- 无过多的 **”魔法字符串“ 注入,只需要导入函数并调用它们,然后享受自动补全的乐趣就好
1.1 安装
# yarn 方式
yarn add pinia
# npm 方式
npm install pinia
Pinia 默认为 Vite + Vue3 组合式 API 提供量身定制的 Store 状态管理。如果 Vue 版本低于 2.7,请安装组合式 API 包:@vue/composition-api。如果你正在使用 Vue CLI,你可以试试这个非官方插件。
1.2 注册 pinia 实例到全局
# main.js
import { createApp } from 'vue'
// 引入 createPinia 函数
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 pinia 实例(根 store)
const pinia = createPinia()
// 创建应用实例
const app = createApp(App)
// 应用中使用 pinia 插件
app.use(pinia)
// 挂载根组件
app.mount('#app')
Vue2 版本还需要引入插件 PiniaPlugin。
1.3 创建一个 Store
Store(如:Pinia)是一个保存状态和业务逻辑的实体,不与组件树绑定。它承载着全局状态(整个应用中访问的数据),每个引入它的组件都可以对其进行读写操作。
Store 中有三个类似组件的概念:state(data)、getter(computed)、action(methods)。
需要注意的是,并不是所有的数据都要放到 Store 中处理,像购物车、用户登录状态等这些需要快速处理跨组件之间的状态,使用 Store 更加便捷;而像控制元素是否可见这种组件内状态,则不需要 Store 了。
举例描述 Store 管理用户登录状态的过程:
当用户登录时,我们可以在状态管理中记录该用户的信息,以便在后续的用户操作中使用。如果用户在网站上注销,则可以清除该用户的状态。通过使用Pinia,开发人员可以轻松地管理用户的登录状态,并在整个应用程序中验证用户身份。
创建 Store 代码示例(如果没有基础,可以跳过这里):
# src/stores/store-demo.js
import { defineStore } from 'pinia'
const filters = {
FINISHED: 'finished',
UNFINISHED: 'unfinished'
}
export const useTodosStore = defineStore('todos', {
state: () => ({
// @type {{id: number, text: string,isFinished: boolean}[]}
todos: [],
// @type {'all' | 'finished' | 'unfinished'}
filter: 'all',
// 类型将自动推断为 number
id: 0
}),
getters: {
// 过滤列表中自动补全的数据
finishedTodos(state) {
return state.todos.filter(todo => todo.isFinished)
},
// 过滤列表中非自动补全的数据
unfinishedTodos(state) {
return state.todos.filter(todo => !todo.isFinished)
},
// 根据 state.filter 结果过滤数据
// @returns {{ id: number, text: string, isFinished: Boolean }[]}
filteredTodos(state) {
console.log(this, this === state) // Proxy(Object) {$id: 'todos', $onAction: ƒ, $patch: ƒ, …} true
if (this.filter === filters.FINISHED) {
// 调用其它带有自动补全的 getters
return this.finishedTodos
} else if (this.filter === filters.UNFINISHED) {
return this.unfinishedTodos
}
return this.todos
}
},
actions: {
// 接受任何数量的参数,返回一个 Promise 或不返回
addTodo(text) {
// 状态变更
this.todos.push({ id: this.id++, text, isFinished: [true, false][parseInt(Math.random() * 2)] })
}
}
})
1.4 组件内使用 Store
<script setup>
import { ref } from 'vue'
import { useTodosStore } from '@/stores/index'
const todos = useTodosStore()
let todoList = ref(todos.todos)
const add = () => {
todos.addTodo('测试')
todoList.value = todos.todos
}
const getFinished = () => {
todoList.value = todos.finishedTodos
}
const getUnfinished = () => {
todoList.value = todos.unfinishedTodos
}
const getRanFilter = () => {
const ranFilter = ['all','finished','unfinished'][parseInt(Math.random() * 3)]
if (todos.filter === ranFilter) return getRanFilter()
todos.filter =ranFilter
todoList.value = todos.filteredTodos
console.log(todos.id, todos.filter,todos.todos)
}
</script>
<template>
<div class="wrap">
<button @click="add">添加 todo</button>
<button @click="getFinished">自动补全</button>
<button @click="getUnfinished">非自动补全</button>
<button @click="getRanFilter">随机过滤</button>
<ul>
<li
v-for="item in todoList"
:key="item.id">
{{ item.text }} ~ {{ item.id }} ~ {{ item.isFinished ? '自动补全' : '非自动补全'}}
</li>
</ul>
</div>
</template>
<style scoped>
.wrap {
padding: 20px;
}
</style>
通过上面演示的代码,对比 Vuex,Pinia 真是太香了!
页面效果:
二、Pinia 核心概念展开学习
Store 的定义和使用
要讲的核心概念其实就是 Store 中的配置信息,而 Store 是通过 defineStore() 来定义的,它接受两个参数:
- 第一个参数是 Store 的名字,它必须是独一无二的(当前应用中 Store 的唯一 ID,Pinia 将用它来连接 store 和 devtools)
- 第二个参数是 Store 中的配置信息,它涵盖三个部分:State、Getter、Action。可接受两类值:Setup 函数或 Option 对象
defineStore() 函数执行后的返回值建议以 “use开头”、“store结尾” 的变量进行接收,当然你也可以采用任意你喜欢的方式进行命名。
定义 Store
Option 对象的方式配置:(类似于 Vuex 的写法)
与 Vue 的选项式 API 类似,传入一个带有 state
、actions
与 getters
属性的 Option 对象
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count:0 }),
getters: {
doubleCount(state){
return state.count * 2
}
},
actions: {
increment(){
this.count++
}
}
})
可以认为
state
是 store 的数据 (data
),getters
是 store 的计算属性 (computed
),而actions
则是方法 (methods
)。当前 Store 中,非箭头函数中的 this 等价于 state
Setup 函数的方式:
与 Vue 组合式 API 的 setup 函数 相似,可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
import { ref } frrom 'vue'
export const useCounterStore = defineStore('counter', {
const count = ref(0)
function increment() {
count.value++
}
return { count, incerement }
}
在 Setup Store 中:
ref()
就是state
属性computed()
就是getters
function()
就是actions
Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。但是,使用组合式函数会让 SSR 变得更加复杂。所以在不同的渲染需求下,选择你觉得最舒服的那一种方式定义 Store 就好。
你可以定义任意多的 store,但为了让使用 pinia 的益处最大化(比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store。
**在组件中使用 Store **
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
counterStore.increment()
console.log(counterStore.count)
</script>
<template>
<div>{{ counterStore.doubleCount }}</div>
</template>
一旦 store 被实例化,你可以直接访问在 store 的 state
、getters
和 actions
中定义的任何属性。
store
是一个用 reactive
包装的对象,这意味着不需要在 getters 后面写 .value
,就像 setup
中的 props
一样,如果你写了,也不能被解构(破坏了响应性)。
为了从 store 中提取属性时保持其响应性,你可以使用 storeToRefs()
,它将为每一个响应式属性创建引用;而 action 则可以直接从 store 中解构。
<script setup>
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
const { increment } = counterStore
</script>
2.1 State
作为 store 的核心,Pinia 将 state 定义为一个返回初始状态的函数。可以同时支持服务端和客户端
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
// 为了完整类型推理,推荐使用箭头函数
state: () => {
return {
// 所有这些属性都将自动推断出它们的类型
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: true,
}
},
})
2.2 Getter
Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore()
中的 getters
属性来定义它们。推荐使用箭头函数,并且它将接收 state
作为第一个参数:
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})
大多数时候,getter 仅依赖 state,不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this
访问到整个 store 实例,但(在 TypeScript 中)必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this
的 getter。
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注
return this.doubleCount + 1
},
},
})
然后你可以直接访问 store 实例上的 getter 了:
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>
2.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
}
},
},
})
你也完全可以自由地设置任何你想要的参数以及返回任何结果。当调用 action 时,一切类型也都是可以被自动推断出来的。
Action 可以像函数或者通常意义上的方法一样被调用:
<script setup>
const store = useCounterStore()
// 将 action 作为 store 的方法进行调用
store.randomizeCounter()
</script>
<template>
<!-- 即使在模板中也可以 -->
<button @click="store.randomizeCounter()">Randomize</button>
</template>
附:
1. 什么是魔法字符串?
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。
风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
function btnHandle (type) {
if (type === 'delete') {
// 删除操作代码
}
// 其它操作判断代码
}
btnHandle('delete')
上述函数中的 ”delete“ 就是魔法字符串。尽管这是一种不太友好的代码书写习惯,但是在日常开发过程中,还是随处可见的。
当字符串与代码融合在一起并出现多次,就与代码形成了 ”强耦合“,这是不利于项目未来的修改和维护的。所以尽量消除这种它,而消除的办法就是把它定义成一个变量。
const DELETE = 'delete'
function btnHandle (type) {
if (type === DELETE) {
// 删除操作代码
}
// 其它操作判断代码
}
btnHandle(DELETE)
如果同一个操作中,设计多个魔法字符串可以通过对象处理:
const types = {
ADD: 'add',
EDIT: 'edit',
DELETE: 'delete',
DOWNLOAD: 'download'
}
function btnHandle (type) {
seitch (type) {
case types.ADD:
// 添加操作
break;
case types.EDIT:
// 编辑操作
break;
case types.DELETE:
// 删除操作
break;
case types.DOWNLOAD:
// 下载操作
break;
}
}