• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 自定义声明式节点 (BuilderNode)
2
3## 概述
4
5自定义声明式节点 ([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)中实现同层渲染。
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](../ui/state-management/arkts-wrapBuilder.md)包装的[全局自定义构建函数](../ui/state-management/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](../ui/state-management/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自定义组件的更新遵循[状态管理](../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)。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。
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> 若前端对象BuilderNode无法释放,容易导致内存泄漏。建议在不再需要对该BuilderNode对象进行操作时,开发者应主动调用dispose释放后端节点,以减少引用关系的复杂性,降低内存泄漏的风险。
305
306## 注入触摸事件
307
308BuilderNode中提供了[postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent),可以通过该接口向BuilderNode中绑定的组件注入[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md#触摸事件),实现事件的模拟转发。
309
310通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。
311
312向BuilderNode中的Column组件转发另一个Column的接收事件,即点击下方的Column组件,上方的Colum组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。
313
314```ts
315import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
316
317class Params {
318  text: string = "this is a text";
319}
320
321@Builder
322function ButtonBuilder(params: Params) {
323  Column() {
324    Button(`button ` + params.text)
325      .borderWidth(2)
326      .backgroundColor(Color.Orange)
327      .width("100%")
328      .height("100%")
329      .gesture(
330        TapGesture()
331          .onAction((event: GestureEvent) => {
332            console.log("TapGesture");
333          })
334      )
335  }
336  .width(500)
337  .height(300)
338  .backgroundColor(Color.Gray)
339}
340
341class MyNodeController extends NodeController {
342  private rootNode: BuilderNode<[Params]> | null = null;
343  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder);
344
345  makeNode(uiContext: UIContext): FrameNode | null {
346    this.rootNode = new BuilderNode(uiContext);
347    this.rootNode.build(this.wrapBuilder, { text: "this is a string" })
348    return this.rootNode.getFrameNode();
349  }
350
351  postTouchEvent(touchEvent: TouchEvent): void {
352    if (this.rootNode == null) {
353      return;
354    }
355    let result = this.rootNode.postTouchEvent(touchEvent);
356    console.log("result " + result);
357  }
358}
359
360@Entry
361@Component
362struct MyComponent {
363  private nodeController: MyNodeController = new MyNodeController();
364
365  build() {
366    Column() {
367      NodeContainer(this.nodeController)
368        .height(300)
369        .width(500)
370      Column()
371        .width(500)
372        .height(300)
373        .backgroundColor(Color.Pink)
374        .onTouch((event) => {
375          if (event != undefined) {
376            this.nodeController.postTouchEvent(event);
377          }
378        })
379    }
380  }
381}
382```
383
384## 节点复用能力
385
386调用[reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12)接口和[recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12)接口,将复用和回收事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点内部的自定义组件的复用。
387
388以下面的Demo为例,被复用的自定义组件ReusableChildComponent可以传递复用和回收事件到其下的自定义组件ReusableChildComponent3,但无法传递给自定义组件ReusableChildComponent2,因为被BuilderNode所隔断。因此需要主动调用BuilderNode的reuse和recycle接口,将复用和回收事件传递给自定义组件ReusableChildComponent2,以达成复用效果。
389![zh-cn_image_reuse-recycle](figures/reuse-recycle.png)
390
391
392```ts
393import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI";
394
395const TEST_TAG: string = "Reuse+Recycle";
396
397class MyDataSource {
398  private dataArray: string[] = [];
399  private listener: DataChangeListener | null = null
400
401  public totalCount(): number {
402    return this.dataArray.length;
403  }
404
405  public getData(index: number) {
406    return this.dataArray[index];
407  }
408
409  public pushData(data: string) {
410    this.dataArray.push(data);
411  }
412
413  public reloadListener(): void {
414    this.listener?.onDataReloaded();
415  }
416
417  public registerDataChangeListener(listener: DataChangeListener): void {
418    this.listener = listener;
419  }
420
421  public unregisterDataChangeListener(): void {
422    this.listener = null;
423  }
424}
425
426class Params {
427  item: string = '';
428
429  constructor(item: string) {
430    this.item = item;
431  }
432}
433
434@Builder
435function buildNode(param: Params = new Params("hello")) {
436  Row() {
437    Text(`C${param.item} -- `)
438    ReusableChildComponent2({ item: param.item }) //该自定义组件在BuilderNode中无法被正确复用
439  }
440}
441
442class MyNodeController extends NodeController {
443  public builderNode: BuilderNode<[Params]> | null = null;
444  public item: string = "";
445
446  makeNode(uiContext: UIContext): FrameNode | null {
447    if (this.builderNode == null) {
448      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
449      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
450    }
451    return this.builderNode.getFrameNode();
452  }
453}
454
455// 被回收复用的自定义组件,其状态变量会更新,而子自定义组件ReusableChildComponent3中的状态变量也会更新,但BuilderNode会阻断这一传递过程
456@Reusable
457@Component
458struct ReusableChildComponent {
459  @Prop item: string = '';
460  @Prop switch: string = '';
461  private controller: MyNodeController = new MyNodeController();
462
463  aboutToAppear() {
464    this.controller.item = this.item;
465  }
466
467  aboutToRecycle(): void {
468    console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`);
469
470    // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ReusableChildComponent2,完成复用
471    if (this.switch === 'open') {
472      this.controller?.builderNode?.recycle();
473    }
474  }
475
476  aboutToReuse(params: object): void {
477    console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`);
478
479    // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ReusableChildComponent2,完成复用
480    if (this.switch === 'open') {
481      this.controller?.builderNode?.reuse(params);
482    }
483  }
484
485  build() {
486    Row() {
487      Text(`A${this.item}--`)
488      ReusableChildComponent3({ item: this.item })
489      NodeContainer(this.controller);
490    }
491  }
492}
493
494@Component
495struct ReusableChildComponent2 {
496  @Prop item: string = "false";
497
498  aboutToReuse(params: Record<string, object>) {
499    console.log(`${TEST_TAG} ReusableChildComponent2 aboutToReuse ${JSON.stringify(params)}`);
500  }
501
502  aboutToRecycle(): void {
503    console.log(`${TEST_TAG} ReusableChildComponent2 aboutToRecycle ${this.item}`);
504  }
505
506  build() {
507    Row() {
508      Text(`D${this.item}`)
509        .fontSize(20)
510        .backgroundColor(Color.Yellow)
511        .margin({ left: 10 })
512    }.margin({ left: 10, right: 10 })
513  }
514}
515
516@Component
517struct ReusableChildComponent3 {
518  @Prop item: string = "false";
519
520  aboutToReuse(params: Record<string, object>) {
521    console.log(`${TEST_TAG} ReusableChildComponent3 aboutToReuse ${JSON.stringify(params)}`);
522  }
523
524  aboutToRecycle(): void {
525    console.log(`${TEST_TAG} ReusableChildComponent3 aboutToRecycle ${this.item}`);
526  }
527
528  build() {
529    Row() {
530      Text(`B${this.item}`)
531        .fontSize(20)
532        .backgroundColor(Color.Yellow)
533        .margin({ left: 10 })
534    }.margin({ left: 10, right: 10 })
535  }
536}
537
538
539@Entry
540@Component
541struct Index {
542  @State data: MyDataSource = new MyDataSource();
543
544  aboutToAppear() {
545    for (let i = 0; i < 100; i++) {
546      this.data.pushData(i.toString());
547    }
548  }
549
550  build() {
551    Column() {
552      List({ space: 3 }) {
553        LazyForEach(this.data, (item: string) => {
554          ListItem() {
555            ReusableChildComponent({
556              item: item,
557              switch: 'open' // 将open改为close可观察到,BuilderNode不通过reuse和recycle接口传递复用时,BuilderNode内部的自定义组件的行为表现
558            })
559          }
560        }, (item: string) => item)
561      }
562      .width('100%')
563      .height('100%')
564    }
565  }
566}
567```
568
569
570## 通过系统环境变化更新节点
571
572使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)来监听[系统环境变化](../reference/apis-ability-kit/js-apis-app-ability-configuration.md)事件,以触发节点的全量更新。
573
574> **说明:**
575>
576> updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。
577
578```ts
579import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
580import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit';
581
582class Params {
583  text: string = ""
584
585  constructor(text: string) {
586    this.text = text;
587  }
588}
589
590// 自定义组件
591@Component
592struct TextBuilder {
593  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
594  @Prop message: string = "TextBuilder";
595
596  build() {
597    Row() {
598      Column() {
599        Text(this.message)
600          .fontSize(50)
601          .fontWeight(FontWeight.Bold)
602          .margin({ bottom: 36 })
603          .fontColor($r(`app.color.text_color`))
604          .backgroundColor($r(`app.color.start_window_background`))
605      }
606    }
607  }
608}
609
610@Builder
611function buildText(params: Params) {
612  Column() {
613    Text(params.text)
614      .fontSize(50)
615      .fontWeight(FontWeight.Bold)
616      .margin({ bottom: 36 })
617      .fontColor($r(`app.color.text_color`))
618    TextBuilder({ message: params.text }) // 自定义组件
619  }.backgroundColor($r(`app.color.start_window_background`))
620}
621
622class TextNodeController extends NodeController {
623  private textNode: BuilderNode<[Params]> | null = null;
624  private message: string = "";
625
626  constructor(message: string) {
627    super()
628    this.message = message;
629  }
630
631  makeNode(context: UIContext): FrameNode | null {
632    return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null;
633  }
634
635  createNode(context: UIContext) {
636    this.textNode = new BuilderNode(context);
637    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
638    builderNodeMap.push(this.textNode);
639  }
640
641  deleteNode() {
642    let node = builderNodeMap.pop();
643    node?.dispose();
644  }
645
646  update(message: string) {
647    if (this.textNode !== null) {
648      // 调用update进行更新。
649      this.textNode.update(new Params(message));
650    }
651  }
652}
653
654// 记录创建的自定义节点对象
655const builderNodeMap: Array<BuilderNode<[Params]>> = new Array();
656
657function updateColorMode() {
658  builderNodeMap.forEach((value, index) => {
659    // 通知BuilderNode环境变量改变
660    value.updateConfiguration();
661  })
662}
663
664@Entry
665@Component
666struct Index {
667  @State message: string = "hello"
668  private textNodeController: TextNodeController = new TextNodeController(this.message);
669  private count = 0;
670
671  aboutToAppear(): void {
672    let environmentCallback: EnvironmentCallback = {
673      onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {
674        console.log('onMemoryLevel');
675      },
676      onConfigurationUpdated: (config: Configuration): void => {
677        console.log('onConfigurationUpdated ' + JSON.stringify(config));
678        updateColorMode();
679      }
680    }
681    // 注册监听回调
682    this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback);
683    //创建自定义节点并添加至map
684    this.textNodeController.createNode(this.getUIContext());
685  }
686
687  aboutToDisappear(): void {
688    //移除map中的引用,并将自定义节点释放
689    this.textNodeController.deleteNode();
690  }
691
692  build() {
693    Row() {
694      Column() {
695        NodeContainer(this.textNodeController)
696          .width('100%')
697          .height(200)
698          .backgroundColor('#FFF0F0F0')
699        Button('Update')
700          .onClick(() => {
701            this.count += 1;
702            const message = "Update " + this.count.toString();
703            this.textNodeController.update(message);
704          })
705      }
706      .width('100%')
707      .height('100%')
708    }
709    .height('100%')
710  }
711}
712```
713
714## 跨页面复用注意事项
715
716在使用[路由](../reference/apis-arkui/js-apis-arkui-UIContext.md#router)接口[router.replaceUrl](../reference/apis-arkui/js-apis-arkui-UIContext.md#replaceurl)、[router.back](../reference/apis-arkui/js-apis-arkui-UIContext.md#back)、[router.clear](../reference/apis-arkui/js-apis-arkui-UIContext.md#clear)、[router.replaceNamedRoute](../reference/apis-arkui/js-apis-arkui-UIContext.md#replacenamedroute)操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以[router.replaceNamedRoute](../reference/apis-arkui/js-apis-arkui-UIContext.md#replacenamedroute)为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为false。
717
718```ts
719// ets/pages/Index.ets
720import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
721import "ets/pages/PageTwo"
722
723@Builder
724function buildText() {
725  // @Builder中使用语法节点生成BuilderProxyNode
726  if (true) {
727    MyComponent()
728  }
729}
730
731@Component
732struct MyComponent {
733  @StorageLink("isShowText") isShowText: boolean = true;
734
735  build() {
736    if (this.isShowText) {
737      Column() {
738        Text("BuilderNode Reuse")
739          .fontSize(36)
740          .fontWeight(FontWeight.Bold)
741          .padding(16)
742      }
743    }
744  }
745}
746
747class TextNodeController extends NodeController {
748  private rootNode: FrameNode | null = null;
749  private textNode: BuilderNode<[]> | null = null;
750
751  makeNode(context: UIContext): FrameNode | null {
752    this.rootNode = new FrameNode(context);
753
754    if (AppStorage.has("textNode")) {
755      // 复用AppStorage中的BuilderNode
756      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
757      const parent = this.textNode.getFrameNode()?.getParent();
758      if (parent) {
759        parent.removeChild(this.textNode.getFrameNode());
760      }
761    } else {
762      this.textNode = new BuilderNode(context);
763      this.textNode.build(wrapBuilder<[]>(buildText));
764      // 将创建的BuilderNode存入AppStorage
765      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
766    }
767    this.rootNode.appendChild(this.textNode.getFrameNode());
768
769    return this.rootNode;
770  }
771}
772
773@Entry({ routeName: "myIndex" })
774@Component
775struct Index {
776  aboutToAppear(): void {
777    AppStorage.setOrCreate<boolean>("isShowText", true);
778  }
779
780  build() {
781    Row() {
782      Column() {
783        NodeContainer(new TextNodeController())
784          .width('100%')
785          .backgroundColor('#FFF0F0F0')
786        Button('Router pageTwo')
787          .onClick(() => {
788            // 改变AppStorage中的状态变量触发Text节点的重新创建
789            AppStorage.setOrCreate<boolean>("isShowText", false);
790
791            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
792          })
793          .margin({ top: 16 })
794      }
795      .width('100%')
796      .height('100%')
797      .padding(16)
798    }
799    .height('100%')
800  }
801}
802```
803
804PageTwo的实现如下:
805
806```ts
807// ets/pages/PageTwo.ets
808// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失
809import "ets/pages/Index"
810
811@Entry({ routeName: "pageTwo" })
812@Component
813struct PageTwo {
814  build() {
815    Column() {
816      Button('Router replace to index')
817        .onClick(() => {
818          this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" });
819        })
820    }
821    .height('100%')
822    .width('100%')
823    .alignItems(HorizontalAlign.Center)
824    .padding(16)
825  }
826}
827```
828
829![BuilderNode Reuse Example](./figures/builder_node_reuse.gif)
830
831在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。
832
833API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。
834
835```ts
836// ets/pages/Index.ets
837import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
838import "ets/pages/PageTwo"
839
840@Builder
841function buildText() {
842  // @Builder中使用语法节点生成BuilderProxyNode
843  if (true) {
844    MyComponent()
845  }
846}
847
848@Component
849struct MyComponent {
850  @StorageLink("isShowText") isShowText: boolean = true;
851
852  build() {
853    if (this.isShowText) {
854      Column() {
855        Text("BuilderNode Reuse")
856          .fontSize(36)
857          .fontWeight(FontWeight.Bold)
858          .padding(16)
859      }
860    }
861  }
862}
863
864class TextNodeController extends NodeController {
865  private rootNode: FrameNode | null = null;
866  private textNode: BuilderNode<[]> | null = null;
867
868  makeNode(context: UIContext): FrameNode | null {
869    this.rootNode = new FrameNode(context);
870
871    if (AppStorage.has("textNode")) {
872      // 复用AppStorage中的BuilderNode
873      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
874      const parent = this.textNode.getFrameNode()?.getParent();
875      if (parent) {
876        parent.removeChild(this.textNode.getFrameNode());
877      }
878    } else {
879      this.textNode = new BuilderNode(context);
880      this.textNode.build(wrapBuilder<[]>(buildText));
881      // 将创建的BuilderNode存入AppStorage
882      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
883    }
884    this.rootNode.appendChild(this.textNode.getFrameNode());
885
886    return this.rootNode;
887  }
888}
889
890@Entry({ routeName: "myIndex" })
891@Component
892struct Index {
893  aboutToAppear(): void {
894    AppStorage.setOrCreate<boolean>("isShowText", true);
895  }
896
897  build() {
898    Row() {
899      Column() {
900        NodeContainer(new TextNodeController())
901          .width('100%')
902          .backgroundColor('#FFF0F0F0')
903        Button('Router pageTwo')
904          .onClick(() => {
905            // 改变AppStorage中的状态变量触发Text节点的重新创建
906            AppStorage.setOrCreate<boolean>("isShowText", false);
907            // 将BuilderNode从AppStorage中移除
908            AppStorage.delete("textNode");
909
910            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
911          })
912          .margin({ top: 16 })
913      }
914      .width('100%')
915      .height('100%')
916      .padding(16)
917    }
918    .height('100%')
919  }
920}
921```
922
923
924## BuilderNode中使用LocalStorage
925
926从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)。
927
928```ts
929import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI';
930
931let localStorage1: LocalStorage = new LocalStorage();
932localStorage1.setOrCreate('PropA', 'PropA');
933
934let localStorage2: LocalStorage = new LocalStorage();
935localStorage2.setOrCreate('PropB', 'PropB');
936
937@Entry(localStorage1)
938@Component
939struct Index {
940  // 'PropA',和localStorage1中'PropA'的双向同步
941  @LocalStorageLink('PropA') PropA: string = 'Hello World';
942  @State count: number = 0;
943  private controller: NodeController = new MyNodeController(this.count, localStorage2);
944
945  build() {
946    Row() {
947      Column() {
948        Text(this.PropA)
949          .fontSize(50)
950          .fontWeight(FontWeight.Bold)
951        // 使用LocalStorage 实例localStorage2
952        Child({ count: this.count }, localStorage2)
953        NodeContainer(this.controller)
954      }
955      .width('100%')
956    }
957    .height('100%')
958  }
959}
960
961interface Params {
962  count: number;
963  localStorage: LocalStorage;
964}
965
966@Builder
967function CreateChild(params: Params) {
968  //构造过程中传递localStorage
969  Child({ count: params.count }, params.localStorage)
970}
971
972class MyNodeController extends NodeController {
973  private count?: number;
974  private localStorage ?: LocalStorage;
975
976  constructor(count: number, localStorage: LocalStorage) {
977    super();
978    this.count = count;
979    this.localStorage = localStorage;
980  }
981
982  makeNode(uiContext: UIContext): FrameNode | null {
983    let builderNode = new BuilderNode<[Params]>(uiContext);
984    //构造过程中传递localStorage
985    builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage });
986    return builderNode.getFrameNode();
987  }
988}
989
990@Component
991struct Child {
992  @Prop count: number;
993  //  'Hello World',和localStorage2中'PropB'的双向同步,如果localStorage2中没有'PropB',则使用默认值'Hello World'
994  @LocalStorageLink('PropB') PropB: string = 'Hello World';
995
996  build() {
997    Text(this.PropB)
998      .fontSize(50)
999      .fontWeight(FontWeight.Bold)
1000  }
1001}
1002```
1003