由於 API 會受網路狀況影響,因此希望 Response Time 大於 1
秒才顯示 Spinner,且若顯示 Spinner 後也不要立即隱藏,最少顯示 0.5
秒,這種需求該如何實現呢 ?
Version
Vue 3.0.11
Tailwind CSS 2.1.4
Promise Chain
若 API 的 response time 小於 1
秒,會立即顯示 data 不會顯示 spinner。
若 API 的 response time 大於 1
秒,會顯示 spinner 且最少顯示 0.5
後才隱藏。
<template lang='pug'>
div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
path(fill='#ffffff', d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')
div
button(@click='onClick').border.border-gray-300.text-xs.text-gray-700.px-2.py-1 Get Books
ul
li(v-for='x in books') {{ x.title }} / {{ x.price }}
</template>
<script setup>
import getBooks from '/src/api/getBooks'
const API_THRESHOLD = 1000
const SPINNER_TIME = 500
ref: books = []
ref: isShowSpinner = false
let sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
let showSpinner = () => isShowSpinner = true
let hideSpinner = () => isShowSpinner = false
let errorHandling = e => {
let { response: { data: { error } } } = e
console.error(error)
}
let onClick = () => {
let booksP = getBooks()
.then(x => x.data)
.then(x => books = x)
.catch(errorHandling)
let showBooksSpinner = () => {
showSpinner()
Promise.all([
booksP,
sleep(SPINNER_TIME)
]).then(hideSpinner)
}
Promise.race([
booksP.then(() => Promise.reject('API response less then 1000ms')),
sleep(API_THRESHOLD)
]).then(showBooksSpinner)
.catch(console.log)
}
</script>
第 2 行
div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
path(fill='#ffffff', d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')
使用 Tailwind 實現 spinner,並使用 isShowSpinner
控制顯示 spinner。
15 行
const API_THRESHOLD = 1000
const SPINNER_TIME = 500
設定 constant,可視需求自行調整:
API_THRESHOLD
:API response time 大於API_THRESHOLD
才會顯示 spinnerSPINNER_TIME
:spinner 最短顯示時間
18 行
ref: books = []
ref: isShowSpinner = false
books
:顯示所有 API 回傳資料isShowSpinner
:控制是否顯示 spinner
21 行
let sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
sleep()
將 setTimeout()
包裹成 Promise,用於讓 spinner 顯示 0.5
秒以上。
23 行
let showSpinner = () => isShowSpinner = true
let hideSpinner = () => isShowSpinner = false
將 spinner 隱藏或顯示。
32 行
let booksP = getBooks()
.then(x => x.data)
.then(x => books = x)
.catch(errorHandling)
按下 onClick()
先執行 getBooks()
呼叫 API,並存入 booksP
Promise,稍後由 Promise.race()
判斷。
45 行
Promise.race([
booksP.then(() => Promise.reject('API response less then 1000ms')),
sleep(API_THRESHOLD)
]).then(showBooksSpinner)
.catch(console.log)
使用 Promise.race()
判斷 API response time 大於 1
秒才顯示 spinner:
booksP
:呼叫 API 所回傳 Promisesleep()
:1
秒 delay 的 Promise
若 sleep()
早於 booksP
Promise,則 Promise.race()
將選擇 sleep()
並繼續執行 showBooksSpinner()
。
若 booksP
Promise 早於 sleep()
,則發起 Rejected Promise 終止後續執行。
37 行
let showBooksSpinner = () => {
showSpinner()
Promise.all([
booksP,
sleep(SPINNER_TIME)
]).then(hideSpinner)
}
當 sleep()
早於 booksP
Promise 時,由於還沒呼叫 API,會在 showBooksSpinner()
呼叫 API。
使用 Promise.all()
讓所有 Promise 都執行才會繼續:
booksP
:呼叫 API 所回傳 Promisesleep()
:0.5
秒顯示 Spinner
若 sleep()
早於 booksP
Promise,則會等 booksP
Promise 呼叫 API 成功後才會隱藏 spinner
若 booksP
Promise 早於 sleep()
,則會等 sleep()
完成後才會隱藏 spinner,因此可確保 spinner 顯示最短時間。
Point-free
<template lang='pug'>
div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
path(fill='#ffffff', d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')
div
button(@click='onClick').border.border-gray-300.text-xs.text-gray-700.px-2.py-1 Get Books
ul
li(v-for='x in books') {{ x.title }} / {{ x.price }}
</template>
<script setup>
import { ref } from 'vue'
import { write } from 'vue3-fp'
import { pipe, compose, andThen as then, otherwise, prop, path, T, F } from 'ramda'
import { sleep, reject, log, error } from 'wink-fp'
import getBooks from '/src/api/getBooks'
const API_THRESHOLD = 1000
const SPINNER_TIME = 500
let books = ref([])
let isShowSpinner = ref(false)
let showSpinner = compose(write(isShowSpinner), T)
let hideSpinner = compose(write(isShowSpinner), F)
let errorHandling = pipe(
path(['response', 'data', 'error']),
error
)
let onClick = () => {
let booksP = pipe(
getBooks,
then(prop('data')),
then(write(books)),
otherwise(errorHandling)
)()
let showBooksSpinner = pipe(
showSpinner,
() => Promise.all([booksP, sleep(SPINNER_TIME)]),
then(hideSpinner)
)
pipe(
() => Promise.race([
booksP.then(() => reject('API response less then 1000ms')),
sleep(API_THRESHOLD)
]),
then(showBooksSpinner),
otherwise(log)
)()
}
</script>
34 行
let booksP = pipe(
getBooks,
then(prop('data')),
then(write(books)),
otherwise(errorHandling)
)()
按下 onClick()
先使用 pipe()
組合 IIFE 回傳 booksP
Promise:
getBooks
:呼叫 API 回傳 Promisethen(prop('data'))
:從 Promise 內取得data
then(write(books))
:從 Promise 內取得內部值寫入books
stateotherwise(errorHandling)
:處理 Rejected Promise
47 行
pipe(
() => Promise.race([
booksP.then(() => reject('API response less then 1000ms')),
sleep(API_THRESHOLD)
]),
then(showBooksSpinner),
otherwise(log)
)()
使用 pipe()
組合 IIFE:
() => Promise.race()
:使用Promise.race()
比較showBooks()
與sleep()
誰先回傳then(showBooksSpinner)
:若sleep()
先回傳則then()
繼續呼叫 APIotherwise(log)
:若showBooks()
先回傳則發出 Rejected Promise 終止後續執行
41 行
let showBooksSpinner = pipe(
showSpinner,
() => Promise.all([booksP, sleep(SPINNER_TIME)]),
then(hideSpinner)
)
使用 pipe()
組合 showBooksSpinner()
,當 sleep()
早於 API response time 時再次處理 booksP
Promise:
showSpinner
:顯示 spinner() => Promise.all([booksP, sleep(SPINNER_TIME)])
:等所有 Promise 都執行完才會繼續,因此會確保 spinner 有最短顯示時間then(hideSpinner)
:隱藏 spinner
Conclusion
- 使用
Promise.race()
判斷1
秒鐘才顯示 spinner - 使用
Promise.all()
確保 spinner 有最短顯示時間