鸿蒙原生应用实战(五):构建优化与踩坑实录

前言

前四篇完成了「单词达人」应用的全部开发工作。本篇作为收官之作,集中总结开发过程中遇到的各种「坑」和对应的解决方案,以及构建优化技巧。希望对正在或准备进行鸿蒙原生开发的同学有所帮助。


一、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 的 findUrlByNameUIContext 的路由。

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 时,可能会遇到滚动冲突。

解决方案:使用 ListedgeEffectnestedScroll 属性,或避免不必要的嵌套。

本应用中,每个页面要么用 Scroll + Column,要么用 List,避免了两层可滚动组件嵌套。


七、项目总结

7.1 项目规模

经过5篇的开发,「单词达人」应用最终规模:

指标 数值
页面数 5个
代码文件 6个(1个模型 + 5个页面)
CET-4 词库 ~530词
总词库 ~565词
构建时间 首次 ~23s / 增量 ~3s
HAP 大小 ~500KB

7.2 技术要点回顾

  1. Stage 模型 + ArkTS:鸿蒙原生应用的标准技术栈
  2. 紧凑词库格式:三元组 [word, phonetic, meaning] 节省60%存储空间
  3. @State 不可变性:修改数组内部对象必须创建新引用
  4. AppStorage 持久化:JSON + AppStorage 实现数据持久化
  5. 路由导航@ohos.router 的 pushUrl/back/getParams

7.3 改进方向

如果后续要完善这个应用,可以考虑:

  1. 扩充词库:将其他分类也扩充到完整词库
  2. 语音播放:接入 TTS 实现单词发音
  3. 记忆曲线:基于艾宾浩斯遗忘曲线安排复习
  4. 云端同步:接入华为帐号实现多设备同步
  5. 暗黑模式:增加 dark theme 支持
  6. 单元测试:使用 @ohos/hamock 编写测试用例
    在这里插入图片描述
    在这里插入图片描述

写在最后

五篇博文,从一个空白项目到一个完整的鸿蒙原生应用。回顾整个开发过程,最大的感受是:

  1. ArkTS 严格模式是把双刃剑 — 它让代码更安全,但也让上手门槛更高
  2. 声明式 UI 的开发思维转型 — 从「怎么修改控件」变成「状态变了 UI 自然会变」
  3. 鸿蒙生态在快速成熟 — API 23 已经相当完善,文档和社区资源也在快速增长

希望这系列文章能为正在学习鸿蒙开发的你提供一些帮助。如果有什么问题,欢迎在评论区交流讨论!


系列目录(完)

  • ✅ (一) 项目规划与技术选型
  • ✅ (二) 数据模型与词库构建
  • ✅ (三) 五个页面开发(上):首页、词库、详情
  • ✅ (四) 五个页面开发(下):测验、个人中心
  • ✅ (五) 构建优化与踩坑实录
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐