• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![web-offline-mode](figures/web-offline-mode.png)
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组件可能会造成白屏。