本文是系列文章的一部分:框架实战指南 - 基础知识
组件非常棒。它们让你的代码逻辑更加模块化,并将该逻辑与相关的 DOM 节点集合关联起来。更重要的是,组件是可组合的;你可以将两个组件组合起来,构建一个同时使用这两个组件的第三个组件。
有时,在构建组件时,您可能会发现需要在多个组件之间共享逻辑。
我们不是在谈论在同一组件的实例之间共享状态或逻辑:
IE:同一组件的两个实例共享相同的数据。
相反,我们正在讨论一种为组件的每个实例共享逻辑的方法。
IE:同一组件的两个实例有各自的数据。
例如,假设您有一些组件代码可以检测当前窗口大小。虽然乍一看这似乎是一个简单的问题,但它需要您:
- 获取初始窗口大小并与组件共享该数据
- 添加和清理用户调整浏览器窗口大小时的事件监听器
- 在其他共享逻辑(例如
onlyShowOnMobile布尔值)中编写窗口大小调整逻辑
组件之间共享此逻辑的方法因框架而异。
| 框架 | 逻辑共享方法 |
|---|---|
| 反应 | 自定义钩子 |
| 角度 | 信号函数 |
| Vue | 作品 |
我们将用本章来讨论如何做到这一切,并了解如何将这些方法应用到生产代码中。
但我最喜欢的是这些方法:我们不需要引入任何新的 API 来使用它们。相反,我们将结合迄今为止学到的其他 API 进行整合。
不用多说,让我们构建窗口大小共享逻辑。
共享数据存储方法
创建可组合共享逻辑的第一步是创建一种在逻辑实例中存储数据的方法:
因为 Vue 的ref数据reactive反应系统可以在任何地方工作,所以我们可以将这些值提取到名为的专用函数中useWindowSize。
// use-window-size.js
import { ref } from "vue";export const useWindowSize = () => { const height = ref(window.innerHeight); const width = ref(window.innerWidth); return { height, width };};
script这个自定义函数通常被称为“组合”,因为我们在其中使用了 Vue 的 Composition API。我们可以像这样在设置中使用这个组合:
<!-- App.vue --><script setup>import { useWindowSize } from "./use-window-size";const { height, width } = useWindowSize();</script><template> <p>The window is {{ height }}px high and {{ width }}px wide</p></template>
虽然 React 要求你将自定义 hooks 命名为“useX”,但自定义组合则无需如此。我们可以轻松地调用这段代码
createWindowSize,并使其同样有效。我们仍然使用
useComposition 前缀来保持可读性。虽然这比较主观,但生态系统似乎更倾向于使用这种命名规范。
共享副作用处理程序
虽然在使用组件之间共享数据本身是有帮助的,但这只是这些框架跨组件逻辑重用功能的一小部分。
组件之间可以重复使用的最强大的功能之一是副作用逻辑。
利用这一点,我们可以得出如下结论:
当实现此共享代码的组件呈现时,执行此行为。
并将其与我们的数据存储结合起来:
当组件渲染时,存储一些计算并将其公开给使用组件。
如果没有代码,讨论起来可能会有点模糊,所以让我们深入研究一下。
虽然我们上一个代码示例能够显示浏览器窗口的高度和宽度,但它无法响应窗口大小调整。这意味着,如果您调整浏览器窗口的大小,height和的值width将不再准确。
让我们使用我们在“副作用”一章中构建的窗口监听器副作用来添加一个事件处理程序来监听窗口大小的调整。
在自定义组合中共享副作用处理与在组件中使用它们一样简单。我们可以像在 中使用一样,简单地使用onMounted相同的生命周期方法。onUnmounted``setup script
// use-window-size.js
import { onMounted, onUnmounted, ref } from "vue";export const useWindowSize = () => { const height = ref(window.innerHeight); const width = ref(window.innerWidth); function onResize() { height.value = window.innerHeight; width.value = window.innerWidth; } onMounted(() => { window.addEventListener("resize", onResize); }); onUnmounted(() => { window.removeEventListener("resize", onResize); }); return { height, width };};
<!-- App.vue --><script setup>import { useWindowSize } from "./use-window-size";const { height, width } = useWindowSize();</script><template> <p>The window is {{ height }}px high and {{ width }}px wide</p></template>
我们也可以使用
watch或watchEffect组合方法,但在这个例子中我们选择不这样做。
编写自定义逻辑
我们已经介绍了共享逻辑如何访问数据存储和副作用处理程序。现在我们来谈谈更有趣的部分:可组合性。
您不仅可以从组件调用自定义逻辑,还可以从其他共享逻辑片段调用它们。
例如,假设我们想要采用窗口大小获取器并创建另一个组成它的自定义逻辑片段。
如果我们使用普通的 ole 函数,它可能看起来像这样:
function getWindowSize() { return { height: window.innerHeight, width: window.innerWidth, };}function isMobile() { const { height, width } = getWindowSize(); if (width <= 480) return true; else return false;}
但是,当尝试将此逻辑纳入框架时,也会带来一些缺点,例如:
- 无法清除副作用
height或width更改时不会自动重新渲染
幸运的是,我们可以使用我们的框架来实现这一点,并且可以完全访问迄今为止我们介绍的所有其他特定于框架的 API。
由于自定义可组合项的行为类似于普通函数,因此编写自定义可组合项(速度提高 10 倍)是一项简单的任务。
// use-mobile-check.js
import { computed } from "vue";import { useWindowSize } from "./use-window-size";export const useMobileCheck = () => { const { height, width } = useWindowSize(); const isMobile = computed(() => { if (width.value <= 480) return true; else return false; }); return { isMobile };};
请注意,我们不会
useWindowSize再次显示源代码,那是因为我们没有改变它!
然后,为了在我们的组件中使用这个新的可组合项,我们像使用以前的可组合项一样使用它:
<!-- App.vue --><script setup>import { useMobileCheck } from "./use-mobile-check";const { isMobile } = useMobileCheck();</script><template> <p>Is this a mobile device? {{ isMobile ? "Yes" : "No" }}</p></template>
挑战
让我们利用所学的有关共享组件逻辑的所有知识,以更小的部分重新创建“组件参考”一章中的组件。ContextMenu
让我们将这些组件分解成更小的部分,并为其创建可组合的逻辑:
- 监听上下文菜单之外的点击
- 获取上下文菜单父元素边界的组合
步骤 1:创建外部点击构图
要监听上下文菜单之外的点击,我们可以利用类似于以下内容的 JavaScript:
const closeIfOutsideOfContext = (e) => { const isClickInside = ref.value.contains(e.target); if (isClickInside) return; closeContextMenu();};document.addEventListener("click", closeIfOutsideOfContext);
让我们将其转变为可以在我们的ContextMenu组件中使用的组合。
// use-outside-click.js
import { onMounted, onUnmounted } from "vue";export const useOutsideClick = ({ ref, onClose }) => { const closeIfOutsideOfContext = (e) => { const isClickInside = ref.value.contains(e.target); if (isClickInside) return; onClose(); }; onMounted(() => { document.addEventListener("click", closeIfOutsideOfContext); }); onUnmounted(() => { document.removeEventListener("click", closeIfOutsideOfContext); });};
然后,我们可以在我们的ContextMenu组件中使用这个组合:
<!-- ContextMenu.vue --><script setup>import { onMounted, onUnmounted, ref } from "vue";import { useOutsideClick } from "./use-outside-click";const props = defineProps(["x", "y"]);const emit = defineEmits(["close"]);const contextMenuRef = ref(null);useOutsideClick({ ref: contextMenuRef, onClose: () => emit("close") });function focusMenu() { contextMenuRef.value.focus();}defineExpose({ focusMenu,});</script><template> <div tabIndex="0" ref="contextMenuRef" :style="{ position: 'fixed', top: props.y + 20, left: props.x + 20, background: 'white', border: '1px solid black', borderRadius: 16, padding: '1rem', }" > <button @click="$emit('close')">X</button> This is a context menu </div></template>
第 2 步:创建 Bounds 可组合项
现在,我们将边界大小的检查也移到可组合函数中。在 JavaScript 中,如下所示:
const resizeListener = () => { if (!el) return; const localBounds = el.getBoundingClientRect(); setBounds(localBounds);};resizeListener();window.addEventListener("resize", resizeListener);window.removeEventListener("resize", resizeListener);
// use-bounds.js
import { ref, onMounted, onUnmounted } from "vue";export const useBounds = () => { const elRef = ref(); const bounds = ref({ height: 0, width: 0, x: 0, y: 0, }); function resizeListener() { if (!elRef.value) return; bounds.value = elRef.value.getBoundingClientRect(); } onMounted(() => { resizeListener(); window.addEventListener("resize", resizeListener); }); onUnmounted(() => { window.removeEventListener("resize", resizeListener); }); return { bounds, ref: elRef };};
最后,我们将在以下位置使用这个组合App:
<!-- App.vue --><script setup>import { onMounted, onUnmounted, ref } from "vue";import ContextMenu from "./ContextMenu.vue";import { useBounds } from "./use-bounds";const isOpen = ref(false);const { ref: contextOrigin, bounds } = useBounds();const contextMenu = ref();function close() { isOpen.value = false;}function open(e) { e.preventDefault(); isOpen.value = true; setTimeout(() => { contextMenu.value.focusMenu(); }, 0);}</script><template> <div :style="{ marginTop: '5rem', marginLeft: '5rem' }"> <div ref="contextOrigin" @contextmenu="open($event)"> Right click on me! </div> </div> <ContextMenu ref="contextMenu" v-if="isOpen" :x="bounds.x" :y="bounds.y" @close="close()" /></template>