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-domains。 91 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 ```