iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
自我挑戰組

從無到有打造驗證碼共享的 Line 機器人系列 第 28

防止使用者頻繁送出 Request & 倒數計時重新發送認證碼

以實務來說,總是會有一些情況導致使用者沒辦法正常收到認證碼,所以系統必須具備 retry on failure 的功能,讓使用者可以發送重新發送認證碼的需求。但使用者頻繁要求重新發送時又會造成伺服器的負擔,所以我們要讓在兩次發送期間要加入等待一段時間後才能再發送的限制。

防止使用者頻繁送出 Request

在先前的範例中,使用者如果連點按鈕就會造成 Request 重複送出,為了防止這種情況,我們可以採用以下幾種做法:

button disabled

修改 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>

loading mask

使用一個 z-index: 9999 的 loading mask 覆蓋全頁面,可讓使用者知道正在讀取中,也防止使用者點擊頁面其他部分造成流程異常。

Debounce / Throttle

Debounce 又稱去抖動,將連續觸發合併成一個事件,可延遲事件執行,在連續觸發時只執行一次,改善效能。

Throttle 又稱函式節流,顧名思義就是會限制函式的呼叫頻率,減少過快的呼叫達到節流,避免過度消耗資源。

可用 Lodash 實現這兩種功能。

ajax 攔截和取消

每次發送 Request 前先判斷是否還有 pending 中的同類請求,若存在則不發送 Request,或者取消先前 pending 中的 Request。

axios 中的 cancel token 就是不錯的實作方式

倒數計時再次發送

實作方式很簡單,添加一個計時器,在 API 回應我們成功送出驗證信時 disabled 按鈕,並開始倒數一分鐘,倒數完畢才解除 disabled 狀態。

setInterval 的問題

一般 JavaScript 要計時都會用 setTimeout / setInterval,但其實這兩者的計時並不精準,詳情可參考以下兩位大大的文章:

使用 requestAnimationFrame

所以這次就使用 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>

測試結果

有成功禁用按鈕 & 倒數計時~
result

鐵人賽也即將邁入尾聲了,這禮拜忙到爆,文章都是慢慢補完 Orz
希望接下來兩天可以正常發文~


上一篇
用 Line LIFF APP 實現信箱驗證綁定功能(4) - 表單驗證電子郵件地址
下一篇
用 Line LIFF APP 實現信箱驗證綁定功能(5) - 前後端認證功能
系列文
從無到有打造驗證碼共享的 Line 機器人30

尚未有邦友留言

立即登入留言