🤖
AI审核中

小程序登录页:从账号框点到密码框,键盘闪一下就收起了

文章摘要

炊想登录页在 iPhone 上出现怪现象:从账号框点到密码框,键盘弹出来闪一下就收回去,得再点一次才能输入。第一次以为是 focus 属性没同步,修完只好了一半;真正的原因是切换输入框时,代码通过 focus 属性设置焦点、blur 和 focus 回调里连续 setData,正好撞上系统从普通键盘换成密码键盘。本文记录两次排查、最终改法(去掉 focus 绑定、高亮延后合并刷新、失焦提示延后显示)和几点体会。

《炊想》的登录页只有两个输入框:账号和密码。就这么简单的一个页面,iPhone 上却出现了一个怪现象:从账号框点到密码框,键盘弹出来闪一下就收回去了,得再点一次密码框才能输入。

这篇记录前后两次排查:第一次修对了一半,第二次才找到真正的原因。

一、问题是怎么被描述出来的

第一次反馈是这样的:

点击密码框,第一次弹出输入框立马关闭了,需要再点击一次才弹出,并且稳定复现。

按这个描述修了一版。过了几天,又收到一条更准确的:

直接点击密码输入框,正常弹出键盘;如果先点击了账号输入框,再点击密码输入框,就会闪一下并关闭键盘。

第二条把触发条件说清楚了:不是「第一次点密码框」,而是「从另一个输入框切过来」。后面能找到真正的原因,靠的就是这条描述。

另外,这个问题在微信开发者工具的模拟器里复现不出来,模拟器没有真的系统键盘,只能在真机上看。

二、页面原来的写法

两个输入框都要在聚焦时高亮边框,所以各自绑了 bindfocus / bindblur,在回调里 setData 一个 focus 字段,外层的 class 根据它切换。

密码框还多了一个 focus 属性,用来在账号框按「下一项」时自动跳到密码框,以及密码错误时自动把焦点放回密码框:

<view class="field {{focus === 'password' ? 'field-focus' : ''}}">
  <input
    password="{{!showPwd}}"
    value="{{password}}"
    focus="{{pwdFocus}}"
    data-field="password"
    bindfocus="onFocus"
    bindblur="onBlur"
  />
</view>
onFocus: function (e) {
  this.setData({ focus: e.currentTarget.dataset.field });
},

onBlur: function (e) {
  this.setData({ focus: '' });
  // 失焦时顺手校验,格式不对就在输入框下面显示提示
  this.setMsg(field, this.validateField(field, value));
},

onAccountConfirm: function () {
  this.setData({ pwdFocus: true });
}

看起来很普通,问题就藏在 focus="{{pwdFocus}}" 和这几次 setData 里。

三、第一次修:把 pwdFocus 跟真实焦点同步

第一次的判断是这样的:focus 是个受控属性。用户手动点密码框时,输入框确实获得了焦点,但数据里的 pwdFocus 还是 false。紧接着 onFocus 里一 setData,页面重新渲染,框架看到 focus 属性仍是 false,就把它当成「这个输入框不该有焦点」,键盘随即收起。第二次点的时候状态对上了,就正常了。

于是改成:获得焦点时把 pwdFocus 设成 true,失去焦点时设回 false,让属性始终和真实焦点一致。

onFocus: function (e) {
  var patch = { focus: field };
  if (field === 'password') patch.pwdFocus = true;
  this.setData(patch);
},

onBlur: function (e) {
  var patch = {};
  if (this.data.focus === field) patch.focus = '';
  if (field === 'password') patch.pwdFocus = false;
  this.setData(patch);
}

改完以后,直接点密码框确实好了。但从账号框切过来,依然会闪退。

四、真正的原因:切换输入框时撞在了一起

把「从账号框切到密码框」这一下拆开看,几乎同时发生了这些事:

  1. 账号框失去焦点,触发 bindblur,立刻 setData 清掉高亮,失焦校验也可能再 setData 一次;
  2. 密码框获得焦点,触发 bindfocus,立刻 setData 加上高亮,**同时把 focus 属性从 false 改成 true**;
  3. 系统键盘从普通键盘切换成密码键盘(iOS 的安全输入键盘)。

第 2 步里,代码在输入框正在获得焦点的过程中,又通过属性去设置一次焦点;这时系统还在换键盘,两边对焦点的处理撞在一起。再加上第 1、2 步在同一瞬间连续刷新页面,结果就是键盘弹出来闪一下又被收回去。

直接点密码框时没有第 1 步和第 3 步:键盘是从无到有直接弹出密码键盘,没有切换过程,所以第一次修完以后就正常了。这也解释了为什么只有「切过来」才会出问题。

还有一个容易被忽略的帮凶:失焦校验的提示。账号框失焦时如果格式不对,下面会多出一行红字,把密码框往下推。正在弹键盘的那个输入框位置一变,也可能丢掉焦点。

五、最终的改法

思路是:焦点交给系统,代码只管样式,而且不在焦点切换的那一瞬间刷新页面。

1. 去掉 focus 属性绑定

<input
  password="{{!showPwd}}"
  value="{{password}}"
  data-field="password"
  bindfocus="onFocus"
  bindblur="onBlur"
/>

代价是账号框按键盘上的「下一项」不会再自动跳到密码框。所以把账号框的 confirm-type 也从 next 改成了 done,免得按了没反应。

2. 高亮延后、合并成一次刷新

写了一个小工具 utils/field-focus.js,所有表单页共用。bindfocus / bindblur 里只记录「现在哪个框有焦点」,真正的 setData 延后约 120 毫秒、合并成一次。等两个输入框的焦点切换和键盘切换都完成了,页面才刷新:

var SETTLE_MS = 120;
var CHECK_DELAY = 250;

function schedule(page) {
  if (page._focusTimer) clearTimeout(page._focusTimer);
  page._focusTimer = setTimeout(function () {
    page._focusTimer = null;
    var field = page._focusField || '';
    if (page.data.focus !== field) {
      page.setData({ focus: field });
    }
  }, SETTLE_MS);
}

function focus(page, field) {
  page._focusField = field || '';
  schedule(page);
}

/** afterBlur:失焦后的校验,等焦点已经切到下一个框再执行 */
function blur(page, field, afterBlur) {
  if (page._focusField === field) page._focusField = '';
  schedule(page);
  if (typeof afterBlur === 'function') setTimeout(afterBlur, CHECK_DELAY);
}

页面里就变成了:

onFocus: function (e) {
  fieldFocus.focus(this, e.currentTarget.dataset.field);
},

onBlur: function (e) {
  var self = this;
  var field = e.currentTarget.dataset.field;
  fieldFocus.blur(self, field, function () {
    if (self.data[field]) {
      self.setMsg(field, self.validateField(field, self.data[field]));
    }
  });
}

从 A 切到 B 时,blur(A) 和 focus(B) 都只是改一下 _focusField、重新计时,最后只执行一次 setData({ focus: 'B' })。高亮晚了 0.1 秒,肉眼基本看不出来。

3. 失焦提示延后显示

格式提示放在 afterBlur 里,延后 0.25 秒。这时焦点早已稳定在下一个输入框上,红字把它往下推也不会再丢焦点。

4. 所有表单页一起改,并加一道检查

注册、修改密码、找回密码、绑定手机号、意见反馈这几个页面虽然没有绑 focus 属性,但同样在 bindfocus / bindblur 里立刻 setData,切换输入框时也有同样的风险,所以统一改用这个工具。

最后在项目的自检脚本里加了一条:表单页的 wxml 里再出现 focus="{{...}}" 就直接报错。首页的搜索框只有一个输入框、不存在切换,是唯一的例外。

if (name !== 'index' && /\sfocus="\{\{/.test(wxml)) {
  fail('pages/' + name + '/' + name + '.wxml 用了 focus="{{}}" 绑定,请去掉');
}

六、几点体会

  • 多个输入框的表单,不要用 focus 属性去控制焦点。它看起来能实现「自动跳到下一个框」,但会和系统的焦点切换抢控制权。为了这点便利换来一个闪退的键盘,不值得。
  • 不要在 bindfocus / bindblur 里同步 setData。这两个回调正好落在焦点和键盘切换的那一瞬间,想改样式就延后一点、合并成一次。
  • 聚焦期间别让布局跳动。失焦校验的提示、错误信息,都等焦点稳定以后再出现。
  • 键盘问题只能在真机上测。模拟器没有真的系统键盘,这类问题在开发者工具里看不出来。
  • 复现步骤越具体越好。「第一次点密码框会闪退」和「先点账号框再点密码框会闪退」看起来差不多,指向的原因却完全不同。第一次修只对了一半,就是因为没弄清楚触发条件。

这次的修复随炊想 2.0.10 发布。验证方法很简单:在 iPhone 上打开登录页,先点账号框,再点密码框,键盘应该直接切成密码键盘,不再收起。

1 条评论

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧

评论区 1

头像
 1 条评论
召田最帅boy 博主

亲测2.0.10版本未成功修复,明天再改86@3x