vue创建的挂钩中出错_建立自己的Vue 3 SWR挂钩

vue创建的挂钩中出错

Version 3 is just around the corner and the composition API is bringing some new exciting possibilities, like building React Hook-like functions to help manage and share common logic and functionality

版本3即将来临,composition API带来了一些令人兴奋的新可能性,例如构建类似React Hook的函数来帮助管理和共享通用逻辑和功能。

One of the cases where we might take advantage of hooks — and that seems to be in the spotlights theses days — is the use of SWR, Stale While Revalidate. It’s a strategy that to keep the balance between immediacy — loading cached content right away — and freshness — ensuring updates to the cached content.

我们可能会利用钩子的情况之一(这似乎是当今流行的焦点)是SWR的使用,即Stale While Revalidate 。 此策略可在即时性(立即加载缓存的内容)和新鲜度(确保更新缓存的内容)之间保持平衡。

Modern browsers these days already have support to use this feature on the fly, but for this, the API must send specific headers in the response. You can learn more about this approach in this article.

如今,现代的浏览器已经支持即时使用此功能,但是为此,API必须在响应中发送特定的标头。 您可以在本文中了解有关此方法的更多信息。

The problem with this approach is that sometimes you use someone else’s APIs, and changing response headers is not a viable option. To solve this, we will build our own custom hook that can be re-used across all your components.

这种方法的问题在于,有时您使用别人的API,并且更改响应标头不是可行的选择。 为了解决这个问题,我们将构建自己的自定义钩子,该钩子可在所有组件中重复使用。

So let’s get our hands dirty and build a simple solution to this.

因此,让我们动手做一个简单的解决方案。

计划 (The Plan)

To start let’s define what we’ll be doing. I’ve made a simple flowchart to explain how this hook will work:

首先,让我们定义我们要做什么。 我制作了一个简单的流程图来说明此挂钩如何工作:

Image for post

We’ll receive a key to identify the request and the promise to be resolved. Then we check if the key already exists in the cache. If so, we inform the caller of the cached value. Then we resolve the promise (if we have the cached result or not) and inform the caller the result: If it’s a success we update the cache and inform the caller of the updated value, otherwise, we inform the caller we had an error resolving the promise.

我们将收到一个密钥,用于标识请求和要解决的承诺。 然后,我们检查密钥是否已存在于缓存中。 如果是这样,我们会将缓存的值告知调用者。 然后我们解析promise(是否有缓存的结果)并通知调用者结果:如果成功,则更新缓存并通知调用者更新后的值,否则,我们通知调用者我们有错误解决承诺。

You might ask why it’s a promise and not the URL of the API that we’re calling. By using a promise as input and not the URL we’re giving the option for this hook to be used in any case the result depends on a promise, whether it’s an API call or not. And even if it will be used only for API calls, we’ll keep the caller’s right to choose which approach will be used: The native fetch API, Axios, jquery’s AJAX , Angular’s $http or any other amongst the many solutions available on the internet.

您可能会问为什么这是一个承诺,而不是我们正在调用的API的URL。 通过使用promise作为输入而不是URL,我们为在任何情况下都可以使用此钩子提供了选项,无论是否是API调用,结果都取决于promise。 即使仅将其用于API调用,我们也将保留调用者选择使用哪种方法的权利:本机访存API,Axios,jquery的AJAX,Angular的$ http或其他许多可用的解决方案互联网。

开始项目 (Starting the Project)

To make our little project we’ll use Vite. It’s a development server and production bundler started by Evan You (vue’s creator) that serves the code using ES modules import and bundles the code using Rollup (a bundler created by Rich Harris, creator of Svelte) for production. It’s much faster than using the traditional vue-cli’s webpack based approach, especially in development mode. Since there’s no bundling involved, the server start and browser refresh is almost immediate.

为了进行我们的小项目,我们将使用Vite 。 这是由Evan You(vue的创建者)启动的开发服务器和生产捆绑器,使用ES模块导入来提供代码服务,并使用Rollup (由Svelte的创建者Rich Harris创建的捆绑器)捆绑代码进行生产。 这比使用传统的vue-cli基于webpack的方法要快得多,尤其是在开发模式下。 由于不涉及捆绑,因此服务器启动和浏览器刷新几乎是即时的。

To start our project we need to have node installed (if you don’t, click here to download and install the LTS version) and I would recommend installing yarn (learn how to that it here), a package manager that replaces npm (node’s native package manager), since yarn is faster than npm in most occasions.

要开始我们的项目,我们需要安装节点(如果没有安装,请单击此处下载并安装LTS版本),我建议安装yarn( 在此处了解如何安装),它是一个替换npm(节点的本地包管理器),因为在大多数情况下yarn比npm快。

With node and yarn installed, go to your terminal in the root folder where you want to create your project and use this command:

安装了node和yarn之后,转到要在其中创建项目的根文件夹中的终端,然后使用以下命令:

yarn create vite-app my-swr-hook

After a few seconds, the process is done, and we can install all the dependencies and run the project using the commands bellow:

几秒钟后,该过程完成,我们可以使用以下命令安装所有依赖项并运行项目:

cd my-swr-hook
yarn
yarn dev

Now just open your browser and navigate to http://localhost:3000 to check the default application running.

现在,只需打开浏览器并导航到http:// localhost:3000,以检查默认应用程序正在运行。

钩子 (The Hook)

Now it’s time to build our custom hook. We create a hooks folder inside src and then create a swr.js file.

现在是时候构建我们​​的自定义钩子了。 我们在src内创建一个hooks文件夹,然后创建一个swr.js文件。

We’ll start by creating a global cache and the function that will be exported and make all the work we need. By putting the cache outside of the returned function we ensure that it’s unique and accessible to all callers. The function will receive a key, and a promise, and will return the cached value if it exists. After that, we’ll resolve the promise and update the cache and/or return the corresponding response. Well use named export for the function (just a personal preference):

我们将从创建全局缓存和将要导出的功能开始,并完成我们需要的所有工作。 通过将缓存放置在返回的函数之外,我们确保该缓存是唯一的,并且可供所有调用者访问。 该函数将接收一个密钥和一个Promise,并将返回缓存的值(如果存在)。 之后,我们将解决承诺并更新缓存和/或返回相应的响应。 很好地使用命名导出功能(只是个人喜好):

const cache = { }
export const swr = (key, promise) => {
  if (cache[key]) return cache[key];
  return promise.then(response => {
    cache = { ... cache, [key]: response };
    return response;
  }).catch(err => {
    throw new Err(err.message)
  })
}

We get a big problem with this code because whether we have or don’t have the cached value, we’ll resolve the promise and return the updated value (or error). But in our piece of code, if we get the cached value it’s returned and that’s it. With this approach, we can’t move on and resolve our promise to revalidate the cache. Another problem is that we’re returning two kinds of response, one is pure data (from the cache) and the other one is a promise. And the error treatment is a little bit rough.

我们在此代码上遇到了一个大问题,因为无论我们是否拥有缓存的值,我们都将解决Promise并返回更新后的值(或错误)。 但是在我们的代码段中,如果我们获得了缓存的值,则返回它,仅此而已。 使用这种方法,我们无法继续解决诺言以重新验证缓存。 另一个问题是我们返回两种响应,一种是纯数据(来自缓存),另一种是promise。 错误处理有点粗糙。

To make this work we’ll use Vue’s composition API ref. This utility creates a reactive and mutable object. By using this all we have to do is return the reactive constant and the callers will be notified of it. We’ll start this constant with the cache’s key-value or null (in case the key doesn’t exist). To avoid the possibility of the caller changing our state, we’ll use another composition API functionality, readonly. The second version of our hook code now looks like this:

为了完成这项工作,我们将使用Vue的composition API ref 。 该实用程序创建一个React性和可变的对象。 通过使用此方法,我们要做的就是返回React常数,并会通知调用方。 我们将从缓存的键值或null(如果键不存在)开始此常量。 为了避免调用者更改我们的状态的可能性,我们将使用另一个组合API功能readonly 。 现在,钩子代码的第二个版本如下所示:

import { ref, readonly } from 'vue'
const cache = { }
export const swr = (key, promise) => {
  const data = ref(cache[key] || null)
  promise.then(response => {
    cache = { ... cache, [key]: response }
    data.value = response
  }).catch(err => {
    throw new Err(err.message)
  })
  return readonly(data)
}

It’s a lot better, but there’s still room for improvement. I think we can add and optional parameter to load the initial state (in case is not already in the cache) and return other parameters so that the caller knows if we are revalidating, if an error has occurred (and which error was that). Since now we are returning multiple values, it’s a better idea to create a state object with all the keys inside and update them accordingly. In this case, reactive is more suitable then ref. Another change we’ll have to do to make it possible for the caller to use destructuring and get individual reactive values is to make use of composition API utility toRefs.

它虽然好很多,但仍有改进的空间。 我认为我们可以添加和可选参数来加载初始状态(如果尚未在缓存中)并返回其他参数,以便调用者知道我们是否正在重新验证,是否发生了错误(以及那个错误是那个)。 由于现在我们返回多个值,因此最好创建一个内部包含所有键的状态对象,并相应地更新它们。 在这种情况下,电比参考更合适。 为了使调用者可以使用解构并获得单个React值,我们必须做的另一项更改是利用composition API实用程序toRefs

Another feature I think would be cool is to add localStorage. With this addition, if the key has already been called anytime in the past, the user will be instantly provided with the data. To make the state saving automatic whenever the data change we can use watchEffect. We’ll wrap the localStorage’s setItem method in a try-catch to avoid problems when the fetched data exceeds the quota, which will make our application stop working.

我认为另一个很酷的功能是添加localStorage 。 有了此附加功能,如果过去在任何时候都已调用过密钥,则将立即为用户提供数据。 为了使每当数据更改时状态自动保存,我们可以使用watchEffect 。 我们将在一次try-catch中包装localStoragesetItem方法,以避免当获取的数据超出配额时出现问题,这将使我们的应用程序停止工作。

import { reactive, readonly, toRefs, watchEffect } from "vue";
const cache = {};
export const swr = (key, promise, initialValue = null) => {
    const state = reactive({
        data:
            cache[key] || localStorage.getItem(`swr-${key}`)
                ? JSON.parse(localStorage.getItem(`swr-${key}`))
                : initialValue,
        isRevalidating: true,
        isError: false,
        error: null,
    });
    promise
        .then((response) => {
            cache[key] = response;
            state.data = response;
            state.isRevalidating = false;
        })
        .catch((err) => {
            state.isRevalidating = false;
            state.isError = true;
            state.error = err.message;
        });
    watchEffect(() => {
        try {
        localStorage.setItem(`swr-${key}`, JSON.stringify(state.data))
                } catch (error) { 
        console.log(error)
}
    }
    );
    return readonly(toRefs(state));
};

With these final changes, our custom hook is ready to be used

完成这些最后的更改后,即可使用我们的自定义钩子

演示应用 (The Demo App)

In order to use our hook and show its advantages over raw promises, we’ll build a simple app using cdnjs public api. We’ll show a list of JavaScript libraries and when the user clicks in one of them we’ll fetch the info of that library and show it on the screen.

为了使用我们的钩子并展示其相对于原始承诺的优势,我们将使用cdnjs public api构建一个简单的应用程序。 我们将显示一个JavaScript库列表,当用户单击其中的一个时,我们将获取该库的信息并将其显示在屏幕上。

Let’s create a new file in the components folder, Libraries.vue. This component will be responsible to fetch and render the libraries list. We’ll use the composition API and dispatch an event when the user clicks on any item, so the App component may know what library is selected and therefore trigger the library detail fetch and render.

让我们在components文件夹Libraries.vue中创建一个新文件。 该组件将负责获取和呈现库列表。 当用户单击任何项​​目时,我们将使用composition API并调度一个事件,因此App组件可能知道选择了哪个库,因此触发了库详细信息的获取和呈现。

<template>
  <div>
    <p v-if="isError">An error has occurred: {{error}}</p>
    <p v-else-if="isLoading">Loading ...</p>
    <ul v-else>
      <li v-for="lib in data" :key="lib.name" @click="select(lib.name)">{{lib.name}}</li>
    </ul>
  </div>
</template>


<script>
import { computed, ref } from "vue";
import { swr } from "../hooks/swr.js";


export default {
  setup(props, { emit }) {
    const list = swr(
      "list",
      fetch("https://api.cdnjs.com/libraries")
        .then((response) => response.json())
        .then((data) => data.results),
      []
    );
    const isLoading = computed(() => !list.data.length && list.isRevalidating);
    const isError = computed(() => list.isError);
    const error = computed(() => list.error);
    const data = computed(() => list.data);
    const select = (name) => emit("select", name);
    return { isLoading, isError, error, data, select };
  },
};
</script>


<style>
</style>

Now let’s change our App.vue file to render the list. We’ll add a selected ref too to receive the event dispatched from the Libraries component.

现在,让我们更改App.vue文件以呈现列表。 我们还将添加一个选定的引用,以接收从Libraries组件调度的事件。

<template>
  <h1>Javascript Libraries</h1>
  <hr />
  <div class="container">
    <div>
      <Libraries @select="v => selected = v" />
    </div>
    <div>
        Details go here
    </div>
  </div>
</template>
<script>
import Libraries from "./components/Libraries.vue";
import { ref } from "vue";
export default {
  components: {
    Library,
  },
  setup() {
    const selected = ref(null);
    return { selected };
  },
};
</script>
<style >
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  padding: 24px;
}
.container {
  display: flex;
}
.container div {
  flex-grow: 1;
  flex-basis: 50%;
}
hr {
  margin: 12px 0;
}
ul {
  list-style: none;
}
ul li {
  cursor: pointer;
}
</style>

You’ll notice that the first time you load the app, the Library component will show Loading, and some seconds later the list will be rendered. Since we’ve stored the data in browser’s localStorage, from the second time on the list will be rendered immediately. But if you open network tab in the browser’s developer tools, you’ll notice that each time you refresh the page the request will still be made in the background. If the returned data is different from the stored one, the list and localStorage value would be updated by our swr hook.

您会注意到,第一次加载应用程序时,“ Library组件将显示“正在加载”,几秒钟后,将呈现该列表。 由于我们已将数据存储在浏览器的localStorage ,因此从列表中的第二次开始将立即呈现该数据。 但是,如果您在浏览器的开发人员工具中打开“网络”标签,则会注意到,每次刷新页面时,请求仍会在后台发出。 如果返回的数据与存储的数据不同,则列表和localStorage值将通过我们的swr挂钩进行更新。

So now let’s build our Library component, which will be responsible to fetch and render de information about the selected library. This information will be received by props passed from the App component. We’ll render just some of the info provided by cdnjs. If you want to inspect the returned data format you can check the vue link here.

现在,让我们构建我们的Library组件,该组件将负责获取和呈现有关所选库的信息。 这些信息将由从App组件传递的道具接收。 我们将仅渲染cdnjs提供的一些信息。 如果要检查返回的数据格式,可以在此处检查vue链接。

Let’s code:

让我们编写代码:

<template>
  <p v-if="isError">An error has occurred: {{error}}</p>
  <p v-else-if="isLoading">Loading ...</p>
  <div v-else>
    <ul>
      <li>Name: {{data.name}}</li>
      <li>Description: {{data.description}}</li>
      <li v-if="data.authors">Authors: {{data.authors.map(author => author.name).join(', ')}}</li>
      <li>
        Latest:
        <a :href="data.latest">{{data.latest}}</a>
      </li>
      <li v-if="data.repository">
        Repository:
        <a :href="data.repository.url">{{data.repository.url}}</a>
      </li>
      <li>License: {{data.license}}</li>


      <li v-if="data.keywords">Keywords: {{data.keywords.join(', ')}}</li>
    </ul>
  </div>
</template>


<script>
import { computed } from "vue";
import { swr } from "../hooks/swr";
export default {
  props: {
    name: { type: String },
  },
  setup(props) {
    const { name } = props;
    const info = swr(
      name,
      fetch(`https://api.cdnjs.com/libraries/${name}`).then((response) =>
        response.json()
      ),
      {}
    );
    const isLoading = computed(() => {
      return !info.data.name && info.isRevalidating;
    });
    const data = computed(() => info.data);
    const isError = computed(() => info.isError);
    const error = computed(() => info.error);
    return { data, isLoading, isError, error };
  },
};
</script>


<style>
</style>

With the Library component ready, it’s time to change our App component so that if a library is selected the Library component is rendered. One special point of attention here is that if we use the Library component in the template, it will be rendered just once and only fetch the information about the first selected library.

准备好Library组件之后,就该更改我们的App组件了,以便在选择了Library情况下呈现Library组件。 这里需要特别注意的一点是,如果我们在模板中使用“ Library组件,它将仅呈现一次,并且仅获取有关第一个选定库的信息。

There are many ways to solve this, like adding a watch to the name prop in the Library component, but there’s an easier way: use the key prop. If we add a key prop tied to the selected library name every time we select a new library the key is updated and the Library component is re-rendered, solving our issue.

解决此问题的方法有很多,例如在“ Library组件中的名称prop中添加手表,但还有一种更简单的方法:使用key prop。 如果每次我们选择一个新库时都添加一个绑定到所选库名称的密钥道具,则密钥会更新,并且Library组件会重新呈现,从而解决了我们的问题。

So our App component will look like this:

因此,我们的App组件将如下所示:

<template>
  <h1>Javascript Libraries</h1>
  <hr />
  <div class="container">
    <div>
      <Libraries @select="v => selected = v" />
    </div>
    <div>
      <div v-if="selected">
        <Library :key="selected" :name="selected" />
      </div>
    </div>
  </div>
</template>
<script>
import Libraries from "./components/Libraries.vue";
import Library from "./components/Library.vue";
import { ref } from "vue";
export default {
  components: {
    Library,
    Libraries,
  },
  setup() {
    const selected = ref(null);
    return { selected };
  },
};
</script>
<style >
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  padding: 24px;
}
.container {
  display: flex;
}
.container div {
  flex-grow: 1;
  flex-basis: 50%;
}
hr {
  margin: 12px 0;
}
ul {
  list-style: none;
}
ul li {
  cursor: pointer;
}
</style>

Like on the Library component, you’ll notice that the first time you click on a library, the loading message is displayed, and shortly after the library info is rendered. If you click on another one and then click back on one you’ve already clicked, the info will be rendered immediately and the fetch request will be made in the background to check if the response is still the same.

就像在“ Library组件上一样,您会注意到,第一次单击库时,会显示加载消息,并且在呈现库信息后不久。 如果单击另一个,然后再单击已单击的一个,则将立即呈现该信息,并且将在后台发出获取请求,以检查响应是否仍然相同。

With this we’ll have achieved our objective — to present the data as soon as we can to our client, re-validate it in the background and update it. You may do some improvements, like adding a time-to-live parameter so that the re-fetching will be done just after it, or add some extra error checking. I’ll leave this as homework: Make new implementations to make this code suitable for your needs.

这样,我们将实现我们的目标-尽快将数据提供给客户,在后台重新验证并更新数据。 您可以做一些改进,例如添加一个生存时间参数,以便在紧随其后进行重新获取,或者添加一些额外的错误检查。 我将把它留作功课:进行新的实现以使此代码适合您的需求。

The source code of the working application is available in my github.

可以在我的github中找到正在运行的应用程序的源代码。

Any suggestions or observations are welcome as always.

一如既往地欢迎您提出任何建议或意见。

Hope you liked the article and learned something new.

希望您喜欢这篇文章并学到新知识。

See you next article.

下篇文章见。

翻译自: https://medium.com/better-programming/build-your-own-vue-3-swr-hook-f54124ee6ed6

vue创建的挂钩中出错

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值