使用Vue3构建更好的高阶组件[译]__Vue.js
发布于 4 年前 作者 banyungong 1883 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

Vue 3 即将发布,通过引入Composition API 。 它具有许多更改和性能改进。

高阶组件(HOC)是使用模板声明性地向您的应用程序添加某些功能的组件。 我相信即使引入了Composition API,它们仍将保持非常重要的关联。

HOC始终无法充分发挥其功能的全部功能,并且由于它们在大多数Vue应用程序中并不常见,因此它们的设计通常很差,可能会带来限制。 这是因为模板就是这样----模板或一种您表达某种逻辑的受约束的语言。 但是,在JavaScript或JSX环境中,表达逻辑要容易得多,因为您可以使用全部JavaScript。

Vue 3 带给桌面的是能够使用Composition API和声明性易用的模板无缝地混合和匹配JavaScript的表达能力。

我正在为各种逻辑(如网络,动画,UI和样式,实用程序和开源库)构建的应用程序中积极使用HOC。 我有一些技巧可以分享如何构建HOC,尤其是即将发布的Vue 3 Composition API。

1 The template

让我们假设以下Fetch组件。 在探讨如何实现这样的组件之前,您应该考虑如何使用组件。 然后,您需要决定如何实现它。 这与TDD类似,但没有经过测试-更像是在构思之前先尝试一下。

理想情况下,该组件将使用一个端点并将其结果作为范围限定的插槽属性返回:

<fetch endpoint="/api/users" v-slot="{ data }">
  <div v-if="data">
    <!-- Show the response data -->
  </div>
</fetch>

现在,尽管此API的基本目的是通过网络获取一些数据并显示它们,但仍有许多丢失的东西将是有用的。

让我们从错误处理开始。 理想情况下,我们希望能够检测到是否抛出了网络或响应错误,并向用户显示一些指示。 让我们将其运用到我们的代码中:

<fetch endpoint="/api/users" v-slot="{ data, error }">
  <div v-if="data">
    <!-- Show the response data -->
  </div>
  
  <div v-if="error">
    {{ error.message }}
  </div>
</fetch>

到目前为止,一切都很好。 但是加载状态呢? 如果我们走相同的道路,我们最终会得到这样的结果:

<fetch endpoint="/api/users" v-slot="{ data, error, loading }">
  <div v-if="data">
    <!-- Show the response data -->
  </div>
  
  <div v-if="error">
    {{ error.message }}
  </div>
  
  <div v-if="loading">
    Loading....
  </div>
</fetch>

现在,假设我们需要分页支持:

<fetch endpoint="/api/users" v-slot="{ data, error, loading, nextPage, prevPage }">
  <div v-if="data">
    <!-- Show the response data -->
  </div>
  
  <div v-if="!loading">
    <button @click="prevPage">Prev Page</button>
    <button @click="nextPage">Next Page</button>
  </div>
  
  <div v-if="error">
    {{ error.message }}
  </div>
  
  <div v-if="loading">
    Loading....
  </div>
</fetch>

你知道这是怎么回事吧? 我们在默认范围内的插槽中添加了太多属性。 相反,让我们将其细分为多个位置:

<fetch endpoint="/api/users">
  <template #default="{ data }">
    <!-- Show the response data -->
  </template>
  
  <template #pagination="{ prevPage, nextPage }">
    <button @click="prevPage">Prev Page</button>
    <button @click="nextPage">Next Page</button>
  </template>
  
  <template #error="{ message }">
    <p>{{ message }}</p>
  </div>
  
  <template #loading>
    Loading....
  </template>
</fetch>

虽然我们拥有的字符数基本相同,但是从某种意义上说,它在组件的不同操作周期中使用多个插槽来显示不同的UI时,这要干净得多。 它甚至允许我们按每个插槽而不是整个组件公开更多数据。

当然,这里还有改进的空间。 但是,让我们确定这些是您想要的组件功能。

什么都没有呢。 您仍然必须实施实际代码,以使其正常工作。

从模板开始,我们只有3个使用v-if显示的插槽:

<template>
  <div>
    <slot v-if="data" :data="data" />
    
    <slot v-if="!loading" name="pagination" v-bind="{ nextPage, prevPage }" />
    
    <slot v-if="error" name="error" :message="error.message" />
    
    <slot v-if="loading" name="loading" />
  </div>
</template>

此处将v-if与多个插槽一起使用是一种抽象,因此该组件的使用者不必有条件地呈现其UI。 这是一项方便的功能。

Composition API为构建更好的HOC提供了独特的机会,这是本文的重点。

2 The JavaScript

有了模板,第一个简单的实现将在单个设置函数中:

import { ref, onMounted } from 'vue';

export default {
  props: {
    endpoint: {
      type: String,
      required: true,
    }
  },
  setup({ endpoint }) {
    const data = ref(null);
    const loading = ref(true);
    const error = ref(null);
    const currentPage = ref(1);
    
    function fetchData(page = 1) {
      // ...
    }
    
    function nextPage() {
      return fetchData(currentPage.value + 1);
    }
    
    function prevPage() {
      if (currentPage.value <= 1) {
        return;
      }
      fetchData(currentPage.value - 1);
    }
    
    
    onMounted(() => {
      fetchData();
    });
    
    return {
      data,
      loading,
      error,
      nextPage,
      prevPage
    };
  }
};

以上是设置功能的概述。 为了完成它,我们可以像下面这样实现fetchData函数:

import { ref, onMounted } from 'vue';

export default {
  props: {
    endpoint: {
      type: String,
      required: true,
    }
  },
  setup({ endpoint }) {
    const data = ref(null);
    const loading = ref(true);
    const error = ref(null);
    const currentPage = ref(1);
    
    function fetchData(page = 1) {
      // ...
    }
    
    function nextPage() {
      return fetchData(currentPage.value + 1);
    }
    
    function prevPage() {
      if (currentPage.value <= 1) {
        return;
      }
  
      fetchData(currentPage.value - 1);
    }
    
    onMounted(() => {
      fetchData();
    });
    
    return {
      data,
      loading,
      error,
      nextPage,
      prevPage
    };
  }
};

一切就绪后,即可使用该组件。 您可以在这里找到它的工作示例。

但是,此HOC组件与Vue 2中的组件相似。您仅使用composition API重新编写了它,尽管它很简洁,但几乎没有用。

我发现,要为Vue 3构建更好的HOC组件(尤其是像这样的面向逻辑的组件),最好以“ Composition-API-first”的方式构建它。 即使您仅打算运送HOC。

您会发现我们已经做到了。 Fetch组件的设置功能可以提取为自己的函数,称为useFetch:

export function useFetch(endpoint) {
  // same code as the setup function
}

相反,我们的组件将如下所示:

import { useFetch } from '@/fetch';

export default {
  props: {
   // ...
  },
  setup({ endpoint }) {
      const api = useFetch(endpoint);
      return api;
  }
}

这种方法提供了一些机会。 首先,它使我们可以在与UI完全隔离的同时考虑逻辑。 这使我们的逻辑可以完全用JavaScript表示。 以后可以将其挂接到UI,这是Fetch组件的责任。

其次,它允许我们的useFetch函数将其逻辑分解为较小的函数。 可以将其视为将相似的东西“分组”在一起,并可能通过包含和排除这些较小的功能来创建我们组件的变体。

3分解

让我们通过将分页逻辑提取为其自身的功能来阐明这一点。 问题就变成了:如何将分页逻辑与获取逻辑分开? 两者似乎交织在一起。

您可以通过关注分页逻辑的功能来弄清楚。 解决它的一种有趣方法是将其拿走并检查您消除的代码。

当前,它的作用是通过添加页面查询参数来修改端点,并在暴露下一个和上一个函数的同时保持currentPage状态的状态。 从字面上看,这就是在上一次迭代中所做的。

通过创建一个名为usePagination的函数,该函数仅执行我们需要的部分,您将获得以下内容:

import { ref, computed } from 'vue';

export function usePagination(endpoint) {
  const currentPage = ref(1);
  const paginatedEndpoint = computed(() => {
    return `${endpoint}?page=${currentPage.value}`;
  });
  
  function nextPage() {
    currentPage.value++;
  }
  
  function prevPage() {
    if (currentPage.value <= 1) {
      return;
    }
    
    currentPage.value--;    
  }
  
  return {
    endpoint: paginatedEndpoint,
    nextPage,
    prevPage
  };
}

这样做的好处是我们隐藏了外部使用者的currentPage引用,这是我在Composition API中最喜欢的部分之一。 我们可以轻松地向API使用者隐藏不重要的细节。

更新useFetch以反映该页面很有趣,因为它似乎需要跟踪usePagination公开的新端点。 幸运的是,watch已经覆盖了我们。

可以不希望端点参数是常规字符串,而可以允许它是反应性值。 这使我们能够观看它,并且每当分页页面更改时,它将产生新的端点值,从而触发重新获取。

import { watch, isRef } from 'vue';

export function useFetch(endpoint) {
  // ...
  function fetchData() {
    // ...
    
    // If it's a ref, get its value
    // otherwise use it directly
      return fetch(isRef(endpoint) ? endpoint.value : endpoint, {
        // Same fetch opts
      }) // ...
  }
  
  // watch the endpoint if its a ref/computed value
  if (isRef(endpoint)) {
    watch(endpoint, () => {
      // refetch the data again
      fetchData();
    });
  } 
  
  return {
    // ...
  };
}

请注意,useFetch和usePagination完全不了解彼此,并且两者的实现就像彼此都不存在一样。 这为我们的HOC提供了更大的灵活性。

您还会注意到,首先通过构建Composition API,我们创建了无法识别您的UI的盲JavaScript。 以我的经验,这对于正确地对数据建模而无需考虑UI或让UI决定数据模型非常有帮助。

另一个很酷的事情是,我们可以创建HOC的两种不同变体:一种允许分页,而另一种则不允许。 这为我们节省了几千字节。

这是仅进行提取的示例:

import { useFetch } from '@/fetch';

export default {
  setup({ endpoint }) {
    return useFetch(endpoint);
  }
};

这是同时做这两项的另一个:

import { useFetch, usePagination } from '@/fetch';

export default {
  setup(props) {
    const { endpoint, nextPage, prevPage } = usePagination(props.endpoint);
    const api = useFetch(endpoint);
    
    return {
      ...api,
      nextPage,
      prevPage
    };
  }
};

更好的是,您可以根据道具有条件地应用usePagination功能以获得更大的灵活性:

import { useFetch, usePagination } from '@/fetch';

export default {
  props: {
      endpoint: String,
      paginate: Boolean
  },
  setup({ paginate, endpoint }) {
    // an object to dump any conditional APIs we may have
    let addonAPI = {};

    // only use the pagination API if requested by a prop
    if (paginate) {
      const pagination = usePagination(endpoint);
      endpoint = pagination.endpoint;
      addonAPI = {
        ...addonAPI,
        nextPage: pagination.nextPage,
        prevPage: pagination.prevPage
      };
    }

    const coreAPI = useFetch(endpoint);
    
    // Merge both APIs
    return {
      ...addonAPI,
      ...coreAPI,
    };
  }
};

对于您的需求而言,这可能太多了,但是它使您的HOC更加灵活。 否则,它们将是僵化的代码体,难以维护。 绝对也更适合单元测试。

这是行动的最终结果:

链接:代码展示地址

总结

总结起来,首先将您的HOC构建为Composition API。 然后,将逻辑部分尽可能地分解为较小的可组合函数。 将它们全都放在您的HOC中以暴露最终结果。

这种方法使您可以构建组件的变体,甚至可以完成所有这些变体而又不会脆弱且难以维护。 通过以composition-api-first的心态进行构建,您可以自己编写与UI无关的孤立的代码部分。 通过这种方式,您可以让HOC成为盲目的JavaScript和无功能的UI之间的桥梁。

原文地址

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: GoSolo 原文链接:https://juejin.im/post/6859571758362525704

回到顶部