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