iOS Safari 验证码自动填充(OTP)改造思维流程复盘
目标:在 iOS Safari(以及 iOS WKWebView)中实现 短信验证码自动填充 与 手动输入 都可靠可用,并兼顾体验(软键盘、光标、自动提交等)。
1. 需求与现象梳理
1.1 业务需求
- 支持短信验证码(4 位)登录/注册
- 支持系统 OTP 自动填充(如 0987 可完整填入)
- 也要支持用户手动输入 4 位验证码
- 输入满 4 位后自动触发登录
- 页面进入验证码态希望尽量拉起软键盘(iOS Safari)
1.2 初始实现方式(多输入框)
- 页面上用 4 个
<input>分别输入 1 位 - 通过
keyup控制焦点移动、拼接vcode - 同时又放了一个“隐藏 input”,用于
autocomplete="one-time-code"
1.3 出现的核心问题
-
OTP 自动填充只进了第 1 个格子
例如验证码0987,第一个输入框只出现0,后面三个格子为空。 -
进入验证码页面时软键盘不自动弹起
调用了focus()但 iOS Safari 不弹键盘(或不稳定)。
2. 关键机制理解(正确的认知模型)
2.1 iOS 的 OTP 自动填充行为
- iOS 的 OTP 自动填充本质是:把整段验证码一次性写入某一个 input
- 它不会帮你“逐格分发到 4 个 input”
- 也不会按你的
keyup逻辑逐个触发输入事件
因此,“4 个输入框一格一位”的结构天然与 iOS OTP 的行为冲突,需要做适配。
2.2 autocomplete="off" 并不能可靠禁止 OTP 命中某个输入框
- OTP 自动填充属于系统/浏览器的特殊能力,不完全等价于表单 autofill
- 即使设置:
autocomplete="off"autocorrect="off"autocapitalize="off"iOS 仍可能把 OTP 灌入“它认为最像验证码输入框”的那个 input
2.3 type="number" + “一位限制”会放大问题
- OTP 一次性写入
0987时,如果目标 input 被你限制为 1 位(maxlength=1或自定义指令截断) - 结果必然变成只留下第 1 位(
0)
此外,type="number" 还会带来:
- 前导 0 行为不一致风险
- 不同浏览器对 value/输入法事件处理差异更大
2.4 iOS Safari 软键盘弹起规则(极关键)
iOS Safari 对脚本唤起键盘有严格限制:
- 只有在用户手势(touch/click)同步调用栈中触发的
focus()才稳定弹键盘 - 如果
focus()发生在:await之后then回调里nextTick里- 定时器里
- 弹窗/验证码组件结束后 很可能只聚焦不弹键盘(被系统拦截)
同时,过度隐藏/离屏 input(left:-100%、height:1px、opacity:0)也会降低弹键盘和 OTP 命中稳定性。
3. 错误点定位(为什么会“只填第一个 0”)
3.1 结构性错误:用 4 个可编辑 input 承接 OTP
- iOS OTP 并不会“填 4 个 input”
- 它会把
0987一次性写入某一个 input(通常是第一个格子/当前焦点所在 input)
3.2 触发方式错误:依赖 keyup 分发
- OTP 自动填充往往只触发一次
input/change - 不会按逐位输入触发
keyup(或行为不稳定) - 所以你的逐格逻辑根本接不到完整验证码
3.3 输入类型/限制错误:type="number" + 一位截断
- OTP 写入第一格后被截断,剩下格子自然不会再被填
3.4 “隐藏 input”位置/可见性错误(导致 OTP 不命中)
- 隐藏 input 被离屏/极小/完全透明,iOS 可能不把它当成“可填充目标”
- 焦 隐藏 input 被离屏/极小/完全透明,iOS 可能不把它当成“可填充目标”
- 焦点也可能被页面中可见输入框抢走,导致 OTP 灌入可见第一格
4. 需要调整的地方(改造策略)
4.1 正确的结构:单一真实 input + 4 格展示
- 页面保留 4 个“格子”,但它们是 div(或 readonly 展示),不参与真实输入
- 增加 1 个真实 input(覆盖在格子上),用于:
- 手动输入
- 粘贴输入
- OTP 自动填充
- 监听真实 input 的
@input,将值拆分到 4 个格子展示
这能从根上解决:
- OTP 只填第一格
- 前导 0 丢失
- 事件触发不完整
4.2 正确的事件:以 input 为主,不以 keyup 为主
input是唯一在“手动输入/粘贴/OTP 自动填充”三种场景下都可靠的事件keyup只适合逐键输入,不适合 OTP
4.3 正确的 input 配置(建议)
真实 input 推荐:
type="text"inputmode="numeric"autocomplete="one-time-code"maxlength="4"pattern="[0-9]*"(可选)
不要用:
type="number"(前导 0、事件差异等坑更多)
4.4 软键盘:用 touchstart 同步 focus(iOS Safari)
为了在 iOS Safari 上稳定弹键盘:
- 验证码区域绑定:
@touchstart.prevent="focusOtp"- 并保留
@click="focusOtp"兜底
focusOtp内不要nextTick/setTimeout,直接同步focus()
4.5 可见性:不要离屏/1px/完全 opacity 0
真实 input 如果想“看不见但可输入”:
- 位置:覆盖在验证码格子上(有正常宽高)
- 透明度:建议
opacity: 0.01而不是 0(部分 iOS 场景更稳定) - 保持可点击、可聚焦
5. 最终正确实现方式(推荐方案)
5.1 UI 结构
- 4 个格子:div 展示(含高亮/光标效果)
- 1 个真实 input:透明覆盖其上
5.2 数据流
- 用户输入/OTP 自动填充 -> 写入真实 input
@input获取完整字符串 -> 清洗为数字、截断 4 位- 更新:
vcode = rawotpDigits = raw.split('')分发到 4 格
- 若
raw.length === 4:自动提交signIn()
5.3 键盘策略
- 不强求“进入页面自动弹键盘”(iOS 限制无法 100%)
- 保证“用户点击验证码区域必弹键盘”(touchstart 同步 focus)
6. 注意事项清单(强烈建议照做)
- 不要用 4 个可编辑 input 来承接 OTP
- OTP 必须依赖
input事件处理 - 真实输入框用
type="text" + inputmode="numeric",避免type="number" autocomplete="off"不能可靠阻止 OTP 写入- iOS Safari 键盘弹起:focus 必须在用户手势同步调用栈中
- 真实 input 不要离屏/1px/完全透明,覆盖在格子上更稳
- 处理前导 0:不要 parseInt;用字符串清洗
replace(/\D/g,'') - 粘贴/自动填充一次性输入:必须能一次处理多位数字
- 满 4 位自动提交:放在
input处理内,而不是 keyup - WebOTP API(navigator.credentials.get)在 iOS Safari 支持有限:可保留但不要依赖
7. 结论总结(正确的验证码填充方式)
在 iOS Safari 中,最可靠的验证码输入/自动填充方案是:
- 一个真实 input(
autocomplete="one-time-code")接收所有输入(手动/粘贴/OTP) - 四个格子仅用于展示
- 用
@input统一解析与分发 - 用
@touchstart触发同步focus()保障键盘弹起
这样可以同时满足:
- OTP 自动填充完整进入(含前导 0)
- 手动输入体验一致
- 逻辑清晰、兼容性最好、维护成本最低