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 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