欢迎光临
我们一直在努力

鸿蒙 Web组件不同窗口间的迁移

本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

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的刷新:

    赞(0)
    未经允许不得转载:171主机测评 » 鸿蒙 Web组件不同窗口间的迁移
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址