使用Vue 3构建更好的高阶组件

时间:2022-07-24
本文章向大家介绍使用Vue 3构建更好的高阶组件,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

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

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

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

模板

让我们假设以下fetch组件。在研究如何实现这样的组件之前,您应该考虑如何使用组件。然后,您需要决定如何实现它。这与TDD类似,但没有经过测试-更像是在尝试该概念之前对其进行了研究。

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

<fetch endpoint ="/api/users" v-slot ="{data}">
  <div v-if ="data">
    <!-显示响应数据->
  </div>
</fetch>

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

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

<fetch endpoint ="/api/users" v-slot ="{数据,错误}">
  <div v-if ="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>

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

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

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

从模板开始,我们只有3个使用来显示的slot 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。这是一个方便的功能。

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

JavaScript

有了模板,第一个天真的实现将在一个setup函数中:

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

这是该setup功能的概述。为了完成它,我们可以实现如下fetchData功能:

function fetchData(page = 1) {
  loading.value = true;
  // I prefer to use fetch
  // you cause use axis as an alternative
  return fetch(`${endpoint}?page=${page}`, {
    // maybe add a prop to control this
    method: 'get',
    headers: {
      'content-type': 'application/json'
    }
  })
    .then(res => {
      // a non-200 response code
      if (!res.ok) {
        // create error instance with HTTP status text
        const error = new Error(res.statusText);
        error.json = res.json();
        throw error;
      }

      return res.json();
    })
    .then(json => {
      // set the response data
      data.value = json;
      // set the current page value
      currentPage.value = page;
    })
    .catch(err => {
      error.value = err;
      // incase a custom JSON error response was provided
      if (err.json) {
        return err.json.then(json => {
          // set the JSON response message
          error.value.message = json.message;
        });
      }
    })
    .then(() => {
      // turn off the loading state
      loading.value = false;
    });
}

在所有这些都准备就绪之后,就可以使用该组件了。您可以在这里找到它的工作示例。

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

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

您会发现我们已经做到了。该fetch组件的setup功能,也能提取到其自身的功能,这就是所谓useFetch:

export function useFetch(endpoint) {
  // same code as the setup function
}
And instead our component will look like this:

import { useFetch } from '@/fetch';

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

分解

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

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

当前,它的作用是endpoint通过附加page查询参数来修改,并currentPage在暴露next和previous起作用时保持状态的状态。从字面上看,这就是在上一次迭代中所做的。

通过创建一个usePagin

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我们已经覆盖了。

与其期望endpoint参数是常规字符串,不如让我们将其作为反应性值。这使我们能够观看它,并且每当分页页面更改时,它将产生新的端点值,从而触发重新获取。

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我们已经覆盖了。

与其期望endpoint参数是常规字符串,不如让我们将其作为反应性值。这使我们能够观看它,并且每当分页页面更改时,它将产生新的端点值,从而触发重新获取。

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构建为Composition API。然后,将逻辑部分尽可能地分解为较小的可组合函数。将它们全都放在您的HOC中以暴露最终结果。

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