
引言
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 中处理