从零构建鸿蒙NEXT ArkUI应用:每日夸夸神器开发实战

一、开篇:鸿蒙生态与ArkUI宣言在这里插入图片描述

在这里插入图片描述

1.1 鸿蒙NEXT的时代意义

2025年,HarmonyOS NEXT正式面向开发者全面开放,标志着华为彻底摆脱Android底层依赖,拥有了完全自主研发的操作系统内核、编程框架和编译工具链。HarmonyOS NEXT 6.1.1(API 24)作为当前最新的稳定版本,带来了更成熟的ArkUI声明式UI框架、更完善的跨设备协同能力、更强大的安全隐私保护机制,以及统一的全场景生态体验。

对于前端开发者、移动端开发者乃至传统后端开发者而言,鸿蒙NEXT提供了一个全新的应用开发入口。它不仅仅是一个手机操作系统,更是一个面向1+8+N全场景生态的统一开发平台——从手机、平板、穿戴设备到智慧屏、车机、PC,一套代码可以灵活适配多端运行。

本文将通过一个实际的ArkUI应用——“每日夸夸神器”(Daily Praise Tool)的完整开发过程,深入浅出地讲解HarmonyOS NEXT应用开发的核心知识点,包括工程结构、ArkUI声明式语法、状态管理、Ability生命周期、页面路由、样式设计、调试与打包等。无论你是刚接触鸿蒙开发的新手,还是已有Android/iOS开发经验想转型鸿蒙的工程师,这篇文章都将为你提供一份可参照的实战指南。

1.2 每日夸夸神器:应用简介

"每日夸夸神器"是一款极简但温暖的轻量级应用。它的核心功能非常简洁:每次点击按钮,随机生成一句充满正能量的夸奖文案,为用户带来片刻的鼓励与治愈。整个应用的UI包含一个标题、一个展示夸奖文案的卡片、一个"生成新夸奖"按钮和一个"自定义文案"按钮。

虽然功能简单,但这个应用麻雀虽小五脏俱全,它涵盖了ArkUI应用的典型开发流程:

  • 工程创建与项目配置(AppScope、module.json5、build-profile.json5)
  • 声明式UI组件树的构建(Column、Text、Button)
  • 状态驱动渲染(@State装饰器)
  • 事件处理(onClick回调)
  • Ability生命周期管理(UIAbility)
  • 资源文件管理(字符串、颜色、图片)
  • 备份扩展能力(BackupExtensionAbility)
  • 调试与构建

接下来,我们就从零开始,一步步拆解这个应用的每个技术细节。


二、鸿蒙NEXT应用工程结构深度解析

2.1 项目概览:从根目录说起

一个标准的HarmonyOS NEXT工程,其根目录结构如下(以"每日夸夸神器"项目为例):

zikua/                          # 项目根目录
├── AppScope/                   # 应用全局配置
│   ├── app.json5               # 应用级别的配置信息
│   └── resources/              # 应用级资源
├── entry/                      # 模块目录(entry类型模块)
│   ├── src/main/ets/           # ArkTS源代码目录
│   │   ├── entryability/       # Ability层代码
│   │   ├── entrybackupability/ # 备份扩展能力
│   │   └── pages/              # 页面代码
│   ├── src/main/resources/     # 模块级资源
│   ├── build-profile.json5     # 模块构建配置
│   └── oh-package.json5        # 模块级依赖管理
├── hvigor/                     # 构建工具配置
├── build-profile.json5         # 项目级构建配置
├── oh-package.json5            # 项目级依赖管理
└── oh-package-lock.json5       # 依赖锁定文件

鸿蒙的工程结构采用了 “项目(Project)— 模块(Module)” 两层架构。一个项目可以包含多个模块,每个模块可以是一个独立的功能单元。最常见的模块类型是 entry 类型——它是应用的入口模块,也是应用的主HAP包。一个应用至少需要一个 entry 模块。

这种分层结构与Android的"Project-Module"模式类似,但对于从Web/H5转向鸿蒙的开发者来说,需要理解"Ability"这一核心概念——它是鸿蒙应用的基本调度单元,相当于Android中的Activity或iOS中的ViewController,但在设计粒度上更加灵活。

2.2 AppScope:应用全局配置

AppScope目录下存放的是整个应用的全局配置,其中最重要的文件是 app.json5

{
  "app": {
    "bundleName": "com.example.zikua",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "buildVersion": "1",
    "icon": "$media:layered_image",
    "label": "$string:app_name"
  }
}
配置项 说明 示例值
bundleName 应用的唯一标识(包名),遵循反向域名命名规则 com.example.zikua
vendor 应用供应商名称 example
versionCode 内部版本号(递增的整数),用于版本升级判断 1000000
versionName 向用户展示的版本名称 1.0.0
icon 应用图标,引用资源文件中的媒体资源 $media:layered_image
label 应用名称,引用字符串资源,支持国际化 $string:app_name

需要注意的是,鸿蒙NEXT中的资源引用使用 $ 语法,格式为 $资源类型:资源名称,开发者不需要关心资源文件的具体路径,框架会自动按设备类型、语言、分辨率等维度匹配最佳资源。

2.3 module.json5:模块描述文件的秘密

entry/src/main/module.json5 是每个模块的核心配置文件,它的作用类似于Android的AndroidManifest.xml。让我们逐字段解析:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone"],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      }
    ]
  }
}

关键字段解读:

  • type:模块类型。entry 表示应用的主入口模块;除 entry 外还有 feature(功能模块,用于动态特性拆分)和 har(静态共享库,相当于Android的AAR或iOS的Framework)。

  • mainElement:指定应用启动时的默认Ability,这里是 EntryAbility

  • deviceTypes:数组类型,声明该模块支持哪些设备类型。当前项目仅支持 phone。你可以将其扩展为 ["phone", "tablet", "car"] 等来支持多设备。

  • abilities:Ability配置数组。每个Ability可以理解为一个"可调度的功能入口"。EntryAbility 配置中:

    • skills:与Android的IntentFilter类似,声明该Ability能响应哪些系统请求。entity.system.home + ohos.want.action.home 的组合,表示它是桌面启动器(Launcher)启动的入口Ability。
    • exported:是否允许被其他应用调用。入口Ability通常设置为 true
    • startWindowBackground:启动窗口的背景色,用于在Ability初始化完成前展示,避免白屏。
  • extensionAbilities:扩展Ability,用于提供系统级能力的扩展点。例如 type: "backup" 表示该模块支持数据备份与恢复。

2.4 构建配置:build-profile.json5

项目根目录的 build-profile.json5 定义了签名配置与应用编译参数:

{
  "app": {
    "signingConfigs": [],
    "compileSdkVersion": 24,
    "compatibleSdkVersion": 24,
    "products": [
      {
        "name": "default",
        "signingConfig": "default"
      }
    ]
  }
}
  • compileSdkVersion:编译时使用的SDK版本(对应API 24)。
  • compatibleSdkVersion:应用兼容的最低SDK版本。
  • products:产品风味配置,支持不同渠道/环境的差异化构建。

三、Ability:鸿蒙应用的调度核心

3.1 UIAbility生命周期详解

Ability是鸿蒙应用最基本的调度单元。对于带有UI界面的应用,我们使用 UIAbility。每一个UIAbility都会经历从创建到销毁的完整生命周期。

我们的 EntryAbility 代码如下:

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.getApplicationContext()
        .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    } catch (err) {
      hilog.error(DOMAIN, 'testTag',
        'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag',
          'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag',
        'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

生命周期流程图:

用户启动应用
      │
      ▼
  onCreate()          ← Ability对象创建,可在此处做全局初始化
      │
      ▼
  onWindowStageCreate()  ← 窗口创建,加载页面内容
      │
      ▼
  onForeground()      ← Ability进入前台(用户可见)
      │
      ├──→ 用户按Home键
      │        │
      │        ▼
      │    onBackground()   ← Ability进入后台(不可见)
      │        │
      │        ├──→ 系统资源紧张 → onDestroy()
      │        │
      │        └──→ 用户返回应用 → onForeground()
      │
      └──→ 用户退出应用
               │
               ▼
           onWindowStageDestroy()
               │
               ▼
           onDestroy()

各生命周期回调的最佳实践:

  1. onCreate(want, launchParam)

    • want:启动参数对象,包含目标设备、包名、ability名称以及自定义参数(类似Intent的Extra)。
    • launchParam:启动原因(如冷启动、热启动等)。
    • 应用场景:全局数据初始化、数据库连接、SDK初始化。
    • 在我们的应用中,我们在 onCreate 中设置了颜色模式为 COLOR_MODE_NOT_SET(跟随系统),避免强制指定亮色/暗色模式。
  2. onWindowStageCreate(windowStage)

    • 窗口创建后的回调,此时UI可以进行页面加载。
    • windowStage.loadContent 是加载页面的核心API,它接受两个参数:页面路径(相对于 pages 目录的路径)和回调函数。
    • 回调函数中的 err 对象包含 code(错误码)和 message(错误信息)。需要检查 err.code 来判断加载是否成功。鸿蒙中,err.code 为0表示成功,非0表示错误。
  3. onForeground()onBackground()

    • 分别对应Ability进入前台(用户可见可交互)和进入后台(被切换或按Home键)。
    • 应用场景:在前台时启动动画/传感器/定时器,在后台时释放资源以节省电量。

3.2 扩展Ability:BackupExtensionAbility

我们的项目中还有一个 EntryBackupAbility,它继承自 BackupExtensionAbility

import { hilog } from '@kit.PerformanceAnalysisKit';
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';

const DOMAIN = 0x0000;

export default class EntryBackupAbility extends BackupExtensionAbility {
  async onBackup() {
    hilog.info(DOMAIN, 'testTag', 'onBackup ok');
    await Promise.resolve();
  }

  async onRestore(bundleVersion: BundleVersion) {
    hilog.info(DOMAIN, 'testTag',
      'onRestore ok %{public}s', JSON.stringify(bundleVersion));
    await Promise.resolve();
  }
}

BackupExtensionAbility 是鸿蒙NEXT提供的系统级数据备份/恢复扩展能力。当用户在设置中触发应用备份或恢复时,系统会自动调用对应的回调:

  • onBackup():执行备份操作,可以将应用的关键数据(如SharedPreferences、数据库文件、沙箱文件)复制到备份目录。
  • onRestore(bundleVersion):执行恢复操作,bundleVersion 参数携带了备份时的应用版本信息,可用于数据迁移与兼容性处理。

module.json5 中通过 metadata 关联了备份配置描述文件 $profile:backup_config,该文件定义了具体哪些文件或数据库需要被备份。


四、ArkUI声明式UI:构建夸夸神器的界面

4.1 什么是声明式UI

ArkUI是鸿蒙自研的声明式UI框架。所谓"声明式",是指开发者通过声明"UI应该是什么样子的"来构建界面,而不是通过一系列命令式的操作(如 createTextView()setText()addView())来逐步构建。

声明式 vs 命令式的对比:

维度 命令式(传统Android View系统) 声明式(ArkUI / SwiftUI / Jetpack Compose)
状态管理 手动调用 setText()setVisibility() 状态变量自动驱动UI重绘
界面构建 inflate XML + findViewById 纯代码或DSL声明组件树
UI刷新 需要开发者精确控制 框架自动计算出最小刷新范围
可读性 逻辑与UI分离,跳转查找 UI与逻辑在同一作用域,上下文清晰

4.2 页面入口:@Entry与@Component

我们的主页面代码位于 pages/Index.ets。首先来看整体结构:

@Entry
@Component
struct Index {
  // 组件的状态变量和成员方法
  build() {
    // 声明UI组件树的描述
  }
}

装饰器说明:

  • @Entry:标记当前组件是页面的入口组件。只有添加了 @Entry 的组件才会被Ability的 loadContent 加载。一个页面文件中可以有多个 @Component 修饰的组件,但只能有一个 @Entry
  • @Component:将自定义结构体标记为ArkUI组件。被 @Component 修饰的 struct 必须实现 build() 方法,该方法返回组件树的描述。
  • struct:ArkUI使用TypeScript/ArkTS中的 struct 来定义组件,这与传统的class相比更轻量,且天然支持值语义。

4.3 @State:驱动UI的状态变量

@State currentWord: string = "点击下方按钮,接收专属夸奖✨"
@State wordList: string[] = [
  "你真的超级优秀,所有努力都会开花结果!",
  "你的眼光和能力,远超身边大多数人。",
  "保持自信,你值得世间所有美好。",
  "今天的你依旧闪闪发光,继续向前走吧!",
  "不用和别人对比,做自己就足够耀眼。",
  "头脑灵活执行力强,没有你办不成的小事。",
  "温柔又强大,你真的太棒了!"
]

@State 是ArkUI中最基础的状态装饰器。它的核心工作原理是:

  1. 监听:框架会追踪 @State 修饰的变量。
  2. 关联:当组件的 build() 方法中使用了该状态变量时,框架自动建立依赖关系。
  3. 触发:当状态变量的值发生变化时,框架自动重新执行 build() 方法,只更新受影响的部分,而非全量刷新。
  4. 最小化更新:ArkUI采用脏检查机制,只对UI树中与变化状态相关的节点进行局部更新,性能优异。

在我们的场景中:

  • currentWord:当前展示的夸奖文案,初始值为引导文案。每次点击按钮更新它,Text组件自动更新显示。
  • wordList:夸奖文案列表,是一个字符串数组。它作为数据源,不会被修改,但仍然用 @State 标记(当然,对于纯数据源也可以用普通变量,但 @State 可以保证未来如果动态增删条目时UI自动响应)。

@State的初始化规则:

// 方式一:在声明时直接初始化(推荐)
@State count: number = 0

// 方式二:在struct的构造函数中初始化
@State count: number
constructor(count: number) {
  this.count = count;
}

// 注意:@State不支持复杂对象的深拷贝监听,
// 对于深层次嵌套对象,需要配合 @Observed 和 @ObjectLink 使用

4.4 build():声明组件树

build() 是ArkUI组件的核心方法,它必须返回一个合法的组件树描述。我们的代码:

build() {
  Column() {
    // 标题
    Text("每日夸夸神器")
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      .margin({ top: 40, bottom: 40 })

    // 夸奖文本卡片
    Text(this.currentWord)
      .fontSize(20)
      .textAlign(TextAlign.Center)
      .padding(30)
      .width("90%")
      .backgroundColor("#FFF0F5")
      .borderRadius(20)
      .margin({ bottom: 40 })

    // 生成按钮
    Button("生成新夸奖")
      .width(180)
      .height(50)
      .fontSize(18)
      .backgroundColor("#FF69B4")
      .fontColor("#FFFFFF")
      .onClick(() => {
        let randomIndex = Math.floor(Math.random() * this.wordList.length)
        this.currentWord = this.wordList[randomIndex]
      })

    Button("自定义文案")
      .width(180)
      .height(50)
      .fontSize(18)
      .margin({ top: 20 })
      .backgroundColor("#87CEFA")
  }
  .width("100%")
  .height("100%")
  .backgroundColor("#ffffff")
  .padding(20)
}
4.4.1 Column:垂直线性布局

Column 是ArkUI最基本的布局容器之一,它将子组件沿垂直方向依次排列,类似于CSS Flexbox中 flex-direction: column

Column的关键属性:

Column() {
  // 子组件按顺序垂直排列
}
.width("100%")    // 宽度撑满父容器
.height("100%")   // 高度撑满父容器
.backgroundColor("#ffffff")  // 背景色:纯白
.padding(20)      // 内边距:20vp(视觉像素单位)

ArkUI中的布局单位是 vp(virtual pixel,虚拟像素),它是一种与设备密度无关的单位,类似于Android中的dp或iOS中的pt。1vp在不同设备上对应的物理像素数不同,但视觉大小保持基本一致。此外还有 lpx(逻辑像素),通常用于响应式设计。

4.4.2 Text:文本组件

Text 组件用于展示文本内容。我们的代码中使用了两种样式的Text:

标题文本:

Text("每日夸夸神器")
  .fontSize(30)              // 字号:30vp
  .fontWeight(FontWeight.Bold) // 字重:粗体
  .margin({ top: 40, bottom: 40 }) // 上下外边距各40vp

内容文本(带状态绑定):

Text(this.currentWord)       // 绑定状态变量
  .fontSize(20)
  .textAlign(TextAlign.Center) // 文本居中
  .padding(30)               // 内边距30vp
  .width("90%")              // 宽度为父容器90%
  .backgroundColor("#FFF0F5") // 背景色:lavender blush(淡紫红)
  .borderRadius(20)          // 圆角半径20vp
  .margin({ bottom: 40 })    // 底部外边距40vp

Text组件的常用属性一览:

属性API 作用 示例
.fontSize(value) 设置字号,支持number(vp)和string(百分比) .fontSize(20)
.fontWeight(value) 设置字重,支持 FontWeight 枚举或number .fontWeight(FontWeight.Bold)
.fontColor(value) 设置字体颜色 .fontColor(Color.Blue).fontColor("#FF0000")
.textAlign(value) 设置文本对齐方式 .textAlign(TextAlign.Center)
.lineHeight(value) 设置行高 .lineHeight(30)
.textOverflow(value) 文本超出时的处理方式 .textOverflow({overflow: TextOverflow.Ellipsis})
.maxLines(value) 最大行数 .maxLines(2)
.decoration(value) 文字装饰(下划线/删除线) .decoration({type: TextDecorationType.Underline})
4.4.3 Button:按钮组件

Button组件用于触发交互操作。我们的页面中有两个按钮:

主按钮——生成新夸奖:

Button("生成新夸奖")
  .width(180)                  // 宽度180vp
  .height(50)                  // 高度50vp
  .fontSize(18)                // 按钮文字字号
  .backgroundColor("#FF69B4")  // 热粉色背景
  .fontColor("#FFFFFF")        // 白色文字
  .onClick(() => {             // 点击回调
    let randomIndex = Math.floor(Math.random() * this.wordList.length)
    this.currentWord = this.wordList[randomIndex]
  })

次按钮——自定义文案:

Button("自定义文案")
  .width(180)
  .height(50)
  .fontSize(18)
  .margin({ top: 20 })         // 与上方按钮的间隔
  .backgroundColor("#87CEFA")  // 浅天蓝色背景

Button的不同创建形式:

ArkUI的Button支持多种创建方式:

// 形式一:带文本标签(最常用)
Button("点击我")
  .onClick(() => { /* ... */ })

// 形式二:无参数,通过子组件自定义内容
Button() {
  Image($r("app.media.icon"))
    .width(24)
    .height(24)
  Text("带图标的按钮")
}
  .onClick(() => { /* ... */ })

// 形式三:带样式参数
Button({ type: ButtonType.Capsule, stateEffect: true }) {
  Text("胶囊按钮")
}

Button类型(ButtonType):

  • ButtonType.Capsule:胶囊形状(两端半圆)
  • ButtonType.Circle:圆形
  • ButtonType.Normal:默认直角矩形

点击事件处理:onClick与手势系统

onClick 是最基础的事件监听器。但在ArkUI中,交互事件远不止于点击。ArkUI提供了丰富的手势系统:

// 基础点击
Button("点击").onClick(() => { })

// 长按
Button("长按").onLongPress(() => { })

// 拖拽
Column()
  .gesture(
    PanGesture()
      .onActionStart(() => { })
      .onActionUpdate((event) => { })
      .onActionEnd(() => { })
  )

// 组合手势(同时识别点击和长按)
Column()
  .gesture(
    GestureGroup(GestureMode.Exclusive,
      TapGesture().onAction(() => { }),
      LongPressGesture().onAction(() => { })
    )
  )

4.5 随机算法:简洁但完整的逻辑实现

生成新夸奖的核心逻辑只有一行:

let randomIndex = Math.floor(Math.random() * this.wordList.length)
this.currentWord = this.wordList[randomIndex]

逐层拆解:

  1. Math.random():返回 [0, 1) 范围内的伪随机浮点数。
  2. Math.random() * this.wordList.length:将随机数映射到 [0, wordList.length) 范围。
  3. Math.floor(...):向下取整,得到 [0, wordList.length - 1] 范围内的整数。
  4. this.wordList[randomIndex]:从数组中取出对应索引的文案。
  5. this.currentWord = ...:赋值给状态变量,触发UI刷新。

避免重复的扩展思路:

当前实现每次独立随机,可能出现连续两次显示相同文案的情况。如果需要避免重复,可以这样改进:

@State lastIndex: number = -1

generatePraise(): void {
  let newIndex: number
  do {
    newIndex = Math.floor(Math.random() * this.wordList.length)
  } while (newIndex === this.lastIndex && this.wordList.length > 1)
  
  this.lastIndex = newIndex
  this.currentWord = this.wordList[newIndex]
}

五、ArkUI中的状态管理进阶

5.1 状态装饰器家族

@State 只是ArkUI状态管理体系中的冰山一角。ArkUI提供了多层次的状态管理方案,来应对从简单到复杂的各种场景:

装饰器 作用域 用途
@State 组件内部 组件内部状态,变化触发本组件刷新
@Prop 父→子(单向) 父组件传递给子组件的状态,子组件可以修改但不会影响父组件
@Link 父↔子(双向) 父组件与子组件共享状态,任何一方的修改都会同步
@Provide / @Consume 祖先↔后代 跨层级组件间的状态共享(无需逐层传递)
@Observed / @ObjectLink 复杂对象 深度监听嵌套对象的属性变化
@StorageProp / @StorageLink App全局 跨Ability/跨页面的全局状态存储
LocalStorage / AppStorage 页面级/应用级 独立于组件树之外的存储实例

5.2 父子组件通信示范

假设我们要将"夸夸神器"中的文案列表抽取成独立的子组件,展示父子组件之间的状态传递:

父组件(Index.ets):

@Entry
@Component
struct Index {
  @State currentWord: string = "点击下方按钮,接收专属夸奖✨"
  @State wordList: string[] = [
    "你真的超级优秀,所有努力都会开花结果!",
    "你的眼光和能力,远超身边大多数人。",
    "保持自信,你值得世间所有美好。",
    "今天的你依旧闪闪发光,继续向前走吧!",
    "不用和别人对比,做自己就足够耀眼。",
    "头脑灵活执行力强,没有你办不成的小事。",
    "温柔又强大,你真的太棒了!"
  ]
  @State wordCount: number = 7

  build() {
    Column() {
      // 将currentWord以@Prop方式传递给子组件
      PraiseCard({ message: this.currentWord })

      // 将wordCount以@Link方式共享
      PraiseCounter({ count: this.wordCount })

      Button("新增一条夸奖")
        .onClick(() => {
          this.wordList.push("新加的一句夸奖")
          this.wordCount = this.wordList.length
        })
    }
  }
}

子组件——PraiseCard(单向@Prop):

@Component
struct PraiseCard {
  @Prop message: string  // 父组件传入,只读但可本地修改

  build() {
    Text(this.message)
      .fontSize(20)
      .padding(20)
      .backgroundColor("#FFF0F5")
      .borderRadius(16)
  }
}

子组件——PraiseCounter(双向@Link):

@Component
struct PraiseCounter {
  @Link count: number  // 与父组件的wordCount双向同步

  build() {
    Text(`当前共有 ${this.count} 条夸奖文案`)
      .onClick(() => {
        this.count++  // 修改会同步回父组件
      })
  }
}

@Prop 与 @Link 的核心区别:

  • @Prop 是单向数据流:父组件修改 → 子组件刷新;子组件修改自己的副本 → 不会传回父组件。
  • @Link 是双向同步:父子任意一方修改值,双方都会刷新。

5.3 @Provide / @Consume:跨层级通信

当组件层级很深时(如祖父→父→子→孙),逐层通过 @Prop 传递会非常繁琐。此时可以使用 @Provide / @Consume

// 祖先组件
@Component
struct GrandParent {
  @Provide("themeColor") themeColor: string = "#FF69B4"

  build() {
    Column() {
      ParentComponent()
    }
  }
}

@Component
struct ParentComponent {
  build() {
    ChildComponent()
  }
}

// 孙组件(无需ParentComponent中转)
@Component
struct ChildComponent {
  @Consume("themeColor") themeColor: string

  build() {
    Button("夸夸我")
      .backgroundColor(this.themeColor)
  }
}

@Provide 会将变量注入到组件子树中,@Consume 可以在任意后代组件中直接获取该变量,无需逐层传递。这是ArkUI实现"依赖注入"的方式。

5.4 AppStorage:应用级全局状态

如果需要在不同Ability或不同模块之间共享状态,可以使用 AppStorage

// 初始化全局状态
AppStorage.SetOrCreate("praiseCount", 0)

// 在任意组件中绑定
@StorageLink("praiseCount") praiseCount: number = 0

// 使用时
Button(`已生成 ${this.praiseCount} 次夸奖`)
  .onClick(() => {
    this.praiseCount++  // 自动同步到所有绑定了该键的组件
  })

六、资源管理与国际化

6.1 资源文件组织

鸿蒙的资源管理遵循限定词目录机制,支持按语言、屏幕密度、设备类型等维度组织资源文件。典型的资源目录结构如下:

resources/
├── base/                  # 基础资源(所有情况通用)
│   ├── element/           # 基础元素(字符串、颜色、数值等)
│   │   ├── string.json    # 字符串定义
│   │   ├── color.json     # 颜色定义
│   │   └── float.json     # 浮点数定义
│   ├── media/             # 媒体文件(图片、音频等)
│   │   ├── startIcon.png
│   │   └── layered_image.png
│   └── profile/           # 配置文件(如备份配置、页面路由配置)
│       ├── main_pages.json
│       └── backup_config.json
├── en_US/                 # 美国英语限定词目录(当系统语言为美式英语时生效)
│   └── element/
│       └── string.json
├── zh_CN/                 # 中文简体限定词目录
│   └── element/
│       └── string.json
├── ldpi/                  # 低密度屏幕(~120dpi)
│   └── media/
└── dark/                  # 深色模式限定词目录
    └── element/
        └── color.json

限定词的匹配优先级: 系统会根据当前设备的语言、区域、屏幕密度、字体大小、深色模式等维度,从最精确匹配的限定词目录中加载资源。

6.2 字符串资源与国际化

resources/base/element/string.json 中定义字符串:

{
  "string": [
    {
      "name": "app_name",
      "value": "每日夸夸神器"
    },
    {
      "name": "module_desc",
      "value": "每日夸夸神器模块"
    },
    {
      "name": "EntryAbility_desc",
      "value": "应用主入口"
    },
    {
      "name": "EntryAbility_label",
      "value": "夸夸神器"
    },
    {
      "name": "praise_title",
      "value": "每日夸夸神器"
    }
  ]
}

在英文资源 resources/en_US/element/string.json 中提供对应翻译:

{
  "string": [
    {
      "name": "app_name",
      "value": "Daily Praise Tool"
    },
    {
      "name": "praise_title",
      "value": "Daily Praise Tool"
    }
  ]
}

在代码中引用资源的方式:

// 方式一:通过 $r 引用(推荐,支持自动适配)
Text($r("app.string.praise_title"))
  .fontSize(30)

// 方式二:硬编码(不利于国际化,不推荐)
Text("每日夸夸神器")

使用 $r("app.string.xxx") 引用字符串资源的好处是:当系统语言切换时,文本会自动切换到对应语言的翻译,无需修改代码。

6.3 颜色与样式资源

颜色也可以抽取到资源文件中,方便统一管理主题色:

// resources/base/element/color.json
{
  "color": [
    {
      "name": "primary_pink",
      "value": "#FF69B4"
    },
    {
      "name": "secondary_blue",
      "value": "#87CEFA"
    },
    {
      "name": "card_bg",
      "value": "#FFF0F5"
    },
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    }
  ]
}

深色模式下的颜色资源(resources/dark/element/color.json):

{
  "color": [
    {
      "name": "card_bg",
      "value": "#2D2D2D"    // 深色模式下卡片背景变为深灰
    },
    {
      "name": "start_window_background",
      "value": "#1A1A2E"    // 深色模式启动窗口背景
    }
  ]
}

在代码中引用颜色:

Text("每日夸夸神器")
  .backgroundColor($r("app.color.card_bg"))
  // 如果当前设备处于深色模式,自动使用 dark 限定词目录下的颜色值

6.4 图片资源管理

图片资源统一放在 resources/base/media/ 目录下,支持PNG、JPG、SVG、WebP等格式。引用方式:

// 在代码中引用
Image($r("app.media.startIcon"))
  .width(48)
  .height(48)

// 在配置文件中引用
// module.json5 中的 startWindowIcon: "$media:startIcon"

鸿蒙还提供了**分层图标(Layered Image)**机制,允许将图标拆分为前景和背景两层,在主题切换时自动适配颜色。


七、调试、测试与构建

7.1 hilog:结构化日志系统

在代码中我们使用了 hilog 进行日志输出:

import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0000;

// 日志输出
hilog.info(DOMAIN, 'testTag', 'Ability onCreate');
hilog.error(DOMAIN, 'testTag', 'Failed: %{public}s', JSON.stringify(err));

hilog的日志级别:

方法 级别 使用场景
hilog.debug() DEBUG 调试信息,发布版本默认不输出
hilog.info() INFO 关键流程信息
hilog.warn() WARN 警告信息,不影响主流程但值得关注
hilog.error() ERROR 错误信息,需要开发者定位处理
hilog.fatal() FATAL 致命错误,会导致应用崩溃

格式化参数:

  • %{public}s:公开字符串(在日志中明文显示)。
  • %{private}s:私有字符串(在正式日志中被脱敏处理为 <private>)。

在开发调试阶段,建议关键信息使用 %{public}s 以方便定位问题;涉及用户隐私的数据(如手机号、账号)务必使用 %{private}s

7.2 单元测试

鸿蒙NEXT推荐使用 @ohos/hypium@ohos/hamock 进行单元测试。测试文件通常放在 src/test/ 目录下:

// List.test.ets
import { describe, it, expect } from '@ohos/hypium';

describe('praiseTest', () => {
  it('should_return_different_word_when_multiple_clicks', 0, () => {
    const wordList = [
      "你真的超级优秀",
      "你的眼光和能力",
      "保持自信"
    ];
    
    const results = new Set<string>();
    for (let i = 0; i < 100; i++) {
      const index = Math.floor(Math.random() * wordList.length);
      results.add(wordList[index]);
    }
    
    // 验证100次随机中,至少出现了2种不同的结果
    expect(results.size).assertGreaterThan(1);
  });
  
  it('should_return_valid_index', 0, () => {
    const wordList = ["a", "b", "c"];
    for (let i = 0; i < 1000; i++) {
      const index = Math.floor(Math.random() * wordList.length);
      // 索引始终在有效范围内
      expect(index).assertLessThan(wordList.length);
      expect(index).assertGreaterThan(-1);
    }
  });
});

LocalUnit.test.ets 文件则展示了本地单元测试的基本结构,可以配合 @ohos/hamock 进行Mock测试。

7.3 应用构建与HAP打包

鸿蒙应用的编译产物是 HAP(Harmony Ability Package) 包。构建过程通过 hvigor 构建工具完成:

# 构建Debug包(用于调试)
hvigor assembleHap

# 构建Release包(用于发布)
hvigor assembleHap --mode release

# 构建App Pack(用于上架应用市场)
hvigor assembleApp --mode release

构建产物通常位于各个模块的 build/default/outputs/ 目录下。Release包需使用发布证书签名,签名配置在 build-profile.json5signingConfigs 中指定。


八、鸿蒙NEXT开发常见问题与避坑指南

8.1 资源引用格式

在代码和配置文件中,资源引用的格式略有不同,初学者容易混淆:

位置 格式 示例
eTS代码 $r("app.type.name") $r("app.string.app_name")
JSON配置文件 $type:name $string:app_name
媒体文件 $media:name(配置) / $r("app.media.name")(代码) $media:startIcon

8.2 类型安全:ArkTS与TypeScript的区别

ArkTS是鸿蒙对TypeScript的定制版本,它做了更严格的类型约束来优化运行时性能:

// TypeScript 允许(ArkTS 不允许)
let data: any = getData()
data.someMethod()

// ArkTS 要求显式类型
let data: Record<string, Object> = getData()

// TypeScript 允许(ArkTS 不允许)
function fn(a: number, b?: number) { }
fn(1)  // 可选参数不加也可以

// ArkTS 要求严格匹配参数
function fn(a: number, b?: number) { }
fn(1)       // 必须传递已声明的所有必选参数

ArkTS的关键限制:

  • 不允许使用 any 类型,必须使用明确类型或 ObjectRecord<string, Object>
  • 不允许使用 as any 类型断言。
  • 不允许使用 ! 非空断言操作符。
  • 不允许在运行时动态修改对象的原型链。
  • 不允许使用 evalFunction 构造函数等动态执行代码。

8.3 布局常见问题

子组件溢出:ColumnRow 的宽度/高度固定,子组件尺寸总和超出时,默认情况下子组件会被压缩或溢出。可以使用 .clip(true) 裁剪超出部分或 .layoutWeight(1) 按比例分配空间。

文本换行: Text组件默认会自动换行,但如果设置 .maxLines() 并配合 .textOverflow({overflow: TextOverflow.Ellipsis}) 可以实现超出省略号效果。

位置偏移: 使用 .offset({x: 10, y: 20}) 可以进行相对位置偏移,而不是通过 marginpadding

8.4 性能优化建议

  1. 避免在build()中执行耗时操作build() 方法可能会被频繁调用,应避免在其中执行计算密集型任务或异步操作。
  2. 合理使用状态变量:不要把所有变量都声明为 @State,只有直接影响UI的变量才需要状态装饰器。
  3. 懒加载列表:当需要展示大量数据时,使用 LazyForEach 代替 ForEach 实现按需渲染。
  4. 图片缓存:对网络图片使用 Image 组件的 .syncLoad(true) 或配合 ImageCache 策略。

九、未来扩展:从单页到全场景

当前的"每日夸夸神器"虽然功能简单,但它拥有巨大的扩展空间。以下是一些值得尝试的进阶方向:

9.1 路由与多页面

引入 router 模块实现页面跳转:

import { router } from '@kit.ArkUI';

// 跳转到自定义文案页面
Button("自定义文案")
  .onClick(() => {
    router.pushUrl({
      url: 'pages/CustomPraise',
      params: { currentList: this.wordList }
    });
  })

9.2 数据持久化

使用 Preferences 保存用户自定义的夸奖文案:

import { preferences } from '@kit.ArkData';

async function savePraiseList(context: Context, list: string[]) {
  const store = await preferences.getPreferences(context, 'praise_store');
  await store.put('praise_list', JSON.stringify(list));
  await store.flush();
}

async function loadPraiseList(context: Context): Promise<string[]> {
  const store = await preferences.getPreferences(context, 'praise_store');
  const json = await store.get('praise_list', '[]');
  return JSON.parse(json.toString());
}

9.3 通知与卡片

通过 @ohos.backgroundTaskManager 实现定时推送每日一句夸奖到通知栏,或开发ArkTS卡片(Form)让用户在桌面直接看到当日的夸奖文案,无需打开应用。

9.4 跨设备流转

利用鸿蒙的分布式能力,将夸奖文案跨端流转到手表或智慧屏上。例如,用户可以在手机上收到每日夸奖,同样在手表上同步展示:

import { distributedObject } from '@kit.DistributedServiceKit';

const object = distributedObject.genCreateObject('praise_sync', {
  word: ''
});

// 更新后自动同步到所有登录同账号的设备
this.currentWord = "你真棒!";
object.word = this.currentWord;

十、总结与展望

通过"每日夸夸神器"这个完整应用,我们系统地走过了HarmonyOS NEXT 6.1.1(API 24)应用开发的全流程:

  1. 工程结构 —— 理解了项目、模块、Ability的层级关系,以及 app.json5module.json5build-profile.json5 等关键配置文件的含义。
  2. UIAbility生命周期 —— 掌握了 onCreateonWindowStageCreateonForegroundonBackgroundonWindowStageDestroyonDestroy 的完整链路,以及各阶段的最佳实践。
  3. ArkUI声明式UI —— 学会了 @Entry@Component@State 装饰器的使用,以及 ColumnTextButton 等基础组件的组合与样式设置。
  4. 状态管理 —— 掌握了 @State 驱动UI刷新的原理,并了解了 @Prop@Link@Provide/@ConsumeAppStorage 等进阶状态管理方案。
  5. 事件处理 —— 掌握了 onClick 交互以及手势系统的扩展能力。
  6. 资源管理 —— 理解了限定词目录机制和国际化实现方式。
  7. 调试与测试 —— 了解了 hilog 日志系统和 hypium 测试框架的基本用法。

鸿蒙NEXT作为新一代的全场景智慧生活操作系统,其声明式UI框架ArkUI在开发效率、运行性能和多端适配方面展现出了强大的能力。从一个人的夸夸神器,到全家人共享的温暖应用,甚至是跨设备流转的分布式体验,鸿蒙生态为开发者提供了无限可能。

最后,送大家一句话:在代码的世界里,每次点击"生成新夸奖",都是一次对生活的温柔致敬。保持好奇,保持温暖,你的下一行代码,也许就是点亮他人的那道光。


参考资料与延伸阅读:

  • 本文所有代码基于 HarmonyOS NEXT 6.1.1 SDK(API 24)
  • ArkUI组件完整API参考
  • ArkTS编程规范与最佳实践
  • 鸿蒙Ability开发指南
Logo

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

更多推荐