1# 使用离线Web组件 2 3Web组件能够实现在不同窗口的组件树上进行挂载或移除操作,这一能力使得开发者可以预先创建Web组件,从而实现性能优化。例如,Tab页为Web组件时,页面预先渲染,便于即时显示。 4 5离线Web组件基于自定义占位组件[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)实现。基本原理是构建支持命令式创建的Web组件,此类组件创建后不会立即挂载到组件树中,状态为Hidden和InActive,因此不会立即对用户呈现。开发者可以在后续使用中按需动态挂载这些组件,以实现更灵活的使用方式。 6 7使用离线Web组件可以预启动渲染进程和预渲染Web页面。 8 9- 预启动渲染进程:在未进入Web页面时,提前创建空Web组件,启动Web的渲染进程,为后续使用做好准备。 10- 预渲染Web页面:在Web页面启动或跳转的场景下,预先在后台创建Web组件,加载数据并完成渲染,从而在Web页面启动或跳转时实现快速显示。 11 12## 整体架构 13 14如下图所示,在需要离屏创建Web组件时,定义一个自定义组件以封装Web组件,此Web组件在离线状态下被创建,封装于无状态的[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)节点中,并与相应的[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)组件绑定。Web组件在后台预渲染完毕后,当需要展示时,通过[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)将其挂载到ViewTree的[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)中,即与对应的[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)组件绑定,即可挂载上树并显示。 15 16 17 18## 创建离线Web组件 19 20本示例展示了如何预先创建离线Web组件,并在需要的时候进行挂载和显示。在后续内容中,预启动渲染进程和预渲染Web页面作为性能优化措施,均利用离线Web组件实现。 21 22> **说明:** 23> 24> 创建Web组件将占用内存(每个Web组件大约200MB)和计算资源,建议避免一次性创建大量离线Web组件,以减少资源消耗。 25 26```ts 27// 载体Ability 28// EntryAbility.ets 29import { createNWeb } from "../pages/common" 30onWindowStageCreate(windowStage: window.WindowStage): void { 31 windowStage.loadContent('pages/Index', (err, data) => { 32 // 创建Web动态组件(需传入UIContext),loadContent之后的任意时机均可创建 33 createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext()); 34 if (err.code) { 35 return; 36 } 37 }); 38} 39``` 40 41```ts 42// 创建NodeController 43// common.ets 44import { UIContext, NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI'; 45import { webview } from '@kit.ArkWeb'; 46 47// @Builder中为动态组件的具体组件内容 48// Data为入参封装类 49class Data{ 50 url: ResourceStr = "https://www.example.com"; 51 controller: WebviewController = new webview.WebviewController(); 52} 53 54@Builder 55function WebBuilder(data:Data) { 56 Column() { 57 Web({ src: data.url, controller: data.controller }) 58 .width("100%") 59 .height("100%") 60 } 61} 62 63let wrap = wrapBuilder<Data[]>(WebBuilder); 64 65// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用 66export class myNodeController extends NodeController { 67 private rootnode: BuilderNode<Data[]> | null = null; 68 // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中 69 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新 70 makeNode(uiContext: UIContext): FrameNode | null { 71 console.log(" uicontext is undefined : "+ (uiContext === undefined)); 72 if (this.rootnode != null) { 73 // 返回FrameNode节点 74 return this.rootnode.getFrameNode(); 75 } 76 // 返回null控制动态组件脱离绑定节点 77 return null; 78 } 79 // 当布局大小发生变化时进行回调 80 aboutToResize(size: Size) { 81 console.log("aboutToResize width : " + size.width + " height : " + size.height ); 82 } 83 84 // 当controller对应的NodeContainer在Appear的时候进行回调 85 aboutToAppear() { 86 console.log("aboutToAppear"); 87 } 88 89 // 当controller对应的NodeContainer在Disappear的时候进行回调 90 aboutToDisappear() { 91 console.log("aboutToDisappear"); 92 } 93 94 // 此函数为自定义函数,可作为初始化函数使用 95 // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容 96 initWeb(url:ResourceStr, uiContext:UIContext, control:WebviewController) { 97 if(this.rootnode != null) 98 { 99 return; 100 } 101 // 创建节点,需要uiContext 102 this.rootnode = new BuilderNode(uiContext); 103 // 创建动态Web组件 104 this.rootnode.build(wrap, { url:url, controller:control }); 105 } 106} 107// 创建Map保存所需要的NodeController 108let NodeMap:Map<ResourceStr, myNodeController | undefined> = new Map(); 109// 创建Map保存所需要的WebViewController 110let controllerMap:Map<ResourceStr, WebviewController | undefined> = new Map(); 111 112// 初始化需要UIContext,需在Ability获取 113export const createNWeb = (url: ResourceStr, uiContext: UIContext) => { 114 // 创建NodeController 115 let baseNode = new myNodeController(); 116 let controller = new webview.WebviewController() ; 117 // 初始化自定义Web组件 118 baseNode.initWeb(url, uiContext, controller); 119 controllerMap.set(url, controller) 120 NodeMap.set(url, baseNode); 121} 122// 自定义获取NodeController接口 123export const getNWeb = (url: ResourceStr) : myNodeController | undefined => { 124 return NodeMap.get(url); 125} 126``` 127 128```ts 129// 使用NodeController的Page页 130// Index.ets 131import { getNWeb } from "./common" 132@Entry 133@Component 134struct Index { 135 build() { 136 Row() { 137 Column() { 138 // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode 139 // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示 140 NodeContainer(getNWeb("https://www.example.com")) 141 .height("90%") 142 .width("100%") 143 } 144 .width('100%') 145 } 146 .height('100%') 147 } 148} 149``` 150 151## 预启动渲染进程 152 153在后台预先创建一个Web组件,以启动用于渲染的Web渲染进程,这样可以节省后续Web组件加载时启动Web渲染进程所需的时间。 154 155> **说明:** 156> 157> 仅在采用单渲染进程模式的应用中,即全局共享一个Web渲染进程时,优化效果显著。Web渲染进程仅在所有Web组件都被销毁后才会终止。因此,建议应用至少保持一个Web组件处于活动状态。 158 159示例在onWindowStageCreate时预创建Web组件加载blank页面,提前启动Render进程,从index跳转到index2时,优化了Web渲染进程启动和初始化的耗时。 160 161由于创建额外的Web组件会产生内存开销,建议在此方案的基础上复用该Web组件。 162 163```ts 164// 载体Ability 165// EntryAbility.ets 166import { createNWeb } from "../pages/common" 167onWindowStageCreate(windowStage: window.WindowStage): void { 168 windowStage.loadContent('pages/Index', (err, data) => { 169 // 创建空的Web动态组件(需传入UIContext),loadContent之后的任意时机均可创建 170 createNWeb("about:blank", windowStage.getMainWindowSync().getUIContext()); 171 if (err.code) { 172 return; 173 } 174 }); 175} 176``` 177 178```ts 179// 创建NodeController 180// common.ets 181import { UIContext, NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI'; 182import { webview } from '@kit.ArkWeb'; 183 184// @Builder中为动态组件的具体组件内容 185// Data为入参封装类 186class Data{ 187 url: ResourceStr = "https://www.example.com"; 188 controller: WebviewController = new webview.WebviewController(); 189} 190 191@Builder 192function WebBuilder(data:Data) { 193 Column() { 194 Web({ src: data.url, controller: data.controller }) 195 .width("100%") 196 .height("100%") 197 } 198} 199 200let wrap = wrapBuilder<Data[]>(WebBuilder); 201 202// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用 203export class myNodeController extends NodeController { 204 private rootnode: BuilderNode<Data[]> | null = null; 205 // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中 206 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新 207 makeNode(uiContext: UIContext): FrameNode | null { 208 console.log(" uicontext is undefined : "+ (uiContext === undefined)); 209 if (this.rootnode != null) { 210 // 返回FrameNode节点 211 return this.rootnode.getFrameNode(); 212 } 213 // 返回null控制动态组件脱离绑定节点 214 return null; 215 } 216 // 当布局大小发生变化时进行回调 217 aboutToResize(size: Size) { 218 console.log("aboutToResize width : " + size.width + " height : " + size.height ); 219 } 220 221 // 当controller对应的NodeContainer在Appear的时候进行回调 222 aboutToAppear() { 223 console.log("aboutToAppear"); 224 } 225 226 // 当controller对应的NodeContainer在Disappear的时候进行回调 227 aboutToDisappear() { 228 console.log("aboutToDisappear"); 229 } 230 231 // 此函数为自定义函数,可作为初始化函数使用 232 // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容 233 initWeb(url:ResourceStr, uiContext:UIContext, control:WebviewController) { 234 if(this.rootnode != null) 235 { 236 return; 237 } 238 // 创建节点,需要uiContext 239 this.rootnode = new BuilderNode(uiContext); 240 // 创建动态Web组件 241 this.rootnode.build(wrap, { url:url, controller:control }); 242 } 243} 244// 创建Map保存所需要的NodeController 245let NodeMap:Map<ResourceStr, myNodeController | undefined> = new Map(); 246// 创建Map保存所需要的WebViewController 247let controllerMap:Map<ResourceStr, WebviewController | undefined> = new Map(); 248 249// 初始化需要UIContext 需在Ability获取 250export const createNWeb = (url: ResourceStr, uiContext: UIContext) => { 251 // 创建NodeController 252 let baseNode = new myNodeController(); 253 let controller = new webview.WebviewController() ; 254 // 初始化自定义Web组件 255 baseNode.initWeb(url, uiContext, controller); 256 controllerMap.set(url, controller) 257 NodeMap.set(url, baseNode); 258} 259// 自定义获取NodeController接口 260export const getNWeb = (url: ResourceStr) : myNodeController | undefined => { 261 return NodeMap.get(url); 262} 263``` 264 265```ts 266// index.ets 267import { webview } from '@kit.ArkWeb'; 268 269@Entry 270@Component 271struct Index1 { 272 controller: webview.WebviewController = new webview.WebviewController(); 273 274 build() { 275 Column() { 276 //已经预启动Render进程 277 Button("跳转到Web页面").onClick(()=>{ 278 this.getUIContext().getRouter().pushUrl({url: "pages/index2"}); 279 }) 280 .width('100%') 281 .height('100%') 282 } 283 } 284} 285``` 286 287```ts 288// index2.ets 289import { webview } from '@kit.ArkWeb'; 290 291@Entry 292@Component 293struct index2 { 294 controller: webview.WebviewController = new webview.WebviewController(); 295 296 build() { 297 Row() { 298 Column() { 299 Web({src: 'https://www.example.com', controller: this.controller}) 300 .width('100%') 301 .height('100%') 302 } 303 .width('100%') 304 } 305 .height('100%') 306 } 307} 308``` 309 310## 预渲染Web页面 311 312预渲染Web页面优化方案适用于Web页面启动和跳转场景,例如,进入首页后,跳转到其他子页。建议在高命中率的页面使用该方案。 313 314预渲染Web页面的实现是提前创建离线Web组件,设置Web为Active状态来开启渲染引擎,进行后台渲染。 315 316> **说明:** 317> 318> 1. 预渲染Web页面时,需要明确加载的资源。 319> 2. 由于该方案会将不可见的后台Web设置为Active状态,建议不要预渲染包含自动播放音视频的页面。应用开发者请自行检查和管理页面行为。 320> 3. 预渲染的网页会在后台不断进行渲染,建议在预渲染完成后立即停止渲染,以防止发热和功耗问题。可以参考以下示例,使用 [onFirstMeaningfulPaint](../reference/apis-arkweb/ts-basic-components-web.md#onfirstmeaningfulpaint12) 来确定停止时机,该接口适用于http和https网页。 321 322```ts 323// 载体Ability 324// EntryAbility.ets 325import {createNWeb} from "../pages/common"; 326import { UIAbility } from '@kit.AbilityKit'; 327import { window } from '@kit.ArkUI'; 328 329export default class EntryAbility extends UIAbility { 330 onWindowStageCreate(windowStage: window.WindowStage): void { 331 windowStage.loadContent('pages/Index', (err, data) => { 332 // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建 333 createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext()); 334 if (err.code) { 335 return; 336 } 337 }); 338 } 339} 340``` 341 342```ts 343// 创建NodeController 344// common.ets 345import { UIContext } from '@kit.ArkUI'; 346import { webview } from '@kit.ArkWeb'; 347import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI'; 348// @Builder中为动态组件的具体组件内容 349// Data为入参封装类 350class Data{ 351 url: string = 'https://www.example.com'; 352 controller: WebviewController = new webview.WebviewController(); 353} 354// 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染 355let shouldInactive: boolean = true; 356@Builder 357function WebBuilder(data:Data) { 358 Column() { 359 Web({ src: data.url, controller: data.controller }) 360 .onPageBegin(() => { 361 // 调用onActive,开启渲染 362 data.controller.onActive(); 363 }) 364 .onFirstMeaningfulPaint(() =>{ 365 if (!shouldInactive) { 366 return; 367 } 368 // 在预渲染完成时触发,停止渲染 369 data.controller.onInactive(); 370 shouldInactive = false; 371 }) 372 .width("100%") 373 .height("100%") 374 } 375} 376let wrap = wrapBuilder<Data[]>(WebBuilder); 377// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用 378export class myNodeController extends NodeController { 379 private rootnode: BuilderNode<Data[]> | null = null; 380 // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中 381 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新 382 makeNode(uiContext: UIContext): FrameNode | null { 383 console.info(" uicontext is undifined : "+ (uiContext === undefined)); 384 if (this.rootnode != null) { 385 // 返回FrameNode节点 386 return this.rootnode.getFrameNode(); 387 } 388 // 返回null控制动态组件脱离绑定节点 389 return null; 390 } 391 // 当布局大小发生变化时进行回调 392 aboutToResize(size: Size) { 393 console.info("aboutToResize width : " + size.width + " height : " + size.height ) 394 } 395 // 当controller对应的NodeContainer在Appear的时候进行回调 396 aboutToAppear() { 397 console.info("aboutToAppear") 398 // 切换到前台后,不需要停止渲染 399 shouldInactive = false; 400 } 401 // 当controller对应的NodeContainer在Disappear的时候进行回调 402 aboutToDisappear() { 403 console.info("aboutToDisappear") 404 } 405 // 此函数为自定义函数,可作为初始化函数使用 406 // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容 407 initWeb(url:string, uiContext:UIContext, control:WebviewController) { 408 if(this.rootnode != null) 409 { 410 return; 411 } 412 // 创建节点,需要uiContext 413 this.rootnode = new BuilderNode(uiContext) 414 // 创建动态Web组件 415 this.rootnode.build(wrap, { url:url, controller:control }) 416 } 417} 418// 创建Map保存所需要的NodeController 419let NodeMap:Map<string, myNodeController | undefined> = new Map(); 420// 创建Map保存所需要的WebViewController 421let controllerMap:Map<string, WebviewController | undefined> = new Map(); 422// 初始化需要UIContext 需在Ability获取 423export const createNWeb = (url: string, uiContext: UIContext) => { 424 // 创建NodeController 425 let baseNode = new myNodeController(); 426 let controller = new webview.WebviewController() ; 427 // 初始化自定义Web组件 428 baseNode.initWeb(url, uiContext, controller); 429 controllerMap.set(url, controller) 430 NodeMap.set(url, baseNode); 431} 432// 自定义获取NodeController接口 433export const getNWeb = (url : string) : myNodeController | undefined => { 434 return NodeMap.get(url); 435} 436``` 437 438```ts 439// 使用NodeController的Page页 440// Index.ets 441import {createNWeb, getNWeb} from "./common"; 442 443@Entry 444@Component 445struct Index { 446 build() { 447 Row() { 448 Column() { 449 // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode 450 // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示 451 NodeContainer(getNWeb("https://www.example.com")) 452 .height("90%") 453 .width("100%") 454 } 455 .width('100%') 456 } 457 .height('100%') 458 } 459} 460``` 461 462## 常见白屏问题排查 463 4641.排查应用上网权限配置。 465 466检查是否已在module.json5中添加网络权限,添加方法请参考在[配置文件中声明权限](../security/AccessToken/declare-permissions.md)。 467 468```ts 469"requestPermissions":[ 470 { 471 "name" : "ohos.permission.INTERNET" 472 } 473 ] 474``` 475 4762.排查[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)与节点绑定的逻辑。 477 478检查节点是否已上组件树,建议在已有的Web组件上方加上Text(请参考以下例子),如果白屏的时候没有出现Text,建议检查[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)与节点绑定的情况。 479 480```ts 481@Builder 482function WebBuilder(data:Data) { 483 Column() { 484 Text('test') 485 Web({ src: data.url, controller: data.controller }) 486 .width("100%") 487 .height("100%") 488 } 489} 490``` 491 4923.排查Web可见性状态。 493 494如果整个节点已上树,可通过日志[WebPattern::OnVisibleAreaChange](../reference/apis-arkui/arkui-ts/ts-universal-component-visible-area-change-event.md#onvisibleareachange)查看Web组件可见性状态是否正确,不可见的Web组件可能会造成白屏。