1. 背景
2. 方案
2.1 使用autocomplete关闭表单自动填充
如何关闭表单自动填充 - Web 安全 | MDN
2.2 使用隐藏的密码框将密码框和文本框隔离开
阻止浏览器记住密码 - CodeAntenna
2.3 手动关闭浏览器保存密码的功能
2.4 使用type=text实现密码框
- 2.2的方法对于我这里是不起作用的(暂时未找出原因);
- 2.1关闭表单的自动填充,但浏览器仍然记住了你的密码,只是不会告诉你它记住了;
- 2.3 通过浏览器设置,手动关闭,不让浏览器记住密码;
综合以上三个方法的不足,我们需要探索新的方法去避免浏览器记住用户密码。浏览器只有在检测到 input 的 type 为password的时候,会认为其是密码框,提供密码记忆功能。所以,如果我们的密码框type不是password呢?浏览器不会记录我们的密码,因此这里考虑使用type=text的方式来模拟type=password的效果。
2.4.1 需要处理的关键问题:
- 将输入值替换为特殊字符“•”;
- 将输入值用“•”替换之后,怎么拿到真实的输入字符串;
问题:
用“•”替换真实字符之后,下一次输入时,onChange事件中拿到的字符串只有最后一位时当前真实输入的字符;
- 如果用户输入了一个特殊字符“•”怎么办?
2.4.2 相关事件以及概念
- 用户输入字符的开始位置和结束位置:
使用键盘输入时的selStart、selEnd位置
鼠标选中时的selStart、selEnd位置
键盘输入字符"s"
之后,当前光标所在位置
- 在onSelect事件中,可以拿到输入字符前光标的起始位置、结束位置;
- 当用户切换为中文输入法时,需要判断输入是否结束
2.4.3 核心思路
- 根据字符串长度变化来判断是输入了字符,还是删除了字符,不需要关注输入的具体字符是什么。
用上一次的字符串长度oldInputval.current.length
和当前的新字符串长度做差值,得到字符串长度变化lengthChange
。
根据lengthChange
判断是插入了字符还是删除了字符,做对应的处理。如果是插入字符,oldInputval
需要从光标结束位置selEnd
开始后的所有值后移lengthChange
位,如果是删除字符,oldInputval
需要从curCursor
位置开始删除lengthChange
位;
2.4.4 核心代码
- 需要的变量以及state
state:
- displayValue :用特殊字符“•”替换真实密码之后的字符串;
变量:
- oldInputval :用来保存真实的字符串
- selStart: 保存输入光标开始位置
- selEnd :保存输入光标结束位置
- curTarget :保存input
- cursor: 保存当前光标
- compositionLockRef :判断输入是否完成
const Password = ()=>{
...
定义state和变量
...
useEffect(() => {
if (props.value) {
oldInputval.current = props.value;
if (type === 'text' && !visibility) {
setDisplayValue('•'.repeat(oldInputval.current.length));
} else {
setDisplayValue(props.value);
}
if (curTarget) {
cursor.current = oldInputval.current.length;
}
}
}, [props.value]);
useEffect(() => {
if (type === 'text' && !visibility) {
curTarget.current && curTarget.current.setSelectionRange(cursor.current, cursor.current);
}
});
useEffect(() => {
if (type === 'text') {
if (visibility) {
setDisplayValue(oldInputval.current);
} else {
setDisplayValue('•'.repeat(oldInputval.current.length));
}
}
}, [visibility]);
function handleChange(curVal: string, e: React.ChangeEvent<HTMLInputElement>) {
if (type === 'text') {
const curCursor = e.target.selectionEnd;
curTarget.current = e.target;
let newVal = '';
const oldStrLength = oldInputval.current.length;
const lengthChange = curVal.length - oldStrLength;
let oldStr: string[];
if (lengthChange >= 0) {
处理增加字符的情况
} else {
处理删除字符的情况
}
newVal = handleReplace(oldStr, curVal.split(''), selStart.current, curCursor - 1);
oldInputval.current = newVal;//更新当前的真实字符串
if (visibility) {
setDisplayValue(curVal);
} else {
setDisplayValue('•'.repeat(newVal.length));
}
cursor.current = curCursor;//保存下当前光标位置
props.onChange && props.onChange(newVal, e);//将真实值传递出去
} else {
setDisplayValue(curVal);
props.onChange && props.onChange(curVal, e);
}
//每次触发onChange事件之后都需要更新一下光标开始、结束位置
selStart.current = e.target.selectionStart;
selEnd.current = e.target.selectionEnd;
}
return (
<Input
...
type={visibility || type === 'text' ? 'text' : 'password'}
value={displayValue}
onChange={(v: string, e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(v, e);
}}
onCompositionStartCapture={(e: React.SyntheticEvent<HTMLInputElement, Event>) => {
//中文输入,
compositionLockRef.current = true;
selStart.current = e.target.selectionStart;
selEnd.current = e.target.selectionEnd;
}}
onCompositionEndCapture={(e: React.SyntheticEvent<HTMLInputElement, Event>) => {
//输入结束
compositionLockRef.current = false;
}}
onSelect={(e: React.SyntheticEvent<HTMLInputElement, Event>) => {
//只有compositionLockRef.current为false时才在onSelect中更新光标起始位置、结束位置
if (!compositionLockRef.current) {
selStart.current = e.target.selectionStart;
selEnd.current = e.target.selectionEnd;
}
}}
/>
);
}
使用onCompositionEndCapture、onCompositionStartCapture是为了处理输入法为中文模式下的selStart、selEnd更新问题;
3. 效果
当type=text时,输入密码,浏览器右侧上方没有小钥匙,避开了浏览器记住密码。