React写input脱敏组件
最近公司在做数据整改,所以就要求前端对输入/反显的用户信息进行脱敏处理。例如
13222334455 // 原手机号
132****4455 // 脱敏后的手机号
123456202304045566 // 原身份证号
123456********5566 // 脱敏后的手机号
好了,需求已经很明显了,现在我们就开始撸代码吧
React中的受控组件/非受控组件
在正式开始之前,我们先理解一个概念。受控组件/非受控组件。
受控组件:受我们控制的组件 非受控组件:不受我们控制的组件
额,这貌似是废话哈,我们举个例子吧。
我们知道,在React中输入一个input控件的话,我们并没有任何指令让input实现输入的同步更新。
class InputComponent extends React.Component {
render () {
return <input name="name" />
}
}
那么我们如何能实现输入的同步更新呢,我们可以定义一个value值
class InputComponent extends React.Component {
constructor (props) {
super(props);
this.state = { name: "senga" }
}
render () {
return <input name="name" value={this.state.name}/>
}
}
当我们进行输入的时候,我们发现input的值没有更新,那是因为此时input的value被this.state.value所控制,value是只读的。如果要想实现更新,我们可以用change来对输入的内容进行监听,并实时更新输入的值
class InputComponent extends React.Component {
constructor (props) {
super(props);
this.state = { name: "senga" }
}
onChange (e) {
console.log(e.target.value);
this.setState({ name: e.target.value })
}
render () {
return <input name="name" value={this.state.name}/>
}
}
现在就实现了state和UI的同步更新。
在 React 中,表单元素通过组件的 state 属性来自己维护 state,并根据用户输入调用[setState()
]来进行数据更新,使 React 的 state 成为“唯一数据源”,被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
从上面的例子中,我们可以看出,受控组件组件主要是通过保存state来实现值的更新,而我们要做的脱敏组件如果使用受控组件,就不太符合要求。因为当我们输入13222334455时,要在输入完成后自动变成132****4455,我们输入的是明文的数字,要显示的是脱敏的数字,所以就得要我们手动设置值。而手动设置值,就需要用到非受控组件。具体非受控组件如何使用,我们先按下不表。
脱敏组件
基本框架
import { useState } from "react";
import { EyeOutlined,EyeInvisibleOutlined } from '@ant-design/icons';
import './style.css';
const Com = () => {
// 显示小眼睛icon
const [show,setShow] = useState(0)
return (
<>
<input />
<span className="icon" onClick={()=>setShow(show ^ 1)}>
{
show ? <EyeOutlined /> : <EyeInvisibleOutlined />
}
</span>
</>
);
};
export default Com;
上述代码是我们的基本框架,主要添加了一个input组件和加了眼睛的icon,然后点击icon进行切换操作
添加类型
由于我们的需求中,只要求对姓名、手机号、身份证号、银行卡号进行脱敏处理,所以我们可以先定义一下类型,设定每种类型需要保留的位数,之所以设置这个值,是为了在输入时校验,如果当前输入的位数少于保留的位数就不进行脱敏处理
脱敏函数
const handleFormat = value => {
if (!value) {
// 设置脱敏的值
setMValue('')
return
}
let str = ''
const len = value.length
const star = Math.max(0, len - preserveNum[format])
if (len <= preserveNum[format]) {
str = value
} else {
switch (format) {
case 'cellphone':
str = value.slice(0, 3) + '*'.repeat(star) + value.slice(3 + star)
break
case 'bankCard':
str = value.slice(0, 4) + '*'.repeat(star) + value.slice(4 + star)
break
case 'identity':
str = value.slice(0, 6) + '*'.repeat(star) + value.slice(6 + star)
break
case 'name':
str = value.slice(0, 1) + '*'.repeat(star)
break
default:
str = value
break
}
}
// 设置脱敏的值
setMValue(str)
return str
}
为了校验效果,我们可以定义一些变量,value是明文的值,mValue是脱敏后的值,然后利用ref获取input的节点,手动设置初始化的值,代码如下:
父组件
<Com data="13222334455" format="cellphone"/>
脱敏组件
import { useState,useRef,useEffect } from "react";
import { EyeOutlined,EyeInvisibleOutlined } from '@ant-design/icons';
import './style.css';
// 需要保留的位数,例如
// cellphone/bankcard/identity/name如果是手机号保留前3后4,银行卡前4后6,身份证号前6后4,姓名前1
const preserveNum = {
cellphone: 7,
bankCard: 10,
identity: 10,
name: 1,
}
const Com = ({
data = '', // 初始值
format = '' // 格式,枚举值为 cellphone|bankcard|identity|name
}) => {
// 显示小眼睛icon
const [show,setShow] = useState(0)
// 明文
const [value,setValue] = useState('')
// 脱敏后的字符
const [mValue,setMValue] = useState('')
// 获取input的节点
const inputRef = useRef(null)
// 更新value
useEffect(() => {
if (data) {
setValue(data)
handleFormat(data)
}
}, [data])
// 设置input值
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = show ? value : mValue
}
}, [show,value, mValue])
// 脱敏处理
const handleFormat = value => {
if (!value) {
setMValue('')
return
}
let str = ''
const len = value.length
const star = Math.max(0, len - preserveNum[format])
if (len <= preserveNum[format]) {
str = value
} else {
switch (format) {
case 'cellphone':
str = value.slice(0, 3) + '*'.repeat(star) + value.slice(3 + star)
break
case 'bankCard':
str = value.slice(0, 4) + '*'.repeat(star) + value.slice(4 + star)
break
case 'identity':
str = value.slice(0, 6) + '*'.repeat(star) + value.slice(6 + star)
break
case 'name':
str = value.slice(0, 1) + '*'.repeat(star)
break
default:
str = value
break
}
}
setMValue(str)
return str
}
return (
<>
<input ref={inputRef}/>
<span className="icon" onClick={()=>setShow(show ^ 1)}>
{
show ? <EyeOutlined /> : <EyeInvisibleOutlined />
}
</span>
</>
);
};
export default Com;
效果:
现在我们已经基本实现了对默认展示的内容进行脱敏的处理,下一步我们要做的是如何在输入时进行脱敏
输入处理
处理输入操作,就是将输入的值通过onChange对值进行监听,然后将值更新到value、mValue中。如果是输入时是处于明文的状态,这很好办,不需要脱敏处理。但是如果输入时小眼睛icon就是闭合的呢,我们如何将当前输入的值转换为明文呢?
问题提出来了,我们先整理一下思路:
1.当处于脱敏输入中时,input的值有很大概率是带*的;
2.value的值始终是明文的,现在要做的就是将新输入的内容截取出来,然后和明文的value进行拼接,这样就会得到一个新的明文value
3.将得到的新的明文value进行handleFormat脱敏处理
好了,具体操作已经很明确了。现在的问题就是如何在输入时,将新输入的内容截取出来,这时就用到了input的一个隐藏属性selectionStart
。
selectionStart
可以获取光标的起始位置,保存光标位置,然后将当前的值与value进行对比,找出新输入/删除的值
import { useState,useRef,useEffect } from "react";
import { EyeOutlined,EyeInvisibleOutlined } from '@ant-design/icons';
import './style.css';
// 需要保留的位数,例如
// cellphone/bankcard/identity/name如果是手机号保留前3后4,银行卡前4后6,身份证号前6后4,姓名前1
const preserveNum = {
cellphone: 7,
bankCard: 10,
identity: 10,
name: 1,
}
const Com = ({
data = '', // 初始值
format = '' // 格式,枚举值为 cellphone|bankcard|identity|name
}) => {
// 显示小眼睛icon
const [show,setShow] = useState(0)
// 明文
const [value,setValue] = useState('')
// 脱敏后的字符
const [mValue,setMValue] = useState('')
// 获取input的节点
const inputRef = useRef(null)
// 更新value
useEffect(() => {
if (data) {
setValue(data)
handleFormat(data)
}
}, [data])
// 设置input值
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = show ? value : mValue
}
}, [show,value, mValue])
// 脱敏处理
const handleFormat = value => {
if (!value) {
setMValue('')
return
}
let str = ''
const len = value.length
const star = Math.max(0, len - preserveNum[format])
if (len <= preserveNum[format]) {
str = value
} else {
switch (format) {
case 'cellphone':
str = value.slice(0, 3) + '*'.repeat(star) + value.slice(3 + star)
break
case 'bankCard':
str = value.slice(0, 4) + '*'.repeat(star) + value.slice(4 + star)
break
case 'identity':
str = value.slice(0, 6) + '*'.repeat(star) + value.slice(6 + star)
break
case 'name':
str = value.slice(0, 1) + '*'.repeat(star)
break
default:
str = value
break
}
}
setMValue(str)
return str
}
const handleChange = e => {
// 获取光标
const selectionStart = e.target.selectionStart
// 光标位置
const ind = selectionStart - 1
let actualVal = value || ''
let currentVal = e.target.value
const isAdd = currentVal.length > actualVal.length
const num = Math.abs(currentVal.length - actualVal.length)
if (isAdd) {
actualVal =
actualVal.slice(0, ind - num + 1) +
currentVal.slice(ind - num + 1, ind + 1) +
actualVal.slice(ind - num + 1)
} else {
actualVal = actualVal.slice(0, ind + 1) + actualVal.slice(ind + num + 1)
}
setValue(actualVal)
handleFormat(actualVal)
}
return (
<>
<input ref={inputRef} onChange={e => handleChange(e)}/>
<span className="icon" onClick={()=>setShow(show ^ 1)}>
{
show ? <EyeOutlined /> : <EyeInvisibleOutlined />
}
</span>
</>
);
};
export default Com;
到这一步,我们已经完成了80%,还剩的20%是啥呢,就是我们即将遇到的两个坑
踩坑记录
不支持中文的输入
运行上述代码时,我们可以发现,当我们在输入中文时,中文还没输入完成,input已经有值了,刚开始想着加个防抖,后来试了一下,不太行,搜了一下,原来还有一个隐藏的技能onCompositionStart
onCompositionEnd
。利用这两个属性,我们可以判断是在进行中文输入,如果正在输入中文,不处理
import { useState,useRef,useEffect } from "react";
import { EyeOutlined,EyeInvisibleOutlined } from '@ant-design/icons';
import './style.css';
// 需要保留的位数,例如
// cellphone/bankcard/identity/name如果是手机号保留前3后4,银行卡前4后6,身份证号前6后4,姓名前1
const preserveNum = {
cellphone: 7,
bankCard: 10,
identity: 10,
name: 1,
}
const Com = ({
data = '', // 初始值
format = '' // 格式,枚举值为 cellphone|bankcard|identity|name
}) => {
// 显示小眼睛icon
const [show,setShow] = useState(0)
// 明文
const [value,setValue] = useState('')
// 脱敏后的字符
const [mValue,setMValue] = useState('')
// 获取input的节点
const inputRef = useRef(null)
// 更新value
useEffect(() => {
if (data) {
setValue(data)
handleFormat(data)
}
}, [data])
// 设置input值
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = show ? value : mValue
}
}, [show,value, mValue])
// 脱敏处理
const handleFormat = value => {
if (!value) {
setMValue('')
return
}
let str = ''
const len = value.length
const star = Math.max(0, len - preserveNum[format])
if (len <= preserveNum[format]) {
str = value
} else {
switch (format) {
case 'cellphone':
str = value.slice(0, 3) + '*'.repeat(star) + value.slice(3 + star)
break
case 'bankCard':
str = value.slice(0, 4) + '*'.repeat(star) + value.slice(4 + star)
break
case 'identity':
str = value.slice(0, 6) + '*'.repeat(star) + value.slice(6 + star)
break
case 'name':
str = value.slice(0, 1) + '*'.repeat(star)
break
default:
str = value
break
}
}
setMValue(str)
return str
}
const handleChange = e => {
if (inputRef.current.cnIputFlag) return
// 获取光标
const selectionStart = e.target.selectionStart
// 光标位置
const ind = selectionStart - 1
let actualVal = value || ''
let currentVal = e.target.value
const isAdd = currentVal.length > actualVal.length
const num = Math.abs(currentVal.length - actualVal.length)
if (isAdd) {
actualVal =
actualVal.slice(0, ind - num + 1) +
currentVal.slice(ind - num + 1, ind + 1) +
actualVal.slice(ind - num + 1)
} else {
actualVal = actualVal.slice(0, ind + 1) + actualVal.slice(ind + num + 1)
}
setValue(actualVal)
handleFormat(actualVal)
}
// 主要为了校验是否在进行中文输入
const composition = e => {
if (e.type === 'compositionend') {
inputRef.current.cnIputFlag = false
handleChange(e)
} else {
inputRef.current.cnIputFlag = true
}
}
return (
<>
<input ref={inputRef}
onChange={e => handleChange(e)}
onCompositionStart={composition}
onCompositionEnd={composition}
/>
<span className="icon" onClick={()=>setShow(show ^ 1)}>
{
show ? <EyeOutlined /> : <EyeInvisibleOutlined />
}
</span>
</>
);
};
export default Com;
效果:
光标乱跑
当我们在文本框中间输入时,发现,当第一次输入后,光标跑到了末尾。这是因为当我们在输入后,对input重新设置值,页面渲染,所以光标跑到了末尾。要解决这个光标问题,还是使用selectionStart
和selectionEnd
,一个记录光标开始的位置,一个记录光标结束的位置。然后值重新设置后,重新设置光标位置
完整代码
import { useState,useRef,useEffect } from "react";
import { EyeOutlined,EyeInvisibleOutlined } from '@ant-design/icons';
import './style.css';
// 需要保留的位数,例如
// cellphone/bankcard/identity/name如果是手机号保留前3后4,银行卡前4后6,身份证号前6后4,姓名前1
const preserveNum = {
cellphone: 7,
bankCard: 10,
identity: 10,
name: 1,
}
// 保存光标位置
let selectionStart = 0,
selectionEnd = 0
const Com = ({
data = '', // 初始值
format = '' // 格式,枚举值为 cellphone|bankcard|identity|name
}) => {
// 显示小眼睛icon
const [show,setShow] = useState(0)
// 明文
const [value,setValue] = useState('')
// 脱敏后的字符
const [mValue,setMValue] = useState('')
// 获取input的节点
const inputRef = useRef(null)
// 更新value
useEffect(() => {
if (data) {
setValue(data)
handleFormat(data)
}
}, [data])
// 设置input值
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = show ? value : mValue
// 还原光标位置,因为重新设置值后,页面刷新会导致光标回到最末尾的位置
inputRef.current.setSelectionRange(selectionStart, selectionEnd)
}
}, [show,value, mValue])
// 脱敏处理
const handleFormat = value => {
if (!value) {
setMValue('')
return
}
let str = ''
const len = value.length
const star = Math.max(0, len - preserveNum[format])
if (len <= preserveNum[format]) {
str = value
} else {
switch (format) {
case 'cellphone':
str = value.slice(0, 3) + '*'.repeat(star) + value.slice(3 + star)
break
case 'bankCard':
str = value.slice(0, 4) + '*'.repeat(star) + value.slice(4 + star)
break
case 'identity':
str = value.slice(0, 6) + '*'.repeat(star) + value.slice(6 + star)
break
case 'name':
str = value.slice(0, 1) + '*'.repeat(star)
break
default:
str = value
break
}
}
setMValue(str)
return str
}
const handleChange = e => {
if (inputRef.current.cnIputFlag) return
// 获取光标
selectionStart = e.target.selectionStart
selectionEnd = e.target.selectionEnd
// 光标位置
const ind = selectionStart - 1
let actualVal = value || ''
let currentVal = e.target.value
const isAdd = currentVal.length > actualVal.length
const num = Math.abs(currentVal.length - actualVal.length)
if (isAdd) {
actualVal =
actualVal.slice(0, ind - num + 1) +
currentVal.slice(ind - num + 1, ind + 1) +
actualVal.slice(ind - num + 1)
} else {
actualVal = actualVal.slice(0, ind + 1) + actualVal.slice(ind + num + 1)
}
setValue(actualVal)
handleFormat(actualVal)
}
// 主要为了校验是否在进行中文输入
const composition = e => {
if (e.type === 'compositionend') {
inputRef.current.cnIputFlag = false
handleChange(e)
} else {
inputRef.current.cnIputFlag = true
}
}
return (
<>
<input ref={inputRef}
onChange={e => handleChange(e)}
onCompositionStart={composition}
onCompositionEnd={composition}
/>
<span className="icon" onClick={()=>setShow(show ^ 1)}>
{
show ? <EyeOutlined /> : <EyeInvisibleOutlined />
}
</span>
</>
);
};
export default Com;
到此,一个基本的input脱敏组件就基本做完了
转载自:https://juejin.cn/post/7218069978045874232