[译] VueJS 中更好的组件组合方式

简介: [译] VueJS 中更好的组件组合方式

VueJS 中更好的组件组合方式


VueJS 中有一些组合组件并复用逻辑的方法。在本文中,我将展示一种在 Vuejs (2.* 及 3.*) 中改进组合方式的方法。

我的确欣赏最近的 Composition API 提案,但我认为视野还可以更开阔。

下面,你可以看到一个实现了一种常规用例(从远端获取一个简单的数据并将其搭配不同的转场效果显示出来)的组件,尽管大部分逻辑及其相关的模版、数据和其它变量等与出现在其它地方或组件中的相同逻辑并无不同,它们还是出现在了该组件中。


<template>
<div>
    <div v-if="loading"> Loading... </div>
    <div v-if="error"> An Error occured, please try again</div>
    <div v-if="hasData"> {{ data }} </div>
</div>
</template>
</template>
<script>
    export default {
        data() {
            return {
                loading: false,
                error: false,
                data: {}
            }
        },
        methods: {
            fetchData() {
                this.loading = true;
                setTimeout(() => {
                    this.data = { text: 'example' };
                    this.loading = false;
                }, 4000);
            }
        },
        computed: {
            hasData() {
                return this.data && !!this.data.text;
            }
        },
        mounted() {
            this.fetchData();
        }
    }
</script>


该如何重构并改善这个组件呢?让我们一步步地让其更易读且更容易复用。

Vue Composition API

感谢新的 Vue Composition API,使得我们可以在不丢失由 Vue 组件提供的响应性或其它特性的前提下,抽出一些逻辑以来复用它。

这种方式有助于组织代码、让组件更易读,并有助于降低总体复杂度。作为一种建议,我相信这些应该是重构巨大、复杂和混乱的组件时的首要之事。

我们将抽取与获取数据有关的部分及相关的变量(loading、error 等……),但我并不想谈论什么是 Composition API 以及其特性、优点和缺点。

让我们来创建一个提供了获取数据必要功能及若干响应式变量的简单函数:


import { reactive, toRefs, computed, Ref, ComputedRef } from '@vue/composition-api';
interface ReceivedData {
    text?: string
}
interface FetchState {
    loading: boolean,
    error: boolean,
    data: ReceivedData
}
interface FetchDataVars {
    loading: Ref<boolean>;
    error: Ref<boolean>;
    data: Ref<object>;
    fetchData: Function;
    hasData: ComputedRef<boolean>
}
export default (): FetchDataVars => {
    const state = reactive<FetchState>({
        loading: true,
        error: false,
        data: {}
    });
    const fetchData = async () => {
        state.loading = true;
        setTimeout(() => {
            state.data = { text: 'example' };
            state.loading = false;
        }, 4000);
    }
    const hasData = computed(() => state.data && !!state.data.text)
    
    return {
        ...toRefs(state),
        fetchData,
        hasData
    }
}

新创建的函数现在返回了可被用于组件的一组响应式变量 (loading、error、data,及 hasData) 及一个用来执行数据获取任务的异步函数 (fetchData,将会改变上述响应式变量) 。

而后,来使用 Composition API 重构组件:


<template>
<div>
    <div v-if="loading"> Loading... </div>
    <div v-if="error"> An Error occured, please try again</div>
    <div v-if="hasData"> {{ data }} </div>
</div>
</template>
</template>
<script lang="ts">
    import useFetchData from '../composables/use-fetch-data';
    import { defineComponent } from '@vue/composition-api';
    export default defineComponent({
        setup() {
            const { loading, error, data, fetchData, hasData } = useFetchData();
            return {
                loading,
                error,
                data, fetchData,
                hasData
            }
        },
        mounted() {
            this.fetchData();
        }
    });
</script>



正如你所注意到的,我们的组件还包含了 setup 方法,由其调用 useFetchData 函数,同时解构返回的变量和函数并将它们返回给组件实例。

在这个例子中,我在 mounted 生命周期钩子中使用了 fetchData 函数,但其实你可以在期望的任意位置调用它。无论何时,被该函数求值或改变的结果都会反映在组件中,因为它们都是响应式属性。

JSX 和 TSX

现在假设我们想要将获取的数据传递到一个内部组件中。借助 VueJS 有多种实现的方法,但我却想使用  TSX (你若更喜欢 JSX 也行) 来重构代码:


<script lang="tsx">
    import useFetchData from '../composables/use-fetch-data';
    import { defineComponent } from '@vue/composition-api';
    export default defineComponent({
        setup() {
            const { loading, error, data, fetchData, hasData } = useFetchData();
            return {
                loading,
                error,
                data, fetchData,
                hasData
            }
        },
        mounted() {
            this.fetchData();
        },
        render() {
            return (
                <div>
                    { this.loading && <div> Loading ... </div> }
                    { this.error && <div> An Error occured, please try again </div> }
                    { <div> { this.data } </div> }
                </div>
            )
        }
    });
</script>

我知道这看起来很像 React,但我相信这开启了以更好的方法优化组合方式的许多可能之门。

这其实很易懂,它完成了和模板同样的事情,但我们将 HTML 部分移入了 render 函数中。

我们尚未完成将数据传递进内部组件的任务,实际上我们像下面这样改进一点代码就行,也就是将所有东西导出成一个我们可复用的函数:


import useFetchData from '../composables/use-fetch-data';
import { defineComponent } from '@vue/composition-api';
import { Component } from 'vue';
export default (component: Component) => defineComponent({
    setup() {
        const { loading, error, data, fetchData, hasData } = useFetchData();
        return {
            loading,
            error,
            data, fetchData,
            hasData
        }
    },
    mounted() {
        this.fetchData();
    },
    render() {
        const injectedComponentProps = {
            data: this.data
        }
        return (
            <div>
                { this.loading && <div> Loading ... </div> }
                { this.error && <div> An Error occured, please try again </div> }
                <component props={ injectedComponentProps } />
            </div>
        )
    }
});

现在我们已经更上一层楼了,摆脱 SFC (单文件组件 -- Single File Component 文件) 后我们就可以真正的改进组织方式了。

在此阶段,我们使用 defineComponent 创建了一个使用 Composition API 的组件并依托 JSX/TSX 消除了模板部分。这种方式的妙处在于可以将一个组件视为一个函数并自如运用函数式编程范式(如一级函数、纯函数等等……)了。

举例来说,render 函数也包含了一个显示数据的 div,但想象下若将一个组件作为刚才所导出函数的一个参数,并在返回的 JSX/TSX 中使用它(将响应/数据作为属性传递给组件)是如何的呢。

看起来可能会是这样的:


import useFetchData from '../composables/use-fetch-data';
import { defineComponent } from '@vue/composition-api';
import { Component } from 'vue';
export default (component: Component) => defineComponent({
    setup() {
        const { loading, error, data, fetchData, hasData } = useFetchData();
        return {
            loading,
            error,
            data, fetchData,
            hasData
        }
    },
    mounted() {
        this.fetchData();
    },
    render() {
        const injectedComponentProps = {
            data: this.data
        }
        return (
            <div>
                { this.loading && <div> Loading ... </div> }
                { this.error && <div> An Error occured, please try again </div> }
                <component props={ injectedComponentProps } />
            </div>
        )
    }
});

现在我们正期待着将一个组件作为参数并在 render 函数中使用它。

还可以做得更多。

实际上,我们也可以期待将 useFetchData 函数作为所导出函数的一个参数。


import useFetchData from '../composables/use-fetch-data';
import { defineComponent, ComputedRef, Ref } from '@vue/composition-api';
import { Component } from 'vue';
interface FetchDataVars {
    loading: Ref<boolean>;
    error: Ref<boolean>;
    data: Ref<object>;
    fetchData: Function;
    hasData: ComputedRef<boolean>
}
type FetchData = () => FetchDataVars ;
export default (component: Component, factoryFetchData: FetchData) => defineComponent({
    setup() {
        const { loading, error, data, fetchData, hasData } = factoryFetchData();
        return {
            loading,
            error,
            data, fetchData,
            hasData
        }
    },
    mounted() {
        this.fetchData();
    },
    render() {
        const injectedComponentProps = {
            data: this.data
        }
        return (
            <div>
                { this.loading && <div> Loading ... </div> }
                { this.error && <div> An Error occured, please try again </div> }
                <component data={ injectedComponentProps } />
            </div>
        )
    }
});

借助这些改变,在组件之上,接受一个类型为 FetchData 并返回一组符合预期的变量/函数/计算值的 函数 作为参数,就可以使用包装过的新组件。

这是一种依托函数式途径达成的相当有用的替代继承/扩展的方法。所以,不同于扩展已有的组件并覆写组件的函数的是,我们可以真正传入期望的组件和函数了。Typescript 在此仅有助于强类型化和类型推断,所以只用 Javascript 也是足够的。

例如,如果我们想要使用它,看起来会是这样的:


import withLoaderAndFetcher from './components/withLoaderAndFetcher';
import useFetchDataForEndpointOne from './composables/useFetchDataForEndpointOne'
import useFetchDataForEndpointTwo from './composables/useFetchDataForEndpointTwo'
import useFetchDataForEndpointThree from './composables/useFetchDataForEndpointThree'
import ComponentA from './components/ComponentA.vue';
import ComponentB from './components/ComponentB.vue';
import ComponentC from './components/ComponentC.vue';
const composedA = withLoaderAndFetcher(ComponentA, useFetchDataForEndpointOne);
const composedB = withLoaderAndFetcher(ComponentB, useFetchDataForEndpointTwo);
const composedC = withLoaderAndFetcher(ComponentC, useFetchDataForEndpointThree);

我们将上例导出的函数称为 withLoaderAndFetcher 并使用其组合了 3 个不同的组件和 3 个不同的函数(装饰者模式)。

这项工作还能推进得更远,但我想展示的是达到这种状态的可能性并增加趋向函数式组合方式的方法数量。这只是示例代码,也可能不会工作得很好,但这种想法和概念才是要义。


相关实践学习
基于Hologres轻量实时的高性能OLAP分析
本教程基于GitHub Archive公开数据集,通过DataWorks将GitHub中的项⽬、行为等20多种事件类型数据实时采集至Hologres进行分析,同时使用DataV内置模板,快速搭建实时可视化数据大屏,从开发者、项⽬、编程语⾔等多个维度了解GitHub实时数据变化情况。
阿里云实时数仓实战 - 用户行为数仓搭建
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求:熟练掌握 SQL 语法熟悉 Linux 命令,对 Hadoop 大数据体系有一定的了解 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
11月前
|
JavaScript
在 Vue 中处理组件选项与 Mixin 选项冲突的详细解决方案
【10月更文挑战第18天】通过以上的分析和探讨,相信你对在 Vue 中使用 Mixin 时遇到组件选项与 Mixin 选项冲突的解决方法有了更深入的理解。在实际开发中,要根据具体情况灵活选择合适的解决方案,以确保代码的质量和可维护性。
382 7
|
3月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
423 0
|
3月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
5月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
601 4
|
3月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
240 8
|
3月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
158 1
|
3月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
101 0
|
5月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
338 6
|
10月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
232 64
|
10月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
313 64