h5 自动注入验证码

105 阅读6分钟

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 出现的核心问题

  1. OTP 自动填充只进了第 1 个格子
    例如验证码 0987,第一个输入框只出现 0,后面三个格子为空。

  2. 进入验证码页面时软键盘不自动弹起
    调用了 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:1pxopacity: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 = raw
    • otpDigits = raw.split('') 分发到 4 格
  • raw.length === 4:自动提交 signIn()

5.3 键盘策略

  • 不强求“进入页面自动弹键盘”(iOS 限制无法 100%)
  • 保证“用户点击验证码区域必弹键盘”(touchstart 同步 focus)

6. 注意事项清单(强烈建议照做)

  1. 不要用 4 个可编辑 input 来承接 OTP
  2. OTP 必须依赖 input 事件处理
  3. 真实输入框用 type="text" + inputmode="numeric",避免 type="number"
  4. autocomplete="off" 不能可靠阻止 OTP 写入
  5. iOS Safari 键盘弹起:focus 必须在用户手势同步调用栈中
  6. 真实 input 不要离屏/1px/完全透明,覆盖在格子上更稳
  7. 处理前导 0:不要 parseInt;用字符串清洗 replace(/\D/g,'')
  8. 粘贴/自动填充一次性输入:必须能一次处理多位数字
  9. 满 4 位自动提交:放在 input 处理内,而不是 keyup
  10. WebOTP API(navigator.credentials.get)在 iOS Safari 支持有限:可保留但不要依赖

7. 结论总结(正确的验证码填充方式)

在 iOS Safari 中,最可靠的验证码输入/自动填充方案是:

  • 一个真实 inputautocomplete="one-time-code")接收所有输入(手动/粘贴/OTP)
  • 四个格子仅用于展示
  • @input 统一解析与分发
  • @touchstart 触发同步 focus() 保障键盘弹起

这样可以同时满足:

  • OTP 自动填充完整进入(含前导 0)
  • 手动输入体验一致
  • 逻辑清晰、兼容性最好、维护成本最低