第2篇|从功能到技术栈:时间轨迹这类鸿蒙应用是怎么搭起来的
第2篇|从功能到技术栈:时间轨迹这类鸿蒙应用是怎么搭起来的
如果说第一篇更像是在讲“某一个页面怎么做”,那这一篇我想换个角度,讲清楚这类应用是怎么从功能、状态、权限、数据到页面,一层一层搭起来的。
这次的重点不放在某一个页面本身,而是放在整个应用的组织方式上。因为真正能把“拍照、水印、工作记录、相册、模板、账号、AI 工具”串起来的,不是某个炫技控件,而是一套足够清楚的技术栈和分层方式。
先看整体结构

这张图想表达的不是“页面很多”,而是“每一层各干各的”:
EntryAbility负责启动和全局初始化Index.ets负责路由和页面切换CameraService、LocationService、PermissionUtil负责能力封装AccountService负责账号与本地数据隔离HomePage、CameraPage、TemplatePage、ProfilePage等负责业务表达
这套技术栈,核心不是多,而是稳
| 层级 | 主要模块 | 作用 |
|---|---|---|
| 启动层 | EntryAbility.ets |
初始化持久化数据、窗口样式、系统栏和安全区 |
| 路由层 | Index.ets |
管理底部 tab、子页面返回、登录跳转 |
| 状态层 | AppStorage / PersistentStorage / @StorageLink |
管理全局状态和用户偏好 |
| 能力层 | CameraService / LocationService / PermissionUtil |
封装拍照、定位、权限申请 |
| 数据层 | AccountService |
保存会话、账号列表、用户数据 |
| 复用层 | watermark-camera HAR / has |
共享相机、水印、导航栏等能力 |
这套结构的好处很直接:业务页面不会直接和底层能力硬耦合,后续加功能的时候,不用到处改。
入口层:先把应用“站稳”
应用真正跑起来之前,先做两件事最重要。
第一件,是把用户会反复用到的状态提前放好。
PersistentStorage.persistProp('wmAutoWatermark', true);
PersistentStorage.persistProp('wmShowTime', true);
PersistentStorage.persistProp('wmShowAddress', true);
PersistentStorage.persistProp('wrAutoRecord', true);
PersistentStorage.persistProp('wmLocationHistory', '[]');
PersistentStorage.persistProp('wrRecords', '[]');
第二件,是把窗口和系统栏先处理好,让内容区真正可以“沉浸式”展开。
win.setWindowLayoutFullScreen(true);
win.setWindowSystemBarProperties({
statusBarColor: '#00000000',
navigationBarColor: '#00000000'
});
这两步看起来不起眼,但它们决定了整个应用的底座。状态不乱,布局不乱,后面的页面才不会一会儿跑偏、一会儿重算。
路由层:一个入口,多条业务线
Index.ets 的角色很像总调度。它不负责做具体业务,它只负责判断当前应该显示哪一页。
if (this.selectedTab === 'home') {
HomePage({ ... })
} else if (this.selectedTab === 'camera') {
CameraPage({ ... })
} else if (this.selectedTab === 'templates') {
TemplatePage({ ... })
} else if (this.selectedTab === 'profile') {
ProfilePage({ ... })
}
这里最值得提的一点,是它把“主页面”和“子页面”分开了。
- 主页面是
home、camera、templates、profile - 子页面是
timeLocation、workRecord、settings、photoAlbum、videoAlbum、loginPage等
这样做的结果就是:底部导航始终稳定,局部业务页面可以自由扩展,不会把整个壳子搞乱。
数据层:账号和本地数据分开存
这部分我觉得是这套应用里最成熟的地方之一。
AccountService 没有把所有数据硬塞进一个地方,而是做了三层区分:
session.json:只存当前会话device_accounts.json:存设备上的账号列表accounts/{openId}/userdata.json:存某个账号自己的业务数据
也就是说,登录态和业务数据是分开的,用户数据和设备数据也是分开的。
if (!this.isLoggedIn) {
let openId = this.huaweiOpenID;
let nickname = this.huaweiNickname;
if (!openId) {
openId = AccountService.getSessionOpenId(ctx);
nickname = AccountService.getSessionNickname(ctx);
}
if (openId !== '') {
AccountService.loadUserData(ctx, openId);
this.huaweiOpenID = openId;
this.huaweiNickname = nickname;
this.isLoggedIn = true;
}
}
这段恢复逻辑的价值很大:
- 用户重新打开应用时,不需要重新配置一遍
- 多账号切换时,数据不会互相覆盖
- 退出登录后,本地数据仍然能留在设备上,后续可以恢复
这就是“功能可用”和“产品可持续用”的区别。
能力层:把复杂系统藏进服务里
【时间轨迹】软件,可以在华为应用市场中下载,或在百度云下载:https://appgallery.huawei.com/app/detail?id=com.example.app_22&channelId=SHARE&source=appshare
真正能把应用做厚的,不是页面多,而是服务层稳。
1. 相机能力
CameraPage 并不是简单地拍一张照片,它实际上在做一条完整链路:
- 申请相机和存储权限
- 初始化相机预览
- 监听设备方向和传感器
- 管理拍照 / 录像 / 对焦 / 闪光灯 / 变焦
- 在保存时叠加水印
- 把结果保存到本地或系统相册
const results = await Promise.all([
PermissionUtil.requestCameraPermissions(ctx),
PermissionUtil.requestMediaPermissions(ctx),
]);
this.permissionGranted = results[0];
this.startLocationWatch();
2. 定位能力
定位不是独立展示,而是直接参与水印内容生成:
- 当前地址可显示
- 经纬度可补充
- 手动地点可覆盖
- 历史地点可回看
也就是说,定位不是“展示信息”,而是“业务输入”。
3. 权限能力
PermissionUtil 把授权拆成了几个清晰的方法:
- 摄像头权限
- 位置信息权限
- 媒体读写权限
这样写的好处是,页面不用自己去理解权限细节,只要知道“我现在能不能做这件事”。
功能层:每个页面都在干自己的活
这个应用的页面很多,但职责并不混乱。
HomePage是入口中枢,负责把功能卡片放到用户最容易看到的位置CameraPage是核心生产页,负责拍照、录像和实时水印TemplatePage负责模板浏览、收藏、最近使用、创建自定义模板TimeLocationPage负责时间、地点、历史地址和手动覆盖WorkRecordPage负责工作记录列表和分类管理ProfilePage负责账号信息、相册、扫描、收藏、最近使用、帮助和设置AIToolsPage则是能力扩展口,给后续智能功能留了位置
这里我很喜欢的一点,是首页的功能卡片不是“装饰”,而是明确的业务分发器。
new HomeFeature('1', '时间地点', '自动水印', '📍', '#EAF2FF'),
new HomeFeature('2', '工作记录', '项目记录', '📋', '#F0ECFF'),
new HomeFeature('3', '相册导入', '添加水印', '🖼️', '#E8F8EE'),
new HomeFeature('4', '水印设置', '编辑水印', '⚙️', '#FFF2E8'),
它不是单纯地“好看”,而是让用户一进来就知道自己该往哪里走。

为什么这套架构值得写
我觉得这套实现最值钱的地方,不是某一个炫目的功能,而是它把很多细碎能力都收进了一个统一框架里。
- 视觉层统一:底部导航、卡片、弹窗、预览风格一致
- 状态层统一:
AppStorage/PersistentStorage贯穿全局 - 能力层统一:相机、定位、权限、存储都被服务层接住
- 数据层统一:账号数据和设备数据分离,恢复逻辑清晰
- 扩展层统一:AI 工具、证件识别、文档扫描都能继续往下长
如果只看功能,这是一款“会拍照、会记录、会管理模板”的应用;
如果看技术栈,它更像是一个把鸿蒙能力串起来的完整业务壳子。
结尾
因为真正能让一个应用长期可维护的,往往不是某个页面做得多漂亮,而是它有没有把状态、权限、账号、能力和路由分层分清楚。
时间轨迹这套实现,恰好就是一个比较完整的例子:它不是把功能堆起来,而是把功能组织起来。
1. 边界和兜底
这类文章最容易被扣分的地方,不是“有没有代码”,而是“有没有把异常情况讲清楚”。
- 没有登录时,
AccountService不应该继续假装自己有用户数据,而是回到登录入口 - 没有定位权限时,水印可以继续工作,但地址字段应该降级为默认文案或手动地点
- 相机能力不稳定时,页面要给出明确提示,不要让预览区一直空白
- 本地数据不存在时,要能回退到默认值,不能直接把页面渲染成一堆报错状态
2. 验证清单
- 启动应用后,检查
PersistentStorage里的默认项是否已经生效 - 切换底部 tab,确认路由切换不会把主壳子打乱
- 退出账号再进入,确认
session.json和账号数据能正确恢复 - 关闭定位权限,确认水印页面仍然能正常拍照和保存
- 重新打开页面,确认历史记录和模板列表来自最新数据,而不是旧缓存
3. 还可以继续往下写什么
- 深浅色适配和沉浸式布局
- 隐私安全和沙盒保存
- 可信存储和迁移恢复
参考
- 鸿蒙三层架构官方文档
- 一体化开发,多端部署 - 工程结构
- 本文对应源码:
EntryAbility.ets、Index.ets、AccountService、CameraService、PermissionUtil
更多推荐


所有评论(0)