handsontable vue 資料載入_【第七期】使用 vueasynmanager 管理 Vue 應用中的非同步呼叫...
不知道大家對 React Suspense 是否有過關注,也許 Suspense 讓人比較激動的是在服務端的流式渲染,然而從目前來看,React Suspense 的功能其實就是個 Loadable。當然啦這是我個人的看法,不過這不是今天的重點,今天的重點是介紹如何在 Vue 應用中更好的管理非同步呼叫,那為什麼會扯到 React Suspense 呢?因為 vue-async-manager 的靈感來自於 React Suspense,因此讓我們開始吧。
vue-async-manager 是一個開源專案:
•GitHub地址:https://github.com/shuidi-fed/vue-async-manager•Docs(中文|英文):https://shuidi-fed.github.io/vue-async-manager/•線上 Demos:https://shuidi-fed.github.io/vue-async-manager/demo.html
指南
在 Vue 應用中更輕鬆的管理非同步呼叫
非同步呼叫指的是什麼?
這裡所說的非同步呼叫,主要指的是兩件事兒:
•非同步元件(Async Component
)的載入•傳送非同步請求從 API 中獲取資料
等待非同步元件的載入
實際上Vue
的非同步元件已經支援在載入過程中展示loading
元件的功能,如下程式碼取自官網:
new Vue({ // ... components: { 'my-component': () => ({ // 非同步元件 component: import('./my-async-component'), // 載入非同步元件過程中展示的 loading 元件 loading: LoadingComponent, // loading 元件展示的延遲時間 delay: 200 }) }})
Tip:delay
用於指定loading
元件展示的延遲時間,如上程式碼中延遲時間為200ms
,如果非同步元件的載入在200ms
之內完成,則loading
元件就沒有展示的機會。
但它存在兩個問題:
•1、loading
元件與非同步元件緊密關聯,無法將loading
元件提升,並用於多個非同步元件的載入。•2、如果非同步元件自身仍有非同步呼叫,例如請求 API,那麼loading
元件是不會等待 API 請求完成之後才隱藏的。
vue-async-manager
提供了元件,可以解決如上兩個問題。
1、使用 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
函式即可。
2、使用
元件包裹非同步元件
<template> <div id="app"> <Suspense> <div slot="fallback">loadingdiv> <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>
只有當和
全部載入完畢後,
loading
元件才會消失。
Tip: Live Demo:等待所有非同步元件載入完畢[1]
配合 vue-router 使用
我們在開發Vue
應用時,最常使用非同步元件的方式是配合vue-router
做程式碼拆分,例如:
const router = new VueRouter({ routes: [ { path: '/', component: () => import('./my-async-component.vue') } ]})
為了讓元件等待這個非同步元件的載入,我們可以使用
lazy
函式包裹這個非同步元件工廠函式:
const router = new VueRouter({ routes: [ { path: '/', component: lazy(() => import('./my-async-component.vue')) } ]})
最後我們只需要用元件包裹渲染出口(
)即可:
<Suspense :delay="200"> <div slot="fallback">loadingdiv> <router-view/>Suspense>
Tip: Live Demo:配合 vue-router[2]
API請求中如何展示 loading
過去,大多是手動維護loading
的展示,例如“開始請求”時展示loading
,“請求結束”後隱藏loading
。而且如果有多個請求併發時,你就得等待所有請求全部完成後再隱藏loading
。總之你需要自己維護loading
的狀態,無論這個狀態是儲存在元件內,還是store
中。
現在來看看vue-async-manager
是如何解決 API 請求過程中loading
展示問題的,假設有如下程式碼:
<Suspense> <div slot="fallback">loading...div> <MyComponent/>Suspense>
在元件內渲染了
元件,該元件是一個普普通通的元件,在該元件內部,會發送 API 請求,如下程式碼所示:
<template> <div>{{ res }}div>template><script>import { getAsyncData } from 'api'export default { data: { res: {} }, async created() { // 非同步請求資料 this.res = await getAsyncData(id) }}script>
這是我們常見的程式碼,通常在created
或者mounted
鉤子中傳送非同步請求獲取資料,然而這樣的程式碼對於元件來說,它並不知道需要等待非同步資料獲取完成後再隱藏
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()
函式進行讀取資料。大家注意,上面的程式碼是以同步的方式來編寫的,並且元件能夠知道該元件正在進行非同步呼叫,因此
元件將等待該非同步呼叫結束之後再隱藏
loading
。
另外我們觀察如上程式碼中的模板部分,我們展示的資料是$rm.$result
,實際上非同步資料獲取成功之後,得到的資料會儲存在資源管理器的$rm.$result
屬性上,需要注意的是,該屬性本身就是響應式的,因此你無需在元件的data
中事先宣告。
Tip: Live Demo:Suspense 元件等待資源管理器獲取資料完成[3]
配合 vuex
配合vuex
很簡單,只需要使用mapActions
將actions
對映為方法即可:
export default { name: "AsyncComponent", methods: { ...mapActions(['increase']) }, created() { this.$rm = createResource(() => this.increase()) this.$rm.read() }};
Tip: Live Demo:配合 vuex[4]
捕獲元件樹中的所有非同步呼叫
元件不僅能捕獲非同步元件的載入,如果該非同步元件自身還有其他的非同步呼叫,例如通過資源管理器獲取資料,那麼
元件也能夠捕獲到這些非同步呼叫,並等待所有非同步呼叫結束之後才隱藏
loading
狀態。
我們來看一個例子:
<Suspense> <div slot="fallback">loadingdiv> <MyLazyComopnent/>Suspense>
在這段程式碼中,元件是一個通過
lazy
函式建立的元件,因此元件可以等待該非同步元件的載入,然而非同步元件自身又通過資源管理器獲取資料:
// 非同步元件export default { created() { // 建立一個資源管理器 this.$rm = createResource((params) => getAsyncData(params)) this.$rm.read(params) }}
這時候,元件會等待兩個非同步呼叫全部結束之後才隱藏
loading
,這兩個非同步呼叫分別是:
•1、非同步元件的載入•2、非同步元件內部通過資源管理器發出的非同步請求
Tip: 這個 Demo 也展示瞭如上描述的功能:
Live Demo:Suspense 元件等待資源管理器獲取資料完成[5]
資源管理器
前面我們一直在強調一個詞:資源管理器,我們把通過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> <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: 更重要的一點是:資源管理器可以脫離單獨使用。
如果資源管理器在請求資料的過程中發生了錯誤,則錯誤資料會儲存在$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'
。
另外以上介紹的內容都是由元件來控制
loading
的展示,並且loading
的內容由元件的
fallback
插槽決定。但有的時候我們希望更加靈活,我們經常遇到這樣的場景:點選按鈕的同時在按鈕上展示一個微小的loading
狀態,我們的程式碼看上去可能是這樣的:
<MyButton :loading="isLoading" >提交MyButton>
loading
的形態由元件提供,換句話說,我們拋棄了
的
fallback
插槽作為loading
來展示。因此,我們需要一個手段來得知當前是否處於正在載入的狀態,在上面我們已經介紹了該問題的解決辦法,我們可以使用資源管理器的$rm.$loading
屬性:
<MyButton :loading="$rm.$loading" >提交MyButton>
錯誤處理
當lazy
元件載入失敗會展示元件的
error
插槽,你也可以通過監聽的
rejected
事件來自定義錯誤處理。
Tip: Live Demo:載入失敗展示 error 插槽[6]
當錯誤發生時除了展示error
插槽,你還可以通過監聽元件的
rejected
事件來自定義處理:
<template> <Suspense :delay="200" @rejected="handleError"> <p class="fallback" slot="fallback">loadingp> <AsyncComponent/> Suspense>template><script>export default { // ...... methods: { handleError() { // Custom behavior } }};script>
Tip: Live Demo:通過事件處理 error[7]
關於 LRU 快取
React Cache
使用LRU
演算法快取資源,這要求 API 具有冪等性,然而在我的工作環境中,在給定時間週期內真正冪等的 API 很少,因此暫時沒有提供對快取資源的能力。
Demo References
[1]
等待所有非同步元件載入完畢:https://shuidi-fed.github.io/vue-async-manager/zh/demo.html#%E7%AD%89%E5%BE%85%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%E7%BB%84%E4%BB%B6%E5%8A%A0%E8%BD%BD%E5%AE%8C%E6%AF%95[2]
配合 vue-router:https://shuidi-fed.github.io/vue-async-manager/zh/demo.html#%E9%85%8D%E5%90%88-vue-router[3]
Suspense 元件等待資源管理器獲取資料完成:https://shuidi-fed.github.io/vue-async-manager/zh/demo.html#suspense-%E7%BB%84%E4%BB%B6%E7%AD%89%E5%BE%85%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E5%99%A8%E8%8E%B7%E5%8F%96%E6%95%B0%E6%8D%AE%E5%AE%8C%E6%88%90[4]
配合 vuex:https://shuidi-fed.github.io/vue-async-manager/zh/demo.html#%E9%85%8D%E5%90%88-vuex[6]
載入失敗展示 error 插槽:https://shuidi-fed.github.io/vue-async-manager/zh/demo.html#%E5%8A%A0%E8%BD%BD%E5%A4%B1%E8%B4%A5%E5%B1%95%E7%A4%BA-error-%E6%8F%92%E6%A7%BD[7]
通過事件處理 error:https://shuidi-fed.github.io/vue-async-manager/zh/demo.html#%E9%80%9A%E8%BF%87%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86-error
水滴前端團隊招募夥伴,歡迎投遞簡歷到郵箱:[email protected]