不知道你們對 React Suspense 是否有過關注,也許 Suspense 讓人比較激動的是在服務端的流式渲染,然而從目前來看,React Suspense 的功能其實就是個 Loadable。固然啦這是我我的的見解,不過這不是今天的重點,今天的重點是介紹如何在 Vue 應用中更好的管理異步調用,那爲何會扯到 React Suspense 呢?由於 vue-async-manager 的靈感來自於 React Suspense,所以讓咱們開始吧。 vue-async-manager 是一個開源項目:javascript
GitHub地址:github.com/shuidi-fed/…html
Docs(中文|英文):shuidi-fed.github.io/vue-async-m…vue
在線 Demos:shuidi-fed.github.io/vue-async-m…java
做者:@hcysunyanggit
在 Vue 應用中更輕鬆的管理異步調用github
這裏所說的異步調用,主要指的是兩件事兒:算法
Async Component
)的加載實際上 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
組件提高,並用於多個異步組件的加載。loading
組件是不會等待 API 請求完成以後才隱藏的。vue-async-manager
提供了 <Suspense>
組件,能夠解決如上兩個問題。
過去咱們建立一個異步組件的方式是:
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
應用時,最常使用異步組件的方式是配合 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 :::
過去,大可能是手動維護 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
很簡單,只須要使用 mapActions
將 actions
映射爲方法便可:
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()
函數用來根據已有資源管理器建立一個如出一轍的資源管理器出來。
當一個 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
是兩個獨立的資源管理器,所以它們互不影響。
假設咱們正在提交表單,若是用戶連續兩次點擊按鈕,就會形成重複提交,以下例子:
<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
,好比頁面頂部有一個長長的加載條,這個加載條不影響其餘內容的正常渲染。
所以 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 :::
React Cache
使用 LRU
算法緩存資源,這要求 API 具備冪等性,然而在個人工做環境中,在給定時間週期內真正冪等的 API 不多,所以暫時沒有提供對緩存資源的能力。