鸿蒙原生应用实战(五):构建优化与踩坑实录
鸿蒙原生应用实战(五):构建优化与踩坑实录
前言
前四篇完成了「单词达人」应用的全部开发工作。本篇作为收官之作,集中总结开发过程中遇到的各种「坑」和对应的解决方案,以及构建优化技巧。希望对正在或准备进行鸿蒙原生开发的同学有所帮助。
一、ArkTS 严格模式踩坑
1.1 arkts-no-untyped-obj-literals(最频繁的报错)
错误信息:
Object literal must correspond to some explicitly declared class or interface
场景复现:当传递对象字面量给函数参数时触发。
// ❌ 报错
router.pushUrl({ url: 'pages/Detail', params: { id: 1 } });
const items = [
{ name: 'Alice', age: 20 }, // ❌ 也报错
];
解决方案:要么提取为类型变量,要么使用类型断言。
// ✅ 方案1:提取为预声明变量
const routeParams: Record<string, Object> = { 'id': 1 };
const route: RouteOption = { url: 'pages/Detail', params: routeParams };
router.pushUrl(route);
// ✅ 方案2:定义接口
interface RouteOption {
url: string;
params?: Record<string, Object>;
}
1.2 arkts-no-noninferrable-arr-literals
错误信息:
Array literal's type cannot be inferred
场景复现:创建数组时类型不明确。
// ❌ 报错 - 类型不明确
const goals = [5, 10, 15, 20]; // OK, 可以推断是 number[]
const data = [
['word', '/phonetic/', 'meaning'], // ❌ 不确定是 string[] 还是 [string, string, string]
];
解决方案:显式类型标注。
// ✅ 显式类型
const data: [string, string, string][] = [
['word', '/phonetic/', 'meaning'],
];
// 或者自定义类型别名
type CompactWord = [string, string, string];
const data: CompactWord[] = [
['word', '/phonetic/', 'meaning'],
];
1.3 函数可能抛出异常
警告信息:
Function may throw exceptions. Special handling is required.
这个警告在 router.pushUrl()、router.back() 等 API 调用时频繁出现。
解决方案:使用 try-catch 包裹(不处理也可以运行,只是警告):
try {
router.pushUrl({ url: 'pages/WordListPage' });
} catch (err) {
console.error('Navigation failed: ' + JSON.stringify(err));
}
不过由于这些只是 WARNING 不是 ERROR,不影响构建,我们选择保持代码简洁,忽略这些警告。
二、@State 响应式陷阱
2.1 深层修改不触发更新
这是开发测验页面时遇到的最大坑。
@State questions: QuizQuestion[] = [];
// ❌ 直接修改内部对象属性 — 不触发 UI 更新
selectOption(index: number) {
const q = this.questions[this.currentIndex];
q.selectedIndex = index; // 数组引用没变,@State 检测不到
q.answered = true;
}
原理:ArkTS 的 @State 通过引用比较检测变化。修改数组元素内部的属性,不会改变数组的引用地址,所以 @State 认为「没有变化」,UI 不会重新渲染。
解决方案:创建新对象 + 新数组,替换整个引用。
// ✅ 正确做法
selectOption(index: number) {
const q = this.questions[this.currentIndex];
const updatedQ: QuizQuestion = {
...q, // 展开原对象
selectedIndex: index,
answered: true,
isCorrect: q.options[index] === q.correctMeaning
};
const newQuestions = [...this.questions];
newQuestions[this.currentIndex] = updatedQ;
this.questions = newQuestions; // ✅ 新引用,触发更新
}
2.2 @State 与 @Prop/@Link 的选择
| 装饰器 | 用途 | 使用场景 |
|---|---|---|
@State |
组件内部状态 | 本组件管理的状态 |
@Prop |
父→子单向传递 | 子组件只读展示 |
@Link |
父子双向同步 | 子组件需要修改父组件状态 |
本应用的所有页面都是独立 Entry,没有子组件传参需求,所以只用到了 @State。
2.3 状态初始化时机
// ✅ 在声明时直接初始化(推荐)
@State stats: UserStats = getDefaultStats();
@State wordOfDay: WordItem = getWordOfDay();
// ✅ 或者在 aboutToAppear 中初始化
aboutToAppear(): void {
this.loadStats();
}
注意:不要在 build() 中修改
@State变量,会导致无限循环渲染。
三、PersistentStorage 与 AppStorage 的选择
鸿蒙提供两种本地存储方案:
| 特性 | AppStorage | PersistentStorage |
|---|---|---|
| 生命周期 | 应用运行期间 | 持久化到磁盘 |
| 使用方式 | get/set | 绑定 @State |
| 适用场景 | 临时状态 | 需要持久化的数据 |
本应用的策略:用 AppStorage 存储,手动 JSON 序列化/反序列化。
// 保存
AppStorage.set<string>('userStats', JSON.stringify(stats));
// 读取
const stored = AppStorage.get<string>('userStats');
if (stored) {
stats = JSON.parse(stored) as UserStats;
}
为什么不直接用 PersistentStorage?因为 PersistentStorage 绑定 @StorageProp 后,数据变化自动同步到磁盘,但需要预定义键和类型,灵活性不如手动管理。
四、路由与页面导航问题
4.1 getParams 被废弃
警告:
'getParams' has been deprecated.
API 23 中 router.getParams() 标注为废弃(deprecated),但仍然可用。替代方案是使用新的路由 API,但 API 23 没有导出。
当前做法:继续使用 router.getParams(),忽略废弃警告。
const routeParams = router.getParams() as Record<string, Object>;
const wordId = routeParams['wordId'] as number;
4.2 router.pushUrl 被废弃
同样标注为废弃,但 API 23 下仍然是最稳定的路由方式。
替代方案(如果 API 版本支持):使用 @ohos.router 的 findUrlByName 或 UIContext 的路由。
4.3 页面间传参不能太复杂
路由参数通过 params 传递,只能传可 JSON 序列化的数据。不要传函数、组件等。
// ✅ 可以传
router.pushUrl({ url: 'pages/Detail', params: { wordId: 1 } });
// ❌ 不能传函数
router.pushUrl({ url: 'pages/Detail', params: { callback: () => {} } });
五、构建优化
5.1 构建命令详解
node.exe hvigorw.js \
--mode module \
-p module=entry@default \
-p product=default \
-p requiredDeviceType=phone \
assembleHap \
--analyze=normal \
--parallel \
--incremental \
--daemon
各参数含义:
| 参数 | 说明 |
|---|---|
--mode module |
模块构建模式 |
-p module=entry@default |
构建 entry 模块的 default 产物 |
-p product=default |
使用 default 产品配置 |
--parallel |
并行构建(加速) |
--incremental |
增量构建(只编译修改的文件) |
--daemon |
启动守护进程(加快后续构建) |
5.2 增量构建提速
首次构建约 20-30 秒,增量构建只需 2-5 秒:
# 首次构建
> hvigor BUILD SUCCESSFUL in 23.5s
# 增量构建(只改了1个文件)
> hvigor BUILD SUCCESSFUL in 2.9s
5.3 HAP 包体积
未签名的 HAP 包约 500KB,其中大部分是 ArkTS 编译产物和资源文件。
5.4 调试构建
DevEco Studio 支持 Debug 模式,可以在断点处暂停查看变量:
# DevEco Studio 中点击 Debug 按钮,或使用命令行
node.exe hvigorw.js --mode module assembleHap --daemon
然后在模拟器或真机上运行,就可以断点调试了。
六、其他常见问题
6.1 @Builder 参数传递
@Builder 可以带参数:
@Builder
masteryButton(label: string, level: MasteryLevel) {
Button(label)
.fontColor(this.isActive(level) ? Color.White : $r('app.color.text_secondary'))
.onClick(() => this.setMastery(level))
}
// 使用
Row() {
this.masteryButton('未学习', MasteryLevel.UNLEARNED)
this.masteryButton('学习中', MasteryLevel.LEARNING)
}
6.2 ForEach 的键生成器
ForEach 的第三个参数是键生成函数,必须返回唯一且稳定的值:
// ✅ 用 ID 做键
ForEach(this.words, (item) => {
// ...
}, (item: WordItem): string => item.id.toString())
// ✅ 用字符串本身做键(选项列表)
ForEach(options, (option, index) => {
// ...
}, (option: string): string => option)
键不稳定会导致列表渲染闪烁或动画异常。
6.3 资源不存在
如果引用了不存在的资源,构建会报错:
ERROR: Resource xxx not found in module
解决方案:检查资源文件中的定义,确保名称拼写一致。
6.4 Scroll 嵌套 List
当 Scroll 包裹 List 时,可能会遇到滚动冲突。
解决方案:使用 List 的 edgeEffect 和 nestedScroll 属性,或避免不必要的嵌套。
本应用中,每个页面要么用 Scroll + Column,要么用 List,避免了两层可滚动组件嵌套。
七、项目总结
7.1 项目规模
经过5篇的开发,「单词达人」应用最终规模:
| 指标 | 数值 |
|---|---|
| 页面数 | 5个 |
| 代码文件 | 6个(1个模型 + 5个页面) |
| CET-4 词库 | ~530词 |
| 总词库 | ~565词 |
| 构建时间 | 首次 ~23s / 增量 ~3s |
| HAP 大小 | ~500KB |
7.2 技术要点回顾
- Stage 模型 + ArkTS:鸿蒙原生应用的标准技术栈
- 紧凑词库格式:三元组
[word, phonetic, meaning]节省60%存储空间 - @State 不可变性:修改数组内部对象必须创建新引用
- AppStorage 持久化:JSON + AppStorage 实现数据持久化
- 路由导航:
@ohos.router的 pushUrl/back/getParams
7.3 改进方向
如果后续要完善这个应用,可以考虑:
- 扩充词库:将其他分类也扩充到完整词库
- 语音播放:接入 TTS 实现单词发音
- 记忆曲线:基于艾宾浩斯遗忘曲线安排复习
- 云端同步:接入华为帐号实现多设备同步
- 暗黑模式:增加 dark theme 支持
- 单元测试:使用
@ohos/hamock编写测试用例

写在最后
五篇博文,从一个空白项目到一个完整的鸿蒙原生应用。回顾整个开发过程,最大的感受是:
- ArkTS 严格模式是把双刃剑 — 它让代码更安全,但也让上手门槛更高
- 声明式 UI 的开发思维转型 — 从「怎么修改控件」变成「状态变了 UI 自然会变」
- 鸿蒙生态在快速成熟 — API 23 已经相当完善,文档和社区资源也在快速增长
希望这系列文章能为正在学习鸿蒙开发的你提供一些帮助。如果有什么问题,欢迎在评论区交流讨论!
系列目录(完)
- ✅ (一) 项目规划与技术选型
- ✅ (二) 数据模型与词库构建
- ✅ (三) 五个页面开发(上):首页、词库、详情
- ✅ (四) 五个页面开发(下):测验、个人中心
- ✅ (五) 构建优化与踩坑实录
更多推荐




所有评论(0)