使用Web组件加载页面
页面加载是Web组件的基本功能。根据页面加载数据来源可以分为三种常用场景,包括加载网络页面、加载本地页面、加载HTML格式的富文本数据。
页面加载过程中,若涉及网络资源获取,需要配置ohos.permission.INTERNET网络访问权限。
加载网络页面
开发者可以在Web组件创建时,指定默认加载的网络页面 。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用loadUrl()接口加载指定的网页。Web组件的第一个参数变量src不能通过状态变量(例如:@State)动态更改地址,如需更改,请通过loadUrl()重新加载。
在下面的示例中,在Web组件加载完“www.example.com”页面后,开发者可通过loadUrl接口将此Web组件显示页面变更为“www.example1.com”。
// xxx.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct WebComponent {
controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
try {
// 点击按钮时,通过loadUrl,跳转到www.example1.com
this.controller.loadUrl('www.example1.com');
} catch (error) {
console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.controller })
}
}
}
加载本地页面
将本地页面文件放在应用的rawfile目录下,开发者可以在Web组件创建的时候指定默认加载的本地页面 ,并且加载完成后可通过调用loadUrl()接口变更当前Web组件的页面。
在下面的示例中展示加载本地页面文件的方法:
- 将资源文件放置在应用的resources/rawfile目录下。图1 资源文件路径
应用侧代码。
// xxx.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct WebComponent {
controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
try {
// 点击按钮时,通过loadUrl,跳转到local1.html
this.controller.loadUrl($rawfile("local1.html"));
} catch (error) {
console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
}
})
// 组件创建时,通过$rawfile加载本地文件local.html
Web({ src: $rawfile("local.html"), controller: this.controller })
}
}
}
local.html页面代码。
<!-- local.html -->
<!DOCTYPE html>
<html>
<body>
<p>Hello World</p>
</body>
</html>
local1.html页面代码。
<!-- local1.html -->
<!DOCTYPE html>
<html>
<body>
<p>This is local1 page</p>
</body>
</html>
加载HTML格式的文本数据
Web组件可以通过loadData()接口实现加载HTML格式的文本数据。当开发者不需要加载整个页面,只需要显示一些页面片段时,可通过此功能来快速加载页面。
// xxx.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct WebComponent {
controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadData')
.onClick(() => {
try {
// 点击按钮时,通过loadData,加载HTML格式的文本数据
this.controller.loadData(
"<html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>",
"text/html",
"UTF-8"
);
} catch (error) {
console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.controller })
}
}
}
动态创建Web组件
支持命令式创建Web组件,这种方式创建的组件不会立即挂载到组件树,即不会对用户呈现(组件状态为Hidden和InActive),开发者可以在后续使用中按需动态挂载。后台启动的Web实例不建议超过200个。
// 载体Ability
// EntryAbility.ets
import { createNWeb } from "../pages/common"
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
// 创建Web动态组件(需传入UIContext),loadContent之后的任意时机均可创建
createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
if (err.code) {
return;
}
});
}
// 创建NodeController
// common.ets
import { UIContext, NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';
// @Builder中为动态组件的具体组件内容
// Data为入参封装类
class Data{
url: string = "https://www.example.com";
controller: WebviewController = new webview.WebviewController();
}
@Builder
function WebBuilder(data:Data) {
Column() {
Web({ src: data.url, controller: data.controller })
.width("100%")
.height("100%")
}
}
let wrap = wrapBuilder<Data[]>(WebBuilder);
// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
export class myNodeController extends NodeController {
private rootnode: BuilderNode<Data[]> | null = null;
// 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中
// 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新
makeNode(uiContext: UIContext): FrameNode | null {
console.log(" uicontext is undefined : "+ (uiContext === undefined));
if (this.rootnode != null) {
// 返回FrameNode节点
return this.rootnode.getFrameNode();
}
// 返回null控制动态组件脱离绑定节点
return null;
}
// 当布局大小发生变化时进行回调
aboutToResize(size: Size) {
console.log("aboutToResize width : " + size.width + " height : " + size.height );
}
// 当controller对应的NodeContainer在Appear的时候进行回调
aboutToAppear() {
console.log("aboutToAppear");
}
// 当controller对应的NodeContainer在Disappear的时候进行回调
aboutToDisappear() {
console.log("aboutToDisappear");
}
// 此函数为自定义函数,可作为初始化函数使用
// 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容
initWeb(url:string, uiContext:UIContext, control:WebviewController) {
if(this.rootnode != null)
{
return;
}
// 创建节点,需要uiContext
this.rootnode = new BuilderNode(uiContext);
// 创建动态Web组件
this.rootnode.build(wrap, { url:url, controller:control });
}
}
// 创建Map保存所需要的NodeController
let NodeMap:Map<string, myNodeController | undefined> = new Map();
// 创建Map保存所需要的WebViewController
let controllerMap:Map<string, WebviewController | undefined> = new Map();
// 初始化需要UIContext 需在Ability获取
export const createNWeb = (url: string, uiContext: UIContext) => {
// 创建NodeController
let baseNode = new myNodeController();
let controller = new webview.WebviewController() ;
// 初始化自定义Web组件
baseNode.initWeb(url, uiContext, controller);
controllerMap.set(url, controller)
NodeMap.set(url, baseNode);
}
// 自定义获取NodeController接口
export const getNWeb = (url : string) : myNodeController | undefined => {
return NodeMap.get(url);
}
// 使用NodeController的Page页
// Index.ets
import { getNWeb } from "./common"
@Entry
@Component
struct Index {
build() {
Row() {
Column() {
// NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode
// Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示
NodeContainer(getNWeb("https://www.example.com"))
.height("90%")
.width("100%")
}
.width('100%')
}
.height('100%')
}
}
管理页面跳转及浏览记录导航
历史记录导航
在前端页面点击网页中的链接时,Web组件默认会自动打开并加载目标网址。当前端页面替换为新的加载链接时,会自动记录已经访问的网页地址。可以通过forward()和backward()接口向前/向后浏览上一个/下一个历史记录。
页面加载过程中,若涉及网络资源获取,需要配置ohos.permission.INTERNET网络访问权限。
在下面的示例中,点击应用的按钮来触发前端页面的后退操作。
// xxx.ets
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadData')
.onClick(() => {
if (this.webviewController.accessBackward()) {
this.webviewController.backward();
}
})
Web({ src: 'https://www.example.com/cn/', controller: this.webviewController })
}
}
}
如果存在历史记录,accessBackward()接口会返回true。同样,您可以使用accessForward()接口检查是否存在前进的历史记录。如果您不执行检查,那么当用户浏览到历史记录的末尾时,调用forward()和backward()接口时将不执行任何操作。
页面跳转
当点击网页中的链接需要跳转到应用内其他页面时,可以通过使用Web组件的onLoadIntercept()接口来实现。
在下面的示例中,应用首页Index.ets加载前端页面route.html,在前端route.html页面点击超链接,可跳转到应用的ProfilePage.ets页面。
- 应用首页Index.ets页面代码。
// index.etsimport { webview } from '@kit.ArkWeb';import { router } from '@kit.ArkUI';@Entry@Componentstruct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); build() { Column() { // 资源文件route.html存放路径src/main/resources/rawfile Web({ src: $rawfile('route.html'), controller: this.webviewController }) .onLoadIntercept((event) => { if (event) { let url: string = event.data.getRequestUrl(); if (url.indexOf('native://') === 0) { // 跳转其他界面 router.pushUrl({ url: url.substring(9) }); return true; } } return false; }) } }}
route.html前端页面代码。<!-- route.html --><!DOCTYPE html><html><body> <div> <a href="native://pages/ProfilePage">个人中心</a> </div></body></html>
跳转页面ProfilePage.ets代码。@Entry@Componentstruct ProfilePage { @State message: string = 'Hello World'; build() { Column() { Text(this.message) .fontSize(20) } }}
### 跨应用跳转Web组件可以实现点击前端页面超链接跳转到其他应用。在下面的示例中,点击call.html前端页面中的超链接,跳转到电话应用的拨号界面。 - 应用侧代码。
// xxx.etsimport { webview } from '@kit.ArkWeb';import { call } from '@kit.TelephonyKit';@Entry@Componentstruct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); build() { Column() { Web({ src: $rawfile('call.html'), controller: this.webviewController }) .onLoadIntercept((event) => { if (event) { let url: string = event.data.getRequestUrl(); // 判断链接是否为拨号链接 if (url.indexOf('tel://') === 0) { // 跳转拨号界面 call.makeCall(url.substring(6), (err) => { if (!err) { console.info('make call succeeded.'); } else { console.info('make call fail, err is:' + JSON.stringify(err)); } }); return true; } } return false; }) } }}
前端页面call.html代码。<!-- call.html --><!DOCTYPE html><html><body> <div> <a href="tel://xxx xxxx xxx">拨打电话</a> </div></body></html>
## 拦截Web组件发起的网络请求 通过网络拦截接口对Web组件发出的请求进行拦截,并可以为被拦截的请求提供自定义的响应头以及响应体。### 为Web组件设置网络拦截器为指定的Web组件或者ServiceWorker设置ArkWeb_SchemeHandler,当Web内核发出相应scheme请求的时候,会触发ArkWeb_SchemeHandler的回调。需要在Web组件初始化之后设置网络拦截器。当请求开始的时候会回调ArkWeb_OnRequestStart,请求结束的时候会回调ArkWeb_OnRequestStop。如果想要拦截Web组件发出的第一个请求,可以通过initializeWebEngine对Web组件提前进行初始化,然后设置拦截器进行拦截。// 创建一个ArkWeb_SchemeHandler对象。 ArkWeb_SchemeHandler *schemeHandler; OH_ArkWeb_CreateSchemeHandler(&schemeHandler); // 为ArkWeb_SchemeHandler设置ArkWeb_OnRequestStart与ArkWeb_OnRequestStop回调。 OH_ArkWebSchemeHandler_SetOnRequestStart(schemeHandler, OnURLRequestStart); OH_ArkWebSchemeHandler_SetOnRequestStop(schemeHandler, OnURLRequestStop); // 拦截webTag为“scheme-handler”的Web组件发出的scheme为“https”的请求。 OH_ArkWeb_SetSchemeHandler("https", "scheme-handler", schemeHandler); OH_ArkWebServiceWorker_SetSchemeHandler("https", schemeHandler);
也可以拦截非Web组件内置scheme的请求。// 创建一个ArkWeb_SchemeHandler对象。 ArkWeb_SchemeHandler *schemeHandler; OH_ArkWeb_CreateSchemeHandler(&schemeHandler); // 为ArkWeb_SchemeHandler设置ArkWeb_OnRequestStart与ArkWeb_OnRequestStop回调。 OH_ArkWebSchemeHandler_SetOnRequestStart(schemeHandler, OnURLRequestStart); OH_ArkWebSchemeHandler_SetOnRequestStop(schemeHandler, OnURLRequestStop); // 拦截webTag为“scheme-handler”的Web组件发出的scheme为“custom”的请求。 OH_ArkWeb_SetSchemeHandler("custom", "scheme-handler", schemeHandler); OH_ArkWebServiceWorker_SetSchemeHandler("custom", schemeHandler);
### 设置自定义scheme需要遵循的规则如果要拦截自定义scheme的请求,需要提前将自定义scheme注册到Web内核。需要在Web组件初始化之前进行注册,Web组件初始化后再注册会失败。// 注册“custom“ scheme到Web组件,并指定该scheme需要遵循标准的scheme规则,允许该scheme发出跨域请求。 OH_ArkWeb_RegisterCustomSchemes("custom", ARKWEB_SCHEME_OPTION_STANDARD | ARKWEB_SCHEME_OPTION_CORS_ENABLED); // 注册“custom-local” scheme到Web组件,并指定该scheme需要遵循与“file” scheme一样的规则。 OH_ArkWeb_RegisterCustomSchemes("custom-local", ARKWEB_SCHEME_OPTION_LOCAL); // 注册“custom-csp-bypassing”到Web组件,并指定该scheme需要遵循标准的scheme规则,允许忽略CSP检查。 OH_ArkWeb_RegisterCustomSchemes("custom-csp-bypassing", ARKWEB_SCHEME_OPTION_CSP_BYPASSING | ARKWEB_SCHEME_OPTION_STANDARD); // 注册“custom-isolated”到Web组件,并指定该scheme的请求必须从相同scheme加载的网页中发起。 OH_ArkWeb_RegisterCustomSchemes("custom-isolated", ARKWEB_SCHEME_OPTION_DISPLAY_ISOLATED);
由于注册scheme需要在Web组件初始化之前进行注册,而网络拦截器需要在Web组件初始化之后设置,建议在EntryAbility的onCreate中调用c++接口注册scheme。scheme注册完毕后,通过initializeWebEngine对Web组件进行初始化,初始化完成后再设置网络拦截器。export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 注册scheme的配置。 testNapi.registerCustomSchemes(); // 初始化Web组件内核,该操作会初始化Browser进程以及创建BrowserContext。 webview.WebviewController.initializeWebEngine(); // 创建并设置ArkWeb_SchemeHandler。 testNapi.setSchemeHandler(); } ... };
### 获取被拦截请求的请求信息通过OH_ArkWebResourceRequest_*接口获取被拦截请求的信息。可以获取url、method、referrer、headers、resourceType等信息。char* url; OH_ArkWebResourceRequest_GetUrl(resourceRequest_, &url); OH_ArkWeb_ReleaseString(url); char* method; OH_ArkWebResourceRequest_GetMethod(resourceRequest_, &method); OH_ArkWeb_ReleaseString(method); int32_t resourceType = OH_ArkWebResourceRequest_GetResourceType(resourceRequest_); char* frameUrl; OH_ArkWebResourceRequest_GetFrameUrl(resourceRequest_, &frameUrl); OH_ArkWeb_ReleaseString(frameUrl); ...
支持获取PUT/POST类请求的上传数据。数据类型支持BYTES、FILE、BLOB和CHUNKED。// 获取被拦截请求的上传数据。 OH_ArkWebResourceRequest_GetHttpBodyStream(resourceRequest(), &stream_); // 设置读取上传数据的读回调。 OH_ArkWebHttpBodyStream_SetReadCallback(stream_, ReadCallback); // 初始化ArkWeb_HttpBodyStream,其它OH_ArkWebHttpBodyStream*函数需要在初始化进行调用。 OH_ArkWebHttpBodyStream_Init(stream_, InitCallback);
### 为被拦截的请求提供自定义的响应体Web组件的网络拦截支持在worker线程以流的方式为被拦截的请求提供自定义的响应体。也可以以特定的网络错误码结束当前被拦截的请求。// 为被拦截的请求创建一个响应头。 ArkWeb_Response *response; OH_ArkWeb_CreateResponse(&response); // 设置HTTP状态码为200。 OH_ArkWebResponse_SetStatus(response, 200); // 设置响应体的编码格式。 OH_ArkWebResponse_SetCharset(response, "UTF-8"); // 设置响应体的大小。 OH_ArkWebResponse_SetHeaderByName(response, "content-length", "1024", false); // 将为被拦截的请求创建的响应头传递给Web组件。 OH_ArkWebResourceHandler_DidReceiveResponse(resourceHandler, response); // 该函数可以调用多次,数据可以分多份来传递给Web组件。 OH_ArkWebResourceHandler_DidReceiveData(resourceHandler, buffer, bufLen); // 读取响应体结束,当然如果希望该请求失败的话也可以通过调用OH_ArkWebResourceHandler_DidFailWithError(resourceHandler_, errorCode); // 传递给Web组件一个错误码并结束该请求。 OH_ArkWebResourceHandler_DidFinish(resourceHandler);
### 完整示例使用DevEco Studio创建一个默认的Native C++工程,需要提前准备一个mp4文件,命名为test.mp4,将test.mp4放到main/resources/rawfile下。main/ets/pages/index.etsimport testNapi from 'libentry.so';import { webview } from '@kit.ArkWeb';import { resourceManager } from '@kit.LocalizationKit';@Entry@Componentstruct Index { mycontroller: webview.WebviewController = new webview.WebviewController("scheme-handler"); build() { Row() { Column() { Button("goback").onClick( event => { this.mycontroller.backward(); }) Web({ src: $rawfile("test.html"), controller: this.mycontroller}) .javaScriptAccess(true) .width('100%') .height('100%') .databaseAccess(true) .fileAccess(false) .domStorageAccess(true) .cacheMode(CacheMode.Default) .onPageBegin( event => { testNapi.initResourceManager(getContext().resourceManager); }) } .width('100%') } .height('100%') }}
main/ets/entryability/EntryAbility.etsimport { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';import { hilog } from '@kit.PerformanceAnalysisKit';import { window } from '@kit.ArkUI';import testNapi from 'libentry.so';import { webview } from '@kit.ArkWeb';export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 注册三方协议的配置。 testNapi.registerCustomSchemes(); // 初始化Web组件内核,该操作会初始化Browser进程以及创建BrowserContext。 webview.WebviewController.initializeWebEngine(); // 设置SchemeHandler。 testNapi.setSchemeHandler(); } onDestroy(): void { } onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (err, data) => { if (err.code) { return; } }); } onWindowStageDestroy(): void { } onForeground(): void { } onBackground(): void { }};
main/cpp/hello.cpp#include "hilog/log.h"#include "napi/native_api.h"#include "rawfile_request.h"#include "rawfile/raw_file_manager.h"#include "web/arkweb_scheme_handler.h"#include "web/arkweb_net_error_list.h"#undef LOG_TAG#define LOG_TAG "ss-handler"ArkWeb_SchemeHandler *g_schemeHandler;ArkWeb_SchemeHandler *g_schemeHandlerForSW;NativeResourceManager *g_resourceManager;// 注册三方协议的配置,需要在Web内核初始化之前调用,否则会注册失败。static napi_value RegisterCustomSchemes(napi_env env, napi_callback_info info){ OH_LOG_INFO(LOG_APP, "register custom schemes"); OH_ArkWeb_RegisterCustomSchemes("custom", ARKWEB_SCHEME_OPTION_STANDARD | ARKWEB_SCHEME_OPTION_CORS_ENABLED); OH_ArkWeb_RegisterCustomSchemes("custom-local", ARKWEB_SCHEME_OPTION_LOCAL); OH_ArkWeb_RegisterCustomSchemes("custom-csp-bypassing", ARKWEB_SCHEME_OPTION_CSP_BYPASSING | ARKWEB_SCHEME_OPTION_STANDARD); OH_ArkWeb_RegisterCustomSchemes("custom-isolated", ARKWEB_SCHEME_OPTION_DISPLAY_ISOLATED); return nullptr;}// 请求开始的回调,在该函数中我们创建一个RawfileRequest来实现对Web内核请求的拦截。void OnURLRequestStart(const ArkWeb_SchemeHandler *schemeHandler, ArkWeb_ResourceRequest *resourceRequest, const ArkWeb_ResourceHandler *resourceHandler, bool *intercept){ *intercept = true; RawfileRequest* request = new RawfileRequest(resourceRequest, resourceHandler, g_resourceManager); OH_ArkWebResourceRequest_SetUserData(resourceRequest, request); request->Start();}// 请求结束的回调,在该函数中我们需要标记RawfileRequest已经结束了,内部不应该再使用ResourceHandler。void OnURLRequestStop(const ArkWeb_SchemeHandler *schemeHandler, const ArkWeb_ResourceRequest *request){ if (!request) { OH_LOG_ERROR(LOG_APP, "on request stop request is nullptr."); return; } RawfileRequest *rawfileRequest = (RawfileRequest *)OH_ArkWebResourceRequest_GetUserData(request); if (rawfileRequest) { rawfileRequest->Stop(); }}void OnURLRequestStartForSW(const ArkWeb_SchemeHandler *schemeHandler, ArkWeb_ResourceRequest *resourceRequest, const ArkWeb_ResourceHandler *resourceHandler, bool *intercept){ *intercept = true; RawfileRequest* request = new RawfileRequest(resourceRequest, resourceHandler, g_resourceManager); OH_ArkWebResourceRequest_SetUserData(resourceRequest, request); request->Start();}void OnURLRequestStopForSW(const ArkWeb_SchemeHandler *schemeHandler, const ArkWeb_ResourceRequest *request){ if (!request) { OH_LOG_ERROR(LOG_APP, "on request stop request is nullptr."); return; } RawfileRequest *rawfileRequest = (RawfileRequest *)OH_ArkWebResourceRequest_GetUserData(request); if (rawfileRequest) { rawfileRequest->Stop(); }}// 设置SchemeHandler。static napi_value SetSchemeHandler(napi_env env, napi_callback_info info){ OH_LOG_INFO(LOG_APP, "set scheme handler"); OH_ArkWeb_CreateSchemeHandler(&g_schemeHandler); OH_ArkWeb_CreateSchemeHandler(&g_schemeHandlerForSW); OH_ArkWebSchemeHandler_SetOnRequestStart(g_schemeHandler, OnURLRequestStart); OH_ArkWebSchemeHandler_SetOnRequestStop(g_schemeHandler, OnURLRequestStop); OH_ArkWebSchemeHandler_SetOnRequestStart(g_schemeHandlerForSW, OnURLRequestStart); OH_ArkWebSchemeHandler_SetOnRequestStop(g_schemeHandlerForSW, OnURLRequestStop); OH_ArkWeb_SetSchemeHandler("custom", "scheme-handler", g_schemeHandler); OH_ArkWeb_SetSchemeHandler("custom-csp-bypassing", "scheme-handler", g_schemeHandler); OH_ArkWeb_SetSchemeHandler("custom-isolated", "scheme-handler", g_schemeHandler); OH_ArkWeb_SetSchemeHandler("custom-local", "scheme-handler", g_schemeHandler); OH_ArkWeb_SetSchemeHandler("https", "scheme-handler", g_schemeHandler); OH_ArkWeb_SetSchemeHandler("http", "scheme-handler", g_schemeHandler); OH_ArkWebServiceWorker_SetSchemeHandler("https", g_schemeHandlerForSW); return nullptr;}static napi_value InitResourceManager(napi_env env, napi_callback_info info){ size_t argc = 2; napi_value argv[2] = {nullptr}; napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); g_resourceManager = OH_ResourceManager_InitNativeResourceManager(env, argv[0]); return nullptr;}EXTERN_C_STARTstatic napi_value Init(napi_env env, napi_value exports){ napi_property_descriptor desc[] = { {"setSchemeHandler", nullptr, SetSchemeHandler, nullptr, nullptr, nullptr, napi_default, nullptr}, {"initResourceManager", nullptr, InitResourceManager, nullptr, nullptr, nullptr, napi_default, nullptr}, {"registerCustomSchemes", nullptr, RegisterCustomSchemes, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports;}EXTERN_C_ENDstatic napi_module demoModule = { .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, .nm_modname = "entry", .nm_priv = ((void*)0), .reserved = { 0 },};extern "C" __attribute__((constructor)) void RegisterEntryModule(void){ napi_module_register(&demoModule);}
main/cpp/CMakeLists.txt# the minimum version of CMake.cmake_minimum_required(VERSION 3.4.1)project(schemehandler)set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})if(DEFINED PACKAGE_INFO_FILE) include(${PACKAGE_INFO_FILE})endif()include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/include)add_library(entry SHARED rawfile_request.cpp hello.cpp)target_link_libraries(entry PUBLIC librawfile.z.so libace_napi.z.so libohweb.so libhilog_ndk.z.so)
main/cpp/types/index.d.tsexport const registerCustomSchemes: () => void;export const setSchemeHandler: () => void;export const initResourceManager: (resmgr: resourceManager.ResourceManager) => void;
main/cpp/rawfile_request.h#ifndef RAWFILE_REQUEST_H#define RAWFILE_REQUEST_H#include <mutex>#include <string>#include <rawfile/raw_file_manager.h>#include "web/arkweb_scheme_handler.h"#include "web/arkweb_net_error_list.h"class RawfileRequest {public: RawfileRequest(const ArkWeb_ResourceRequest *resourceRequest, const ArkWeb_ResourceHandler *resourceHandler, const NativeResourceManager* resourceManager); ~RawfileRequest(); void Start(); void Stop(); void ReadRawfileDataOnWorkerThread(); const ArkWeb_ResourceHandler *resourceHandler() { return resourceHandler_; } const ArkWeb_ResourceRequest *resourceRequest() { return resourceRequest_; } const NativeResourceManager *resourceManager() { return resourceManager_; } ArkWeb_Response *response() { return response_; } ArkWeb_HttpBodyStream *stream() { return stream_; } const std::string rawfilePath() { return rawfilePath_; } void DidReceiveResponse(); void DidReceiveData(const uint8_t *buffer, int64_t bufLen); void DidFinish(); void DidFailWithError(ArkWeb_NetError errorCode);private: const ArkWeb_ResourceRequest *resourceRequest_{nullptr}; const ArkWeb_ResourceHandler *resourceHandler_{nullptr}; const NativeResourceManager *resourceManager_{nullptr}; ArkWeb_Response *response_; bool stopped_{false}; std::string rawfilePath_; ArkWeb_HttpBodyStream *stream_{nullptr}; std::mutex mutex_;};#endif // RAWFILE_REQUEST_H
main/cpp/rawfile_request.cpp#include "rawfile_request.h"#include "threads.h"#include "hilog/log.h"#include "rawfile/raw_file.h"#include "rawfile/raw_file_manager.h"#undef LOG_TAG#define LOG_TAG "ss-handler"namespace {uint8_t buffer[1024];cnd_t http_body_cnd;mtx_t http_body_mtx;// HttpBodyStream的读回调。void ReadCallback(const ArkWeb_HttpBodyStream *httpBodyStream, uint8_t* buffer, int bytesRead){ OH_LOG_INFO(LOG_APP, "read http body back."); bool isEof = OH_ArkWebHttpBodyStream_IsEof(httpBodyStream); if (!isEof && bytesRead != 0) { memset(buffer, 0, 1000); OH_ArkWebHttpBodyStream_Read(httpBodyStream, buffer, 1000); } else { RawfileRequest *rawfileRequest = (RawfileRequest *)OH_ArkWebHttpBodyStream_GetUserData(httpBodyStream); if (rawfileRequest) { rawfileRequest->ReadRawfileDataOnWorkerThread(); cnd_signal(&http_body_cnd); } }}int ReadHttpBodyOnWorkerThread(void* userData){ memset(buffer, 0, 1000); ArkWeb_HttpBodyStream *httpBodyStream = (ArkWeb_HttpBodyStream *)userData; OH_ArkWebHttpBodyStream_Read(httpBodyStream, buffer, 1000); cnd_init(&http_body_cnd); mtx_init(&http_body_mtx, mtx_plain); cnd_wait(&http_body_cnd, &http_body_mtx); return 0;}int ReadRawfileOnWorkerThread(void* userData){ RawfileRequest * rawfileRequest = (RawfileRequest *)userData; if (rawfileRequest) { rawfileRequest->ReadRawfileDataOnWorkerThread(); } return 0;}// ArkWeb_HttpBodyStream的初始化回调。void InitCallback(const ArkWeb_HttpBodyStream *httpBodyStream, ArkWeb_NetError result){ OH_LOG_INFO(LOG_APP, "init http body stream done %{public}d.", result); bool isChunked = OH_ArkWebHttpBodyStream_IsChunked(httpBodyStream); OH_LOG_INFO(LOG_APP, "http body stream is chunked %{public}d.", isChunked); thrd_t th; if (thrd_create(&th, ReadHttpBodyOnWorkerThread, (void *)httpBodyStream) != thrd_success) { OH_LOG_ERROR(LOG_APP, "create thread failed."); return; } if (thrd_detach(th) != thrd_success) { OH_LOG_ERROR(LOG_APP, "detach thread failed."); }}const int blockSize = 1024 * 8;} // namespaceRawfileRequest::RawfileRequest(const ArkWeb_ResourceRequest *resourceRequest, const ArkWeb_ResourceHandler *resourceHandler, const NativeResourceManager* resourceManager) : resourceRequest_(resourceRequest), resourceHandler_(resourceHandler), resourceManager_(resourceManager) {}RawfileRequest::~RawfileRequest() {}void RawfileRequest::Start(){ OH_LOG_INFO(LOG_APP, "start a rawfile request."); char* url; OH_ArkWebResourceRequest_GetUrl(resourceRequest_, &url); std::string urlStr(url); std::size_t position = urlStr.rfind('/'); if (position != std::string::npos) { rawfilePath_ = urlStr.substr(position + 1); } OH_ArkWeb_ReleaseString(url); OH_ArkWeb_CreateResponse(&response_); OH_ArkWebResourceRequest_GetHttpBodyStream(resourceRequest(), &stream_); if (stream_) { OH_LOG_ERROR(LOG_APP, "have http body stream"); OH_ArkWebHttpBodyStream_SetUserData(stream_, this); OH_ArkWebHttpBodyStream_SetReadCallback(stream_, ReadCallback); OH_ArkWebHttpBodyStream_Init(stream_, InitCallback); } else { thrd_t th; if (thrd_create(&th, ReadRawfileOnWorkerThread, (void *)this) != thrd_success) { OH_LOG_ERROR(LOG_APP, "create thread failed."); return; } if (thrd_detach(th) != thrd_success) { OH_LOG_ERROR(LOG_APP, "detach thread failed."); } }}// 在worker线程中读取rawfile,并通过ResourceHandler返回给Web内核。void RawfileRequest::ReadRawfileDataOnWorkerThread(){ OH_LOG_INFO(LOG_APP, "read rawfile in worker thread."); const struct UrlInfo { std::string resource; std::string mimeType; } urlInfos[] = { {"test.html", "text/html"}, {"video.html", "text/html"}, {"isolated.html", "text/html"}, {"csp_bypassing.html", "text/html"}, {"post_data.html", "text/html"}, {"chunked_post_stream.html", "text/html"}, {"local.html", "text/html"}, {"service_worker.html", "text/html"}, {"csp_script.js", "text/javascript"}, {"sw.js", "text/javascript"}, {"isolated_script.js", "text/javascript"}, {"local_script.js", "text/javascript"}, {"test.mp4", "video/mp4"}, {"xhr", "application/json"} }; if (!resourceManager()) { OH_LOG_ERROR(LOG_APP, "read rawfile error, resource manager is nullptr."); return; } RawFile *rawfile = OH_ResourceManager_OpenRawFile(resourceManager(), rawfilePath().c_str()); if (!rawfile) { OH_ArkWebResponse_SetStatus(response(), 404); } else { OH_ArkWebResponse_SetStatus(response(), 200); } for (auto &urlInfo : urlInfos) { if (urlInfo.resource == rawfilePath()) { OH_ArkWebResponse_SetMimeType(response(), urlInfo.mimeType.c_str()); break; } } OH_ArkWebResponse_SetCharset(response(), "UTF-8"); long len = OH_ResourceManager_GetRawFileSize(rawfile); OH_ArkWebResponse_SetHeaderByName(response(), "content-length", std::to_string(len).c_str(), false); DidReceiveResponse(); long consumed = 0; uint8_t buffer[blockSize]; while (true) { int ret = OH_ResourceManager_ReadRawFile(rawfile, buffer, blockSize); OH_LOG_INFO(LOG_APP, "read rawfile %{public}d bytes.", ret); if (ret == 0) { break; } consumed += ret; OH_ResourceManager_SeekRawFile(rawfile, consumed, 0); DidReceiveData(buffer, ret); memset(buffer, 0, blockSize); } OH_ResourceManager_CloseRawFile(rawfile); DidFinish();}void RawfileRequest::Stop(){ OH_LOG_INFO(LOG_APP, "stop the rawfile request."); std::lock_guard<std::mutex> guard(mutex_); stopped_ = true; if (response_) { OH_ArkWeb_DestroyResponse(response_); } OH_ArkWebResourceRequest_Destroy(resourceRequest_); OH_ArkWebResourceHandler_Destroy(resourceHandler_);}void RawfileRequest::DidReceiveResponse(){ OH_LOG_INFO(LOG_APP, "did receive response."); std::lock_guard<std::mutex> guard(mutex_); if (!stopped_) { OH_ArkWebResourceHandler_DidReceiveResponse(resourceHandler_, response_); }}void RawfileRequest::DidReceiveData(const uint8_t *buffer, int64_t bufLen){ OH_LOG_INFO(LOG_APP, "did receive data."); std::lock_guard<std::mutex> guard(mutex_); if (!stopped_) { OH_ArkWebResourceHandler_DidReceiveData(resourceHandler_, buffer, bufLen); }}void RawfileRequest::DidFinish(){ OH_LOG_INFO(LOG_APP, "did finish."); std::lock_guard<std::mutex> guard(mutex_); if (!stopped_) { OH_ArkWebResourceHandler_DidFinish(resourceHandler_); }}void RawfileRequest::DidFailWithError(ArkWeb_NetError errorCode){ OH_LOG_INFO(LOG_APP, "did finish with error %{public}d.", errorCode); if (!stopped_) { OH_ArkWebResourceHandler_DidFailWithError(resourceHandler_, errorCode); }}
main/resources/rawfile/test.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><h1> 网络拦截测试demo</h1><a href="https://www.example.com/video.html">拦截视频资源请求,读取本地mp4文件</a><br/><a href="https://www.example.com/csp_bypassing.html">测试三方协议忽略csp检查,并成功拦截</a><br/><a href="https://www.example.com/isolated.html">测试拦截设置ISOLATED属性的三方协议</a><br/><a href="https://www.example.com/local.html">测试拦截设置LOCAL属性的三方协议</a><br/><a href="https://www.example.com/service_worker.html">测试拦截service worker触发的请求</a><br/><a href="https://www.example.com/post_data.html">测试读取blob类型http body stream</a><br/><a href="https://www.example.com/chunked_post_stream.html">测试读取chunked类型http body stream</a></body></html>
main/resources/rawfile/cat.svg<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.37 10.79"><path d="M12.8 10.18l-.8-.8c-.98-.8-.86-1.92-.87-2.04-.02-.1-.02-.58.02-.74.04-.15 0-.32 0-.32.28-1.18 1.2-.85 1.2-.85.38.04.4-.33.4-.33.25-.13.2-.4.2-.4l-.47-.48c-.18-.48-.7-.6-.7-.6.08-.48-.17-.78-.17-.78-.03.14-.58.72-.62.73-.63.15-.43.26-.83.55-.4.28-1.26.63-1.64.43-.37-.2-3.5-.5-4.86-.5-.4 0-.7.1-.95.2-.23-.16-.52-.52-.73-1.02-.3-.74-.36-1.48-.12-1.98.13-.27.28-.42.44-.45.23-.05.52.16.6.24.17.14.42.13.56-.03.15-.15.14-.4-.02-.55C3.38.4 2.8-.1 2.14.02c-.42.08-.76.38-1 .9-.34.7-.3 1.66.1 2.6.18.44.47.93.83 1.25-.1.13-.13.23-.13.23-.12.27-.44.9-.33 1.45.13.56-.22.82-.3.88-.05.07-.73.47-.73.47L0 9.78c-.08.38.43.6.43.6.18-.03.2-.63.2-.63l.44-1.04 1.66-.6s0 .7-.02.83-.1.35-.1.35c.08.46 1.2 1.5 1.2 1.5h.85v-.26c-.07-.3-.5-.16-.5-.16l-.62-.95c.66-.5.93-1.38.93-1.38.3.26 1.8-.22 1.8-.22l.9.1-.25 2.1c-.07.5.05.68.05.68h.4c.3 0 .48.03.48-.27 0-.28-.4-.23-.4-.23l1-1.95c.93-.58 1.53.26 1.53.26l.05.3c.37.53 2.38 1.9 2.38 1.9h1v-.3c-.18-.32-.6-.2-.6-.2z"/></svg>
main/resources/rawfile/csp_bypassing.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'self'; media-src 'self'"></head><body><p>scheme: custom-csp-bypassing</p><p>options: ARKWEB_SCHEME_OPTION_CSP_BYPASSING | ARKWEB_SCHEME_OPTION_STANDARD</p><script src="custom-csp-bypassing://www.example.com/csp_script.js"></script></body></html>
main/resources/rawfile/csp_script.jsconst body = document.body;const element = document.createElement('div');element.textContent = 'csp_script.js bypass the csp rules';body.appendChild(element);
main/resources/rawfile/isolated_script.jsconst element = document.getElementById('isolated_test');element.textContent = 'isolated_script.js not blocked';
main/resources/rawfile/isolated.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><p>scheme: custom-isolated</p><p>options: ARKWEB_SCHEME_OPTION_DISPLAY_ISOLATED</p><div id="isolated_test">isolated_script.js 被拦截</div><script src="custom-isolated://www.example.com/isolated_script.js"></script></body></html>
main/resources/rawfile/local_script.jsconst element = document.getElementById('local_test');element.textContent = 'local_script.js not blocked.';
main/resources/rawfile/local.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><p>scheme: custom-local</p><p>options: ARKWEB_SCHEME_OPTION_LOCAL</p><div id="local_test">local_script.js 被拦截</div><script src="custom-local://www.example.com/local_script.js"></script></body></html>
main/resources/rawfile/post_data.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><script> function textPostXhr(url) { var formData = new FormData(); var myBlob = new Blob(["This is my blob content"], {type : "text/plain"}); formData.append("upload", myBlob); var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.send(formData); xhr.onreadystatechange = function (err) { console.log(err.target.status); } } function textPutXhr(url) { var formData = new FormData(); var myBlob = new Blob(["This is my blob content"], {type : "text/plain"}); formData.append("upload", myBlob); var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.send(formData); xhr.onreadystatechange = function (err) { console.log(err.target.status); } }</script></head><body><div onclick="textPostXhr('https://www.example.com/xhr')">test xhr post</div><div onclick="textPutXhr('https://www.example.com/xhr')">test xhr put</div></body></html>
main/resources/rawfile/service_worker.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><script>function registerSuccess() { const body = document.body; const element = document.createElement('div'); element.textContent = 'register sw successful.'; body.appendChild(element);}navigator.serviceWorker.register('/sw.js') .then(reg => registerSuccess()) .catch(error => console.log('failed!', error))</script></head><body></body></html>
main/resources/rawfile/sw.jsself.addEventListener('install', event => { console.log('v1 installing'); event.waitUntil( caches.open('static-v1').then(cache => cache.add('/cat.svg')) );});self.addEventListener('activate', event => { console.log("v1 now redy to handle fetches.");});
main/resources/rawfile/video.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><video width="400" height="400" controls> <source src="https://www.example.com/test.mp4" type="video/mp4"></video></body></html>
main/resources/rawfile/chunked_post_stream.html<html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><script>let uploaded = 0;let buf = new Uint8Array(1024 * 50);let start = Date.now();var rs = new ReadableStream({ pull(ctrl) { uploaded += buf.byteLength; crypto.getRandomValues(buf); ctrl.enqueue(buf); if ((start + 1000) < Date.now()) ctrl.close(); }});function test() { fetch('https://www.example.com/xhr', { method: 'POST', body: rs, duplex: 'half' }).then(r => r.json()).then(console.log);}</script><body><div onclick="test()">test post chunked http body.</div></body></html>
main/resources/rawfile/xhr{}
## 自定义页面请求响应 Web组件支持在应用拦截到页面请求后自定义响应请求能力。开发者通过onInterceptRequest()接口来实现自定义资源请求响应 。自定义请求能力可以用于开发者自定义Web页面响应、自定义文件资源响应等场景。Web网页上发起资源加载请求,应用层收到资源请求消息。应用层构造本地资源响应消息发送给Web内核。Web内核解析应用层响应信息,根据此响应信息进行页面资源加载。在下面的示例中,Web组件通过拦截页面请求“https://www.example.com/test.html”, 在应用侧代码构建响应资源,实现自定义页面响应场景。 - 前端页面index.html代码。
<!DOCTYPE html><html><head> <meta charset="utf-8"></head><body><!-- 页面资源请求 --><a href="https://www.example.com/test.html">intercept test!</a></body></html>
应用侧代码。// xxx.etsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct WebComponent { controller: webview.WebviewController = new webview.WebviewController(); responseResource: WebResourceResponse = new WebResourceResponse(); // 开发者自定义响应数据 @State webData: string = '<!DOCTYPE html>\n' + '<html>\n' + '<head>\n' + '<title>intercept test</title>\n' + '</head>\n' + '<body>\n' + '<h1>intercept ok</h1>\n' + '</body>\n' + '</html>' build() { Column() { Web({ src: $rawfile('index.html'), controller: this.controller }) .onInterceptRequest((event) => { if (event) { console.info('url:' + event.request.getRequestUrl()); // 拦截页面请求 if (event.request.getRequestUrl() !== 'https://www.example.com/test.html') { return null; } } // 构造响应数据 this.responseResource.setResponseData(this.webData); this.responseResource.setResponseEncoding('utf-8'); this.responseResource.setResponseMimeType('text/html'); this.responseResource.setResponseCode(200); this.responseResource.setReasonMessage('OK'); return this.responseResource; }) } }}
为自定义的JavaScript请求响应生成 CodeCache:自定义请求响应的资源类型如果是JavaScript脚本,可以在响应头中添加“ResponseDataID”字段,Web内核读取到该字段后会在为该JS资源生成CodeCache,加速JS执行,并且ResponseData如果有更新时必须更新该字段。不添加“ResponseDataID”字段的情况下默认不生成CodeCache。在下面的示例中,Web组件通过拦截页面请求“https://www.example.com/test.js”, 应用侧代码构建响应资源,在响应头中添加“ResponseDataID”字段,开启生成CodeCache的功能。 - 前端页面index.html代码。
<!DOCTYPE html><html><head> <meta charset="utf-8"></head><body><div id="div-1">this is a test div</div><div id="div-2">this is a test div</div><div id="div-3">this is a test div</div><div id="div-4">this is a test div</div><div id="div-5">this is a test div</div><div id="div-6">this is a test div</div><div id="div-7">this is a test div</div><div id="div-8">this is a test div</div><div id="div-9">this is a test div</div><div id="div-10">this is a test div</div><div id="div-11">this is a test div</div><script src="https://www.example.com/test.js"></script></body></html>
应用侧代码。// xxx.etsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct WebComponent { controller: webview.WebviewController = new webview.WebviewController(); responseResource: WebResourceResponse = new WebResourceResponse(); // 开发者自定义响应数据(响应数据长度需大于等于1024才会生成codecache) @State jsData: string = 'let text_msg = "the modified content:version 0000000000001";\n' + 'let element1 = window.document.getElementById("div-1");\n' + 'let element2 = window.document.getElementById("div-2");\n' + 'let element3 = window.document.getElementById("div-3");\n' + 'let element4 = window.document.getElementById("div-4");\n' + 'let element5 = window.document.getElementById("div-5");\n' + 'let element6 = window.document.getElementById("div-6");\n' + 'let element7 = window.document.getElementById("div-7");\n' + 'let element8 = window.document.getElementById("div-8");\n' + 'let element9 = window.document.getElementById("div-9");\n' + 'let element10 = window.document.getElementById("div-10");\n' + 'let element11 = window.document.getElementById("div-11");\n' + 'element1.innerHTML = text_msg;\n' + 'element2.innerHTML = text_msg;\n' + 'element3.innerHTML = text_msg;\n' + 'element4.innerHTML = text_msg;\n' + 'element5.innerHTML = text_msg;\n' + 'element6.innerHTML = text_msg;\n' + 'element7.innerHTML = text_msg;\n' + 'element8.innerHTML = text_msg;\n' + 'element9.innerHTML = text_msg;\n' + 'element10.innerHTML = text_msg;\n' + 'element11.innerHTML = text_msg;\n'; build() { Column() { Web({ src: $rawfile('index.html'), controller: this.controller }) .onInterceptRequest((event) => { // 拦截页面请求 if (event?.request.getRequestUrl() == 'https://www.example.com/test.js') { // 构造响应数据 this.responseResource.setResponseHeader([ { // 格式:不超过13位纯数字。js识别码,Js有更新时必须更新该字段 headerKey: "ResponseDataID", headerValue: "0000000000001" }]); this.responseResource.setResponseData(this.jsData); this.responseResource.setResponseEncoding('utf-8'); this.responseResource.setResponseMimeType('application/javascript'); this.responseResource.setResponseCode(200); this.responseResource.setReasonMessage('OK'); return this.responseResource; } return null; }) } }}
## 加速Web页面的访问 当Web页面加载缓慢时,可以使用预连接、预加载和预获取post请求的能力加速Web页面的访问。### 预解析和预连接可以通过prepareForPageLoad()来预解析或者预连接将要加载的页面。在下面的示例中,在Web组件的onAppear中对要加载的页面进行预连接。// xxx.etsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); build() { Column() { Button('loadData') .onClick(() => { if (this.webviewController.accessBackward()) { this.webviewController.backward(); } }) Web({ src: 'https://www.example.com/', controller: this.webviewController }) .onAppear(() => { // 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址进行dns预解析 // 第三个参数为要预连接socket的个数。最多允许6个。 webview.WebviewController.prepareForPageLoad('https://www.example.com/', true, 2); }) } }}
也可以通过initializeBrowserEngine()来提前初始化内核,然后在初始化内核后调用prepareForPageLoad()对即将要加载的页面进行预解析、预连接。这种方式适合提前对首页进行预解析、预连接。在下面的示例中,Ability的onCreate中提前初始化Web内核并对首页进行预连接。// xxx.etsimport { webview } from '@kit.ArkWeb';import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { console.log("EntryAbility onCreate"); webview.WebviewController.initializeWebEngine(); // 预连接时,需要將'https://www.example.com'替换成真实要访问的网站地址。 webview.WebviewController.prepareForPageLoad("https://www.example.com/", true, 2); AppStorage.setOrCreate("abilityWant", want); console.log("EntryAbility onCreate done"); }}
### 预加载如果能够预测到Web组件将要加载的页面或者即将要跳转的页面。可以通过prefetchPage()来预加载即将要加载页面。预加载会提前下载页面所需的资源,包括主资源子资源,但不会执行网页JavaScript代码。预加载是WebviewController的实例方法,需要一个已经关联好Web组件的WebviewController实例。在下面的示例中,在onPageEnd的时候触发下一个要访问的页面的预加载。// xxx.etsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); build() { Column() { Web({ src: 'https://www.example.com/', controller: this.webviewController }) .onPageEnd(() => { // 预加载https://www.iana.org/help/example-domains。 this.webviewController.prefetchPage('https://www.iana.org/help/example-domains'); }) } }}
### 预获取post请求可以通过prefetchResource()预获取将要加载页面中的post请求。在页面加载结束时,可以通过clearPrefetchedResource()清除后续不再使用的预获取资源缓存。以下示例,在Web组件onAppear中,对要加载页面中的post请求进行预获取。在onPageEnd中,可以清除预获取的post请求缓存。// xxx.etsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); build() { Column() { Web({ src: "https://www.example.com/", controller: this.webviewController}) .onAppear(() => { // 预获取时,需要將"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。 webview.WebviewController.prefetchResource( {url:"https://www.example1.com/post?e=f&g=h", method:"POST", formData:"a=x&b=y",}, [{headerKey:"c", headerValue:"z",},], "KeyX", 500); }) .onPageEnd(() => { // 清除后续不再使用的预获取资源缓存。 webview.WebviewController.clearPrefetchedResource(["KeyX",]); }) } }}
如果能够预测到Web组件将要加载页面或者即将要跳转页面中的post请求。可以通过prefetchResource()预获取即将要加载页面的post请求。以下示例,在onPageEnd中,触发预获取一个要访问页面的post请求。// xxx.etsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); build() { Column() { Web({ src: 'https://www.example.com/', controller: this.webviewController}) .onPageEnd(() => { // 预获取时,需要將"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。 webview.WebviewController.prefetchResource( {url:"https://www.example1.com/post?e=f&g=h", method:"POST", formData:"a=x&b=y",}, [{headerKey:"c", headerValue:"z",},], "KeyX", 500); }) } }}
也可以通过initializeBrowserEngine()提前初始化内核,然后在初始化内核后调用prefetchResource()预获取将要加载页面中的post请求。这种方式适合提前预获取首页的post请求。以下示例,在Ability的onCreate中,提前初始化Web内核并预获取首页的post请求。// xxx.etsimport { webview } from '@kit.ArkWeb';import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { console.log("EntryAbility onCreate"); webview.WebviewController.initializeWebEngine(); // 预获取时,需要將"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。 webview.WebviewController.prefetchResource( {url:"https://www.example1.com/post?e=f&g=h", method:"POST", formData:"a=x&b=y",}, [{headerKey:"c", headerValue:"z",},], "KeyX", 500); AppStorage.setOrCreate("abilityWant", want); console.log("EntryAbility onCreate done"); }}
### 预编译生成编译缓存可以通过precompileJavaScript()在页面加载前提前生成脚本文件的编译缓存。推荐配合动态组件使用,使用离线的Web组件用于生成字节码缓存,并在适当的时机加载业务用Web组件使用这些字节码缓存。下方是代码示例: - 首先,在EntryAbility中将UIContext存到localStorage中。
// EntryAbility.etsimport { UIAbility } from '@kit.AbilityKit';import { window } from '@kit.ArkUI';const localStorage: LocalStorage = new LocalStorage('uiContext');export default class EntryAbility extends UIAbility { storage: LocalStorage = localStorage; onWindowStageCreate(windowStage: window.WindowStage) { windowStage.loadContent('pages/Index', this.storage, (err, data) => { if (err.code) { return; } this.storage.setOrCreate<UIContext>("uiContext", windowStage.getMainWindowSync().getUIContext()); }); }}
编写动态组件所需基础代码。// DynamicComponent.etsimport { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';export interface BuilderData { url: string; controller: WebviewController;}const storage = LocalStorage.getShared();export class NodeControllerImpl extends NodeController { private rootNode: BuilderNode<BuilderData[]> | null = null; private wrappedBuilder: WrappedBuilder<BuilderData[]> | null = null; constructor(wrappedBuilder: WrappedBuilder<BuilderData[]>) { super(); this.wrappedBuilder = wrappedBuilder; } makeNode(): FrameNode | null { if (this.rootNode != null) { return this.rootNode.getFrameNode(); } return null; } initWeb(url: string, controller: WebviewController) { if(this.rootNode != null) { return; } const uiContext: UIContext = storage.get<UIContext>("uiContext") as UIContext; if (!uiContext) { return; } this.rootNode = new BuilderNode(uiContext); this.rootNode.build(this.wrappedBuilder, { url: url, controller: controller }); }}export const createNode = (wrappedBuilder: WrappedBuilder<BuilderData[]>, data: BuilderData) => { const baseNode = new NodeControllerImpl(wrappedBuilder); baseNode.initWeb(data.url, data.controller); return baseNode;}
编写用于生成字节码缓存的组件,本例中的本地Javascript资源内容通过文件读取接口读取rawfile目录下的本地文件。// PrecompileWebview.etsimport { BuilderData } from "./DynamicComponent";import { Config, configs } from "./PrecompileConfig";@Builderfunction WebBuilder(data: BuilderData) { Web({ src: data.url, controller: data.controller }) .onControllerAttached(() => { precompile(data.controller, configs); }) .fileAccess(true)}export const precompileWebview = wrapBuilder<BuilderData[]>(WebBuilder);export const precompile = async (controller: WebviewController, configs: Array<Config>) => { for (const config of configs) { let content = await readRawFile(config.localPath); try { controller.precompileJavaScript(config.url, content, config.options) .then(errCode => { console.error("precompile successfully! " + errCode); }).catch((errCode: number) => { console.error("precompile failed. " + errCode); }); } catch (err) { console.error("precompile failed. " + err.code + " " + err.message); } }}async function readRawFile(path: string) { try { return await getContext().resourceManager.getRawFileContent(path);; } catch (err) { return new Uint8Array(0); }}
JavaScript资源的获取方式也可通过网络请求的方式获取,但此方法获取到的http响应头非标准HTTP响应头格式,需额外将响应头转换成标准HTTP响应头格式后使用。如通过网络请求获取到的响应头是e-tag,则需要将其转换成E-Tag后使用。 - 编写业务用组件代码。
// BusinessWebview.etsimport { BuilderData } from "./DynamicComponent";@Builderfunction WebBuilder(data: BuilderData) { // 此处组件可根据业务需要自行扩展 Web({ src: data.url, controller: data.controller }) .cacheMode(CacheMode.Default)}export const businessWebview = wrapBuilder<BuilderData[]>(WebBuilder);
编写资源配置信息。// PrecompileConfig.etsimport { webview } from '@kit.ArkWeb'export interface Config { url: string, localPath: string, // 本地资源路径 options: webview.CacheOptions}export let configs: Array<Config> = [ { url: "https://www.example.com/example.js", localPath: "example.js", options: { responseHeaders: [ { headerKey: "E-Tag", headerValue: "aWO42N9P9dG/5xqYQCxsx+vDOoU="}, { headerKey: "Last-Modified", headerValue: "Wed, 21 Mar 2024 10:38:41 GMT"} ] } }]
在页面中使用。// Index.etsimport { webview } from '@kit.ArkWeb';import { NodeController } from '@kit.ArkUI';import { createNode } from "./DynamicComponent"import { precompileWebview } from "./PrecompileWebview"import { businessWebview } from "./BusinessWebview"@Entry@Componentstruct Index { @State precompileNode: NodeController | undefined = undefined; precompileController: webview.WebviewController = new webview.WebviewController(); @State businessNode: NodeController | undefined = undefined; businessController: webview.WebviewController = new webview.WebviewController(); aboutToAppear(): void { // 初始化用于注入本地资源的Web组件 this.precompileNode = createNode(precompileWebview, { url: "https://www.example.com/empty.html", controller: this.precompileController}); } build() { Column() { // 在适当的时机加载业务用Web组件,本例以Button点击触发为例 Button("加载页面") .onClick(() => { this.businessNode = createNode(businessWebview, { url: "https://www.example.com/business.html", controller: this.businessController }); }) // 用于业务的Web组件 NodeContainer(this.businessNode); } }}
当需要更新本地已经生成的编译字节码时,修改cacheOptions参数中responseHeaders中的E-Tag或Last-Modified响应头对应的值,再次调用接口即可。### 离线资源免拦截注入可以通过injectOfflineResources()在页面加载前提前将图片、样式表或脚本资源注入到应用的内存缓存中。推荐配合动态组件使用,使用离线的Web组件用于将资源注入到内核的内存缓存中,并在适当的时机加载业务用Web组件使用这些资源。下方是代码示例: - 首先,在EntryAbility中将UIContext存到localStorage中。
// EntryAbility.etsimport { UIAbility } from '@kit.AbilityKit';import { window } from '@kit.ArkUI';const localStorage: LocalStorage = new LocalStorage('uiContext');export default class EntryAbility extends UIAbility { storage: LocalStorage = localStorage; onWindowStageCreate(windowStage: window.WindowStage) { windowStage.loadContent('pages/Index', this.storage, (err, data) => { if (err.code) { return; } this.storage.setOrCreate<UIContext>("uiContext", windowStage.getMainWindowSync().getUIContext()); }); }}
编写动态组件所需基础代码。// DynamicComponent.etsimport { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';export interface BuilderData { url: string; controller: WebviewController;}const storage = LocalStorage.getShared();export class NodeControllerImpl extends NodeController { private rootNode: BuilderNode<BuilderData[]> | null = null; private wrappedBuilder: WrappedBuilder<BuilderData[]> | null = null; constructor(wrappedBuilder: WrappedBuilder<BuilderData[]>) { super(); this.wrappedBuilder = wrappedBuilder; } makeNode(): FrameNode | null { if (this.rootNode != null) { return this.rootNode.getFrameNode(); } return null; } initWeb(url: string, controller: WebviewController) { if(this.rootNode != null) { return; } const uiContext: UIContext = storage.get<UIContext>("uiContext") as UIContext; if (!uiContext) { return; } this.rootNode = new BuilderNode(uiContext); this.rootNode.build(this.wrappedBuilder, { url: url, controller: controller }); }}export const createNode = (wrappedBuilder: WrappedBuilder<BuilderData[]>, data: BuilderData) => { const baseNode = new NodeControllerImpl(wrappedBuilder); baseNode.initWeb(data.url, data.controller); return baseNode;}
编写用于注入资源的组件代码,本例中的本地资源内容通过文件读取接口读取rawfile目录下的本地文件。// InjectWebview.etsimport { webview } from '@kit.ArkWeb';import { resourceConfigs } from "./Resource";import { BuilderData } from "./DynamicComponent";@Builderfunction WebBuilder(data: BuilderData) { Web({ src: data.url, controller: data.controller }) .onControllerAttached(async () => { try { data.controller.injectOfflineResources(await getData ()); } catch (err) { console.error("error: " + err.code + " " + err.message); } }) .fileAccess(true)}export const injectWebview = wrapBuilder<BuilderData[]>(WebBuilder);export async function getData() { const resourceMapArr: Array<webview.OfflineResourceMap> = []; // 读取配置,从rawfile目录中读取文件内容 for (let config of resourceConfigs) { let buf: Uint8Array = new Uint8Array(0); if (config.localPath) { buf = await readRawFile(config.localPath); } resourceMapArr.push({ urlList: config.urlList, resource: buf, responseHeaders: config.responseHeaders, type: config.type, }) } return resourceMapArr;}export async function readRawFile(url: string) { try { return await getContext().resourceManager.getRawFileContent(url); } catch (err) { return new Uint8Array(0); }}
编写业务用组件代码。// BusinessWebview.etsimport { BuilderData } from "./DynamicComponent";@Builderfunction WebBuilder(data: BuilderData) { // 此处组件可根据业务需要自行扩展 Web({ src: data.url, controller: data.controller }) .cacheMode(CacheMode.Default)}export const businessWebview = wrapBuilder<BuilderData[]>(WebBuilder);
编写资源配置信息。// Resource.etsimport { webview } from '@kit.ArkWeb';export interface ResourceConfig { urlList: Array<string>, type: webview.OfflineResourceType, responseHeaders: Array<Header>, localPath: string, // 本地资源存放在rawfile目录下的路径}export const resourceConfigs: Array<ResourceConfig> = [ { localPath: "example.png", urlList: [ "https://www.example.com/", "https://www.example.com/path1/example.png", "https://www.example.com/path2/example.png", ], type: webview.OfflineResourceType.IMAGE, responseHeaders: [ { headerKey: "Cache-Control", headerValue: "max-age=1000" }, { headerKey: "Content-Type", headerValue: "image/png" }, ] }, { localPath: "example.js", urlList: [ // 仅提供一个url,这个url既作为资源的源,也作为资源的网络请求地址 "https://www.example.com/example.js", ], type: webview.OfflineResourceType.CLASSIC_JS, responseHeaders: [ // 以<script crossorigin="anoymous" />方式使用,提供额外的响应头 { headerKey: "Cross-Origin", headerValue:"anonymous" } ] },];
在页面中使用。// Index.etsimport { webview } from '@kit.ArkWeb';import { NodeController } from '@kit.ArkUI';import { createNode } from "./DynamicComponent"import { injectWebview } from "./InjectWebview"import { businessWebview } from "./BusinessWebview"@Entry@Componentstruct Index { @State injectNode: NodeController | undefined = undefined; injectController: webview.WebviewController = new webview.WebviewController(); @State businessNode: NodeController | undefined = undefined; businessController: webview.WebviewController = new webview.WebviewController(); aboutToAppear(): void { // 初始化用于注入本地资源的Web组件, 提供一个空的html页面作为url即可 this.injectNode = createNode(injectWebview, { url: "https://www.example.com/empty.html", controller: this.injectController}); } build() { Column() { // 在适当的时机加载业务用Web组件,本例以Button点击触发为例 Button("加载页面") .onClick(() => { this.businessNode = createNode(businessWebview, { url: "https://www.example.com/business.html", controller: this.businessController }); }) // 用于业务的Web组件 NodeContainer(this.businessNode); } }}
加载的HTML网页示例。<!DOCTYPE html><html lang="en"><head></head><body> <img src="https://www.example.com/path1/request.png" /> <img src="https://www.example.com/path2/request.png" /> <script src="https://www.example.com/example.js" crossorigin="anonymous"></script></body></html>
## Web前进后退缓存 开启Web组件前进后退缓存功能,在前进后退的场景达到秒开的效果。### 开启前进后退缓存可以通过enableBackForwardCache()来开启web组件使用前进后退缓存的功能。需要在initializeBrowserEngine()初始化内核之前调用。// xxx.tsimport { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';import { hilog } from '@kit.PerformanceAnalysisKit';import { window } from '@kit.ArkUI';import { webview } from '@kit.ArkWeb';export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { let features = new webview.BackForwardCacheSupportedFeatures(); features.nativeEmbed = true; features.mediaTakeOver = true; webview.WebviewController.enableBackForwardCache(features); webview.WebviewController.initializeWebEngine(); AppStorage.setOrCreate("abilityWant", want); }}
### 设置缓存的页面数量和页面留存的时间可以通过setBackForwardCacheOptions()来设置每一个web示例前进后退缓存的策略。在下面的示例中,设置web组件可以缓存的最大数量为10,每个页面在缓存中停留300s。// EntryAbility.tsimport { webview } from '@kit.ArkWeb';@Entry@Componentstruct Index { controller: webview.WebviewController = new webview.WebviewController(); build() { Column() { Row() { Button("Add options").onClick((event: ClickEvent) => { let options = new webview.BackForwardCacheOptions(); options.size = 10; options.timeToLive = 300; this.controller.setBackForwardCacheOptions(options); }) Button("Backward").onClick((event: ClickEvent) => { this.controller.backward(); }) Button("Forward").onClick((event: ClickEvent) => { this.controller.forward(); }) } Web({ src: "https://www.example.com", controller: this.controller }) } .height('100%') .width('100%') }}
版权归原作者 兔子不吃饭 所有, 如有侵权,请联系我们删除。