HarmonyOS 组件封装与复用最佳实践(上篇)
写给那些在 ArkUI 里摸爬滚打的开发者们开篇为什么我们要折腾组件封装与复用写代码这事儿说白了就是重复劳动。今天写个按钮明天写个列表后天又要写个按钮。你要是每次都从零开始那这辈子也别想下班了。HarmonyOS 的 ArkUI 框架给咱们提供了一套挺不错的机制让你能把那些重复的东西封装起来下次直接用。但这事儿没那么简单封装得不好复用不起来复用得不对性能反而更差。我花了些时间把华为官方文档里关于组件封装和复用的内容扒拉了一遍结合自己踩过的坑写了这么一篇文章。不整那些虚头巴脑的概念就讲实际开发中怎么用、怎么避坑。一、组件动态操作让组件在需要的时候才出现1.1 什么是动态操作先说个场景你有个页面上面有一堆组件。用户刚打开页面的时候其实只需要看到一部分内容但如果你把所有组件都创建好了等着那启动速度肯定慢。动态操作就是解决这个问题的。它允许你在非 build 生命周期中创建组件也就是提前创建好等需要的时候直接拿出来用。这里有个核心概念叫组件预创建。在声明式范式中组件只能在 build 环节中被创建你没法在其他生命周期阶段创建组件。但 ArkUI 提供了 UI 动态操作支持组件的预创建。图组件动态操作架构示意1.2 动态操作的核心FrameNode 和 NodeController要用动态操作你得先了解两个东西FrameNode 和 NodeController。FrameNode是 ArkUI 里的自定义节点。跟普通的自定义组件不一样FrameNode 不需要创建组件对象和状态变量也不需要收集依赖关系所以创建速度特别快。NodeController是个抽象类你得继承它才能实现动态操作。它主要负责管理自定义节点的创建、显示、更新等操作。图NodeController 生命周期流程下面这段代码展示了怎么实现一个最简单的动态节点import{BuilderNode,FrameNode,NodeController}fromkit.ArkUI;classParams{text:stringHello World;constructor(text:string){this.texttext;}}BuilderfunctiontestBuilder(params:Params){Column(){Text(params.text).fontSize(50).fontWeight(FontWeight.Bold).margin({bottom:36})}}classTextNodeControllerextendsNodeController{privatetextNode:BuilderNode[Params]|nullnull;privatemessage:string;constructor(message:string){super();this.messagemessage;}makeNode(context:UIContext):FrameNode|null{// 创建 BuilderNode 实例this.textNodenewBuilderNode(context);// 构建组件树this.textNode.build(wrapBuilder[Params](testBuilder),newParams(this.message));// 返回要显示的节点returnthis.textNode.getFrameNode();}}这段代码干了这么几件事定义了一个Params类用来传递参数用Builder装饰器定义了一个构建函数testBuilder继承NodeController实现TextNodeController在makeNode()方法里创建并返回节点1.3 怎么显示动态节点创建好 NodeController 之后你得用NodeContainer来显示它EntryComponentstruct Index{Statemessage:stringhello;privatetextNodeController:TextNodeControllernewTextNodeController(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()方法classTextNodeControllerextendsNodeController{privatetextNode:BuilderNode[Params]|nullnull;privatemessage:string;constructor(message:string){super();this.messagemessage;}makeNode(context:UIContext):FrameNode|null{if(this.textNodenull){this.textNodenewBuilderNode(context);this.textNode.build(wrapBuilder[Params](testBuilder),newParams(this.message));}returnthis.textNode.getFrameNode();}replaceBuilderNode(newNode:BuilderNodeObject[]){this.textNodenewNode;// rebuild 方法会重新调用 makeNodethis.rebuild();}}删除节点更简单让makeNode()返回null就行remove(){this.isRemovetrue;}makeNode(context:UIContext):FrameNode|null{if(this.isRemove){returnnull;}// ...}1.5 NodeController 的生命周期NodeController有几个重要的生命周期函数你得知道makeNode()必须重写用于构建节点树返回节点挂载到 NodeContaineraboutToResize()节点布局时回调入参是布局大小aboutToAppear()节点出现时回调aboutToDisappear()节点消失时回调onTouchEvent()收到触摸事件时回调rebuild()必须实现用于刷新节点1.6 实战案例列表流广告说个实际场景列表流广告。就是在你刷新闻、刷商品的时候中间穿插的广告条目。这种广告的布局和内容在开发阶段是不确定的可能是图文、视频或者其他形式。通常是在运行阶段根据服务器下发的数据来构建布局。实现方案大概是这样的用列表数据构建 List 布局根据数据类型分别执行对应逻辑如果是广告类型用NodeContainer进行预占位当NodeContainer渲染时发起请求获取广告信息解析数据明确广告类型后构建具体的广告布局布局构建完成后返回 rootNode 实现组件上树核心代码exportclassAdNodeControllerextendsNodeController{privaterootNode:FrameNode|nullnull;privateadNode:BuilderNode[AdParams]|nullnull;privateisRemove:booleanfalse;privateuiContext?:UIContext;makeNode():FrameNode|null{if(this.isRemove){returnnull;}if(this.rootNode!null){returnthis.rootNode;}returnnull;}initAd(uiContext:UIContext,id:string,adType:string){this.uiContextuiContext;this.rootNodenewFrameNode(this.uiContext);this.adNodenewBuilderNode(this.uiContext);this.adNode.build(wrapBuilder(adBuilder),{id:id,isVideo:adTypevideo});this.rootNode.getRenderNode()?.appendChild(this.adNode.getFrameNode()?.getRenderNode());}remove(){this.isRemovetrue;}}在列表中使用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((){letnode:AdNodeController|undefinednodeMap.get(this.adId);if(node!undefined){node.remove();node.rebuild();}this.dialogController.close();})1.7 性能对比官方给了个性能对比数据。同样的场景用声明式开发范式完成时延是 13.7ms用 FrameNode 扩展模式下是 6.1ms。这个数据仅供参考实际会因设备和场景不同而有差异。但能看出来动态操作在性能上确实有优势。这篇我们讲到这里就结束了。下一篇我们将讲解组件封装的三种方式。