

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名)
大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。
我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括 前端工程化、小程序、React / RN、Flutter、跨端方案,
在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。
技术方向: 前端 / 跨端 / 小程序 / 移动端工程化 内容平台: 掘金、知乎、CSDN、简书 创作特点: 实战导向、源码拆解、少空谈多落地 **文章状态:**长期稳定更新,大量原创输出
我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在"API 怎么用",而是更关注为什么这么设计、在什么场景下容易踩坑、真实项目中如何取舍,希望能帮你在实际工作中少走弯路。
子玥酱 · 前端成长记录官 ✨
👋 如果你正在做前端,或准备长期走前端这条路
📚 关注我,第一时间获取前端行业趋势与实践总结
🎁 可领取 11 类前端进阶学习资源 (工程化 / 框架 / 跨端 / 面试 / 架构)
💡 一起把技术学"明白",也用"到位"
持续写作,持续进阶。
愿我们都能在代码和生活里,走得更稳一点 🌱
文章目录
-
- 引言
- 第一阶段:大多数项目的"默认焦点模型"
- 第二阶段:先建立"唯一可信源"
- [第三阶段:引入统一调度,而不是到处 requestFocus](#第三阶段:引入统一调度,而不是到处 requestFocus)
- 第四阶段:键盘事件必须"先路由,再处理"
- 第五阶段:多窗口让焦点系统必须分层
- [第六阶段:Tab 顺序必须成为"策略",而不是事件](#第六阶段:Tab 顺序必须成为“策略”,而不是事件)
- 为什么焦点重建总是在项目后期发生?
- 总结
引言
如果你已经在 HarmonyOS 上把应用做到 PC 形态,并且开始认真打磨体验,大概率迟早会遇到一个阶段:
界面已经稳定,但交互始终不稳定。
表现通常很隐蔽:
- 光标偶尔丢失
- 键盘输入间歇失效
- Tab 顺序越来越不可预测
- 多窗口切换后需要"点一下才能继续用"
这些问题看起来彼此无关,但它们往往只有一个共同根源:
焦点系统从未被真正设计过。
在移动端,这件事可以被系统托底;但在 PC 上,如果你不设计焦点,焦点就一定会失控。
第一阶段:大多数项目的"默认焦点模型"
很多工程最初的写法,其实都差不多:
ts
@State isFocused: boolean = false
onFocus() {
this.isFocused = true
}
onBlur() {
this.isFocused = false
}
再配合:
ts
onClick() {
this.requestFocus()
}
在单页面 + 单输入区阶段,这完全没问题。
但一旦进入 PC 典型场景:
- 多输入组件并存
- 弹层与主界面叠加
- 窗口频繁切换
这个模型会迅速崩塌,因为:
焦点被拆散到了每一个组件里。
而 PC 世界真正需要的是:
全局唯一、可推理、可恢复的输入所有权模型。
第二阶段:先建立"唯一可信源"
焦点系统重建的第一步,从来不是 UI 改造,而是模型集中化。
ts
export class FocusModel {
private focusedId?: string
focus(id: string) {
this.focusedId = id
}
blur(id: string) {
if (this.focusedId === id) {
this.focusedId = undefined
}
}
current(): string | undefined {
return this.focusedId
}
}
这里最关键的设计不是代码本身,而是三个约束:
- 焦点只存在一份
- 组件不能私自修改
- 任何输入判断都必须读取它
当这一层建立后,很多"随机问题"会第一次变得:
可以被解释。
第三阶段:引入统一调度,而不是到处 requestFocus
焦点混乱最常见的根源,是多个来源同时抢占:
ts
onAppear() { this.requestFocus() }
onClick() { this.requestFocus() }
onResume() { this.requestFocus() }
短期有效,长期灾难。重建思路是:
所有焦点变化必须经过同一个调度入口。
ts
export class FocusController {
constructor(private model: FocusModel) {}
request(id: string) {
this.model.focus(id)
}
clear(id: string) {
this.model.blur(id)
}
}
从此以后:
- 点击
- Tab 导航
- 窗口激活
- 生命周期恢复
全部走同一条路径,焦点第一次具备:
可预测性。
第四阶段:键盘事件必须"先路由,再处理"
很多项目即使集中建模,仍然会卡在这里。
典型写法:
ts
onKeyDown(e) {
if (this.isFocused) {
this.handleKey(e)
}
}
问题在于:
组件仍在决定输入归属。
真正的 PC 输入模型应该是:
ts
function onGlobalKeyDown(e) {
const id = focusModel.current()
dispatchKey(id, e)
}
ts
function dispatchKey(id: string | undefined, e) {
if (!id) return
registry.get(id)?.onKey(e)
}
这里发生的本质变化是:
- 组件不再"监听世界"
- 世界先决定"谁拥有输入"
这一步,才是真正进入 桌面级交互模型。
第五阶段:多窗口让焦点系统必须分层
在单窗口下,全局唯一焦点还能勉强成立。
但 PC 的真实使用方式是:
- 同时打开多个工作区
- 在窗口间高速切换
- 后台挂起再恢复
如果仍然只有:
ts
let focusedId
那么输入错位只是时间问题。
正确方向:窗口级焦点域
ts
class WorkspaceFocus {
workspaceId: string
focusModel = new FocusModel()
}
窗口切换只需要切换:
ts
activeWorkspace = nextWorkspace
而不是重建整套状态,这意味着:
焦点终于具备"冻结与恢复"的能力。
第六阶段:Tab 顺序必须成为"策略",而不是事件
很多工程仍然把 Tab 写成:
ts
onTab() {
focusNext()
}
但 PC 真实需求远不止如此:
- 不同区域顺序不同
- 弹层需要临时接管
- 无障碍依赖稳定遍历
因此顺序本身也必须建模:
ts
class FocusOrder {
private order: string[] = []
next(current: string) {
const i = this.order.indexOf(current)
return this.order[i + 1]
}
}
当顺序进入模型层后,你才真正拥有:
可维护的键盘导航体系。
为什么焦点重建总是在项目后期发生?
因为它具有典型的"系统级问题"特征:
- 前期被 UI 成功掩盖
- 中期被局部修补延缓
- 后期在多窗口下集中爆发
这让很多团队误判为:
性能问题、框架问题、甚至系统 Bug。
但更真实的答案往往更简单:
交互底座从未完成桌面化升级。
总结
HarmonyOS 应用走向 PC,本质不是一次"界面适配",而是一场:
输入体系的重建。
而焦点系统,正是这场重建里最核心的一环。
真正让 PC 体验稳定下来的,从来不是更多的 UI 修补,而是三件更底层的工程决策:
- 建立唯一可信的焦点模型
- 统一输入路由与调度
- 在多窗口下分层管理焦点域
当这些完成之后,你会明显感受到一个转变:
应用不只是"能在 PC 上运行",而是第一次开始------像一个真正的 PC 应用那样运作。