这是一篇关于这次漫长排查过程的技术博文:
十轮排查,凶手竟在门口 —— 记一次支付宝小程序 input 无法删除的 Debug 之旅
—— 当所有不可能都被排除,剩下的那个即使再离谱,也是真相。
问题
公司项目是支付宝小程序,某个搜索列表组件中的 <input> 输入框在所有手机上都工作正常——唯独在荣耀(Honor)手机上,用户输入文字后按删除键毫无反应。
听起来像是一个普通的兼容性问题,对吧?我也这么想。然后我花了整整十轮才找到真凶。
第一轮:经典套路 controlled
支付宝小程序文档明确提到 <input> 的 controlled 属性用于解决 Android 输入框删除问题。加上去。
<input controlled value="{{searchKeyword}}" onInput="onSearchInput" />
结果:无效。
第二轮:setData 渲染竞态
会不会是每次 onInput 调 this.setData() 触发组件重渲染,和键盘的删除操作产生了竞态?把 onInput 改成只存本地变量,确认搜索时才同步:
onSearchInput(e) {
this._inputValue = e.detail.value; // 不调 setData
},
onSearchConfirm() {
this.setData({ searchKeyword: this._inputValue });
this.fetchData(true);
}
结果:无效。
第三轮:scroll-view 的罪过?
input 嵌套在 <scroll-view> 内部,原生滚动容器可能拦截了键盘事件。把筛选区从 scroll-view 中提出来,只让结果列表滚动。
结果:无效。
第四轮:overflow: hidden?
外层容器有 height: 100vh; overflow: hidden; position: relative——是不是固定视口高度在键盘弹起时导致了布局异常?移除所有定位,改成 min-height: 100vh 流式布局。
结果:无效。
第五轮:textarea + enableNative
<input> 不行,换成原生层级的 <textarea>,再加上 enableNative 属性强制启用原生控件。
结果:无效。
第六轮:slot 提升法
在 Component() 中渲染的 input 无论如何都不行。那通过 slot 让 input 在 Page() 上下文中声明呢?组件暴露一个 <slot name="search-input" />,页面在 slot 中放 input。
<search-list>
<view slot="search-input">
<input value="{{keyword}}" onInput="onSearchChange" />
</view>
</search-list>
结果:无效。
第七轮:外部 fixed 层
slot 也不行,干脆把 input 提到组件外部,用 position: fixed 悬浮在组件上方。
结果:无效。
第八轮:完全放弃组件
把整个 search-list 组件拆掉,所有逻辑、模板、样式全部内联到 Page 中。input 是 Page 模板的直接子元素,不经过任何 Component 节点。
结果:无效。
第九轮:极简对照实验
这是最关键的一步。我注意到项目里有一个测试页面 /pages/test-input,里面就一个 input,在荣耀手机上删除完全正常。
我把 lawyer 页面删到和测试页一样极简:
<!-- lawyer/index.axml -->
<view class="law-page">
<view class="law-test-body">
<input value="{{keyword}}" onInput="onInput" />
</view>
</view>
// lawyer/index.ts
Page({
data: { keyword: '' },
onInput(e) { this.setData({ keyword: e.detail.value }); },
});
// lawyer/index.json
{ "defaultTitle": "请律师" }
两个页面现在 100% 同构——同样的 template 结构、同样的 1 个 data key、同样的 CSS、同样的 Page() 上下文。
结果:终于能删除了!
找到凶手
极简版能正常工作,说明问题不在 template、不在 CSS、不在 JS 逻辑、不在 Component。
我开始往回加东西。加回 transparentTitle: "always":
{
"defaultTitle": "请律师",
"transparentTitle": "always"
}
结果:立即无法删除。
移除 transparentTitle,一切恢复正常。又试了 "auto" 模式——同样不行。
真凶找到了。
真相
支付宝小程序中,transparentTitle: "always" 会让导航栏变为透明,页面内容延伸到状态栏区域。这个效果在 iOS 和其他 Android 机型上一切正常,但在荣耀(Honor)手机的 Android WebView 中,框架为了渲染透明导航栏会在页面顶部创建一个 native OS 层级的覆盖层。
这个覆盖层的范围是整个页面——不是因为 input 在导航栏下面被遮挡,而是这个 native 层从物理上拦截了键盘的 Backspace 事件。不管你的 input 离导航栏多远(即使用 padding-top: 200px 把 input 推到屏幕中央),keydown 事件依然被吞。
这也是为什么 <input>、<textarea>、controlled、enableNative 全部无效——这些属性影响的是 WebView 层面的 input 行为,但背身键事件在到达 WebView 之前就已经被 OS 层的透明导航栏覆盖层劫持了。
test-input 页面之所以正常,是因为它的 index.json 里从来没有 transparentTitle。
修复
移除所有包含 input 的搜索页面的 transparentTitle 配置:
{
"defaultTitle": "请律师",
- "transparentTitle": "always",
- "titleBarColor": "#ffffff",
"usingComponents": { "search-list": "/components/search-list/index" }
}
仅此一行修改。六行代码的删除,解决了十轮排查的难题。
反思
这次 debug 让我重新理解了”逐层排除法”:
- 从最可能的开始(
controlled属性、setData竞态) - 逐步走向不可能的(
scroll-view原生层、overflowCSS、position定位) - 建立对照组(极简测试页 vs 完整页面的对照实验)
- 二分逼近(加回一个配置 → 测试 → 定位)
第 9 轮的极简对照实验是整个过程的转折点。在此之前,我一直在 Component/CSS/属性体系中打转;当我把页面删到和测试页一模一样时,差异终于暴露出来——配置层,而不是代码层。
Bug 不一定藏在你的代码里。有时候,它静静躺在配置文件的最深处,等待你用对照实验把它揪出来。
