状态范围
在设计 Vue 应用程序(或者实际上,任何基于组件的应用程序)时,有不同类型的数据取决于我们正在处理的问题,并且每个都有自己首选的通信渠道。
全局状态:可能包括登录用户、当前主题等。
本地状态:表单属性、禁用按钮状态等。
请注意,全局状态的一部分可能在某个时候最终进入本地状态,并且它可以像任何其他本地状态一样传递给子组件,无论是完全还是稀释以匹配用例。
沟通渠道
通道是一个松散的术语,我将使用它来指代围绕 Vue 应用程序交换数据的具体实现。
每个实现都针对特定的通信渠道,其中包括:
不同的关注点与不同的沟通渠道有关。
道具:直接亲子
Vue 中用于单向数据绑定的最简单的通信通道。
事件:直接子父
$emit
和$on
。最简单的直接儿童与父母沟通的沟通渠道。事件启用 2-way 数据绑定。
提供/注入:全球或遥远的本地状态
在 Vue 2.2+ 中添加,与 React 的上下文 API 非常相似,可以用作事件总线的可行替代品。
在组件树中的任何点,组件都可以提供一些数据,任何下线的子都可以通过inject
组件的属性访问这些数据。
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})
这可用于在应用程序的根部提供全局状态,或在树的子集中提供本地化状态。
集中存储(全局状态)
Vuex是 Vue.js 应用程序的状态管理模式 + 库。它充当应用程序中所有组件的集中存储,其规则确保状态只能以可预测的方式发生变化。
现在你问:
[S]我应该为每个次要通信创建 vuex 存储吗?
在处理全局状态时它真的很出色,包括但不限于:
- 从后端接收的数据,
- 像主题一样的全局 UI 状态,
- 任何数据持久层,例如保存到后端或与本地存储接口,
- 吐司消息或通知,
- 等等
因此,您的组件可以真正专注于它们应该成为的东西,管理用户界面,而全局商店可以管理/使用一般业务逻辑并通过getter和action提供清晰的 API 。
这并不意味着您不能将它用于组件逻辑,但我个人会将该逻辑范围限定为仅具有必要的全局 UI 状态的命名空间Vuex 模块。
为避免在全局状态下处理混乱的所有内容,请参阅应用程序结构建议。
参考和方法:边缘案例
尽管存在 props 和 events,但有时您可能仍需要直接访问 JavaScript 中的子组件。
它仅作为直接子操作的逃生舱口- 您应该避免$refs
从模板或计算属性中访问。
如果您发现自己经常使用 refs 和子方法,那么可能是时候提升状态或考虑此处或其他答案中描述的其他方式了。
与 类似$root
,该$parent
属性可用于从子级访问父级实例。这可能很容易成为使用道具传递数据的懒惰替代方案。
在大多数情况下,深入到父级会使您的应用程序更难调试和理解,尤其是当您更改父级中的数据时。稍后查看该组件时,将很难弄清楚该突变来自何处。
实际上,您可以使用$parent
,$ref
或来导航整个树结构$root
,但这类似于将所有内容都全局化,并且可能会变成无法维护的意大利面条。
事件总线:全局/远程本地状态
有关事件总线模式的最新信息,请参阅@AlexMA 的答案。
这是过去的模式,将 props 从远处传递到深度嵌套的子组件,中间几乎没有其他组件需要这些。谨慎使用精心挑选的数据。
注意:随后创建的将自身绑定到事件总线的组件将被绑定多次——导致多个处理程序被触发和泄漏。我个人从来没有觉得在我过去设计的所有单页应用程序中都需要事件总线。
下面演示了一个简单的错误如何导致Item
即使从 DOM 中移除组件仍会触发的泄漏。
// A component that binds to a custom 'update' event.
var Item = {
template: `<li>{{text}}</li>`,
props: {
text: Number
},
mounted() {
this.$root.$on('update', () => {
console.log(this.text, 'is still alive');
});
},
};
// Component that emits events
var List = new Vue({
el: '#app',
components: {
Item
},
data: {
items: [1, 2, 3, 4]
},
updated() {
this.$root.$emit('update');
},
methods: {
onRemove() {
console.log('slice');
this.items = this.items.slice(0, -1);
}
}
});
<script src="https://unpkg.com/vue@2.5.17/dist/vue.min.js"></script>
<div id="app">
<button type="button" @click="onRemove">Remove</button>
<ul>
<item v-for="item in items" :key="item" :text="item"></item>
</ul>
</div>
destroyed
请记住在生命周期挂钩中删除侦听器。
组件类型
免责声明:以下“容器”与“展示”组件只是构建项目的一种方式,现在有多种选择,例如可以有效替换我在下面描述的“应用程序特定容器”的新组合 API 。
为了协调所有这些通信,以简化可重用性和测试,我们可以将组件视为两种不同的类型。
同样,这并不意味着应该重用通用组件或不能重用特定于应用程序的容器,但它们有不同的职责。
应用特定容器
注意:请参阅新的Composition API作为这些容器的替代方案。
这些只是包装其他 Vue 组件(通用或其他应用程序特定容器)的简单 Vue 组件。这是 Vuex store 通信应该发生的地方,并且这个容器应该通过其他更简单的方式进行通信,例如 props 和事件侦听器。
这些容器甚至可以完全没有原生 DOM 元素,让通用组件处理模板和用户交互。
以某种方式作用域events
或stores
兄弟组件的可见性
这是范围界定发生的地方。大多数组件不知道 store,并且这个组件应该(大部分)使用一个命名空间的 store 模块,该模块具有一组有限的getters
并actions
与提供的Vuex binding helpers一起应用。
通用/展示组件
这些应该从 props 接收数据,对自己的本地数据进行更改,并发出简单的事件。大多数时候,他们根本不应该知道 Vuex 商店的存在。
它们也可以称为容器,因为它们的唯一职责可能是分派给其他 UI 组件。
兄弟姐妹交流
那么,在这一切之后,我们应该如何在两个兄弟组件之间进行通信呢?
举个例子更容易理解:假设我们有一个输入框,它的数据应该在应用程序之间共享(树中不同位置的兄弟姐妹)并通过后端持久保存。
❌ 混合关注点
从最坏的情况开始,我们的组件将混合表示和业务逻辑。
// MyInput.vue
<template>
<div class="my-input">
<label>Data</label>
<input type="text"
:value="value"
:input="onChange($event.target.value)">
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
value: "",
};
},
mounted() {
this.$root.$on('sync', data => {
this.value = data.myServerValue;
});
},
methods: {
onChange(value) {
this.value = value;
axios.post('http://example.com/api/update', {
myServerValue: value
});
}
}
}
</script>
虽然它对于一个简单的应用程序来说可能看起来不错,但它有很多缺点:
- 显式使用全局 axios 实例
- UI 内的硬编码 API
- 与根组件紧密耦合(事件总线模式)
- 更难做单元测试
✅ 关注点分离
为了分离这两个问题,我们应该将我们的组件包装在一个特定于应用程序的容器中,并将表示逻辑保留在我们的通用输入组件中。
使用以下模式,我们可以:
- 使用单元测试轻松测试每个问题
- 在完全不影响组件的情况下更改 API
- 随心所欲地配置 HTTP 通信(axios、获取、添加中间件、测试等)
- 在任何地方重用输入组件(减少耦合)
- 通过全局商店绑定对应用程序中任何地方的状态更改做出反应
- 等等
我们的输入组件现在是可重用的,并且不知道后端也不知道兄弟姐妹。
// MyInput.vue
// the template is the same as above
<script>
export default {
props: {
initial: {
type: String,
default: ""
}
},
data() {
return {
value: this.initial,
};
},
methods: {
onChange(value) {
this.value = value;
this.$emit('change', value);
}
}
}
</script>
我们的应用程序特定容器现在可以成为业务逻辑和表示通信之间的桥梁。
// MyAppCard.vue
<template>
<div class="container">
<card-body>
<my-input :initial="serverValue" @change="updateState"></my-input>
<my-input :initial="otherValue" @change="updateState"></my-input>
</card-body>
<card-footer>
<my-button :disabled="!serverValue || !otherValue"
@click="saveState"></my-button>
</card-footer>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { NS, ACTIONS, GETTERS } from '@/store/modules/api';
import { MyButton, MyInput } from './components';
export default {
components: {
MyInput,
MyButton,
},
computed: mapGetters(NS, [
GETTERS.serverValue,
GETTERS.otherValue,
]),
methods: mapActions(NS, [
ACTIONS.updateState,
ACTIONS.saveState,
])
}
</script>
由于 Vuex 存储操作处理后端通信,因此我们这里的容器不需要了解 axios 和后端。