mcpField
S
state-management

state-management

github.com/patternsdev开发258194 次安装29 周前更新
代码仓库

关于 state-management

state-management 是面向 Vue 开发者的应用状态管理指南,介绍从 props 和自定义事件到共享 store 与 Pinia 的选择。它根据应用复杂度说明状态共享方式及各自的取舍。

主要功能

  1. 01说明 props 与自定义事件通信
  2. 02介绍基于 reactive() 的简单 store
  3. 03使用 Pinia 管理应用状态
  4. 04比较不同方案的适用取舍

适用场景

  1. 01在父子组件间传递数据和事件
  2. 02在兄弟或深层组件间共享响应式状态
  3. 03为较大型应用组织状态与操作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill state-management

安装后在 Claude Code 中输入/state-management确认已加载。

详细介绍

状态管理

目录

Vue 组件是 Vue 应用的构建模块,它们让我们能够在组件中组合标记(HTML)、逻辑(JS)和样式(CSS)。

使用场景

  • 需要在同级组件或深层嵌套的组件之间共享响应式数据时使用
  • 除了简单的父子组件 prop 传递外,这也有助于管理全局应用状态

说明

  • 使用 props 实现从父组件到子组件的数据流,并使用自定义事件实现从子组件到父组件的通信
  • 对于小型应用,使用 reactive() 创建简单的 store
  • 对于需要开发者工具、插件和 TypeScript 支持的大型应用,使用 Pinia(官方 Vue 状态管理库)
  • 根据应用的复杂度选择状态管理方式——不要过度设计小型应用

详情

下面是一个单文件组件示例,它会显示一系列来自数据属性的数字:

<template>
  <div>
    <h2>The numbers are {{ numbers }}!</h2>
  </div>
</template>

<script setup>
  import { ref } from "vue";

  const numbers = ref([1, 2, 3]);
</script>

ref() 函数会使组件具备_响应性_。如果模板中使用的响应式属性值发生变化,组件视图就会重新渲染以显示该变化。

如果 numbers 是一个需要在另一个组件中访问的数据值,该怎么办?如果我们想在多个组件之间共享 numbers,它就不再只是组件级数据,_而且_还是应用级数据。这就引出了状态管理这一主题——对应用级数据的管理。

Props

Vue 让我们能够使用 props 将数据从父组件传递给子组件。使用 props 相当简单。基本上,我们只需要在渲染子组件时,将一个值绑定到 prop 属性上即可。

ParentComponent:

<template>
  <div>
    <ChildComponent :numbers="numbers" />
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent";

  const numbers = ref([1, 2, 3]);
</script>

ChildComponent:

<template>
  <div>
    <h2>{{ numbers }}</h2>
  </div>
</template>

<script setup>
  const { numbers } = defineProps(["numbers"]);
</script>
组件事件

如果我们需要以相反的方向传递信息,该怎么办?我们不能使用 props,因为 props 只能用于单向传递数据(从父组件传到子组件)。为了让子组件能够通知父组件某件事情,我们可以使用自定义事件。

Vue 中的自定义事件以原生 CustomEvents 的形式派发,用于组件之间的通信。

ChildComponent:

<template>
  <div>
    <h2>{{ numbers }}</h2>
    <input v-model="number" type="number" />
    <button @click="$emit('number-added', Number(number))">
      Add new number
    </button>
  </div>
</template>

<script setup>
  const { numbers } = defineProps(["numbers"]);
</script>

ParentComponent:

<template>
  <div>
    <ChildComponent :numbers="numbers" @number-added="(n) => numbers.push(n)" />
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent";

  const numbers = ref([1, 2, 3]);
</script>
简单的状态管理

我们可以使用 props 向下传递数据,并使用自定义事件向上发送消息。那么,如何在两个不同的同级组件之间传递数据或进行通信呢?

一种简单的应用级状态管理方式是创建一种 store 模式,让组件共享数据存储。store 可以管理应用的状态,以及负责更改状态的方法。

import { reactive } from "vue";

export const store = reactive({
  numbers: [1, 2, 3],
  addNumber(newNumber) {
    this.numbers.push(newNumber);
  },
});

store 包含一个 numbers 数组和一个 addNumber 方法。该方法接收一个 payload,并直接更新 store 中的 numbers 值。

使用 Vue 3.x,我们可以导入并使用 reactive() 函数,从 JavaScript 对象声明响应式状态。当通过 addNumber() 方法更改此响应式状态时,任何使用该状态的组件都会自动更新!

NumberDisplay:

<template>
  <div>
    <h2>{{ store.numbers }}</h2>
  </div>
</template>

<script setup>
  import { store } from "../store.js";
</script>

NumberSubmit:

<template>
  <div>
    <input v-model="numberInput" type="number" />
    <button @click="store.addNumber(numberInput)">Add new number</button>
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import { store } from "../store.js";

  const numberInput = ref(0);
</script>

这里说组件彼此交互时,“交互”一词的含义比较宽泛。组件不会对彼此执行任何操作,而是通过 store 来触发彼此相关的更改。

仔细看看所有直接与 store 交互的部分,我们可以归纳出一种模式:

  • NumberSubmit 中的方法负责直接调用 store 方法,因此我们可以称其为 store action。
  • store 方法也有特定的职责——直接修改 store 状态。因此,我们称其为 store mutation。
  • NumberDisplay 并不关心 store 或 NumberSubmit 中有哪些类型的方法,它只关心从 store 获取信息。因此,我们可以说 NumberDisplay 可以视为 store getter。

action 提交 mutation。mutation 修改状态,进而影响视图/组件。视图/组件通过 getter 获取 store 数据。我们正逐渐接近一种更有条理的应用级状态管理方式。

Pinia

Pinia 是 Vue.js 的一种状态管理模式和库,提供了更有条理、可扩展性更强的应用级状态管理方式。

Pinia 是 Vuex 等其他状态管理解决方案的替代方案,现在也是 Vue 官方的状态管理库。它提供了一种简单高效的方式来创建和管理 store,用于封装状态、action 和 getter。

在 Pinia 中,我们可以使用 defineStore() 函数定义 store。这里使用 Composition API 语法定义 useNumbersStore() 函数,以创建一个 numbers store。

import { ref } from "vue";
import { defineStore } from "pinia";

export const useNumbersStore = defineStore("numbers", () => {
  const numbers = ref([1, 2, 3]);

  function addNumber(newNumber) {
    numbers.value.push(newNumber);
  }

  return { numbers, addNumber };
});

接下来,我们可以创建一个 Pinia 实例,并将其安装到 Vue 应用中。

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount("#app");

在 NumberDisplay 组件中:

<template>
  <div>
    <h2>{{ store.numbers }}</h2>
  </div>
</template>

<script setup>
  import { useNumbersStore } from "../store";

  const store = useNumbersStore();
</script>

在 NumberSubmit 组件中:

<template>
  <div>
    <input v-model="numberInput" type="number" />
    <button @click="store.addNumber(numberInput)">Add new number</button>
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import { useNumbersStore } from "../store";

  const store = useNumbersStore();
  const numberInput = ref(0);
</script>

对于这样简单的实现,Pinia store 可能并非必要,其行为与使用 reactive() 函数创建的 store 非常相似。尽管如此,Pinia 还提供了适用于更复杂使用场景的其他功能,例如通过插件扩展 Pinia 功能、支持 devtools,以及提供更完善的 TypeScript 支持和服务端渲染支持。

哪种方式才正确?

每种应用级状态管理方式都有各自的优缺点。

简单 Store
  • 优点:相对容易建立。
  • 缺点:状态及可能发生的状态更改没有明确定义。
Pinia
  • 优点:支持 devtools、插件、TypeScript 和服务端渲染
  • 缺点:需要编写额外的样板代码。

归根结底,我们需要了解应用的需求,并判断哪种方式最合适。

来源

参考资料

评论

支持 Markdown