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

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(轻量优先) |
更多推荐




所有评论(0)