大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

本文目录:

前言

老实说,我第一次上手 ArkTS + ArkUI 的时候,整个人是懵的:
既像 TypeScript,又哪儿哪儿都不太一样;
既说是声明式 UI,又跟 React / Vue 的套路不完全一样;
写着写着还冒出来一堆 @State@Prop@Provide……
一时间我甚至怀疑:是不是只有官方 Demo 才能流畅运行,我自己的项目注定又长又丑又难维护?

后来真把几个完整项目撸下来,我才发现 ArkTS 这套东西——
如果你只是“会用”,它确实有点怪;
但如果你“真正吃透”,写起来是真的顺、性能也真能打。

这篇文章,我想带你踩一遍“已经有人踩过”的坑,然后绕开它们。
按你的大纲,我们从这几块展开:

  1. ArkTS 语言特性——到底跟普通 TS 有啥不一样?
  2. 声明式 UI 语法——build 到底在“声明”什么?
  3. @State / @Prop / @Provide 等状态管理——数据到底怎么在组件树里流动?
  4. 自定义组件——写个 Button / 卡片,到底该怎么封装?
  5. 复用与模块化——别把所有页面写成一个 2000 行的地狱组件
  6. 性能优化技巧——别等真机一卡一卡才想起来“原来有性能这回事”

不整花架子,每一节都有代码、有思路、有“过来人的碎碎念”
你要是正准备写一个鸿蒙 App,这篇可以直接当“心法 + 代码手册”一起用。


一、ArkTS 语言特性:不是“换皮版 TS”,是更偏工程化的一套玩意

先把一个误会扯清楚:ArkTS 不是“把 TS 改个名字”。
它的语法确实很接近 TypeScript,但做了很多“更偏系统 / 更偏工程”的增强,尤其是:

  • 更严格的静态检查
  • 更强调编译期优化
  • 对 UI & 状态有原生支持(装饰器那一套)

1.1 类型系统:别再拿 any 当“万金油”

ArkTS 还是熟悉的强类型风格,但对“乱写类型”会更严格一点。比如:

let count: number = 1;
// ❌ 这种写法直接就会被教做人
count = '1'; 

// 推荐写法
let name: string = 'ArkTS';
let age: number | undefined;

几点习惯,真心建议尽早养成:

  • 能写具体类型就别写 any
  • 避免“类型靠猜”,接口 / 模型自己定义清楚
  • 该用 enum 的地方不用字符串硬凑

比如 UI 模式常见的写法:

enum PageMode {
  VIEW,
  EDIT,
  CREATE,
}

@State mode: PageMode = PageMode.VIEW;

比你到处写 "view" | "edit" | "create" 要稳太多。

1.2 类、模块、import/export 都很熟悉,但更讲究“规范”

ArkTS 模块化这块基本和 TS 一样,用法没什么门槛:

// user-model.ets
export interface User {
  id: number;
  name: string;
  age: number;
}

// service.ets
import type { User } from './user-model';

export async function fetchUser(id: number): Promise<User> {
  // ...
}

但真正在项目里要注意的是:

  • modelserviceui 按层拆模块
  • UI 文件别同时干接口请求、数据持久化、业务逻辑所有事情
  • 组件层只引用“向上封装好的” service,不要直接调最底层

后面讲“复用与模块化”时咱们会把这套结构捋得更细。

1.3 装饰器的“正经用法”:不只是好看,是 ArkUI 的灵魂

ArkTS 最大的特色之一,就是装饰器用在“UI + 状态”上,这跟普通 TS 里那种“装饰一个 class 玩玩注解”的思路完全不是一个级别。

最常见的几个:

  • @Entry:应用入口组件(一个 Ability 的 root)
  • @Component:声明这是一个 UI 组件
  • @State:组件自身持有、会触发刷新的状态
  • @Prop:从父组件传入的属性(只读)
  • @Provide / @Consume:上下文注入 / 依赖注入式的状态共享

例子先看一眼感受下:

@Entry
@Component
struct HomePage {
  @State count: number = 0;

  build() {
    Column() {
      Text(`当前计数:${this.count}`)
      Button('加一')
        .onClick(() => this.count++)
    }
  }
}

哪怕你没看文档,大概也能猜到:
每次 this.count++,UI 会自动重建显示新的数字。
这就是 ArkTS + ArkUI 的核心感觉:你写的是“状态 → UI 的映射关系”,不是传统那种手动改 DOM / 刷 UI。


二、声明式 UI 语法:build() 里面到底在“声明”什么?

很多人刚接触 ArkUI 时,看到这个写法会有点懵:

build() {
  Column() {
    Text('Hello')
    Button('点我')
  }
}

看起来像函数,又不像 JSX,那这玩意到底是什么?

2.1 你写的不是“过程”,你写的是“结构 + 约束”

理解 ArkUI 的关键一句话:

build() 里写的不是“执行过程”,而是在声明 UI 的结构和属性。

你可以把它当成一种特殊语法的 DSL(领域专用语言):

  • Column() / Row() / List() 类似“组件标签”
  • 花括号 { ... } 内部是子组件
  • 组件后面的 .width().height().backgroundColor() 是属性链式设置

比如一个简单卡片:

build() {
  Column() {
    Text(this.title)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 8 })

    Text(this.desc)
      .fontSize(14)
      .opacity(0.8)
  }
  .padding(16)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
}

这段的含义是:

  • 外面是一个列容器 Column(相当于 <div style="display:flex;flex-direction:column"> 那味)
  • 里面两个 Text 垂直排列
  • 最外层 Column 有 padding / 背景色 / 圆角

2.2 生命周期钩子:不仅有 build,还有“出现 / 消失 / 首次构建”

常见生命周期:

  • aboutToAppear():组件即将出现在界面时调用,适合初始化数据
  • aboutToDisappear():组件即将从界面移除
  • onPageShow() / onPageHide():配合导航场景使用

举个常见场景:页面打开拉首屏数据:

@Entry
@Component
struct UserPage {
  @State loading: boolean = true;
  @State userName: string = '';

  aboutToAppear() {
    this.loadUser();
  }

  async loadUser() {
    try {
      const user = await fetchUserFromApi();
      this.userName = user.name;
    } finally {
      this.loading = false;
    }
  }

  build() {
    Column() {
      if (this.loading) {
        Text('加载中…')
      } else {
        Text(`你好,${this.userName}`)
      }
    }
  }
}

小心得:

  • 在生命周期里做异步请求很正常,但一定记得做异常处理 + 状态兜底,不然 UI 很容易卡在一个奇怪的状态。

三、@State / @Prop / @Provide:ArkTS 状态管理写好了,UI 自然顺

实话说,很多鸿蒙项目一开始写得很痛苦,很大一部分就是因为状态管理没想明白:
@State 到底什么时候用?
父子传参到底用 @Prop 还是 @Link
全局主题 / 用户信息该用 @Provide 吗?

咱们一块一块拆开。

3.1 @State:组件自己的“小情绪”

@State 是“组件内部管理的状态”,特点:

  • 只在当前组件内部可修改
  • 修改会触发当前组件重新 build(以及使用它的子树)
  • 不应该被外部强行控制

比如最经典的计数器:

@Component
struct Counter {
  @State count: number = 0;

  build() {
    Row() {
      Button('-').onClick(() => this.count--)
      Text(this.count.toString()).margin({ left: 10, right: 10 })
      Button('+').onClick(() => this.count++)
    }
  }
}

用它的时候,不需要操心它内部怎么管理 count,只要把它当“一个会自己变的组件”就行。

3.2 @Prop:只读的“外部输入”,不要在里面乱改

@Prop 表示“从父组件传进来的属性”,有几个原则:

  • 子组件可以读,但不应该直接改
  • 父组件改了 @Prop 绑定的值,子组件会自动更新
  • 更适合作为“配置项”而不是“状态源头”

例子:

@Component
struct UserCard {
  @Prop name: string;
  @Prop age: number;

  build() {
    Column() {
      Text(this.name)
      Text(`${this.age}`)
        .fontSize(12)
        .opacity(0.8)
    }
  }
}

// 使用
UserCard({ name: '张三', age: 18 })

如果你在子组件里写:

this.name = '李四'; // ❌ 这是和数据流作对

那就说明这段设计思路一开始就不对。

3.3 @Link:父子双向绑定,一定要“克制”使用

有时候你确实希望子组件能“改动父组件状态”,比如:

  • 开关组件:内部切换 on/off,其实要影响外部状态
  • 表单输入:子组件是输入框,但数据存的是父组件那一坨 form 对象

这时候就轮到 @Link 出场了。

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

  build() {
    Column() {
      SwitchRow({
        label: '深色模式',
        value: $enableDarkMode, // 注意这里的 $,传的是“引用”
      })

      Text(this.enableDarkMode ? '已开启深色' : '浅色模式')
    }
  }
}

// 子组件
@Component
struct SwitchRow {
  @Prop label: string;
  @Link value: boolean;

  build() {
    Row() {
      Text(this.label)
      Toggle({ checked: this.value })
        .onChange((checked) => {
          this.value = checked;  // 这里会同步到父组件的 enableDarkMode
        })
    }
  }
}

关键点:

  • 父组件传 $state,子组件用 @Link
  • 子组件改 value,等于在改父组件的 enableDarkMode
  • 用的时候一定要节制,别到处搞双向绑定,不然数据流难排查

3.4 @Provide / @Consume:全局 / 跨层级共享状态的“注入方案”

你总会遇到这种需求:

  • 整个 App 共享一个主题对象 theme
  • 全局用户信息 userInfo
  • 多个页面都需要访问某个 config

这时候如果靠一层层用 @Prop 传,传到你自己都崩溃。
ArkTS 给了一套非常自然的做法:@Provide + @Consume

// 顶层
@Entry
@Component
struct AppRoot {
  @State isDark: boolean = false;
  @Provide theme = this.isDark ? DarkTheme : LightTheme;

  build() {
    Column() {
      // 顶部主题切换
      Row() {
        Text(this.isDark ? '🌙 深色' : '☀ 浅色')
        Button('切换').onClick(() => this.isDark = !this.isDark)
      }.padding(12)

      // 所有子组件都可以 @Consume theme
      HomePage()
    }
    .backgroundColor(this.theme.pageBgColor)
  }
}

子组件里:

@Component
struct HomePage {
  @Consume theme;

  build() {
    Column() {
      Text('首页')
        .fontColor(this.theme.titleColor)
      // ...
    }
  }
}

这个模式,非常适合做:

  • 主题(Theme)
  • 语言(I18N 文本资源)
  • 全局配置 / 环境变量

3.5 小结:状态选型一句话口诀

  • 组件内部小状态:@State
  • 父传子只读配置:@Prop
  • 父子同步改值:@Link(慎用)
  • 全局 / 跨层级共享:@Provide + @Consume

记住一句话:

状态越“局部”,越好维护;一上来全想做全局,后面你会被自己整崩溃。


四、自定义组件:不要一上来就写“大页面”,先学会写好“小组件”

几乎所有“写着写着项目开始失控”的鸿蒙 UI,都有一个共同特征:
——页面里塞满了所有 UI 细节,完全不做组件拆分。

那怎么拆?从最小的 UI 片段开始:Button、Card、ListItem、Dialog……

4.1 写一个有点“人味”的 Button 组件

我们写一个略微“正式一点”的按钮:

  • 支持主按钮 / 次按钮
  • 支持禁用 / 加载中
  • 支持宽度铺满
enum ButtonType {
  Primary,
  Secondary,
}

@Component
struct AppButton {
  @Prop text: string;
  @Prop type: ButtonType = ButtonType.Primary;
  @Prop disabled: boolean = false;
  @Prop loading: boolean = false;
  @Prop fullWidth: boolean = false;
  @Prop onClick?: () => void;

  private getBgColor(): string {
    if (this.disabled) {
      return '#CCCCCC';
    }
    return this.type === ButtonType.Primary ? '#007DFF' : 'transparent';
  }

  private getTextColor(): string {
    if (this.disabled) {
      return '#888888';
    }
    return this.type === ButtonType.Primary ? '#FFFFFF' : '#007DFF';
  }

  build() {
    Button(this.loading ? '处理中…' : this.text)
      .backgroundColor(this.getBgColor())
      .fontColor(this.getTextColor())
      .borderRadius(12)
      .borderWidth(this.type === ButtonType.Secondary ? 1 : 0)
      .borderColor('#007DFF')
      .width(this.fullWidth ? '100%' : 'auto')
      .enabled(!this.disabled && !this.loading)
      .onClick(() => {
        if (this.disabled || this.loading) return;
        this.onClick && this.onClick();
      })
  }
}

使用起来就很清爽:

AppButton({
  text: '登录',
  type: ButtonType.Primary,
  fullWidth: true,
  loading: this.logining,
  onClick: () => this.submit()
})

你会发现,自定义组件本质上就是:
用一层“语义更强”的封装,把底层 Button / Text / Row 这些原子组件组织起来。

4.2 一个典型“卡片组件”:传数据、暴露点击事件

再来一个 UserCard 示例:

export interface UserInfo {
  name: string;
  role: string;
  avatarUrl?: string;
}

@Component
struct UserCard {
  @Prop user: UserInfo;
  @Prop onClick?: () => void;

  build() {
    Row() {
      // 左边头像
      if (this.user.avatarUrl) {
        Image(this.user.avatarUrl)
          .width(40)
          .height(40)
          .borderRadius(20)
      } else {
        // 简单用首字母占位
        Text(this.user.name.slice(0, 1))
          .width(40)
          .height(40)
          .borderRadius(20)
          .backgroundColor('#EEEEEE')
          .fontSize(20)
          .textAlign(TextAlign.Center)
          .alignItems(VerticalAlign.Center)
      }

      // 右边文字
      Column() {
        Text(this.user.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        Text(this.user.role)
          .fontSize(12)
          .opacity(0.7)
      }
      .margin({ left: 12 })
    }
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 6, color: '#00000022', offsetX: 0, offsetY: 2 })
    .onClick(() => this.onClick && this.onClick())
  }
}

调用:

UserCard({
  user: { name: '张三', role: '开发工程师' },
  onClick: () => this.gotoUserDetail()
})

习惯用这种方式封装,哪怕 UI 改版,你只改 UserCard 的内部实现,使用的地方基本不动,眼泪立刻少流一半。


五、复用与模块化:从“写页面”升级为“写界面系统”

到这一步,其实你已经可以写出“能看的 UI”了,但距离“工程级”还有一段——怎么把这些组件、页面和逻辑组织得有层次?

5.1 一个推荐的项目结构(可以直接抄过去用)

/entry/src/main
  /common
    theme.ets           # 主题 / 颜色 / 字体等
    constants.ets       # 一些常量
    types.ets           # 全局类型
  /model
    user-model.ets
    todo-model.ets
  /service
    http-client.ets
    user-service.ets
    todo-service.ets
  /uikit                # 你自己的组件库
    /foundation
    /tokens
    /generic
    /feedback
  /pages
    HomePage.ets
    UserDetailPage.ets
    SettingsPage.ets
  /components           # 业务级组件(跟 uikit 区分开)
    TodoList.ets
    UserList.ets

几个核心思路:

  • UI 基础组件(Button、Dialog)放 /uikit,可复用性高,尽量无业务逻辑

  • 某个业务强相关的组件(例如“任务列表卡片”)放 /components

  • 页面级 .ets 文件只负责:

    • 串业务
    • 做路由
    • 管理页面级状态
  • 所有请求、缓存的细节尽量压到 /service 里去

5.2 模块化 = “UI 复用 + 逻辑复用”

以一个“待办任务列表页面”为例:

  • TodoService 负责:

    • 拉取任务列表
    • 新增 / 完成 / 删除任务
    • 本地缓存等
// /service/todo-service.ets
import type { TodoItem } from '../model/todo-model';

export class TodoService {
  async listTodos(): Promise<TodoItem[]> {
    // 请求接口 / 读本地存储…
  }

  async addTodo(title: string): Promise<void> {
    // ...
  }

  async completeTodo(id: number): Promise<void> {
    // ...
  }
}
  • UI 组件 TodoList 负责展示和交互:
// /components/TodoList.ets
@Component
export struct TodoList {
  @Prop items: TodoItem[];
  @Prop onToggle?: (item: TodoItem) => void;
  @Prop onDelete?: (item: TodoItem) => void;

  build() {
    List() {
      LazyForEach(this.items, (item: TodoItem) => {
        ListItem() {
          Row() {
            Checkbox({ checked: item.completed })
              .onChange(() => this.onToggle && this.onToggle(item))
            Text(item.title)
              .decoration(item.completed ? TextDecoration.LineThrough : TextDecoration.None)
            Blank()
            Button('删除')
              .onClick(() => this.onDelete && this.onDelete(item))
          }
        }
      })
    }
  }
}
  • 页面 TodoPage 串起来:
// /pages/TodoPage.ets
@Entry
@Component
struct TodoPage {
  private todoService: TodoService = new TodoService();

  @State loading: boolean = true;
  @State todos: TodoItem[] = [];

  aboutToAppear() {
    this.refresh();
  }

  async refresh() {
    this.loading = true;
    this.todos = await this.todoService.listTodos();
    this.loading = false;
  }

  build() {
    Column() {
      if (this.loading) {
        Text('加载中…')
      } else {
        TodoList({
          items: this.todos,
          onToggle: async (item: TodoItem) => {
            await this.todoService.completeTodo(item.id);
            await this.refresh();
          },
          onDelete: async (item: TodoItem) => {
            await this.todoService.deleteTodo(item.id);
            await this.refresh();
          }
        })
      }
    }
  }
}

你看:

  • 页面不关心 UI 细节(TodoList 包了)
  • UI 不关心数据来源(通过 Props + 回调通信)
  • Service 不关心 UI 怎么展示

这就是一个比较健康的 ArkTS 模块化结构。


六、性能优化技巧:写得优雅,也要跑得丝滑

等你页面写多了,就会遇到那句开发者名言:“能跑不代表好用。”
真机上一滑一卡的那种窘迫,谁体验谁知道。

ArkTS + ArkUI 这套在性能上其实挺给力,但用不好也能被你写卡。
下面这几条是我自己踩坑总结出来的“血泪经验”。

6.1 @State 粒度控制:状态写高了,整棵树都跟着抖

问题案例:

@Entry
@Component
struct BigPage {
  @State selectedId: number = -1;

  build() {
    Column() {
      Header()

      // 列表非常长
      List() {
        LazyForEach(this.getItems(), (item) => {
          ListItem() {
            ItemRow({
              item: item,
              selected: this.selectedId === item.id,
              onClick: (id: number) => this.selectedId = id
            })
          }
        })
      }

      FooterInfo({ selectedId: this.selectedId })
    }
  }
}

每次你修改 selectedIdBigPage 整个 build() 会重新执行一遍,整棵 UI 树都可能参与重建

优化方式:

  • 把列表抽成子组件
  • 甚至可以把“选中状态”局部化
@Entry
@Component
struct BigPage {
  @State selectedId: number = -1;

  build() {
    Column() {
      Header()

      ItemList({
        selectedId: this.selectedId,
        onSelect: (id: number) => this.selectedId = id
      })

      FooterInfo({ selectedId: this.selectedId })
    }
  }
}

再进一步,在 ItemList 里想办法减少无关项的刷新。

6.2 避免在 build() 里做重逻辑 / 重计算

千万不要在 build() 里做任何“跟渲染无关的复杂逻辑”
build() 可能被调用很多次,把它当纯函数用,别在里面搞:

  • 大量循环
  • IO 操作
  • 网络请求
  • JSON 大解析

这些都应该放生命周期里,结果进 @Statebuild() 只负责根据 @State 渲染。

6.3 列表一定要用 LazyForEach,别拿 ForEach 硬刚

列表稍微长一点,就乖乖用 LazyForEach

List() {
  LazyForEach(this.items, (item: Item) => {
    ListItem() {
      ItemRow({ item })
    }
  })
}

LazyForEach 帮你做了可见区域复用,
如果用普通 ForEach,你就等着大列表滑动时白给。

6.4 大任务拆分:一帧做一点,别一口吃成胖子

有些计算你确实必须在前端做,比如:

  • 复杂过滤
  • 客户端排序 / 聚合
  • 格式化大量数据

这时候尽量“分片执行”:

async function heavyCalcChunk(list: number[]): Promise<number[]> {
  let result: number[] = [];
  const CHUNK_SIZE = 2000;
  let index = 0;

  return new Promise(resolve => {
    function runChunk() {
      const start = Date.now();
      while (index < list.length) {
        result.push(list[index] * 2);
        index++;

        if (index % CHUNK_SIZE === 0 && Date.now() - start > 8) {
          // 分帧执行,留时间给 UI
          globalThis.setTimeout(runChunk, 0);
          return;
        }
      }
      resolve(result);
    }
    runChunk();
  });
}

这种写法比一口气卡主线程要友好得多。

6.5 动画、过渡别乱写,善用系统能力

如果你要加一些小动画,优先用 ArkUI 提供的动画能力,不要自己用疯狂 setTimeout 去改状态。

原则:

  • 动画用系统 API
  • 状态更新频率控制在合理范围
  • 仅动画相关的那一小块组件参与变化

写在最后:ArkTS 写久了,你会发现它更像“搭积木”,不是“凑页面”

如果你一开始上手 ArkTS + ArkUI 的感受是“有点别扭”,那很正常。
因为它强迫你从一开始就思考:

  • 状态放哪一层最合理?
  • 这个 UI 段落是不是应该抽成组件?
  • 能不能通过主题注入统一样式?
  • 这个计算重不重,会不会卡 UI?

说难听点,就是它不太允许你“随便糊一个能运行的界面就完事”
但说好听点,只要你遵循这套思路,项目越大,你越庆幸当初没偷懒。

把今天这几点记住:

  1. ArkTS 是 TS 的进阶形态:更强调类型、更重视工程约束
  2. 声明式 UI = 写“状态到界面”的映射,build 要保持“干净”
  3. @State / @Prop / @Provide 把数据流理顺了,组件自然好用
  4. 自定义组件一定要语义化、可复用,不要 copy 贴页面代码
  5. 模块化拆分:uikit / 业务组件 / 页面 / service 各司其职
  6. 性能从一开始就要有意识,别等用户来当你的测试员

等你真用这套思路完整写完一个鸿蒙 App,你会发现:
以前那种“改个按钮颜色得全项目搜索”的噩梦,慢慢就离你远去了。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐