目录(建议直接用 CSDN 目录功能生成)


一、引言:打开 DevEco 后,你真正要搞懂的三件事

很多人学鸿蒙的第一天,会同时撞上三堵墙:

  1. 语言:文件后缀是 .ets,看起来像 TypeScript,一用 anyvar、对象动态加字段就编译失败。

  2. 工程:DevEco Studio 新建 Empty Ability 后,左边一堆 AppScopeentryoh_moduleshvigorfile.ts,不知道改哪个文件界面才会变。

  3. 运行:改了 Index.ets 没反应,其实页面没登记进 main_pages.json;或者字符串写死在代码里,多语言/深色模式全废。

这三件事其实是一条链:

ArkTS 负责把逻辑写对,ArkUI 负责把界面画出来,工程目录和 json5 负责告诉系统「谁是入口、有哪些页、用哪些资源」。

本文按「语言 → 类型 → 目录 → 启动链路 → 完整案例」一次讲完。读完你应该能:看懂 Empty Ability 工程里每一个默认文件的职责,用 ArkTS 写出带类型的业务代码,并跑通一个任务清单小应用。


二、问题根源:ArkTS 不是「能跑的 TypeScript」

ArkTS 是 HarmonyOS 应用开发的官方语言。它基于 TypeScript 的语法风格,但为了编译期静态检查和运行时性能,砍掉了一批 TS 里过于动态的能力。

JavaScript

TypeScript

ArkTS

类型

运行时才知道

可选静态类型,可关严格检查

强制静态类型

any / unknown

无所谓

常用逃生舱

禁止

var

支持

支持

禁止,用 let/const

运行时改对象结构

随便加字段

严格模式下会报,绕得过

禁止,对象布局编译期钉死

结构化类型(鸭子类型)

本质就是

支持

不支持,必须靠 class / interface / 继承

UI

配套 ArkUI 声明式语法(.ets

文件

.js

.ts

.ets(UI + 逻辑);纯逻辑也可 .ts 但工程以 .ets 为主

官方原话可以概括成三条铁律:

  1. 强制静态类型:变量类型在编译前就必须确定,减少运行时类型检查,换性能和稳健性。

  2. 禁止运行时改变对象布局:不能 delete 字段、不能给实例动态加属性、不能靠 as any 绕过。

  3. 限制部分运算符语义:例如一元 + 只能作用在数字上。

ArkTS 仍然能和 TS/JS 生态互操作(OHPM 三方库),但你自己写的业务代码要按 ArkTS 约束来。把 TS 项目原样粘进来,大概率过不了编译。

官方入口:


三、ArkTS 基本语法

3.1 声明、作用域与模块
// 变量:块级作用域,必须能推断或显式标注类型
let count: number = 0;
let title = '任务清单'; // 推断为 string

// 常量:绑定后不能再赋值(对象内部字段仍可变)
const MAX_TASK = 100;
const appName: string = 'TodoDemo';

// 不支持:
// var x = 1;

模块用 ES Module:

// model/Task.ets
export enum TaskStatus {
  Todo = 0,
  Doing = 1,
  Done = 2
}

export interface Task {
  id: string;
  title: string;
  status: TaskStatus;
  priority?: Priority; // 可选属性
}

export type Priority = 'low' | 'normal' | 'high';

// pages/Index.ets
import { Task, TaskStatus } from '../model/Task';

import / export 是工程里组织代码的唯一正道。不要依赖「一个巨大的 Index.ets 包打天下」。

3.2 运算符、语句、函数与类

控制流与 TS 几乎一样:if / switch / for / while / try-catch、三元表达式、模板字符串。

function sum(a: number, b: number): number {
  return a + b;
}

// 箭头函数(推荐;ArkTS 不支持 function 表达式)
const double = (n: number): number => n * 2;

function log(msg: string): void {
  console.info(`[Todo] ${msg}`);
}

class TaskService {
  private tasks: Task[] = [];

  add(title: string): Task {
    const item: Task = {
      id: `${Date.now()}`,
      title: title,
      status: TaskStatus.Todo
    };
    this.tasks.push(item);
    return item;
  }

  list(): Task[] {
    return this.tasks;
  }
}

几个必须记住的写法:

场景

正确

错误

变量

let x = 1

var x = 1

私有字段

private name: string = ''

#name = ''

类型断言

shape as Circle

<Circle>shape

匿名函数

(x: number) => x + 1

function (x: number) { ... }(函数表达式)

访问对象字段

user.name

user['name'](动态索引访问对象字段不支持)

访问数组元素

arr[0]

用字符串当对象键去「当 map 用」

类字段必须在类体里声明,不能只在 constructor 参数里偷偷声明字段(TS 的参数属性写法要改成显式字段)。

class Point {
  public x: number = 0;
  public y: number = 0;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

3.3 从 TS 迁过来最容易踩的约束

把官方适配规则压成一张「会不会过编译」的表:

约束

含义

怎么改

禁止 any / unknown

类型必须具体

定义 interface / class / 联合类型

禁止 var

只有块级作用域

let / const

禁止改对象布局

不能动态加/删字段

字段写进 class/interface

不支持 structural typing

「长得像」不等于同一类型

显式 implements / 继承

不支持条件类型、infer

没有 T extends U ? X : Y

拆成明确的类型别名或重载

不支持交叉类型 A & B

不能用 intersection

用继承或合并成新 interface

不支持 this 类型

链式调用要写返回类型

写成具体 class 名

不支持 Symbol()

运行时唯一键没有意义

用字面量字段名

对象字面量要有明确类型

不能丢给 Object 就完事

let p: Point = { x: 1, y: 2 }

泛型有时要显式实参

不能只靠返回值推断

choose<number>(1, 2)

完整清单以官方为准:从 TypeScript 到 ArkTS 的适配规则


四、ArkTS 支持的类型(全表 + 例子)

4.1 基本类型

类型

含义

例子

number

IEEE 754 双精度,整数和小数都用它

let score: number = 99.5;

string

字符串,无独立 char

let name: string = 'ArkTS';

boolean

真假

let done: boolean = false;

bigint

超过 2^53-1 的整数,后缀 n

let big: bigint = 9007199254740993n;

null

空值

let n: string | null = null;

undefined

未定义

let u: number | undefined = undefined;

void

函数无返回值

function f(): void {}

number 对应 Java 里 byte/short/int/long/float/double 一整串。它不是 Java 的 int。超过安全整数用 bigint,否则会丢精度。

let age: number = 20;
let pi: number = 3.14159;
let hello: string = '鸿蒙';
let tip: string = `你好,${hello}`;
let ok: boolean = true;
let huge: bigint = 10n ** 20n;

4.2 联合、字面量、可选、枚举、别名
type Priority = 'low' | 'normal' | 'high'; // 字符串字面量联合
type Id = string | number;                 // 联合类型

enum TaskStatus {
  Todo,
  Doing,
  Done
}

interface User {
  id: string;
  name: string;
  vip?: boolean; // 可选
}

type Result = TaskStatus; // 类型别名

联合类型是日常主力:网络请求「成功数据 | 错误信息」、UI「加载中 | 空 | 列表」都靠它,而不是 any

4.3 数组、元组、对象、Map / Set
let nums: number[] = [1, 2, 3];
let names: Array<string> = ['a', 'b'];
nums.push(4); // 动态数组,不会像 Java 那样定长

let pair: [string, number] = ['count', 3]; // 元组

let map: Map<string, number> = new Map();
map.set('todo', 2);

let set: Set<string> = new Set(['a', 'b']);

数组下标越界得到 undefined,不会抛 Java 那种越界异常。业务里要自己判断长度。

对象用 interface / class 描述形状,不要写「匿名对象类型当类型声明」。

interface Point {
  x: number;
  y: number;
}

let p: Point = { x: 1, y: 2 };

4.4 函数类型与泛型
type Mapper = (n: number) => string;

function identity<T>(value: T): T {
  return value;
}

let a: number = identity<number>(1);
let b: string = identity<string>('ok');

4.5 明确不支持、不要再写的类型

不要写

原因

替代

any

静态类型被掏空

具体 interface / 联合

unknown

同上

联合 + 收窄

A & B

不支持交叉类型

新 interface 或继承

T extends U ? X : Y

不支持条件类型

多个类型别名

obj['key'] 动态取对象字段

布局必须静态可知

obj.key

对象类型里的 index signature

{ [k: string]: number }

Map<string, number>

对照 Java 程序员的速查(官方也有专文):Java 程序员的 ArkTS 入门


五、ArkUI 声明式语法:语言之上的 UI 层

.ets 里除了 ArkTS 语言,还有一套 ArkUI 声明式 UI。这是鸿蒙页面开发的正文,不是「另外一门语言」,但装饰器和组件 API 是额外一层。

一个页面最小骨架:

@Entry          // 表示这是页面入口组件(要登记到 main_pages.json)
@Component      // 自定义组件
struct Index {
  @State message: string = 'Hello World';

  build() {
    Column() {
      Text(this.message)
        .fontSize(24)
      Button('点我')
        .onClick(() => {
          this.message = 'Hi ArkTS';
        })
    }
    .width('100%')
    .height('100%')
  }
}

常用装饰器:

装饰器

作用

@Entry

页面入口,一个文件通常一个

@Component

自定义组件

@Builder

轻量 UI 片段(可复用的 build 块)

@State

组件自己的状态,改了会刷新 UI

@Prop

父 → 子单向同步

@Link

父子双向同步

@Provide / @Consume

跨层级依赖注入式状态

@Watch

某状态变化时回调

@BuilderParam

把 UI 片段当参数传给子组件

数据流可以记成:

@State(源头)
   │  传给子组件
   ├─ @Prop   子能读,改了不同步回父
   └─ @Link   子改了,父也会变

组件链式调用是 ArkUI 的风格:Text('hi').fontSize(16).fontColor(Color.Black)。布局优先用 Column / Row / Flex / Stack / List + ForEach

页面跳转:旧模板常用 @ohos.router当前官方推荐 Navigation + NavPathStack组件导航(Navigation)。入门阶段先把单页状态跑通,再上 Navigation。


六、DevEco Studio 工程目录:每个文件现在都在干什么

新建 Application → Empty Ability(Stage) 后,工程大致长这样(不同版本可能多 entrybackupabilitycode-linter.json5 等,职责不变):

HelloWorld/
├── AppScope/                      应用全局:包名、图标、版本
│   ├── app.json5
│   └── resources/base/...
├── entry/                         主模块,编译成 HAP
│   ├── src/main/
│   │   ├── ets/                   ArkTS / ArkUI 源码
│   │   │   ├── entryability/      UIAbility 生命周期
│   │   │   ├── entrybackupability/备份恢复扩展
│   │   │   └── pages/             页面
│   │   ├── resources/             模块资源
│   │   └── module.json5           模块身份证
│   ├── src/ohosTest/              设备上的仪器测试
│   ├── src/test/                  本地单元测试
│   ├── src/mock/                  Mock
│   ├── build-profile.json5        模块编译选项、产物 target
│   ├── hvigorfile.ts              模块级构建脚本
│   ├── oh-package.json5           模块依赖
│   └── obfuscation-rules.txt      Release 混淆规则
├── hvigor/                        构建工具配置(类似 Gradle Wrapper)
├── oh_modules/                    三方库下载目录(类似 node_modules)
├── build-profile.json5            工程级:签名、产品、SDK API
├── hvigorfile.ts                  工程级构建入口
├── oh-package.json5               工程级依赖 / overrides
├── oh-package-lock.json5          锁版本
├── local.properties               本机 SDK 路径(不要当业务配置改)
├── code-linter.json5              代码检查规则
└── .idea / .hvigor / build        IDE 与编译缓存,不要手改

6.1 工程级:打开工程第一眼看到的那些

路径

干什么

你什么时候改

AppScope/

整个 App 的全局配置和全局资源

改包名、应用名、图标、版本

entry/

主模块源码

日常开发主战场

oh_modules/

OHPM 装下来的依赖

不手改,等价 node_modules

hvigor/

Hvigor 构建系统

一般不动

build-profile.json5

签名、product、compileSdkVersion、compatibleSdkVersion

换 API 版本、配签名、多产品风味

hvigorfile.ts

工程构建任务

自定义构建时才动

oh-package.json5

依赖、overrides、parameterFile

加三方库

oh-package-lock.json5

锁定依赖树

提交仓库,不手改

local.properties

本机 HarmonyOS SDK 路径

换电脑会变,通常不入库或按团队规范

code-linter.json5

ESLint/ArkTS 检查规则

统一团队规范时

.hvigor/ build/ .idea/

缓存与 IDE

.gitignore

Hvigor 是鸿蒙的构建系统,角色接近 Android 的 Gradle。OHPM 是包管理器,角色接近 npm。oh-package.json5 里加依赖后,DevEco 会把包拉到 oh_modules

工程级 build-profile.json5 里最常看的字段:

  • signingConfigs:调试/发布签名

  • products:例如 default,里面指定 compatibleSdkVersionruntimeOS

  • modules:当前工程有哪些模块(entry、feature、har…)以及它们的 srcPath

6.2 AppScope:整个应用的身份证

AppScope 由工具生成,不要改文件夹名字

AppScope/app.json5 典型字段:

{
  "app": {
    "bundleName": "com.example.tododemo",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:layered_image",
    "label": "$string:app_name"
  }
}

字段

含义

bundleName

应用唯一包名,上架后不要乱改

vendor

厂商

versionCode / versionName

内部版本与展示版本

icon / label

桌面图标和名称,引用 AppScope/resources

AppScope/resources/base/element/string.json 放应用名等全局字符串;media 放分层图标(foreground/background)。

编译时 AppScope 的资源会合并进各 Module。重名时 AppScope 优先。HAR 模块引用不到 AppScope 资源,共享库里不要写 $r('app.xxx') 指望用宿主的 AppScope。

官方:app.json5 配置文件

6.3 entry 模块:真正写代码的地方

entrymodule.json5"type": "entry",编译产物是 HAP(Harmony Ability Package)。同一设备类型一个应用只能有一个 entry HAP。

src/main/ets/entryability/EntryAbility.ets

对应 Android 的 Application + 主 Activity 启动那一段。它不是页面,负责窗口和生命周期:

回调

何时

onCreate

Ability 创建,做初始化

onWindowStageCreate

窗口就绪,在这里 loadContent('pages/Index') 加载首页

onForeground / onBackground

前台 / 后台

onWindowStageDestroy / onDestroy

窗口销毁 / Ability 销毁

onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      hilog.error(0x0000, 'testTag', 'Failed to load the content.');
      return;
    }
    hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
  });
}

首页路径 'pages/Index' 必须能在 main_pages.json 里对上。

src/main/ets/pages/

每个 @Entry 页面一个 .ets。默认只有 Index.ets。新建页面:右键 pages → New → Page,工具会帮你登记路由。

src/main/ets/entrybackupability/

备份恢复扩展(ExtensionAbility,type 为 backup)。普通业务可以先不理。

建议自己加的目录(Empty 模板没有,但项目一长大就会需要):

ets/
├── entryability/
├── pages/
├── components/     可复用 UI 组件
├── model/          interface / enum / 数据模型
├── common/         常量、工具函数
└── viewmodel/      页面状态与业务(可选)

模块根目录其它文件

文件

作用

src/main/module.json5

模块类型、设备、Ability、权限、页面列表引用

build-profile.json5

模块 compile 选项、是否混淆、targets(default/ohosTest)

oh-package.json5

本模块依赖,可与工程级依赖并存

hvigorfile.ts

模块构建脚本

obfuscation-rules.txt

Release 混淆白名单/规则

src/ohosTest

跑在真机/模拟器上的测试

src/test

本地单元测试

src/mock

接口 Mock

module.json5 里你每天都会碰到的字段:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone"],
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ],
    "requestPermissions": []
  }
}

skills 里配了 entity.system.home + ohos.want.action.home,桌面才会出现图标。申请权限写在 requestPermissions,例如相机、位置。

官方:module.json5 配置文件

6.4 resources:字符串、颜色、图片、页面清单
entry/src/main/resources/
├── base/                 默认资源(必须有,其它限定词覆盖它)
│   ├── element/          string.json / color.json / float.json ...
│   ├── media/            图片、音频
│   └── profile/          main_pages.json 等 JSON 配置
├── dark/                 深色模式覆盖
├── zh_CN/  en_US/        多语言覆盖
└── rawfile/              不参与资源编译的原始文件

element 常见文件:

文件

类型

代码里怎么引用

string.json

字符串

$r('app.string.xxx')

color.json

颜色

$r('app.color.xxx')

float.json

尺寸/浮点

$r('app.float.xxx')

integer.json

整数

$r('app.integer.xxx')

media 下的图

媒体

$r('app.media.icon')

rawfile

原始文件

$rawfile('config.json')

base/profile/main_pages.json 不是可有可无

{
  "src": [
    "pages/Index",
    "pages/Detail"
  ]
}

新建了 Detail.ets 却忘记登记,运行时 loadContent / router 会失败。DevEco 用向导建 Page 会自动写这一项;手建文件就要自己补。

资源匹配规则:系统按当前语言、色模式、屏幕密度找限定词目录,找不到回退 base。所以 key 必须先在 base 里有一份默认值

官方:资源分类与访问

6.5 HAP / HAR / HSP:编译出来到底是什么

产物

module.json5 type

是什么

能不能单独安装

HAP

entry / feature

安装运行的基本单位

entry 可以;feature 跟包或按需

HAR

har

静态共享库,编译期打进使用方

否,像 SDK

HSP

shared

动态共享库,运行时一份代码

否,必须跟 HAP 一起

同一应用、同一设备类型:只能有一个 entry HAP。功能模块用 feature HAP。多个模块都要复用同一份代码时:只有一处用 → HAR 就够;多 HAP 都依赖同一份、还在意包体积 → 用 HSP,避免 HAR 被复制多份。

最终上架形态是 App Pack(.app,里面装一个或多个 HAP(以及 HSP)。

官方:应用程序包基础知识


七、启动链路:从点 Run 到 Index 出现在屏幕上

把「点绿色三角形」拆开:

  1. Hvigor 读工程 build-profile.json5 + 模块 build-profile.json5,编译 .ets.abc 字节码,合并 AppScopeentry 的 resources,生成 HAP。

  2. 系统读 AppScope/app.json5 知道包名、图标。

  3. entry/src/main/module.json5,找到 mainElement: EntryAbility,以及 skills 里的桌面入口。

  4. 拉起 EntryAbility.ets,走到 onWindowStageCreate

  5. loadContent('pages/Index') 对照 resources/base/profile/main_pages.json

  6. 实例化 pages/Index.ets 里带 @Entry 的组件,调用 build() 画出第一帧。

任意一环对不上,表现都不一样:

现象

先查

能装但不能从桌面打开

module.json5 的 skills / exported

启动白屏 / Failed to load content

main_pages.json 路径、loadContent 字符串

改了 Index 没变化

是不是改错模块、有没有重新 Run、预览器缓存

资源找不到

$r 的 type/name 是否和 json 里 name 一致;HAR 误引用 AppScope


八、完整案例:一个可运行的任务清单

下面这份代码可以贴进 Empty Ability 工程直接跑。它同时练习:类型、模块拆分、@State/@Prop、List/ForEach、资源引用、module 页面登记

8.1 资源

entry/src/main/resources/base/element/string.json

{
  "string": [
    { "name": "app_name", "value": "任务清单" },
    { "name": "input_placeholder", "value": "输入任务,回车或点添加" },
    { "name": "add", "value": "添加" },
    { "name": "empty", "value": "还没有任务" },
    { "name": "done_fmt", "value": "已完成 %d 项" }
  ]
}

entry/src/main/resources/base/element/color.json

{
  "color": [
    { "name": "page_bg", "value": "#F5F7FA" },
    { "name": "title", "value": "#1F2937" },
    { "name": "done_text", "value": "#9CA3AF" },
    { "name": "accent", "value": "#2563EB" }
  ]
}

entry/src/main/resources/base/profile/main_pages.json

{
  "src": [
    "pages/Index"
  ]
}

8.2 模型:ets/model/Task.ets
export enum TaskStatus {
  Todo = 0,
  Done = 1
}

export type Priority = 'low' | 'normal' | 'high';

export interface TaskItem {
  id: string;
  title: string;
  status: TaskStatus;
  priority: Priority;
}

export function createTask(title: string, priority: Priority = 'normal'): TaskItem {
  return {
    id: `${Date.now()}_${Math.floor(Math.random() * 1000)}`,
    title: title,
    status: TaskStatus.Todo,
    priority: priority
  };
}

这里没有 any:任务状态用枚举,优先级用字面量联合,工厂函数返回值写死 TaskItem

8.3 子组件:ets/components/TaskRow.ets
import { TaskItem, TaskStatus } from '../model/Task';

@Component
export struct TaskRow {
  @Prop item: TaskItem;
  onToggle: (id: string) => void = () => {};
  onDelete: (id: string) => void = () => {};

  build() {
    Row() {
      Checkbox()
        .select(this.item.status === TaskStatus.Done)
        .onChange(() => {
          this.onToggle(this.item.id);
        })

      Text(this.item.title)
        .fontSize(16)
        .fontColor(this.item.status === TaskStatus.Done ?
          $r('app.color.done_text') : $r('app.color.title'))
        .decoration({
          type: this.item.status === TaskStatus.Done ?
            TextDecorationType.LineThrough : TextDecorationType.None
        })
        .layoutWeight(1)
        .margin({ left: 8 })

      Text(this.item.priority)
        .fontSize(12)
        .fontColor($r('app.color.accent'))
        .margin({ right: 8 })

      Button('删除')
        .fontSize(12)
        .onClick(() => {
          this.onDelete(this.item.id);
        })
    }
    .width('100%')
    .padding(12)
  }
}

@Prop item 是父到子的只读快照。切换完成、删除都通过回调交给父组件改 @State 列表——这是 ArkUI 里最不容易把状态改乱的方式。

8.4 页面:ets/pages/Index.ets
import { TaskItem, TaskStatus, Priority, createTask } from '../model/Task';
import { TaskRow } from '../components/TaskRow';

@Entry
@Component
struct Index {
  @State tasks: TaskItem[] = [
    createTask('读完 ArkTS 类型一章', 'high'),
    createTask('搞清 entry 和 AppScope 的区别', 'normal')
  ];
  @State draft: string = '';
  @State filter: 'all' | 'todo' | 'done' = 'all';

  private visibleList(): TaskItem[] {
    if (this.filter === 'todo') {
      return this.tasks.filter((t: TaskItem) => t.status === TaskStatus.Todo);
    }
    if (this.filter === 'done') {
      return this.tasks.filter((t: TaskItem) => t.status === TaskStatus.Done);
    }
    return this.tasks;
  }

  private doneCount(): number {
    return this.tasks.filter((t: TaskItem) => t.status === TaskStatus.Done).length;
  }

  private addTask(): void {
    const title: string = this.draft.trim();
    if (title.length === 0) {
      return;
    }
    const next: TaskItem[] = this.tasks.slice();
    next.push(createTask(title, 'normal'));
    this.tasks = next;
    this.draft = '';
  }

  private toggle(id: string): void {
    const next: TaskItem[] = this.tasks.map((t: TaskItem) => {
      if (t.id !== id) {
        return t;
      }
      const status: TaskStatus = t.status === TaskStatus.Done ?
        TaskStatus.Todo : TaskStatus.Done;
      const copy: TaskItem = {
        id: t.id,
        title: t.title,
        status: status,
        priority: t.priority
      };
      return copy;
    });
    this.tasks = next;
  }

  private remove(id: string): void {
    this.tasks = this.tasks.filter((t: TaskItem) => t.id !== id);
  }

  build() {
    Column() {
      Text($r('app.string.app_name'))
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.title'))
        .margin({ bottom: 8 })

      Text(`已完成 ${this.doneCount()} / ${this.tasks.length}`)
        .fontSize(14)
        .margin({ bottom: 16 })

      Row() {
        TextInput({ placeholder: $r('app.string.input_placeholder'), text: this.draft })
          .layoutWeight(1)
          .onChange((v: string) => {
            this.draft = v;
          })
          .onSubmit(() => {
            this.addTask();
          })
        Button($r('app.string.add'))
          .margin({ left: 8 })
          .onClick(() => {
            this.addTask();
          })
      }
      .width('100%')

      Row() {
        this.filterChip('all', '全部')
        this.filterChip('todo', '未完成')
        this.filterChip('done', '已完成')
      }
      .margin({ top: 12, bottom: 8 })

      if (this.visibleList().length === 0) {
        Text($r('app.string.empty'))
          .margin({ top: 40 })
      } else {
        List() {
          ForEach(this.visibleList(), (item: TaskItem) => {
            ListItem() {
              TaskRow({
                item: item,
                onToggle: (id: string) => {
                  this.toggle(id);
                },
                onDelete: (id: string) => {
                  this.remove(id);
                }
              })
            }
          }, (item: TaskItem) => item.id)
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor($r('app.color.page_bg'))
  }

  @Builder
  filterChip(key: 'all' | 'todo' | 'done', label: string) {
    Button(label)
      .fontSize(12)
      .backgroundColor(this.filter === key ?
        $r('app.color.accent') : Color.Transparent)
      .fontColor(this.filter === key ? Color.White : $r('app.color.title'))
      .margin({ right: 8 })
      .onClick(() => {
        this.filter = key;
      })
  }
}

注意几处「像 TS 但必须按 ArkTS 写」的细节:

  1. @State 数组要换新引用。 this.tasks.push(...) 往往不会触发刷新,用 slice + push 再整体赋值。

  2. ForEach 第三个参数是 key。item.id,不要用下标,删除/勾选才不会错位。

  3. filter 的类型是字面量联合 'all' | 'todo' | 'done',不是 string,点 chip 时赋值会被编译器盯着。

  4. 文案和颜色走 $r,后面做中英文、深色模式只加限定词目录,不用改 UI 代码。

8.5 确认 Ability 加载的是这一页

EntryAbility.etsloadContent 保持:

windowStage.loadContent('pages/Index');

连真机或模拟器,点 Run。能添加、勾选、筛选、删除,这一套类型 + 目录 + UI 就算闭环了。

想加第二页(例如任务详情):New → Page 生成 Detail.ets,确认 main_pages.json 出现 "pages/Detail",再用 router.pushUrl({ url: 'pages/Detail', params: { id: item.id } })。新项目更建议直接上 Navigation


九、常见坑与自查清单

  1. 写了 any / var / user['name']:按第三节约束表改掉,不要关检查。

  2. 页面空白main_pages.json 有没有路径;loadContent 字符串是否 pages/Index 这种不含 .ets 的模块路径。

  3. 状态改了 UI 不动@State 对象/数组是否还是同一个引用;子组件该用回调而不是改 @Prop

  4. $r 编译失败string.json 里是 name 不是文件名;引用写成 $r('app.string.app_name')

  5. 改了应用名桌面没变:改的是 AppScope$string:app_name,不是 Index 里的 Text。

  6. 三方库装上了 import 失败:依赖写在谁的 oh-package.json5(工程级还是 entry 级);是否 Sync 过。

  7. HAR 里用 AppScope 资源:HAR 打包不含 AppScope,资源放到 HAR 自己的 resources

  8. 权限相关 API 崩溃module.json5requestPermissions 申请了吗?危险权限还要运行时弹窗。

  9. ForEach 闪动/删错项:没提供稳定 key。

  10. oh_modules / build 当源码改:下次 Sync 会被覆盖。


十、总结与延伸

鸿蒙应用不是「会一点 TS 就能写」。你要同时握住三块:

  • ArkTS:强制静态类型的 TS 子集。let/const、具体类型、class/interface、联合类型是基本功;any、动态对象、鸭子类型是禁区。

  • ArkUI@Entry + @Component + @State 画出界面;父子通信优先 @Prop + 回调。

  • 工程目录AppScope 是应用身份证,entry 是主 HAP,EntryAbility 拉起窗口,pages + main_pages.json 决定有哪些页面,resources 决定文案和皮肤。

下一步建议按这个顺序加码:

  1. 把任务清单的数据存进 Preferences / 关系型数据库

  2. Navigation 换成多页面

  3. 抽一个 HAR 放 model + components,entry 只保留页面

  4. ArkTS 编码规范

一句话: 先认目录,再认类型,最后才堆组件。目录认错,语法再熟也只是在错误的文件里把 Hello World 改来改去。


权威参考

  1. ArkTS 语言

  2. ArkTS 开发入门

  3. 从 TypeScript 到 ArkTS 的适配规则

  4. Java 程序员的 ArkTS 入门

  5. 应用程序包基础知识(HAP/HAR/HSP)

  6. app.json5 配置文件

  7. module.json5 配置文件

  8. 资源分类与访问

  9. UIAbility 生命周期

  10. ArkUI 组件导航 Navigation

  11. DevEco Studio 使用指南

  12. 在工程中添加模块

Logo

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

更多推荐