在这里插入图片描述

HarmonyKit | 鸿蒙开发:@Builder 与 @BuilderParam 的插槽模式与作用域规则

引言:Builder 是 ArkUI 独有的武器

Flutter 有 Widget 方法,React 有函数组件,Vue 有 template slot——每个框架都有自己的"可复用 UI 片段"方案。ArkUI 的答案是 @Builder 装饰器——一种比 @Component 更轻量的 UI 构建方式。

在 HarmonyKit 中,@Builder 被用于两个关键场景:主页的 ToolGrid 被 5 个 TabContent 共享调用,文本统计工具的 StatCard 被 6 次调用生成了 6 个统计卡片。这篇文章将拆解 Builder 的作用域规则、参数传递、与 @Component 的边界、以及 @BuilderParam 的插槽模式。

项目仓库:https://atomgit.com/VON-/harmony-kit

@Builder 的本质:轻量级 UI 工厂

@Builder 的本质是一个带 UI DSL 上下文的函数。它和普通函数的区别在于:

  • 普通函数返回 string/number/void
  • @Builder 函数直接输出 UI 组件树
  • @Builder 内部只能使用 UI DSL(与 build() 相同的语法约束)
  • @Builder 不能有 @State——它依赖于父组件的状态

ToolGrid:参数化的复用 Builder

主页 5 个分类 Tab,每个都需要渲染一个工具卡片网格。如果不用 Builder,5 段几乎完全相同的 Grid 代码分布在 5 个 TabContent 中:
在这里插入图片描述

@Builder
ToolGrid(category: string) {
  Scroll() {
    Grid() {
      ForEach(getToolsByCategory(category), (tool: ToolItem) => {
        GridItem() { ToolCard({ tool: tool }) }
      })
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(12).rowsGap(12)
    .padding({ left: 16, right: 16, top: 8, bottom: 80 })
    .width('100%')
  }
  .scrollBar(BarState.Off)
}

5 个 Tab 各自调用 this.ToolGrid(cat)——同一个 Builder,不同的 cat 参数。如果以后网格从 2 列改为 3 列,只需要改 columnsTemplate('1fr 1fr 1fr')——5 个 Tab 同时生效。

这种模式的另一个好处是:减少缩进深度。 如果不用 Builder,每个 TabContent 内部需要完整写一遍 Grid + Scroll + ForEach + ToolCard,缩进可以达到 8 层,阅读体验极差。Builder 的抽象让主页的 build() 方法保持扁平:
在这里插入图片描述

build() {
  Column() {
    HdsTabs() {
      TabContent() { this.ToolGrid('format') }    // 格式化工具
      TabContent() { this.ToolGrid('encoder') }   // 编解码工具
      TabContent() { this.ToolGrid('converter') } // 转换工具
      TabContent() { this.ToolGrid('generator') } // 生成工具
      TabContent() { this.ToolGrid('analyzer') }  // 分析工具
    }
  }
}

一眼能看出主页的结构:5 个 Tab,每个内部是同类工具的网格。不需要关心 Grid 内部具体怎么布局——那是 ToolGrid Builder 的事。

StatCard:为什么用 Builder 而不用 Component

文本统计工具页需要 6 个统计卡片,展示"总字符数"“字节数”"行数"等指标。每个卡片结构完全相同:一个大数字 + 一个小标签。

@Builder
StatCard(label: string, value: string, color: string) {
  Column() {
    Text(value).fontSize(20).fontWeight(FontWeight.Bold)
      .fontColor(color).fontFamily('monospace');
    Text(label).fontSize(10).fontColor('#999').margin({ top: 4 });
  }
  .width('100%').padding({ top: 14, bottom: 14 })
  .backgroundColor('#ffffff').borderRadius(10);
}

调用:this.StatCard('总字符数', String(this.charCount), '#007aff')

为什么不用 @Component?StatCard 不需要自己的状态,不需要生命周期回调,不需要属性装饰器。它只是一个纯函数——给定参数,返回 UI。Builder 是这种场景下最轻量的选择。

如果用 @Component,每个 StatCard 会创建独立的组件实例,产生额外的实例管理开销。6 个 StatCard 就是 6 个组件实例。"无状态时用 Builder,有状态时用 Component"是值得养成的代码习惯。

// 使用 Builder 的完整上下文
build() {
  Column() {
    // 6 次调用,生成 6 个统计卡片
    Row() {
      this.StatCard('总字符数', String(this.charCount), '#007aff')
      this.StatCard('字节数', String(this.byteCount), '#34c759')
    }
    Row() {
      this.StatCard('行数', String(this.lineCount), '#ff9500')
      this.StatCard('单词数', String(this.wordCount), '#ff3b30')
    }
    Row() {
      this.StatCard('中文字数', String(this.cjkCount), '#5856d6')
      this.StatCard('空格数', String(this.spaceCount), '#af52de')
    }
  }
}

这种"6 次调用产生 6 个卡片"的模式清晰直观。如果用 @Component,需要定义 StatCard 组件并传入 6 组不同的参数,组件注册和声明的开销对于"纯展示型"组件来说过于沉重。

@Builder 的作用域陷阱

Builder 可以访问 @State/@Prop 和传入的参数,但不能访问普通类成员变量

// 错误
private prefix: string = '工具: ';
@Builder myBuilder(name: string) {
  Text(this.prefix + name)  // 编译错误!Builder 不能访问非响应式成员
}

// 正确:声明为 @State
@State prefix: string = '工具: ';

限制是合理的。Builder 不创建独立组件实例,运行在父组件的上下文中。如果 Builder 可以随意访问父组件所有成员,任意改动都可能破坏 Builder 的渲染。限制为只能访问响应式变量,让 Builder 的依赖关系显式可追踪。

更完整的说明:@Builder 中可以访问:

  • ✅ 通过参数传入的变量:@Builder myBuilder(param: string) { Text(param) }
  • ✅ 当前组件的 @State / @Prop 变量:Text(this.stateVar)
  • ✅ 组件方法(getter 方法):Text(this.getDisplayText())
  • ❌ 普通类成员变量(非 @State/@Prop):private temp: string

这个限制在实际开发中几乎不会造成困扰——需要展示的数据天然应该是 @State 或 @Prop,如果某个变量不应该是响应式的,那它很可能不需要在 UI 中展示。

@BuilderParam:插槽模式

@BuilderParam 是 ArkUI 的"插槽"——父组件将自己的 @Builder 传给子组件,子组件在指定位置渲染。类似于 React 的 render props:

// 子组件:暴露一个 Builder 插槽
@Component
struct CustomContainer {
  @BuilderParam header: () => void;
  
  build() {
    Column() {
      this.header()     // 渲染父组件传入的 header
      Text('Content')
    }
  }
}

// 父组件:定义并传入 Builder
@Builder myHeader() { Text('Header').fontSize(20) }
CustomContainer({ header: this.myHeader })

@BuilderParam 的典型应用场景是容器组件——弹窗、对话框、抽屉等。容器提供"壳"(背景、动画、关闭按钮),业务提供"肉"(内容区域):

// 通用弹窗组件
@Component
export struct ModalDialog {
  @BuilderParam content: () => void;
  @State visible: boolean = false;

  build() {
    Stack() {
      if (this.visible) {
        Column() {
          // 弹窗背景
          Column() {
            // 标题栏
            Row() {
              Text('提示').fontSize(18)
            }
            .padding(16)
            // 业务内容 — 由 @BuilderParam 注入
            this.content()
          }
          .width('80%')
          .backgroundColor('#fff')
          .borderRadius(12)
        }
        .width('100%').height('100%')
        .backgroundColor('rgba(0,0,0,0.3)')
        .onClick(() => { this.visible = false; })
      }
    }
  }
}

// 使用
@Builder confirmContent() {
  Column() {
    Text('确定要删除吗?')
    Row() {
      Button('取消')
      Button('确定').backgroundColor('#ff3b30')
    }
  }
}

ModalDialog({ content: this.confirmContent })

HarmonyKit 暂未使用此模式。但对于容器组件(弹出面板、对话框、抽屉)极有价值——“框架写壳,业务写肉”。

@Builder 的全局 vs 局部作用域

在 ArkUI 中,@Builder 可以定义在组件内部(局部 Builder)或组件外部(全局 Builder):

// 全局 Builder — 在任何组件中都能引用
@Builder function GlobalBuilder() {
  Text('全局 Builder')
}

// 局部 Builder — 只能在当前组件中通过 this 引用
@Component
struct MyComponent {
  @Builder LocalBuilder() {
    Text('局部 Builder')
  }

  build() {
    Column() {
      GlobalBuilder()     // 直接调用
      this.LocalBuilder() // 通过 this 调用
    }
  }
}

全局 Builder 适用于所有页面共享的 UI 片段(如统一错误提示样式)。局部 Builder 适用于页面内部复用的 UI 片段。

Builder 的另一个受限之处:全局 @Builder 不能访问任何组件的 @State。 它只能使用传入的参数。这使得全局 Builder 更像一个纯粹的 UI 函数——输入参数,输出 UI,没有副作用。

@Builder 的性能考量

@Builder 不创建独立组件实例,所以它的性能开销比 @Component 更小。在需要频繁调用(如 ForEach 中的每个 item)或创建大量实例(如 Grid 中的 GridItem)的场景中,Builder 比 Component 更高效。

但这不意味着"永远用 Builder 代替 Component"。当 UI 片段需要:

  • 自己的 @State(状态隔离)
  • 生命周期回调(aboutToAppear 等)
  • 被多个页面共享复用

此时应该选择 @Component。Builder 适合轻量、无状态、局部复用的场景。

Builder vs Component 决策树

需求 选择
需要 @State @Component
需要生命周期 @Component
需要 @Prop/@Consume @Component
纯展示、无状态 @Builder
跨组件传递 UI 片段 @BuilderParam
全局复用(多个页面共享) 全局 @Builder 或 @Component
页面内部局部复用 局部 @Builder
ForEach/Grid 中的重复项 @Builder(轻量优先)

项目仓库:https://atomgit.com/VON-/harmony-kit

Logo

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

更多推荐