有15萬開發者使用LeanCloud服務,其中不乏知乎、懂球帝、愛範兒、拉卡拉等知名應用,LeanCloud提供了數據存儲、即時消息……等一站式服務,並從常用的用户管理需求出發,提供了郵箱驗證、短信驗證……等用户賬户相關的服務。
為防止攻擊者惡意發送海量短信造成用户賬户損失並影響正常業務,LeanCloud推出了免費圖形校驗碼服務,並且可以在應用設置中設置“強制短信驗證服務使用圖形校驗碼”。
Vue是目前使用較廣泛的三大前端框架之一,其數據驅動及組件化的特性使得前端開發更為快捷便利。
LeanCloud提供的由客户發起的短信發送場景主要有用户驗證、用户密碼重置,雖然不是很多場景,但如果每個場景下都單獨進行圖形校驗碼相關開發,則費時費力且對一些需要統一設置的參數調整不夠靈活。
本文在LeanCloud短信轟炸與圖形校驗碼官方文檔基礎上,從封裝需要出發開發一個簡單的短信圖形驗證Vue組件。
組件命名為Mobile,基於Element-UI的Form組件和Input組件進行開發,如果用户對佈局和樣式有特殊要求,只需要改為自己相應的組件,或者使用原生HTML元素並設置樣式即可,同時需要將Element-UI提供的$message改為自己相應的API調用。
組件行為
開發的Mobile組件用於發送短信驗證碼,因此需要能夠輸入手機號碼和圖形校驗碼,並可觸發發送短信的動作,觸發發送短信動作成功後,需要禁用發送短信功能,並進行倒計時,倒計時結束後才能重新發送短信。
因此具體的組件行為主要是以下幾點:
- 提供一個輸入手機號的輸入框,該輸入框內容可以由用户輸入,也可以從用户信息中獲取。
- 提供一個輸入圖形校驗碼的輸入框。
- 頁面加載完畢顯示圖形校驗碼。
- 提供一個發送短信的按鈕,用户點擊發送短信的按鈕,校驗圖形校驗碼,若校驗通過,使用手機號碼,並以圖形校驗碼校驗返回的validataionToken作為option參數發送短信。
- 短信發送成功,禁用發送短信的按鈕,啓動定時器進行倒計時,倒計時結束後恢復發送短信的按鈕。
- 組件使用Element-UI的Form組件的佈局,需要考慮el-form的labelWidth標籤寬度設置與父組件中的el-form匹配。
其中發送短信的行為必須調用不同場景下的API,因此我們需要將此按鈕綁定的事件emit到父組件,由父組件決定具體調用哪個API。
組件props
從上述組件行為出發,分析需要傳入組件的props:
- 表示手機號碼的屬性。我們發送短信驗證碼的目的是最終用於後續的驗證或密碼重置操作,並且有可能還需要將手機號碼保存到用户屬性中,因此該屬性可以從外部傳入,並且能夠在組件內部修改後返回父組件,因此該屬性必須是雙向綁定的,Vue組件中雙向綁定的屬性有兩種,一是自定義v-model,屬性名必須是value,一是可以使用.sync修飾符綁定的屬性,這裏將手機號碼屬性設置為Mobile組件的v-model屬性,屬性名為value。
- 通知Mobile組件短信已發送的屬性。屬性名為smsSent,類型為Boolean,以禁用發送短信的按鈕,並啓動倒計時。
- el-form的labelWidth屬性。設置默認值,並接受來自父組件中傳遞的數據以保持與父組件中其他元素/組件佈局一致。
組件的props選項如下:
props: {
labelWidth: {
type: String,
default: '100px'
},
value: String,
smsSent: Boolean
},
組件模板中,與props相關的考量主要有如下三方面:
- 組件的根元素是一個el-form組件,其label-width屬性綁定到來自父組件的labelWidth屬性,
<el-form :label-width="labelWidth">。 - 手機號碼輸入框使用el-input組件,綁定到value屬性,要實現雙向綁定,不能直接使用v-model進行數據綁定,而是要將v-model綁定轉換為v-bind:value屬性綁定和@input事件綁定,
<el-input :value="value" @input="value => $emit('input', value)">,這樣就可以實現“v-model透傳”。 - (間接)發送短信按鈕的禁用狀態。發送短信按鈕的禁用狀態由倒計時的計數器組件data數據觸發,當該數據不為0時,發送短信的按鈕禁用。倒計時觸發方式是通過父組件中綁定的smsSent屬性,因此需要在子組件中watch該屬性,並在該值為真是設置倒計時計數器,並通過setInterval進行倒計時。
圖形校驗碼加載
為在組件加載時顯示圖形校驗碼,需要在組件的mounted生命週期鈎子中調用LeanCloud的API。
在AV.Captcha.request()的回調中綁定校驗碼輸入框、圖形校驗碼元素以及發送短信按鈕元素,綁定參數對象的三個屬性均可以是表示元素id的string或實際HTMLElement,由於我們創建的是Vue組件,因此直接使用組件的$refs屬性來指定實際HTMLElement,需要注意的是,el-input中input元素是ref的$el屬性的children[0],而el-button中button元素是ref的$el。
綁定函數還需要傳入第二個參數,這是一個含有success和error方法的對象,用於提供圖形校驗碼校驗成功和失敗的操作。
發送短信驗證碼
發送短信驗證碼在傳遞的第二個參數對象的success方法中進行,在這裏,我們首先更新組件的smsSent屬性為false,這樣,當在父組件中實際完成短信發送之後設置smsSent為true時才會觸發針對smsSent屬性的watcher,同時,需要注意在父組件中綁定smsSent屬性時,必須使用.sync修飾符。然後向父組件emit自定義sendSmsCode事件,並將success回調時的validateToken參數透傳過去。
mounted () {
this.$emit('update:smsSent', false)
AV.Captcha.request().then((captcha) => {
captcha.bind({
textInput: this.$refs.captcha.$el.children[0],
image: this.$refs.captchaImage,
verifyButton: this.$refs.sendSms.$el
}, {
success: (validateToken) => {
this.$emit('sendSmsCode', validateToken)
},
error: () => {
this.$message.error('請輸入正確的圖形校驗碼!')
}
})
})
}
組件使用
首先在父組件的組件選項中添加包含Mobile組件的components,然後在模板中添加mobile組件。
<mobile v-model="mobileForm.mobile"
:sms-sent.sync="mobileForm.smsSent"
@sendSmsCode="sendSms"></mobile>
其中綁定sendSmsCode事件的方法如下:
sendSms (validateToken) {
this.sendSmsCode({
mobile: this.mobileForm.mobile,
validateToken
}).then(() => {
this.mobileForm.smsSent = true
})
},
完整組件代碼
<template>
<el-form class="mobile-form"
:label-width="labelWidth"
ref="mobile-form">
<el-form-item label="手機號碼" prop="mobile">
<el-input :value="value"
@input="value => $emit('input', value)"
:maxlength="11"
type="tel">
</el-input>
</el-form-item>
<el-form-item label="圖形校驗碼">
<el-input type="text" ref="captcha"></el-input>
<img ref="captchaImage">
</el-form-item>
<el-form-item>
<el-button type="info"
ref="sendSms"
:disabled="smsCodeCountingDown > 0 ||
value.length !== 11">
發送驗證碼
</el-button>
<span v-if="smsCodeCountingDown > 0">
{{smsCodeCountingDown}} 秒後重新發送
</span>
</el-form-item>
</el-form>
</template>
<script>
import AV from 'leancloud-storage'
export default {
data () {
return {
smsCodeCountingDown: 0
}
},
props: {
labelWidth: {
type: String,
default: '100px'
},
value: String,
smsSent: Boolean
},
watch: {
smsSent (val) {
if (val) {
this.smsCodeCountingDown = 30
let countingDownInterval = setInterval(() => {
this.smsCodeCountingDown--
if (this.smsCodeCountingDown === 0) {
clearInterval(countingDownInterval)
}
}, 1000)
}
}
},
mounted () {
AV.Captcha.request().then((captcha) => {
captcha.bind({
textInput: this.$refs.captcha.$el.children[0],
image: this.$refs.captchaImage,
verifyButton: this.$refs.sendSms.$el
}, {
success: (validateToken) => {
this.$emit('update:smsSent', false)
this.$emit('sendSmsCode', validateToken)
},
error: () => {
this.$message.error('請輸入正確的圖形校驗碼!')
}
})
})
}
}
</script>
<style scoped>
.sms-code-form {
width: 360px;
}
</style>