鸿蒙开发从工程打开到第一行 ArkTS:语法、类型、DevEco 目录与完整案例
目录(建议直接用 CSDN 目录功能生成)
一、引言:打开 DevEco 后,你真正要搞懂的三件事
很多人学鸿蒙的第一天,会同时撞上三堵墙:
-
语言:文件后缀是
.ets,看起来像 TypeScript,一用any、var、对象动态加字段就编译失败。 -
工程:DevEco Studio 新建 Empty Ability 后,左边一堆
AppScope、entry、oh_modules、hvigorfile.ts,不知道改哪个文件界面才会变。 -
运行:改了
Index.ets没反应,其实页面没登记进main_pages.json;或者字符串写死在代码里,多语言/深色模式全废。
这三件事其实是一条链:
ArkTS 负责把逻辑写对,ArkUI 负责把界面画出来,工程目录和 json5 负责告诉系统「谁是入口、有哪些页、用哪些资源」。
本文按「语言 → 类型 → 目录 → 启动链路 → 完整案例」一次讲完。读完你应该能:看懂 Empty Ability 工程里每一个默认文件的职责,用 ArkTS 写出带类型的业务代码,并跑通一个任务清单小应用。
二、问题根源:ArkTS 不是「能跑的 TypeScript」
ArkTS 是 HarmonyOS 应用开发的官方语言。它基于 TypeScript 的语法风格,但为了编译期静态检查和运行时性能,砍掉了一批 TS 里过于动态的能力。
|
JavaScript |
TypeScript |
ArkTS |
|
|---|---|---|---|
|
类型 |
运行时才知道 |
可选静态类型,可关严格检查 |
强制静态类型 |
|
|
无所谓 |
常用逃生舱 |
禁止 |
|
|
支持 |
支持 |
禁止,用 let/const |
|
运行时改对象结构 |
随便加字段 |
严格模式下会报,绕得过 |
禁止,对象布局编译期钉死 |
|
结构化类型(鸭子类型) |
本质就是 |
支持 |
不支持,必须靠 class / interface / 继承 |
|
UI |
无 |
无 |
配套 ArkUI 声明式语法( |
|
文件 |
|
|
|
官方原话可以概括成三条铁律:
-
强制静态类型:变量类型在编译前就必须确定,减少运行时类型检查,换性能和稳健性。
-
禁止运行时改变对象布局:不能
delete字段、不能给实例动态加属性、不能靠as any绕过。 -
限制部分运算符语义:例如一元
+只能作用在数字上。
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;
}
}
几个必须记住的写法:
|
场景 |
正确 |
错误 |
|---|---|---|
|
变量 |
|
|
|
私有字段 |
|
|
|
类型断言 |
|
|
|
匿名函数 |
|
|
|
访问对象字段 |
|
|
|
访问数组元素 |
|
用字符串当对象键去「当 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 迁过来最容易踩的约束
把官方适配规则压成一张「会不会过编译」的表:
|
约束 |
含义 |
怎么改 |
|---|---|---|
|
禁止 |
类型必须具体 |
定义 interface / class / 联合类型 |
|
禁止 |
只有块级作用域 |
|
|
禁止改对象布局 |
不能动态加/删字段 |
字段写进 class/interface |
|
不支持 structural typing |
「长得像」不等于同一类型 |
显式 implements / 继承 |
|
不支持条件类型、 |
没有 |
拆成明确的类型别名或重载 |
|
不支持交叉类型 |
不能用 intersection |
用继承或合并成新 interface |
|
不支持 |
链式调用要写返回类型 |
写成具体 class 名 |
|
不支持 |
运行时唯一键没有意义 |
用字面量字段名 |
|
对象字面量要有明确类型 |
不能丢给 |
|
|
泛型有时要显式实参 |
不能只靠返回值推断 |
|
完整清单以官方为准:从 TypeScript 到 ArkTS 的适配规则。
四、ArkTS 支持的类型(全表 + 例子)
4.1 基本类型
|
类型 |
含义 |
例子 |
|---|---|---|
|
|
IEEE 754 双精度,整数和小数都用它 |
|
|
|
字符串,无独立 |
|
|
|
真假 |
|
|
|
超过 |
|
|
|
空值 |
|
|
|
未定义 |
|
|
|
函数无返回值 |
|
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 明确不支持、不要再写的类型
|
不要写 |
原因 |
替代 |
|---|---|---|
|
|
静态类型被掏空 |
具体 interface / 联合 |
|
|
同上 |
联合 + 收窄 |
|
|
不支持交叉类型 |
新 interface 或继承 |
|
|
不支持条件类型 |
多个类型别名 |
|
|
布局必须静态可知 |
|
|
对象类型里的 index signature |
如 |
|
对照 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%')
}
}
常用装饰器:
|
装饰器 |
作用 |
|---|---|
|
|
页面入口,一个文件通常一个 |
|
|
自定义组件 |
|
|
轻量 UI 片段(可复用的 build 块) |
|
|
组件自己的状态,改了会刷新 UI |
|
|
父 → 子单向同步 |
|
|
父子双向同步 |
|
|
跨层级依赖注入式状态 |
|
|
某状态变化时回调 |
|
|
把 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) 后,工程大致长这样(不同版本可能多 entrybackupability、code-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 工程级:打开工程第一眼看到的那些
|
路径 |
干什么 |
你什么时候改 |
|---|---|---|
|
|
整个 App 的全局配置和全局资源 |
改包名、应用名、图标、版本 |
|
|
主模块源码 |
日常开发主战场 |
|
|
OHPM 装下来的依赖 |
不手改,等价 |
|
|
Hvigor 构建系统 |
一般不动 |
|
|
签名、product、compileSdkVersion、compatibleSdkVersion |
换 API 版本、配签名、多产品风味 |
|
|
工程构建任务 |
自定义构建时才动 |
|
|
依赖、overrides、parameterFile |
加三方库 |
|
|
锁定依赖树 |
提交仓库,不手改 |
|
|
本机 HarmonyOS SDK 路径 |
换电脑会变,通常不入库或按团队规范 |
|
|
ESLint/ArkTS 检查规则 |
统一团队规范时 |
|
|
缓存与 IDE |
|
Hvigor 是鸿蒙的构建系统,角色接近 Android 的 Gradle。OHPM 是包管理器,角色接近 npm。oh-package.json5 里加依赖后,DevEco 会把包拉到 oh_modules。
工程级 build-profile.json5 里最常看的字段:
-
signingConfigs:调试/发布签名 -
products:例如default,里面指定compatibleSdkVersion、runtimeOS -
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"
}
}
|
字段 |
含义 |
|---|---|
|
|
应用唯一包名,上架后不要乱改 |
|
|
厂商 |
|
|
内部版本与展示版本 |
|
|
桌面图标和名称,引用 |
AppScope/resources/base/element/string.json 放应用名等全局字符串;media 放分层图标(foreground/background)。
编译时 AppScope 的资源会合并进各 Module。重名时 AppScope 优先。HAR 模块引用不到 AppScope 资源,共享库里不要写 $r('app.xxx') 指望用宿主的 AppScope。
6.3 entry 模块:真正写代码的地方
entry 的 module.json5 里 "type": "entry",编译产物是 HAP(Harmony Ability Package)。同一设备类型一个应用只能有一个 entry HAP。
src/main/ets/entryability/EntryAbility.ets
对应 Android 的 Application + 主 Activity 启动那一段。它不是页面,负责窗口和生命周期:
|
回调 |
何时 |
|---|---|
|
|
Ability 创建,做初始化 |
|
|
窗口就绪,在这里 |
|
|
前台 / 后台 |
|
|
窗口销毁 / 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/ 页面状态与业务(可选)
模块根目录其它文件
|
文件 |
作用 |
|---|---|
|
|
模块类型、设备、Ability、权限、页面列表引用 |
|
|
模块 compile 选项、是否混淆、targets(default/ohosTest) |
|
|
本模块依赖,可与工程级依赖并存 |
|
|
模块构建脚本 |
|
|
Release 混淆白名单/规则 |
|
|
跑在真机/模拟器上的测试 |
|
|
本地单元测试 |
|
|
接口 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,例如相机、位置。
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 常见文件:
|
文件 |
类型 |
代码里怎么引用 |
|---|---|---|
|
|
字符串 |
|
|
|
颜色 |
|
|
|
尺寸/浮点 |
|
|
|
整数 |
|
|
media 下的图 |
媒体 |
|
|
|
原始文件 |
|
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 |
是什么 |
能不能单独安装 |
|---|---|---|---|
|
HAP |
|
安装运行的基本单位 |
entry 可以;feature 跟包或按需 |
|
HAR |
|
静态共享库,编译期打进使用方 |
否,像 SDK |
|
HSP |
|
动态共享库,运行时一份代码 |
否,必须跟 HAP 一起 |
同一应用、同一设备类型:只能有一个 entry HAP。功能模块用 feature HAP。多个模块都要复用同一份代码时:只有一处用 → HAR 就够;多 HAP 都依赖同一份、还在意包体积 → 用 HSP,避免 HAR 被复制多份。
最终上架形态是 App Pack(.app),里面装一个或多个 HAP(以及 HSP)。
官方:应用程序包基础知识

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

把「点绿色三角形」拆开:
-
Hvigor 读工程
build-profile.json5+ 模块build-profile.json5,编译.ets→.abc字节码,合并AppScope与entry的 resources,生成 HAP。 -
系统读
AppScope/app.json5知道包名、图标。 -
读
entry/src/main/module.json5,找到mainElement: EntryAbility,以及skills里的桌面入口。 -
拉起
EntryAbility.ets,走到onWindowStageCreate。 -
loadContent('pages/Index')对照resources/base/profile/main_pages.json。 -
实例化
pages/Index.ets里带@Entry的组件,调用build()画出第一帧。
任意一环对不上,表现都不一样:
|
现象 |
先查 |
|---|---|
|
能装但不能从桌面打开 |
|
|
启动白屏 / Failed to load content |
|
|
改了 Index 没变化 |
是不是改错模块、有没有重新 Run、预览器缓存 |
|
资源找不到 |
|
八、完整案例:一个可运行的任务清单
下面这份代码可以贴进 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 写」的细节:
-
改
@State数组要换新引用。this.tasks.push(...)往往不会触发刷新,用slice+push再整体赋值。 -
ForEach第三个参数是 key。 用item.id,不要用下标,删除/勾选才不会错位。 -
filter的类型是字面量联合'all' | 'todo' | 'done',不是string,点 chip 时赋值会被编译器盯着。 -
文案和颜色走
$r,后面做中英文、深色模式只加限定词目录,不用改 UI 代码。
8.5 确认 Ability 加载的是这一页
EntryAbility.ets 的 loadContent 保持:
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。
九、常见坑与自查清单
-
写了
any/var/user['name']:按第三节约束表改掉,不要关检查。 -
页面空白:
main_pages.json有没有路径;loadContent字符串是否pages/Index这种不含.ets的模块路径。 -
状态改了 UI 不动:
@State对象/数组是否还是同一个引用;子组件该用回调而不是改@Prop。 -
$r编译失败:string.json里是name不是文件名;引用写成$r('app.string.app_name')。 -
改了应用名桌面没变:改的是
AppScope的$string:app_name,不是Index里的 Text。 -
三方库装上了 import 失败:依赖写在谁的
oh-package.json5(工程级还是 entry 级);是否 Sync 过。 -
HAR 里用 AppScope 资源:HAR 打包不含 AppScope,资源放到 HAR 自己的
resources。 -
权限相关 API 崩溃:
module.json5的requestPermissions申请了吗?危险权限还要运行时弹窗。 -
ForEach 闪动/删错项:没提供稳定 key。
-
把
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决定文案和皮肤。
下一步建议按这个顺序加码:
-
把任务清单的数据存进 Preferences / 关系型数据库
-
用
Navigation换成多页面 -
抽一个 HAR 放
model+components,entry 只保留页面
一句话: 先认目录,再认类型,最后才堆组件。目录认错,语法再熟也只是在错误的文件里把 Hello World 改来改去。
权威参考
更多推荐

所有评论(0)