1# 自定义声明式节点 (BuilderNode) 2 3<!--Kit: ArkUI--> 4<!--Subsystem: ArkUI--> 5<!--Owner: @xiang-shouxing--> 6<!--Designer: @xiang-shouxing--> 7<!--Tester: @sally__--> 8<!--Adviser: @HelloCrease--> 9 10## 概述 11 12自定义声明式节点 ([BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md))提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过[全局自定义构建函数](../ui/state-management/arkts-builder.md#全局自定义构建函数)@Builder定制组件树。组件树的根[FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md)节点可通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取,该节点既可直接由[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)返回并挂载于[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)节点下,亦可在FrameNode树与[RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md)树中嵌入声明式组件,实现混合显示。同时,BuilderNode具备纹理导出功能,导出的纹理可在[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)中实现同层渲染。 13 14由BuilderNode构建的ArkTS组件树,支持与自定义节点(如FrameNode、RenderNode)关联使用,确保了系统组件与自定义节点的混合显示效果。对于需与自定义节点对接的第三方框架,BuilderNode提供了嵌入系统组件的方法。 15 16此外,BuilderNode还提供了组件预创建的能力,能够自定义系统组件的创建开始的时间,在后续业务中实现动态挂载与显示。此功能尤其适用于初始化耗时较长的声明式组件,如[Web](../reference/apis-arkweb/arkts-basic-components-web.md)、[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)等,通过预创建,可以有效减少初始化时间,优化组件加载效率。 17 18 19 20## 基本概念 21 22- 系统组件:组件是UI的必要元素,形成了在界面中的样子,由ArkUI直接提供的称为[系统组件](arkts-ui-development-overview.md)。 23 24- 实体节点:由后端创建的Native节点。 25 26BuilderNode仅可作为叶子节点进行使用。如有更新需要,建议通过BuilderNode中的[update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update)方式触发更新,不建议通过BuilderNode中获取的RenderNode对节点进行修改操作。 27 28> **说明:** 29> 30> - BuilderNode只支持一个由[wrapBuilder](../ui/state-management/arkts-wrapBuilder.md)包装的[全局自定义构建函数](../ui/state-management/arkts-builder.md#全局自定义构建函数)@Builder。 31> 32> - 一个新建的BuilderNode在[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build)之后才能通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取到一个指向根节点的FrameNode对象,否则返回null。 33> 34> - 如果传入的Builder的根节点为语法节点(if/else/foreach/...),需要额外生成一个FrameNode,在节点树中的显示为“BuilderProxyNode”。 35> 36> - 如果BuilderNode通过getFrameNode将节点挂载在另一个FrameNode上,或者将其作为子节点挂载在NodeContainer节点上。则节点中使用父组件的布局约束进行布局。 37> 38> - 如果BuilderNode的FrameNode通过[getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode)形式将自己的节点挂载在RenderNode节点上,由于其FrameNode未上树,其大小默认为0,需要通过构造函数中的[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)显式指定布局约束大小,才能正常显示。 39> 40> - BuilderNode的预加载并不会减少组件的创建时间。Web组件创建的时候需要在内核中加载资源,预创建不能减少Web组件的创建的时间,但是可以让内核进行预加载,减少正式使用时候内核的加载耗时。 41 42## 创建BuilderNode对象 43 44BuilderNode对象为一个模板类,需要在创建的时候指定类型。该类型需要与后续build方法中传入的[WrappedBuilder](../ui/state-management/arkts-wrapBuilder.md)的类型保持一致,否则会存在编译告警导致编译失败。 45 46## 创建组件树 47 48通过BuilderNode的build可以实现组件树的创建。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。 49 50> **说明:** 51> 52> 无状态的UI方法全局@Builder最多拥有一个根节点。 53> 54> build方法中对应的@Builder支持一个参数作为入参。 55> 56> build中对于@Builder嵌套@Builder进行使用的场景,需要保证嵌套的参数与build的中提供的入参一致。 57> 58> 对于@Builder嵌套@Builder进行使用的场景,如果入参类型不一致,则要求增加[BuilderOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildoptions12)字段作为[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build12)的入参。 59> 60> 需要操作BuilderNode中的对象时,需要保证其引用不被回收。当BuilderNode对象被虚拟机回收之后,它的FrameNode、RenderNode对象也会与后端节点解引用。即从BuilderNode中获取的FrameNode对象不对应任何一个节点。 61 62创建离线节点以及组件树,结合FrameNode进行使用。 63 64BuilderNode的根节点直接作为[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)的[makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode)返回值。 65 66```ts 67import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 68 69class Params { 70 text: string = ""; 71 72 constructor(text: string) { 73 this.text = text; 74 } 75} 76 77@Builder 78function buildText(params: Params) { 79 Column() { 80 Text(params.text) 81 .fontSize(50) 82 .fontWeight(FontWeight.Bold) 83 .margin({ bottom: 36 }) 84 } 85} 86 87class TextNodeController extends NodeController { 88 private textNode: BuilderNode<[Params]> | null = null; 89 private message: string = "DEFAULT"; 90 91 constructor(message: string) { 92 super(); 93 this.message = message; 94 } 95 96 makeNode(context: UIContext): FrameNode | null { 97 this.textNode = new BuilderNode(context); 98 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)) 99 return this.textNode.getFrameNode(); 100 } 101} 102 103@Entry 104@Component 105struct Index { 106 @State message: string = "hello"; 107 108 build() { 109 Row() { 110 Column() { 111 NodeContainer(new TextNodeController(this.message)) 112 .width('100%') 113 .height(100) 114 .backgroundColor('#FFF0F0F0') 115 } 116 .width('100%') 117 .height('100%') 118 } 119 .height('100%') 120 } 121} 122``` 123 124将BuilderNode与RenderNode进行结合使用。 125 126BuilderNode的RenderNode挂载其它RenderNode下时,需要明确定义[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)的大小作为BuilderNode的布局约束。不推荐通过该方式挂载节点。 127 128```ts 129import { NodeController, BuilderNode, FrameNode, UIContext, RenderNode } from "@kit.ArkUI"; 130 131class Params { 132 text: string = ""; 133 134 constructor(text: string) { 135 this.text = text; 136 } 137} 138 139@Builder 140function buildText(params: Params) { 141 Column() { 142 Text(params.text) 143 .fontSize(50) 144 .fontWeight(FontWeight.Bold) 145 .margin({ bottom: 36 }) 146 } 147} 148 149class TextNodeController extends NodeController { 150 private rootNode: FrameNode | null = null; 151 private textNode: BuilderNode<[Params]> | null = null; 152 private message: string = "DEFAULT"; 153 154 constructor(message: string) { 155 super(); 156 this.message = message; 157 } 158 159 makeNode(context: UIContext): FrameNode | null { 160 this.rootNode = new FrameNode(context); 161 let renderNode = new RenderNode(); 162 renderNode.clipToFrame = false; 163 this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } }); 164 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); 165 const textRenderNode = this.textNode?.getFrameNode()?.getRenderNode(); 166 167 const rootRenderNode = this.rootNode.getRenderNode(); 168 if (rootRenderNode !== null) { 169 rootRenderNode.appendChild(renderNode); 170 renderNode.appendChild(textRenderNode); 171 } 172 173 return this.rootNode; 174 } 175} 176 177@Entry 178@Component 179struct Index { 180 @State message: string = "hello"; 181 182 build() { 183 Row() { 184 Column() { 185 NodeContainer(new TextNodeController(this.message)) 186 .width('100%') 187 .height(100) 188 .backgroundColor('#FFF0F0F0') 189 } 190 .width('100%') 191 .height('100%') 192 } 193 .height('100%') 194 } 195} 196``` 197 198## 更新组件树 199 200通过BuilderNode对象的build创建组件树。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。 201 202自定义组件的更新遵循[状态管理](../ui/state-management/arkts-state-management-overview.md)的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为[\@Prop](../ui/state-management/arkts-prop.md)或者[\@ObjectLink](../ui/state-management/arkts-observed-and-objectlink.md)。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。 203 204 205使用update更新BuilderNode中的节点。 206 207使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)触发BuilderNode中节点的全量更新。 208 209更新BuilderNode中的节点。 210 211```ts 212import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 213 214class Params { 215 text: string = ""; 216 constructor(text: string) { 217 this.text = text; 218 } 219} 220 221// 自定义组件 222@Component 223struct TextBuilder { 224 // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop 225 @Prop message: string = "TextBuilder"; 226 227 build() { 228 Row() { 229 Column() { 230 Text(this.message) 231 .fontSize(50) 232 .fontWeight(FontWeight.Bold) 233 .margin({ bottom: 36 }) 234 .backgroundColor(Color.Gray) 235 } 236 } 237 } 238} 239 240@Builder 241function buildText(params: Params) { 242 Column() { 243 Text(params.text) 244 .fontSize(50) 245 .fontWeight(FontWeight.Bold) 246 .margin({ bottom: 36 }) 247 TextBuilder({ message: params.text }) // 自定义组件 248 } 249} 250 251class TextNodeController extends NodeController { 252 private textNode: BuilderNode<[Params]> | null = null; 253 private message: string = ""; 254 255 constructor(message: string) { 256 super() 257 this.message = message 258 } 259 260 makeNode(context: UIContext): FrameNode | null { 261 this.textNode = new BuilderNode(context); 262 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)) 263 return this.textNode.getFrameNode(); 264 } 265 266 update(message: string) { 267 if (this.textNode !== null) { 268 // 调用update进行更新。 269 this.textNode.update(new Params(message)); 270 } 271 } 272} 273 274@Entry 275@Component 276struct Index { 277 @State message: string = "hello"; 278 private textNodeController: TextNodeController = new TextNodeController(this.message); 279 private count = 0; 280 281 build() { 282 Row() { 283 Column() { 284 NodeContainer(this.textNodeController) 285 .width('100%') 286 .height(200) 287 .backgroundColor('#FFF0F0F0') 288 Button('Update') 289 .onClick(() => { 290 this.count += 1; 291 const message = "Update " + this.count.toString(); 292 this.textNodeController.update(message); 293 }) 294 } 295 .width('100%') 296 .height('100%') 297 } 298 .height('100%') 299 } 300} 301``` 302 303## 解除实体节点引用关系 304 305由于BuilderNode对应的是后端的实体节点,正常的内存释放依赖前端对象的回收。如果期望直接释放后端的节点对象,则可以通过调用[dispose](../reference/apis-arkui/js-apis-arkui-builderNode.md#dispose12)与实体节点解除引用关系,此时持有的前端BuilderNode对象不再影响实体节点的生命周期。 306 307> **说明:** 308> 309> 当BuilderNode对象调用dispose之后,不仅BuilderNode对象与后端实体节点解除引用关系,BuilderNode中的FrameNode与RenderNode也会同步和实体节点解除引用关系。 310> 311> 若前端对象BuilderNode无法释放,容易导致内存泄漏。建议在不再需要对该BuilderNode对象进行操作时,开发者应主动调用dispose释放后端节点,以减少引用关系的复杂性,降低内存泄漏的风险。 312 313## 注入触摸事件 314 315BuilderNode中提供了[postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent),可以通过该接口向BuilderNode中绑定的组件注入[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md),实现事件的模拟转发。 316 317通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。 318 319向BuilderNode中的Column组件转发另一个Column接收的事件,即点击下方的Column组件,上方的Column组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。 320 321```ts 322import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI'; 323 324class Params { 325 text: string = "this is a text"; 326} 327 328@Builder 329function ButtonBuilder(params: Params) { 330 Column() { 331 Button(`button ` + params.text) 332 .borderWidth(2) 333 .backgroundColor(Color.Orange) 334 .width("100%") 335 .height("100%") 336 .gesture( 337 TapGesture() 338 .onAction((event: GestureEvent) => { 339 console.info("TapGesture"); 340 }) 341 ) 342 } 343 .width(500) 344 .height(300) 345 .backgroundColor(Color.Gray) 346} 347 348class MyNodeController extends NodeController { 349 private rootNode: BuilderNode<[Params]> | null = null; 350 private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder); 351 352 makeNode(uiContext: UIContext): FrameNode | null { 353 this.rootNode = new BuilderNode(uiContext); 354 this.rootNode.build(this.wrapBuilder, { text: "this is a string" }) 355 return this.rootNode.getFrameNode(); 356 } 357 358 postTouchEvent(touchEvent: TouchEvent): void { 359 if (this.rootNode == null) { 360 return; 361 } 362 let result = this.rootNode.postTouchEvent(touchEvent); 363 console.info("result " + result); 364 } 365} 366 367@Entry 368@Component 369struct MyComponent { 370 private nodeController: MyNodeController = new MyNodeController(); 371 372 build() { 373 Column() { 374 NodeContainer(this.nodeController) 375 .height(300) 376 .width(500) 377 Column() 378 .width(500) 379 .height(300) 380 .backgroundColor(Color.Pink) 381 .onTouch((event) => { 382 if (event != undefined) { 383 this.nodeController.postTouchEvent(event); 384 } 385 }) 386 } 387 } 388} 389``` 390 391## BuilderNode内的BuilderProxyNode导致树结构发生变化 392 393若传入的Builder的根节点为语法节点(if/else/foreach/…)或自定义组件,将额外生成一个FrameNode,在节点树中显示为“BuilderProxyNode”,这会导致树结构变化,影响某些测试的传递过程。 394 395在以下示例中,Column和Row绑定了触摸事件,同时Column设置了[hitTestBehavior](../reference/apis-arkui/arkui-ts/ts-universal-attributes-hit-test-behavior.md#hittestbehavior)属性为[HitTestMode.Transparent](../reference/apis-arkui/arkui-ts/ts-appendix-enums.md#hittestmode9)。然而,由于生成了BuilderProxyNode,且BuilderProxyNode无法设置属性,因此在触摸Column时,Column的触摸测试无法传递到Row上。 396 397 398 399```ts 400import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI'; 401 402@Component 403struct BlueRowComponent { 404 build() { 405 Row() { 406 Row() { 407 } 408 .width('100%') 409 .height('200vp') 410 .backgroundColor(0xFF2787D9) 411 .onTouch((event: TouchEvent) => { 412 // 触摸绿色Column,蓝色Row的触摸事件不触发 413 console.info("blue touched: " + event.type); 414 }) 415 } 416 } 417} 418 419@Component 420struct GreenColumnComponent { 421 build() { 422 Column() { 423 } 424 .width('100%') 425 .height('100vp') 426 .backgroundColor(0xFF17A98D) 427 .hitTestBehavior(HitTestMode.Transparent) 428 .onTouch((event: TouchEvent) => { 429 console.info("green touched: " + event.type); 430 }) 431 } 432} 433 434@Builder 435function buildBlueRow() { 436 // Builder直接挂载自定义组件,生成BuilderProxyNode 437 BlueRowComponent() 438} 439 440@Builder 441function buildGreenColumn() { 442 // Builder直接挂载自定义组件,生成BuilderProxyNode 443 GreenColumnComponent() 444} 445 446class MyNodeController extends NodeController { 447 makeNode(uiContext: UIContext): FrameNode | null { 448 const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer'); 449 450 const blueRowNode = new BuilderNode(uiContext); 451 blueRowNode.build(wrapBuilder(buildBlueRow)); 452 453 const greenColumnNode = new BuilderNode(uiContext); 454 greenColumnNode.build(wrapBuilder(buildGreenColumn)); 455 456 // greenColumnNode覆盖在blueRowNode上 457 relativeContainer.appendChild(blueRowNode.getFrameNode()); 458 relativeContainer.appendChild(greenColumnNode.getFrameNode()); 459 460 return relativeContainer; 461 } 462} 463 464@Entry 465@Component 466struct Index { 467 build() { 468 Column() { 469 NodeContainer(new MyNodeController()) 470 } 471 } 472} 473``` 474 475在上述场景中,若要实现触摸测试的传递,可以使用一个容器组件包裹语法节点或自定义组件,以避免生成BuilderProxyNode,并将容器组件的hitTestBehavior设置为HitTestMode.Transparent,从而向兄弟节点传递触摸测试。 476 477 478 479```ts 480import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI'; 481 482@Component 483struct BlueRowComponent { 484 build() { 485 Row() { 486 Row() { 487 } 488 .width('100%') 489 .height('200vp') 490 .backgroundColor(0xFF2787D9) 491 .onTouch((event: TouchEvent) => { 492 // 触摸绿色Column,蓝色Row的触摸事件触发 493 console.info("blue touched: " + event.type); 494 }) 495 } 496 } 497} 498 499@Component 500struct GreenColumnComponent { 501 build() { 502 Column() { 503 } 504 .width('100%') 505 .height('100vp') 506 .backgroundColor(0xFF17A98D) 507 .hitTestBehavior(HitTestMode.Transparent) 508 .onTouch((event: TouchEvent) => { 509 console.info("green touched: " + event.type); 510 }) 511 } 512} 513 514@Builder 515function buildBlueRow() { 516 // Builder直接挂载自定义组件,生成BuilderProxyNode 517 BlueRowComponent() 518} 519 520@Builder 521function buildGreenColumn() { 522 // Builder根节点为容器组件,不会生成BuilderProxyNode,可以设置属性 523 Stack() { 524 GreenColumnComponent() 525 } 526 .hitTestBehavior(HitTestMode.Transparent) 527} 528 529class MyNodeController extends NodeController { 530 makeNode(uiContext: UIContext): FrameNode | null { 531 const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer'); 532 533 const blueRowNode = new BuilderNode(uiContext); 534 blueRowNode.build(wrapBuilder(buildBlueRow)); 535 536 const greenColumnNode = new BuilderNode(uiContext); 537 greenColumnNode.build(wrapBuilder(buildGreenColumn)); 538 539 // greenColumnNode覆盖在blueRowNode上 540 relativeContainer.appendChild(blueRowNode.getFrameNode()); 541 relativeContainer.appendChild(greenColumnNode.getFrameNode()); 542 543 return relativeContainer; 544 } 545} 546 547@Entry 548@Component 549struct Index { 550 build() { 551 Column() { 552 NodeContainer(new MyNodeController()) 553 } 554 } 555} 556``` 557 558此外,对于自定义组件,可以直接设置属性,此时将额外生成节点__Common__,自定义组件的属性将挂载于__Common__上,同样能够实现上述效果。 559 560 561 562```ts 563import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI'; 564 565@Component 566struct BlueRowComponent { 567 build() { 568 Row() { 569 Row() { 570 } 571 .width('100%') 572 .height('200vp') 573 .backgroundColor(0xFF2787D9) 574 .onTouch((event: TouchEvent) => { 575 // 触摸绿色Column,蓝色Row的触摸事件触发 576 console.info("blue touched: " + event.type); 577 }) 578 } 579 } 580} 581 582@Component 583struct GreenColumnComponent { 584 build() { 585 Column() { 586 } 587 .width('100%') 588 .height('100vp') 589 .backgroundColor(0xFF17A98D) 590 .hitTestBehavior(HitTestMode.Transparent) 591 .onTouch((event: TouchEvent) => { 592 console.info("green touched: " + event.type); 593 }) 594 } 595} 596 597@Builder 598function buildBlueRow() { 599 // Builder直接挂载自定义组件,生成BuilderProxyNode 600 BlueRowComponent() 601} 602 603@Builder 604function buildGreenColumn() { 605 // 给自定义组件设置属性生成__Common__节点,Builder根节点为__Common__节点,不会生成BuilderProxyNode 606 GreenColumnComponent() 607 .hitTestBehavior(HitTestMode.Transparent) 608} 609 610class MyNodeController extends NodeController { 611 makeNode(uiContext: UIContext): FrameNode | null { 612 const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer'); 613 614 const blueRowNode = new BuilderNode(uiContext); 615 blueRowNode.build(wrapBuilder(buildBlueRow)); 616 617 const greenColumnNode = new BuilderNode(uiContext); 618 greenColumnNode.build(wrapBuilder(buildGreenColumn)); 619 620 // greenColumnNode覆盖在blueRowNode上 621 relativeContainer.appendChild(blueRowNode.getFrameNode()); 622 relativeContainer.appendChild(greenColumnNode.getFrameNode()); 623 624 return relativeContainer; 625 } 626} 627 628@Entry 629@Component 630struct Index { 631 build() { 632 Column() { 633 NodeContainer(new MyNodeController()) 634 } 635 } 636} 637``` 638 639## BuilderNode调用reuse和recycle接口实现节点复用能力 640 641调用[reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12)接口和[recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12)接口,将复用和回收事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点内部的自定义组件的复用。 642 643以下面的Demo为例,被复用的自定义组件ReusableChildComponent可以传递复用和回收事件到其下的自定义组件ChildComponent3,但无法传递给自定义组件ChildComponent2,因为被BuilderNode所隔断。因此需要主动调用BuilderNode的reuse和recycle接口,将复用和回收事件传递给自定义组件ChildComponent2,以达成复用效果。 644 645 646 647 648```ts 649import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI"; 650 651const TEST_TAG: string = "Reuse+Recycle"; 652 653class MyDataSource { 654 private dataArray: string[] = []; 655 private listener: DataChangeListener | null = null 656 657 public totalCount(): number { 658 return this.dataArray.length; 659 } 660 661 public getData(index: number) { 662 return this.dataArray[index]; 663 } 664 665 public pushData(data: string) { 666 this.dataArray.push(data); 667 } 668 669 public reloadListener(): void { 670 this.listener?.onDataReloaded(); 671 } 672 673 public registerDataChangeListener(listener: DataChangeListener): void { 674 this.listener = listener; 675 } 676 677 public unregisterDataChangeListener(): void { 678 this.listener = null; 679 } 680} 681 682class Params { 683 item: string = ''; 684 685 constructor(item: string) { 686 this.item = item; 687 } 688} 689 690@Builder 691function buildNode(param: Params = new Params("hello")) { 692 Row() { 693 Text(`C${param.item} -- `) 694 ChildComponent2({ item: param.item }) //该自定义组件在BuilderNode中无法被正确复用 695 } 696} 697 698class MyNodeController extends NodeController { 699 public builderNode: BuilderNode<[Params]> | null = null; 700 public item: string = ""; 701 702 makeNode(uiContext: UIContext): FrameNode | null { 703 if (this.builderNode == null) { 704 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 705 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 706 } 707 return this.builderNode.getFrameNode(); 708 } 709} 710 711// 被回收复用的自定义组件,其状态变量会更新,而子自定义组件ChildComponent3中的状态变量也会更新,但BuilderNode会阻断这一传递过程 712@Reusable 713@Component 714struct ReusableChildComponent { 715 @Prop item: string = ''; 716 @Prop switch: string = ''; 717 private controller: MyNodeController = new MyNodeController(); 718 719 aboutToAppear() { 720 this.controller.item = this.item; 721 } 722 723 aboutToRecycle(): void { 724 console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`); 725 726 // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ChildComponent2,完成复用 727 if (this.switch === 'open') { 728 this.controller?.builderNode?.recycle(); 729 } 730 } 731 732 aboutToReuse(params: object): void { 733 console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`); 734 735 // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ChildComponent2,完成复用 736 if (this.switch === 'open') { 737 this.controller?.builderNode?.reuse(params); 738 } 739 } 740 741 build() { 742 Row() { 743 Text(`A${this.item}--`) 744 ChildComponent3({ item: this.item }) 745 NodeContainer(this.controller); 746 } 747 } 748} 749 750@Component 751struct ChildComponent2 { 752 @Prop item: string = "false"; 753 754 aboutToReuse(params: Record<string, object>) { 755 console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`); 756 } 757 758 aboutToRecycle(): void { 759 console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`); 760 } 761 762 build() { 763 Row() { 764 Text(`D${this.item}`) 765 .fontSize(20) 766 .backgroundColor(Color.Yellow) 767 .margin({ left: 10 }) 768 }.margin({ left: 10, right: 10 }) 769 } 770} 771 772@Component 773struct ChildComponent3 { 774 @Prop item: string = "false"; 775 776 aboutToReuse(params: Record<string, object>) { 777 console.log(`${TEST_TAG} ChildComponent3 aboutToReuse ${JSON.stringify(params)}`); 778 } 779 780 aboutToRecycle(): void { 781 console.log(`${TEST_TAG} ChildComponent3 aboutToRecycle ${this.item}`); 782 } 783 784 build() { 785 Row() { 786 Text(`B${this.item}`) 787 .fontSize(20) 788 .backgroundColor(Color.Yellow) 789 .margin({ left: 10 }) 790 }.margin({ left: 10, right: 10 }) 791 } 792} 793 794 795@Entry 796@Component 797struct Index { 798 @State data: MyDataSource = new MyDataSource(); 799 800 aboutToAppear() { 801 for (let i = 0; i < 100; i++) { 802 this.data.pushData(i.toString()); 803 } 804 } 805 806 build() { 807 Column() { 808 List({ space: 3 }) { 809 LazyForEach(this.data, (item: string) => { 810 ListItem() { 811 ReusableChildComponent({ 812 item: item, 813 switch: 'open' // 将open改为close可观察到,BuilderNode不通过reuse和recycle接口传递复用时,BuilderNode内部的自定义组件的行为表现 814 }) 815 } 816 }, (item: string) => item) 817 } 818 .width('100%') 819 .height('100%') 820 } 821 } 822} 823``` 824 825 826## BuilderNode在子自定义组件中使用@Reusable装饰器 827 828BuilderNode节点的复用机制与使用[@Reusable](./state-management/arkts-reusable.md)装饰器的自定义组件的复用机制会相互冲突。因此,当BuilderNode的子节点为自定义组件时,不支持该自定义组件使用@Reusable装饰器标记,否则将导致应用程序触发JSCrash。若需要使用@Reusable装饰器,应使用一个普通自定义组件包裹该自定义组件。 829 830在下面的示例中,ReusableChildComponent作为BuilderNode的子自定义组件,无法标记为@Reusable。通过ChildComponent2对其包裹,ReusableChildComponent可以使用@Reusable装饰器标记。 831 832 833 834```ts 835import { FrameNode, NodeController, BuilderNode, UIContext } from '@kit.ArkUI'; 836 837const TEST_TAG: string = "Reusable"; 838 839class Params { 840 item: string = ''; 841 842 constructor(item: string) { 843 this.item = item; 844 } 845} 846 847@Builder 848function buildNode(param: Params = new Params("Hello")) { 849 ChildComponent2({ item: param.item }) 850 // 如果直接使用ReusableChildComponent,则会编译报错 851 // ReusableChildComponent({ item: param.item }) 852} 853 854class MyNodeController extends NodeController { 855 public builderNode: BuilderNode<[Params]> | null = null; 856 public item: string = ""; 857 858 constructor(item: string) { 859 super(); 860 this.item = item; 861 } 862 863 makeNode(uiContext: UIContext): FrameNode | null { 864 if (this.builderNode == null) { 865 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 866 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 867 } 868 return this.builderNode.getFrameNode(); 869 } 870} 871 872// 标记了@Reusable的自定义组件,无法直接被BuilderNode挂载为子节点 873@Reusable 874@Component 875struct ReusableChildComponent { 876 @Prop item: string = ''; 877 878 aboutToReuse(params: object): void { 879 console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`); 880 } 881 882 aboutToRecycle(): void { 883 console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`); 884 } 885 886 build() { 887 Text(`A--${this.item}`) 888 } 889} 890 891// 未标记@Reusable的自定义组件 892@Component 893struct ChildComponent2 { 894 @Prop item: string = ""; 895 896 aboutToReuse(params: Record<string, object>) { 897 console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`); 898 } 899 900 aboutToRecycle(): void { 901 console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`); 902 } 903 904 build() { 905 ReusableChildComponent({ item: this.item }) 906 } 907} 908 909 910@Entry 911@Component 912struct Index { 913 @State controller: MyNodeController = new MyNodeController("Child"); 914 915 build() { 916 Column() { 917 NodeContainer(this.controller) 918 } 919 .width('100%') 920 .height('100%') 921 } 922} 923``` 924 925## 通过系统环境变化更新节点 926 927使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)来监听[系统环境变化](../reference/apis-ability-kit/js-apis-app-ability-configuration.md)事件,以触发节点的全量更新。 928 929> **说明:** 930> 931> updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。 932 933```ts 934import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 935import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit'; 936 937class Params { 938 text: string = "" 939 940 constructor(text: string) { 941 this.text = text; 942 } 943} 944 945// 自定义组件 946@Component 947struct TextBuilder { 948 // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop 949 @Prop message: string = "TextBuilder"; 950 951 build() { 952 Row() { 953 Column() { 954 Text(this.message) 955 .fontSize(50) 956 .fontWeight(FontWeight.Bold) 957 .margin({ bottom: 36 }) 958 .fontColor($r(`app.color.text_color`)) 959 .backgroundColor($r(`app.color.start_window_background`)) 960 } 961 } 962 } 963} 964 965@Builder 966function buildText(params: Params) { 967 Column() { 968 Text(params.text) 969 .fontSize(50) 970 .fontWeight(FontWeight.Bold) 971 .margin({ bottom: 36 }) 972 .fontColor($r(`app.color.text_color`)) 973 TextBuilder({ message: params.text }) // 自定义组件 974 }.backgroundColor($r(`app.color.start_window_background`)) 975} 976 977class TextNodeController extends NodeController { 978 private textNode: BuilderNode<[Params]> | null = null; 979 private message: string = ""; 980 981 constructor(message: string) { 982 super() 983 this.message = message; 984 } 985 986 makeNode(context: UIContext): FrameNode | null { 987 return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null; 988 } 989 990 createNode(context: UIContext) { 991 this.textNode = new BuilderNode(context); 992 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); 993 builderNodeMap.push(this.textNode); 994 } 995 996 deleteNode() { 997 let node = builderNodeMap.pop(); 998 node?.dispose(); 999 } 1000 1001 update(message: string) { 1002 if (this.textNode !== null) { 1003 // 调用update进行更新。 1004 this.textNode.update(new Params(message)); 1005 } 1006 } 1007} 1008 1009// 记录创建的自定义节点对象 1010const builderNodeMap: Array<BuilderNode<[Params]>> = new Array(); 1011 1012function updateColorMode() { 1013 builderNodeMap.forEach((value, index) => { 1014 // 通知BuilderNode环境变量改变 1015 value.updateConfiguration(); 1016 }) 1017} 1018 1019@Entry 1020@Component 1021struct Index { 1022 @State message: string = "hello" 1023 private textNodeController: TextNodeController = new TextNodeController(this.message); 1024 private count = 0; 1025 1026 aboutToAppear(): void { 1027 let environmentCallback: EnvironmentCallback = { 1028 onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { 1029 console.info('onMemoryLevel'); 1030 }, 1031 onConfigurationUpdated: (config: Configuration): void => { 1032 console.info('onConfigurationUpdated ' + JSON.stringify(config)); 1033 updateColorMode(); 1034 } 1035 } 1036 // 注册监听回调 1037 this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback); 1038 //创建自定义节点并添加至map 1039 this.textNodeController.createNode(this.getUIContext()); 1040 } 1041 1042 aboutToDisappear(): void { 1043 //移除map中的引用,并将自定义节点释放 1044 this.textNodeController.deleteNode(); 1045 } 1046 1047 build() { 1048 Row() { 1049 Column() { 1050 NodeContainer(this.textNodeController) 1051 .width('100%') 1052 .height(200) 1053 .backgroundColor('#FFF0F0F0') 1054 Button('Update') 1055 .onClick(() => { 1056 this.count += 1; 1057 const message = "Update " + this.count.toString(); 1058 this.textNodeController.update(message); 1059 }) 1060 } 1061 .width('100%') 1062 .height('100%') 1063 } 1064 .height('100%') 1065 } 1066} 1067``` 1068 1069## 跨页面复用注意事项 1070 1071在使用[路由](../reference/apis-arkui/arkts-apis-uicontext-router.md)接口[router.replaceUrl](../reference/apis-arkui/arkts-apis-uicontext-router.md#replaceurl)、[router.back](../reference/apis-arkui/arkts-apis-uicontext-router.md#back)、[router.clear](../reference/apis-arkui/arkts-apis-uicontext-router.md#clear)、[router.replaceNamedRoute](../reference/apis-arkui/arkts-apis-uicontext-router.md#replacenamedroute)操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以[router.replaceNamedRoute](../reference/apis-arkui/arkts-apis-uicontext-router.md#replacenamedroute)为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为false。 1072 1073```ts 1074// ets/pages/Index.ets 1075import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 1076import "ets/pages/PageTwo" 1077 1078@Builder 1079function buildText() { 1080 // @Builder中使用语法节点生成BuilderProxyNode 1081 if (true) { 1082 MyComponent() 1083 } 1084} 1085 1086@Component 1087struct MyComponent { 1088 @StorageLink("isShowText") isShowText: boolean = true; 1089 1090 build() { 1091 if (this.isShowText) { 1092 Column() { 1093 Text("BuilderNode Reuse") 1094 .fontSize(36) 1095 .fontWeight(FontWeight.Bold) 1096 .padding(16) 1097 } 1098 } 1099 } 1100} 1101 1102class TextNodeController extends NodeController { 1103 private rootNode: FrameNode | null = null; 1104 private textNode: BuilderNode<[]> | null = null; 1105 1106 makeNode(context: UIContext): FrameNode | null { 1107 this.rootNode = new FrameNode(context); 1108 1109 if (AppStorage.has("textNode")) { 1110 // 复用AppStorage中的BuilderNode 1111 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 1112 const parent = this.textNode.getFrameNode()?.getParent(); 1113 if (parent) { 1114 parent.removeChild(this.textNode.getFrameNode()); 1115 } 1116 } else { 1117 this.textNode = new BuilderNode(context); 1118 this.textNode.build(wrapBuilder<[]>(buildText)); 1119 // 将创建的BuilderNode存入AppStorage 1120 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 1121 } 1122 this.rootNode.appendChild(this.textNode.getFrameNode()); 1123 1124 return this.rootNode; 1125 } 1126} 1127 1128@Entry({ routeName: "myIndex" }) 1129@Component 1130struct Index { 1131 aboutToAppear(): void { 1132 AppStorage.setOrCreate<boolean>("isShowText", true); 1133 } 1134 1135 build() { 1136 Row() { 1137 Column() { 1138 NodeContainer(new TextNodeController()) 1139 .width('100%') 1140 .backgroundColor('#FFF0F0F0') 1141 Button('Router pageTwo') 1142 .onClick(() => { 1143 // 改变AppStorage中的状态变量触发Text节点的重新创建 1144 AppStorage.setOrCreate<boolean>("isShowText", false); 1145 1146 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 1147 }) 1148 .margin({ top: 16 }) 1149 } 1150 .width('100%') 1151 .height('100%') 1152 .padding(16) 1153 } 1154 .height('100%') 1155 } 1156} 1157``` 1158 1159PageTwo的实现如下: 1160 1161```ts 1162// ets/pages/PageTwo.ets 1163// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失 1164import "ets/pages/Index" 1165 1166@Entry({ routeName: "pageTwo" }) 1167@Component 1168struct PageTwo { 1169 build() { 1170 Column() { 1171 Button('Router replace to index') 1172 .onClick(() => { 1173 this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" }); 1174 }) 1175 } 1176 .height('100%') 1177 .width('100%') 1178 .alignItems(HorizontalAlign.Center) 1179 .padding(16) 1180 } 1181} 1182``` 1183 1184 1185 1186在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。 1187 1188API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。 1189 1190```ts 1191// ets/pages/Index.ets 1192import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 1193import "ets/pages/PageTwo" 1194 1195@Builder 1196function buildText() { 1197 // @Builder中使用语法节点生成BuilderProxyNode 1198 if (true) { 1199 MyComponent() 1200 } 1201} 1202 1203@Component 1204struct MyComponent { 1205 @StorageLink("isShowText") isShowText: boolean = true; 1206 1207 build() { 1208 if (this.isShowText) { 1209 Column() { 1210 Text("BuilderNode Reuse") 1211 .fontSize(36) 1212 .fontWeight(FontWeight.Bold) 1213 .padding(16) 1214 } 1215 } 1216 } 1217} 1218 1219class TextNodeController extends NodeController { 1220 private rootNode: FrameNode | null = null; 1221 private textNode: BuilderNode<[]> | null = null; 1222 1223 makeNode(context: UIContext): FrameNode | null { 1224 this.rootNode = new FrameNode(context); 1225 1226 if (AppStorage.has("textNode")) { 1227 // 复用AppStorage中的BuilderNode 1228 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 1229 const parent = this.textNode.getFrameNode()?.getParent(); 1230 if (parent) { 1231 parent.removeChild(this.textNode.getFrameNode()); 1232 } 1233 } else { 1234 this.textNode = new BuilderNode(context); 1235 this.textNode.build(wrapBuilder<[]>(buildText)); 1236 // 将创建的BuilderNode存入AppStorage 1237 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 1238 } 1239 this.rootNode.appendChild(this.textNode.getFrameNode()); 1240 1241 return this.rootNode; 1242 } 1243} 1244 1245@Entry({ routeName: "myIndex" }) 1246@Component 1247struct Index { 1248 aboutToAppear(): void { 1249 AppStorage.setOrCreate<boolean>("isShowText", true); 1250 } 1251 1252 build() { 1253 Row() { 1254 Column() { 1255 NodeContainer(new TextNodeController()) 1256 .width('100%') 1257 .backgroundColor('#FFF0F0F0') 1258 Button('Router pageTwo') 1259 .onClick(() => { 1260 // 改变AppStorage中的状态变量触发Text节点的重新创建 1261 AppStorage.setOrCreate<boolean>("isShowText", false); 1262 // 将BuilderNode从AppStorage中移除 1263 AppStorage.delete("textNode"); 1264 1265 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 1266 }) 1267 .margin({ top: 16 }) 1268 } 1269 .width('100%') 1270 .height('100%') 1271 .padding(16) 1272 } 1273 .height('100%') 1274 } 1275} 1276``` 1277 1278 1279## BuilderNode中使用LocalStorage 1280 1281从API version 12开始,自定义组件支持接收[LocalStorage](../ui/state-management/arkts-localstorage.md)实例。可以通过[传递LocalStorage实例](../ui/state-management/arkts-localstorage.md#自定义组件接收localstorage实例)来使用LocalStorage相关的装饰器[@LocalStorageProp](../ui/state-management/arkts-localstorage.md#localstorageprop)、[@LocalStorageLink](../ui/state-management/arkts-localstorage.md#localstoragelink)。 1282 1283```ts 1284import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI'; 1285 1286let localStorage1: LocalStorage = new LocalStorage(); 1287localStorage1.setOrCreate('PropA', 'PropA'); 1288 1289let localStorage2: LocalStorage = new LocalStorage(); 1290localStorage2.setOrCreate('PropB', 'PropB'); 1291 1292@Entry(localStorage1) 1293@Component 1294struct Index { 1295 // 'PropA',和localStorage1中'PropA'的双向同步 1296 @LocalStorageLink('PropA') PropA: string = 'Hello World'; 1297 @State count: number = 0; 1298 private controller: NodeController = new MyNodeController(this.count, localStorage2); 1299 1300 build() { 1301 Row() { 1302 Column() { 1303 Text(this.PropA) 1304 .fontSize(50) 1305 .fontWeight(FontWeight.Bold) 1306 // 使用LocalStorage 实例localStorage2 1307 Child({ count: this.count }, localStorage2) 1308 NodeContainer(this.controller) 1309 } 1310 .width('100%') 1311 } 1312 .height('100%') 1313 } 1314} 1315 1316interface Params { 1317 count: number; 1318 localStorage: LocalStorage; 1319} 1320 1321@Builder 1322function CreateChild(params: Params) { 1323 //构造过程中传递localStorage 1324 Child({ count: params.count }, params.localStorage) 1325} 1326 1327class MyNodeController extends NodeController { 1328 private count?: number; 1329 private localStorage ?: LocalStorage; 1330 1331 constructor(count: number, localStorage: LocalStorage) { 1332 super(); 1333 this.count = count; 1334 this.localStorage = localStorage; 1335 } 1336 1337 makeNode(uiContext: UIContext): FrameNode | null { 1338 let builderNode = new BuilderNode<[Params]>(uiContext); 1339 //构造过程中传递localStorage 1340 builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage }); 1341 return builderNode.getFrameNode(); 1342 } 1343} 1344 1345@Component 1346struct Child { 1347 @Prop count: number; 1348 // 'Hello World',和localStorage2中'PropB'的双向同步,如果localStorage2中没有'PropB',则使用默认值'Hello World' 1349 @LocalStorageLink('PropB') PropB: string = 'Hello World'; 1350 1351 build() { 1352 Text(this.PropB) 1353 .fontSize(50) 1354 .fontWeight(FontWeight.Bold) 1355 } 1356} 1357``` 1358 1359## 查询当前BuilderNode是否解除引用 1360 1361前端节点均绑定有相应的后端实体节点,当节点调用dispose接口解除绑定后,再次调用接口可能会出现crash、返回默认值的情况。 1362 1363从API version 20开始,使用[isDisposed](../reference/apis-arkui/js-apis-arkui-builderNode.md#isdisposed20)接口查询当前BuilderNode对象是否已解除与后端实体节点的引用关系,从而可以在操作节点前检查其有效性,避免潜在风险。 1364 1365```ts 1366import { NodeController, FrameNode, BuilderNode } from '@kit.ArkUI'; 1367 1368@Builder 1369function buildText() { 1370 Text("Test") 1371 .fontSize(20) 1372 .fontWeight(FontWeight.Bold) 1373} 1374 1375class MyNodeController extends NodeController { 1376 private rootNode: FrameNode | null = null; 1377 private builderNode: BuilderNode<[]> | null = null; 1378 1379 makeNode(uiContext: UIContext): FrameNode | null { 1380 this.rootNode = new FrameNode(uiContext); 1381 this.rootNode.commonAttribute.width(100).height(100).backgroundColor(Color.Pink); 1382 this.builderNode = new BuilderNode<[]>(uiContext); 1383 this.builderNode.build(wrapBuilder<[]>(buildText)); 1384 1385 // 挂载BuilderNode 1386 this.rootNode.appendChild(this.builderNode.getFrameNode()); 1387 return this.rootNode; 1388 } 1389 1390 disposeBuilderNode() { 1391 // 解除BuilderNode与后端实体节点的引用关系 1392 this.builderNode?.dispose(); 1393 } 1394 1395 isDisposed() : string { 1396 if (this.builderNode !== null) { 1397 // 查询BuilderNode是否解除引用 1398 if (this.builderNode.isDisposed()) { 1399 return 'builderNode isDisposed is true'; 1400 } 1401 else { 1402 return 'builderNode isDisposed is false'; 1403 } 1404 } 1405 return 'builderNode is null'; 1406 } 1407} 1408 1409@Entry 1410@Component 1411struct Index { 1412 @State text: string = '' 1413 private myNodeController: MyNodeController = new MyNodeController(); 1414 1415 build() { 1416 Column({ space: 4 }) { 1417 NodeContainer(this.myNodeController) 1418 Button('BuilderNode dispose') 1419 .onClick(() => { 1420 this.myNodeController.disposeBuilderNode(); 1421 this.text = ''; 1422 }) 1423 .width(200) 1424 .height(50) 1425 Button('BuilderNode isDisposed') 1426 .onClick(() => { 1427 this.text = this.myNodeController.isDisposed(); 1428 }) 1429 .width(200) 1430 .height(50) 1431 Text(this.text) 1432 .fontSize(25) 1433 } 1434 .width('100%') 1435 .height('100%') 1436 } 1437} 1438``` 1439 1440## 设置BuilderNode继承冻结能力 1441 1442ArkUI支持[自定义组件冻结](./state-management/arkts-custom-components-freeze.md),该功能冻结非激活状态组件的刷新能力。当组件处于非激活状态时,即便其绑定状态变量发生变化,也不会触发组件UI的重新渲染,从而减少复杂UI场景的刷新负载。 1443 1444从API version 20开始,BuilderNode节点可以通过[inheritFreezeOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#inheritfreezeoptions20)接口继承父自定义组件(即从该BuilderNode节点向上查找的第一个自定义组件)的冻结策略。当BuilderNode节点继承父自定义组件的冻结策略时,若父自定义组件的冻结策略设置为开启组件冻结(即[freezeWhenInactive](../reference/apis-arkui/arkui-ts/ts-custom-component-parameter.md#componentoptions)选项设为true),则BuilderNode节点在不活跃时将会冻结,当切换至活跃状态时解冻,并使用缓存的数据更新节点。 1445 1446BuilderNode节点只有通过以下方式上下树时,才会根据该节点是否继承父自定义组件的冻结策略,来更新自己的冻结策略: 1447 1448| 类 | 接口 | 1449| -------- | -------- | 1450| [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md) | [appendChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#appendchild12)、[insertChildAfter](../reference/apis-arkui/js-apis-arkui-frameNode.md#insertchildafter12)、[removeChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#removechild12)、[clearChildren](../reference/apis-arkui/js-apis-arkui-frameNode.md#clearchildren12)、[addComponentContent](../reference/apis-arkui/js-apis-arkui-frameNode.md#addcomponentcontent12) | 1451| [NodeContent](../reference/apis-arkui/js-apis-arkui-NodeContent.md) | [addFrameNode](../reference/apis-arkui/js-apis-arkui-NodeContent.md#addframenode12)、[removeFrameNode](../reference/apis-arkui/js-apis-arkui-NodeContent.md#removeframenode12) | 1452| [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md) | [makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode) | 1453| [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) | [appendChild](../reference/apis-arkui/js-apis-arkui-renderNode.md#appendchild)、[insertChildAfter](../reference/apis-arkui/js-apis-arkui-renderNode.md#insertchildafter)、[removeChild](../reference/apis-arkui/js-apis-arkui-renderNode.md#removechild)、[clearChildren](../reference/apis-arkui/js-apis-arkui-renderNode.md#clearchildren) | 1454| [NodeAdaper](../reference/apis-arkui/js-apis-arkui-frameNode.md#nodeadapter12) | 节点通过[懒加载](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)方式上下树时 | 1455 1456> **说明:** 1457> 1458> 当BuilderNode节点设置为继承父自定义组件的冻结策略时,BuilderNode节点的冻结策略将与其上层最近的自定义组件或BuilderNode节点的冻结策略保持一致。 1459> 1460> 当BuilderNode节点被冻结时,调用[update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update)接口不会触发节点的更新,等其被解冻时再更新节点。 1461 1462```ts 1463 1464import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI'; 1465 1466class Params { 1467 count: number = 0; 1468 1469 constructor(count: number) { 1470 this.count = count; 1471 } 1472} 1473 1474@Builder 1475function buildText(params: Params) { 1476 1477 Column() { 1478 TextBuilder({ message: params.count }) 1479 } 1480} 1481 1482class TextNodeController extends NodeController { 1483 private rootNode: FrameNode | null = null; 1484 private textNode: BuilderNode<[Params]> | null = null; 1485 private count: number = 0; 1486 1487 makeNode(context: UIContext): FrameNode | null { 1488 this.rootNode = new FrameNode(context); 1489 this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } }); 1490 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.count)); 1491 this.textNode.inheritFreezeOptions(true); // 设置BuilderNode的冻结继承状态为true 1492 if (this.rootNode !== null) { 1493 this.rootNode.appendChild(this.textNode.getFrameNode()); // 将BuilderNode上树 1494 } 1495 return this.rootNode; 1496 } 1497 1498 update(): void { 1499 if (this.textNode !== null) { 1500 this.count += 1; 1501 this.textNode.update(new Params(this.count)); // 更新BuilderNode中的数据,可以触发Log 1502 } 1503 1504 } 1505} 1506 1507const textNodeController: TextNodeController = new TextNodeController(); 1508 1509@Entry 1510@Component 1511struct MyNavigationTestStack { 1512 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 1513 @State message: number = 0; 1514 @State logNumber: number = 0; 1515 1516 @Builder 1517 PageMap(name: string) { 1518 if (name === 'pageOne') { 1519 pageOneStack({ message: this.message, logNumber: this.logNumber }) 1520 } else if (name === 'pageTwo') { 1521 pageTwoStack({ message: this.message, logNumber: this.logNumber }) 1522 } 1523 } 1524 1525 build() { 1526 Column() { 1527 Button('update builderNode') // 点击更新BuildrNode 1528 .onClick(() => { 1529 textNodeController.update(); 1530 }) 1531 Navigation(this.pageInfo) { 1532 Column() { 1533 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 1534 .width('80%') 1535 .height(40) 1536 .margin(20) 1537 .onClick(() => { 1538 this.pageInfo.pushPath({ name: 'pageOne' }); // 将name指定的NavDestination页面信息入栈 1539 }) 1540 } 1541 }.title('NavIndex') 1542 .navDestination(this.PageMap) 1543 .mode(NavigationMode.Stack) 1544 } 1545 } 1546} 1547 1548@Component 1549struct pageOneStack { 1550 @Consume('pageInfo') pageInfo: NavPathStack; 1551 @State index: number = 1; 1552 @Link message: number; 1553 @Link logNumber: number; 1554 1555 build() { 1556 NavDestination() { 1557 Column() { 1558 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 1559 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 1560 .width('80%') 1561 .height(40) 1562 .margin(20) 1563 .onClick(() => { 1564 this.pageInfo.pushPathByName('pageTwo', null); 1565 }) 1566 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 1567 .width('80%') 1568 .height(40) 1569 .margin(20) 1570 .onClick(() => { 1571 this.pageInfo.pop(); 1572 }) 1573 }.width('100%').height('100%') 1574 }.title('pageOne') 1575 .onBackPressed(() => { 1576 this.pageInfo.pop(); 1577 return true; 1578 }) 1579 } 1580} 1581 1582@Component 1583struct pageTwoStack { 1584 @Consume('pageInfo') pageInfo: NavPathStack; 1585 @State index: number = 2; 1586 @Link message: number; 1587 @Link logNumber: number; 1588 1589 build() { 1590 NavDestination() { 1591 Column() { 1592 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 1593 Text('BuilderNode处于冻结') 1594 .fontWeight(FontWeight.Bold) 1595 .margin({ top: 48, bottom: 48 }) 1596 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 1597 .width('80%') 1598 .height(40) 1599 .margin(20) 1600 .onClick(() => { 1601 this.pageInfo.pop(); 1602 }) 1603 }.width('100%').height('100%') 1604 }.title('pageTwo') 1605 .onBackPressed(() => { 1606 this.pageInfo.pop(); 1607 return true; 1608 }) 1609 } 1610} 1611 1612@Component({ freezeWhenInactive: true }) // 设置冻结策略为不活跃冻结 1613struct NavigationContentMsgStack { 1614 @Link message: number; 1615 @Link index: number; 1616 @Link logNumber: number; 1617 1618 build() { 1619 Column() { 1620 if (this.index === 1) { 1621 NodeContainer(textNodeController) 1622 } 1623 } 1624 } 1625} 1626 1627@Component({ freezeWhenInactive: true }) // 设置冻结策略为不活跃冻结 1628struct TextBuilder { 1629 @Prop @Watch("info") message: number = 0; 1630 1631 info(): void { 1632 console.info(`freeze-test TextBuilder message callback ${this.message}`); // 根据message内容变化来打印日志来判断是否冻结 1633 } 1634 1635 build() { 1636 Row() { 1637 Column() { 1638 Text(`文本更新次数: ${this.message}`) 1639 .fontWeight(FontWeight.Bold) 1640 .margin({ top: 48, bottom: 48 }) 1641 } 1642 } 1643 } 1644} 1645``` 1646 1647 1648 1649## 设置BuilderNode支持内部@Consume接收外部的@Provide数据 1650 1651从API version 20开始,通过配置BuildOptions参数,BuilderNode内部自定义组件的[@Consume](./state-management/arkts-provide-and-consume.md)支持接收所在页面的[@Provide](./state-management/arkts-provide-and-consume.md)数据。 1652 1653参见[示例代码](../reference/apis-arkui/js-apis-arkui-builderNode.md#示例7buildernode支持内部consume接收外部的provide数据)。 1654 1655## BuilderNode结合ArkWeb组件实现预渲染页面 1656 1657预渲染适用于Web页面启动与跳转等场景。通过结合BuilderNode,可以将ArkWeb组件提前进行离线预渲染,组件不会即时挂载至页面,而是在需要时通过NodeController动态挂载与显示。此举能够提高页面切换的流畅度及用户体验。 1658 1659> **说明** 1660> 1661> 访问在线网页时需添加网络权限:ohos.permission.INTERNET,具体申请方式请参考[声明权限](../security/AccessToken/declare-permissions.md)。 1662 16631. 创建载体Ability,并创建Web组件。 1664 ```ts 1665 // 载体Ability 1666 // EntryAbility.ets 1667 import { createNWeb } from "../pages/common"; 1668 import { UIAbility } from '@kit.AbilityKit'; 1669 import { window } from '@kit.ArkUI'; 1670 1671 export default class EntryAbility extends UIAbility { 1672 onWindowStageCreate(windowStage: window.WindowStage): void { 1673 windowStage.loadContent('pages/Index', (err, data) => { 1674 // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建。 1675 createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext()); 1676 if (err.code) { 1677 return; 1678 } 1679 }); 1680 } 1681 } 1682 ``` 16832. 创建NodeContainer和对应的NodeController,渲染后台Web组件。 1684 1685 ```ts 1686 // 创建NodeController。 1687 // common.ets 1688 import { UIContext } from '@kit.ArkUI'; 1689 import { webview } from '@kit.ArkWeb'; 1690 import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI'; 1691 // @Builder中为动态组件的具体组件内容。 1692 // Data为入参封装类。 1693 class Data{ 1694 url: string = 'https://www.example.com'; 1695 controller: WebviewController = new webview.WebviewController(); 1696 } 1697 // 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染。 1698 let shouldInactive: boolean = true; 1699 @Builder 1700 function WebBuilder(data:Data) { 1701 Column() { 1702 Web({ src: data.url, controller: data.controller }) 1703 .onPageBegin(() => { 1704 // 调用onActive,开启渲染。 1705 data.controller.onActive(); 1706 }) 1707 .onFirstMeaningfulPaint(() =>{ 1708 if (!shouldInactive) { 1709 return; 1710 } 1711 // 在预渲染完成时触发,停止渲染。 1712 data.controller.onInactive(); 1713 shouldInactive = false; 1714 }) 1715 .width("100%") 1716 .height("100%") 1717 } 1718 } 1719 let wrap = wrapBuilder<Data[]>(WebBuilder); 1720 // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。 1721 export class myNodeController extends NodeController { 1722 private rootnode: BuilderNode<Data[]> | null = null; 1723 // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中。 1724 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。 1725 makeNode(uiContext: UIContext): FrameNode | null { 1726 console.info(" uicontext is undifined : "+ (uiContext === undefined)); 1727 if (this.rootnode != null) { 1728 // 返回FrameNode节点。 1729 return this.rootnode.getFrameNode(); 1730 } 1731 // 返回null控制动态组件脱离绑定节点。 1732 return null; 1733 } 1734 // 当布局大小发生变化时进行回调。 1735 aboutToResize(size: Size) { 1736 console.info("aboutToResize width : " + size.width + " height : " + size.height ); 1737 } 1738 // 当controller对应的NodeContainer在Appear的时候进行回调。 1739 aboutToAppear() { 1740 console.info("aboutToAppear"); 1741 // 切换到前台后,不需要停止渲染。 1742 shouldInactive = false; 1743 } 1744 // 当controller对应的NodeContainer在Disappear的时候进行回调。 1745 aboutToDisappear() { 1746 console.info("aboutToDisappear"); 1747 } 1748 // 此函数为自定义函数,可作为初始化函数使用。 1749 // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容。 1750 initWeb(url:string, uiContext:UIContext, control:WebviewController) { 1751 if(this.rootnode != null){ 1752 return; 1753 } 1754 // 创建节点,需要uiContext。 1755 this.rootnode = new BuilderNode(uiContext); 1756 // 创建动态Web组件。 1757 this.rootnode.build(wrap, { url:url, controller:control }); 1758 } 1759 } 1760 // 创建Map保存所需要的NodeController。 1761 let NodeMap:Map<string, myNodeController | undefined> = new Map(); 1762 // 创建Map保存所需要的WebViewController。 1763 let controllerMap:Map<string, WebviewController | undefined> = new Map(); 1764 // 初始化需要UIContext 需在Ability获取。 1765 export const createNWeb = (url: string, uiContext: UIContext) => { 1766 // 创建NodeController。 1767 let baseNode = new myNodeController(); 1768 let controller = new webview.WebviewController() ; 1769 // 初始化自定义Web组件。 1770 baseNode.initWeb(url, uiContext, controller); 1771 controllerMap.set(url, controller); 1772 NodeMap.set(url, baseNode); 1773 } 1774 // 自定义获取NodeController接口。 1775 export const getNWeb = (url : string) : myNodeController | undefined => { 1776 return NodeMap.get(url); 1777 } 1778 ``` 17793. 通过NodeContainer使用已经预渲染的页面。 1780 1781 ```ts 1782 // 使用NodeController的Page页。 1783 // Index.ets 1784 import { createNWeb, getNWeb } from "./common"; 1785 1786 @Entry 1787 @Component 1788 struct Index { 1789 build() { 1790 Row() { 1791 Column() { 1792 // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode。 1793 // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示。 1794 NodeContainer(getNWeb("https://www.example.com")) 1795 .height("90%") 1796 .width("100%") 1797 } 1798 .width('100%') 1799 } 1800 .height('100%') 1801 } 1802 } 1803 ``` 1804