• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 加速Web页面的访问
2<!--Kit: ArkWeb-->
3<!--Subsystem: Web-->
4<!--Owner: @aohui-->
5<!--Designer: @yaomingliu-->
6<!--Tester: @ghiker-->
7<!--Adviser: @HelloCrease-->
8
9当Web页面加载缓慢时,可以使用预连接、预加载和预获取post请求的能力加速Web页面的访问。
10针对Web页面加载性能优化的详细内容请参考[Web页面加载优化性能指导](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization#section128761465256)
11
12## 预解析和预连接
13
14此方法可以针对域名级进行优化,通过[prepareForPageLoad()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#prepareforpageload10)来预解析或者预连接将要加载的页面。该方式仅对url进行DNS解析以及建立tcp连接,但不会获取主资源子资源。
15
16  在下面的示例中,在Web组件的onAppear中对要加载的页面进行预连接。
17
18```ts
19// xxx.ets
20import { webview } from '@kit.ArkWeb';
21
22@Entry
23@Component
24struct WebComponent {
25  webviewController: webview.WebviewController = new webview.WebviewController();
26
27  build() {
28    Column() {
29      Button('loadData')
30        .onClick(() => {
31          if (this.webviewController.accessBackward()) {
32            this.webviewController.backward();
33          }
34        })
35      Web({ src: 'https://www.example.com/', controller: this.webviewController })
36        .onAppear(() => {
37          // 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址进行dns预解析
38          // 第三个参数为要预连接socket的个数。最多允许6个。
39          webview.WebviewController.prepareForPageLoad('https://www.example.com/', true, 2);
40        })
41    }
42  }
43}
44```
45
46也可以通过[initializeWebEngine()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#initializewebengine)来提前初始化内核,然后在初始化内核后调用
47[prepareForPageLoad()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#prepareforpageload10)对即将要加载的页面进行预解析、预连接。这种方式适合提前对首页进行
48预解析、预连接。
49
50  在下面的示例中,Ability的onCreate中提前初始化Web内核并对首页进行预连接。
51
52```ts
53// xxx.ets
54import { webview } from '@kit.ArkWeb';
55import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
56
57export default class EntryAbility extends UIAbility {
58  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
59    console.log("EntryAbility onCreate");
60    webview.WebviewController.initializeWebEngine();
61    // 预连接时,需要将'https://www.example.com'替换成真实要访问的网站地址。
62    webview.WebviewController.prepareForPageLoad("https://www.example.com/", true, 2);
63    AppStorage.setOrCreate("abilityWant", want);
64    console.log("EntryAbility onCreate done");
65  }
66}
67```
68
69## 预加载
70
71此方法可针对资源级进行优化。如果能够预测到Web组件将要加载的页面或者即将要跳转的页面。可以通过[prefetchPage()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#prefetchpage10)来预加载即将要加载页面。
72
73预加载会提前下载页面所需的资源,包括主资源子资源,避免阻塞页面渲染。但不会执行网页JavaScript代码。预加载是WebviewController的实例方法,需要一个已经关联好Web组件的WebviewController实例。
74
75在下面的示例中,在onPageEnd的时候触发下一个要访问的页面的预加载。
76
77```ts
78// xxx.ets
79import { webview } from '@kit.ArkWeb';
80
81@Entry
82@Component
83struct WebComponent {
84  webviewController: webview.WebviewController = new webview.WebviewController();
85
86  build() {
87    Column() {
88      Web({ src: 'https://www.example.com/', controller: this.webviewController })
89        .onPageEnd(() => {
90          // 预加载https://www.iana.org/help/example-domains91          this.webviewController.prefetchPage('https://www.iana.org/help/example-domains');
92        })
93    }
94  }
95}
96```
97
98## 预获取post请求
99
100此方法可以针对请求级进行优化。可以通过[prefetchResource()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#prefetchresource12)预获取将要加载页面中的post请求。在页面加载结束时,可以通过[clearPrefetchedResource()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#clearprefetchedresource12)清除后续不再使用的预获取资源缓存。
101
102  以下示例,在Web组件onAppear中,对要加载页面中的post请求进行预获取。在onPageEnd中,可以清除预获取的post请求缓存。
103
104```ts
105// xxx.ets
106import { webview } from '@kit.ArkWeb';
107
108@Entry
109@Component
110struct WebComponent {
111  webviewController: webview.WebviewController = new webview.WebviewController();
112
113  build() {
114    Column() {
115      Web({ src: "https://www.example.com/", controller: this.webviewController })
116        .onAppear(() => {
117          // 预获取时,需要将"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。
118          webview.WebviewController.prefetchResource(
119            {
120              url: "https://www.example1.com/post?e=f&g=h",
121              method: "POST",
122              formData: "a=x&b=y",
123            },
124            [{
125              headerKey: "c",
126              headerValue: "z",
127            },],
128            "KeyX", 500);
129        })
130        .onPageEnd(() => {
131          // 清除后续不再使用的预获取资源缓存。
132          webview.WebviewController.clearPrefetchedResource(["KeyX",]);
133        })
134    }
135  }
136}
137```
138
139如果能够预测到Web组件将要加载页面或者即将要跳转页面中的post请求。可以通过[prefetchResource()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#prefetchresource12)预获取即将要加载页面的post请求。
140
141  以下示例,在onPageEnd中,触发预获取一个要访问页面的post请求。
142
143```ts
144// xxx.ets
145import { webview } from '@kit.ArkWeb';
146
147@Entry
148@Component
149struct WebComponent {
150  webviewController: webview.WebviewController = new webview.WebviewController();
151
152  build() {
153    Column() {
154      Web({ src: 'https://www.example.com/', controller: this.webviewController })
155        .onPageEnd(() => {
156          // 预获取时,需要将"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。
157          webview.WebviewController.prefetchResource(
158            {
159              url: "https://www.example1.com/post?e=f&g=h",
160              method: "POST",
161              formData: "a=x&b=y",
162            },
163            [{
164              headerKey: "c",
165              headerValue: "z",
166            },],
167            "KeyX", 500);
168        })
169    }
170  }
171}
172```
173
174也可以通过[initializeWebEngine()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#initializewebengine)提前初始化内核,然后在初始化内核后调用[prefetchResource()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#prefetchresource12)预获取将要加载页面中的post请求。这种方式适合提前预获取首页的post请求。
175
176  以下示例,在Ability的onCreate中,提前初始化Web内核并预获取首页的post请求。
177
178```ts
179// xxx.ets
180import { webview } from '@kit.ArkWeb';
181import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
182
183export default class EntryAbility extends UIAbility {
184  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
185    console.log("EntryAbility onCreate");
186    webview.WebviewController.initializeWebEngine();
187    // 预获取时,需要将"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。
188    webview.WebviewController.prefetchResource(
189      {
190        url: "https://www.example1.com/post?e=f&g=h",
191        method: "POST",
192        formData: "a=x&b=y",
193      },
194      [{
195        headerKey: "c",
196        headerValue: "z",
197      },],
198      "KeyX", 500);
199    AppStorage.setOrCreate("abilityWant", want);
200    console.log("EntryAbility onCreate done");
201  }
202}
203```
204
205## 预编译生成编译缓存
206
207可以通过[precompileJavaScript()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#precompilejavascript12)在页面加载前提前生成脚本文件的编译缓存。
208
209推荐配合动态组件使用,使用离线的Web组件用于生成字节码缓存,并在适当的时机加载业务用Web组件使用这些字节码缓存。下方是代码示例:
210
2111. 首先,在EntryAbility中将UIContext存到localStorage中。
212
213   ```ts
214   // EntryAbility.ets
215   import { UIAbility } from '@kit.AbilityKit';
216   import { window } from '@kit.ArkUI';
217
218   const localStorage: LocalStorage = new LocalStorage('uiContext');
219
220   export default class EntryAbility extends UIAbility {
221     storage: LocalStorage = localStorage;
222
223     onWindowStageCreate(windowStage: window.WindowStage) {
224       windowStage.loadContent('pages/Index', this.storage, (err, data) => {
225         if (err.code) {
226           return;
227         }
228
229         this.storage.setOrCreate<UIContext>("uiContext", windowStage.getMainWindowSync().getUIContext());
230       });
231     }
232   }
233   ```
234
2352. 编写动态组件所需基础代码。
236
237   ```ts
238   // DynamicComponent.ets
239   import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
240
241   export interface BuilderData {
242     url: string;
243     controller: WebviewController;
244     context: UIContext;
245   }
246
247   let storage : LocalStorage | undefined = undefined;
248
249   export class NodeControllerImpl extends NodeController {
250     private rootNode: BuilderNode<BuilderData[]> | null = null;
251     private wrappedBuilder: WrappedBuilder<BuilderData[]> | null = null;
252
253     constructor(wrappedBuilder: WrappedBuilder<BuilderData[]>, context: UIContext) {
254       storage = context.getSharedLocalStorage();
255       super();
256       this.wrappedBuilder = wrappedBuilder;
257     }
258
259     makeNode(): FrameNode | null {
260       if (this.rootNode != null) {
261         return this.rootNode.getFrameNode();
262       }
263       return null;
264     }
265
266     initWeb(url: string, controller: WebviewController) {
267       if(this.rootNode != null) {
268         return;
269       }
270
271       const uiContext: UIContext = storage!.get<UIContext>("uiContext") as UIContext;
272       if (!uiContext) {
273         return;
274       }
275       this.rootNode = new BuilderNode(uiContext);
276       this.rootNode.build(this.wrappedBuilder, { url: url, controller: controller });
277     }
278   }
279
280   export const createNode = (wrappedBuilder: WrappedBuilder<BuilderData[]>, data: BuilderData) => {
281     const baseNode = new NodeControllerImpl(wrappedBuilder, data.context);
282     baseNode.initWeb(data.url, data.controller);
283     return baseNode;
284   }
285   ```
286
2873. 编写用于生成字节码缓存的组件,本例中的本地Javascript资源内容通过文件读取接口读取rawfile目录下的本地文件。
288
289   ```ts
290   // PrecompileWebview.ets
291   import { BuilderData } from "./DynamicComponent";
292   import { Config, configs } from "./PrecompileConfig";
293
294   @Builder
295   function WebBuilder(data: BuilderData) {
296     Web({ src: data.url, controller: data.controller })
297       .onControllerAttached(() => {
298         precompile(data.controller, configs, data.context);
299       })
300       .fileAccess(true)
301   }
302
303   export const precompileWebview = wrapBuilder<BuilderData[]>(WebBuilder);
304
305   export const precompile = async (controller: WebviewController, configs: Array<Config>, context: UIContext) => {
306     for (const config of configs) {
307       let content = await readRawFile(config.localPath, context);
308
309       try {
310         controller.precompileJavaScript(config.url, content, config.options)
311           .then(errCode => {
312             console.error("precompile successfully! " + errCode);
313           }).catch((errCode: number) => {
314             console.error("precompile failed. " + errCode);
315         });
316       } catch (err) {
317         console.error("precompile failed. " + err.code + " " + err.message);
318       }
319     }
320   }
321
322   async function readRawFile(path: string, context: UIContext) {
323     try {
324       return await context.getHostContext()!.resourceManager.getRawFileContent(path);
325     } catch (err) {
326       return new Uint8Array(0);
327     }
328   }
329   ```
330
331JavaScript资源的获取方式也可通过[网络请求](../reference/apis-network-kit/js-apis-http.md)的方式获取,但此方法获取到的http响应头非标准HTTP响应头格式,需额外将响应头转换成标准HTTP响应头格式后使用。如通过网络请求获取到的响应头是e-tag,则需要将其转换成E-Tag后使用。
332
3334. 编写业务用组件代码。
334
335   ```ts
336   // BusinessWebview.ets
337   import { BuilderData } from "./DynamicComponent";
338
339   @Builder
340   function WebBuilder(data: BuilderData) {
341     // 此处组件可根据业务需要自行扩展
342     Web({ src: data.url, controller: data.controller })
343       .cacheMode(CacheMode.Default)
344   }
345
346   export const businessWebview = wrapBuilder<BuilderData[]>(WebBuilder);
347   ```
348
3495. 编写资源配置信息。
350
351   ```ts
352   // PrecompileConfig.ets
353   import { webview } from '@kit.ArkWeb'
354
355   export interface Config {
356     url:  string,
357     localPath: string, // 本地资源路径
358     options: webview.CacheOptions
359   }
360
361   export let configs: Array<Config> = [
362     {
363       url: "https://www.example.com/example.js",
364       localPath: "example.js",
365       options: {
366         responseHeaders: [
367           { headerKey: "E-Tag", headerValue: "aWO42N9P9dG/5xqYQCxsx+vDOoU="},
368           { headerKey: "Last-Modified", headerValue: "Wed, 21 Mar 2024 10:38:41 GMT"}
369         ]
370       }
371     }
372   ]
373   ```
374
3756. 在页面中使用。
376
377   ```ts
378   // Index.ets
379   import { webview } from '@kit.ArkWeb';
380   import { NodeController } from '@kit.ArkUI';
381   import { createNode } from "./DynamicComponent"
382   import { precompileWebview } from "./PrecompileWebview"
383   import { businessWebview } from "./BusinessWebview"
384
385   @Entry
386   @Component
387   struct Index {
388     @State precompileNode: NodeController | undefined = undefined;
389     precompileController: webview.WebviewController = new webview.WebviewController();
390
391     @State businessNode: NodeController | undefined = undefined;
392     businessController: webview.WebviewController = new webview.WebviewController();
393
394     aboutToAppear(): void {
395       // 初始化用于注入本地资源的Web组件
396       this.precompileNode = createNode(precompileWebview,
397         { url: "https://www.example.com/empty.html", controller: this.precompileController, context: this.getUIContext()});
398     }
399
400     build() {
401       Column() {
402         // 在适当的时机加载业务用Web组件,本例以Button点击触发为例
403         Button("加载页面")
404           .onClick(() => {
405             this.businessNode = createNode(businessWebview, {
406               url:  "https://www.example.com/business.html",
407               controller: this.businessController,
408               context: this.getUIContext()
409             });
410           })
411         // 用于业务的Web组件
412         NodeContainer(this.businessNode);
413       }
414     }
415   }
416   ```
417
418当需要更新本地已经生成的编译字节码时,修改cacheOptions参数中responseHeaders中的E-Tag或Last-Modified响应头对应的值,再次调用接口即可。
419
420## 离线资源免拦截注入
421可以通过[injectOfflineResources()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#injectofflineresources12)在页面加载前提前将图片、样式表或脚本资源注入到应用的内存缓存中。
422
423推荐配合动态组件使用,使用离线的Web组件用于将资源注入到内核的内存缓存中,并在适当的时机加载业务用Web组件使用这些资源。下方是代码示例:
424
4251. 首先,在EntryAbility中将UIContext存到localStorage中。
426
427   ```ts
428   // EntryAbility.ets
429   import { UIAbility } from '@kit.AbilityKit';
430   import { window } from '@kit.ArkUI';
431
432   const localStorage: LocalStorage = new LocalStorage('uiContext');
433
434   export default class EntryAbility extends UIAbility {
435     storage: LocalStorage = localStorage;
436
437     onWindowStageCreate(windowStage: window.WindowStage) {
438       windowStage.loadContent('pages/Index', this.storage, (err, data) => {
439         if (err.code) {
440           return;
441         }
442
443         this.storage.setOrCreate<UIContext>("uiContext", windowStage.getMainWindowSync().getUIContext());
444       });
445     }
446   }
447   ```
448
4492. 编写动态组件所需基础代码。
450
451   ```ts
452   // DynamicComponent.ets
453   import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
454
455   export interface BuilderData {
456     url: string;
457     controller: WebviewController;
458     context: UIContext;
459   }
460
461   let storage : LocalStorage | undefined = undefined;
462
463   export class NodeControllerImpl extends NodeController {
464     private rootNode: BuilderNode<BuilderData[]> | null = null;
465     private wrappedBuilder: WrappedBuilder<BuilderData[]> | null = null;
466
467     constructor(wrappedBuilder: WrappedBuilder<BuilderData[]>,  context: UIContext) {
468      storage = context.getSharedLocalStorage();
469       super();
470       this.wrappedBuilder = wrappedBuilder;
471     }
472
473     makeNode(): FrameNode | null {
474       if (this.rootNode != null) {
475         return this.rootNode.getFrameNode();
476       }
477       return null;
478     }
479
480     initWeb(url: string, controller: WebviewController) {
481       if(this.rootNode != null) {
482         return;
483       }
484
485       const uiContext: UIContext = storage!.get<UIContext>("uiContext") as UIContext;
486       if (!uiContext) {
487         return;
488       }
489       this.rootNode = new BuilderNode(uiContext);
490       this.rootNode.build(this.wrappedBuilder, { url: url, controller: controller });
491     }
492   }
493
494   export const createNode = (wrappedBuilder: WrappedBuilder<BuilderData[]>, data: BuilderData) => {
495     const baseNode = new NodeControllerImpl(wrappedBuilder, data.context);
496     baseNode.initWeb(data.url, data.controller);
497     return baseNode;
498   }
499   ```
500
5013. 编写用于注入资源的组件代码,本例中的本地资源内容通过文件读取接口读取rawfile目录下的本地文件。
502
503   <!--code_no_check-->
504   ```ts
505   // InjectWebview.ets
506   import { webview } from '@kit.ArkWeb';
507   import { resourceConfigs } from "./Resource";
508   import { BuilderData } from "./DynamicComponent";
509
510   @Builder
511   function WebBuilder(data: BuilderData) {
512     Web({ src: data.url, controller: data.controller })
513       .onControllerAttached(async () => {
514         try {
515           data.controller.injectOfflineResources(await getData (data.context));
516         } catch (err) {
517           console.error("error: " + err.code + " " + err.message);
518         }
519       })
520       .fileAccess(true)
521   }
522
523   export const injectWebview = wrapBuilder<BuilderData[]>(WebBuilder);
524
525   export async function getData(context: UIContext) {
526     const resourceMapArr: Array<webview.OfflineResourceMap> = [];
527
528     // 读取配置,从rawfile目录中读取文件内容
529     for (let config of resourceConfigs) {
530       let buf: Uint8Array = new Uint8Array(0);
531       if (config.localPath) {
532         buf = await readRawFile(config.localPath, context);
533       }
534
535       resourceMapArr.push({
536         urlList: config.urlList,
537         resource: buf,
538         responseHeaders: config.responseHeaders,
539         type: config.type,
540       })
541     }
542
543     return resourceMapArr;
544   }
545
546   export async function readRawFile(url: string, context: UIContext) {
547     try {
548       return await context.getHostContext()!.resourceManager.getRawFileContent(url);
549     } catch (err) {
550       return new Uint8Array(0);
551     }
552   }
553   ```
554
5554. 编写业务用组件代码。
556
557   <!--code_no_check-->
558   ```ts
559   // BusinessWebview.ets
560   import { BuilderData } from "./DynamicComponent";
561
562   @Builder
563   function WebBuilder(data: BuilderData) {
564     // 此处组件可根据业务需要自行扩展
565     Web({ src: data.url, controller: data.controller })
566       .cacheMode(CacheMode.Default)
567   }
568
569   export const businessWebview = wrapBuilder<BuilderData[]>(WebBuilder);
570   ```
571
5725. 编写资源配置信息。
573
574   ```ts
575   // Resource.ets
576   import { webview } from '@kit.ArkWeb';
577
578   export interface ResourceConfig {
579     urlList: Array<string>,
580     type: webview.OfflineResourceType,
581     responseHeaders: Array<Header>,
582     localPath: string, // 本地资源存放在rawfile目录下的路径
583   }
584
585   export const resourceConfigs: Array<ResourceConfig> = [
586     {
587       localPath: "example.png",
588       urlList: [
589         "https://www.example.com/",
590         "https://www.example.com/path1/example.png",
591         "https://www.example.com/path2/example.png",
592       ],
593       type: webview.OfflineResourceType.IMAGE,
594       responseHeaders: [
595         { headerKey: "Cache-Control", headerValue: "max-age=1000" },
596         { headerKey: "Content-Type", headerValue: "image/png" },
597       ]
598     },
599     {
600       localPath: "example.js",
601       urlList: [ // 仅提供一个url,这个url既作为资源的源,也作为资源的网络请求地址
602         "https://www.example.com/example.js",
603       ],
604       type: webview.OfflineResourceType.CLASSIC_JS,
605       responseHeaders: [
606         // 以<script crossorigin="anonymous" />方式使用,提供额外的响应头
607         { headerKey: "Cross-Origin", headerValue:"anonymous" }
608       ]
609     },
610   ];
611   ```
612
6136. 在页面中使用。
614   ```ts
615   // Index.ets
616   import { webview } from '@kit.ArkWeb';
617   import { NodeController } from '@kit.ArkUI';
618   import { createNode } from "./DynamicComponent"
619   import { injectWebview } from "./InjectWebview"
620   import { businessWebview } from "./BusinessWebview"
621
622   @Entry
623   @Component
624   struct Index {
625     @State injectNode: NodeController | undefined = undefined;
626     injectController: webview.WebviewController = new webview.WebviewController();
627
628     @State businessNode: NodeController | undefined = undefined;
629     businessController: webview.WebviewController = new webview.WebviewController();
630
631     aboutToAppear(): void {
632       // 初始化用于注入本地资源的Web组件, 提供一个空的html页面作为url即可
633       this.injectNode = createNode(injectWebview,
634           { url: "https://www.example.com/empty.html", controller: this.injectController, context: this.getUIContext()});
635     }
636
637     build() {
638       Column() {
639         // 在适当的时机加载业务用Web组件,本例以Button点击触发为例
640         Button("加载页面")
641           .onClick(() => {
642             this.businessNode = createNode(businessWebview, {
643               url: "https://www.example.com/business.html",
644               controller: this.businessController,
645               context: this.getUIContext()
646             });
647           })
648         // 用于业务的Web组件
649         NodeContainer(this.businessNode);
650       }
651     }
652   }
653   ```
654
6557. 加载的HTML网页示例。
656
657   ```HTML
658   <!DOCTYPE html>
659   <html lang="en">
660   <head></head>
661   <body>
662     <img src="https://www.example.com/path1/request.png" />
663     <img src="https://www.example.com/path2/request.png" />
664     <script src="https://www.example.com/example.js" crossorigin="anonymous"></script>
665   </body>
666   </html>
667   ```