揭秘 Vue.js 组件通信:provide 和 inject 的魔法
在 Vue.js 中,紧密耦合的组件通常共享状态或行为,这使得它们彼此依赖。
例如:Avatar
和 AvatarGroup
,Tabs
和 TabPanel
,Accordion
和 AccordionPanel
。
这种方式在某些模式和用例中很有用,例如使用 provide
和 inject
在父子组件之间建立通信桥梁。本文将探讨如何在 Vue.js 中使用 provide
和 inject
创建紧密耦合的组件,并以 VTabs
和 VTabPanel
为例进行说明。
什么是 Provide 和 Inject?
Vue 核心库提供的 provide
和 inject
函数允许祖先组件(组件树中更高的组件)向其所有后代组件提供数据,无论组件树有多深。这对于构建组件库和创建可重用的、相互关联的组件特别有用。
-
provide
:在提供者组件中使用,指定要提供给后代组件的数据。 -
inject
:在消费者组件中使用,访问提供的数据(可选地,如果没有任何祖先提供指定的数据,它也可以定义默认值)。
示例用例:vTabs 和 vTabPanel 组件
让我们实现一个 VTabs
组件和 VTabPanel
组件,可以像这样使用:
VTabs>
VTabPanel title="Tab 1">
p>Tab 1 Contentp>
VTabPanel>
VTabPanel title="Tab 2">
p>Tab 2 Contentp>
VTabPanel>
VTabPanel title="Tab 3">
p>Tab 3 Contentp>
VTabPanel>
VTabs>
生成以下结果:
这种方法的优点在于,我们可以将活动选项卡的状态保存在一个地方(Vtabs
),确保一次只有一个选项卡处于打开状态。此外,我们还可以完全控制每个选项卡的内容(利用插槽),并且选项卡名称与选项卡内容位于同一位置(我们不必将选项卡名称定义为 VTabs
的属性,然后将其映射到每个 VTabPanel
)。简单地说,我们的组件 API 非常易于开发者使用。
vTabs 组件定义(提供数据的地方)
在我们的场景中,vTabs
组件应该为其子组件 VTabPanel
提供上下文。这允许 VTabPanel
共享确定哪个选项卡显示给所有面板的状态。它还允许共享任何其他方便共享的数据。
script setup lang="ts">
import { provide, ref, readonly } from "vue";
// 我们提供一个地方来注册所有应该显示的选项卡
// 这样我们就可以遍历它们并为每个选项卡显示可点击的选项卡
// 它最初是空的,因为我们让子组件 `vTabPanel` 确定选项卡标题(基于它们的 "title" 属性)
const tabs = ref([]);
// 这是跟踪单个活动选项卡的状态
// 一次只能打开一个
// 我们将通过在此处设置活动选项卡的标题来确定哪个选项卡应该打开
const activeTab = ref();
// 这是一个设置活动选项卡的辅助函数。
// 如果我们想允许子组件 `vTabPanel` 设置活动选项卡,将其包装在一个函数中很重要
// 为什么?因为 Vue 文档中建议
// 将对提供状态的直接修改保留在定义状态的组件(父组件)中。
// 如果你想允许子组件进行修改,你应该提供一个函数。
// 这使得父组件可以控制状态,只允许子组件以允许的方式修改状态
function activateTab(title: string) {
activeTab.value = title;
}
// 此函数将允许子组件 `vTabPanels` 向父组件 `vTabs` 注册其标题
// 同样,由于上述原因,它是一个函数。
function registerTab(title: string) {
if (tabs.value.includes(title)) return;
tabs.value.push(title);
}
// 这就是神奇的地方。
// provide 函数将数据暴露给子组件
// 注入键是一个唯一的标识符,以便我们可以
// 在子组件中使用相同的键 "拾取" 数据
provide(injectionKey, {
// 这只是一种在子组件中检查 `vTabPanel` 是否正确使用在 `vTabs` 组件上下文中的好方法
withinTabs: true,
// 我们将上面定义的两个函数暴露给子组件
registerTab,
activateTab,
// 我们将活动选项卡暴露给子组件
// 但注意我们使用 readonly 来防止子组件直接修改它
activeTab: readonly(activeTab),
});
script>
script lang="ts">
import type { InjectionKey, Ref } from "vue";
export const injectionKey = Symbol("vTabs") as InjectionKey withinTabs: boolean;
registerTab: (title: string) => void;
activeTab: Readonlyundefined>>;
activateTab: (title: string) => void;
}>;
script>
template>
div class="tabs">
div class="tab-trigger-wrapper">
button
v-for="tab in tabs"
:key="tab"
class="tab-trigger"
:class="{
active: activeTab === tab,
}"
@click="activateTab(tab)"
>
{{ tab }}
button>
div>
div class="tab-content-wrapper">
slot>slot>
div>
div>
template>
VTabPanel 组件(注入提供的数据的地方)
VTabPanel
组件将注入父组件提供的数据,并使用它来:
-
向父组件注册其 title
作为选项卡 -
检查它是否在 vTabs
组件中使用(如果不是,则抛出一个错误,因为选项卡面板在vTabs
之外没有意义) -
在加载时激活第一个选项卡 -
根据 activeTab
状态有条件地显示选项卡内容
script setup lang="ts">
import { inject, ref, readonly, computed } from "vue";
// 注意,我们从 `vTabs` 组件导入注入键
// 因为它是一个符号,我们可以绝对确定它是唯一的
// 而且由于这些组件紧密耦合,从父组件获取它是有意义的
import { injectionKey } from "./vTabs.vue";
// 这是一个简单的 title 属性
const props = defineProps title: string;
}>();
// 这就是神奇的地方
// 我们在这里 "拾取" 父组件提供的数据
// 我们还为与提供的数据类型相同的默认值提供了一些默认值
// 以使 TypeScript 满意并保持我们的 IDE 正确地自动完成
const tabsProvider = inject(injectionKey, {
withinTabs: false,
registerTab: (title: string) => {},
activeTab: readonly(ref()),
activateTab: (title: string) => {},
});
// 如果 withinTabs 为 false,则说明注入的数据未提供
// 为什么?因为 withinTabs 默认为 false
// 所以我们不在 `vTabs` 的上下文中,这不是使用面板组件的有效方式
if (!tabsProvider.withinTabs) {
throw new Error("vTab 必须在 vTabs 组件中使用");
}
// 我们将面板标题推送到父组件,以便它可以正确显示选项卡
tabsProvider.registerTab(props.title);
// 如果没有设置活动选项卡,请继续设置它
// 这只会对第一个面板成立
// 也就是说,第一个面板将始终是默认的活动面板
if (!tabsProvider.activeTab.value) {
tabsProvider.activateTab(props.title);
}
// 最后,只需检查此面板是否应该处于活动状态
// 基于父组件的活动 `activeTab` 状态
const isActive = computed(() => tabsProvider.activeTab.value === props.title);
script>
template>
div class="tab-content" v-show="isActive">
slot>slot>
div>
template>
使用紧密耦合的 Vue 组件
就是这样!使用 provide/inject,我们能够提供一对紧密协作的组件,通过流畅直观的组件 API 提供强大的功能。
以下是再次使用的示例。
VTabs>
VTabPanel title="Tab 1">
p>Tab 1 Contentp>
VTabPanel>
VTabPanel title="Tab 2">
p>Tab 2 Contentp>
VTabPanel>
VTabPanel title="Tab 3">
p>Tab 3 Contentp>
VTabPanel>
VTabs>
紧密耦合组件的优缺点
与代码中的所有设计选择一样,总是有利有弊。在结束之前,让我们简要地提一下这种组件设计策略的优缺点。
优点:
-
封装性: 提供了一种将相关功能封装在一组组件中的方法。 -
可重用性: 可以创建一组可重用的组件,这些组件可以在应用程序的不同部分使用。它们可以独立地反复使用,因为它们不依赖于共享的全局状态。 -
关注点分离: 允许父组件管理状态和行为,而子组件专注于其特定的渲染逻辑。 -
直观的开发体验: 允许使用直观且易于使用的 API。
缺点:
-
紧密耦合: 组件变得紧密耦合,这可能使它们更难独立重用。在大多数用例中,这不是问题,因为你只希望子组件在父组件的上下文中使用。 -
测试: 由于紧密耦合组件的依赖关系,测试它们可能会更复杂。我的建议是一起测试它们,就像它们在现实生活中使用的方式一样。不要尝试单独测试每个组件。
使用 Provide/Inject 实现紧密耦合组件的结论
在 Vue.js 中使用 provide
和 inject
允许你创建可以共享状态和行为的紧密耦合组件。这种模式对于创建可重用的 UI 组件特别有用。虽然这种方法有很多好处,但重要的是要考虑权衡,并确保紧密耦合的合理性。通过仔细设计,provide
和 inject
可以帮助你构建健壮且可维护的 Vue.js 应用程序。