以實務來說,總是會有一些情況導致使用者沒辦法正常收到認證碼,所以系統必須具備 retry on failure 的功能,讓使用者可以發送重新發送認證碼的需求。但使用者頻繁要求重新發送時又會造成伺服器的負擔,所以我們要讓在兩次發送期間要加入等待一段時間後才能再發送的限制。
在先前的範例中,使用者如果連點按鈕就會造成 Request 重複送出,為了防止這種情況,我們可以採用以下幾種做法:
修改 BindMailForm.vue 如下:
<script setup>
import {onMounted, ref, defineEmits} from 'vue'
import {sendValidationCodePost} from '/src/service/api'
import * as yup from 'yup';
const userName = ref("");
const userToken = ref("");
const userEmail = ref("");
const inputEmail = ref("");
// 增加一個狀態變數判斷表單是否正在送出
const onSubmit = ref(false);
const emit = defineEmits(['nextStep'])
const mailSchema = yup.string().email().required();
const submit = async (mail) => {
// 表單請求送出時
onSubmit.value = true;
const isMailValid = await mailSchema.isValid(mail);
if (isMailValid) {
sendValidationCodePost(userName.value, mail, userToken.value)
.then((res) => {
console.log("res: ", res);
const result = res.data.message;
emit('nextStep', result);
})
.catch((err) => {
console.log("err: ", err);
})
.finally(() => {onSubmit.value = false}); // 表單請求處理結束
} else {
onSubmit.value = false; // 表單請求處理結束
alert('請輸入有效的信箱地址');
}
}
onMounted(() => {
liff.ready.then(() => {
const user = liff.getDecodedIDToken();
userName.value = user && user.name;
userToken.value = user && user.sub;
userEmail.value = user && user.email;
})
})
</script>
<template>
<template v-if="userEmail">
<p>將發送身份認證碼到 {{ userEmail }}</p>
// 當 onSubmit 為 true 時幫按鈕加上 disabled
<button type="button" class="btn" @click="submit(userEmail)" :disabled="onSubmit">確定</button>
</template>
<template v-else>
<p>發送身份認證碼到 <input type="email" v-model="inputEmail" placeholder="請輸入 Email"></p>
// 當 onSubmit 為 true 時幫按鈕加上 disabled
<button type="button" class="btn" @click="submit(inputEmail)" :disabled="onSubmit">確定</button>
</template>
</template>
使用一個 z-index: 9999
的 loading mask 覆蓋全頁面,可讓使用者知道正在讀取中,也防止使用者點擊頁面其他部分造成流程異常。
Debounce 又稱去抖動,將連續觸發合併成一個事件,可延遲事件執行,在連續觸發時只執行一次,改善效能。
Throttle 又稱函式節流,顧名思義就是會限制函式的呼叫頻率,減少過快的呼叫達到節流,避免過度消耗資源。
可用 Lodash 實現這兩種功能。
每次發送 Request 前先判斷是否還有 pending 中的同類請求,若存在則不發送 Request,或者取消先前 pending 中的 Request。
axios 中的 cancel token 就是不錯的實作方式
實作方式很簡單,添加一個計時器,在 API 回應我們成功送出驗證信時 disabled 按鈕,並開始倒數一分鐘,倒數完畢才解除 disabled 狀態。
一般 JavaScript 要計時都會用 setTimeout / setInterval,但其實這兩者的計時並不精準,詳情可參考以下兩位大大的文章:
所以這次就使用 requestAnimationFrame 來實作倒數計時功能吧~
修改 BindMail.vue
<script setup>
import {onMounted, ref, provide} from 'vue'
import BindMailForm from "./BindMailForm.vue";
import BindMailResult from "./BindMailResult.vue";
const errorMsg = ref("");
const bindStep = ref('form');
const resMessage = ref('');
provide('resMessage', resMessage);
const next = (event) => {
resMessage.value = event
bindStep.value = 'result'
}
// 加入 back step
const back = () => {
resMessage.value = ""
bindStep.value = 'form'
}
const initializeApp = () => {
if (!liff.isLoggedIn() || !liff.isInClient()) {
errorMsg.value = "please use line liff open";
}
};
onMounted(() => {
liff.init({
liffId: 'YOUR_LIFF_ID'
}).then(() => {
initializeApp();
}).catch((err) => {
errorMsg.value = "initialize LIFF fail";
});
});
</script>
<template>
<h1>驗證碼小幫手 - 身份認證</h1>
// 綁定 backStep emit event
<component v-if="!errorMsg" :is="(bindStep === 'form') ? BindMailForm : BindMailResult" @backStep="back" @nextStep="next"></component>
<p v-else class="error">{{ errorMsg }}</p>
</template>
修改 BindMailResult.vue
<template>
<p>{{(result === 'success') ? '已將驗證碼發送至信箱' : '發送失敗,請稍後再試'}}</p>
<div class="inline-btns">
// 增加再次發送的按鈕
<button type="button" class="btn" @click="emit('backStep')" :disabled="backDisabled">
再次發送
<span v-if="interval < 60000">({{60 - Math.floor(interval/1000)}})</span>
</button>
<button type="button" class="btn" @click="closeLiff">關閉</button>
</div>
</template>
<script setup>
import {ref, inject, onMounted} from "vue";
const start = ref(null);
const interval = ref(0);
const backDisabled = ref(true);
const emit = defineEmits(['backStep', 'nextStep']);
const result = inject('resMessage');
const closeLiff = () => {
liff.closeWindow();
}
// 使用 requestAnimationFrame 實作倒數計時的功能
const countDown = (timestamp) => {
if (!start.value) start.value = timestamp;
interval.value = timestamp - start.value;
if (interval.value < 60000) {
window.requestAnimationFrame(countDown);
} else {
backDisabled.value = false;
}
}
onMounted(() => {
start.value = null;
window.requestAnimationFrame(countDown);
})
</script>
有成功禁用按鈕 & 倒數計時~
鐵人賽也即將邁入尾聲了,這禮拜忙到爆,文章都是慢慢補完 Orz
希望接下來兩天可以正常發文~