在这里插入图片描述
在这里插入图片描述

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名)

大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。

我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括 前端工程化、小程序、React / RN、Flutter、跨端方案
在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。

技术方向:前端 / 跨端 / 小程序 / 移动端工程化
内容平台:
掘金、知乎、CSDN、简书
创作特点:
实战导向、源码拆解、少空谈多落地
文章状态:
长期稳定更新,大量原创输出

我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、真实项目中如何取舍,希望能帮你在实际工作中少走弯路。

子玥酱 · 前端成长记录官 ✨
👋 如果你正在做前端,或准备长期走前端这条路
📚 关注我,第一时间获取前端行业趋势与实践总结
🎁 可领取 11 类前端进阶学习资源(工程化 / 框架 / 跨端 / 面试 / 架构)
💡 一起把技术学“明白”,也用“到位”

持续写作,持续进阶。
愿我们都能在代码和生活里,走得更稳一点 🌱

引言

如果你已经把 HarmonyOS 应用做到 PC 形态,大概率迟早会遇到这些问题:

Tab 键乱跳,焦点有时消失,鼠标点了,键盘却没反应
多窗口一切换,输入全失效

第一反应通常是:

是不是系统焦点机制太复杂?

于是你开始:

  • 强行 requestFocus
  • 到处打 log
  • 在组件生命周期里补焦点
  • 写一堆兜底逻辑

但越补越乱,因为真正的问题,并不在“焦点 API”。

一个必须先承认的事实

在 PC 场景下:

焦点不是 UI 状态,而是一种“交互所有权”。

而大多数项目,一开始就把它当成了:

“当前哪个组件高亮了”。

这一步,就已经走偏了。

焦点 ≠ 高亮

很多代码,焦点逻辑是这样写的:

@State isFocused: boolean = false

onFocus() {
  this.isFocused = true
}

onBlur() {
  this.isFocused = false
}

然后所有行为都基于这个状态判断。

问题在于:

  • 高亮只是表现,焦点却决定 输入去向

当你把两者绑死时:

视觉没问题,交互已经乱了。

PC 焦点的本质:输入路由权

在 HarmonyOS PC 下,焦点至少决定三件事:

  1. 键盘事件发给谁
  2. 快捷键是否生效
  3. 输入法是否激活

但很多项目,焦点分散在各个组件里:

TextInput {
  onFocus() { /* ... */ }
}

List {
  onFocus() { /* ... */ }
}

结果就是:

没有任何地方,知道“现在谁真正拥有输入”。

高频坑:组件各自抢焦点

你一定见过这种写法:

onClick() {
  this.requestFocus()
}
onAppear() {
  this.requestFocus()
}

短期看能解决问题,长期看是灾难:

  • 多个组件同时请求
  • 窗口切换时反复触发
  • 焦点状态不可预测

最终表现出来的就是:

焦点像在“漂移”。

正确思路:焦点必须集中建模

在 PC 项目中,有且只能有一个地方回答这个问题:

“当前输入属于谁?”

我们把它单独建模。

一、定义一个明确的 FocusModel

// 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 只是“注册者”

二、组件只声明“我能不能被聚焦”

// pc/focus/Focusable.ts
export interface Focusable {
  id: string
  canFocus(): boolean
}
class EditorView implements Focusable {
  id = 'editor'

  canFocus() {
    return true
  }
}

组件不主动抢焦点,只声明能力。

三、焦点切换由 Controller 统一调度

// pc/focus/FocusController.ts
export class FocusController {
  constructor(private focusModel: FocusModel) {}

  requestFocus(target: Focusable) {
    if (target.canFocus()) {
      this.focusModel.focus(target.id)
    }
  }
}

现在:

  • 点击
  • Tab
  • 窗口激活

全部走同一条路径。

四、键盘事件只看 FocusModel

这是最容易被忽略、但最关键的一步。

错误做法:组件自己处理键盘

onKeyDown(e) {
  if (this.isFocused) {
    handleKey(e)
  }
}

正确做法:集中分发

function onKeyDown(e) {
  const focusedId = focusModel.current()
  dispatchKeyEvent(focusedId, e)
}
function dispatchKeyEvent(id: string, e) {
  const handler = registry.get(id)
  handler?.onKey(e)
}

焦点决定“谁接收”,不是“谁自己判断”。

五、Tab / 方向键:不是 UI 行为,而是焦点策略

很多项目会这样写:

onTab() {
  focusNext()
}

问题是:

  • “下一个”是谁?
  • 顺序在哪里定义?
  • 不同窗口是否一致?

正确方式:焦点顺序也是模型

class FocusOrder {
  private order: string[] = []

  next(current: string): string | undefined {
    const index = this.order.indexOf(current)
    return this.order[index + 1]
  }
}
onTab() {
  const next = focusOrder.next(focusModel.current())
  if (next) focusModel.focus(next)
}

这样你才能:

  • 控制 Tab 行为
  • 做无障碍支持
  • 支持键盘优先模式

六、多窗口场景:焦点必须绑定 Workspace

在 PC 上,焦点不是全局唯一的

class WorkspaceFocus {
  workspaceId: string
  focusModel: FocusModel
}

窗口切换时:

  • Workspace A 的焦点被冻结
  • Workspace B 的焦点恢复

否则你一定会遇到:

在 A 窗口打字,却改了 B 的内容。

为什么焦点问题这么“折磨人”?

因为:

  • 错乱立刻体现在输入上
  • 键盘问题比渲染更明显
  • 用户会直接觉得“不能用”

但根因往往是:

你从来没有一个地方,真正定义过“焦点是什么”。

一个快速自检清单

如果你的 HarmonyOS PC 项目:

  • 在组件里频繁 requestFocus
  • 焦点状态分散在 UI State
  • 键盘事件由组件自己判断
  • Tab 行为写在页面逻辑里

那几乎可以确定:

焦点模型缺失。

总结

在 HarmonyOS PC 上,焦点不是一个 UI 技巧,而是一种输入资源的分配机制。

  • 模型不集中,焦点必乱
  • 焦点不稳定,交互必崩
  • 焦点一乱,多输入全废
Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐