【手把手教学】基于vue封装一个安全键盘组件
基于vue封装一个安全键盘组件
为什么需要安全键盘
大部分中文应用弹出的默认键盘是简体中文输入法键盘,在输入用户名和密码的时候,如果使用简体中文输入法键盘,输入英文字符和数字字符的用户名和密码时,会自动启动系统输入法自动更正提示,然后用户的输入记录会被缓存下来。
系统键盘缓存最方便拿到的就是利用系统输入法自动更正的字符串输入记录。 缓存文件的地址是:
/private/var/mobile/Library/Keyboard/dynamic-text.dat
导出该缓存文件,查看内容,欣喜的发现一切输入记录都是明文存储的。因为系统不会把所有的用户输入记录都当作密码等敏感信息来处理。 一般情况下,一个常规 iPhone 用户的 dynamic-text.dat 文件,高频率出现的字符串就是用户名和密码。
使用自己定制的安全键盘的原因主要有:
- 避免第三方读取系统键盘缓存
- 防止屏幕录制 (自己定制的键盘按键不加按下效果)
实现方案
封装组件
首先建一个文件safeKeyboard.vue安全键盘子组件.
话不多说,直接上
才艺(代码)
<template>
<div class="keyboard">
<div class="key_title">
<p><img src="../../../../static/img/ic_logo@2x.png"><span>小猴子的安全键盘span>p>
div>
<p v-for="keys in keyList" :style="(keys.length<10&&keys.indexOf('ABC')<1&&keys.indexOf('del')<1&&keys.indexOf('suc')<1)?'padding: 0px 20px;':''">
<template v-for="key in keys">
<i v-if="key === 'top'" @click.stop="clickKey" @touchend.stop="clickKey" class="tab-top"><img class="top" :src='top_img'>i>
<i v-else-if="key === 'del'" @click.stop="clickKey" @touchend.stop="clickKey" class="key-delete"><img class="delete" src='删除图标路径'>i>
<i v-else-if="key === 'blank'" @click.stop="clickBlank" class="tab-blank">空格i>
<i v-else-if="key === 'suc'" @click.stop="success" @touchend.stop="success" class="tab-suc">确定i>
<i v-else-if="key === '.?123' || key === 'ABC'" @click.stop="symbol" class="tab-sym">{{(status==0||status==1)?'.?123':'ABC'}}i>
<i v-else-if="key === '123' || key === '#+='" @click.stop="number" class="tab-num">{{status==3?'123':'#+='}}i>
<i v-else @click.stop="clickKey" @touchend.stop="clickKey">{{key}}i>
template>
p>
div>
template>
<script>
export default {
data () {
return {
keyList: [],
status: 0, // 0 小写 1 大写 2 数字 3 符号
topStatus: 0, // 0 小写 1 大写
top_img: require('小写图片路径'),
lowercase: [
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['top', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'del'],
['.?123', 'blank', 'suc']
],
numbercase: [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['-', '/', ':', ';', '(', ')', '$', '&', '@', '"'],
['#+=', '.', ',', '?', '!', "'", 'del'],
['ABC', 'blank', 'suc']
],
symbolcase: [
['[', ']', '{', '}', '#', '%', '^', '*', '+', '='],
['_', '\\', '|', '~', '<', '>', '€', '`', '¥', '·'],
['123', '.', ',', '?', '!', "'", 'del'],
['ABC', 'blank', 'suc']
],
uppercase: [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['top', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', 'del'],
['.?123', 'blank', 'suc']
],
equip: !!navigator.userAgent.toLocaleLowerCase().match(/ipad|mobile/i)// 是否是移动设备
}
},
props: {
option: {
type: Object
}
},
mounted () {
this.keyList = this.lowercase
},
methods: {
tabHandle ({value = ''}) {
if (value.indexOf('tab-num') > -1) {
if (this.status === 3) {
this.status = 2
this.keyList = this.numbercase
} else {
this.status = 3
this.keyList = this.symbolcase
}
// 数字键盘数据
} else if (value.indexOf('delete') > -1) {
this.emitValue('delete')
} else if (value.indexOf('tab-blank') > -1) {
this.emitValue(' ')
} else if (value.indexOf('tab-point') > -1) {
this.emitValue('.')
} else if (value.indexOf('tab-sym') > -1) {
if (this.status === 0) {
this.topStatus = 0
this.status = 2
this.keyList = this.numbercase
} else if (this.status === 1) {
this.topStatus = 1
this.status = 2
this.keyList = this.numbercase
} else {
if (this.topStatus == 0) {
this.status = 0
this.top_img = require('小写图片路径')
this.keyList = this.lowercase
}else{
this.status = 1
this.keyList = this.uppercase
this.top_img = require('大写图片路径')
}
}
// 符号键盘数据
} else if (value.indexOf('top') > -1) {
if (this.status === 0) {
this.status = 1
this.keyList = this.uppercase
this.top_img = require('大写图片路径')
} else {
this.status = 0
this.keyList = this.lowercase
this.top_img = require('小写图片路径')
}
} else if (value.indexOf('tab-suc') > -1) {
this.$emit('closeHandle', this.option) // 关闭键盘
}
},
number (event) {
this.tabHandle(event.srcElement.classList)
},
clickBlank (event) {
this.tabHandle(event.srcElement.classList)
},
symbol (event) {
this.tabHandle(event.srcElement.classList)
},
success (event) {
this.tabHandle(event.srcElement.classList)
},
english (event) {
this.tabHandle(event.srcElement.classList)
},
clickKey (event) {
if (event.type === 'click' && this.equip) return
let value = event.srcElement.innerText
value ? this.emitValue(value) : this.tabHandle(event.srcElement.classList)
},
emitValue (key) {
this.$emit('keyVal', key) // 向父组件传值
},
closeModal (e) {
if (e.target !== this.option.sourceDom) {
this.$emit('closeHandle', this.option)
this.keyList = this.lowercase
}
},
}
}
script>
<style scoped lang="scss">
.keyboard {
width: 100%;
margin: 0 auto;
font-size: 18px;
border-radius: 2px;
background-color: #fff;
box-shadow: 0 -2px 2px 0 rgba(89,108,132,0.20);
user-select: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
pointer-events: auto;
.key_title{
height: 84px;
font-size: 32px;
color: #0B0B0B;
overflow: hidden;
margin-bottom: 16px;
p{
display: flex;
justify-content: center;
align-items: center;
min-width: 302px;
height: 32px;
margin: 32px auto 0px;
img{
width: 32px;
height: 32px;
margin-right: 10px;
}
}
}
p {
width: 99%;
margin: 0 auto;
height: 84px;
margin-bottom: 24px;
display: flex;
display: -webkit-box;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
box-sizing: border-box;
i {
position: relative;
display: block;
margin: 0px 5px;
height: 84px;
line-height: 84px;
font-style: normal;
font-size: 48px;
border-radius: 8px;
width: 64px;
background-color: #F2F4F5;
box-shadow: 0 2px 0 0 rgba(0,0,0,0.25);
text-align: center;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
-webkit-box-flex: 1;
img{
width: 48px;
height: 48px;
}
}
i:first-child{
margin-left: 0px
}
i:last-child{
margin-right: 0px
}
i:active {
background-color: #A9A9A9;
}
.tab-top, .key-delete, .tab-num, .tab-eng, .tab-sym{
background-color: #CED6E0;
}
.tab-top,.key-delete {
display: flex;
justify-content: center;
align-items: center;
width: 84px;
height: 84px;
}
.tab-top{
margin-right: 30px;
font-size: 32px;
}
.key-delete{
margin-left: 30px;
}
.tab-num, .tab-eng, .tab-sym{
font-size: 32px;
}
.tab-point {
width: 70px;
}
.tab-blank, .tab-suc{
text-align: center;
line-height: 84px;
font-size: 32px;
color: #000;
}
.tab-blank{
flex: 2.5;
}
.tab-suc{
background-color: #CFA46A;
}
}
p:last-child{
margin-bottom: 8px;
}
}
style>
但是,键盘的特性是,点击除键盘和输入框以外的地方,键盘收起。
所以还需要一个clickoutside.js文件,用来自定义一个指令,实现需求:
代码如下:
export default {
bind(el, binding, vnode) {
function documentHandler(e) {
if (el.contains(e.target)) {
return false;
}
if (binding.expression) {
binding.value(e);
}
}
el.__vueClickOutside__ = documentHandler;
document.addEventListener('click', documentHandler);
},
unbind(el, binding) {
document.removeEventListener('click', el.__vueClickOutside__);
delete el.__vueClickOutside__;
}
};
然后在safeKeyboard.vue中引入:
import clickoutside from './clickoutside'
并注册局部指令:
directives: { clickoutside }
然后绑定方法:
class="keyboard" v-clickoutside="closeModal">
声明方法:
closeModal (e) {
if (e.target !== this.option.sourceDom) {
this.$emit('closeHandle', this.option)
this.keyList = this.lowercase
}
},
安全键盘组件就构建完成了,接下来是在需要用到安全键盘的页面引入使用了。
使用组件
引入组件
import Keyboard from './safeKeyboard'
components: {
Keyboard
}
使用范例
type="password" ref="setPwd" v-model='password'/>
v-if="option.show" :option="option" @keyVal="getInputValue" @closeHandle="onLeave">
键盘相关数据对象及方法
- option
option: {
show: false, // 键盘是否显示
sourceDom: '', // 键盘绑定的Input元素
_type: '' // 键盘绑定的input元素ref
},
- getInputValue
getInputValue(val)会接收键盘录入的数据,val是输入的单个字符或者是删除操作,由于是单个字符,所以需在方法中手动拼接成字符串。在方法中根据option._type区分是哪个输入框的数据。
- onLeave
onLeave()相当于blur,这是由于在移动端H5项目中,input获取焦点时会调起手机软键盘,所以需要禁止软键盘被调起来,办法是:
document.activeElement.blur() // ios隐藏键盘
this.$refs.setPwd.blur() // android隐藏键盘
就相当于强制使input元素处于blur状态,那么软键盘就不会被调起,所以如果要做blur监听,就需要onLeave()。
但是这样出现了一个新的问题,输入框里面没有光标!!虽然不影响业务逻辑,但是用户用起来会很不舒服。
所以,只能和input元素说再见了,自己手写一个吧:
输入框组件
再来一个子组件cursorBlink.vue
<template>
<div class="cursor-blink" @click.stop="isShow">
<span v-if="pwd.length>0" :style="options.show?'':'border:0;animation:none;'" class="blink">{{passwordShow}}span>
<span v-else style="color: #ddd" :style="options.show?'':'border:0;animation:none;'" class="blink_left">{{options.desc}}span>
div>
template>
<script>
export default {
props: {
pwd: {
type: String
},
options: {
type: Object
},
},
data(){
return {
passwordShow: '',
}
},
mounted() {
if(this.pwd.length > 0){
for (let i = 0; i < this.pwd.length; i++) {
this.passwordShow += '*' // 显示为掩码
}
}
},
watch: {
pwd(curVal, oldVal){
if (oldVal.length < curVal.length) {
// 输入密码时
this.passwordShow += '*'
} else if (oldVal.length > curVal.length) {
// 删除密码时
this.passwordShow = this.passwordShow.slice(0, this.passwordShow.length - 1)
}
}
},
methods: {
isShow(){
this.$emit('cursor')
}
},
}
script>
<style lang="scss" scoped>
.cursor-blink{
display: inline-block;
width: 500px;
height: 43px;
letter-spacing: 0px;
word-spacing: 0px;
padding: 2px 0px;
font-size: 28px;
overflow: hidden;
.blink,.blink_left{
display: inline;
margin: 0px;
}
.blink{ // 输入密码后
border-right: 2px solid #000;
animation: blink 1s infinite steps(1, start);
}
.blink_left{ // 输入密码前
border-left: 2px solid #000;
animation: blinkLeft 1s infinite steps(1, start);
}
}
@keyframes blink {
0%, 100% {
border-right: 2px solid #fff;
}
50% {
border-right: 2px solid #000;
}
}
@keyframes blinkLeft {
0%, 100% {
border-left: 2px solid #fff;
}
50% {
border-left: 2px solid #000;
}
}
style>
引入之后光荣的接替input的位置:
<CursorBlink :pwd='password' ref="setPwd" :options='option2' @cursor="onFocus"></CursorBlink>
数据方法说明:
option2: {
show: false, // 区分输入前输入后
desc: '请重复输入密码' // 相当于placeholder
},
onFocus() 相当于input标签的focus
这样一个完美的安全键盘就做好了。
我是摸鱼君,你的【三连】就是摸鱼君创作的最大动力,如果本篇文章有任何错误和建议,欢迎大家留言!
来源:juejin.cn/post/7309158055018168346