专栏首页云前端[译] VueJS 中更好的组件组合方式

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

  • 原文地址:https://itnext.io/better-composition-in-vue-fd35b9fe9c79
  • 原文作者:?Francesco Vitullo
  • 译文出自:?掘金翻译计划
  • 本文永久链接:https://github.com/xitu/gold-miner/blob/master/article/2020/better-composition-in-vue.md
  • 校对者:?Gesj-yean, ?dupanpan

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';

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>
        )
    }
});

现在我们已经更上一层楼了,摆脱 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 个不同的函数(装饰者模式)。

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

干杯 :)

--End--

文章分享自微信公众号:
云前端

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:云前端
原始发表时间:2020-09-23
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 【译】Angular中,向子组件传值的5种方式

    翻译:http://blog.briebug.com/5-ways-to-pass-data-into-child-components-in-angular ...

    申君健
  • Vuejs中父组件主动调用子组件的方法

    我们都知道,vue是单向流,但是有时候我们需要在父组件中主动通知子组件一些信息,使其做出一些响应变化,那么如何在父组件中去主动调用子组件的方法呢?Vue当然给我...

    飞奔去旅行
  • vuejs中的组件以及父子组件间通信传值

    您将在本文当中了解到,往网页中添加数据,从传统的dom操作过渡到数据层操作,实现同一个目标,两种不同的方式.以及什么是组件,如何定义和使用组件,父子组件之间如何...

    itclanCoder
  • Vue3,用组合的方式来编写更好的代码(1/5)

    它们让你把小块的逻辑提取到函数中,我们可以轻松地重复使用,这样的代码更容易编写和阅读。

    前端小智@大迁世界
  • Vue中组件间通信的方式

    这种组件通信的方式是我们运用的非常多的一种,props以单向数据流的形式可以很好的完成父子组件的通信,所谓单向数据流,就是数据只能通过props由父组件流向子组...

    WindrunnerMax
  • React中组件通信的几种方式

    React数据流动是单向的,父组件向子组件通信也是最常见的;父组件通过props向子组件传递需要的信息 Child.jsx

    木子星兮
  • React中组件间通信的方式

    props适用于父子组件的通信,props以单向数据流的形式可以很好的完成父子组件的通信,所谓单向数据流,就是数据只能通过props由父组件流向子组件,而子组件...

    WindrunnerMax
  • React中创建组件的3种方式

    那么问题来了,这三种方式有啥区别呢?这里说明一个问题,很多时候同一种效果往往有很多种实现方式,所以我们在学习的过程中要避免章节化思维,要对技术进行...

    IT人一直在路上
  • Vue中组件最常见通信的方式

      父组件通过prop的方式向子组件传递数据,而通过$emit子组件可以向父组件通信。

    前端迷
  • vue中组件间通信方式的总结

    这是我们比较熟悉的方式,主要是父子组件之间的传递方式,父传子使用props,子传父使用$emit.

    用户9914333
  • Vue3 + Vite2 项目实战复盘总结(干货!)

    •背景•vue3 项目开发 get 到的知识•让我惊讶的 vite•项目中遇到的困难•总结•项目技术栈•资料推荐

    Nealyang
  • Vue入门第一本学习笔记

    个人基于对 Vue.js 的学习制作了一个 Todo 单页应用 Lightodo,功能包括:添加待办事项卡片,对卡片或待办事项进行排序或删除操作,设置待办事项定...

    用户1097444
  • [先行者周末课程] 日历组件的开发思路讲解&&日历组件在实际工作中的使用方式

    各位同学们大家好,今天又到了周日,视频课程的时候。上次咱们讲的是日历组件。 简短的回顾一下上周的内容,免得同学们一时断篇,想不起来身在何方。日历这种东西,初学者...

    web前端教室
  • Spring5 - 向IOC容器中添加组件的4种方式

    查考: Spring5源码 - Spring IOC 注解复习 @CompentScan 部分

    小小工匠
  • [译] 用 Vue.js 3 Composition API 创建 i18n 插件

    原文:https://vuedose.tips/create-a-i18n-plugin-with-composition-api-in-vuejs-3/

    江米小枣
  • RAICC:揭示安卓应用中的非典型组件间通信方式 (CS)

    组件间通信(ICC)是Android中的一个关键机制。它使开发者能够组成丰富的功能,并探索应用内和跨应用的重用。遗憾的是,正如大量文献所报道的那样,ICC相当 ...

    管欣8078776
  • 焕然一新的 Vue3 中文文档来了!

    大家好,我是LBJ,最近参与了 Vue 3新文档的翻译和校验工作 (vuejs/docs contributor[3] 和 docs-zh-cn contrib...

    前端达人
  • 焕然一新的 Vue3 中文文档来了!

    大家好,我是LBJ,最近参与了 Vue 3新文档的翻译和校验工作 (vuejs/docs contributor[3] 和 docs-zh-cn contrib...

    @超人

扫码关注腾讯云开发者

领取腾讯云代金券