使用 vue-async-manager 管理 Vue 中的异步调用

不知道你们对 React Suspense 是否有过关注,也许 Suspense 让人比较激动的是在服务端的流式渲染,然而从目前来看,React Suspense 的功能其实就是个 Loadable。固然啦这是我我的的见解,不过这不是今天的重点,今天的重点是介绍如何在 Vue 应用中更好的管理异步调用,那为何会扯到 React Suspense 呢?由于 vue-async-manager 的灵感来自于 React Suspense,所以让咱们开始吧。 vue-async-manager 是一个开源项目:javascript

指南

在 Vue 应用中更轻松的管理异步调用github

异步调用指的是什么?

这里所说的异步调用,主要指的是两件事儿:算法

  • 异步组件(Async Component)的加载
  • 发送异步请求从 API 中获取数据

等待异步组件的加载

实际上 Vue 的异步组件已经支持在加载过程当中展现 loading 组件的功能,以下代码取自官网:vue-router

new Vue({
  // ...
  components: {
    'my-component': () => ({
        // 异步组件
        component: import('./my-async-component'),
        // 加载异步组件过程当中展现的 loading 组件
        loading: LoadingComponent,
        // loading 组件展现的延迟时间
        delay: 200
    })
  }
})
复制代码

:::tip delay 用于指定 loading 组件展现的延迟时间,如上代码中延迟时间为 200ms,若是异步组件的加载在 200ms 以内完成,则 loading 组件就没有展现的机会。 :::vuex

但它存在两个问题:api

  • 一、loading 组件与异步组件紧密关联,没法将 loading 组件提高,并用于多个异步组件的加载。
  • 二、若是异步组件自身仍有异步调用,例如请求 API,那么 loading 组件是不会等待 API 请求完成以后才隐藏的。

vue-async-manager 提供了 <Suspense> 组件,能够解决如上两个问题。

一、使用 lazy 函数建立异步组件

过去咱们建立一个异步组件的方式是:

const asyncComponent = () => import('./my-async.component.vue')
复制代码

如今咱们使用 vue-async-manager 提供的 lazy 函数来建立异步组件:

import { lazy } from 'vue-async-manager'
 
const asyncComponent = lazy(() => import('./my-async.component.vue'))
复制代码

如上代码所示,仅仅是将原来的异步工厂函数做为参数传递给 lazy 函数便可。

二、使用 <Suspense> 组件包裹异步组件

<template>
  <div id="app">
    <!-- 使用 Suspense 组件包裹可能出现异步组件的组件树 -->
    <Suspense>
      <!-- 展现 loading -->
      <div slot="fallback">loading</div>
      <!-- 异步组件 -->
      <asyncComponent1/>
      <asyncComponent2/>
    </Suspense>
  </div>
</template>

<script> // 建立异步组件 const asyncComponent1 = lazy(() => import('./my-async.component1.vue')) const asyncComponent2 = lazy(() => import('./my-async.component2.vue')) export default { name: 'App', components: { // 注册组件 asyncComponent1, asyncComponent2 } } </script>
复制代码

只有当 <asyncComponent1/><asyncComponent2/> 所有加载完毕后,loading 组件才会消失。

:::tip Live Demo: 等待全部异步组件加载完毕 :::

配合 vue-router 使用

咱们在开发 Vue 应用时,最常使用异步组件的方式是配合 vue-router 作代码拆分,例如:

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: () => import('./my-async-component.vue')
    }
  ]
})
复制代码

为了让 <Suspense> 组件等待这个异步组件的加载,咱们可使用 lazy 函数包裹这个异步组件工厂函数:

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: lazy(() => import('./my-async-component.vue'))
    }
  ]
})
复制代码

最后咱们只须要用 <Suspense> 组件包裹渲染出口(<router-view>)便可:

<Suspense :delay="200">
  <div slot="fallback">loading</div>
  <!-- 渲染出口 -->
  <router-view/>
</Suspense>
复制代码

:::tip Live Demo: 配合 vue-router :::

API请求中如何展现 loading

过去,大可能是手动维护 loading 的展现,例如“开始请求”时展现 loading,“请求结束”后隐藏 loading。并且若是有多个请求并发时,你就得等待全部请求所有完成后再隐藏 loading。总之你须要本身维护 loading 的状态,不管这个状态是存储在组件内,仍是 store 中。

如今来看看 vue-async-manager 是如何解决 API 请求过程当中 loading 展现问题的,假设有以下代码:

<Suspense>
  <div slot="fallback">loading...</div>
  <MyComponent/>
</Suspense>
复制代码

<Suspense> 组件内渲染了 <MyComponent> 组件,该组件是一个普普统统的组件,在该组件内部,会发送 API 请求,以下代码所示:

<!-- MyComponent.vue -->
<template>
  <!-- 展现请求回来的数据 -->
  <div>{{ res }}</div>
</template>
 
<script> import { getAsyncData } from 'api' export default { data: { res: {} }, async created() { // 异步请求数据 this.res = await getAsyncData(id) } } </script>
复制代码

这是咱们常见的代码,一般在 created 或者 mounted 钩子中发送异步请求获取数据,然而这样的代码对于 <Suspense> 组件来讲,它并不知道须要等待异步数据获取完成后再隐藏 loading。为了解决这个问题,咱们可使用 vue-async-manager 提供的 createResource 函数建立一个资源管理器

<template>
  <!-- 展现请求回来的数据 -->
  <div>{{ $rm.$result }}</div>
</template>
 
<script> import { getAsyncData } from 'api' import { createResource } from 'vue-async-manager' export default { created() { // 建立一个资源管理器 this.$rm = createResource((params) => getAsyncData(params)) // 读取数据 this.$rm.read(params) } } </script>
复制代码

createResource 函数传递一个工厂函数,咱们建立了一个资源管理器 $rm,接着调用资源管理器的 $rm.read() 函数进行读取数据。你们注意,上面的代码是以同步的方式来编写的,而且 <Suspense> 组件可以知道该组件正在进行异步调用,所以 <Suspense> 组件将等待该异步调用结束以后再隐藏 loading

另外咱们观察如上代码中的模板部分,咱们展现的数据是 $rm.$result,实际上异步数据获取成功以后,获得的数据会保存在资源管理器$rm.$result 属性上,须要注意的是,该属性自己就是响应式的,所以你无需在组件的 data 中事先声明。


:::tip Live Demo: Suspense 组件等待资源管理器获取数据完成 :::

配合 vuex

配合 vuex 很简单,只须要使用 mapActionsactions 映射为方法便可:

export default {
  name: "AsyncComponent",
  methods: {
    ...mapActions(['increase'])
  },
  created() {
    this.$rm = createResource(() => this.increase())
    this.$rm.read()
  }
};
复制代码

:::tip Live Demo: 配合 vuex :::

捕获组件树中的全部异步调用

<Suspense> 组件不只能捕获异步组件的加载,若是该异步组件自身还有其余的异步调用,例如经过资源管理器获取数据,那么 <Suspense> 组件也可以捕获到这些异步调用,并等待全部异步调用结束以后才隐藏 loading 状态。

咱们来看一个例子:

<Suspense>
  <div slot="fallback">loading</div>
  <!-- MyLazyComponent 是经过 lazy 函数建立的组件 -->
  <MyLazyComopnent/>
</Suspense>
复制代码

在这段代码中,<MyLazyComopnent/> 组件是一个经过 lazy 函数建立的组件,所以 <Suspense> 组件能够等待该异步组件的加载,然而异步组件自身又经过资源管理器获取数据:

// 异步组件
export default {
  created() {
    // 建立一个资源管理器
    this.$rm = createResource((params) => getAsyncData(params))
    this.$rm.read(params)
  }
}
复制代码

这时候,<Suspense> 组件会等待两个异步调用所有结束以后才隐藏 loading,这两个异步调用分别是:

  • 一、异步组件的加载
  • 二、异步组件内部经过资源管理器发出的异步请求

:::tip 这个 Demo 也展现了如上描述的功能:

Live Demo: Suspense 组件等待资源管理器获取数据完成 :::

资源管理器

前面咱们一直在强调一个词:资源管理器,咱们把经过 createResource() 函数建立的对象称为资源管理器(Resource Manager),所以咱们约定使用名称 $rm 来存储 createResource() 函数的返回值。

资源管理器的完整形态以下:

this.$rm = createResource(() => getAsyncData())

this.$rm = {
    read(){},   // 一个函数,调用该函数会真正发送异步请求获取数据
    $result,    // 初始值为 null,异步数据请求成功后,保存着取得的数据
    $error,     // 初始值为 null,当异步请求出错时,其保存着 err 数据
    $loading,   // 一个boolean值,初始值为 false,表明着是否正在请求中
    fork()      // 根据已有资源管理器 fork 一个新的资源管理器
}
复制代码

其中 $rm.read() 函数用来发送异步请求获取数据,可屡次调用,例如点击按钮再次调用其获取数据。$rm.$result 咱们也已经见过了,用来存储异步获取来的数据。$rm.$loading 是一个布尔值,表明着请求是否正在进行中,一般咱们能够像以下这样自定义 loading 展现:

<template>
  <!-- 控制 loading 的展现 -->
  <MyButton :loading="$rm.$loading" @click="submit" >提交</MyButton>
</template>
 
<script> import { getAsyncData } from 'api' import { createResource } from 'vue-async-manager' export default { created() { // 建立一个资源管理器 this.$rm = createResource((id) => getAsyncData(id)) }, methods: { submit() { this.$rm.read(id) } } } </script>
复制代码

:::tip 更重要的一点是:资源管理器能够脱离 <Suspense> 单独使用。 :::

若是资源管理器在请求数据的过程当中发生了错误,则错误数据会保存在 $rm.$error 属性中。$rm.fork() 函数用来根据已有资源管理器建立一个如出一辙的资源管理器出来。

fork 一个资源管理器

当一个 API 用来获取数据,而且咱们须要并发的获取两次数据,那么只须要调用两次 $rm.read() 便可:

<script>
import { getAsyncData } from 'api'
import { createResource } from 'vue-async-manager'
 
export default {
  created() {
    // 建立一个资源管理器
    this.$rm = createResource((type) => getAsyncData(type))
     
    // 连续获取两次数据
    this.$rm.read('top')
    this.$rm.read('bottom')
  }
}
</script>
复制代码

可是这么作会产生一个问题,因为一个资源管理器对应一个 $rm.$result,它只维护一份请求回来的数据以及 loading 状态,所以如上代码中,$rm.$result 最终只会保存 $rm.read('bottom') 的数据。固然了,有时候这是符合需求的,但若是须要保存两次调用的数据,那么就须要 fork 出一个新的资源管理器:

<script>
import { getAsyncData } from 'api'
import { createResource } from 'vue-async-manager'
 
export default {
  created() {
    // 建立一个资源管理器
    this.$rm = createResource((type) => getAsyncData(type))
    this.$rm2 = this.$rm.fork()
     
    // 连续获取两次数据
    this.$rm.read('top')
    this.$rm2.read('bottom')
  }
}
</script>
复制代码

这样,因为 $rm$rm2 是两个独立的资源管理器,所以它们互不影响。

prevent 选项与防止重复提交

假设咱们正在提交表单,若是用户连续两次点击按钮,就会形成重复提交,以下例子:

<template>
  <button @click="submit">提交</button>
</template>
<script>
import { getAsyncData } from 'api'
import { createResource } from 'vue-async-manager'
 
export default {
  created() {
    // 建立一个资源管理器
    this.$rm = createResource((type) => getAsyncData(type))
  },
  methods: {
    submit() {
      this.$rm.read(data)
    }
  }
}
</script>
复制代码

实际上,咱们能够在建立资源管理器的时候提供 prevent 选项,这样建立出来的资源管理器将自动为咱们防止重复提交:

<template>
  <button @click="submit">提交</button>
</template>
<script>
import { getAsyncData } from 'api'
import { createResource } from 'vue-async-manager'
 
export default {
  created() {
    // 建立一个资源管理器
    this.$rm = createResource((type) => getAsyncData(type), { prevent: true })
  },
  methods: {
    submit() {
      this.$rm.read(data)
    }
  }
}
</script>
复制代码

当第一次点击按钮时会发送一个请求,在这个请求完成以前,将不会再次发送下一次请求。直到上一次请求完成以后,$rm.read() 函数才会再次发送请求。

loading 的展现形态

loading 的展现形态能够分为两种:一种是只展现 loading,不展现其余内容;另外一种是正常渲染其余内容的同时展现 loading,好比页面顶部有一个长长的加载条,这个加载条不影响其余内容的正常渲染。

所以 vue-async-manager 提供了两种渲染模式:

import VueAsyncManager from 'vue-async-manager'
Vue.use(VueAsyncManager, {
  mode: 'visible' // 指定渲染模式,可选值为 'visible' | 'hidden',默认值为:'visible'
})
复制代码

默认状况下采用 'visible' 的渲染模式,意味着 loading 的展现能够与其余内容共存,若是你不想要这种渲染模式,你能够指定 mode'hidden'

另外以上介绍的内容都是由 <Suspense> 组件来控制 loading 的展现,而且 loading 的内容由 <Suspense> 组件的 fallback 插槽决定。但有的时候咱们但愿更加灵活,咱们常常遇到这样的场景:点击按钮的同时在按钮上展现一个微小的 loading 状态,咱们的代码看上去多是这样的:

<MyButton :loading="isLoading" >提交</MyButton>
复制代码

loading 的形态由 <MyButton> 组件提供,换句话说,咱们抛弃了 <Suspense>fallback 插槽做为 loading 来展现。所以,咱们须要一个手段来得知当前是否处于正在加载的状态,在上面咱们已经介绍了该问题的解决办法,咱们可使用资源管理器的 $rm.$loading 属性:

<MyButton :loading="$rm.$loading" >提交</MyButton>
复制代码

错误处理

lazy 组件加载失败会展现 <Suspense> 组件的 error 插槽,你也能够经过监听 <Suspense>rejected 事件来自定义错误处理。

:::tip Live Demo: 加载失败展现 error 插槽 :::

当错误发生时除了展现 error 插槽,你还能够经过监听 <Suspense> 组件的 rejected 事件来自定义处理:

<template>
  <Suspense :delay="200" @rejected="handleError">
    <p class="fallback" slot="fallback">loading</p>
    <AsyncComponent/>
  </Suspense>
</template>
<script> export default { // ...... methods: { handleError() { // Custom behavior } } }; </script>
复制代码

:::tip Live Demo: 经过事件处理 error :::

关于 LRU 缓存

React Cache 使用 LRU 算法缓存资源,这要求 API 具备幂等性,然而在个人工做环境中,在给定时间周期内真正幂等的 API 不多,所以暂时没有提供对缓存资源的能力。

相关文章
相关标签/搜索