ArkTS 语言核心特性完全指南 - 装饰器、类型系统、异步编程深度解析

系列文章:HarmonyOS 6.0 实战开发 - 「今天空白」应用
第3篇 / 共30篇
发布时间:2026-02-09
阅读时长:20分钟
难度:(进阶)


本文导读

在上一篇中,我们成功搭建了 HarmonyOS 6.0 开发环境。从今天开始,我们深入 ArkTS 语言本身。

代码说明

  • 本文所有代码示例均来自实际运行的项目
  • 完整源码:GitHub - Today_is_blank
  • 建议对照源码阅读,理解完整的项目结构和设计思路

ArkTS 是什么?

  • TypeScript 的超集(TypeScript 代码可以直接运行)
  • 针对 HarmonyOS 优化的声明式UI语言
  • 支持装饰器系统的现代编程语言

本文内容

  • ArkTS vs TypeScript:关键差异
  • 装饰器系统完全解析
  • 类型系统与类型推导
  • 异步编程:Promise、async/await
  • 模块化与依赖管理
  • 真实项目代码剖析

学完本文你将

  • 理解 ArkTS 的设计哲学
  • 掌握装饰器的正确使用方式
  • 能够编写类型安全的 ArkTS 代码
  • 理解「今天空白」项目的核心代码

ArkTS vs TypeScript

相同点

ArkTS 完全兼容 TypeScript 语法:

//  这些 TypeScript 代码在 ArkTS 中都能运行

// 1. 类型注解
let name: string = '今天空白';
let age: number = 25;
let isActive: boolean = true;

// 2. 接口
interface User {
  id: number;
  name: string;
  email?: string;  // 可选属性
}

// 3. 泛型
function identity<T>(arg: T): T {
  return arg;
}

// 4. 类
class Person {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  greet(): string {
    return `Hello, ${this.name}`;
  }
}

// 5. 箭头函数
const add = (a: number, b: number): number => a + b;

// 6. 解构
const { name: userName, age: userAge } = user;

关键差异

特性 TypeScript ArkTS 说明
装饰器 实验性特性(需开启) 核心特性 ArkUI 的基础
UI 构建 build() 方法 声明式 UI
状态管理 需第三方库 内置装饰器 @State、@ObservedV2 等
资源访问 普通字符串 $r() 语法 访问资源文件
运行环境 Node.js/浏览器 ArkTS Runtime HarmonyOS 专用

装饰器系统:ArkTS 的灵魂

装饰器是 ArkTS 最重要的特性,用于:

  • 定义组件
  • 管理状态
  • 控制生命周期
  • 注入依赖

1. 组件装饰器

@ComponentV2 - 新一代组件
@ComponentV2
export struct TodayCard {
  // 组件内容
  build() {
    Column() {
      Text('今天空白')
    }
  }
}

关键点

  • @ComponentV2 是 V2 状态管理的基础
  • 必须有 build() 方法
  • export 让组件可被其他文件引用
@Entry - 页面入口
@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      Text('这是首页')
    }
  }
}

关键点

  • @Entry 标记页面入口
  • 一个页面只能有一个 @Entry 组件
  • 通常在 pages/ 目录下

2. 状态管理装饰器(V2)

@Local - 组件内部状态
@ComponentV2
struct Counter {
  // 组件私有状态
  @Local count: number = 0;

  build() {
    Column() {
      Text(`点击次数:${this.count}`)

      Button('点击')
        .onClick(() => {
          this.count++;  // 修改会触发 UI 更新
        })
    }
  }
}

关键点

  • @Local 用于组件内部状态
  • 状态变化会自动更新 UI
  • 不能传递给子组件
@Param - 父子组件通信
// 子组件
@ComponentV2
export struct UserCard {
  // 接收父组件传入的参数
  @Param userName: string = '';
  @Param @Require age!: number;  // 必传参数

  build() {
    Column() {
      Text(this.userName)
      Text(`${this.age}`)
    }
  }
}

// 父组件
@ComponentV2
struct Parent {
  @Local name: string = '张三';
  @Local age: number = 25;

  build() {
    Column() {
      // 传递参数给子组件
      UserCard({ userName: this.name, age: this.age })
    }
  }
}

关键点

  • @Param 接收父组件传入的值
  • @Require 标记必传参数
  • 父组件修改,子组件自动更新
@ObservedV2 - 响应式数据类
// 定义响应式数据模型
@ObservedV2
export class UserInfo {
  @Trace name: string = '';
  @Trace age: number = 0;
}

// 在组件中使用
@ComponentV2
struct Profile {
  @Local user: UserInfo = new UserInfo();

  build() {
    Column() {
      Text(this.user.name)  // 自动追踪变化
      Text(`${this.user.age}`)

      Button('修改姓名')
        .onClick(() => {
          this.user.name = '李四';  // UI 自动更新
        })
    }
  }
}

关键点

  • @ObservedV2 让类变为响应式
  • @Trace 标记需要追踪的属性
  • 修改属性会自动触发 UI 更新

3. 真实项目示例:今天空白

让我们看看「今天空白」项目中的真实代码:

代码说明: 以下代码来自实际运行的项目,完整代码请查看 GitHub 仓库

DailyEntry.ets - 领域模型

// 每日记录数据模型
export interface DailyEntry {
  date: string;        // YYYY-MM-DD
  text: string;        // 记录内容
  createdAt: number;   // 创建时间戳
  updatedAt: number;   // 更新时间戳
}

// 文本标准化处理
export function normalizeText(text: string): string {
  return text.trim();
}

// 检查文本是否为空
export function isBlankText(text: string): boolean {
  return normalizeText(text).length === 0;
}

// 创建新记录
export function createEntry(dateKey: string, text: string, nowMs: number): DailyEntry {
  const normalized = normalizeText(text);
  const entry: DailyEntry = {
    date: dateKey,
    text: normalized,
    createdAt: nowMs,
    updatedAt: nowMs
  };
  return entry;
}

// 更新现有记录
export function updateEntry(prev: DailyEntry, text: string, nowMs: number): DailyEntry {
  const normalized = normalizeText(text);
  const entry: DailyEntry = {
    date: prev.date,
    text: normalized,
    createdAt: prev.createdAt,  // 保持原创建时间
    updatedAt: nowMs            // 更新修改时间
  };
  return entry;
}

关键设计要点

  • 使用 interface 定义数据结构,不使用装饰器(纯数据模型)
  • 导出独立的工具函数,符合函数式编程理念
  • createEntryupdateEntry 分离,语义清晰
  • 时间戳由调用方传入,便于测试和时间控制

TodayStore.ets - 状态管理

// Usage: const store = new TodayStore(repo); await store.refresh()
import { todayKey } from '../../common/time/DateKey';
import { createEntry, isBlankText, normalizeText, updateEntry } from './DailyEntry';
import { TodayRepo } from './TodayRepo';

export type TodayStatus = 'loading' | 'blank' | 'recorded' | 'editing' | 'saving';

@ObservedV2
export class TodayStore {
  // 响应式状态 - 使用 @Trace 标记
  @Trace status: TodayStatus = 'loading';
  @Trace dateKey: string = '';
  @Trace text: string = '';
  @Trace draftText: string = '';
  @Trace updatedAt: number = 0;
  @Trace errorMessage: string = '';

  // 私有字段 - 不需要响应式
  private repo: TodayRepo;
  private lastStableStatus: 'blank' | 'recorded' = 'blank';

  constructor(repo: TodayRepo) {
    this.repo = repo;
  }

  // 刷新今日数据
  async refresh(now: Date = new Date()): Promise<void> {
    this.errorMessage = '';
    this.status = 'loading';
    const key = todayKey(now);  // 使用外部工具函数获取日期键
    this.dateKey = key;

    const entry = await this.repo.get(key);
    if (!entry) {
      // 没有记录 - 今天空白
      this.text = '';
      this.updatedAt = 0;
      this.status = 'blank';
      this.lastStableStatus = 'blank';
      return;
    }

    // 有记录 - 已记录
    this.text = entry.text;
    this.updatedAt = entry.updatedAt;
    this.status = 'recorded';
    this.lastStableStatus = 'recorded';
  }

  // 打开编辑器
  openEditor(): void {
    this.errorMessage = '';
    this.draftText = this.text;  // 复制当前文本到草稿
    this.status = 'editing';
  }

  // 取消编辑
  cancelEdit(): void {
    this.errorMessage = '';
    this.draftText = '';
    this.status = this.lastStableStatus;  // 恢复之前的稳定状态
  }

  // 保存记录
  async save(nextText: string): Promise<void> {
    const normalized = normalizeText(nextText);
    if (isBlankText(normalized)) {
      this.errorMessage = '内容不能为空';
      return;
    }

    this.status = 'saving';
    this.errorMessage = '';
    const nowMs = Date.now();

    try {
      const existing = await this.repo.get(this.dateKey);
      // 根据是否存在选择创建或更新
      const entry = existing
        ? updateEntry(existing, normalized, nowMs)
        : createEntry(this.dateKey, normalized, nowMs);

      await this.repo.set(entry);

      // 更新成功
      this.text = entry.text;
      this.updatedAt = entry.updatedAt;
      this.draftText = '';
      this.status = 'recorded';
      this.lastStableStatus = 'recorded';
    } catch (_err) {
      this.errorMessage = '保存失败';
      this.status = 'editing';
    }
  }

  // 清空记录
  async clear(): Promise<void> {
    this.status = 'saving';
    this.errorMessage = '';

    try {
      await this.repo.remove(this.dateKey);

      // 清空成功
      this.text = '';
      this.updatedAt = 0;
      this.draftText = '';
      this.status = 'blank';
      this.lastStableStatus = 'blank';
    } catch (_err) {
      this.errorMessage = '保存失败';
      this.status = this.lastStableStatus;
    }
  }
}

关键设计要点

  • @ObservedV2 将类变为响应式,配合 @Trace 实现精确更新
  • 使用 type 定义状态枚举,提供类型安全
  • 区分稳定状态 (blank/recorded) 和过渡状态 (loading/editing/saving)
  • lastStableStatus 用于取消编辑时恢复状态
  • 使用外部工具函数 (todayKeycreateEntryupdateEntry) 保持职责单一
  • Repository 模式隔离数据访问逻辑

TodayPage.ets - UI 组件

注意: 以下展示核心结构,实际项目包含完整的 UI 实现(背景图、设置按钮、清空确认对话框等),完整代码约 209 行,请查看 GitHub - TodayPage.ets

// Usage: TodayPage({ store, onSave, onClear, onOpenSettings })
import { TodayStore } from './TodayStore';
import { EditSheet } from './EditSheet';
import { UiTokens } from '../ui/UiTokens';

@ComponentV2
export struct TodayPage {
  // 必传参数 - 使用 @Require + @Param
  @Require
  @Param store!: TodayStore;
  @Require
  @Param onSave!: (text: string) => void;
  @Require
  @Param onClear!: () => void;
  @Require
  @Param onOpenSettings!: () => void;

  // 组件内部状态
  @Local showClearConfirm: boolean = false;

  // 计算属性(通过方法实现)
  private showBlank(): boolean {
    return this.store.status === 'blank' || this.store.status === 'loading';
  }

  build() {
    Stack() {
      // 背景层
      Column() {}
        .width('100%')
        .height('100%')
        .backgroundColor(UiTokens.ColorPageBg);

      Image($r('app.media.background'))
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)
        .opacity(0.18);

      // 主内容区
      Column() {
        // 头部:日期 + 设置按钮
        Row() {
          Column() {
            Text('今天')
              .fontSize(18)
              .fontWeight(FontWeight.Medium)
              .fontColor(UiTokens.ColorText);

            Text(this.store.dateKey || '')
              .fontSize(12)
              .fontColor(UiTokens.ColorTextMuted)
              .margin({ top: 2 });
          }

          Button('设置')
            .onClick(() => this.onOpenSettings())
            .height(UiTokens.ButtonHeight);
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%');

        // 内容卡片
        Column() {
          if (this.showBlank()) {
            // 空白态
            Text('今天空白')
              .fontSize(30)
              .fontColor(UiTokens.ColorText);
          } else {
            // 已记录态
            Text(this.store.text)
              .fontSize(20)
              .fontColor(UiTokens.ColorText)
              .lineHeight(28);
          }

          // 错误提示
          if (this.store.errorMessage && this.store.status !== 'editing') {
            Text(this.store.errorMessage)
              .fontSize(12)
              .fontColor(UiTokens.ColorDanger);
          }
        }
        .backgroundColor(UiTokens.ColorSurface)
        .borderRadius(UiTokens.RadiusL)
        .width('100%');

        // 底部按钮区
        if (this.showBlank()) {
          // 空白态:显示"记录一件事"按钮
          Button('记录一件事')
            .onClick(() => this.store.openEditor())
            .backgroundColor(UiTokens.ColorPrimary)
            .fontColor(UiTokens.ColorPrimaryText)
            .width('100%');
        } else {
          // 已记录态:显示"修改"和"清空"按钮
          Row() {
            Button('修改')
              .onClick(() => this.store.openEditor())
              .backgroundColor(UiTokens.ColorPrimary)
              .layoutWeight(1);

            Button('清空')
              .onClick(() => {
                this.showClearConfirm = true;
              })
              .backgroundColor(UiTokens.ColorSurfaceSoft)
              .layoutWeight(1);
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween);
        }
      }
      .padding(UiTokens.PagePadding)
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceBetween);

      // 编辑弹层
      if (this.store.status === 'editing') {
        Stack() {
          // 半透明遮罩
          Column() {}
            .width('100%')
            .height('100%')
            .backgroundColor(UiTokens.ColorPrimary)
            .opacity(0.35);

          // 编辑表单组件
          EditSheet({
            store: this.store,
            onSave: (text: string) => this.onSave(text)
          });
        }
        .width('100%')
        .height('100%');
      }

      // 清空确认对话框(实际代码中还有完整实现,此处省略)
      // if (this.showClearConfirm) { ... }
    }
    .width('100%')
    .height('100%');
  }
}

关键设计要点

  • @Require + @Param 组合确保父组件必须传递参数,编译时校验
  • @Local 管理组件内部状态(showClearConfirm
  • 使用方法实现计算属性(showBlank()),保持逻辑清晰
  • 条件渲染根据状态切换 UI(if/else
  • 使用 UiTokens 统一管理设计令牌(颜色、尺寸、间距)
  • Stack 布局实现背景图 + 内容的层叠效果
  • 组件间通过回调函数通信(onSaveonClearonOpenSettings
  • 职责清晰:页面组件只负责 UI 渲染和事件分发,业务逻辑在 Store 中

类型系统

基础类型

// 1. 基本类型
let str: string = 'Hello';
let num: number = 42;
let bool: boolean = true;
let undef: undefined = undefined;
let nul: null = null;

// 2. 数组
let arr1: number[] = [1, 2, 3];
let arr2: Array<string> = ['a', 'b', 'c'];

// 3. 元组
let tuple: [string, number] = ['今天空白', 1];

// 4. 枚举
enum Status {
  Loading = 'loading',
  Blank = 'blank',
  Recorded = 'recorded'
}

let status: Status = Status.Blank;

// 5. Any(尽量避免)
let anything: any = '可以是任何类型';

接口与类型别名

// 接口(推荐用于对象)
interface Entry {
  date: string;
  text: string;
  readonly id: number;  // 只读属性
  tags?: string[];      // 可选属性
}

// 类型别名(推荐用于联合类型)
type Status = 'loading' | 'blank' | 'recorded';
type ID = number | string;

// 类型别名也可以用于对象(但接口更灵活)
type Point = {
  x: number;
  y: number;
};

联合类型与交叉类型

// 联合类型(或)
type Result = string | number;
let result: Result = '成功';
result = 200;  // 

// 交叉类型(且)
interface Nameable {
  name: string;
}

interface Ageable {
  age: number;
}

type Person = Nameable & Ageable;

let person: Person = {
  name: '张三',
  age: 25
};

泛型

// 泛型函数
function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>('Hello');  // 明确指定类型
let output2 = identity(123);              // 类型推导

// 泛型接口
interface Repository<T> {
  save(item: T): Promise<void>;
  load(id: string): Promise<T | null>;
  delete(id: string): Promise<void>;
}

// 实现泛型接口
class EntryRepository implements Repository<DailyEntry> {
  async save(item: DailyEntry): Promise<void> {
    // 保存逻辑
  }

  async load(id: string): Promise<DailyEntry | null> {
    // 加载逻辑
    return null;
  }

  async delete(id: string): Promise<void> {
    // 删除逻辑
  }
}

类型推导与类型断言

// 类型推导(TypeScript 自动推断)
let message = 'Hello';  // 推导为 string
let count = 42;         // 推导为 number

// 类型断言(手动指定类型)
let someValue: any = 'this is a string';

// 方式1:<> 语法(不推荐,在 JSX/TSX 中冲突)
let strLength1 = (<string>someValue).length;

// 方式2:as 语法(推荐)
let strLength2 = (someValue as string).length;

异步编程

HarmonyOS 中的大部分操作都是异步的:

  • 网络请求
  • 文件读写
  • 数据库操作

Promise 基础

// 创建 Promise
function delay(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// 使用 Promise
delay(1000).then(() => {
  console.log('1秒后执行');
});

// 链式调用
fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

async/await(推荐)

// 异步函数
async function loadData(): Promise<void> {
  try {
    // await 等待 Promise 完成
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);

    console.log(posts);
  } catch (error) {
    console.error('加载失败', error);
  }
}

// 真实示例:TodayStore.init()
async init(): Promise<void> {
  this.status = 'loading';

  try {
    // await 等待数据库读取完成
    const entry = await this.repo.loadToday(this.dateKey);

    if (entry && entry.text.trim().length > 0) {
      this.text = entry.text;
      this.status = 'recorded';
    } else {
      this.status = 'blank';
    }
  } catch (error) {
    this.errorMessage = '加载失败';
    this.status = 'blank';
  }
}

并发执行

// 串行执行(慢)
async function sequential() {
  const user = await fetchUser();     // 等待 1 秒
  const posts = await fetchPosts();   // 等待 2 秒
  // 总计:3 秒
}

// 并行执行(快)
async function parallel() {
  const [user, posts] = await Promise.all([
    fetchUser(),    // 同时开始
    fetchPosts()    // 同时开始
  ]);
  // 总计:2 秒(最慢的那个)
}

// 真实示例:批量加载
async function loadMultipleDays(): Promise<DailyEntry[]> {
  const dateKeys = ['2026-01-20', '2026-01-21', '2026-01-22'];

  // 并行加载
  const entries = await Promise.all(
    dateKeys.map(key => repo.loadToday(key))
  );

  // 过滤掉 null
  return entries.filter(e => e !== null) as DailyEntry[];
}

模块化

导出

// 导出接口
export interface DailyEntry {
  date: string;
  text: string;
}

// 导出类
export class TodayStore {
  // ...
}

// 导出函数
export function formatDate(date: Date): string {
  return date.toISOString();
}

// 导出常量
export const API_URL = 'https://api.example.com';

// 默认导出(一个文件只能有一个)
export default class App {
  // ...
}

导入

// 导入具名导出
import { DailyEntry, TodayStore } from './TodayStore';

// 导入默认导出
import App from './App';

// 导入所有(as 语法)
import * as Utils from './Utils';
Utils.formatDate(new Date());

// 导入并重命名
import { TodayStore as Store } from './TodayStore';

命名空间

// 定义命名空间
export namespace DailyEntry {
  export function create(date: string, text: string): DailyEntry {
    // ...
  }

  export function validate(entry: DailyEntry): boolean {
    // ...
  }
}

// 使用命名空间
import { DailyEntry } from './DailyEntry';

const entry = DailyEntry.create('2026-01-20', '今天完成了...');
const isValid = DailyEntry.validate(entry);

资源访问

ArkTS 提供了 $r() 语法访问资源文件:

// 访问字符串资源
Text($r('app.string.app_name'))

// 访问图片资源
Image($r('app.media.background'))

// 访问颜色资源
.backgroundColor($r('app.color.primary'))

// 资源文件位置
// entry/src/main/resources/base/element/string.json
{
  "string": [
    {
      "name": "app_name",
      "value": "今天空白"
    }
  ]
}

最佳实践

1. 优先使用 const

//  推荐
const name = '今天空白';

//  避免
let name = '今天空白';  // 如果不需要重新赋值

2. 明确类型注解

//  推荐(清晰)
function add(a: number, b: number): number {
  return a + b;
}

//  避免(依赖推导)
function add(a, b) {  // 类型为 any
  return a + b;
}

3. 使用可选链

//  推荐
const userName = user?.profile?.name ?? '未知';

//  避免
const userName = user && user.profile && user.profile.name
  ? user.profile.name
  : '未知';

4. 避免使用 any

//  避免
let data: any = fetchData();

//  推荐
interface ApiResponse {
  code: number;
  data: DailyEntry[];
}

let response: ApiResponse = await fetchData();

5. 合理使用装饰器

//  推荐:Store 类使用 @ObservedV2
@ObservedV2
export class TodayStore {
  @Trace status: string = 'loading';
}

//  避免:普通数据类不需要装饰器
interface DailyEntry {  // 不需要 @ObservedV2
  date: string;
  text: string;
}

实战练习

练习 1:创建计数器组件

要求:

  • 使用 @ComponentV2
  • 使用 @Local 管理计数
  • 点击按钮增加计数
点击查看答案
@ComponentV2
export struct Counter {
  @Local count: number = 0;

  build() {
    Column({ space: 20 }) {
      Text(`计数:${this.count}`)
        .fontSize(24)

      Button('增加')
        .onClick(() => {
          this.count++;
        })
    }
    .padding(20)
  }
}

练习 2:创建用户信息 Store

要求:

  • 使用 @ObservedV2
  • 包含 name 和 age 属性
  • 提供 updateName 和 updateAge 方法
点击查看答案
@ObservedV2
export class UserStore {
  @Trace name: string = '';
  @Trace age: number = 0;

  updateName(newName: string): void {
    this.name = newName;
  }

  updateAge(newAge: number): void {
    this.age = newAge;
  }
}

练习 3:异步加载数据

要求:

  • 模拟延迟 1 秒
  • 使用 async/await
  • 处理错误
点击查看答案
function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function loadUserData(): Promise<User | null> {
  try {
    await delay(1000);  // 模拟网络请求

    // 模拟返回数据
    return {
      id: 1,
      name: '张三',
      age: 25
    };
  } catch (error) {
    console.error('加载失败', error);
    return null;
  }
}

学习检查清单

完成本文后,你应该能够:

  • 理解 ArkTS 与 TypeScript 的关系
  • 掌握 @ComponentV2、@Local、@Param 等装饰器
  • 理解 @ObservedV2 和 @Trace 的作用
  • 能够定义接口和类型
  • 熟练使用 async/await 处理异步操作
  • 理解模块导入导出机制
  • 能够阅读「今天空白」项目代码

扩展学习

推荐文档

进阶主题

  • 装饰器的实现原理
  • 类型体操(高级类型)
  • 依赖注入(IoC)

下期预告

下一篇:《V2 状态管理完全指南(上)- 核心概念与基础用法》

内容包括:

  • V1 vs V2:为什么必须升级
  • @ObservedV2 深度解析
  • @Trace 依赖追踪原理
  • Store 模式设计
  • 性能对比与优化

发布时间:2026-02-09


互动环节

今日思考题

  1. 你之前使用过 TypeScript 吗?感觉 ArkTS 有什么不同?
  2. 你最喜欢 ArkTS 的哪个特性?为什么?
  3. 对装饰器的理解还有疑问吗?

欢迎在评论区分享!


如果本文对你有帮助,请点赞、收藏、关注我!

代码看不懂?评论区提问,我会详细解答!


关键词:#ArkTS #TypeScript #装饰器 #异步编程 #类型系统 #HarmonyOS开发

本文 GitHubToday_is_blank
系列目录README

Logo

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

更多推荐