专栏首页志学Python使用Vue 3构建更好的高阶组件

使用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之间的桥梁。

本文分享自微信公众号 - 志学Python(lijinwen1996329ken),作者:志学Python

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Vue 3 条件渲染

    v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。

    公众号---志学Python
  • 从头创建您自己的vue.js——第4部分(构建反应性)

    状态反应是当应用程序(一组变量)的状态发生变化时,我们做某事(反应)。我们分两步来完成:

    公众号---志学Python
  • Vue 2.x 移动端长按事件实现方式

    大家好啊,我是你们的攻城狮,我是 Ken,人贱贱爱的前端攻城狮,我要告诉你个严重的问题,这几天心情低落,代码撸不动啊,结果今天一到公司,打开禅道,大家可能不知道...

    公众号---志学Python
  • Angular 实现一个 Dialog 组件

    这里有一个细节是base-dialog的z-index一定要大于overlay的,已保证dialog能显示在遮盖层上方。

    mafeifan
  • 12月5日 云头条:TIOBE公布12月编程语言排行

    TIOBE 公布了 12 月编程语言排行榜,前五名依旧是 Java、C、Python、C++ 和 Visual Basic.NET。TIOBE 编程社区指数是编...

    February
  • React和Vue中,是如何监听变量变化的

    假设有这样一个场景,父组件传递子组件一个A参数,子组件需要监听A参数的变化转换为state。

    用户2356368
  • RxJS Subject

    观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

    阿宝哥
  • js函数大全(2)

    原文地址:http://phperbar.blog.163.com/blog/static/162596182201032935815391/

    乔达摩@嘿
  • 有效的括号

    给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

    宇宙之一粟
  • Pwnhub之奇妙的巨蟒 Writeup

    本周的Pwnhub延迟到了周一,所以周一中午就看了下这题,是一道Python 的pyc逆向题,思路到挺简单的,但是很花精力

    Seebug漏洞平台

扫码关注云+社区

领取腾讯云代金券