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的渲染进程,为后续的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![web-offline-mode](figures/web-offline-mode.png)
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的Render进程启动和初始化的耗时。
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
266import router from '@ohos.router'
267@Entry
268@Component
269struct Index1 {
270  WebviewController: webview.WebviewController = new webview.WebviewController();
271
272  build() {
273    Column() {
274      //已经预启动Render进程
275      Button("跳转到Web页面").onClick(()=>{
276        router.pushUrl({url: "pages/index2"})
277      })
278        .width('100%')
279        .height('100%')
280    }
281  }
282}
283```
284
285```ts
286import web_webview from '@ohos.web.webview'
287@Entry
288@Component
289struct index2 {
290  WebviewController: webview.WebviewController = new webview.WebviewController();
291
292  build() {
293    Row() {
294      Column() {
295        Web({src: 'https://www.example.com', controller: this.webviewController})
296          .width('100%')
297          .height('100%')
298      }
299      .width('100%')
300    }
301    .height('100%')
302  }
303}
304```
305
306## 预渲染Web页面
307
308预渲染Web页面优化方案适用于Web页面启动和跳转场景,例如,进入首页后,跳转到其他子页。建议在高命中率的页面使用该方案。
309
310预渲染Web页面的实现方案是提前创建离线Web组件,并设置Web为Active状态来开启渲染引擎,进行后台渲染。
311
312> **说明:**
313>
314> 1. 预渲染的Web页面需要确定加载的资源。
315> 2. 由于该方案会将激活不可见的后台Web(即设置为Active状态),建议不要对存在自动播放音视频的页面进行预渲染。应用侧请自行检查和管理页面的行为。
316> 3. 在后台,预渲染的网页会持续进行渲染,为了防止发热和功耗问题,建议在预渲染完成后立即停止渲染过程。可以参考以下示例,使用 [onFirstMeaningfulPaint](../reference/apis-arkweb/ts-basic-components-web.md#onfirstmeaningfulpaint12) 来确定预渲染的停止时机,该接口适用于http和https的在线网页。
317
318```ts
319// 载体Ability
320// EntryAbility.ets
321import {createNWeb} from "../pages/common";
322import { UIAbility } from '@kit.AbilityKit';
323import { window } from '@kit.ArkUI';
324
325export default class EntryAbility extends UIAbility {
326  onWindowStageCreate(windowStage: window.WindowStage): void {
327    windowStage.loadContent('pages/Index', (err, data) => {
328      // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建
329      createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
330      if (err.code) {
331        return;
332      }
333    });
334  }
335}
336```
337
338```ts
339// 创建NodeController
340// common.ets
341import { UIContext } from '@kit.ArkUI';
342import { webview } from '@kit.ArkWeb';
343import { NodeController, BuilderNode, Size, FrameNode }  from '@kit.ArkUI';
344// @Builder中为动态组件的具体组件内容
345// Data为入参封装类
346class Data{
347  url: string = 'https://www.example.com';
348  controller: WebviewController = new webview.WebviewController();
349}
350// 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染
351let shouldInactive: boolean = true;
352@Builder
353function WebBuilder(data:Data) {
354  Column() {
355    Web({ src: data.url, controller: data.controller })
356      .onPageBegin(() => {
357        // 调用onActive,开启渲染
358        data.controller.onActive();
359      })
360      .onFirstMeaningfulPaint(() =>{
361        if (!shouldInactive) {
362          return;
363        }
364        // 在预渲染完成时触发,停止渲染
365        data.controller.onInactive();
366        shouldInactive = false;
367      })
368      .width("100%")
369      .height("100%")
370  }
371}
372let wrap = wrapBuilder<Data[]>(WebBuilder);
373// 用于控制和反馈对应的NodeContianer上的节点的行为,需要与NodeContainer一起使用
374export class myNodeController extends NodeController {
375  private rootnode: BuilderNode<Data[]> | null = null;
376  // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContianer中
377  // 在对应NodeContianer创建的时候调用、或者通过rebuild方法调用刷新
378  makeNode(uiContext: UIContext): FrameNode | null {
379    console.info(" uicontext is undifined : "+ (uiContext === undefined));
380    if (this.rootnode != null) {
381      // 返回FrameNode节点
382      return this.rootnode.getFrameNode();
383    }
384    // 返回null控制动态组件脱离绑定节点
385    return null;
386  }
387  // 当布局大小发生变化时进行回调
388  aboutToResize(size: Size) {
389    console.info("aboutToResize width : " + size.width  +  " height : " + size.height )
390  }
391  // 当controller对应的NodeContainer在Appear的时候进行回调
392  aboutToAppear() {
393    console.info("aboutToAppear")
394    // 切换到前台后,不需要停止渲染
395    shouldInactive = false;
396  }
397  // 当controller对应的NodeContainer在Disappear的时候进行回调
398  aboutToDisappear() {
399    console.info("aboutToDisappear")
400  }
401  // 此函数为自定义函数,可作为初始化函数使用
402  // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容
403  initWeb(url:string, uiContext:UIContext, control:WebviewController) {
404    if(this.rootnode != null)
405    {
406      return;
407    }
408    // 创建节点,需要uiContext
409    this.rootnode = new BuilderNode(uiContext)
410    // 创建动态Web组件
411    this.rootnode.build(wrap, { url:url, controller:control })
412  }
413}
414// 创建Map保存所需要的NodeController
415let NodeMap:Map<string, myNodeController | undefined> = new Map();
416// 创建Map保存所需要的WebViewController
417let controllerMap:Map<string, WebviewController | undefined> = new Map();
418// 初始化需要UIContext 需在Ability获取
419export const createNWeb = (url: string, uiContext: UIContext) => {
420  // 创建NodeController
421  let baseNode = new myNodeController();
422  let controller = new webview.WebviewController() ;
423  // 初始化自定义Web组件
424  baseNode.initWeb(url, uiContext, controller);
425  controllerMap.set(url, controller)
426  NodeMap.set(url, baseNode);
427}
428// 自定义获取NodeController接口
429export const getNWeb = (url : string) : myNodeController | undefined => {
430  return NodeMap.get(url);
431}
432```
433
434```ts
435// 使用NodeController的Page页
436// Index.ets
437import {createNWeb, getNWeb} from "./common"
438
439@Entry
440@Component
441struct Index {
442  build() {
443    Row() {
444      Column() {
445        // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode
446        // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示
447        NodeContainer(getNWeb("https://www.example.com"))
448          .height("90%")
449          .width("100%")
450      }
451      .width('100%')
452    }
453    .height('100%')
454  }
455}
456```
457
458## 常见白屏问题排查
459
4601.排查应用上网权限配置。
461
462检查是否已在module.json5中添加网络权限,添加方法请参考在[配置文件中声明权限](../security/AccessToken/declare-permissions.md)。
463
464```ts
465"requestPermissions":[
466    {
467      "name" : "ohos.permission.INTERNET"
468    }
469  ]
470```
471
4722.排查[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)与节点绑定的逻辑。
473
474检查节点是否已上组件树,建议在已有的Web组件上方加上Text(请参考以下例子),如果白屏的时候没有出现Text,建议检查[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)与节点绑定的情况。
475
476```ts
477@Builder
478function WebBuilder(data:Data) {
479  Column() {
480    Text('test')
481    Web({ src: data.url, controller: data.controller })
482      .width("100%")
483      .height("100%")
484  }
485}
486```
487
4883.排查Web可见性状态。
489
490如果整个节点已上树,可通过日志[WebPattern::OnVisibleAreaChange](../reference/apis-arkui/arkui-ts/ts-universal-component-visible-area-change-event.md#onvisibleareachange)查看Web组件可见性状态是否正确,不可见的Web组件可能会造成白屏。