本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
HarmonyOS 的 Web组件跨窗口迁移 功能将同一个Web组件在不同窗口之间动态挂载或移除,实现类似现代浏览器中“标签页拖拽成独立窗口”或“标签页合并”的交互。
场景:
-
浏览器标签页拖拽成独立窗口
-
多窗口应用中Web内容的动态迁移
-
分屏浏览时Web组件的灵活调度
二、技术原理
2.1 基于自定义节点能力
Web组件窗口迁移的核心技术基于HarmonyOS的 自定义节点(User-Defined Node) 能力实现:
BuilderNode:创建Web组件的离线节点
自定义占位节点:控制Web节点的挂载与移除
NodeController:管理节点的生命周期
2.2 迁移流程
原始窗口Web组件 → 移除挂载 → 创建离线节点 → 挂载到目标窗口 → 目标窗口显示Web内容
三、核心类与接口
3.1 BuilderNode
用于创建和管理离线组件的节点,支持动态构建组件树。
import { BuilderNode } from '@kit.ArkUI';
// 创建BuilderNode实例
let builderNode: BuilderNode<[Data]> = new BuilderNode(uiContext);
3.2 NodeController
必须与 NodeContainer 配合使用,负责控制节点的挂载行为。
必须重写的方法:
-
makeNode(uiContext: UIContext): FrameNode | null:构建节点树并返回
3.3 NodeContainer
用于在UI中绑定 NodeController,显示动态组件。
NodeContainer(this.nodeController)
.height('80%')
.width('80%')
四、代码示例
4.1 主窗口Ability初始化
// Entry3Ability.ets
import { createNWeb, defaultUrl } from '../pages/common';
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err && err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content.');
return;
}
// 创建Web动态组件(需传入UIContext)
// loadContent之后的任意时机均可创建,应用仅创建一个Web组件
createNWeb(defaultUrl, windowStage.getMainWindowSync().getUIContext());
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
}
4.2 通用工具模块(common.ets)
4.2.1 数据结构定义
// Data为入参封装类
class Data {
url: string = '';
webController: webview.WebviewController | null = null;
constructor(url: string, webController: webview.WebviewController) {
this.url = url;
this.webController = webController;
}
}
4.2.2 Web组件构建器
// @Builder中为动态组件的具体组件内容
@Builder
function WebBuilder(data: Data) {
Web({
src: data.url,
controller: data.webController
})
.width("100%")
.height("100%")
.borderStyle(BorderStyle.Dashed)
.borderWidth(2)
}
// 包装Builder函数
let wrap = wrapBuilder<[Data]>(WebBuilder);
4.2.3 自定义NodeController实现
export class MyNodeController extends NodeController {
private builderNode: BuilderNode<[Data]> | null | undefined = null;
private webController: webview.WebviewController | null | undefined = null;
private rootNode: FrameNode | null = null;
constructor(builderNode: BuilderNode<[Data]> | undefined,
webController: webview.WebviewController | undefined) {
super();
this.builderNode = builderNode;
this.webController = webController;
}
// 必须重写的方法:构建节点树
makeNode(uiContext: UIContext): FrameNode | null {
// 该节点会被挂载在NodeContainer的父节点下
return this.rootNode;
}
// 挂载Webview
attachWeb(): void {
if (this.builderNode) {
let frameNode: FrameNode | null = this.builderNode.getFrameNode();
// 重要:挂载前检查节点是否已被挂载
if (frameNode?.getParent() != null) {
hilog.error(0x0000, 'testTag', 'The frameNode is already attached');
return;
}
this.rootNode = this.builderNode.getFrameNode();
}
}
// 卸载Webview
detachWeb(): void {
this.rootNode = null;
}
getWebController(): webview.WebviewController | null | undefined {
return this.webController;
}
}
4.2.4 全局资源管理
// 创建Map保存BuilderNode
let builderNodeMap: Map<string, BuilderNode<[Data]> | undefined> = new Map();
// 创建Map保存WebviewController
let webControllerMap: Map<string, webview.WebviewController | undefined> = new Map();
// 初始化创建Web组件
export const createNWeb = (url: string, uiContext: UIContext) => {
// 创建WebviewController
let webController = new webview.WebviewController();
// 创建BuilderNode
let builderNode: BuilderNode<[Data]> = new BuilderNode(uiContext);
// 构建动态Web组件
builderNode.build(wrap, new Data(url, webController));
// 保存到全局Map
builderNodeMap.set(url, builderNode);
webControllerMap.set(url, webController);
}
// 获取BuilderNode
export const getBuilderNode = (url: string): BuilderNode<[Data]> | undefined => {
return builderNodeMap.get(url);
}
// 获取WebviewController
export const getWebviewController = (url: string): webview.WebviewController | undefined => {
return webControllerMap.get(url);
}
4.3 页面实现(Index.ets)
// pages/Index.ets
import { getBuilderNode, MyNodeController, defaultUrl, getWebviewController } from "./common"
@Entry
@Component
struct Index {
// 创建NodeController实例
private nodeController: MyNodeController =
new MyNodeController(getBuilderNode(defaultUrl), getWebviewController(defaultUrl));
build() {
Row() {
Column() {
// 挂载按钮
Button("Attach Webview")
.onClick(() => {
// 重要:不要将同一个节点同时挂载在不同的页面上!
this.nodeController.attachWeb();
this.nodeController.rebuild(); // 触发makeNode刷新
})
// 卸载按钮
Button("Detach Webview")
.onClick(() => {
this.nodeController.detachWeb();
this.nodeController.rebuild();
})
// NodeContainer用于与NodeController节点绑定
// rebuild会触发makeNode,实现动态显示
NodeContainer(this.nodeController)
.height('80%')
.width('80%')
}
.width('100%')
}
.height('100%')
}
}
五、注意事项
5.1 UIContext的重要性
Web组件的创建和迁移必须基于正确的 UIContext:
// 从窗口获取UIContext
windowStage.getMainWindowSync().getUIContext()
// 从组件获取UIContext
this.getUIContext()
5.2 节点状态管理
-
挂载前检查:必须检查节点是否已被挂载到其他父节点
-
及时清理:卸载节点时应清空 rootNode 引用
-
单例管理:同一个Web组件应在全局范围内只创建一个实例
5.3 重建机制
调用 rebuild() 方法会触发 makeNode() 的重新执行,实现UI的刷新:



