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进行切换操作

image.png

添加类型

由于我们的需求中,只要求对姓名、手机号、身份证号、银行卡号进行脱敏处理,所以我们可以先定义一下类型,设定每种类型需要保留的位数,之所以设置这个值,是为了在输入时校验,如果当前输入的位数少于保留的位数就不进行脱敏处理

脱敏函数

  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;

复制代码

效果:

image.png

现在我们已经基本实现了对默认展示的内容进行脱敏的处理,下一步我们要做的是如何在输入时进行脱敏

输入处理

处理输入操作,就是将输入的值通过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;

复制代码

效果: image.png

光标乱跑

当我们在文本框中间输入时,发现,当第一次输入后,光标跑到了末尾。这是因为当我们在输入后,对input重新设置值,页面渲染,所以光标跑到了末尾。要解决这个光标问题,还是使用selectionStartselectionEnd,一个记录光标开始的位置,一个记录光标结束的位置。然后值重新设置后,重新设置光标位置

完整代码

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脱敏组件就基本做完了

猜你喜欢

转载自juejin.im/post/7218069978045874232