uni-app 鸿蒙 App input 输入框 blur 后无法再次编辑:根因分析与终极修复
项目:Vue 3 + uni-app 3.0 App,编译目标 HarmonyOS App 环境:HBuilderX 5.x + DevEco Studio 5.0 + 鸿蒙真机
一、故障现象
填空组件(fill_blank.vue)在鸿蒙真机上出现了一个特殊的焦点 bug——在 Android、iOS、微信小程序上完全正常:
- 页面加载后,点击第一个填空输入框,弹出键盘,可以正常输入文字。
- 输入完成后,点击第二个填空输入框,键盘切换到第二个输入框,可以正常输入。
- 切换回第一个输入框——输入框获得了焦点(边框高亮样式正确),但 键盘无法弹出,无法输入任何文字。
更具体地说:
- 填完第一个空后,切换到第二个空,回来再点第一个空 → 无法编辑
- 只要是"输入过内容再失去焦点"的输入框,再次获取焦点后就已经半残
- 偶尔关闭系统键盘再重新打开能临时恢复,但无法稳定复现
- 这个 bug 只存在于鸿蒙设备上,Android / iOS / 微信小程序均无此问题
涉及的核心代码模式:
<template>
<input
v-for="(blank, idx) in blanks"
:key="idx"
:focus="autoFocusIndex === idx"
:value="userAnswers[idx]"
@click.stop="handleInputClick(idx)"
@focus="handleInputFocus(idx)"
@blur="handleInputBlur(idx)"
@input="(e) => handleInputChange(e, idx)"
/>
</template>
<script setup>
const autoFocusIndex = ref(-1)
const handleInputClick = (idx: number) => {
// 点击切换焦点
autoFocusIndex.value = idx
}
</script>
二、排查过程
2.1 第一次尝试:延时方案
初步猜测是鸿蒙 native 层 blur → focus 的切换需要充裕的时间间隔。于是将焦点切换改为"先失焦、等一帧、再聚焦":
const handleInputClick = (idx: number) => {
lastFocusedIndex.value = idx
autoFocusIndex.value = -1 // 先失焦
nextTick(() => {
autoFocusIndex.value = idx // 再聚焦
})
}
结果:无效。 输入框样式上显示了焦点态(边框高亮),但键盘依然不出现。
2.2 第二次尝试:加大延时
怀疑 nextTick(microtask)太短,改用了项目中已有的焦点切换模式——nextTick + setTimeout(100):
const handleInputClick = (idx: number) => {
lastFocusedIndex.value = idx
autoFocusIndex.value = -1
nextTick(() => {
setTimeout(() => {
autoFocusIndex.value = idx
}, 100)
})
}
这个模式在 submitAnswer 的"请填写第 N 个答案"toast 重定向中已经使用,在 Android / iOS 上验证可靠。
结果:仍然无效。 鸿蒙设备上无论等 100ms、200ms 还是 500ms,都无法恢复焦点后的键盘连接。
2.3 关键发现
经过反复对比测试,得出关键结论:
这不是时序问题,而是底层机制问题。鸿蒙 uni-app runtime 上,原生 input 在
@blur之后,通过:focus指令(从false变为true)重新聚焦时,不会触发完整的 native focus 生命周期。 输入框会进入一种「看起来有焦点,但键盘不连接」的中间状态。
具体来说:
- input 首次渲染时,
v-if创建原生 input,此时:focus="true"→ 完整 focus 生命周期,键盘正常 - input blur 后(用户点击别处),
@blur→autoFocusIndex = -1→:focus变为false - 用户再次点击 input →
autoFocusIndex = idx→:focus从false变为true - 鸿蒙上,这种"已存在 input 的
:focus属性变化" 不会触发完整的 native focus → 键盘不连接
三、根因分析
其他平台:
旧 input :focus: false ──→ true → native focus 生命周期完整 → 键盘正常
鸿蒙平台:
旧 input :focus: false ──→ true → native focus 生命周期不完整 → 键盘不连接
鸿蒙的 uni-app 桥接层在将 Vue 响应式的 :focus 属性变化映射为 ArkUI 原生 focusControl.requestFocus() 调用时,存在一个边界 bug:对于已经 blur 过的 input 组件,focusControl.requestFocus() 虽然返回成功,但 input 组件并没有进入真正的可编辑状态(IME 输入法未激活)。
四、终极修复::key 强制重建
4.1 核心思路
既然已存在的 input 无法被正确重新聚焦,那就不让它存在。
通过递增 :key 值,让 Vue 在重新聚焦前先销毁旧的原生 input,再创建一个全新的 input。新 input 在初始渲染时就带着 :focus="true",触发的是「首次渲染 focus」代码路径,而非「属性变化 refocus」代码路径。
旧方案(无效):
旧 input :focus: false ──→ true → 鸿蒙键盘不连接
新方案(有效):
销毁旧 input → 新建 input,初始 :focus="true" → 键盘正常连接
4.2 完整实现
第一步:添加鸿蒙平台检测和渲染版本号
// 鸿蒙设备检测
let isHarmonyOS = false
// #ifndef H5
try {
const deviceInfo = uni.getDeviceInfo()
const platform = (deviceInfo.platform || '').toLowerCase()
isHarmonyOS = platform.includes('harmony') || platform === 'ohos'
} catch (e) {
isHarmonyOS = false
}
// #endif
// 输入框渲染版本号——鸿蒙上通过递增 :key 强制重建原生 input
const inputRenderKeys = ref<number[]>([])
// 在答案解析的 computed 中初始化(与其他数组一起)
// inputRenderKeys.value = Array.from({ length: count }).fill(0)
第二步:模板中绑定动态 :key
<input
v-if="!isIOS()"
:key="`fill-blank-input-${segment.index - 1}-${inputRenderKeys[segment.index - 1] || 0}`"
:focus="autoFocusIndex === segment.index - 1"
@click.stop="handleInputClick(segment.index - 1)"
...
/>
key 只在鸿蒙上变化(非鸿蒙设备上 inputRenderKeys 始终为 0,行为与改动前完全一致),确保不影响其他平台。
第三步:焦点切换时递增渲染版本号
const handleInputClick = (idx: number) => {
if (submitted.value || props.disabled) return
// 点击当前已聚焦的输入框,无需处理
if (autoFocusIndex.value === idx) {
lastFocusedIndex.value = idx
return
}
// 先失焦
lastFocusedIndex.value = idx
autoFocusIndex.value = -1
// 鸿蒙设备:递增 :key 强制销毁旧 input、创建新 input
if (isHarmonyOS) {
inputRenderKeys.value[idx] = (inputRenderKeys.value[idx] || 0) + 1
}
// 等 Vue 重建 input 后再聚焦
nextTick(() => {
setTimeout(() => {
autoFocusIndex.value = idx
}, isHarmonyOS ? 150 : 100)
})
}
4.3 其他需要同样修复的位置
凡是涉及到"重新聚焦到某个 input"的地方,都需要在鸿蒙上递增 inputRenderKeys:
| 位置 | 场景 |
|---|---|
handleInputClick | 用户手动点击切换焦点 |
submitAnswer(toast redirect) | 提交时发现有空答案,聚焦到未填的空 |
handleInputConfirm(考试模式) | 考试模式回车确认时聚焦到未填的空 |
修复模式统一为:
// 在设置 autoFocusIndex 之前
autoFocusIndex.value = -1
if (isHarmonyOS) {
inputRenderKeys.value[targetIdx] = (inputRenderKeys.value[targetIdx] || 0) + 1
}
nextTick(() => {
setTimeout(() => {
autoFocusIndex.value = targetIdx
}, isHarmonyOS ? 150 : 0)
})
4.4 setTimeout(150) 的作用
鸿蒙上多出的 50ms 延迟(150ms vs 100ms)是为了给 native 层充裕的时间完成:
- 销毁旧 input → ArkUI 组件卸载
- 创建新 input → ArkUI 组件挂载 + 初始化
- 新 input 接收
:focus="true"→ 触发完整 IME 激活
五、为什么不影响其他平台
| 平台 | isHarmonyOS | inputRenderKeys[idx] | :key 实际值 | 行为 |
|---|---|---|---|---|
| Android | false | 始终 0 | fill-blank-input-0-0 | 不变,input 不重建 |
| iOS | false | 始终 0 | fill-blank-input-0-0 | 不变,input 不重建 |
| 微信小程序 | false | 始终 0 | fill-blank-input-0-0 | 不变,input 不重建 |
| 鸿蒙 | true | 递增 | fill-blank-input-0-1, -2, ... | input 重建,焦点正常 |
所有平台检测都在运行时完成,不依赖条件编译(#ifdef),不会造成编译产物的差异。
六、对比总结
| 方案 | 原理 | 结果 |
|---|---|---|
直接赋值 autoFocusIndex = idx | :focus 属性变化触发 native focus | ❌ 鸿蒙无效 |
nextTick + setTimeout(100) | 增加 blur→focus 间隔 | ❌ 鸿蒙无效(不是时序问题) |
:key 递增强制重建 input | 新 input 初始渲染时 :focus="true" 走完整 native 生命周期 | ✅ 全平台有效 |
七、核心教训
鸿蒙的
:focus指令对「已 blur 的原生 input」存在致命兼容问题。这大概率是 uni-app 鸿蒙运行时的桥接层 bug,而非应用层代码问题。时序方案对这类底层 bug 无效。加大
setTimeout延迟只是猜测,问题本质是focusControl.requestFocus()在已 blur 的 input 上不激活 IME,与等待时长无关。「销毁 → 重建」是处理原生组件状态异常的通用模式。类似问题在 React Native、Flutter 中也有出现——当原生组件进入不可恢复的中间态时,强制重建是最可靠的修复手段。
目标平台修复,不影响其他平台。通过运行时检测
isHarmonyOS,所有修复逻辑只对鸿蒙生效。inputRenderKeys在非鸿蒙上始终为 0,:key值不变,行为与改动前完全相同。该模式可复用于其他受影响的组件。任何依赖
:focus指令切换焦点的场景(如搜索框自动聚焦、表单验证后聚焦首个错误字段),如果在鸿蒙上出现类似的「焦点不连接键盘」问题,都可以采用同样的:key重建方案。
本文记录了 uni-app 鸿蒙 App input 焦点 bug 从现象到根因再到修复的完整过程,核心发现是鸿蒙运行时的 :focus 指令无法在已 blur 的原生 input 上正确激活 IME。希望这个修复模式对同样在鸿蒙上踩坑焦点问题的开发者有所帮助。