@Entry 与 @Component 装饰器:组件声明、导出与页面入口

前言

在 ArkUI 中,装饰器 是声明式 UI 的核心语法。@Entry@Component 是最基础也是最重要的两个装饰器,它们分别用于声明页面入口和自定义组件。

"海风日记"共有 37 个 .ets 源文件 ,每个文件都使用了 @Component@Entry 装饰器。本文将从源码出发,深入讲解这两个装饰器的语法、机制、生命周期以及最佳实践,帮助你全面掌握 ArkUI 的组件化开发。


一、装饰器概述

1.1 什么是装饰器

装饰器是 ArkTS 语言提供的一种声明式语法,用于为类或类成员添加元数据和行为。在 ArkUI 中,装饰器主要用于:

  1. 声明组件@Component 将一个类标记为 UI 组件
  2. 声明页面入口@Entry 将一个组件标记为独立页面
  3. 状态管理@State@Prop@Link 等管理组件状态
  4. 构建函数@Builder 封装 UI 构建逻辑

1.2 @Entry 与 @Component 的关系

装饰器 作用 是否必须 使用场景
@Component 声明这是一个 UI 组件 所有组件必须使用
@Entry 声明这是一个页面入口 否(仅页面需要) 注册在 main_pages.json 中的页面
typescript 复制代码
// 页面组件:同时使用 @Entry 和 @Component
@Entry
@Component
struct SplashPage { ... }

// 普通组件:只使用 @Component
@Component
export struct DiaryCard { ... }

1.3 海风日记中的组件统计

类型 装饰器 数量 示例
页面组件 @Entry @Component 26 个 SplashPage, Index.ets
通用组件 @Component + export 4 个 DiaryCard, MoodSelector
内部组件 @Component(不导出) 7 个 ReviewTab 内的统计卡片

二、@Component 装饰器详解

2.1 基本语法

typescript 复制代码
@Component
struct ComponentName {
  // 状态变量
  @State message: string = 'Hello'

  // 构建方法
  build() {
    // 返回 UI 描述
    Column() {
      Text(this.message)
    }
  }
}

2.2 @Component 的规则

  1. 必须使用 struct@Component 只能装饰 struct 类型
  2. 必须包含 build() 方法build() 返回组件的 UI 描述
  3. build() 必须有且只有一个根节点:根节点可以是 Column、Row、Stack 等容器
  4. 不支持继承@Component 修饰的 struct 不能继承其他类

2.3 可导出与不可导出

typescript 复制代码
// 可导出的组件(供其他文件 import 使用)
@Component
export struct DiaryCard {
  // 属性声明
  moodColor: MoodColor = 'default'
  content: string = ''

  build() {
    Row() { ... }
  }
}

// 不导出的组件(仅在本文件内使用)
@Component
struct InnerComponent {
  build() {
    Column() { ... }
  }
}

2.4 组件属性

@Component 中的非 @State 属性可以通过构造函数参数传递:

typescript 复制代码
@Component
export struct DiaryCard {
  // 这些属性可以通过父组件传入
  moodColor: MoodColor = 'default'
  timeStr: string = ''
  content: string = ''
  tags: string[] = []
  isPinned: boolean = false
  hasImage: boolean = false
  moodEmoji: string = ''
  onTap: () => void = () => {}  // 函数类型属性

  build() {
    Row() {
      // 使用属性构建 UI
      Text(this.content)
    }
    .onClick(() => { this.onTap() })
  }
}

// 在父组件中使用
DiaryCard({
  moodColor: getMoodColor(item.mood),
  timeStr: '今天 · 06:12',
  content: item.content,
  tags: item.tags,
  isPinned: item.isPinned,
  hasImage: item.imageUrls.length > 0,
  moodEmoji: getMoodEmoji(item.mood),
  onTap: () => { router.pushUrl({ url: 'pages/diary/DiaryDetailPage' }) }
})

三、@Entry 装饰器详解

3.1 基本语法

typescript 复制代码
@Entry
@Component
struct SplashPage {
  build() {
    Stack() {
      // 页面内容
    }
    .width('100%')
    .height('100%')
  }
}

3.2 @Entry 的作用

@Entry 做了以下事情:

  1. 注册页面:将该组件注册为可被路由跳转的页面
  2. 提供生命周期 :添加 aboutToAppear()aboutToDisappear()onPageShow()onPageHide() 等生命周期回调
  3. 绑定路由参数 :可以通过 router.getParams() 获取路由参数

3.3 @Entry 的生命周期

typescript 复制代码
@Entry
@Component
struct DiaryDetailPage {
  aboutToAppear() {
    // 1. 页面即将显示时调用
    // 适合:获取路由参数、初始化数据
    const params = router.getParams() as Record<string, string>
    if (params?.diaryId) {
      this.diaryId = params.diaryId
    }
  }

  onPageShow() {
    // 2. 页面显示时调用(每次回到该页面都会触发)
    // 适合:刷新数据、恢复状态
    this.refreshData()
  }

  onPageHide() {
    // 3. 页面隐藏时调用
    // 适合:暂停动画、保存草稿
  }

  aboutToDisappear() {
    // 4. 页面即将销毁时调用
    // 适合:清理资源、取消订阅
    this.cleanup()
  }
}

3.4 生命周期执行顺序

页面启动时的生命周期顺序:

复制代码
1. aboutToAppear()     ← 页面即将显示
2. build()             ← 构建 UI
3. onPageShow()        ← 页面已显示

页面跳转到其他页面的生命周期顺序:

复制代码
1. onPageHide()        ← 当前页面隐藏
2. aboutToAppear()     ← 新页面即将显示
3. build()             ← 新页面构建
4. onPageShow()        ← 新页面已显示

四、组件导出与导入

4.1 export 导出组件

typescript 复制代码
// DiaryCard.ets
@Component
export struct DiaryCard {
  // 组件定义
}

// MoodSelector.ets
@Component
export struct MoodSelector {
  // 组件定义
}

// WaveHeaderBand.ets
@Component
export struct WaveHeaderBand {
  // 组件定义
}

4.2 import 导入组件

typescript 复制代码
// HomeTab.ets --- 导入组件
import { DiaryCard } from '../../common/components/DiaryCard'
import { WaveHeaderBand } from '../../common/components/WaveHeaderBand'

// CalendarTab.ets --- 导入数据和模型
import { MOCK_DIARIES } from '../../common/model/DiaryModel'
import { COLOR_PRIMARY } from '../../common/constants/Colors'

// Index.ets --- 导入子页面组件
import { HomeTab } from './home/HomeTab'
import { CalendarTab } from './calendar/CalendarTab'
import { ReviewTab } from './review/ReviewTab'
import { MineTab } from './mine/MineTab'

4.3 导入路径的规则

复制代码
当前文件: pages/home/HomeTab.ets
导入组件: ../../common/components/DiaryCard.ets
          ↑↑ 从当前目录上溯两级到 common
路径 说明
./ 当前目录
../ 上一级目录
../../ 上两级目录
common/components/ 通用组件目录
common/model/ 数据模型目录
common/constants/ 常量目录

五、@Component 中的状态管理

5.1 @State 装饰器

@State 标记的变量发生变化时,组件会自动重新渲染:

typescript 复制代码
@Component
export struct HomeTab {
  @State isGridView: boolean = true
  @State diaryList: DiaryEntry[] = MOCK_DIARIES

  build() {
    Column() {
      // 切换视图
      Button()
        .onClick(() => { this.isGridView = !this.isGridView })

      // 根据 isGridView 状态渲染不同布局
      if (this.isGridView) {
        Grid() { ... }    // 双栏网格
      } else {
        List() { ... }    // 单栏列表
      }
    }
  }
}

@Link 实现父子组件之间的双向绑定:

typescript 复制代码
// 父组件
@Component
export struct WriteDiaryPage {
  @State selectedMood: number = 3
  @State selectedWeather: number = 0

  build() {
    MoodSelector({
      selectedMood: this.selectedMood,      // 传入 @State
      selectedWeather: this.selectedWeather  // 传入 @State
    })
  }
}

// 子组件
@Component
export struct MoodSelector {
  @Link selectedMood: number      // @Link 双向绑定
  @Link selectedWeather: number   // @Link 双向绑定

  build() {
    Column() {
      // 修改 @Link 变量会同步到父组件的 @State
      Text('😊')
        .onClick(() => { this.selectedMood = 4 })
    }
  }
}
装饰器 数据流向 声明位置 修改权限
@State 内部状态 组件内部 仅本组件可修改
@Prop 父→子(单向) 子组件 子组件可修改,但不会同步回父组件
@Link 双向绑定 子组件 子组件修改会同步回父组件

5.4 状态管理的最佳实践

typescript 复制代码
// 1. 状态尽量定义在需要修改的组件中
@State currentIndex: number = 0  // Index 中定义,Index 修改

// 2. 子组件通过 @Link 获取修改能力
@Link selectedMood: number  // MoodSelector 中修改,同步到父组件

// 3. 纯展示数据通过普通属性传递
content: string = ''  // DiaryCard 中仅展示,不修改

六、组件设计模式

6.1 容器组件模式

容器组件负责布局和状态管理,子组件负责具体展示:

typescript 复制代码
// Index.ets --- 容器组件
@Entry
@Component
struct Index {
  @State currentIndex: number = 0

  build() {
    Tabs() {
      TabContent() { HomeTab() }      // 子组件
      TabContent() { CalendarTab() }  // 子组件
      TabContent() { ReviewTab() }    // 子组件
      TabContent() { MineTab() }      // 子组件
    }
  }
}

6.2 展示组件模式

展示组件专注于 UI 呈现,不管理业务状态:

typescript 复制代码
// DiaryCard.ets --- 纯展示组件
@Component
export struct DiaryCard {
  // 所有属性由父组件传入
  moodColor: MoodColor = 'default'
  timeStr: string = ''
  content: string = ''
  tags: string[] = []
  isPinned: boolean = false
  hasImage: boolean = false
  moodEmoji: string = ''
  onTap: () => void = () => {}

  build() {
    // 仅 UI 展示,不包含业务逻辑
  }
}

6.3 组件粒度控制

粒度 特点 适用场景 示例
粗粒度 代码集中,耦合度高 简单页面 FaqPage
中粒度 适度拆分,可复用 通用功能 DiaryCard
细粒度 高度复用,代码分散 多处使用 MoodSelector

七、组件生命周期详解

7.1 页面组件的完整生命周期

typescript 复制代码
@Entry
@Component
struct ExamplePage {
  // 1. 构造函数
  constructor() {
    console.info('constructor')
  }

  // 2. 状态初始化
  @State count: number = 0

  // 3. aboutToAppear(页面即将显示)
  aboutToAppear() {
    console.info('aboutToAppear')
  }

  // 4. build(构建 UI)
  build() {
    Column() {
      Text(`${this.count}`)
    }
    .onClick(() => { this.count++ })
  }

  // 5. onPageShow(页面已显示)
  onPageShow() {
    console.info('onPageShow')
  }

  // 6. onPageHide(页面隐藏)
  onPageHide() {
    console.info('onPageHide')
  }

  // 7. aboutToDisappear(页面即将销毁)
  aboutToDisappear() {
    console.info('aboutToDisappear')
  }
}

7.2 非页面组件的生命周期

@Entry 组件只有 aboutToAppearaboutToDisappear

typescript 复制代码
@Component
export struct DiaryCard {
  aboutToAppear() {
    // 组件即将显示
  }

  aboutToDisappear() {
    // 组件即将销毁
  }
}

7.3 生命周期使用场景

生命周期回调 典型用途
aboutToAppear 获取路由参数、初始化数据、发起网络请求
onPageShow 每次页面显示时刷新数据
onPageHide 暂停动画、保存草稿、释放临时资源
aboutToDisappear 清理定时器、取消订阅、释放内存

八、常见问题与最佳实践

8.1 组件未注册

问题:使用 @Component 装饰的组件在页面中不显示。

原因 :组件未使用 export 导出,或在父组件中未正确 import

解决方案

typescript 复制代码
// 1. 导出组件
@Component
export struct MyComponent { ... }

// 2. 导入组件
import { MyComponent } from './path/to/MyComponent'

8.2 build() 方法缺少根节点

问题:编译报错,提示 build() 必须包含一个根节点。

解决方案:使用容器组件包裹多个子元素:

typescript 复制代码
build() {
  // 错误:多个子组件没有根节点
  // Text('Hello')
  // Text('World')

  // 正确:使用 Column 作为根节点
  Column() {
    Text('Hello')
    Text('World')
  }
}

8.3 @State 更新不触发 UI 刷新

问题:修改了 @State 变量,但 UI 没有更新。

原因:直接修改了数组或对象的内部属性,未触发引用变化。

解决方案:创建新的数组或对象引用:

typescript 复制代码
// 错误:直接修改数组内部
this.tags.splice(idx, 1)  // UI 不会刷新

// 正确:创建新数组
this.tags = [...this.tags]  // 触发 UI 刷新

总结

本文详细讲解了 ArkUI 中 @Entry@Component 两个核心装饰器的完整用法:

  1. @Component 基础:组件声明、build() 方法、导出规则
  2. @Entry 装饰器:页面入口注册、生命周期回调
  3. 组件导入导出:export 和 import 的使用方法
  4. 状态管理:@State、@Link、@Prop 的对比和使用
  5. 组件设计模式:容器组件、展示组件、粒度控制
  6. 生命周期:页面组件和普通组件的生命周期差异
  7. 最佳实践:常见问题的排查和解决方案

至此,第一系列"应用架构与导航"的 10 篇文章全部完成 !从下一篇文章开始,我们将进入 第二系列:闪屏与引导页,深入讲解 Canvas 2D 绘制、LinearGradient 渐变、Swiper 轮播等内容,敬请期待。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
w139548564226 小时前
Shadow 阴影体系:多层级阴影提升视觉层次
后端
IT_陈寒6 小时前
JavaScript的异步地狱里,我的Promise掉进了微任务陷阱
前端·人工智能·后端
xianjixiance_7 小时前
Swiper 轮播组件实现 3 页新用户引导
后端
CaffeinePro7 小时前
FastAPI高并发实战:缓存、分布式限流、链路日志三位一体架构
后端·fastapi
fengxinzi_zack7 小时前
年统计与最长连续天数:数据聚合可视化
后端
全堆鸿蒙7 小时前
aboutToAppear 生命周期:路由参数获取与数据初始化
后端
2501_931803757 小时前
Go 反射:原理、核心机制与实践指南
开发语言·后端·golang
程序员爱钓鱼8 小时前
Rust if let 与 while let 详解:简化模式匹配
前端·后端·rust
爸爸6198 小时前
UIAbility 生命周期在日记 App 中的实践
后端·华为·harmonyos·鸿蒙系统