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