

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名)
大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。
我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括 前端工程化、小程序、React / RN、Flutter、跨端方案,
在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。
技术方向: 前端 / 跨端 / 小程序 / 移动端工程化 内容平台: 掘金、知乎、CSDN、简书 创作特点: 实战导向、源码拆解、少空谈多落地 **文章状态:**长期稳定更新,大量原创输出
我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在"API 怎么用",而是更关注为什么这么设计、在什么场景下容易踩坑、真实项目中如何取舍,希望能帮你在实际工作中少走弯路。
子玥酱 · 前端成长记录官 ✨
👋 如果你正在做前端,或准备长期走前端这条路
📚 关注我,第一时间获取前端行业趋势与实践总结
🎁 可领取 11 类前端进阶学习资源 (工程化 / 框架 / 跨端 / 面试 / 架构)
💡 一起把技术学"明白",也用"到位"
持续写作,持续进阶。
愿我们都能在代码和生活里,走得更稳一点 🌱
文章目录
-
- 引言
- 一个必须先承认的事实
- [焦点 ≠ 高亮](#焦点 ≠ 高亮)
- [PC 焦点的本质:输入路由权](#PC 焦点的本质:输入路由权)
- 高频坑:组件各自抢焦点
- 正确思路:焦点必须集中建模
- [一、定义一个明确的 FocusModel](#一、定义一个明确的 FocusModel)
- 二、组件只声明"我能不能被聚焦"
- [三、焦点切换由 Controller 统一调度](#三、焦点切换由 Controller 统一调度)
- [四、键盘事件只看 FocusModel](#四、键盘事件只看 FocusModel)
- [五、Tab / 方向键:不是 UI 行为,而是焦点策略](#五、Tab / 方向键:不是 UI 行为,而是焦点策略)
- [六、多窗口场景:焦点必须绑定 Workspace](#六、多窗口场景:焦点必须绑定 Workspace)
- 为什么焦点问题这么"折磨人"?
- 一个快速自检清单
- 总结
引言
如果你已经把 HarmonyOS 应用做到 PC 形态,大概率迟早会遇到这些问题:
Tab 键乱跳,焦点有时消失,鼠标点了,键盘却没反应
多窗口一切换,输入全失效
第一反应通常是:
是不是系统焦点机制太复杂?
于是你开始:
- 强行
requestFocus - 到处打 log
- 在组件生命周期里补焦点
- 写一堆兜底逻辑
但越补越乱,因为真正的问题,并不在"焦点 API"。
一个必须先承认的事实
在 PC 场景下:
焦点不是 UI 状态,而是一种"交互所有权"。
而大多数项目,一开始就把它当成了:
"当前哪个组件高亮了"。
这一步,就已经走偏了。
焦点 ≠ 高亮
很多代码,焦点逻辑是这样写的:
ts
@State isFocused: boolean = false
onFocus() {
this.isFocused = true
}
onBlur() {
this.isFocused = false
}
然后所有行为都基于这个状态判断。
问题在于:
- 高亮只是表现,焦点却决定 输入去向
当你把两者绑死时:
视觉没问题,交互已经乱了。
PC 焦点的本质:输入路由权
在 HarmonyOS PC 下,焦点至少决定三件事:
- 键盘事件发给谁
- 快捷键是否生效
- 输入法是否激活
但很多项目,焦点分散在各个组件里:
ts
TextInput {
onFocus() { /* ... */ }
}
List {
onFocus() { /* ... */ }
}
结果就是:
没有任何地方,知道"现在谁真正拥有输入"。
高频坑:组件各自抢焦点
你一定见过这种写法:
ts
onClick() {
this.requestFocus()
}
ts
onAppear() {
this.requestFocus()
}
短期看能解决问题,长期看是灾难:
- 多个组件同时请求
- 窗口切换时反复触发
- 焦点状态不可预测
最终表现出来的就是:
焦点像在"漂移"。
正确思路:焦点必须集中建模
在 PC 项目中,有且只能有一个地方回答这个问题:
"当前输入属于谁?"
我们把它单独建模。
一、定义一个明确的 FocusModel
ts
// pc/focus/FocusModel.ts
export class FocusModel {
private focusedId?: string
focus(id: string) {
this.focusedId = id
}
blur(id: string) {
if (this.focusedId === id) {
this.focusedId = undefined
}
}
isFocused(id: string): boolean {
return this.focusedId === id
}
}
注意这里的关键点:
- 焦点不是组件实例
- 而是一个稳定的标识
- UI 只是"注册者"
二、组件只声明"我能不能被聚焦"
ts
// pc/focus/Focusable.ts
export interface Focusable {
id: string
canFocus(): boolean
}
ts
class EditorView implements Focusable {
id = 'editor'
canFocus() {
return true
}
}
组件不主动抢焦点,只声明能力。
三、焦点切换由 Controller 统一调度
ts
// pc/focus/FocusController.ts
export class FocusController {
constructor(private focusModel: FocusModel) {}
requestFocus(target: Focusable) {
if (target.canFocus()) {
this.focusModel.focus(target.id)
}
}
}
现在:
- 点击
- Tab
- 窗口激活
全部走同一条路径。
四、键盘事件只看 FocusModel
这是最容易被忽略、但最关键的一步。
错误做法:组件自己处理键盘
ts
onKeyDown(e) {
if (this.isFocused) {
handleKey(e)
}
}
正确做法:集中分发
ts
function onKeyDown(e) {
const focusedId = focusModel.current()
dispatchKeyEvent(focusedId, e)
}
ts
function dispatchKeyEvent(id: string, e) {
const handler = registry.get(id)
handler?.onKey(e)
}
焦点决定"谁接收",不是"谁自己判断"。
五、Tab / 方向键:不是 UI 行为,而是焦点策略
很多项目会这样写:
ts
onTab() {
focusNext()
}
问题是:
- "下一个"是谁?
- 顺序在哪里定义?
- 不同窗口是否一致?
正确方式:焦点顺序也是模型
ts
class FocusOrder {
private order: string[] = []
next(current: string): string | undefined {
const index = this.order.indexOf(current)
return this.order[index + 1]
}
}
ts
onTab() {
const next = focusOrder.next(focusModel.current())
if (next) focusModel.focus(next)
}
这样你才能:
- 控制 Tab 行为
- 做无障碍支持
- 支持键盘优先模式
六、多窗口场景:焦点必须绑定 Workspace
在 PC 上,焦点不是全局唯一的。
ts
class WorkspaceFocus {
workspaceId: string
focusModel: FocusModel
}
窗口切换时:
- Workspace A 的焦点被冻结
- Workspace B 的焦点恢复
否则你一定会遇到:
在 A 窗口打字,却改了 B 的内容。
为什么焦点问题这么"折磨人"?
因为:
- 错乱立刻体现在输入上
- 键盘问题比渲染更明显
- 用户会直接觉得"不能用"
但根因往往是:
你从来没有一个地方,真正定义过"焦点是什么"。
一个快速自检清单
如果你的 HarmonyOS PC 项目:
- 在组件里频繁
requestFocus - 焦点状态分散在 UI State
- 键盘事件由组件自己判断
- Tab 行为写在页面逻辑里
那几乎可以确定:
焦点模型缺失。
总结
在 HarmonyOS PC 上,焦点不是一个 UI 技巧,而是一种输入资源的分配机制。
- 模型不集中,焦点必乱
- 焦点不稳定,交互必崩
- 焦点一乱,多输入全废