鸿蒙App与PC应用开发实战:从环境搭建到多端适配全流程指南
鸿蒙开发最近热度又上来了,尤其是PC版逐步放出来之后,问“做鸿蒙APP和PC应用到底怎么入手”的人越来越多。我基于最近一个从零到一的真实小项目,把整个链路完整跑了一遍:从DevEco Studio装环境、ArkTS写界面、状态管理、页面路由、本地存储,到多端适配思路、PC形态适配、调试与打包上架,甚至包括过程中踩过的几个典型的坑。这篇东西不求面面俱到,但求每一步都落地,拿过去能照着弄。
不管你是从Android/iOS转过来的移动端老手,还是写前端想蹭鸿蒙风口的新人,又或者是被公司派活做“鸿蒙化适配”的倒霉蛋,这篇都有参考价值。我会把技术选型背后的理由也讲清楚,避免你光抄代码、不懂为什么要这么干。
1. 整体设计与技术选型思路
1.1 这个项目要解决的到底是什么问题
先说背景。我这边的目标不是写一个Demo完事,而是要做一个真正能装到手机上、也适配PC窗口形态的鸿蒙应用,覆盖APP端和PC端两个入口。功能其实不复杂:一个带分类的日常事项记录工具,支持待办勾选、数据本地保存、统计展示。但正是这个“不复杂”的功能,把鸿蒙开发的骨架全串起来了。
如果你也是第一次做鸿蒙,建议先别急着上复杂业务,用这种“功能简单但链路完整”的项目练手最合适。因为鸿蒙开发最大的门槛不在业务逻辑,而在于整个开发范式和Android/iOS都不一样,这和前端、后端都不同:声明式UI、状态驱动、分布式能力、权限模型、多设备适配,这些基础概念没搞清楚,后面写稍微大一点的工程就会卡壳。
这个项目覆盖了几个关键链路:
- 从工程创建到真机/模拟器运行
- ArkTS声明式UI编写页面
- 页面路由与参数传递
- 状态管理与数据响应式更新
- 本地持久化存储
- 构建打包与签名配置
- PC窗口形态的适配思路
这几条串起来,基本就是鸿蒙应用开发的“主血管”。
1.2 技术栈选择:为什么是ArkTS + ArkUI + DevEco Studio
鸿蒙应用开发目前主流技术栈是:DevEco Studio作为IDE,ArkTS作为主要开发语言,ArkUI作为声明式UI框架,配合Stage模型管理应用生命周期。
这里说说为什么选这个组合。
ArkTS是首选语言 。虽然系统支持Java(旧版)和C++(通过NDK),但新项目从官方推荐到社区落地,都是ArkTS为主。ArkTS本质上是TypeScript的超集,在TS基础上加了静态类型约束和UI开发相关的语法糖。如果你写过Vue3或者React+TS,上手会非常顺。我第一次打开ArkTS文件的感觉是“这不就是Vue的setup语法换个皮吗”,虽然实际细节有差异,但这个迁移成本确实很低。
ArkUI是UI层核心 。它属于声明式UI,结构上和SwiftUI、Jetpack Compose是同一类东西,但语法更接近前端习惯。用@State装饰器管理状态,用build方法描述界面,数据和视图自动双向绑定。刚开始不熟悉这套写法会有点不习惯,但等你习惯了状态驱动视图之后,再回头写命令式UI真的会觉得难受。
Stage模型 这个一定要提前搞懂。新版HarmonyOS应用的入口结构,从“一个Ability打天下”变成了“Module → Ability → UIAbility”的层级关系,每个UIAbility有独立的生命周期。这直接影响你怎么设计应用入口、怎么管理页面栈,所以开头就把它梳理清楚,后面少踩很多坑。
1.3 “一次开发,多端部署”的真实含义
鸿蒙宣传最多的一句话是“一次开发,多端部署”,但这句话容易被误解为“写一套代码到处跑,完全不用改”。真实情况是: 逻辑层可以复用,UI层必须做适配 。
手机屏幕窄长,触控为主;PC窗口宽大,鼠标键盘为主;折叠屏和Pad介于两者之间。同样一个列表页面,手机上是全屏卡片流,PC上可能是左侧导航+右侧内容的三栏布局。ArkUI提供的解决方案是栅格布局(GridRow/GridCol)、断点监听(breakpoints)、自适应拉伸(Flex自适应、百分比)这几种机制的组合。
所以我在设计项目时,从一开始就把“多端适配”当成核心需求来写,而不是功能做完再补适配。鸿蒙的响应式布局不是在PC上简单把窗口拉大就行,而是要利用系统能力做布局重构。这点我会在第4节单独展开。
2. 环境准备与首个工程搭建
2.1 DevEco Studio安装与版本坑
去官网下载DevEco Studio,装完之后会带一个HarmonyOS SDK Manager,类似Android Studio的SDK Manager。这里有几个版本层面的坑需要提前说:
- 新版本API(比如API 12+)老设备不支持,需要根据目标机型选择合适的compileSdkVersion。
- 模拟器方面,手机上装模拟器还好,PC上跑模拟器对硬件要求不低,不过开发阶段用Previewer看改版效果也够用。
- 首次新建工程时需要自动下载SDK和构建工具,国内网络环境有时候会卡住,出现“下载超时”别慌,多试几次,或者手动配置镜像源。
这个环节看起来琐碎,但一定别跳过,我把好几个人问过的问题都列出来:创建工程后提示“SDK component missing”、Previewer不能实时刷新、模拟器启动黑屏。这些大概率是SDK版本不匹配或者预览器缓存问题。处理方式分别对应:打开SDK Manager重新安装对应版本组件、删除Previewer缓存重启IDE、冷启动一次模拟器。
2.2 创建工程与目录结构理解
新建工程时选择 Empty Ability模板 ,然后选择Stage模型、ArkTS语言。工程建好后,目录结构大概长这样:
AppScope/
app.json5 // 全局配置,应用图标、名称等
entry/
src/main/
ets/
entryability/
EntryAbility.ets // 页面入口能力,管理生命周期
pages/
Index.ets // 首页
resources/
base/
element/ // 字符串、颜色等资源
media/ // 图片资源
module.json5 // 模块配置,权限声明等
build-profile.json5 // 构建配置
oh-package.json5 // 依赖声明,类似package.json
这里面最需要理解的是
EntryAbility.ets
和
pages
目录的关系。
EntryAbility.ets
相当于应用的入口Activity,它负责加载首页并管理整个应用的生命周期;
pages
下面就是实际的页面组件,每个
.ets
文件就是一个可独立路由的页面。
对于前端转过来的朋友,我做个对照:
-
module.json5类似app.json(小程序)或AndroidManifest.xml,负责全局配置。 -
pages目录类似pages路由目录。 -
oh-package.json5类似package.json,管理三方依赖。 -
EntryAbility.ets类似App.vue+main.js的入口角色。
搞清这个映射关系,你的第一反应就不会是“这什么鬼”,而是“哦,这不就那啥嘛”。
2.3 快速跑通Hello World
新建工程默认的
Index.ets
已经有一个页面示例,先别改,直接点击顶部工具栏的运行按钮,选模拟器或Previewer看一眼效果。如果这一步能正常跑通,你的鸿蒙开发环境就基本OK了。
跑通之后,可以试着改一下页面的标题和文本,体验一把热更新。改文件保存后,Previewer一般自动刷新,模拟器上则需要点击重新运行。
在开始写业务代码前,我建议你先做一个“十分钟小练习”:写一个页面,放一个TextField输入框和一个Button按钮,点击按钮后把输入内容显示在页面下方。这个练习涉及@State状态、TextInput组件、Button组件、@Builder自定义构建函数,练完你对ArkUI的基础写法就有手感了。
我当时的练习代码长这样,你也可以直接复制跑一下:
@Entry
@Component
struct Index {
@State inputValue: string = ''
@State displayValue: string = ''
build() {
Column({ space: 16 }) {
TextInput({ placeholder: '请输入内容', text: this.inputValue })
.onChange((value: string) => {
this.inputValue = value
})
.height(48)
.backgroundColor('#f5f5f5')
Button('显示内容')
.onClick(() => {
this.displayValue = this.inputValue
})
Text(this.displayValue)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(16)
}
}
这段代码看起来简单,但已经把ArkUI最核心的机制走了一遍:
@State
声明响应式状态、
build
声明式描述UI、组件事件绑定。你只要把这三个点练熟,后面写复杂页面就是“复制粘贴+调整结构”的事。
3. APP端实战:一个带统计功能的事项管理应用
3.1 页面结构与路由设计
我这次做的是一个事项管理应用,整体有两类核心页面:
- 首页列表页:展示事项列表,支持新增、勾选、删除、分类筛选
- 统计页:展示完成率、各分类数量等统计图表(用基础组件拼,不引三方库)
页面结构不复杂,但路由跳转、参数传递是必练的点。
鸿蒙路由使用
router
模块,跳转方式有两种:
// 方式一:通过router模块跳转
import { router } from '@kit.ArkUI'
router.pushUrl({
url: 'pages/StatisticsPage',
params: {
category: 'work'
}
})
// 方式二:Navigation组件,类似Flutter的Navigator
如果做的是单模块中小型应用,用
router
就够了
,简单直接;但如果你要做底部Tab+多页面栈的大型应用,建议用
Navigation
组件来做,因为它的页面栈管理更灵活,支持
Navigation
嵌套、转场动画自定义等高级特性。
页面间传参时,接收方的写法:
import { router } from '@kit.ArkUI'
@Entry
@Component
struct StatisticsPage {
@State category: string = ''
aboutToAppear(): void {
const params = router.getParams() as Record<string, string>
if (params) {
this.category = params.category
}
}
}
这里有个坑要注意:
router.getParams()
返回的是
Object
类型,最好先判空再断言类型,不然编译时可能报空指针相关警告
。我刚写的时候直接强制断言类型,传参没问题,但不传参进入页面时会拿到
undefined
,后面一用就崩。
3.2 ArkTS状态管理:@State、@Prop、@Link、@Observed
ArkUI的状态管理是整个框架的魂。理解好这几个装饰器,你的页面就越写越顺手。
-
@State:组件内部的状态,变化时自动刷新当前组件。 -
@Prop:父组件传给子组件的值,单向同步,子组件内的修改不会影响父组件。 -
@Link:父子组件双向同步,子组件修改会同步回父组件。 -
@Provide/@Consume:跨多层级传值,类似Vue的provide/inject。 -
@Observed+@ObjectLink:用于观察对象内部属性变化,适合复杂数据模型。
我用一个实际场景说明区别。在事项列表应用里,条目组件
TodoItem
需要显示待办内容,并支持点击勾选。如果我直接传一个
Todo
对象给子组件,子组件内修改
todo.checked
,父组件的列表数据不会感知到变化,因为普通对象不是响应式的。解决方案就是:
方案A:把选中状态提升到父组件,父组件用@State数组保存,子组件通过@Link绑定当前项的选中值。
方案B:给Todo类加上@Observed装饰器,子组件用@ObjectLink接收,这样对象内部属性变化也会触发刷新。
我实际选的是方案B,代码大概是这个样子:
@Observed
class Todo {
id: number = 0
title: string = ''
checked: boolean = false
category: string = ''
constructor(id: number, title: string, category: string) {
this.id = id
this.title = title
this.category = category
}
}
@Component
struct TodoItem {
@ObjectLink todo: Todo
build() {
Row({ space: 12 }) {
Checkbox()
.select(this.todo.checked)
.onChange((value: boolean) => {
this.todo.checked = value
})
Text(this.todo.title)
.strikethrough(this.todo.checked)
}
}
}
注意
@Observed
只能观察“对象内部已定义的属性”,如果给对象动态加新属性,不会触发UI刷新。所以
定义模型时要把可能用到的字段都声明好
,别学JS那套动态挂属性的用法。
3.3 数据持久化:首选项(Preferences)与关系型数据库的取舍
事项数据不能一重启就丢,所以必须做本地持久化。HarmonyOS提供了两种基础方案:
- 首选项(Preferences) :类似Android的SharedPreferences,适合轻量键值对存储,比如用户设置、配置信息。
- 关系型数据库(RDB) :基于SQLite封装,适合结构化数据、查询需求复杂的数据。
对事项列表这种数据,最合理的做法是用RDB存数据、用Preferences存“当前选中的分类”这类轻量状态。
RDB的建表和插入操作,代码流程如下:
import { relationalStore } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'todo_app.db',
securityLevel: relationalStore.SecurityLevel.S1
}
let store: relationalStore.RdbStore | null = null
async function getStore(context: common.UIAbilityContext): Promise<relationalStore.RdbStore> {
if (store) return store
store = await relationalStore.getRdbStore(context, STORE_CONFIG)
await store.executeSql(
'CREATE TABLE IF NOT EXISTS todo (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, checked INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL)'
)
return store
}
写入数据用
store.insert
,查询用
store.querySql
,更新用
store.update
。SQL语法和SQLite基本一致,写过Android数据库的会非常熟悉。
这里有个真实踩坑:
第一次调
getRdbStore
必须在UIAbilityContext下执行
,不能在组件里直接调。如果你的页面里突然发现拿不到context,检查一下是不是忘了从
getContext(this)
获取。
写入数据后,页面要刷新,通常的做法是把数据库查询结果放到
@State
数组中。但要注意,
@State
在初始化时赋值一次,后面手动修改需要直接替换整个数组引用
,不能只调
push
或
splice
就指望UI跟着变。
3.4 列表渲染:ForEach的key生成规则
列表是APP应用最核心的组件之一。ArkUI里用
ForEach
渲染列表,但它和普通的前端
v-for
有一个很大区别:
ForEach必须要给
keyGenerator
,用来唯一标识每一条数据
。
ForEach(
this.todoList,
(todo: Todo) => {
TodoItem({ todo: todo })
},
(todo: Todo) => todo.id.toString()
)
第三个参数就是keyGenerator。如果不传或者乱传(比如传index),在删除某条数据时可能会出现界面错乱,因为ArkUI的diff算法依赖key来判断哪条是新增、哪条是删除。
key不要用index的主要原因是:当列表插入或删除一条数据后,index会整体后移或前移,系统会误以为原来的条目被删了、新条目加进来了,导致不必要的组件重建,严重时还会产生动画错乱。 用数据库自增id做key是最稳的 。
4. PC应用方向:多端适配与桌面形态实战
4.1 鸿蒙PC应用到底是个什么形态
这个节骨眼上,鸿蒙PC版已经不只是概念了,开发版的PC窗口形态应用已经能跑起来。从开发者的视角看,PC应用不是独立的一套技术栈,而是“同一个Ability在不同窗口尺寸下的差异化呈现”。
也就是说,你的手机APP和PC应用可以共用一套工程,用系统提供的断点去判断窗口宽度,进而切换不同的布局策略。这也是“从APP到PC应用实战”这个标题真正的含金量所在。
我做了几个适配点,原则是移动端单栏、PC端多栏:
4.2 用断点和栅格布局做响应式结构
ArkUI提供
GridRow
和
GridCol
来实现栅格布局。以手机竖屏(小于600vp)为单列布局,平板或PC宽屏(大于840vp)为双栏或三栏布局。
我做的适配逻辑是:
@State currentBreakpoint: string = 'sm'
aboutToAppear(): void {
this.updateBreakpoint()
}
// 通过窗口宽度判断当前断点
private updateBreakpoint(): void {
const windowWidth = this.getWindowWidth()
if (windowWidth >= 840) {
this.currentBreakpoint = 'lg'
} else if (windowWidth >= 600) {
this.currentBreakpoint = 'md'
} else {
this.currentBreakpoint = 'sm'
}
}
@Builder
ListPage() {
// 列表内容
}
@Builder
SideBar() {
// 侧边栏内容
}
build() {
if (this.currentBreakpoint === 'lg') {
Row() {
this.SideBar()
Divider().vertical(true)
this.ListPage()
Divider().vertical(true)
this.StatisticsPanel()
}
.width('100%')
.height('100%')
} else {
Column() {
this.ListPage()
}
.width('100%')
.height('100%')
}
}
这段代码反映了核心思想: UI布局可以按断点拆成多个Builder函数,在大屏组合,在小屏堆叠 。逻辑层(数据获取、状态管理)完全不用改,UI层换一套组合方式。
4.3 输入与交互适配:鼠标、键盘、触控
PC端和移动端最大的差异是输入方式。手机靠触摸、滑动手势;PC端,用户首先会Expect鼠标悬停反馈、右键菜单、滚轮滑动、快捷键操作。这几个点如果不做,在PC上就是“能看但不好用”。
我在项目里做了三件小事:
-
列表项加入
hoverEffect属性,鼠标悬停时有高亮反馈。 -
长按操作在PC上换成右键菜单,用
bindContextMenu绑定。 - 支持键盘上下键在列表间快速选择,回车切换勾选状态。
这些适配工作量不大,但对PC端的体验提升非常明显。尤其是 右键菜单 ,手机端用户完全无感,但PC端用户下意识就会右键,这个不做会觉得“这应用不像原生的”。
4.4 多窗口能力和元服务扩展
PC端还有两个值得关注的点: 自由调整窗口大小 和 后台任务/元服务全局能力 。
自由调整窗口方面,手机上的应用窗口基本是固定比例,PC上窗口可以被用户拉到很窄或很宽,所以上述的断点适配需要在窗口尺寸变化时实时响应。ArkUI提供了
onWindowSizeChange
这类回调,也可以监听窗口变化事件重新计算断点,这个必须做,否则用户一拉窗口布局就乱。
至于元服务和卡片,属于另一个大的扩展方向。元服务可以做成桌面卡片、负一屏卡片等轻量形态,PC端也可以放桌面小组件。我的项目里做了一版简单的卡片,显示今天待办数量,点击跳进主应用。这个扩展方向很适合工具类应用,因为用户不需要打开完整APP就能看到关键信息,符合“快”的需求。
5. 调试、打包与常见问题排查实录
5.1 日志与调试工具链
鸿蒙开发的日志打印用的是
hilog
,类似Android的
Logcat
。基本用法:
import { hilog } from '@kit.PerformanceAnalysisKit'
const DOMAIN = 0x0001
const TAG = 'TodoApp'
hilog.info(DOMAIN, TAG, '当前事项数量: %{public}d', this.todoList.length)
注意
%{public}d
这个写法,鸿蒙的hilog要求显式标记“是否隐私”,公共信息用
public
,隐私信息用
private
。如果直接写
%d
会遇到格式无法正确打印的问题。
DevEco Studio内置的调试功能比较全,包括断点调试、性能分析器(Profiler)、内存检测。性能分析器在PC模拟器上比较吃资源,实测下来启动会比较慢,但分析问题确实很好用,比如卡顿定位可以用帧率分析。
5.2 真机调试与签名配置
模拟器跑了没问题之后,一定要拿真机试一遍。真机调试需要:
- 手机上开启开发者模式,USB连接电脑。
- DevEco Studio里配置自动签名。首次需要登录华为开发者账号并同意相关协议,系统自动生成调试证书和Profile。
- 如果是商用发布,还需要申请签名证书和发布Profile,走AGC(AppGallery Connect)流程。
真机调试有个我印象很深的坑: 应用签名不一致导致安装失败 。现象是卸载旧包后仍然提示签名冲突,这是因为系统缓存了旧的授权指纹。处理方式是去手机设置里把“应用热修复”相关的开发者授权清掉,或者手动重启开发手机。
另外,
HarmonyOS NEXT版本对权限管理非常严格
,如果你的应用用到了敏感权限(比如读取位置、读取通讯录),首次申请时要配合
onPermissionRequest
回调,建议在代码里做“申请弹窗被拒绝后的引导跳转设置页”逻辑。
5.3 常见问题速查表
我把开发过程中遇到的比较典型的问题整理成了一个表格,希望对你有直接帮助。
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| Previewer不显示内容 | Previewer缓存异常或组件不兼容 | 删掉预览器缓存,重启IDE;或改用模拟器验证 |
| 编译报错“Cannot find module” | 三方库未安装或路径不对 |
在oh-package.json5中检查依赖,执行
ohpm install
|
| 状态更新但UI不刷新 | @State引用未替换;@Observed未正确标记属性 | 替换整个数组引用;确保对象属性事先声明 |
| 数据库打开失败 | 没有正确传入Context | 使用getContext(this)而不是全局静态Context |
| 真机安装失败签名冲突 | 自动签名和缓存签名不一致 | 删除工程签名配置,重新自动签名;重启手机 |
| PC窗口拉伸布局错乱 | 没有监听窗口尺寸变化并更新断点 | 增加窗口尺寸监听并动态刷新breakpoint |
| 应用在PC上触摸与鼠标操作冲突 | 触控和鼠标事件未区分 | 使用hoverEffect、右键菜单等PC专属交互 |
| hilog日志不输出 | 没有打tag或过滤条件不对 | 检查TAG和DOMAIN,在DevEco过滤栏搜TAG |
5.4 PC端升级刷机场景的一个补充提醒
评论区可能有人问“把开发电脑升级成鸿蒙PC版后进不了系统怎么办”。这属于设备升级场景,不是应用开发问题,但我也提一嘴:如果你是拿现役主力设备折腾开发者预览版,风险确实存在,升级前务必备份数据,并且确认你的设备在官方支持列表里。做开发的人可以准备一台专门的测试设备,不要拿工作主力机去刷,这是我从嵌入式开发那边习得的教训:开发环境归开发环境,生产环境归生产环境,混在一起迟早出事。
6. 从这往后怎么走:一些来自实跑过程的建议
如果你完整跟着上面的路子走完一遍,那你已经具备了一个鸿蒙开发者的基本盘。但离真正能在团队里独当一面,还有几个方向值得继续深挖。
第一个方向是 元服务和卡片开发 。这算鸿蒙生态里最“原汁原味”的能力,Android和iOS都没有对等的开发范式。卡片不只是桌面小组件,还能嵌入到负一屏、锁屏、甚至系统搜索里,很适合工具类App做用户触达。
第二个方向是 分布式能力 。鸿蒙的多设备协同是其他平台没有的核心差异化能力,跨设备流转、分布式数据管理,这些在智能家居、车机、办公场景都有应用空间。我下一步计划就是给这个事项应用加上跨设备同步能力,让手机上的待办在PC上也能实时看到。
第三个方向是 AI应用开发 。热搜里也能看到智能体开发、大模型微调之类的词越来越热,鸿蒙生态现在也提供了端侧AI能力和大模型接入接口,比如可以把意图识别、语音转文字这类能力接进应用。这个方向建议先了解系统AI接口,再逐步接入自己的业务场景。
最后一个建议,也是我每次带新人都会强调的: 开发联调时,手边一定要有真机和模拟器两个环境 。Previewer看布局效率高,模拟器跑完整流程,真机最后验收所有细节。不要图省事只在预览器里改改就交付,真机上的字体渲染、性能表现和输入体验,跟模拟器还是有差别的。
我做这个项目最深的一个体会是,鸿蒙开发其实已经相当成熟了,问题在于国内的中文教程和社区积累还没有Android那么庞大,很多问题要靠翻官方文档和调试源码自己解决。但也正因为这样,现在入局的人能吃到技术红利——会鸿蒙的人越来越多,但真正能把APP和PC形态都做好的人,在各行各业里都还是稀缺资源。你如果能把这篇里的每一步都亲手做一遍,就已经超过了绝大多数只停留在“看教程”阶段的人了。
更多推荐

所有评论(0)