Skip to content

自定义组件

@Component + build() 是 ArkTS 组件化的基础,理解其生命周期、状态、传参是掌握 ArkUI 的关键。


1. 自定义组件基本结构

typescript
@Component
struct MyComponent {
    // 1. 状态变量
    @State title: string = '标题'
    @State count: number = 0

    // 2. 生命周期
    aboutToAppear() {
        // 组件创建后、build 前执行(只执行一次)
        console.log('组件创建')
    }

    aboutToDisappear() {
        // 组件销毁前执行
        console.log('组件销毁')
    }

    // 3. 构建函数
    build() {
        Column() {
            Text(this.title)
                .fontSize(24fp)
            Button('点击')
                .onClick(() => {
                    this.count++
                })
            Text(`计数: ${this.count}`)
        }
        .width('100%')
        .height('100%')
    }
}

2. 组件的生命周期

2.1 组件级生命周期

阶段函数触发时机执行次数
创建后aboutToAppear()组件创建完成,build 之前1次
渲染中build()组件首次渲染或状态变化时N次
销毁前aboutToDisappear()组件被移除前1次
typescript
@Component
struct LifecycleDemo {
    aboutToAppear() {
        // ✅ 可以做数据初始化
        console.log('aboutToAppear: 组件即将渲染')
        // ❌ 不能在 build 之前修改 @State 触发渲染
    }

    aboutToDisappear() {
        // ✅ 清理资源
        console.log('aboutToDisappear: 组件即将销毁')
    }

    build() {
        Text('生命周期演示')
    }
}

2.2 页面级生命周期(@Entry 组件特有)

阶段函数触发时机
页面显示onPageShow()页面进入前台
页面隐藏onPageHide()页面离开前台
返回onBackPress()用户点击返回
typescript
@Entry
@Component
struct Index {
    onPageShow() {
        console.log('页面显示,刷新数据')
    }

    onPageHide() {
        console.log('页面隐藏,暂停任务')
    }

    onBackPress() {
        console.log('返回按钮被点击')
        return false  // false = 不拦截,true = 拦截返回
    }

    build() {
        Column() {
            Text('首页')
        }
    }
}

3. 组件的嵌套

3.1 嵌套结构

typescript
// 父组件
@Component
struct Parent {
    @State message: string = 'Hello from Parent'

    build() {
        Column() {
            Text('父组件')
                .fontSize(28fp)
                .fontWeight(FontWeight.Bold)
                .margin({ bottom: 16 })
            Child({ message: this.message })
        }
        .width('100%')
        .height('100%')
        .padding(16)
    }
}

// 子组件
@Component
struct Child {
    @Prop message: string = ''

    build() {
        Column() {
            Text('子组件收到: ' + this.message)
                .fontSize(18fp)
            GrandChild()
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.FromRGB(0xF5, 0xF5, 0xF5))
        .borderRadius(8)
    }
}

// 孙子组件
@Component
struct GrandChild {
    build() {
        Text('孙子组件')
            .fontSize(14fp)
            .fontColor(Color.Gray)
            .margin({ top: 8 })
    }
}

3.2 组件嵌套的最佳实践

✅ 推荐:
├─ 嵌套不超过 3-4 层
├─ 每层职责单一
├─ 提取复杂逻辑为子组件
├─ 使用 @Builder 提取 UI 片段

❌ 避免:
├─ 一个组件超过 100 行
├─ 嵌套超过 4 层
├─ 组件职责混杂(既做 UI 又做逻辑)
└─ 在 build() 中写业务逻辑

4. 组件的传参

4.1 @Prop — 父传子(单向)

typescript
// 子组件
@Component
struct UserInfo {
    @Prop name: string = '未知'
    @Prop age: number = 0
    @Prop avatar: Resource = $r('app.media.default_avatar')

    build() {
        Row() {
            Image(this.avatar).width(48).height(48).borderRadius(24)
            Column() {
                Text(this.name).fontSize(16fp)
                Text(`${this.age}岁`).fontSize(14fp).fontColor(Color.Gray)
            }.margin({ left: 12 })
        }
    }
}

// 父组件
@Entry
@Component
struct Index {
    @State user: { name: string; age: number; avatar: Resource } = {
        name: '小明',
        age: 25,
        avatar: $r('app.media.avatar')
    }

    build() {
        Column() {
            UserInfo({
                name: this.user.name,
                age: this.user.age,
                avatar: this.user.avatar
            })
        }
    }
}
typescript
// 子组件
@Component
struct NameInput {
    @Link name: string  // 双向绑定

    build() {
        TextInput({ placeholder: '请输入名字' })
            .onChange((value: string) => {
                this.name = value  // 修改子组件 → 自动同步到父组件
            })
    }
}

// 父组件
@Entry
@Component
struct Index {
    @Link pageName: string = '默认名称'  // @Link 必须是 @State 或 @Link

    build() {
        Column() {
            Text(`当前名字: ${this.pageName}`)
            NameInput({ name: this.pageName })  // 双向绑定
        }
    }
}
场景推荐原因
组件内部管理数据@State私有状态
父→子单向传递@Prop单向拷贝
父↔子双向同步@Link双向引用
跨树共享@Provide/@Consume爷孙通信

5. @Builder 提取 UI 片段

5.1 基本用法

typescript
@Component
struct OrderCard {
    @State orderNo: string = 'ORD001'
    @State amount: number = 99.9
    @State status: string = '已完成'

    // 提取订单头部
    @Builder
    renderOrderHeader() {
        Row() {
            Text('订单号: ' + this.orderNo)
                .fontSize(14fp)
                .fontColor(Color.Gray)
            Blank()
            Text(this.status)
                .fontSize(14fp)
                .fontColor(Color.Green)
                .backgroundColor(Color.FromRGBA(0, 0x80, 0, 0.1))
                .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                .borderRadius(4)
        }
        .width('100%')
    }

    build() {
        Column() {
            this.renderOrderHeader()
            Divider().margin({ top: 8, bottom: 8 })
            Row() {
                Text('金额: ')
                Text(`¥${this.amount.toFixed(2)}`)
                    .fontSize(20fp)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(Color.Red)
            }
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(8)
    }
}

5.2 @Builder 带参

typescript
@Component
struct ListCard {
    @State items: Array<{ name: string; price: number }> = []

    // 带参数的 @Builder
    @Builder
    renderItem(item: { name: string; price: number }) {
        Row() {
            Text(item.name)
                .fontSize(16fp)
            Blank()
            Text(`¥${item.price}`)
                .fontSize(16fp)
                .fontColor(Color.Red)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
    }

    build() {
        Column() {
            ForEach(this.items, (item: any) => {
                this.renderItem(item)
                Divider()
            })
        }
    }
}

6. 组件化最佳实践

6.1 单一职责原则

✅ 好的组件:
├─ ComponentA.ts — 只做头部导航
├─ ComponentB.ts — 只做用户信息展示
├─ ComponentC.ts — 只做列表项

❌ 不好的组件:
└─ OneBigComponent.ts — 导航 + 用户 + 列表 + 弹窗(>500行)

6.2 组件复用

typescript
// 复用组件:抽取为公共组件
// common/components/ActionButton.ets
@Component
export struct ActionButton {
    @Prop text: string = ''
    @Prop icon: Resource = $r('app.media.default')
    @Prop onClick: () => void = () => {}

    build() {
        Row() {
            Image(this.icon).width(20).height(20)
            Text(this.text).fontSize(16fp).margin({ left: 8 })
        }
        .padding({ left: 16, right: 16, top: 10, bottom: 10 })
        .backgroundColor(Color.White)
        .borderRadius(8)
        .onClick(this.onClick)
    }
}
typescript
// 使用公共组件
import { ActionButton } from '../../common/components/ActionButton'

@Entry
@Component
struct Index {
    build() {
        Column() {
            ActionButton({
                text: '拍照',
                icon: $r('app.media.camera'),
                onClick: () => this.takePhoto()
            })
            ActionButton({
                text: '相册',
                icon: $r('app.media.gallery'),
                onClick: () => this.openGallery()
            })
        }
    }
}

7. 面试高频考点

Q1: 自定义组件的生命周期?

回答:组件级有 aboutToAppear(创建后)、build(渲染)、aboutToDisappear(销毁前)。页面级额外有 onPageShowonPageHideonBackPress

Q2: build() 函数内有什么限制?

回答:只能写 UI 描述代码,不能写 console.log、不能定义局部变量、不能直接调用非 @Builder 装饰的函数。build 是纯 UI 描述。

Q3: 组件化有什么好处?

回答:代码复用、职责单一、易于维护、团队分工、可测试性、降低耦合。


🐱 小猫提示:自定义组件记住三件事:生命周期(aboutToAppear/build/aboutToDisappear)、传参(@State/@Prop/@Link)、@Builder 复用。面试必考。