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-domains。 85 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