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