一、前言

1.1 为什么选择 ArkTS

2024 年,华为推出了 HarmonyOS NEXT,彻底剥离了 Android 兼容层,标志着鸿蒙真正成为了一个独立的操作系统。而 ArkTS 作为鸿蒙原生应用的首选开发语言,基于 TypeScript 语法进行了深度扩展,专为声明式 UI 开发而设计。

对于前端开发者来说,ArkTS 的学习曲线非常友好。TypeScript 开发者可以直接上手,React/Vue 开发者能快速理解状态驱动的理念,而 Flutter/Compose 开发者则会发现 ArkTS 的声明式 UI 语法几乎是直觉式的。

更重要的是,ArkTS 对于 UI 的响应式更新是自动且精准的。开发者只需要关注数据逻辑,框架会在底层自动追踪状态依赖,只更新受影响的组件,而不是全量刷新。这和 React 的虚拟 DOM diff 不同,ArkTS 采用的是更细粒度的观察者模式——每个 @State 变量都维护着自己的订阅列表。

1.2 本文目标

本文将以一个生肖查询工具为载体,从零开始讲解 ArkTS 的核心概念。这个例子虽然简单,但涵盖了一个高频交互页面所需的所有基础能力:

  • 状态管理(@State
  • 用户输入处理(TextInput
  • 事件响应(onClick
  • 布局编排(ColumnFlexAlign
  • 条件逻辑与错误处理
  • 字符串模板与数组操作

我们不仅要讲清楚"怎么写",更要讲清楚"为什么这么写"。


二、鸿蒙开发环境准备

2.1 工具链概览

在开始写代码之前,先了解鸿蒙开发的完整工具链:

工具用途下载方式
DevEco Studio官方 IDE,基于 IntelliJ华为开发者官网
HarmonyOS SDK编译工具链、模拟器随 DevEco Studio 安装
ArkTS 编译器将 ArkTS 编译为方舟字节码SDK 内置
预览器 Previewer实时代码预览DevEco Studio 内置

DevEco Studio 内置了实时预览器(Previewer),修改代码后几乎立即能看到界面变化。这个功能在开发 UI 密集的页面时非常高效。

2.2 创建项目

在 DevEco Studio 中,选择 File → New → Create Project,选择 Empty Ability 模板。这会生成一个标准的 Index.ets 文件——也就是我们写代码的地方。

项目结构示意:

MyZodiacApp/
├── entry/
│   ├── src/main/
│   │   └── ets/
│   │       └── pages/
│   │           └── Index.ets      ← 我们在这写代码
│   ├── resources/
│   └── build-profile.json5
├── oh_modules/
└── hvigor/

2.3 理解 @Entry 和 @Component

每个 ArkTS 页面都由两个装饰器标记:

@Entry        // 标记该组件为页面入口
@Component    // 标记该结构体为一个组件
struct Index { // 组件名,通常与文件名一致
  // ...
}
  • @Entry —— 告诉框架这个组件是一个页面的根节点。一个页面只能有一个 @Entry 组件。你可以把它理解为路由的终点——当用户导航到该页面时,框架会实例化这个组件。
  • @Component —— 声明一个自定义组件。组件的核心是 build() 方法,它描述了组件的 UI 结构。

组件可以嵌套使用,例如在一个页面中:

@Entry
@Component
struct MainPage {
  build() {
    Column() {
      Header()      // 子组件
      Content()     // 子组件
      Footer()      // 子组件
    }
  }
}

@Component
struct Header {
  build() {
    Text('页头')
  }
}

这种组件化拆分方式,和 React 的函数组件、Vue 的 SFC 思想完全一致——高内聚、低耦合


三、完整代码与效果预览

3.1 代码全文

在正式拆解之前,先看完整代码,建立整体认知:

@Entry
@Component
struct Index {
  @State year: string = ''
  @State zodiac: string = '请输入出生年份'

  getZodiac() {
    const arr = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪']
    let y = parseInt(this.year)
    if (isNaN(y)) {
      this.zodiac = '请输入有效年份'
      return
    }
    let idx = (y - 4) % 12
    this.zodiac = `生肖:${arr[idx]}`
  }

  build() {
    Column({ space: 30 }) {
      TextInput({ text: this.year, placeholder: '输入出生年份' })
        .width('80%')
        .height(50)
        .onChange(v => this.year = v)

      Button('查询生肖')
        .onClick(() => this.getZodiac())
        .width(120)

      Text(this.zodiac)
        .fontSize(22)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
  }
}

38 行代码,五行核心逻辑,三行 UI 声明。整个组件从数据到界面,一气呵成。

3.2 运行效果

在模拟器中运行后,屏幕上显示:

+----------------------------------+
|                                  |
|                                  |
|    [ 输入出生年份            ]    |
|                                  |
|          [ 查询生肖 ]             |
|                                  |
|          生肖:龙                 |
|                                  |
|                                  |
+----------------------------------+

页面整体垂直居中,输入框占 80% 宽度,按钮居中,结果文字 22 号字,清晰易读。

用户输入"2024",点击按钮后,页面立即显示"生肖:龙"。如果输入非法字符,则提示"请输入有效年份"。


四、逐行深度解析

好的,接下来我们逐行拆解,深入到每一行代码背后的设计理念。

4.1 响应式状态 @State —— 数据的"神经中枢"

@State year: string = ''
@State zodiac: string = '请输入出生年份'

@State 是 ArkTS 中最重要的装饰器之一,它实现了响应式数据绑定

4.1.1 工作原理

@State 修饰的变量发生变化时,ArkTS 框架会:

  1. 自动检测 —— 在运行时维护一个依赖追踪图,记录每个 @State 变量被哪些 UI 节点读取
  2. 精准更新 —— 只重新渲染读取了该变量的 UI 组件,而不是整个页面
  3. 批量合并 —— 在同一帧内发生的多次状态变更会合并为一次渲染,避免重复计算

这和 Vue 3 的 ref() / reactive()、SwiftUI 的 @State、Flutter 的 setState()、Compose 的 mutableStateOf() 本质上是同一类机制——细粒度响应式

背后的大致实现原理(简化版):

// 伪代码:@State 底层简化概念
class StateVariable<T> {
  private _value: T
  private subscribers: Set<UIComponent> = new Set()

  get value(): T {
    // 在 build 中读取时,自动注册订阅
    currentBuildContext?.addDependency(this)
    return this._value
  }

  set value(newVal: T) {
    if (this._value !== newVal) {
      this._value = newVal
      // 通知所有订阅者重新渲染
      this.subscribers.forEach(comp => comp.requestReRender())
    }
  }
}

当然,ArkTS 的底层实现远比这个伪代码复杂,但核心思想一致——状态和 UI 之间建立订阅关系,数据变了 UI 自动更新

4.1.2 何时用 @State

@State 适合以下场景:

场景示例是否适合 @State
组件内部的可变状态输入框内容、开关状态
父组件传到子组件的数据用户信息、配置项❌ 用 @Prop
全局共享状态登录态、主题色❌ 用 @StorageLink
计算属性根据状态推导的值❌ 直接写 getter

在本文的例子中,yearzodiac 都是组件内部的状态,且数据流是单向的(输入框 → year → 算法 → zodiac → UI),用 @State 恰到好处。

4.2 生肖算法 —— 核心业务逻辑

getZodiac() {
  const arr = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪']
  let y = parseInt(this.year)
  if (isNaN(y)) {
    this.zodiac = '请输入有效年份'
    return
  }
  let idx = (y - 4) % 12
  this.zodiac = `生肖:${arr[idx]}`
}
4.2.1 数学原理:为什么减 4

十二生肖是中国传统文化中记录年份的符号系统,每 12 年一个轮回。生肖的顺序是固定的:

鼠 → 牛 → 虎 → 兔 → 龙 → 蛇 → 马 → 羊 → 猴 → 鸡 → 狗 → 猪

我们需要找一个已知的锚点年份。查阅干支纪年表可知:

  • 公元 4 年 = 甲子年 = 鼠年
  • 公元 5 年 = 乙丑年 = 牛年
  • 公元 6 年 = 丙寅年 = 虎年
  • ...依此类推

因此,公式 (year - 4) % 12 中,- 4 就是将年份偏移到以鼠年(索引 0)为起点的坐标系。

验证几个已知年份:

公式余数生肖是否正确
(2024 - 4) % 1280→鼠,1→牛,...,8→龙✅ 2024 是龙年
(2023 - 4) % 1277→兔✅ 2023 是兔年
(2020 - 4) % 1200→鼠✅ 2020 是鼠年
(1996 - 4) % 1200→鼠✅ 1996 是鼠年
(1988 - 4) % 1200→龙不对,1988是龙年...等等,让我重新算

等一下,我手动验证一下 1988:

(1988 - 4) % 12 = 1984 % 12 = 1984 / 12 = 165.333... 12 × 165 = 1980,余数 4。 4 对应数组索引 4 → arr[4] = '龙'。

✅ 1988 年确实是龙年。公式正确。

4.2.2 边界情况处理

好的代码不仅要处理"正常路径",还要覆盖边界情况

这个例子中涉及三个边界场景:

① 空字符串

用户打开页面直接点击按钮,year 为空字符串。parseInt('') 返回 NaN,进入 isNaN 分支,显示友好提示。

② 非数字字符

用户输入"abc"或"二零二四",parseInt 同样返回 NaN,被兜底拦截。

③ 负数年份

用户输入"-1000",parseInt 会解析为 -1000,公式依然可以计算:

(-1000 - 4) % 12 = (-1004) % 12

在 JavaScript/TypeScript 中,负数的 % 运算结果可能是负数。让我们验证:

-1004 % 12 = -8 (因为 -1004 + 12×83 = -1004 + 996 = -8)

数组索引 -8 在 JavaScript 中是 undefined,导致结果是"生肖:undefined"。

这是一个隐蔽的 bug!修复方案:对求模结果取绝对值或做正数化处理。

或者更安全地,限制输入范围:

if (y < 1900 || y > 2100) {
  this.zodiac = '请输入 1900-2100 之间的年份'
  return
}

这就是为什么写好边界处理很重要——真实环境中的用户行为永远比预期更"丰富"。

4.3 build 方法 —— 声明式 UI 的编排

build() {
  Column({ space: 30 }) {
    TextInput({ text: this.year, placeholder: '输入出生年份' })
      .width('80%')
      .height(50)
      .onChange(v => this.year = v)

    Button('查询生肖')
      .onClick(() => this.getZodiac())
      .width(120)

    Text(this.zodiac)
      .fontSize(22)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .padding(20)
}
4.3.1 布局容器 Column

Column 是 ArkTS 中最基本的布局容器,子组件从上到下纵向排列。

参数 { space: 30 } 表示子组件之间的间距为 30。

对应的底层布局计算:

若 Column 高度为 H,子组件数量为 n=3,间距 space=30
则子组件总内容高度 = Sum(子组件自身高度) + (n-1)×space
剩余空间 = H - 总内容高度
剩余空间按 justifyContent 规则分配

其他布局容器:

容器方向类似 CSS
Column纵向flex-direction: column
Row横向flex-direction: row
Flex自定义方向display: flex
Stack层叠position: absolute + 相对定位
Grid网格display: grid
List列表虚拟滚动列表,性能最优
4.3.2 TextInput —— 用户输入
TextInput({ text: this.year, placeholder: '输入出生年份' })
  .width('80%')
  .height(50)
  .onChange(v => this.year = v)
  • text: this.year —— 绑定当前值,这是受控组件模式:输入框显示的内容永远等于 this.year
  • placeholder —— 占位提示,用户未输入时显示灰色文字
  • .width('80%') —— 宽度为父容器的 80%(相对单位)
  • .height(50) —— 高度固定 50 像素(绝对单位)
  • .onChange(v => this.year = v) —— 输入内容变化时,更新状态 year

受控组件设计的好处:

  1. 单一数据源 —— 组件的状态 year 是唯一真相来源,输入框只是状态的"投影"
  2. 易于校验 —— 可以在赋值前校验或转换输入
  3. 可预测 —— UI 始终和状态同步,不会出现"显示的内容和实际数据不一致"

扩展一下,如果我们要做输入实时校验:

.onChange(v => {
  // 只允许数字输入
  const filtered = v.replace(/\D/g, '')
  this.year = filtered
  // 如果输入非数字字符,输入框不会显示它们
})
4.3.3 Button —— 触发事件
Button('查询生肖')
  .onClick(() => this.getZodiac())
  .width(120)

Button 组件接收一个字符串参数作为按钮文本。

.onClick() 是点击事件绑定,参数是一个回调函数。

ArkTS 支持的事件类型远不止点击:

事件描述适用场景
onClick点击按钮、列表项
onLongPress长按上下文菜单
onSwipe滑动卡片滑动删除
onPinch捏合图片缩放
onRotate旋转图片旋转
onDragStart拖拽开始拖拽排序
onTouch原始触摸事件自定义手势
4.3.4 Text —— 展示结果
Text(this.zodiac)
  .fontSize(22)

Text 用于展示文本内容,直接绑定 this.zodiac 状态。

.fontSize(22) 设置字号。ArkTS 中默认单位为 vp(virtual pixel,虚拟像素),会自适应不同屏幕密度。

Text 组件支持丰富的样式:

Text('示例文本')
  .fontSize(22)
  .fontColor('#333333')
  .fontWeight(FontWeight.Bold)
  .textAlign(TextAlign.Center)
  .lineHeight(32)
  .letterSpacing(2)
  .textOverflow({ overflow: TextOverflow.Ellipsis })
  .maxLines(2)
4.3.5 容器属性

Column 容器上的链式调用:

.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
  • .width('100%').height('100%') —— 铺满整个屏幕
  • .justifyContent(FlexAlign.Center) —— 主轴方向(纵向)居中
  • .padding(20) —— 四边内边距,防止内容紧贴边缘

FlexAlign 枚举值:

效果
Start顶部对齐
Center居中对齐
End底部对齐
SpaceBetween均匀分布,首尾贴边
SpaceAround均匀分布,首尾留一半间距
SpaceEvenly均匀分布,全部间距相等

五、扩展与优化

基础版本已经跑通了,但作为一个真正的 App,还有很多可以完善的地方。

5.1 增加输入验证

@State errorMsg: string = ''

getZodiac() {
  const arr = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪']
  let y = parseInt(this.year)

  if (isNaN(y) || this.year.trim() === '') {
    this.errorMsg = '请输入有效的四位年份'
    this.zodiac = ''
    return
  }

  if (y < 1900 || y > 2100) {
    this.errorMsg = '请输入 1900-2100 之间的年份'
    this.zodiac = ''
    return
  }

  this.errorMsg = ''
  let idx = ((y - 4) % 12 + 12) % 12
  this.zodiac = `生肖:${arr[idx]}`
}

这样用户体验更加友好:输入不合法时,明确告知问题所在,而不是笼统地显示"请输入有效年份"。

5.2 改用 Select 下拉框

对于生肖查询这种场景,用输入框其实不太理想——用户可能不知道自己的出生年份,或者记错了。更好的方案是用年份选择器

@State selectedYear: number = 2024

build() {
  Column({ space: 30 }) {
    // 使用 Select 组件让用户选择年份
    Select([
      { value: '1990' },
      { value: '1991' },
      // ... 逐年生成
    ])
    .onSelect((index, value) => {
      this.selectedYear = parseInt(value)
    })

    Button('查询生肖')
      .onClick(() => this.getZodiacWithYear(this.selectedYear))

    Text(this.zodiac)
      .fontSize(22)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

但手动写 100 多个选项太笨了。更好的做法是动态生成:

getYearOptions(): Array<SelectOption> {
  const options: Array<SelectOption> = []
  const currentYear = new Date().getFullYear()
  for (let y = currentYear - 80; y <= currentYear; y++) {
    options.push({ value: y.toString() })
  }
  return options
}

这样就生成了从当前年份往前 80 年的选项列表,覆盖了从幼儿到长者的年龄段。

5.3 添加生肖动画

静态文字不够有趣,可以加上动画效果

ArkTS 的 animation API 支持过渡动画,属性变化时自动产生平滑过渡效果。支持动画化的属性包括:opacitytranslatescalerotatebackgroundColorwidthheight 等。

5.4 多语言支持

十二生肖不只是中国有,很多东亚国家也有类似的生肖体系,但动物名称不同:

序号中文英文日文越南文
0Rat鼠(ねずみ)Chuột (鼠)
1Ox牛(うし)Trâu (水牛)
2Tiger虎(とら)Hổ (虎)
3Rabbit兎(うさぎ)Mèo (猫)
4Dragon龍(たつ)Rồng (龙)
5Snake蛇(へび)Rắn (蛇)
6Horse馬(うま)Ngựa (马)
7Goat羊(ひつじ)Dê (羊)
8Monkey猿(さる)Khỉ (猴)
9Rooster鶏(とり)Gà (鸡)
10Dog犬(いぬ)Chó (狗)
11Pig猪(いのしし)Lợn (猪)

可以看到,越南的生肖中用猫代替了兔子。加入多语言支持可以这样设计:

5.5 接入生肖运势 API

如果想让这个工具更有用,可以接入一个每天更新运势的 API:

@State fortune: string = ''

async fetchFortune(zodiacName: string) {
  try {
    const response = await fetch('https://api.example.com/zodiac/fortune', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ zodiac: zodiacName, date: new Date().toISOString() })
    })
    const data = await response.json()
    this.fortune = data.fortune
  } catch (e) {
    this.fortune = '获取运势失败,请稍后重试'
  }
}

Copy

在 ArkTS 中,网络请求使用标准的 fetch API,与 Web 标准保持一致。

5.6 历史同年出生名人

增加一个趣味功能——显示同年出生的名人:

const famousPeople: Record<number, string[]> = {
  1988: ['刘亦菲', '林宥嘉', '李现'],
  1990: ['吴亦凡', '华晨宇', '鹿晗'],
  // ...
}

getFamousPeople(year: number): string {
  const list = famousPeople[year]
  if (list && list.length > 0) {
    return '同年出生名人:' + list.join('、')
  }
  return ''
}

六、常见问题与调试技巧

6.1 状态不更新

现象:修改了 @State 变量,但 UI 没有变化。

原因和解决

  1. 直接修改对象属性:ArkTS 的 @State 对对象的深度变化追踪有特殊性。如果是对象类型,直接修改对象的某个属性可能不会触发更新。
// ❌ 不会触发的写法
@State user: User = { name: '张三', age: 25 }
this.user.name = '李四'  // UI 不会更新

// ✅ 正确的写法
this.user = { ...this.user, name: '李四' }  // 返回新对象
  1. 异步回调中修改:如果在 setTimeout 或 Promise 回调中修改状态,需要确保回调执行时组件还未销毁。

  2. 修改不在 build 中读取的变量:只有被 UI 读取的 @State 变量变化才会触发渲染。如果变量只用于内部计算,不会被 UI 读取,那它的变化不会引起重渲染。

6.2 键盘弹起遮挡输入框

现象:在手机上,键盘弹起后输入框被遮挡。

解决方法

Column({ space: 30 }) {
  // ...
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
// 添加键盘避让
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

ArkTS 提供了 expandSafeArea 方法,可以指定需要避让的安全区域类型(键盘、刘海屏、导航条等)。

6.3 输入框类型设置

默认的 TextInput 会弹出全键盘,对于年份输入,应该限制为数字键盘:

TextInput({ text: this.year, placeholder: '输入出生年份' })
  .width('80%')
  .height(50)
  .type(InputType.Number)       // 数字键盘
  .maxLength(4)                   // 最多 4 位
  .onChange(v => this.year = v)

InputType 支持的类型:

类型键盘样式
Normal全键盘
Number数字键盘
PhoneNumber电话拨号盘
Email带 @ 和 . 的键盘
Password隐藏输入的密码键盘

6.4 调试工具

ArkTS 开发中常用的调试手段:

  1. console.log 打印日志:在 DevEco Studio 的 Logcat 面板查看
  2. @Watch 装饰器:监测状态变化
@State @Watch('onYearChange') year: string = ''

onYearChange() {
  console.info(`year 变为: ${this.year}`)
}
  1. Previewer 实时预览:右侧预览面板实时显示 UI 效果,修改代码即时刷新
  2. Inspector 布局检查:运行时可以查看组件树和各组件的布局属性

七、与其他框架的深度对比

7.1 ArkTS vs SwiftUI

维度ArkTSSwiftUI
语言TypeScript 扩展Swift
状态@State@State
布局Column/Row/StackVStack/HStack/ZStack
修饰符链式调用 .width()链式调用 .frame()
预览DevEco PreviewerXcode Canvas
发布App GalleryApp Store

语法结构惊人地相似——苹果和华为在声明式 UI 设计上殊途同归。

7.2 ArkTS vs Jetpack Compose

维度ArkTSJetpack Compose
语言TypeScriptKotlin
状态@StatemutableStateOf / remember
布局ColumnColumn
作用域链式调用中缀/点调用
重组自动追踪依赖自动追踪依赖

Compose 中的 remember 对应 ArkTS 的 @State;Compose 中的 LaunchedEffect 对应 ArkTS 的 @MonitoraboutToAppear 生命周期钩子。

7.3 ArkTS vs Flutter

维度ArkTSFlutter
语言TypeScriptDart
状态@StatesetState / ValueNotifier
组件树Column({ children })Column({ children })
自定义组件@Component structStatelessWidget / StatefulWidget
热重载支持支持

Flutter 的 setState 是全量更新(整个 Widget 树重建),而 ArkTS 的 @State 是细粒度更新(只渲染变化的组件)。这是架构上的本质区别——Flutter 依赖组件的 == 判断来决定是否重建,而 ArkTS 依赖编译期的依赖追踪。

7.4 架构设计理念对比

React 模型(全量 diff):

State → Virtual DOM → diff → 真实 DOM 最小化更新

Compose 模型(范围重组):

State → 读取该 State 的 Composable 函数 → 跳过未读取的状态 → 只重组受影响的范围

ArkTS 模型(细粒度观察):

State → 读取该 State 的 UI 节点 → 更新这些节点的属性 → 跳过其他所有节点

ArkTS 的模型在理论上性能更高,因为它不需要遍历组件树做 diff,也不需要执行函数体来判断状态依赖——依赖关系在编译时就确定了。

不过,实际性能还取决于具体实现和场景优化。对于大多数业务页面来说,三个框架的表现差距微乎其微。


八、项目结构的最佳实践

如果这个生肖查询工具只是一个更大 App 中的一小部分,合理的项目结构应该是:

entry/src/main/ets/
├── pages/
│   └── Index.ets             ← 入口页面
├── components/
│   ├── ZodiacCard.ets        ← 生肖卡片组件
│   ├── YearInput.ets         ← 年份输入组件
│   └── ZodiacResult.ets      ← 结果展示组件
├── models/
│   └── ZodiacData.ets        ← 数据和算法模型
├── utils/
│   ├── ZodiacCalculator.ets  ← 生肖计算工具类
│   └── FormValidator.ets     ← 表单校验工具
└── constants/
    └── ZodiacConstants.ets   ← 常量定义(生肖数组等)

这样拆分后,每个组件职责单一,易于测试和复用。

例如,将业务逻辑抽离到单独的模型文件中:

// ZodiacData.ets
export class ZodiacData {
  static readonly ZODIAC_NAMES = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪']
  static readonly ZODIAC_EMOJIS = ['🐭','🐮','🐯','🐰','🐲','🐍','🐴','🐏','🐵','🐔','🐶','🐷']

  static getZodiacName(year: number): string {
    const idx = ((year - 4) % 12 + 12) % 12
    return this.ZODIAC_NAMES[idx]
  }

  static getZodiacEmoji(year: number): string {
    const idx = ((year - 4) % 12 + 12) % 12
    return this.ZODIAC_EMOJIS[idx]
  }

  static isValidYear(year: number): boolean {
    return !isNaN(year) && year >= 1900 && year <= 2100
  }
}

然后在组件中引用:

import { ZodiacData } from '../models/ZodiacData'

@Component
struct ZodiacResult {
  @Prop year: number

  build() {
    Row({ space: 8 }) {
      Text(ZodiacData.getZodiacEmoji(this.year))
        .fontSize(32)
      Text('生肖:' + ZodiacData.getZodiacName(this.year))
        .fontSize(22)
    }
    .alignItems(VerticalAlign.Center)
  }
}

九、性能优化要点

虽然这个小例子不需要优化,但了解一些原则总是好的:

9.1 避免不必要的状态更新

每次 @State 变化都会触发 UI 更新。如果某个变量只在内部计算中使用,不要用 @State 修饰。

// ❌ 不必要的 @State
@State tempResult: number = 0

// ✅ 使用普通变量
tempResult: number = 0

9.2 使用 @Prop 和 @ObjectLink

数据从父组件传到子组件时,根据数据类型选择合适的装饰器:

  • 简单类型(string、number、boolean)→ @Prop
  • 对象类型 → @ObjectLink
  • 数组 → @Prop(用 @ObjectLink 可以避免深拷贝)

9.3 使用 LazyForEach 优化长列表

如果需要在列表中展示多年份的生肖数据,用 LazyForEach 替代 ForEach,它只会渲染当前可见的项:

LazyForEach(this.yearDataSource, (item: number) => {
  Text(`${item}年 - ${ZodiacData.getZodiacName(item)}`)
}, (item: number) => item.toString())

十、总结

这篇文章从一个简单的生肖查询工具出发,深入探讨了 ArkTS 的核心概念和最佳实践。

10.1 核心要点回顾

  1. @State 响应式状态 —— 数据驱动 UI,自动追踪依赖,精准更新
  2. 声明式布局 —— ColumnRowStack 等布局容器描述界面结构
  3. 事件绑定 —— onChangeonClick 等链式回调处理交互
  4. 组件化设计 —— 将 UI 拆分为可复用的组件,职责单一
  5. 边界处理 —— 输入校验、异常捕获、友好提示

10.2 学习路线建议

如果你刚接触 ArkTS,建议的学习顺序:

  1. 基础语法:TypeScript 类型系统、装饰器、箭头函数
  2. 布局组件:Column、Row、Stack、Flex、Grid
  3. 状态管理:@State → @Prop → @Link → @Provide/Consume → @StorageLink
  4. 事件系统:点击、触摸、手势
  5. 动画:显式动画、隐式动画、转场动画
  6. 网络与数据:fetch、本地存储、数据库
  7. 高级特性:自定义绘制、Native 插件、多线程

10.3 完整代码回顾

最后,让我们回到最初的代码。38 行,简洁、完整、可运行:

@Entry
@Component
struct Index {
  @State year: string = ''
  @State zodiac: string = '请输入出生年份'

  getZodiac() {
    const arr = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪']
    let y = parseInt(this.year)
    if (isNaN(y)) {
      this.zodiac = '请输入有效年份'
      return
    }
    let idx = (y - 4) % 12
    this.zodiac = `生肖:${arr[idx]}`
  }

  build() {
    Column({ space: 30 }) {
      TextInput({ text: this.year, placeholder: '输入出生年份' })
        .width('80%')
        .height(50)
        .onChange(v => this.year = v)

      Button('查询生肖')
        .onClick(() => this.getZodiac())
        .width(120)

      Text(this.zodiac)
        .fontSize(22)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
  }
}

这 38 行代码背后,我们讨论了响应式编程模型、声明式 UI 设计、组件化架构、布局计算原理、边界条件处理、跨框架对比、项目结构设计、性能优化原则——这些知识才是真正有价值的资产。

代码很简短,但背后的思想很丰富。

Logo

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

更多推荐