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