1# Web组件在不同的窗口间迁移
2
3Web组件能够实现在不同窗口的组件树上进行挂载或移除操作,这一能力使得开发者可以将同一个Web组件在不同窗口间迁移。例如,将浏览器的Tab页拖出成独立窗口,或拖入浏览器的另一个窗口。
4
5Web组件在不同窗口间迁移,是基于[自定义节点](../ui/arkts-user-defined-node.md)能力实现的。实现的基本原理是:通过[BuilderNode](../ui/arkts-user-defined-arktsNode-builderNode.md),开发者可创建Web组件的离线节点,并结合[自定义占位节点](../ui/arkts-user-defined-place-hoder.md)控制Web节点的挂载与移除。当从一个窗口上移除Web节点,并挂载到另一个窗口中,即完成Web组件在窗口间的迁移。
6
7在以下示例中,主窗Ability启动时,通过命令式的方式创建了一个Web组件。开发者可以利用common.ets中提供的方法和类,实现Web组件的挂载和移除。Index.ets则提供了一种挂载和移除Web组件的实现方法。通过这种方式,开发者能够实现Web组件在不同窗口中页面的挂载与移除,即实现了Web组件在不同窗口间的迁移。下图是展示了这一迁移过程的示意图。
8
9![Web组件迁移示例](./figures/web-component-migrate.png)
10
11> **说明:**
12>
13> 不要将一个Web组件同时挂载在两个父节点下,这会导致非预期行为。
14
15```ts
16// 主窗Ability
17// EntryAbility.ets
18import { createNWeb, defaultUrl } from '../pages/common'
19
20// ...
21
22  onWindowStageCreate(windowStage: window.WindowStage): void {
23    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
24
25    windowStage.loadContent('pages/Index', (err) => {
26      if (err.code) {
27        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
28        return;
29      }
30      // 创建Web动态组件(需传入UIContext),loadContent之后的任意时机均可创建,应用仅创建一个Web组件
31      createNWeb(defaultUrl, windowStage.getMainWindowSync().getUIContext());
32      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
33    });
34  }
35
36// ...
37```
38
39```ts
40// 提供动态挂载Web组件能力
41// pages/common.ets
42import { UIContext, NodeController, BuilderNode, FrameNode } from '@kit.ArkUI';
43import { webview } from '@kit.ArkWeb';
44import { hilog } from '@kit.PerformanceAnalysisKit';
45
46export const defaultUrl : string = 'https://www.example.com';
47
48// Data为入参封装类
49class Data{
50  url: string = '';
51  webController: webview.WebviewController | null = null;
52
53  constructor(url: string, webController: webview.WebviewController) {
54    this.url = url;
55    this.webController = webController;
56  }
57}
58
59// @Builder中为动态组件的具体组件内容
60@Builder
61function WebBuilder(data:Data) {
62  Web({ src: data.url, controller: data.webController })
63    .width("100%")
64    .height("100%")
65    .borderStyle(BorderStyle.Dashed)
66    .borderWidth(2)
67}
68
69let wrap = wrapBuilder<[Data]>(WebBuilder);
70
71// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
72export class MyNodeController extends NodeController {
73  private builderNode: BuilderNode<[Data]> | null | undefined = null;
74  private webController : webview.WebviewController | null | undefined = null;
75  private rootNode : FrameNode | null = null;
76
77  constructor(builderNode : BuilderNode<[Data]> | undefined, webController : webview.WebviewController | undefined) {
78    super();
79    this.builderNode = builderNode;
80    this.webController = webController;
81  }
82
83  // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中
84  // 在对应NodeContainer创建的时候调用或者通过rebuild方法调用刷新
85  makeNode(uiContext: UIContext): FrameNode | null {
86    // 该节点会被挂载在NodeContainer的父节点下
87    return this.rootNode;
88  }
89
90  // 挂载Webview
91  attachWeb() : void {
92    if (this.builderNode) {
93      let frameNode : FrameNode | null = this.builderNode.getFrameNode();
94      if (frameNode?.getParent() != null) {
95        // 挂载自定义节点前判断该节点是否已经被挂载
96        hilog.error(0x0000, 'testTag', '%{public}s', 'The frameNode is already attached');
97        return;
98      }
99      this.rootNode = this.builderNode.getFrameNode();
100    }
101  }
102
103  // 卸载Webview
104  detachWeb() : void {
105    this.rootNode = null;
106  }
107
108  getWebController() : webview.WebviewController | null | undefined {
109    return this.webController;
110  }
111}
112
113// 创建Map保存所需要的BuilderNode
114let builderNodeMap : Map<string, BuilderNode<[Data]> | undefined> = new Map();
115// 创建Map保存所需要的webview.WebviewController
116let webControllerMap : Map<string, webview.WebviewController | undefined> = new Map();
117
118// 初始化需要UIContext对象,UIContext对象可通过窗口或自定义组件的getUIContext方法获取
119export const createNWeb = (url: string, uiContext: UIContext) => {
120  // 创建WebviewController
121  let webController = new webview.WebviewController() ;
122  // 创建BuilderNode
123  let builderNode : BuilderNode<[Data]> = new BuilderNode(uiContext);
124  // 创建动态Web组件
125  builderNode.build(wrap, new Data(url, webController));
126
127  // 保存BuilderNode
128  builderNodeMap.set(url, builderNode);
129  // 保存WebviewController
130  webControllerMap.set(url, webController);
131}
132
133// 自定义获取BuilderNode的接口
134export const getBuilderNode = (url : string) : BuilderNode<[Data]> | undefined => {
135  return builderNodeMap.get(url);
136}
137// 自定义获取WebviewController的接口
138export const getWebviewController = (url : string) : webview.WebviewController | undefined => {
139  return webControllerMap.get(url);
140}
141
142```
143
144```ts
145// 使用NodeController的Page页
146// pages/Index.ets
147import { getBuilderNode, MyNodeController, defaultUrl, getWebviewController } from "./common"
148
149@Entry
150@Component
151struct Index {
152  private nodeController : MyNodeController =
153    new MyNodeController(getBuilderNode(defaultUrl), getWebviewController(defaultUrl));
154
155  build() {
156    Row() {
157      Column() {
158        Button("Attach Webview")
159          .onClick(() => {
160            // 注意不要将同一个节点同时挂载在不同的页面上!
161            this.nodeController.attachWeb();
162            this.nodeController.rebuild();
163          })
164        Button("Detach Webview")
165          .onClick(() => {
166            this.nodeController.detachWeb();
167            this.nodeController.rebuild();
168          })
169        // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode
170        // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示
171        NodeContainer(this.nodeController)
172          .height("80%")
173          .width("80%")
174      }
175      .width('100%')
176    }
177    .height('100%')
178  }
179}
180
181```
182
183