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

鸿蒙系统中,Web组件能够实现在不同窗口的组件树上进行挂载或移除操作。这一能力使得开发者可以预先创建Web组件,从而实现性能优化。例如,当Tab页为Web组件时,可以在后台预先渲染,便于即时显示。

离线Web组件基于自定义占位组件[NodeContainer]实现,核心原理是:构建支持命令式创建的Web组件,此类组件创建后不会立即挂载到组件树中,状态为Hidden和Inactive,因此不会立即对用户呈现。可以在后续使用中按需动态挂载这些组件。

二、离线Web组件

1. 预启动渲染进程

  • 在未进入Web页面时,提前创建空Web组件

  • 启动Web的渲染进程,为后续使用做好准备

  • 节省Web组件加载时启动渲染进程的时间

2. 预渲染Web页面

  • 在Web页面启动或跳转场景下使用

  • 预先在后台创建Web组件,加载数据并完成渲染

  • 实现Web页面启动或跳转时的快速显示

三、工作原理

架构流程图

离屏创建Web组件
    ↓
定义自定义组件封装Web组件
    ↓
封装于无状态的NodeContainer节点中
    ↓
与NodeController组件绑定
    ↓
Web组件后台预渲染
    ↓
需要展示时通过NodeController挂载
    ↓
挂载到ViewTree的NodeContainer中
    ↓
显示

组件关系

  • NodeContainer:自定义占位组件,用于挂载动态组件

  • NodeController:节点控制器,控制和反馈NodeContainer上的节点行为

  • BuilderNode:构建动态组件的核心类

  • WebviewController:Web组件控制器,管理Web行为

四、创建离线Web组件

1. 在Ability中预创建Web组件

// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err, data) => {
        // 创建Web动态组件(需传入UIContext)
        // loadContent之后的任意时机均可创建
        createNWeb('www.example.com', 
                   windowStage.getMainWindowSync().getUIContext());
        if (err.code) {
            return;
        }
    });
}

2. 创建NodeController和Builder

// Common.ets
import { UIContext, NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';

// Data为入参封装类
class Data {
    public url: ResourceStr = 'www.example.com';
    public controller: WebviewController = new webview.WebviewController();
}

@Builder
function webBuilder(data: Data) {
    Column() {
        Web({ src: data.url, controller: data.controller })
            .width('100%')
            .height('100%')
    }
}

let wrap = wrapBuilder<Data[]>(webBuilder);

// 用于控制和反馈对应的NodeContainer上的节点的行为
export class MyNodeController extends NodeController {
    private rootNode: BuilderNode<Data[]> | null = null;

    // 必须重写的方法:构建节点数、返回节点挂载在对应NodeContainer中
    makeNode(uiContext: UIContext): FrameNode | null {
        console.info('uicontext is undefined : ' + (uiContext === undefined));
        if (this.rootNode !== null) {
            return this.rootNode.getFrameNode(); // 返回FrameNode节点
        }
        return null; // 返回null控制动态组件脱离绑定节点
    }

    // 当布局大小发生变化时回调
    aboutToResize(size: Size) {
        console.info('aboutToResize width : ' + size.width + ' height : ' + size.height);
    }

    // 当controller对应的NodeContainer在Appear时回调
    aboutToAppear() {
        console.info('aboutToAppear');
    }

    // 当controller对应的NodeContainer在Disappear时回调
    aboutToDisappear() {
        console.info('aboutToDisappear');
    }

    // 自定义初始化函数:通过UIContext初始化BuilderNode
    initWeb(url: ResourceStr, uiContext: UIContext, control: WebviewController) {
        if (this.rootNode !== null) {
            return;
        }
        // 创建节点,需要uiContext
        this.rootNode = new BuilderNode(uiContext);
        // 创建动态Web组件
        this.rootNode.build(wrap, { url: url, controller: control });
    }
}

// 创建Map保存所需要的NodeController
let nodeMap: Map<ResourceStr, MyNodeController | undefined> = new Map();
// 创建Map保存所需要的WebViewController
let controllerMap: Map<ResourceStr, WebviewController | undefined> = new Map();

// 初始化需要UIContext,需在Ability获取
export const createNWeb = (url: ResourceStr, uiContext: UIContext) => {
    // 创建NodeController
    let baseNode = new MyNodeController();
    let controller = new webview.WebviewController();
    // 初始化自定义Web组件
    baseNode.initWeb(url, uiContext, controller);
    controllerMap.set(url, controller);
    nodeMap.set(url, baseNode);
}

// 自定义获取NodeController接口
export const getNWeb = (url: ResourceStr): MyNodeController | undefined => {
    return nodeMap.get(url);
}

3. 在页面中挂载显示

// Index.ets
import { getNWeb } from './common'

@Entry
@Component
struct Index {
    build() {
        Row() {
            Column() {
                // NodeContainer用于与NodeController节点绑定
                // rebuild会触发makeNode
                // Page页通过NodeContainer接口绑定NodeController
                NodeContainer(getNWeb('www.example.com'))
                    .height('90%')
                    .width('100%')
            }
            .width('100%')
        }
        .height('100%')
    }
}

五、预启动优化

场景

  • 采用单渲染进程模式的应用(全局共享一个Web渲染进程)

  • Web渲染进程仅在所有Web组件都被销毁后才会终止

  • 建议应用至少保持一个Web组件处于活动状态

原理

在onWindowStageCreate时预创建Web组件加载blank页面,提前启动Render进程,从index跳转到index2时,优化Web渲染进程启动和初始化的耗时。

代码实现

// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err, data) => {
        // 创建空的Web动态组件,加载about:blank页面
        // 提前启动Render进程
        createNWeb('about:blank', 
                   windowStage.getMainWindowSync().getUIContext());
        if (err.code) {
            return;
        }
    });
}

// index.ets - 首页
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct Index1 {
    webviewController: webview.WebviewController = new webview.WebviewController();
    
    build() {
        Column() {
            // 已经预启动Render进程,跳转后Web页面加载更快
            Button('Jump to web page').onClick(()=>{
                this.getUIContext().getRouter().pushUrl({url: 'pages/index2'});
            })
            .width('100%')
            .height('100%')
        }
    }
}

// index2.ets - 目标页
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct index2 {
    webviewController: web_webview.WebviewController = new web_webview.WebviewController();
    
    build() {
        Row() {
            Column() {
                // 由于渲染进程已预启动,加载速度更快
                Web({src: 'www.example.com', controller: this.webviewController})
                    .width('100%')
                    .height('100%')
            }
            .width('100%')
        }
        .height('100%')
    }
}

注意事项

  • 内存开销:创建额外的Web组件会产生额外内存占用(每个Web组件约200MB)

  • 适用条件:仅在单渲染进程模式下优化效果显著

  • 建议:避免一次性创建大量离线Web组件

六、预渲染Web页面优化

适用场景

  • Web页面启动和跳转场景(如进入首页后跳转到子页)

  • 建议在高命中率的页面使用该方案

实现原理

提前创建离线Web组件,设置Web为Active状态来开启渲染引擎,进行后台渲染。预渲染完成后立即停止渲染,防止发热和功耗问题。

代码实现

// Common.ets - 带预渲染控制的NodeController
import { UIContext } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';
import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI';

// 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染
let shouldInactive: boolean = true;

class Data {
    public url: string = 'www.example.com';
    public controller: WebviewController = new webview.WebviewController();
}

@Builder
function webBuilder(data: Data) {
    Column() {
        Web({ src: data.url, controller: data.controller })
            .onPageBegin(() => {
                // 调用onActive,开启渲染
                data.controller.onActive();
            })
            .onFirstMeaningfulPaint(() => {
                // 首次有意义绘制完成时触发
                if (!shouldInactive) {
                    return;
                }
                // 在预渲染完成时触发,停止渲染,防止发热和功耗问题
                data.controller.onInactive();
                shouldInactive = false;
            })
            .width('100%')
            .height('100%')
    }
}

let wrap = wrapBuilder<Data[]>(webBuilder);

export class MyNodeController extends NodeController {
    private rootNode: BuilderNode<Data[]> | null = null;

    makeNode(uiContext: UIContext): FrameNode | null {
        if (this.rootNode !== null) {
            return this.rootNode.getFrameNode();
        }
        return null;
    }

    aboutToResize(size: Size) {
        console.info('aboutToResize width : ' + size.width + ' height : ' + size.height);
    }

    aboutToAppear() {
        console.info('aboutToAppear');
        // 切换到前台后,不需要停止渲染
        shouldInactive = false;
    }

    aboutToDisappear() {
        console.info('aboutToDisappear');
    }

    initWeb(url: string, uiContext: UIContext, control: WebviewController) {
        if (this.rootNode !== null) {
            return;
        }
        this.rootNode = new BuilderNode(uiContext);
        this.rootNode.build(wrap, { url: url, controller: control });
    }
}

// 创建Map保存所需要的NodeController
let nodeMap: Map<string, MyNodeController | undefined> = new Map();
let controllerMap: Map<string, WebviewController | undefined> = new Map();

export const createNWeb = (url: string, uiContext: UIContext) => {
    let baseNode = new MyNodeController();
    let controller = new webview.WebviewController();
    baseNode.initWeb(url, uiContext, controller);
    controllerMap.set(url, controller)
    nodeMap.set(url, baseNode);
}

export const getNWeb = (url: string): MyNodeController | undefined => {
    return nodeMap.get(url);
}

预渲染生命周期

  • onPageBegin:页面开始加载时调用onActive开启渲染

  • onFirstMeaningfulPaint:首次有意义绘制完成时调用onInactive停止渲染

  • aboutToAppear:组件出现在前台时重置控制变量

注意事项

  • 需要明确预加载的资源

  • 不建议预渲染包含自动播放音视频的页面

  • 预渲染完成后应立即停止渲染,防止发热和功耗问题

  • onFirstMeaningfulPaint接口仅适用于http和https网页

七、复用离线Web组件

应用有多个UI页面都需要显示Web内容时,建议复用离线Web组件,减少组件创建和销毁的性能消耗以及创建多个Web组件的内存占用。

方法

步骤1:离线Web组件不再使用时加载空白页

// 组件即将被回收时,加载about:blank
aboutToDisappear() {
    // 调用WebController的loadUrl方法加载about:blank空页面
    // 为下次其他UI页面复用做准备
    this.webviewController?.loadUrl('about:blank');
}

步骤2:新UI页面复用时加载目标页面

// 复用离线Web组件时
onPageShow() {
    // 再次调用loadUrl加载需要的Web页面
    this.webviewController?.loadUrl('www.new-page.com');
}

复用优势

  • 减少Web组件创建和销毁的性能消耗

  • 降低多个Web组件同时存在时的内存占用

  • 提升页面切换的响应速度

八、释放离线Web组件

释放时机

  • 应用退至后台

  • 明确在特定时间段内不再需要使用离线Web组件时

重要前提

仅当离线Web组件未绑定到UI页面时,才能释放该组件,否则可能导致NodeContainer组件显示空白。

代码

// Common.ets - 完整版
// 创建Map保存所需要的NodeController
let nodeMap: Map<ResourceStr, MyNodeController | undefined> = new Map();
// 创建保存uiContext的全局变量
let globalUiContext: UIContext | undefined = undefined;
// 创建Set保存已释放的离线组件url信息
let recycledNWebs: Set<ResourceStr> = new Set()

// 初始化需要UIContext,需在Ability获取
export const createNWeb = (url: ResourceStr, uiContext: UIContext) => {
    console.info('createNWeb, url = ' + url);
    
    if (!globalUiContext) {
        globalUiContext = uiContext;
    }
    
    if (getNWeb(url)) {
        console.info('createNWeb, already exit this node, url:' + url);
        return;
    }
    
    let baseNode = new MyNodeController();
    // 初始化自定义Web组件
    baseNode.initWeb(url, uiContext);
    nodeMap.set(url, baseNode);
    recycledNWebs.delete(url); // 从已释放集合中移除
}

// 自定义释放/回收离线Web组件的接口
// 释放成功返回true
export const recycleNWeb = (url: ResourceStr, force: boolean = false): boolean => {
    console.info('recycleNWeb, url = ' + url);
    
    let baseNode = nodeMap.get(url);
    if (!baseNode) {
        console.info('no such node, url = ' + url);
        return false;
    }
    
    // 检查组件是否被绑定
    if (!force && baseNode.isBound()) {
        console.info('the node is in bound and not force, can not delete');
        return false;
    }
    
    // 释放资源
    baseNode.rootNode?.dispose();
    baseNode.rebuild(); // 触发重建,使NodeContainer更新
    nodeMap.delete(url);
    recycledNWebs.add(url); // 记录已释放
    
    return true;
}

// 自定义释放所有离线Web组件的接口
export const recycleNWebs = (force: boolean = false) => {
    nodeMap.forEach((_node: MyNodeController | undefined, url: ResourceStr) => {
        recycleNWeb(url, force);
    });
}

// 自定义恢复之前释放离线Web组件的接口
export const restoreNWebs = (uiContext: UIContext | undefined = undefined) => {
    if (!uiContext) {
        uiContext = globalUiContext;
    }
    
    for (let url of recycledNWebs) {
        if (uiContext) {
            createNWeb(url, uiContext); // 重新创建
        }
    }
    recycledNWebs.clear() // 清空释放集合
}

// MyNodeController中需要添加isBound方法
export class MyNodeController extends NodeController {
    private bound: boolean = false;
    
    // ... 其他方法 ...
    
    // 检查当前节点是否被绑定到NodeContainer
    isBound(): boolean {
        return this.bound;
    }
    
    // 可以通过aboutToAppear/aboutToDisappear跟踪绑定状态
    aboutToAppear() {
        console.info('aboutToAppear');
        this.bound = true;
    }
    
    aboutToDisappear() {
        console.info('aboutToDisappear');
        this.bound = false;
    }
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐