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