使用 vue-asyn-manager 管理 Vue 應用中的異步調用

不知道你們對 React Suspense 是否有過關注,也許 Suspense 讓人比較激動的是在服務端的流式渲染,然而從目前來看,React Suspense 的功能其實就是個 Loadable。固然啦這是我我的的見解,不過這不是今天的重點,今天的重點是介紹如何在 Vue 應用中更好的管理異步調用,那爲何會扯到 React Suspense 呢?由於 vue-async-manager 的靈感來自於 React Suspense,所以讓咱們開始吧。javascript

vue-async-manager 是一個開源項目:html

指南

在 Vue 應用中更輕鬆的管理異步調用vue

異步調用指的是什麼?

這裏所說的異步調用,主要指的是兩件事兒:java

  • 異步組件(Async Component)的加載
  • 發送異步請求從 API 中獲取數據

等待異步組件的加載

實際上 Vue 的異步組件已經支持在加載過程當中展現 loading 組件的功能,以下代碼取自官網:git

new Vue({
  // ...
  components: {
    'my-component': () => ({
        // 異步組件
        component: import('./my-async-component'),
        // 加載異步組件過程當中展現的 loading 組件
        loading: LoadingComponent,
        // loading 組件展現的延遲時間
        delay: 200
    })
  }
})
複製代碼

:::tip delay 用於指定 loading 組件展現的延遲時間,如上代碼中延遲時間爲 200ms,若是異步組件的加載在 200ms 以內完成,則 loading 組件就沒有展現的機會。 :::github

但它存在兩個問題:算法

  • 一、loading 組件與異步組件緊密關聯,沒法將 loading 組件提高,並用於多個異步組件的加載。
  • 二、若是異步組件自身仍有異步調用,例如請求 API,那麼 loading 組件是不會等待 API 請求完成以後才隱藏的。

vue-async-manager 提供了 <Suspense> 組件,能夠解決如上兩個問題。vue-router

一、使用 lazy 函數建立異步組件

過去咱們建立一個異步組件的方式是:vuex

const asyncComponent = () => import('./my-async.component.vue')
複製代碼

如今咱們使用 vue-async-manager 提供的 lazy 函數來建立異步組件:api

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 不多,所以暫時沒有提供對緩存資源的能力。

相關文章
相關標籤/搜索