HarmonyOS 组件封装与复用最佳实践(上篇)
先说个场景:你有个页面,上面有一堆组件。用户刚打开页面的时候,其实只需要看到一部分内容,但如果你把所有组件都创建好了等着,那启动速度肯定慢。动态操作就是解决这个问题的。它允许你在非 build 生命周期中创建组件,也就是提前创建好,等需要的时候直接拿出来用。这里有个核心概念叫组件预创建。在声明式范式中,组件只能在 build 环节中被创建,你没法在其他生命周期阶段创建组件。但 ArkUI 提供了
写给那些在 ArkUI 里摸爬滚打的开发者们
开篇:为什么我们要折腾组件封装与复用
写代码这事儿,说白了就是重复劳动。今天写个按钮,明天写个列表,后天又要写个按钮。你要是每次都从零开始,那这辈子也别想下班了。
HarmonyOS 的 ArkUI 框架给咱们提供了一套挺不错的机制,让你能把那些重复的东西封装起来,下次直接用。但这事儿没那么简单,封装得不好,复用不起来;复用得不对,性能反而更差。
我花了些时间,把华为官方文档里关于组件封装和复用的内容扒拉了一遍,结合自己踩过的坑,写了这么一篇文章。不整那些虚头巴脑的概念,就讲实际开发中怎么用、怎么避坑。
一、组件动态操作:让组件在需要的时候才出现
1.1 什么是动态操作
先说个场景:你有个页面,上面有一堆组件。用户刚打开页面的时候,其实只需要看到一部分内容,但如果你把所有组件都创建好了等着,那启动速度肯定慢。
动态操作就是解决这个问题的。它允许你在非 build 生命周期中创建组件,也就是提前创建好,等需要的时候直接拿出来用。
这里有个核心概念叫组件预创建。在声明式范式中,组件只能在 build 环节中被创建,你没法在其他生命周期阶段创建组件。但 ArkUI 提供了 UI 动态操作,支持组件的预创建。

图:组件动态操作架构示意
1.2 动态操作的核心:FrameNode 和 NodeController
要用动态操作,你得先了解两个东西:FrameNode 和 NodeController。
FrameNode 是 ArkUI 里的自定义节点。跟普通的自定义组件不一样,FrameNode 不需要创建组件对象和状态变量,也不需要收集依赖关系,所以创建速度特别快。
NodeController 是个抽象类,你得继承它才能实现动态操作。它主要负责管理自定义节点的创建、显示、更新等操作。

图:NodeController 生命周期流程
下面这段代码展示了怎么实现一个最简单的动态节点:
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
class Params {
text: string = 'Hello World';
constructor(text: string) {
this.text = text;
}
}
@Builder
function testBuilder(params: Params) {
Column() {
Text(params.text)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.margin({bottom: 36})
}
}
class TextNodeController extends NodeController {
private textNode: BuilderNode<[Params]> | null = null;
private message: string = '';
constructor(message: string) {
super();
this.message = message;
}
makeNode(context: UIContext): FrameNode | null {
// 创建 BuilderNode 实例
this.textNode = new BuilderNode(context);
// 构建组件树
this.textNode.build(wrapBuilder<[Params]>(testBuilder), new Params(this.message));
// 返回要显示的节点
return this.textNode.getFrameNode();
}
}
这段代码干了这么几件事:
- 定义了一个
Params类,用来传递参数 - 用
@Builder装饰器定义了一个构建函数testBuilder - 继承
NodeController实现TextNodeController - 在
makeNode()方法里创建并返回节点
1.3 怎么显示动态节点
创建好 NodeController 之后,你得用 NodeContainer 来显示它:
@Entry
@Component
struct Index {
@State message: string = "hello";
private textNodeController: TextNodeController = new TextNodeController(this.message);
build() {
Row() {
Column() {
NodeContainer(this.textNodeController)
.width('100%')
.height(100)
.backgroundColor('#FFF0F0F0')
}
.width('100%')
.height('100%')
}
.height('100%')
}
}
NodeContainer 会调用 NodeController 的 makeNode() 方法,把返回的节点显示出来。

图:动态节点显示流程
1.4 动态更新和删除
有时候你需要替换节点,或者把节点删掉。这时候可以用 rebuild() 方法:
class TextNodeController extends NodeController {
private textNode: BuilderNode<[Params]> | null = null;
private message: string = '';
constructor(message: string) {
super();
this.message = message;
}
makeNode(context: UIContext): FrameNode | null {
if (this.textNode == null) {
this.textNode = new BuilderNode(context);
this.textNode.build(wrapBuilder<[Params]>(testBuilder), new Params(this.message));
}
return this.textNode.getFrameNode();
}
replaceBuilderNode(newNode: BuilderNode<Object[]>) {
this.textNode = newNode;
// rebuild 方法会重新调用 makeNode
this.rebuild();
}
}
删除节点更简单,让 makeNode() 返回 null 就行:
remove() {
this.isRemove = true;
}
makeNode(context: UIContext): FrameNode | null {
if (this.isRemove) {
return null;
}
// ...
}
1.5 NodeController 的生命周期
NodeController 有几个重要的生命周期函数,你得知道:
makeNode():必须重写,用于构建节点树,返回节点挂载到 NodeContaineraboutToResize():节点布局时回调,入参是布局大小aboutToAppear():节点出现时回调aboutToDisappear():节点消失时回调onTouchEvent():收到触摸事件时回调rebuild():必须实现,用于刷新节点

1.6 实战案例:列表流广告
说个实际场景:列表流广告。就是在你刷新闻、刷商品的时候,中间穿插的广告条目。
这种广告的布局和内容在开发阶段是不确定的,可能是图文、视频或者其他形式。通常是在运行阶段,根据服务器下发的数据来构建布局。

实现方案大概是这样的:
- 用列表数据构建 List 布局
- 根据数据类型分别执行对应逻辑,如果是广告类型,用
NodeContainer进行预占位 - 当
NodeContainer渲染时,发起请求获取广告信息 - 解析数据明确广告类型后,构建具体的广告布局
- 布局构建完成后,返回 rootNode 实现组件上树
核心代码:
export class AdNodeController extends NodeController {
private rootNode: FrameNode | null = null;
private adNode: BuilderNode<[AdParams]> | null = null;
private isRemove: boolean = false;
private uiContext?: UIContext;
makeNode(): FrameNode | null {
if (this.isRemove) {
return null;
}
if (this.rootNode != null) {
return this.rootNode;
}
return null;
}
initAd(uiContext: UIContext, id: string, adType: string) {
this.uiContext = uiContext;
this.rootNode = new FrameNode(this.uiContext);
this.adNode = new BuilderNode(this.uiContext);
this.adNode.build(wrapBuilder(adBuilder), { id: id, isVideo: adType === 'video' });
this.rootNode.getRenderNode()?.appendChild(this.adNode.getFrameNode()?.getRenderNode());
}
remove() {
this.isRemove = true;
}
}
在列表中使用:
List({ space: 3 }) {
LazyForEach(this.data, (item: CardData) => {
ListItem() {
if (item.isAdCard()) {
// 广告项,用 NodeContainer 占位
NodeContainer(getAdNodeController(this.getUIContext(), item.getId()))
.width($r('app.string.percent_100'));
} else {
// 普通项
CardComponent({ cardData: item });
}
}
}, (item: CardData) => item.getId())
}
关闭广告的时候,调用 node.remove() 和 node.rebuild() 就行:
Button($r('app.string.text_dialog_shield'))
.onClick(() => {
let node: AdNodeController | undefined = nodeMap.get(this.adId);
if (node !== undefined) {
node.remove();
node.rebuild();
}
this.dialogController.close();
})
1.7 性能对比
官方给了个性能对比数据。同样的场景,用声明式开发范式完成时延是 13.7ms,用 FrameNode 扩展模式下是 6.1ms。
这个数据仅供参考,实际会因设备和场景不同而有差异。但能看出来,动态操作在性能上确实有优势。
这篇我们讲到这里就结束了。下一篇我们将讲解组件封装的三种方式。
更多推荐



所有评论(0)