背景

首页顶部有个搜索入口。我们想做的体验很朴素:用户点一下,跳到搜索页,键盘自己就上来了,手指不用再戳第二次。少这一下点击,搜索的手感和转化都更好。

要做到这点,绕不开一个移动端的老约束。focus() 想拉起软键盘,必须发生在用户手势触发的事件里,也就是 clicktouchend 这类「可信事件」。浏览器这么限制是为了防滥用,不然页面一打开就自动弹键盘会很烦。

跳页面恰好和这个约束冲突。新页面的渲染、useEffectuseReady 这些生命周期回调,都排在用户那一下点击之后的异步队列里。对浏览器来说,点击带来的「手势额度」已经过期,你在搜索页里调 focus(),它只当普通脚本,不给开键盘。安卓相对宽松大多能弹,iOS 基本不买账。

所以这个功能的核心矛盾是:能弹键盘的时机(点击那一刻)和想聚焦的元素(下一个页面的输入框),不在同一个执行上下文里。

思路

既然手势额度只在点击回调里有效,那就在点击回调里把键盘先「点着」。

具体做法是:跳转的同时,往当前页面塞一个看不见的原生 input,立刻 focus 它。这一步还在手势有效期内,键盘被合法唤起;等搜索页加载完,真正的输入框接过焦点,键盘就一直挂着没落下去。相当于借点击这一下手势先把键盘续上,再交棒给目标页。

另外这段代码会进多端打包,H5、微信、支付宝跑同一份,所以还得保证它在没有 DOM 的小程序端不报错、不影响主流程。

实现

const invokeNativeInputKeyboard = useCallback(() => {
  try {
    if (window && document) {
      const nativeInput = document.createElement('input');
      // 藏起来,但不能 display:none——隐藏元素 focus 不生效
      nativeInput.style.width = '1px';
      nativeInput.style.height = '1px';
      nativeInput.style.opacity = '0.01';
      nativeInput.style.pointerEvents = 'none';
      nativeInput.style.position = 'fixed';
      nativeInput.style.zIndex = '-1';
      nativeInput.style.left = '0px';
      nativeInput.style.bottom = '0px';
      document.body.appendChild(nativeInput);
      nativeInput.focus();
      // 键盘起来后移除 DOM,防止内存泄漏或 DOM 无限增长
      setTimeout(() => {
        nativeInput?.remove();
      }, 1000);
    }
  } catch (error) {
    //
  }
}, []);

调用紧跟着跳转:

const handleSearchClick = useCallback((searchInfo) => {
  // ...拼参数
  $route.navigateTo({ url: SEARCH_PAGE(params) });
  invokeNativeInputKeyboard();   // 趁手势还热乎
}, [/* deps */]);

几个关键细节

藏元素别用 display:nonevisibility:hidden。被这两个隐藏掉的元素根本聚焦不了,focus() 直接哑火。我们要的是「肉眼看不见,但浏览器仍认它能接收焦点」,所以压到 1px、透明度拉到几乎为零,再用 z-index:-1pointer-events:none 兜底——元素活着,只是你看不见也点不到。

临时 input 一定要删。键盘起来后它就没用了,可如果用户反复点搜索,body 上会越积越多空 input,DOM 慢慢膨胀。隔 1 秒再 remove,等焦点转移和键盘动画都稳了再清场。

最外层那圈 try/catchwindow && document 判断也不是摆设。小程序里没有 document,真去 createElement 会直接抛错,把整个点击流程带崩。先探宿主、再兜异常,让它在不支持的端上安静地什么都不做。