HarmonyOS 开发必会 5 种 Builder 详解

0 阅读10分钟

HarmonyOS 开发必会 5 种 Builder 详解,建议收藏

万少:华为HDE、鸿蒙极客

个人主页:blog.zbztb.cn/

2025年参与孵化了20+鸿蒙应用、技术文章300+、鸿蒙知识库用户500+、鸿蒙免费课程2套。

如果你也喜欢交流AI和鸿蒙技术,欢迎扣我。

前言

HarmonyOS应用开发中提供了众多的Builder用于实现结构复用,其中有**@Builder**、@LocalBuilder@BuilderParam

wrapBuildermutableBuilde等。

熟练掌握以上的Builder使用更加有利于开发出高质量的HarmonyOS应用。

@Builder装饰器

@Builder装饰器提供了最轻量级的组件结构复用,它不想自定义组件具有自己的生命周期和内部状态,所以运行起来体验和性能要更好,当需要在组件中实现结构复用时,优先使用它!

@ComponentV2
@Entry
struct Index {
  build() {
    Column({ space: 10 }) {
      this.MenuItem($r("sys.symbol.position"), "位置")
      this.MenuItem($r("sys.symbol.message"), "信息")
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)
  }

  /**
   * @Builder 装饰器:用于定义一个轻量级的 UI 复用单元
   * 与 @Component 相比,@Builder 更适合简单的、可复用的 UI 片段
   * 该方法可以在 build() 方法中通过 this.MenuItem() 直接调用
   *
   * @param src - 图标资源,使用 Resource 类型支持系统符号或本地资源
   * @param text - 菜单项显示的文本内容
   */
  @Builder
  MenuItem(src: Resource, text: string) {
    Row() {
      SymbolGlyph(src) // 系统符号图标
      Text(text) // 显示的文本
    }
  }
}

效果如图所示。

转存失败,建议直接上传图片文件

@BuilderParam装饰器

@BuilderParam 用于声明一个UI 插槽,让父组件可以向子组件传递自定义的 UI 内容。

/**
 * 自定义组件 MenuItemChild
 * 演示 @BuilderParam 的使用 - 用于接收父组件传递的 UI 结构(类似于 Vue 的 slot 插槽)
 */
@ComponentV2
struct MenuItemChild {
  /**
   * @BuilderParam 装饰器:用于声明一个"插槽"属性,接收外部传入的 UI 构建函数
   * - 允许父组件向子组件传递自定义 UI 内容
   * - = this.defaultBuilder 表示设置默认值,当父组件不传递内容时使用
   * - 类型 () => void 表示一个无参数无返回值的构建函数
   */
  @BuilderParam
  slotBuilder: () => void = this.defaultBuilder

  /**
   * @Builder 装饰器:定义一个轻量级的 UI 构建函数
   * - 这里的 defaultBuilder 作为 @BuilderParam 的默认值
   * - 当父组件不传递 slot 内容时,显示此默认 UI
   */
  @Builder
  defaultBuilder() {
    // 默认结构:当父组件未传入自定义内容时显示
    Button("默认结构")
  }

  /**
   * build 方法:组件的 UI 构建入口
   * - 直接调用 slotBuilder() 来渲染传入或默认的 UI 内容
   */
  build() {
    this.slotBuilder()
  }
}


/**
 * 主页面组件
 * 演示 @BuilderParam 的使用方式:
 * 1. 传入自定义内容:MenuItemChild() { Button("父组件") } - 尾随闭包语法
 * 2. 不传入内容:MenuItemChild() - 使用默认的 defaultBuilder
 * 
 * @Entry 装饰器:标记此组件为应用入口页面
 * @ComponentV2 装饰器:标记此结构体为 UI 组件(V2 版本,性能更优)
 */
@ComponentV2
@Entry
struct Index {
  build() {
    Column({ space: 10 }) {

      // 方式1:通过尾随闭包向子组件传递自定义 UI(@BuilderParam 接收)
      MenuItemChild() {
        Button("父组件")
      }

      // 方式2:不传递内容,子组件使用 @BuilderParam 的默认值
      MenuItemChild()

    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)
  }
}

效果如图。

@LocalBuilder装饰器

@LocalBuilder的作用和**@Builder**类似,都可以实现组件内的组件复用。但是两者存在区别

传递给子组件的参数中的 this 指向:

  • @Builder:指向子组件(this 上下文改变)
  • @LocalBuilder:指向父组件(this 上下文保持不变)
/**
 * 子组件:演示 @BuilderParam 接收外部传入的 UI 构建函数
 */
@ComponentV2
struct MenuItemChild {
  /**
   * @Local 装饰器:声明组件内的本地状态变量(V2 状态管理)
   */
  @Local title: string = "子组件"
  /**
   * @BuilderParam 装饰器:声明一个 UI 插槽属性
   * - 用于接收父组件传递的 @Builder@LocalBuilder 函数
   * - 必须由父组件提供值(无默认值时)
   */
  @BuilderParam
  slotBuilder: () => void

  build() {
    this.slotBuilder()
  }
}


@ComponentV2
@Entry
struct Index {
  @Local title: string = "父组件"

  @Builder
  MenuItem1() {
    Button(this.title) // 传递时捕获 title 的当前值
  }

  @LocalBuilder
  MenuItem2() {
    Button(this.title) // 始终引用最新的 title 值
  }

  build() {
    Column({ space: 10 }) {
      // 使用 @Builder this.MenuItem1 中的 this 指向的是 MenuItemChild组件
      MenuItemChild({ slotBuilder: this.MenuItem1 })

      // 使用 @LocalBuilder this.MenuItem2 中的 this 指向的是 当前的Index组件
      MenuItemChild({ slotBuilder: this.MenuItem2 })

    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)
  }
}

效果如图。

wrapBuilder

@Builder存在一个缺陷:当@Builder方法赋值给变量或者数组后,在UI方法中无法使用。

如以下代码:

@Builder
function btn() {
  Button("按钮")
}

const builderArr: Function[] = [btn]

@ComponentV2
@Entry
struct Index {
  build() {
    Column({ space: 10 }) {
      ForEach(builderArr, (item: Function) => {
        item()// 错误:当@Builder方法赋值给变量或者数组后,在UI方法中无法使用。
      })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

wrapBuilder便是来解决这个问题的:

    1. 动态存储多个 @Builder 供运行时选择使用
    1. @Builder 作为参数传递给其他函数/组件
    1. 在循环、数组等场景中复用 @Builder
/**
 * 数据源:用于 ForEach 循环渲染时传递参数
 */
const titles = ["红牛", "康师傅", "小肥羊"]

/**
 * @Builder 函数:定义一个带参数的 UI 构建函数
 * @param text - 按钮显示的文本
 */
@Builder
function btn(text: string) {
  Button(text)
}


/**
 * wrapBuilder 函数详解
 * ─────────────────────────────────────────────────────────────────
 * 【作用】将全局 @Builder 函数包装成可复用的 WrappedBuilder 对象
 *
 * 【为什么需要 wrapBuilder?】
 * - @Builder 函数本身不能直接赋值给变量、不能放入数组、不能作为参数传递
 * - wrapBuilder 将其转换为普通的 JavaScript 对象,支持以上操作
 *
 * 【语法】
 * let 变量名: WrappedBuilder<[参数类型]> = wrapBuilder(builder函数名)
 *
 * 【本例用法】
 * - 将 wrapBuilder 返回的对象放入数组中存储
 * - 数组类型:WrappedBuilder<[string]>[] 表示存储多个包装后的 builder
 * - 每个元素都可以通过 .builder(参数) 调用原始的 @Builder
 *
 *
 * 【注意】
 * - 只能包装全局 @Builder,不能包装组件内的 @Builder
 * - 参数类型必须与 @Builder 函数签名完全匹配
 * ─────────────────────────────────────────────────────────────────
 */
let globalBuilder: WrappedBuilder<[string]>[] = [wrapBuilder(btn), wrapBuilder(btn)]; //可以放入数组

@ComponentV2
@Entry
struct Index {
  build() {
    Column({ space: 10 }) {
      /**
       * 遍历 globalBuilder 数组,调用每个 WrappedBuilder
       * - item: WrappedBuilder<[string]> - 包装后的 builder 对象
       * - item.builder(titles[index]) - 调用原始 @Builder,传入对应参数
       *
       * 执行流程:
       * index=0 → item.builder("红牛") → 渲染 Button("红牛")
       * index=1 → item.builder("康师傅") → 渲染 Button("康师傅")
       */
      ForEach(globalBuilder, (item: WrappedBuilder<[string]>, index: number) => {
        item.builder(titles[index])
      })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

效果如图。

mutableBuilder

当状态变量是wrapBuilder类型时,状态变量发生改变,UI不会跟随改变。此时可以使用mutableBuilder代替wrapBuilder

wrapBuilder的缺陷:

@Builder
function textBuilder(p: string) {
  Text(p)
}

@Builder
function buttonBuilder(p: string) {
  Button(p)
}

@Entry
@ComponentV2
struct Index {
  @Local message: string = 'init';
  @Local text: WrappedBuilder<[string]> = wrapBuilder(textBuilder);

  build() {
    Column() {
      this.text.builder(this.message) // 预期发生更新,但是实际没有更新
      Button("改变").onClick(() => {
        this.text = wrapBuilder(buttonBuilder); // 点击Button, 页面不会发生更新
      })
    }
  }
}

WrappedBuilder替换为mutableBuilder

  @Local text: MutableBuilder<[string]> = mutableBuilder(textBuilder);

完整代码:

@Builder
function textBuilder(p: string) {
  Text(p)
}

@Builder
function buttonBuilder(p: string) {
  Button(p)
}

@Entry
@ComponentV2
struct Index {
  @Local message: string = 'init';
  @Local text: MutableBuilder<[string]> = mutableBuilder(textBuilder);

  build() {
    Column({ space: 10 }) {
      this.text.builder(this.message) // 预期发生更新,但是实际没有更新
      Button("改变").onClick(() => {
        this.text = wrapBuilder(buttonBuilder); // 点击Button, 页面不会发生更新
      })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

总结

Builder 类型对比表

Builder 类型核心作用this 指向是否可赋值给变量/数组响应式更新典型使用场景
@Builder轻量级 UI 复用单元调用处的组件❌ 不支持✅ 支持组件内简单 UI 结构复用
@BuilderParamUI 插槽声明子组件自身❌ 不支持✅ 支持父组件向子组件传递自定义 UI 内容
@LocalBuilder组件内 UI 复用(保持 this 上下文)定义处的组件❌ 不支持✅ 支持需要保持原始 this 上下文的 UI 复用
wrapBuilder包装 @Builder 为可传递对象包装后的对象✅ 支持❌ 不支持动态存储、数组循环、参数传递
mutableBuilder可响应式更新的包装器包装后的对象✅ 支持✅ 支持需要响应式更新的动态 Builder 切换

最后

关注我,持续分享鸿蒙开发 + AI 提效的实战技巧。