HarmonyOS应用<节气通>开发第32篇:ArkTS语法快速入门——从TypeScript到声明式UI的完整指南

引言

ArkTS 是 HarmonyOS 应用开发的唯一官方语言,它在 TypeScript 基础上进行了扩展:加入了装饰器(Decorator)系统、声明式 UI 描述、状态管理机制等。对于有 TypeScript/JavaScript 经验的开发者来说,ArkTS 的学习曲线很平缓;但对于从 Java/Android 或 Swift/iOS 转过来的开发者,需要理解一套全新的编程范式。

本文将系统讲解 ArkTS 的核心语法,覆盖 类型系统、组件模型、状态管理全家桶、布局体系、控制流、生命周期、以及「节气通」中的实际代码模式。目标是让你读完就能写出符合规范的 ArkUI 代码。


学习目标

完成本文后,你将能够:

  • ✅ 掌握 ArkTS 类型系统(基础类型、联合类型、字面量类型、泛型)
  • ✅ 理解装饰器体系(@Component / @Entry / @Builder / @Styles 等)
  • ✅ 熟练使用 7 种状态管理装饰器及其适用场景
  • ✅ 掌握 5 种布局容器的使用技巧与性能对比
  • ✅ 正确使用 ForEach / If / 条件渲染等控制流
  • ✅ 理解组件生命周期与页面生命周期的区别
  • ✅ 避开 ArkTS 开发中 10 个最常见的坑

一、ArkTS 与 TypeScript 的关系

1.1 语言定位

复制代码
┌─────────────────────────────────────────────┐
│              JavaScript (ECMAScript)          │
│                    ↑ 继承并强化               │
│              TypeScript (超集)                │
│                    ↑ 扩展                     │
│              ArkTS (HarmonyOS 专用)           │
│                                             │
│  ArkTS = TypeScript + 装饰器 + 声明式UI      │
│         + 状态管理 + ArkUI 组件库            │
└─────────────────────────────────────────────┘

1.2 ArkTS 相对 TS 的主要变化

特性 TypeScript ArkTS 说明
装饰器 @decorator(实验性) 一等公民 @Component/@State 等内置装饰器
声明式 UI 无(JSX 非标准) build() 方法 框架级支持,非 JSX
状态响应式 无需(React 手动) 自动追踪 修改 @State 变量自动触发重渲染
any 类型 允许 严格限制 不建议使用 any,用 unknown 替代
动态属性访问 obj[dynamicKey] 受限 索引签名有限制,优先用固定属性名
prototype 操作 自由修改 禁止 不能动态修改原型链

二、类型系统详解

2.1 基础类型

typescript 复制代码
// ====== 原始类型 ======
let appName: string = '节气通';           // 字符串
let versionCode: number = 100;             // 数字(无 int/float 区分)
let isReleased: boolean = true;            // 布尔值

// ====== 特殊类型 ======
let data: null = null;                      // 只能是 null
let unused: undefined = undefined;          // 只能是 undefined
// 注意: ArkTS 中 null 和 undefined 是不同类型

// ====== 对象类型 ======

// 数组 --- 两种写法等价
let solarTerms: string[] = ['立春', '雨水', '惊蛰', '春分'];
let numbers: Array<number> = [1, 2, 3];

// 元组 --- 固定长度、固定类型的数组
let coordinate: [number, number] = [39.9, 116.4];   // [经度, 纬度]
let pair: [string, number] = ['立春', 1];            // [名称, 序号]

// 枚举 --- 数字枚举(默认从 0 开始)
enum SolarTermSeason {
  Spring,    // 0
  Summer,    // 1
  Autumn,    // 2
  Winter     // 3
}

// 字符串枚举
enum ThemeMode {
  Light = 'light',
  Dark = 'dark',
  System = 'system'
}

// 联合类型 --- 值可以是多种类型之一
type StringOrNumber = string | number;
let value: StringOrNumber = 'hello';
value = 42;  // 合法

// 字面量类型 --- 限定具体值
type Alignment = 'left' | 'center' | 'right';
type PageSize = 10 | 20 | 50;

// 可选类型 --- 等价于 T | undefined
interface User {
  id: string;
  name: string;
  avatar?: string;    // 等价于 avatar: string | undefined
}

2.2 接口与类型别名

typescript 复制代码
// ====== 接口 Interface ======
// 推荐:用于定义对象结构(可继承、可实现)

interface SolarTerm {
  readonly id: string;            // readonly --- 只读,赋值后不可修改
  name: string;
  order: number;                  // 在24节气中的序号(1-24)
  solarDate: string;             // 公历日期 "02-04"
  lunarDate: string;             // 农历日期
  season: SolarTermSeason;        // 使用上面定义的枚举
  description: string;
  customs?: string[];             // 可选属性
  healthTips?: HealthTip[];       // 可选属性
}

interface HealthTip {
  title: string;
  content: string;
  category: 'diet' | 'exercise' | 'sleep' | 'mood';
}

// 接口继承
interface DetailedSolarTerm extends SolarTerm {
  threePhenomena: string[];       // 三候
  suitableActivities: string[];   // 宜
  avoidActivities: string[];      // 忌
  poem?: string;                  // 相关诗词
}

// ====== 类型别名 Type Alias ======
// 推荐:用于联合类型、交叉类型、函数签名、映射类型

// 函数类型别名
type FetchCallback = (data: SolarTerm[], error?: Error) => void;
type AsyncFetcher<T> = (id: string) => Promise<T>;

// 映射类型 --- 批量生成类型
type ReadonlySolarTerm = {
  readonly [K in keyof SolarTerm]: SolarTerm[K]
};

// 条件类型
type NonNullable<T> = T extends null | undefined ? never : T;

// 工具类型(ArkTS 内置)
type PartialSolarTerm = Partial<SolarTerm>;     // 所有属性变可选
type RequiredSolarTerm = Required<SolarTerm>;   // 所有属性变必填
type SolarTermNames = Pick<SolarTerm, 'name' | 'order'>;  // 只取部分属性

2.3 泛型

typescript 复制代码
// ====== 泛型函数 ======
// 让函数/类能处理多种类型,同时保持类型安全

function firstElement<T>(arr: T[]): T | undefined {
  return arr.length > 0 ? arr[0] : undefined;
}

// 使用时自动推断类型
const firstTerm = firstElement(['立春', '雨水']);  // 返回 string | undefined
const firstNum = firstElement([1, 2, 3]);          // 返回 number | undefined

// 显式指定类型
const result = firstElement<SolarTerm>(solarTermsList);

// ====== 泛型约束 ======
// 限制 T 必须满足某个条件
interface HasId {
  id: string;
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}

// ====== 泛型类 ======
class ApiResponse<T> {
  constructor(
    public code: number,
    public message: string,
    public data: T
  ) {}

  isSuccess(): boolean {
    return this.code === 200;
  }

  getData(): T {
    return this.data;
  }
}

// 使用
const termResponse = new ApiResponse(200, 'OK', { id: '1', name: '立春' } as SolarTerm);
const listResponse = new ApiResponse(200, 'OK', [] as SolarTerm[]);

三、装饰器体系

3.1 装饰器全景图

复制代码
┌──────────────────────────────────────────────────────────┐
│                   ArkTS 装饰器体系                         │
│                                                          │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────────────┐ │
│  │ 组件装饰器    │  │ 状态装饰器    │  │ 内置结构体装饰器   │ │
│  │             │  │             │  │                  │ │
│  │ @Entry      │  │ @State      │  │ @Builder         │ │
│  │ @Component  │  │ @Prop       │  │ @BuilderParam    │ │
│  │ @Preview    │  │ @Link       │  │ @Styles          │ │
│  │ @CustomDialog│  │ @Provide    │  │ @Extend          │ │
│  │ @Reusable   │  │ @Consume    │  │ @ObjectLink      │ │
│  │             │  │ @Watch      │  │ @StorageProp     │ │
│  │             │  │ @Computed   │  │ @StorageLink     │ │
│  │             │  │ @StorageProp│  │ @LocalStorageProp│ │
│  │             │  │ @StorageLink│  │ @LocalStorageLink│ │
│  └─────────────┘  └─────────────┘  └──────────────────┘ │
│                                                          │
│  ┌─────────────┐  ┌─────────────┐                        │
│  │ 页面装饰器    │  │ 方法/变量装饰器│                       │
│  │             │  │             │                        │
│  │ @Route      │  │ @Trace      │                        │
│  │ @Builder    │  │ @Volatile   │                        │
│  └─────────────┘  └─────────────┘                        │
└──────────────────────────────────────────────────────────┘

3.2 组件装饰器详解

typescript 复制代码
// ====== @Entry + @Component --- 最基础的组件定义 ======

/**
 * @Entry: 标记该组件为页面入口(一个页面只能有一个 @Entry)
 * @Component: 标记这是一个自定义组件(必须配合 struct 使用)
 *
 * 规则:
 * - 被 @Component 装饰的 struct 必须有 build() 方法
 * - build() 方法内只能使用声明式 UI 描述(不能写普通逻辑语句)
 * - build() 方法不能有返回值
 */
@Entry
@Component
struct SplashPage {
  // ... 组件内容见下文
}

// ====== @Preview --- 仅用于预览(不参与实际编译产物)=====
/**
 * DevEco Studio 中单独预览组件用
 * 实际运行时不生效,仅开发调试用途
 */
@Preview
@Component
struct SolarTermCardPreview {
  build() {
    Text('立春').fontSize(20).padding(16).backgroundColor('#E8F5EE')
  }
}

// ====== @CustomDialog --- 自定义弹窗 ======
@CustomDialog
struct ConfirmDialog {
  controller: CustomDialogController;
  title: string = '';
  message: string = '';
  confirmText: string = '确定';
  cancelText: string = '取消';
  onConfirm?: () => void;
  onCancel?: () => void;

  build() {
    Column({ space: 16 }) {
      Text(this.title).fontSize(18).fontWeight(FontWeight.Bold)
      Text(this.message).fontSize(14).fontColor('#666666')

      Row({ space: 12 }) {
        Button(this.cancelText)
          .layoutWeight(1)
          .onClick(() => {
            this.controller.close();
            this.onCancel?.();
          })
        Button(this.confirmText)
          .layoutWeight(1)
          .backgroundColor('#4A9B6D')
          .onClick(() => {
            this.controller.close();
            this.onConfirm?.();
          })
      }.width('100%')
    }.padding(20).borderRadius(12).backgroundColor('#FFFFFF')
  }
}

// 使用自定义弹窗
dialogController: CustomDialogController = new CustomDialogController({
  builder: ConfirmDialog({
    title: '确认删除',
    message: '删除后无法恢复,确定要继续吗?',
    onConfirm: () => { /* 处理确认 */ },
    onCancel: () => { /* 取消 */ }
  }),
  autoCancel: true,
  alignment: DialogAlignment.Center,
})

// 弹出: this.dialogController.open()

3.3 结构体装饰器

typescript 复制代码
// ====== @Builder --- 轻量复用 UI 片段 ======
/**
 * 用途: 抽取重复的 UI 结构,类似"函数版组件"
 * 特点:
 * - 定义在组件内部或全局
 * - 可以传参数
 * - 不是独立组件(不触发独立的生命周期)
 */

@Component
struct ArticleListPage {
  // ★ 方式1: 成员方法 Builder(可以访问 this)
  @Builder articleItem(title: string, summary: string) {
    Row({ space: 12 }) {
      Column() {
        Text(title).fontSize(16).fontColor('#333333').maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(summary).fontSize(13).fontColor('#999999').maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)

      Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }

  build() {
    List({ space: 8 }) {
      ListItem() {
        // 调用 Builder
        this.articleItem('节气通入门指南', '本文将带你了解二十四节气的起源与发展...')
      }
      ListItem() {
        this.articleItem('春分养生之道', '春分时节,阴阳平衡,正是调养身体的好时机...')
      }
    }.padding(12)
  }
}

// ====== @BuilderParam --- 允许外部传入 UI 片段(插槽模式)=====
@Component
struct EmptyState {
  icon: Resource = $r('sys.media.ohos_ic_public_fill');
  text: string = '暂无数据';
  // ★ 外部通过这个参数传入自定义操作按钮
  @BuilderParam actionBtn: () => void = () => {};

  build() {
    Column({ space: 16 }) {
      Image(this.icon).width(80).height(80).fillColor('#CCCCCC')
      Text(this.text).fontSize(15).fontColor('#999999')

      // 渲染外部传入的 UI
      if (this.actionBtn) {
        this.actionBtn()
      }
    }.width('100%').height(300).justifyContent(FlexAlign.Center)
  }
}

// 使用
EmptyState({
  icon: $r('app.media.empty_collection'),
  text: '还没有收藏任何内容',
  actionBtn: () => {
    Button('去探索')  // 这里就是插槽内容
      .onClick(() => router.pushUrl({ url: 'pages/MainPage' }))
  }
})

// ====== @Styles --- 抽取通用样式(纯样式,不含子组件)=====
@Styles function cardStyle() {
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .shadow({ radius: 8, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 2 })
}

// 使用
Text('卡片标题').cardStyle().padding(16)

// ====== @Extend --- 扩展特定组件的样式方法(比 @Styles 更灵活)=====
@Extend(Text) function titleText() {
  .fontSize(18)
  .fontWeight(FontWeight.Bold)
  .fontColor('#1A1A1A')
  .maxLines(1)
  .textOverflow({ overflow: TextOverflow.Ellipsis })
}

// 使用(只对 Text 生效)
Text('文章标题').titleText()
// Row().titleText()  ← 编译错误!@Extend 限定了组件类型

四、状态管理全家桶

这是 ArkTS 最核心也最容易混淆的部分。下面用一张决策图帮你选对装饰器。

4.1 状态装饰器选择指南

复制代码
需要管理状态? ─────────────────────────────────────┐
                                                     │
  数据归谁所有?                                       │
                                                     │
  ┌─ 当前组件私有 → @State                            │
  │                                                   │
  ├─ 从父组件传来(只读)→ @Prop                       │
  │                                                   │
  ├─ 从父组件传来(双向绑定)→ @Link                   │
  │                                                   │
  ├─ 祖先组件提供(跨层级单向)→ @Provide / @Consume   │
  │                                                   │
  ├─ 祖先组件提供(跨层级双向)→ @StorageLink          │
  │                                                   │
  ├─ AppStorage 全局读 → @StorageProp                 │
  │                                                   │
  ├─ AppStorage 全局读写 → @StorageLink               │
  │                                                   │
  └─ 派生计算值 → @Computed                           │
                                                     │
  性能优化提示:                                      │
  @Reusable 组件 + @Param → 减少不必要的重建          │

4.2 各装饰器详细对比与示例

typescript 复制代码
// ==========================================
// 1. @State --- 组件内部私有状态
// ==========================================
// 适用场景: 表单输入、本地开关、展开收起、加载状态
// 特点: 修改后只刷新当前组件

@Component
struct SearchBar {
  @State searchText: string = '';       // 搜索关键词
  @State isFocused: boolean = false;    // 是否聚焦
  @State historyList: string[] = [];    // 搜索历史

  build() {
    Column({ space: 8 }) {
      TextInput({ placeholder: '搜索节气、文章...' })
        .value(this.searchText)
        .onChange((value: string) => {
          this.searchText = value;      // ★ 修改 @State 自动触发 UI 更新
        })
        .onFocus(() => { this.isFocused = true })
        .onBlur(() => { this.isFocused = false })

      if (this.isFocused && this.historyList.length > 0) {
        // 历史记录列表...
      }
    }
  }
}


// ==========================================
// 2. @Prop --- 父到子单向传递(只读)
// ==========================================
// 适用场景: 列表项接收数据、展示型子组件
// 特点: 子组件不能反向修改父组件的数据
// ⚠️ 重要: @Prop 是"深拷贝",父组件重新渲染时会覆盖子组件本地修改

@Component
struct SolarTermListItem {
  @Prop name: string = '';             // 从父组件传入
  @Prop order: number = 0;
  @Prop isSelected: boolean = false;

  build() {
    Row({ space: 12 }) {
      Text(`${this.order}`).fontSize(14).fontColor('#999999')
        .width(28).height(28)
        .borderRadius(14)
        .backgroundColor(this.isSelected ? '#4A9B6D' : '#EEEEEE')
        .textAlign(TextAlign.Center)
        .fontColor(this.isSelected ? '#FFFFFF' : '#666666')

      Text(this.name).fontSize(16).flexGrow(1)
    }
    .width('100%').padding(12)
  }
}

// 父组件使用
ListItem() {
  SolarTermListItem({
    name: '立春',
    order: 1,
    isSelected: true
  })
}


// ==========================================
// 3. @Link --- 双向绑定(父子同步)
// ==========================================
// 适用场景: 表单控件、设置页开关、计数器
// 特点: 子组件修改会同步回父组件
// ⚠️ 语法: 父组件传参时加 $ 前缀: $variableName

@Component
struct ThemeToggle {
  @Link isDarkMode: boolean;          // 双向绑定

  build() {
    Row({ space: 8 }) {
      Text('深色模式').fontSize(15)
      Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
        .onChange((isOn: boolean) => {
          this.isDarkMode = isOn;     // ★ 修改会同步到父组件
        })
    }.width('100%').padding(12)
  }
}

// 父组件使用
@Entry
@Component
struct SettingsPage {
  @State isDarkMode: boolean = false;

  build() {
    Column() {
      ThemeToggle({ isDarkMode: $this.isDarkMode })  // ★ 注意 $ 符号
    }
  }
}


// ==========================================
// 4. @Provide / @Consume --- 跨后代组件传递
// ==========================================
// 适用场景: 主题色、语言设置、用户信息等深层共享数据
// 特点: 无需逐层传递,后代组件直接消费
// ⚠️ 同一个 @Provide key 在祖先链中只能有一个

// 祖先组件
@Entry
@Component
struct MainPage {
  @Provide('currentTheme') currentTheme: ThemeConfig = {
    mode: 'light',
    primaryColor: '#4A9B6D'
  };

  build() {
    // 无论嵌套多深的子组件都可以直接 Consume
    DeepNestedChild()
  }
}

// 深层后代组件(无需中间层转发)
@Component
struct DeepNestedChild {
  @Consume('currentTheme') currentTheme: ThemeConfig;

  build() {
    Text(`当前主题: ${this.currentTheme.mode}`)
      .fontColor(this.currentTheme.primaryColor)
  }
}


// ==========================================
// 5. @Watch --- 监听状态变化
// ==========================================
// 适用场景: 状态变化时需要执行副作用(持久化、联动其他状态、上报埋点)

@Component
struct SettingsPage {
  @State fontSize: number = 16;
  @State language: string = 'zh';

  // ★ 当 fontSize 变化时自动调用 onFontSizeChange
  @Watch('onFontSizeChange')
  @State fontSize: number = 16;

  onFontSizeChange(newVal: number, oldVal: number): void {
    console.info(`字体大小变更: ${oldVal} → ${newVal}`);
    // 保存到本地存储
    PreferencesHelper.set('font_size', newVal.toString());
    // 上报埋点
    AnalyticsService.trackEvent('setting_font_change', { size: newVal });
  }

  build() {
    Slider({
      min: 12,
      max: 24,
      step: 1,
      value: this.fontSize
    }).onChange((value: number) => {
      this.fontSize = Math.round(value);  // 会触发 @Watch 回调
    })
  }
}


// ==========================================
// 6. @Computed --- 计算属性
// ==========================================
// 适用场景: 从已有状态派生出新值(自动缓存)
// 特点: 依赖的状态不变时不会重新计算

@Component
struct QuizResultPage {
  @State totalQuestions: number = 20;
  @State correctAnswers: number = 16;

  // ★ 计算属性:依赖 totalQuestions 和 correctAnswers
  @Computed get score(): number {
    return Math.round((this.correctAnswers / this.totalQuestions) * 100);
  }

  @Computed get grade(): string {
    if (this.score >= 90) return '优秀';
    if (this.score >= 70) return '良好';
    if (this.score >= 60) return '及格';
    return '继续加油';
  }

  @Computed get isPassed(): boolean {
    return this.score >= 60;
  }

  build() {
    Column({ space: 16 }) {
      Text(`得分: ${this.score}分`).fontSize(32).fontWeight(FontWeight.Bold)
      Text(`评级: ${this.grade}`).fontSize(18).fontColor(
        this.isPassed ? '#4A9B6D' : '#FF4D4F'
      )
      Text(`${this.correctAnswers}/${this.totalQuestions}`)
        .fontSize(14).fontColor('#999999')
    }
  }
}


// ==========================================
// 7. @StorageProp / @StorageLink --- AppStorage 全局状态
// ==========================================
// 适用场景: 多个不相关的页面/组件需要共享同一份数据
// 特点: 类似 React Context / Vue Pinia,应用级全局存储

// 写入端(任意位置)
AppStorage.setOrCreate('appLanguage', 'zh');
AppStorage.setOrCreate('isLoggedIn', false);

// 读取端(只读)
@Component
struct LanguageLabel {
  @StorageProp('appLanguage') lang: string = 'zh';

  build() {
    Text(`当前语言: ${this.lang}`)
  }
}

// 读取+写入端(双向)
@Component
struct LoginButton {
  @StorageLink('isLoggedIn') isLoggedIn: boolean = false;

  build() {
    Button(this.isLoggedIn ? '退出登录' : '登录')
      .onClick(() => {
        this.isLoggedIn = !this.isLoggedIn;  // 所有 @StorageLink 同步更新
      })
  }
}

4.3 状态装饰器速查表

装饰器 数据方向 传递方式 深拷贝? 典型场景
@State 组件内部 --- --- 本地表单、开关、加载态
@Prop 父→子(只读) 属性传参 列表项、展示组件
@Link 父↔子(双向) $varName 设置开关、计数器
@Provide 祖先→后代 按 key 匹配 主题、语言、用户信息
@Watch 监听回调 配合其他装饰器 --- 持久化、埋点、联动
@Computed 派生值 自动计算 --- 分数、过滤后列表、格式化文本
@StorageProp AppStorage 读 按 key --- 全局配置展示
@StorageLink AppStorage 读写 按 key --- 全局登录态、主题切换

五、布局体系

5.1 五大容器对比

typescript 复制代码
@Entry
@Component
struct LayoutShowcase {

  build() {
    Scroll() {
      Column({ space: 24 }) {

        // ========== 1. Column --- 纵向线性布局 ==========
        // 最常用的布局,子元素从上到下排列
        SectionTitle('Column 纵向布局')
        Column({ space: 12 }) {
          Text('第一行').itemStyle()
          Text('第二行').itemStyle()
          Text('第三行').itemStyle()
        }
        .width('100%')
        .padding(16)
        .sectionBox()


        // ========== 2. Row --- 横向线性布局 ==========
        // 子元素从左到右排列
        SectionTitle('Row 横向布局')
        Row({ space: 12 }) {
          Text('左').itemStyle().layoutWeight(1)
          Text('中').itemStyle().layoutWeight(2)
          Text('右').itemStyle().layoutWeight(1)
        }
        .width('100%')
        .padding(16)
        .sectionBox()


        // ========== 3. Stack --- 层叠布局 ==========
        // 子元素堆叠在一起(后绘制的在上层)
        SectionTitle('Stack 层叠布局')
        Stack({ alignContent: Alignment.Center }) {
          // 底层背景
          Rectangle()
            .width(200)
            .height(120)
            .fill({ type: FillType.LinearGradient,
                    angle: 135,
                    colors: [['#4A9B6D', 0], ['#2E7D52', 1]] })

          // 中层内容
          Column({ space: 8 }) {
            Text('立春').fontSize(22).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('Spring Begins').fontSize(13).fontColor('rgba(255,255,255,0.8)')
          }

          // 右上角标签
          Text('第1个节气')
            .position({ x: 0, y: 0 })
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .backgroundColor('rgba(255,255,255,0.25)')
            .borderRadius({ topLeft: 12, bottomRight: 12 })
            .fontSize(11).fontColor('#FFFFFF')
        }
        .width(200)
        .height(120)
        .sectionBox()


        // ========== 4. Flex --- 弹性布局 ==========
        // 比 Row/Column 更灵活的对齐和换行能力
        SectionTitle('Flex 弹性布局')
        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(['立春', '雨水', '惊蛰', '春分', '清明', '谷雨'], (term: string) =>
            Text(term)
              .padding(12)
              .backgroundColor('#E8F5EE')
              .borderRadius(8)
              .fontSize(13)
          )
        }
        .width('100%')
        .padding(16)
        .sectionBox()


        // ========== 5. RelativeContainer --- 相对布局 ==========
        // 通过 ID 锚定相对位置(适合复杂叠加场景)
        SectionTitle('RelativeContainer 相对布局')
        RelativeContainer() {
          Row() { Text('左上角') }
            .id('topLeft')
            .alignRules({
              top: { anchor: '__container__', align: VerticalAlign.Top },
              left: { anchor: '__container__', align: HorizontalAlign.Start }
            })
            .padding(12)
            .backgroundColor('#FFE0B2')
            .borderRadius(8)

          Row() { Text('右下角') }
            .id('bottomRight')
            .alignRules({
              bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
              right: { anchor: '__container__', align: HorizontalAlign.End }
            })
            .padding(12)
            .backgroundColor('#B3E5FC')
            .borderRadius(8)

          Row() { Text('居中') }
            .id('center')
            .alignRules({
              center: { anchor: '__container__', align: LayoutCenter.Center }
            })
            .padding(12)
            .backgroundColor('#C8E6C9')
            .borderRadius(8)
        }
        .width('100%')
        .height(150)
        .sectionBox()

      }
      .width('100%')
      .padding(16)
    }
    .width('100%').height('100%')
    .backgroundColor('#F5F5F5')
  }
}

// 辅助组件
@Component
struct SectionTitle {
  title: string = ''
  build() {
    Text(this.title).fontSize(16).fontWeight(FontWeight.Bold).margin({ bottom: 8 })
  }
}

// 辅助样式函数
@Styles function itemStyle() {
  .padding(16)
  .backgroundColor('#FFFFFF')
  .borderRadius(8)
  .fontSize(14)
  .textAlign(TextAlign.Center)
}

@Styles function sectionBox() {
  .backgroundColor('#FAFAFA')
  .borderRadius(12)
}

5.2 布局选择决策树

复制代码
需要排列多个子组件?
│
├─ 单方向排列?
│   ├─ 纵向 → Column(最常用)
│   └─ 横向 → Row
│
├─ 需要重叠/叠加?
│   └─ Stack(如:图片+文字标签、卡片+悬浮按钮)
│
├─ 需要自动换行?
│   └─ Flex({ wrap: FlexWrap.Wrap })
│
├─ 需要精确定位(相对锚点)?
│   └─ RelativeContainer
│
└─ 需要网格/表格?
    └─ Grid(见下文列表章节)

5.3 布局性能注意事项

typescript 复制代码
// ❌ 反例: 过度嵌套导致布局计算开销增大
Column() {
  Row() {
    Column() {
      Row() {
        Column() {
          Text('太深了')  // 5 层嵌套!
        }
      }
    }
  }
}

// ✅ 正确: 尽量扁平化(不超过 3 层)
Column({ space: 8 }) {
  Text('标题')
  Row({ space: 12 }) {
    Text('左')
    Text('右')
  }
}

// ✅ 性能优化: 长列表使用 LazyForEach + @Reusable
// (详见第46篇 性能优化实战)

六、控制流

6.1 条件渲染

typescript 复制代码
@Component
struct ConditionalDemo {
  @State isLoading: boolean = true;
  @State hasData: boolean = true;
  @State error: Error | null = null;
  @State userLevel: 'vip' | 'normal' | 'guest' = 'normal';

  build() {
    Column() {

      // ====== 方式1: If / Else --- 条件增减 DOM ======
      // 适用: 大块内容的显示隐藏(如整个错误页面 vs 内容区)
      // 特点: 为 false 时节点完全不在组件树上(节省资源)
      if (this.isLoading) {
        LoadingView({ text: '加载中...' })
      } else if (this.error) {
        ErrorView({ message: this.error.message, onRetry: () => this.reloadData() })
      } else if (!this.hasData) {
        EmptyState({ text: '暂无数据' })
      } else {
        // 主内容区
        ContentArea()
      }


      // ====== 方式2: 三元表达式 --- 切换样式/小段文字 ======
      // 适用: 样式切换、短文本变化
      Text(this.hasData ? '有数据' : '无数据')
        .fontColor(this.userLevel === 'vip' ? '#FFD700' : '#333333')


      // ====== 方式3: Visibility / Display --- 控制显隐但保留节点 ======
      // 适用: 动画过渡场景(需要保留动画起始状态)
      Text('这段文字隐藏但仍占位')
        .Visibility(this.showHint ? Visibility.Visible : Visibility.Hidden)  // 占位
        // .visibility(this.showHint ? Visibility.Visible : Visibility.None)  // 不占位

    }
  }

  private reloadData(): void {
    this.isLoading = true;
    this.error = null;
    // 重新请求数据...
  }
}

6.2 循环渲染

typescript 复制代码
@Component
struct LoopDemo {

  // ====== 基础 ForEach ======
  @State terms: SolarTermSummary[] = [
    { id: '1', name: '立春', season: '春' },
    { id: '2', name: '雨水', season: '春' },
    // ...
  ];

  build() {
    Column() {

      // ★ ForEach 三参数: (数组, item生成函数, key生成函数)
      // ⚠️ keyGenerator 非常重要!决定虚拟DOM diff效率
      List({ space: 8 }) {
        ForEach(
          this.terms,                                    // 数据源
          (item: SolarTermSummary, index: number) => {    // item 生成函数
            ListItem() {
              TermRow({ term: item, index: index })
            }
          },
          (item: SolarTermSummary) => item.id             // ★ key 生成函数(必须稳定且唯一)
        )
      }


      // ====== ForEach 常见错误 ======
      // ❌ 错误1: 用 index 做 key(列表增删时会导致错误的复用)
      ForEach(items, (item, i) => ItemComp(), (item, i) => i.toString())

      // ❌ 错误2: key 不唯一
      ForEach(items, (item) => ItemComp(), (item) => item.name)  // 名字可能重复!

      // ✅ 正确: 用业务唯一标识做 key
      ForEach(items, (item) => ItemComp(), (item) => item.id)


      // ====== ForEach 与 LazyForEach 的选择 ======
      // 数据量 < 50 → ForEach(简单够用)
      // 数据量 > 50 或无限滚动 → LazyForEach + DataSource(性能关键)
    }
  }
}

七、组件生命周期

7.1 生命周期流程图

复制代码
组件创建
  │
  ▼
aboutToAppear()  ──── ★ 初始化操作:获取路由参数、请求初始数据
  │
  ▼
build() 首次执行    ──── 构建 UI 树
  │
  ▼
[ 组件显示在屏幕上 ]
  │
  ├── @State 变更 ──→ build() 重新执行(只更新变化的节点)
  │
  ├── 父组件重建 ──→ aboutToReuse()? → 如果是 @Reusable 组件
  │
  └── 组件即将销毁
       │
       ▼
  aboutToDisappear() ── ★ 清理操作:取消订阅、释放资源、停止定时器
       │
       ▼
  [ 组件从组件树移除 ]

7.2 生命周期代码模板

typescript 复制代码
@Component
struct LifecycleDemo {
  @State data: Article[] = [];
  @State page: number = 1;
  private timer: number = -1;
  private dataChangeListener: (() => void) | null = null;

  // ★ aboutToAppear: 组件即将显示时调用(每次挂载都会调用)
  aboutToAppear(): void {
    console.info('[LifecycleDemo] aboutToAppear');

    // 1. 获取路由参数
    const params = router.getParams() as Record<string, Object>;
    const categoryId = params?.['categoryId'] as string ?? '';

    // 2. 加载初始数据
    this.loadData(categoryId);

    // 3. 订阅全局事件
    this.dataChangeListener = EventBus.on('data_refreshed', () => {
      this.loadData(categoryId);  // 刷新数据
    });

    // 4. 启动定时器(如轮询)
    this.timer = setInterval(() => {
      this.checkUpdates();
    }, 30_000);  // 30秒一次
  }

  // ★ aboutToDisappear: 组件即将销毁时调用
  aboutToDisappear(): void {
    console.info('[LifecycleDemo] aboutToDisappear');

    // 1. 清除定时器(防止内存泄漏!)
    if (this.timer !== -1) {
      clearInterval(this.timer);
      this.timer = -1;
    }

    // 2. 取消事件订阅
    if (this.dataChangeListener) {
      EventBus.off('data_refreshed', this.dataChangeListener);
      this.dataChangeListener = null;
    }

    // 3. 取消网络请求(如果有正在进行的)
    HttpManager.cancelPendingRequests();

    // 4. 释放原生资源(如 PixelMap)
    // pixelMap?.release();  // 见性能优化篇
  }

  // ★ onPageShow / onPageHide: 页面级生命周期(仅在 @Entry 组件生效)
  onPageShow(): void {
    console.info('[LifecycleDemo] 页面显示');
    // 适用于: 从后台切回前台时刷新数据
    this.checkUpdates();
  }

  onPageHide(): void {
    console.info('[LifecycleDemo] 页面隐藏');
    // 适用于: 暂停耗时操作以省电
  }

  async loadData(categoryId: string): Promise<void> {
    try {
      this.data = await ArticleService.getListByCategory(categoryId, this.page);
    } catch (e) {
      Logger.error('LifecycleDemo', '数据加载失败', e);
    }
  }

  private checkUpdates(): void {
    // 轮询检查是否有新内容
  }

  build() {
    // UI 构建...
  }
}

7.3 PageAbility 生命周期(入口 Ability 级别)

typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
import { preferences } from '@kit.ArkData';

export default class EntryAbility extends UIAbility {
  // ★ 应用冷启动时调用
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'EntryAbility', '%{public}s', 'Ability onCreate');

    // 初始化全局服务
    DistributedManager.getInstance().initialize('com.example.jieqitong');
    ThemeManager.initFromStorage();
  }

  // ★ 窗口创建完成时调用(在此设置导航栏等)
  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(0x0000, 'EntryAbility', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/SplashPage', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'EntryAbility', 'Failed to load. Cause: %{public}s', JSON.stringify(err));
        return;
      }
    });

    // 隐藏导航栏(全屏沉浸式体验)
    windowStage.getMainWindow((err, mainWindow) => {
      if (!err && mainWindow) {
        mainWindow.setWindowSystemBarProperties({
          navigationBarColor: '#00000000',  // 导航栏透明
          navigationBarContentColor: '#FFFFFF',
          statusBarColor: '#00000000',       // 状态栏透明
          statusBarContentColor: '#FFFFFF'
        });
        mainWindow.setFullScreen(true);       // 全屏
      }
    });
  }

  // ★ 窗口销毁时
  onWindowStageDestroy(): void {
    hilog.info(0x0000, 'EntryAbility', '%{public}s', 'Ability onWindowStageDestroy');
  }

  // ★ 应用从前台切到后台
  onBackground(): void {
    hilog.info(0x0000, 'EntryAbility', '%{public}s', 'Ability onBackground');
    // 保存未持久化的数据
    // 暂停定位、蓝牙等耗电操作
  }

  // ★ 应用从后台切回前台
  onForeground(): void {
    hilog.info(0x0000, 'EntryAbility', '%{public}s', 'Ability onForeground');
    // 检查是否有新通知需要展示
  }

  // ★ 应用销毁时(用户强制关闭)
  onDestroy(): void {
    hilog.info(0x0000, 'EntryAbility', '%{public}s', 'Ability onDestroy');
    // 释放全局资源
    // 上报退出日志
  }
}

7.4 两个生命周期的关系

复制代码
时间轴 →

App 启动
  │
  ▼
EntryAbility.onCreate()          ← Ability 级别(整个应用只一次)
  │
  ▼
EntryAbility.onWindowStageCreate()
  │
  ▼
windowStage.loadContent('SplashPage')
  │
  ▼
SplashPage.aboutToAppear()        ← 组件级别(每次进入页面都调用)
  │
  ▼
SplashPage.build()
  │
  ▼
[ 用户在 SplashPage 操作 ]
  │
  ▼
router.pushUrl({ url: 'MainPage' })
  │
  ▼
SplashPage.aboutToDisappear()     ← SplashPage 销毁
  MainPage.aboutToAppear()         ← MainPage 创建
  │
  ▼
[ 用户按 Home 键 ]
  │
  ▼
MainPage.onPageHide()             ← 页面隐藏
EntryAbility.onBackground()        ← 应用进入后台
  │
  ▼
[ 用户从任务栏切回 ]
  │
  ▼
EntryAbility.onForeground()        ← 应用回到前台
MainPage.onPageShow()              ← 页面重新显示

八、「节气通」中的常用代码模式

8.1 标准页面模板

typescript 复制代码
/**
 * 「节气通」标准页面模板
 * 每个 Page 都遵循此结构以保证一致性
 */
@Entry
@Component
struct StandardPageTemplate {
  // ====== 状态 ======
  @State isLoading: boolean = true;
  @State dataList: DataItem[] = [];
  @State errorMsg: string = '';
  private page: number = 1;
  private pageSize: number = 20;

  // ====== 生命周期 ======
  aboutToAppear(): void {
    this.fetchData();
  }

  aboutToDisappear(): void {
    // 清理
  }

  // ====== 数据加载 ======
  async fetchData(refresh: boolean = false): Promise<void> {
    try {
      this.isLoading = true;
      this.errorMsg = '';

      const result = await SomeService.getList({
        page: refresh ? 1 : this.page,
        pageSize: this.pageSize
      });

      if (refresh) {
        this.dataList = result.items;
      } else {
        this.dataList = [...this.dataList, ...result.items];
      }
    } catch (e) {
      this.errorMsg = e.message || '加载失败,请重试';
      Logger.error('StandardPage', 'fetchData failed', e);
    } finally {
      this.isLoading = false;
    }
  }

  // 下拉刷新
  onRefresh(): void {
    this.fetchData(true);
  }

  // 上拉加载更多
  onLoadMore(): void {
    this.page++;
    this.fetchData();
  }

  // ====== UI 构建 ======
  build() {
    Column() {
      // 顶部导航栏
      this.buildHeader()

      // 内容区域
      if (this.isLoading && this.dataList.length === 0) {
        LoadingView({ text: '加载中...' })
      } else if (this.errorMsg) {
        ErrorView({
          message: this.errorMsg,
          onRetry: () => this.onRefresh()
        })
      } else if (this.dataList.length === 0) {
        EmptyState({
          text: '暂无数据',
          actionBtn: () => Button('重试').onClick(() => this.onRefresh())
        })
      } else {
        this.buildContent()
      }
    }
    .width('100%').height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder buildHeader() {
    // 自定义头部实现
  }

  @Builder buildContent() {
    // 列表/网格内容实现
    RefreshList({
      data: this.dataList,
      onRefresh: () => this.onRefresh(),
      onLoadMore: () => this.onLoadMore(),
      renderItem: (item: DataItem, index: number) => {
        // 列表项
      }
    })
  }
}

8.2 异步操作的统一处理

typescript 复制代码
/**
 * 「节气通」异步操作标准封装
 * 解决: try/catch 重复、loading 状态管理分散的问题
 */
class AsyncTask {
  /**
   * 包装异步操作,统一处理 loading/error/空状态
   */
  static async execute<T>(
    setLoading: (v: boolean) => void,
    setData: (data: T[]) => void,
    setError: (msg: string) => void,
    operation: () => Promise<T[]>,
    append: boolean = false
  ): Promise<void> {
    try {
      setLoading(true);
      setError('');
      const result = await operation();
      setData(append ? (prev: T[]) => [...prev, ...result] : result);
    } catch (e: any) {
      setError(e?.message || '操作失败');
      Logger.error('AsyncTask', 'execute failed', e);
    } finally {
      setLoading(false);
    }
  }
}

// 使用方式简化为:
await AsyncTask.execute(
  (v) => { this.isLoading = v; },
  (data) => { this.dataList = data; },
  (msg) => { this.errorMsg = msg; },
  () => ArticleService.getList({ page: this.page })
);

九、常见踩坑与避坑指南

9.1 十大高频坑位

# 坑位 症状 解决方案
1 @State 直接修改数组/对象内部属性 UI 不更新 必须整体替换:this.list = [...this.list, newItem]splice 后重新赋值
2 ForEach 缺少 keyGenerator 列表闪烁/顺序错乱 始终提供稳定的唯一 key
3 @Prop 以为能双向传值 子组件修改无效 改用 @Link + $varName
4 aboutToForget 中忘记清理定时器/监听 内存泄漏 检查清单:setInterval/clearInterval、eventBus.on/off、subscribe/unsubscribe
5 build() 里写业务逻辑(如 API 调用) 每次状态变更都重复调用 业务逻辑放 aboutToAppear 或事件回调中,build() 只做 UI 描述
6 @StorageLink / @Provide key 拼写错误 静默失效,拿不到值 key 是字符串,用常量文件统一定义,不要手写字符串
7 Row/Column 不设 width/height 导致子组件挤压 布局异常 容器组件显式设置尺寸约束
8 Scroll 嵌套 List/Grid 滑动冲突 避免嵌套滚动容器,或使用 nestedScroll
9 在 @Builder 函数中使用 @State 无法正确响应 Builder 内如需状态,应通过参数传入而非直接访问成员变量
10 any 类型滥用 编译通过但运行时崩溃 用具体类型或 unknown + 类型守卫替代

9.2 典型错误代码对照

typescript 复制代码
// ❌ 错误: 直接修改 @State 数组元素
@State items: Item[] = [{ id: '1', name: 'A' }];
someMethod() {
  this.items[0].name = 'B';  // UI 不会更新!
}

// ✅ 正确: 整体替换触发更新
someMethod() {
  const newItems = [...this.items];
  newItems[0] = { ...newItems[0], name: 'B' };
  this.items = newItems;  // 触发 UI 更新
}

// 或者使用 splice(返回新数组引用)
someMethod() {
  this.items.splice(0, 1, { ...this.items[0], name: 'B' });
  this.items = [...this.items];  // 需要重新赋值
}


// ❌ 错误: build() 中发起网络请求
build() {
  Column() {
    if (this.loading) {
      LoadingView()
    }
    // 这里的 fetch 会在每次 rebuild 时被调用!
    this.fetchData()  // 危险!
  }
}

// ✅ 正确: 在生命周期或事件回调中请求
aboutToAppear() {
  this.fetchData();  // 只调用一次
}

async fetchData(): Promise<void> {
  this.loading = true;
  try {
    this.data = await Api.getData();
  } finally {
    this.loading = false;  // 修改 State 触发 rebuild
  }
}

本章小结

核心知识点速查

1. 类型系统

  • 基础类型、接口、类型别名、泛型、工具类型(Partial/Pick/Omit)
  • 避免使用 any,优先用具体类型或 unknown

2. 装饰器体系

  • @Entry + @Component = 页面组件
  • @Component = 普通组件
  • @Builder = UI 片段复用
  • @Styles / @Extend = 样式复用
  • @CustomDialog = 弹窗

3. 状态管理(按频率排序)

  • @State > @Prop > @Link > @Provide/@Consume > @Watch > @Computed > @Storage*

4. 布局

  • Column/Row(日常首选)、Stack(叠加)、Flex(换行)、RelativeContainer(精确定位)、Grid(网格)

5. 生命周期

  • aboutToAppear → 初始化 / aboutToDisappear → 清理(最重要的一对)
  • Ability 级别只在 EntryAbility 中处理

相关链接

相关推荐
小雨下雨的雨2 小时前
HarmonyOS ArkUI训练营入门-组件掌握系列-Animation 动画效果实现-PC版本
学习·华为·harmonyos·鸿蒙
2601_962072554 小时前
李梦娇常识4600问|题库|打印版
sql·华为od·华为·c#·华为云·.net·harmonyos
伶俜664 小时前
鸿蒙原生应用实战(十九)ArkUI 喝水提醒 App:定时通知 + 每日记录 + 统计图表
华为·harmonyos
风华圆舞4 小时前
Flutter + 鸿蒙 Intents Kit:页面直达能力的完整接入方案
flutter·ui·华为·harmonyos
三声三视5 小时前
Electron 在鸿蒙 PC 上跑 webview,我是怎么把首屏从 4.2s 干到 1.1s 的
华为·electron·harmonyos·鸿蒙
互联网散修6 小时前
鸿蒙实战:从0到1构建功能完备的搜索页面
华为·harmonyos
花椒技术6 小时前
RN 多包热更新实践:更新校验、运行时加载与 Bridge 缓存治理
react native·react.js·harmonyos
不喝水就会渴6 小时前
【共创季稿事节】HarmonyOS 7.0 时代的新基建 :DevEco CLI + Claude Code,鸿蒙 AI 开发的黄金搭档
人工智能·华为·harmonyos
星释7 小时前
鸿蒙智能体开发实战:2.创建单Agent
harmonyos·智能体