HarmonyOS 一次开发、多端部署实战:Phone、Pad、PC、Watch 到底该共享什么

最开始做 Nameo 多端适配时,我不是“差点”把 Phone 页面直接搬到大屏——我真的搬了,而且它还真的跑起来了。

然后尴尬也跟着跑来了。

Phone 那一小块布局安静地待在屏幕正中间,左右两边空得十分讲礼貌,谁也不挨着谁。到了设置页,内容在目标窗口里看起来大约只占六成,剩下的地方仿佛在提醒我:这是一块很大的屏幕,而你只带来了一颗很小的心。

那一刻我脑子里只有一个问题:

如果用户把应用装到 Pad 或 PC 上,看到的只是“手机版远距离观赏模式”,那他安装大屏版本的意义是什么?

“一次开发,多端部署”最容易制造的误会,就是把“一次开发”听成“一张页面到处拉伸”。如果 Ctrl+C 真能解决多端开发,架构师可能早就转行卖键盘了。

这篇不做 ArkUI API 大全。我想拿 Nameo 的 Phone、Pad、PC、Watch 实际拆分讲清楚五件事:设备职责怎么定、代码共享到哪一层、什么时候共用 HAP、什么时候独立拆包,以及“能构建”和“真机跑过”到底差多少。


一、“一次开发”的“一”,不是一张 UI

HarmonyOS 官方把“一多”拆成工程组织、功能兼容和界面适配几个层次。翻译成项目里的大白话就是:

  • 工程尽量是一套;
  • 数据模型、存储、网络和业务能力尽量共享;
  • 到了产品体验层,允许每种设备诚实地不一样。

所以 Nameo 当前并不是“四个端就复制四个工程”,也不是“一张 UI 按屏幕比例缩放”。它的真实拓扑是:

4 种产品体验,3 个产品 HAP

Phone HAP ── phone + tablet
PC HAP    ── 2in1
Wearable  ── wearable

根据真实配置脱敏简化后,三个入口的差异就是:

// Phone entry
deviceTypes: ['phone', 'tablet']

// PC entry
deviceTypes: ['2in1']

// Wearable entry
deviceTypes: ['wearable']

Phone 和 Pad 共用一个 HAP,但不共用一张页面;PC 和 Watch 因为任务、交互方式、产品能力差异更大,使用独立产品入口。

如果你刚从 Java/Vue 转过来,可以先这样理解:HAP 是应用的部署包单元,HAR 是静态共享包;这里说的 feature,是按业务能力拆出的模块,Shell 则是一端页面的导航与布局外壳。

共享的是“会做什么”,产品层决定“在这台设备上怎么做才不别扭”。

图里画的是主干,不是一条必须逐层穿过的流水线:products 会按需依赖 featurescommonfeatures 也会依赖 common

在这里插入图片描述

二、我不是先分屏幕,而是先分四种用户时刻

多端规划如果从“手机多少寸、平板多少寸”开始,很容易最后只剩一张响应式尺寸表。Nameo 先问的是:用户为什么在这一刻拿起这台设备?

用户时刻 主要职责 坚决不直接搬过去的东西
Phone 信息刚出现,随手记一下 语音、拍照、文件、分享采集,即时问答和轻量待办 PC 多栏管理、长时间批量编辑
Pad 坐下来阅读、整理或开会 列表+详情、会议工作区、知识整理和筛选 只把 Phone 页面横向拉宽
PC 办公桌面批量处理 多工作区、文件采集、会议、知识与待办重操作 手机底部导航和一层层返回
Watch 抬腕几秒钟确认或补记 摘要、待办、闪卡、知识速览、短输入 长文编辑、复杂表单和多层导航

这些职责不是先画一张漂亮表格再往项目里套,而是从大屏空旷、导航上下文丢失、PC 专属工作流和 Watch 圆屏限制等实际问题倒推出来的。Phone 解决“别让信息跑掉”;Pad 解决“上下文别总被新页面顶走”;PC 解决“我现在要坐下来认真干活”;Watch 解决“抬腕看一眼、点一下、说一句”。

让手表编辑五千字会议纪要,不叫“一次开发,多端部署”,叫对手腕实施职场霸凌。

三、Phone 和 Pad 为什么共用 HAP,却不共用 Shell

Nameo 的 Phone entry 同时声明了 phonetablet。窗口创建或尺寸变化后,工程会把像素换算成 vp,再根据 600vp、840vp 两个项目断点写入布局状态:

Window size
   ↓
BreakpointSystem
   ↓
compact / regular / expanded
   ↓
Phone Shell / PadAdaptiveShell

这里有一个特别容易写错的判断:断点判断的是当前窗口,不是设备身份证。

我关心的不是“你是不是折叠屏”,而是“你现在展开到了什么窗口形态”。按窗口断点的设计,宽折叠、双折叠、三折叠在不同展开状态下可能进入不同布局;窄窗口继续走 Phone 形态,达到 regular/expanded 后,信息结构才切成 Pad 工作台。这里说的是布局策略,不代表当前版本已经验收了所有折叠型号。

脱敏后的核心分支其实很短:

const mode = updateBreakpoint(windowWidthVp)

if (mode === 'compact') {
  PhoneShell()
} else {
  PadAdaptiveShell()
}

这也是我们没有急着再拆一个独立 Tablet HAP 的重要原因。未来达到 regular/expanded 窗口的折叠形态,本身就需要复用 Pad 布局。如果 Phone、Tablet 过早分成两套产品,后面同一套适配要维护两遍;同一 bundle 下再塞一个 Tablet entry,还会带来安装、共存和测试成本。算完这笔账,我决定先不拆,而不是为了目录看起来更“多端”就多造一个包。

但“共用 HAP”绝不等于“Pad 是大号 Phone”。

Phone 上打开知识卡,可以进入独立详情页;Pad 上打开同一张卡,左侧列表应该保留,只更新右侧详情。Phone 上页面跳转是合理导航,Pad 上如果每点一次都把工作区整个顶走,大屏就只剩“屏幕大,迷路也更远”。

四、设置页把我教育了:大屏不是把 width 改成 100%

第一次直接搬 Phone 设置页时,最刺眼的问题就是中央内容太小、两边太空。但修复它也不是把每一行强行拉满整块屏幕。

如果“AI 模式”开关从左边横跨到右边,信息之间的关联反而会变弱。

Nameo 后来的 Pad 设置不再沿用手机的长列表,而是单独做了设置工作区:账号中心、声纹、系统设置、关于页各自进入工作区;登录资料与知识统计可以组成双栏;设置项按成对的两列重新组织;内容区还设置最大宽度,避免在超宽窗口里无限拉长。

注意,这里的关键不是某个固定百分比。设置页“约占六成”只是我当时看到的现象,不是 HarmonyOS 规范。真正可复用的方法是:

窄窗口:接近全宽,保证可用
中宽窗口:开始重排,单栏变双栏
大窗口:限制内容宽度,给结构留白,而不是给空白留白

在这里插入图片描述

留白本身没有罪。没有信息结构的留白,才会让用户觉得“这应用只是被放大了”。

五、Nameo 为什么最终让 PC 独立拆包

看到这里可能有人会问:既然 Phone/Pad 可以共用,PC 为什么不继续共用?

对 Nameo 来说,决定因素不是多了一种设备,而是 PC 的任务、窗口、交互和功能演进已经与移动端明显分叉。

Nameo 的 PC 不只是多一个侧栏。它有独立的桌面工作台和自由窗口处理;AI 通话收起后是能在窗口内拖动、带边界约束的浮条;会议有列表/详情和录制控制/实时转写的双栏结构;文件采集要承接系统选择、待处理文件和批量入库。

这些都不是在 Phone 页面外面包一层 if (isPC) 就能体面解决的。

我一开始也舍不得拆。毕竟代码都写了,能共用一行是一行。但当产品层的判断越来越像春运售票窗口——Phone 走左边、Tablet 走右边、2in1 请上二楼——所谓复用就已经变成大家挤在同一间屋子里互相打扰。

所以 PC 使用独立产品 HAP,但这不意味着全部重写。PC 的页面、路由、窗口和交互独立,待办、知识、会议和认证等领域能力仍然依赖共享模块。

一句话概括:

PC 拆的是产品体验,不是把业务底座也复制一份。

六、feature 到底拆多细?我的标准不是“看起来高级”

Nameo 的工程边界可以简化成三层。注意这里不是一条必须逐层经过的单行流水线:产品层会同时依赖 feature 和 common,feature 也会依赖 common。

products
├─ phone:Phone 页面 + Pad adaptive 工作台
├─ pc:桌面窗口、导航和 PC 交互
└─ wearable:圆屏、表冠和轻动作
   ├────→ features:todo / knowledge / chat / me / meeting
   └────→ common:模型、数据库、KV、网络、认证、底层服务
features ─→ common

common 不是“暂时不知道放哪儿”的代码收容所,features 也不是为了目录好看而切得越碎越好。

我现在判断拆分力度,会连续问三个问题:

  1. 这段代码在不同端解决的是不是同一个业务问题?
  2. 它们变化的原因是不是相同?
  3. 抽出来以后,产品层会不会反而塞满端类型判断?

待办的读取、完成状态持久化、知识卡更新,可以共享;Pad 的筛选工作区、PC 的分组列表、Watch 的紧凑字段投影,不该硬共享。变化原因相同的代码才值得放到一起,不是长得像就必须住一间房。

七、同一份数据,为什么仍会在不同端出问题

多端架构图画完以后,最容易产生一种错觉:既然 ViewModel 和 Repository 共用了,四端状态就会自动整齐得像军训方阵。

现实当然没有这么配合。

同一条待办在领域层有相同语义:读取、勾选、持久化、同步。但到了产品层:

  • Pad 会投影成待处理/已处理与类型筛选;
  • PC 会按普通、紧急、逾期重新分组;
  • Watch 只保留标题、待办文本和完成按钮等紧凑字段。

共享 ViewModel 更新,不等于这些端侧二次数组一定自动刷新。上一篇《Java 程序员第一次看 ArkTS:像老乡,但口音有点重》里详细复盘过的“PC 待办勾选后数量变了、列表不动”,本质上就是这一类问题:数据已经变了,界面仍然持有旧快照。

最后的修复不是放弃共享,而是把边界说清:领域层尽量收住业务真相,产品层负责生成自己的展示投影;不能指望对旧数组原地改两下,框架就能自动理解你的良苦用心。Nameo 目前仍在渐进收紧这条边界,并不是已经完成了教科书式的纯 MVVM。

这也是多端项目比 Demo 更值钱的地方:难点往往不在“能不能调用 API”,而在共享状态经过不同页面结构、导航和生命周期后,还能不能保持一致。

对 AI 应用也是同一条逻辑:Phone/Watch 更像轻量采集入口,Pad/PC 更适合整理和深度操作;共享层维持知识与待办语义,RAG 负责从知识中找到依据,Agent 再决定要不要拆任务、调工具。多端页面不应该各自复制一套 AI 业务真相。

八、Watch 不是缩小版 Phone,KV 同步也不是写一行 put

Watch 最先逼我承认的一件事是:通用组件能画出来,不代表它适合圆屏。

Nameo 的 Watch 首页采用纵向 5 屏 ArcSwiper,承载用户信息、今日摘要、知识速览、待办和闪卡。我没有继续拿普通 Swiper 硬撑。ArcSwiper 面向弧形轮播,平台能力覆盖触摸、导航点和表冠等切页方式,其中表冠响应还依赖组件获焦;Nameo 当前先用它解决五屏内容在圆形视口中的组织问题,具体真机动作只按后面的专项证据陈述。组件换成圆屏专用形态之后,UI 才像手表应用,而不是有人拿剪刀把手机页面裁成了一个圆。

更麻烦的其实是登录同步。

Watch 不提供一套独立登录页,而是由 Phone 通过分布式 KV 同步登录状态,Watch 启动后读取,再恢复本地账号资料。先把安全边界说在前面:这里只能放最小必要状态或受保护的短期凭据,绝不能把长期认证信息明文塞进去。下面只讲初始化和补发时序,不会展示真实认证字段。至于怎样过期、撤销和解绑,还得按正式安全方案设计,不能把这段实现照搬成万能登录方案。架构图上看起来只有几根箭头:

Phone 登录
  → 写入认证 KV
  → 分布式同步
  → Watch 读取
  → 恢复登录状态

真机第一次却出现过“手机明明已经登录,手表没反应”。根因不是 KV 不会同步,而是时序:Phone 的认证服务先恢复了登录态,分布式同步服务还没初始化好;登录回调只发布一次,失败以后就没人再管。Watch 初装时又读得太早,于是两边都很努力,刚好彼此错过。

Watch 端对应的恢复逻辑,根据真实代码脱敏后大致如下:

await syncService.init(context)
await authService.init(context)
await authService.loadAuthFromKvStore()

syncService.setAuthChangeCallback(() => {
  authService.loadAuthFromKvStore()
})
scheduleAuthReload(2500)
scheduleAuthReload(8000)

后来的实现补了几层兜底:

  • Phone 在同步服务初始化完成后重新发布当前登录态;
  • Phone 发现远端设备或重新回到前台时再次补发;
  • Watch 监听登录状态 key 变化;
  • Watch 启动后和回前台进行少量延迟重读。

在这里插入图片描述

登录状态数据和知识卡数据会分开处理,退出登录也有单独标记。补发与重读必须保持幂等,正式生产还要处理版本、过期、撤销和旧值覆盖;延迟重读只是兜底,不是一致性协议。时序跑通也不等于安全工作结束,最小权限、凭据保护和设备解绑还得单独设计。

这段代码不如架构图优雅,但它很真实:多设备的上线顺序不可控,不能把“发过一次”当成“对方一定收到”。

这些不是纸上设计。我在 HUAWEI WATCH 5 上把摘要、速记、待办、知识速览、闪卡和设置都完整走过,后来又拿 Watch 5 和 Mate 60 Pro 专门联调了分布式 KV 登录、用户名和头像同步。也正是这次真机联调,才让我碰到了前面那个“手机已经登录,手表却没反应”的时序问题。

九、同一个 HAP,还是独立 HAP?我现在只问五个问题

以后再做多端技术选型,我不会先问“能不能复用”,而是按顺序问:

  1. 不同设备上的核心任务是否相同?
  2. 页面结构和主要交互是否相似?
  3. 自适应伸缩、折行或响应式断点能否解决?
  4. 输入方式、窗口生命周期和系统能力是否已经明显不同?
  5. 强行共享后,产品层的条件分支是否开始破坏可读性和测试成本?

对应的决策大致是:

任务相同、结构相似
  └─ 同一 HAP + 自适应

任务相同、结构会随窗口改变
  └─ 同一 HAP + 响应式 Shell

任务、交互、窗口和功能范围明显分叉
  └─ 产品层独立,必要时独立 HAP

Phone/Pad 是第二种;PC 和 Watch 更接近第三种。

这三行表示主要策略,不是互斥的 API 清单。同一页面经常会先在断点处切换结构,再用拉伸、隐藏、折行等能力完成局部自适应。

但这不是所有项目的标准答案。一个只展示新闻列表的 PC 端,可能完全没必要独立 HAP;一个重度生产力 Pad 应用,也可能最终值得拆开。架构选择必须跟着产品职责和未来重点走,不是照着 Nameo 的目录抄作业。

在这里插入图片描述

十、正式复盘

这次多端拆分最终让我确定了四条原则:

  1. 先定义每种设备上的用户任务,再讨论页面与代码复用。
  2. 断点服务于窗口,不要用设备名称代替布局判断。
  3. common/features 共享业务语义,products 尊重页面、导航、输入和生命周期差异。
  4. 多设备同步不能只在登录回调里“发一次就算了”,还要处理服务初始化、设备上线和回前台的时序。

复用率不是越高越好。真正应该放在一起的,是那些因为同一个原因而变化的代码。

这篇解决的是“同一套能力应该在哪台设备上怎样出现”。下一篇,我们继续往 AI 深水区走:聊天机器人、RAG、Workflow、Agent 到底差在哪儿?Nameo 为什么在六月准备提审时,又因为一个多跳问题返工?

这次不聊“接个大模型 API 就算 AI 应用”,聊企业真正敢用的 RAG 和 Agent 边界。


延伸阅读

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐