1# 自定义声明式节点 (BuilderNode)
2
3## 概述
4
5自定义声明式节点 ([BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md))提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过[全局自定义构建函数](../quick-start/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)中实现同层渲染。
6
7由BuilderNode构建的ArkTS原生控件树,支持与自定义节点(如FrameNode、RenderNode)关联使用,确保了系统组件与自定义节点的混合显示效果。对于需与自定义节点对接的第三方框架,BuilderNode提供了嵌入系统组件的方法。
8
9此外,BuilderNode还提供了组件预创建的能力,能够自定义系统组件的创建开始的时间,在后续业务中实现动态挂载与显示。此功能尤其适用于初始化耗时较长的声明式组件,如[Web](../reference/apis-arkweb/ts-basic-components-web.md)、[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)等,通过预创建,可以有效减少初始化时间,优化组件加载效率。
10
11![zh-cn_image_builder-node](figures/builder-node.png)
12
13## 基本概念
14
15- 系统组件:组件是UI的必要元素,形成了在界面中的样子,由ArkUI直接提供的称为[系统组件](arkts-ui-development-overview.md)。
16
17- 实体节点:由后端创建的Native节点。
18
19BuilderNode仅可作为叶子节点进行使用。如有更新需要,建议通过BuilderNode中的[update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update)方式触发更新,不建议通过BuilderNode中获取的RenderNode对节点进行修改操作。
20
21> **说明:**
22>
23> - BuilderNode只支持一个由[wrapBuilder](../quick-start/arkts-wrapBuilder.md)包装的[全局自定义构建函数](../quick-start/arkts-builder.md#全局自定义构建函数)@Builder。
24>
25> - 一个新建的BuildNode在[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build)之后才能通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取到一个指向根节点的FrameNode对象,否则返回null。
26>
27> - 如果传入的Builder的根节点为语法节点(if/else/foreach/...),需要额外生成一个FrameNode,在节点树中的显示为“BuilderProxyNode”。
28>
29> - 如果BuilderNode通过getFrameNode将节点挂载在另一个FrameNode上,或者将其作为子节点挂载在NodeContainer节点上。则节点中使用父组件的布局约束进行布局。
30>
31> - 如果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)显式指定布局约束大小,才能正常显示。
32>
33> - BuilderNode的预加载并不会减少组件的创建时间。Web组件创建的时候需要在内核中加载资源,预创建不能减少Web组件的创建的时间,但是可以让内核进行预加载,减少正式使用时候内核的加载耗时。
34
35## 创建BuilderNode对象
36
37BuilderNode对象为一个模板类,需要在创建的时候指定类型。该类型需要与后续build方法中传入的[WrappedBuilder](../quick-start/arkts-wrapBuilder.md#wrapbuilder封装全局builder)的类型保持一致,否则会存在编译告警导致编译失败。
38
39## 创建原生组件树
40
41通过BuilderNode的build可以实现原生组件树的创建。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。
42
43> **说明:**
44>
45> 无状态的UI方法全局@Builder最多拥有一个根节点。
46>
47> build方法中对应的@Builder支持一个参数作为入参。
48>
49> build中对于@Builder嵌套@Builder进行使用的场景,需要保证嵌套的参数与build的中提供的入参一致。
50>
51> 对于@Builder嵌套@Builder进行使用的场景,如果入参类型不一致,则要求增加[BuilderOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildoptions12)字段作为[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build12)的入参。
52>
53> 需要操作BuilderNode中的对象时,需要保证其引用不被回收。当BuilderNode对象被虚拟机回收之后,它的FrameNode、RenderNode对象也会与后端节点解引用。即从BuilderNode中获取的FrameNode对象不对应任何一个节点。
54
55创建离线节点以及原生组件树,结合FrameNode进行使用。
56
57BuilderNode的根节点直接作为[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)的[makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode)返回值。
58
59```ts
60import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
61
62class Params {
63  text: string = "";
64
65  constructor(text: string) {
66    this.text = text;
67  }
68}
69
70@Builder
71function buildText(params: Params) {
72  Column() {
73    Text(params.text)
74      .fontSize(50)
75      .fontWeight(FontWeight.Bold)
76      .margin({ bottom: 36 })
77  }
78}
79
80class TextNodeController extends NodeController {
81  private textNode: BuilderNode<[Params]> | null = null;
82  private message: string = "DEFAULT";
83
84  constructor(message: string) {
85    super();
86    this.message = message;
87  }
88
89  makeNode(context: UIContext): FrameNode | null {
90    this.textNode = new BuilderNode(context);
91    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
92    return this.textNode.getFrameNode();
93  }
94}
95
96@Entry
97@Component
98struct Index {
99  @State message: string = "hello";
100
101  build() {
102    Row() {
103      Column() {
104        NodeContainer(new TextNodeController(this.message))
105          .width('100%')
106          .height(100)
107          .backgroundColor('#FFF0F0F0')
108      }
109      .width('100%')
110      .height('100%')
111    }
112    .height('100%')
113  }
114}
115```
116
117将BuilderNode与RenderNode进行结合使用。
118
119BuilderNode的RenderNode挂载其它RenderNode下时,需要明确定义[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)的大小作为BuilderNode的布局约束。不推荐通过该方式挂载节点。
120
121```ts
122import { NodeController, BuilderNode, FrameNode, UIContext, RenderNode } from "@kit.ArkUI";
123
124class Params {
125  text: string = "";
126
127  constructor(text: string) {
128    this.text = text;
129  }
130}
131
132@Builder
133function buildText(params: Params) {
134  Column() {
135    Text(params.text)
136      .fontSize(50)
137      .fontWeight(FontWeight.Bold)
138      .margin({ bottom: 36 })
139  }
140}
141
142class TextNodeController extends NodeController {
143  private rootNode: FrameNode | null = null;
144  private textNode: BuilderNode<[Params]> | null = null;
145  private message: string = "DEFAULT";
146
147  constructor(message: string) {
148    super();
149    this.message = message;
150  }
151
152  makeNode(context: UIContext): FrameNode | null {
153    this.rootNode = new FrameNode(context);
154    let renderNode = new RenderNode();
155    renderNode.clipToFrame = false;
156    this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } });
157    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
158    const textRenderNode = this.textNode?.getFrameNode()?.getRenderNode();
159
160    const rootRenderNode = this.rootNode.getRenderNode();
161    if (rootRenderNode !== null) {
162      rootRenderNode.appendChild(renderNode);
163      renderNode.appendChild(textRenderNode);
164    }
165
166    return this.rootNode;
167  }
168}
169
170@Entry
171@Component
172struct Index {
173  @State message: string = "hello";
174
175  build() {
176    Row() {
177      Column() {
178        NodeContainer(new TextNodeController(this.message))
179          .width('100%')
180          .height(100)
181          .backgroundColor('#FFF0F0F0')
182      }
183      .width('100%')
184      .height('100%')
185    }
186    .height('100%')
187  }
188}
189```
190
191## 更新原生组件树
192
193通过BuilderNode对象的build创建原生组件树。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。
194
195自定义组件的更新遵循[状态管理](../quick-start/arkts-state-management-overview.md)的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为[\@Prop](../quick-start/arkts-prop.md)或者[\@ObjectLink](../quick-start/arkts-observed-and-objectlink.md)。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。
196
197
198使用update更新BuilderNode中的节点。
199
200使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)触发BuilderNode中节点的全量更新。
201
202更新BuilderNode中的节点。
203
204```ts
205import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
206
207class Params {
208  text: string = "";
209  constructor(text: string) {
210    this.text = text;
211  }
212}
213
214// 自定义组件
215@Component
216struct TextBuilder {
217  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
218  @Prop message: string = "TextBuilder";
219
220  build() {
221    Row() {
222      Column() {
223        Text(this.message)
224          .fontSize(50)
225          .fontWeight(FontWeight.Bold)
226          .margin({ bottom: 36 })
227          .backgroundColor(Color.Gray)
228      }
229    }
230  }
231}
232
233@Builder
234function buildText(params: Params) {
235  Column() {
236    Text(params.text)
237      .fontSize(50)
238      .fontWeight(FontWeight.Bold)
239      .margin({ bottom: 36 })
240    TextBuilder({ message: params.text }) // 自定义组件
241  }
242}
243
244class TextNodeController extends NodeController {
245  private textNode: BuilderNode<[Params]> | null = null;
246  private message: string = "";
247
248  constructor(message: string) {
249    super()
250    this.message = message
251  }
252
253  makeNode(context: UIContext): FrameNode | null {
254    this.textNode = new BuilderNode(context);
255    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
256    return this.textNode.getFrameNode();
257  }
258
259  update(message: string) {
260    if (this.textNode !== null) {
261      // 调用update进行更新。
262      this.textNode.update(new Params(message));
263    }
264  }
265}
266
267@Entry
268@Component
269struct Index {
270  @State message: string = "hello";
271  private textNodeController: TextNodeController = new TextNodeController(this.message);
272  private count = 0;
273
274  build() {
275    Row() {
276      Column() {
277        NodeContainer(this.textNodeController)
278          .width('100%')
279          .height(200)
280          .backgroundColor('#FFF0F0F0')
281        Button('Update')
282          .onClick(() => {
283            this.count += 1;
284            const message = "Update " + this.count.toString();
285            this.textNodeController.update(message);
286          })
287      }
288      .width('100%')
289      .height('100%')
290    }
291    .height('100%')
292  }
293}
294```
295
296## 解除实体节点引用关系
297
298由于BuilderNode对应的是后端的实体节点,正常的内存释放依赖前端对象的回收。如果期望直接释放后端的节点对象,则可以通过调用[dispose](../reference/apis-arkui/js-apis-arkui-builderNode.md#dispose12)与实体节点解除引用关系,此时持有的前端BuilderNode对象不再影响实体节点的生命周期。
299
300> **说明:**
301>
302> 当BuilderNode对象调用dispose之后,不仅BuilderNode对象与后端实体节点解除引用关系,BuilderNode中的FrameNode与RenderNode也会同步和实体节点解除引用关系。
303
304## 注入触摸事件
305
306BuilderNode中提供了[postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent),可以通过该接口向BuilderNode中绑定的组件注入[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md#触摸事件),实现事件的模拟转发。
307
308通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。
309
310向BuilderNode中的Column组件转发另一个Column的接收事件,即点击下方的Column组件,上方的Colum组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。
311
312```ts
313import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
314
315class Params {
316  text: string = "this is a text";
317}
318
319@Builder
320function ButtonBuilder(params: Params) {
321  Column() {
322    Button(`button ` + params.text)
323      .borderWidth(2)
324      .backgroundColor(Color.Orange)
325      .width("100%")
326      .height("100%")
327      .gesture(
328        TapGesture()
329          .onAction((event: GestureEvent) => {
330            console.log("TapGesture");
331          })
332      )
333  }
334  .width(500)
335  .height(300)
336  .backgroundColor(Color.Gray)
337}
338
339class MyNodeController extends NodeController {
340  private rootNode: BuilderNode<[Params]> | null = null;
341  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder);
342
343  makeNode(uiContext: UIContext): FrameNode | null {
344    this.rootNode = new BuilderNode(uiContext);
345    this.rootNode.build(this.wrapBuilder, { text: "this is a string" })
346    return this.rootNode.getFrameNode();
347  }
348
349  postTouchEvent(touchEvent: TouchEvent): void {
350    if (this.rootNode == null) {
351      return;
352    }
353    let result = this.rootNode.postTouchEvent(touchEvent);
354    console.log("result " + result);
355  }
356}
357
358@Entry
359@Component
360struct MyComponent {
361  private nodeController: MyNodeController = new MyNodeController();
362
363  build() {
364    Column() {
365      NodeContainer(this.nodeController)
366        .height(300)
367        .width(500)
368      Column()
369        .width(500)
370        .height(300)
371        .backgroundColor(Color.Pink)
372        .onTouch((event) => {
373          if (event != undefined) {
374            this.nodeController.postTouchEvent(event);
375          }
376        })
377    }
378  }
379}
380```
381
382## 节点复用能力
383
384将[reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12)事件和[recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12)事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点的复用。
385
386```ts
387import { FrameNode,NodeController,BuilderNode,UIContext } from "@kit.ArkUI";
388
389class MyDataSource {
390  private dataArray: string[] = [];
391  private listener: DataChangeListener | null = null
392
393  public totalCount(): number {
394    return this.dataArray.length;
395  }
396
397  public getData(index: number) {
398    return this.dataArray[index];
399  }
400
401  public pushData(data: string) {
402    this.dataArray.push(data);
403  }
404
405  public reloadListener(): void {
406    this.listener?.onDataReloaded();
407  }
408
409  public registerDataChangeListener(listener: DataChangeListener): void {
410    this.listener = listener;
411  }
412
413  public unregisterDataChangeListener(): void {
414    this.listener = null;
415  }
416}
417
418class Params {
419  item: string = '';
420
421  constructor(item: string) {
422    this.item = item;
423  }
424}
425
426@Builder
427function buildNode(param: Params = new Params("hello")) {
428  ReusableChildComponent2({ item: param.item });
429}
430
431class MyNodeController extends NodeController {
432  public builderNode: BuilderNode<[Params]> | null = null;
433  public item: string = "";
434
435  makeNode(uiContext: UIContext): FrameNode | null {
436    if (this.builderNode == null) {
437      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
438      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
439    }
440    return this.builderNode.getFrameNode();
441  }
442}
443
444@Reusable
445@Component
446struct ReusableChildComponent {
447  @State item: string = '';
448  private controller: MyNodeController = new MyNodeController();
449
450  aboutToAppear() {
451    this.controller.item = this.item;
452  }
453
454  aboutToRecycle(): void {
455    console.log("ReusableChildComponent aboutToRecycle " + this.item);
456    this.controller?.builderNode?.recycle();
457  }
458
459  aboutToReuse(params: object): void {
460    console.log("ReusableChildComponent aboutToReuse " + JSON.stringify(params));
461    this.controller?.builderNode?.reuse(params);
462  }
463
464  build() {
465    NodeContainer(this.controller);
466  }
467}
468
469@Component
470struct ReusableChildComponent2 {
471  @Prop item: string = "false";
472
473  aboutToReuse(params: Record<string, object>) {
474    console.log("ReusableChildComponent2 Reusable 2 " + JSON.stringify(params));
475  }
476
477  aboutToRecycle(): void {
478    console.log("ReusableChildComponent2 aboutToRecycle 2 " + this.item);
479  }
480
481  build() {
482    Row() {
483      Text(this.item)
484        .fontSize(20)
485        .backgroundColor(Color.Yellow)
486        .margin({ left: 10 })
487    }.margin({ left: 10, right: 10 })
488  }
489}
490
491
492@Entry
493@Component
494struct Index {
495  @State data: MyDataSource = new MyDataSource();
496
497  aboutToAppear() {
498    for (let i = 0;i < 100; i++) {
499      this.data.pushData(i.toString());
500    }
501  }
502
503  build() {
504    Column() {
505      List({ space: 3 }) {
506        LazyForEach(this.data, (item: string) => {
507          ListItem() {
508            ReusableChildComponent({ item: item })
509          }
510        }, (item: string) => item)
511      }
512      .width('100%')
513      .height('100%')
514    }
515  }
516}
517```
518
519## 通过系统环境变化更新节点
520
521使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)来监听[系统环境变化](../reference/apis-ability-kit/js-apis-app-ability-configuration.md)事件,以触发节点的全量更新。
522
523> **说明:**
524>
525> updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。
526
527```ts
528import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
529import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit';
530
531class Params {
532  text: string = ""
533
534  constructor(text: string) {
535    this.text = text;
536  }
537}
538
539// 自定义组件
540@Component
541struct TextBuilder {
542  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
543  @Prop message: string = "TextBuilder";
544
545  build() {
546    Row() {
547      Column() {
548        Text(this.message)
549          .fontSize(50)
550          .fontWeight(FontWeight.Bold)
551          .margin({ bottom: 36 })
552          .fontColor($r(`app.color.text_color`))
553          .backgroundColor($r(`app.color.start_window_background`))
554      }
555    }
556  }
557}
558
559@Builder
560function buildText(params: Params) {
561  Column() {
562    Text(params.text)
563      .fontSize(50)
564      .fontWeight(FontWeight.Bold)
565      .margin({ bottom: 36 })
566      .fontColor($r(`app.color.text_color`))
567    TextBuilder({ message: params.text }) // 自定义组件
568  }.backgroundColor($r(`app.color.start_window_background`))
569}
570
571class TextNodeController extends NodeController {
572  private textNode: BuilderNode<[Params]> | null = null;
573  private message: string = "";
574
575  constructor(message: string) {
576    super()
577    this.message = message;
578  }
579
580  makeNode(context: UIContext): FrameNode | null {
581    return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null;
582  }
583
584  createNode(context: UIContext) {
585    this.textNode = new BuilderNode(context);
586    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
587    builderNodeMap.push(this.textNode);
588  }
589
590  deleteNode() {
591    let node = builderNodeMap.pop();
592    node?.dispose();
593  }
594
595  update(message: string) {
596    if (this.textNode !== null) {
597      // 调用update进行更新。
598      this.textNode.update(new Params(message));
599    }
600  }
601}
602
603// 记录创建的自定义节点对象
604const builderNodeMap: Array<BuilderNode<[Params]>> = new Array();
605
606function updateColorMode() {
607  builderNodeMap.forEach((value, index) => {
608    // 通知BuilderNode环境变量改变
609    value.updateConfiguration();
610  })
611}
612
613@Entry
614@Component
615struct Index {
616  @State message: string = "hello"
617  private textNodeController: TextNodeController = new TextNodeController(this.message);
618  private count = 0;
619
620  aboutToAppear(): void {
621    let environmentCallback: EnvironmentCallback = {
622      onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {
623        console.log('onMemoryLevel');
624      },
625      onConfigurationUpdated: (config: Configuration): void => {
626        console.log('onConfigurationUpdated ' + JSON.stringify(config));
627        updateColorMode();
628      }
629    }
630    // 注册监听回调
631    this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback);
632    //创建自定义节点并添加至map
633    this.textNodeController.createNode(this.getUIContext());
634  }
635
636  aboutToDisappear(): void {
637    //移除map中的引用,并将自定义节点释放
638    this.textNodeController.deleteNode();
639  }
640
641  build() {
642    Row() {
643      Column() {
644        NodeContainer(this.textNodeController)
645          .width('100%')
646          .height(200)
647          .backgroundColor('#FFF0F0F0')
648        Button('Update')
649          .onClick(() => {
650            this.count += 1;
651            const message = "Update " + this.count.toString();
652            this.textNodeController.update(message);
653          })
654      }
655      .width('100%')
656      .height('100%')
657    }
658    .height('100%')
659  }
660}
661```
662
663## 跨页面复用注意事项
664
665在使用[路由](../reference/apis-arkui/js-apis-router.md)接口[router.replaceUrl](../reference/apis-arkui/js-apis-router.md#routerreplaceurl9)、[router.back](../reference/apis-arkui/js-apis-router.md#routerback)、[router.clear](../reference/apis-arkui/js-apis-router.md#routerclear)、[router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10)操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以[router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10)为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为false。
666
667```ts
668// ets/pages/Index.ets
669import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
670import "ets/pages/PageTwo"
671
672@Builder
673function buildText() {
674  // @Builder中使用语法节点生成BuilderProxyNode
675  if (true) {
676    MyComponent()
677  }
678}
679
680@Component
681struct MyComponent {
682  @StorageLink("isShowText") isShowText: boolean = true;
683
684  build() {
685    if (this.isShowText) {
686      Column() {
687        Text("BuilderNode Reuse")
688          .fontSize(36)
689          .fontWeight(FontWeight.Bold)
690          .padding(16)
691      }
692    }
693  }
694}
695
696class TextNodeController extends NodeController {
697  private rootNode: FrameNode | null = null;
698  private textNode: BuilderNode<[]> | null = null;
699
700  makeNode(context: UIContext): FrameNode | null {
701    this.rootNode = new FrameNode(context);
702
703    if (AppStorage.has("textNode")) {
704      // 复用AppStorage中的BuilderNode
705      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
706      const parent = this.textNode.getFrameNode()?.getParent();
707      if (parent) {
708        parent.removeChild(this.textNode.getFrameNode());
709      }
710    } else {
711      this.textNode = new BuilderNode(context);
712      this.textNode.build(wrapBuilder<[]>(buildText));
713      // 将创建的BuilderNode存入AppStorage
714      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
715    }
716    this.rootNode.appendChild(this.textNode.getFrameNode());
717
718    return this.rootNode;
719  }
720}
721
722@Entry({ routeName: "myIndex" })
723@Component
724struct Index {
725  aboutToAppear(): void {
726    AppStorage.setOrCreate<boolean>("isShowText", true);
727  }
728
729  build() {
730    Row() {
731      Column() {
732        NodeContainer(new TextNodeController())
733          .width('100%')
734          .backgroundColor('#FFF0F0F0')
735        Button('Router pageTwo')
736          .onClick(() => {
737            // 改变AppStorage中的状态变量触发Text节点的重新创建
738            AppStorage.setOrCreate<boolean>("isShowText", false);
739
740            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
741          })
742          .margin({ top: 16 })
743      }
744      .width('100%')
745      .height('100%')
746      .padding(16)
747    }
748    .height('100%')
749  }
750}
751```
752
753PageTwo的实现如下:
754
755```ts
756// ets/pages/PageTwo.ets
757// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失
758import "ets/pages/Index"
759
760@Entry({ routeName: "pageTwo" })
761@Component
762struct PageTwo {
763  build() {
764    Column() {
765      Button('Router replace to index')
766        .onClick(() => {
767          this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" });
768        })
769    }
770    .height('100%')
771    .width('100%')
772    .alignItems(HorizontalAlign.Center)
773    .padding(16)
774  }
775}
776```
777
778![BuilderNode Reuse Example](./figures/builder_node_reuse.gif)
779
780在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。
781
782API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。
783
784```ts
785// ets/pages/Index.ets
786import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
787import "ets/pages/PageTwo"
788
789@Builder
790function buildText() {
791  // @Builder中使用语法节点生成BuilderProxyNode
792  if (true) {
793    MyComponent()
794  }
795}
796
797@Component
798struct MyComponent {
799  @StorageLink("isShowText") isShowText: boolean = true;
800
801  build() {
802    if (this.isShowText) {
803      Column() {
804        Text("BuilderNode Reuse")
805          .fontSize(36)
806          .fontWeight(FontWeight.Bold)
807          .padding(16)
808      }
809    }
810  }
811}
812
813class TextNodeController extends NodeController {
814  private rootNode: FrameNode | null = null;
815  private textNode: BuilderNode<[]> | null = null;
816
817  makeNode(context: UIContext): FrameNode | null {
818    this.rootNode = new FrameNode(context);
819
820    if (AppStorage.has("textNode")) {
821      // 复用AppStorage中的BuilderNode
822      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
823      const parent = this.textNode.getFrameNode()?.getParent();
824      if (parent) {
825        parent.removeChild(this.textNode.getFrameNode());
826      }
827    } else {
828      this.textNode = new BuilderNode(context);
829      this.textNode.build(wrapBuilder<[]>(buildText));
830      // 将创建的BuilderNode存入AppStorage
831      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
832    }
833    this.rootNode.appendChild(this.textNode.getFrameNode());
834
835    return this.rootNode;
836  }
837}
838
839@Entry({ routeName: "myIndex" })
840@Component
841struct Index {
842  aboutToAppear(): void {
843    AppStorage.setOrCreate<boolean>("isShowText", true);
844  }
845
846  build() {
847    Row() {
848      Column() {
849        NodeContainer(new TextNodeController())
850          .width('100%')
851          .backgroundColor('#FFF0F0F0')
852        Button('Router pageTwo')
853          .onClick(() => {
854            // 改变AppStorage中的状态变量触发Text节点的重新创建
855            AppStorage.setOrCreate<boolean>("isShowText", false);
856            // 将BuilderNode从AppStorage中移除
857            AppStorage.delete("textNode");
858
859            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
860          })
861          .margin({ top: 16 })
862      }
863      .width('100%')
864      .height('100%')
865      .padding(16)
866    }
867    .height('100%')
868  }
869}
870```
871