使用Vue.js编写抽象组件
Vue 组件很棒,对吧? 它们将您的应用程序的视图和行为封装成漂亮的可组合小块。 如果您需要一些额外的功能,只需附加指令! 事情是,指令相当不灵活,不能做所有事情。 例如,指令不能(轻松)发出事件。 嗯,这就是 Vue,当然有解决方案。 抽象组件!
抽象组件与普通组件一样,只是它们不向 DOM 呈现任何内容。 他们只是为现有的行为添加了额外的行为。 您可能熟悉 Vue 内置的抽象组件,例如 <transition>
、<component>
和 <slot>
。
抽象组件的一个很好的用例是跟踪元素何时使用 IntersectionObserver
进入视口。 让我们看一下实现一个简单的抽象组件来处理它。
如果您想要一个适当的生产就绪实现,请查看本教程所基于的 vue-intersect。
入门
首先,我们将创建一个简单地呈现其内容的快速抽象组件。 为此,我们将快速了解 渲染函数 。
IntersectionObserver.vue
export default { // Enables an abstract component in Vue. // This property is undocumented and may change at any time, // but your component should work without it. abstract: true, // Yay, render functions! render() { // Without using a wrapper component, we can only render one child component. try { return this.$slots.default[0]; } catch (e) { throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.'); } return null; } }
恭喜! 你现在有一个抽象组件,好吧,它什么都不做! 它只是渲染它的孩子。
添加 IntersectionObserver
好的,现在让我们坚持 [X39X] 的逻辑。
IntersectionObserver
在 IE 或 Safari 中本机不支持,因此您可能想为它获取 polyfill 。
IntersectionObserver.vue
export default { // Enables an abstract component in Vue. // This property is undocumented and may change at any time, // but your component should work without it. abstract: true, // Yay, render functions! render() { // Without using a wrapper component, we can only render one child component. try { return this.$slots.default[0]; } catch (e) { throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.'); } return null; }, mounted () { // There's no real need to declare observer as a data property, // since it doesn't need to be reactive. this.observer = new IntersectionObserver((entries) => { this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]); }); // You have to wait for the next tick so that the child element has been rendered. this.$nextTick(() => { this.observer.observe(this.$slots.default[0].elm); }); } }
好的,所以现在我们有了一个可以像这样使用的抽象组件:
<intersection-observer @intersect-enter="handleEnter" @intersect-leave="handleLeave"> <my-honest-to-goodness-component></my-honest-to-goodness-component> </intersection-observer>
虽然我们还没有完成……
整理起来
当组件从 DOM 中移除时,我们需要确保不会留下任何悬空的 IntersectionObservers
,所以现在让我们快速修复这个问题。
IntersectionObserver.vue
export default { // Enables an abstract component in Vue. // This property is undocumented and may change at any time, // but your component should work without it. abstract: true, // Yay, render functions! render() { // Without using a wrapper component, we can only render one child component. try { return this.$slots.default[0]; } catch (e) { throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.'); } return null; }, mounted() { // There's no real need to declare observer as a data property, // since it doesn't need to be reactive. this.observer = new IntersectionObserver((entries) => { this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]); }); // You have to wait for the next tick so that the child element has been rendered. this.$nextTick(() => { this.observer.observe(this.$slots.default[0].elm); }); }, destroyed() { // Why did the W3C choose "disconnect" as the method anyway? this.observer.disconnect(); } }
只是为了加分,让我们使用道具使观察者阈值可配置。
IntersectionObserver.vue
export default { // Enables an abstract component in Vue. // This property is undocumented and may change at any time, // but your component should work without it. abstract: true, // Props work just fine in abstract components! props: { threshold: { type: Array } }, // Yay, render functions! render() { // Without using a wrapper component, we can only render one child component. try { return this.$slots.default[0]; } catch (e) { throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.'); } return null; }, mounted() { // There's no real need to declare observer as a data property, // since it doesn't need to be reactive. this.observer = new IntersectionObserver((entries) => { this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]); }, { threshold: this.threshold || 0 }); // You have to wait for the next tick so that the child element has been rendered. this.$nextTick(() => { this.observer.observe(this.$slots.default[0].elm); }); }, destroyed() { // Why did the W3C choose "disconnect" as the method anyway? this.observer.disconnect(); } }
最终的用法如下所示:
<intersection-observer @intersect-enter="handleEnter" @intersect-leave="handleLeave" :threshold="[0, 0.5, 1]"> <my-honest-to-goodness-component></my-honest-to-goodness-component> </intersection-observer>
给你! 你的第一个抽象组件。
非常感谢 Thomas Kjærgaard / Heavyy 最初的 实施和想法 !