使用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.ets import { webview } from '@kit.ArkWeb'; import { router } from '@kit.ArkUI'; @Entry @Component struct 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 @Component struct ProfilePage { @State message: string = 'Hello World'; build() { Column() { Text(this.message) .fontSize(20) } } }
跨应用跳转
Web组件可以实现点击前端页面超链接跳转到其他应用。
在下面的示例中,点击call.html前端页面中的超链接,跳转到电话应用的拨号界面。
-
应用侧代码。
// xxx.ets import { webview } from '@kit.ArkWeb'; import { call } from '@kit.TelephonyKit'; @Entry @Component struct 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.ets
import testNapi from 'libentry.so'; import { webview } from '@kit.ArkWeb'; import { resourceManager } from '@kit.LocalizationKit'; @Entry @Component struct 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.ets
import { 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_START static 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_END static 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.ts
export 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; } // namespace RawfileRequest::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.js
const 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.js
const 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.js
const 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.js
self.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.ets import { webview } from '@kit.ArkWeb'; @Entry @Component struct 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.ets import { webview } from '@kit.ArkWeb'; @Entry @Component struct 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.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/', controller: this.webviewController }) .onAppear(() => { // 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址进行dns预解析 // 第三个参数为要预连接socket的个数。最多允许6个。 webview.WebviewController.prepareForPageLoad('https://www.example.com/', true, 2); }) } } }
也可以通过initializeBrowserEngine()来提前初始化内核,然后在初始化内核后调用
prepareForPageLoad()对即将要加载的页面进行预解析、预连接。这种方式适合提前对首页进行
预解析、预连接。
在下面的示例中,Ability的onCreate中提前初始化Web内核并对首页进行预连接。
// xxx.ets import { 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.ets import { webview } from '@kit.ArkWeb'; @Entry @Component struct 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.ets import { webview } from '@kit.ArkWeb'; @Entry @Component struct 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.ets import { webview } from '@kit.ArkWeb'; @Entry @Component struct 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.ets import { 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.ets import { 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.ets import { 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.ets import { BuilderData } from "./DynamicComponent"; import { Config, configs } from "./PrecompileConfig"; @Builder function 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.ets import { BuilderData } from "./DynamicComponent"; @Builder function WebBuilder(data: BuilderData) { // 此处组件可根据业务需要自行扩展 Web({ src: data.url, controller: data.controller }) .cacheMode(CacheMode.Default) } export const businessWebview = wrapBuilder<BuilderData[]>(WebBuilder);
编写资源配置信息。
// PrecompileConfig.ets import { 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.ets import { webview } from '@kit.ArkWeb'; import { NodeController } from '@kit.ArkUI'; import { createNode } from "./DynamicComponent" import { precompileWebview } from "./PrecompileWebview" import { businessWebview } from "./BusinessWebview" @Entry @Component struct 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.ets import { 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.ets import { 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.ets import { webview } from '@kit.ArkWeb'; import { resourceConfigs } from "./Resource"; import { BuilderData } from "./DynamicComponent"; @Builder function 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.ets import { BuilderData } from "./DynamicComponent"; @Builder function WebBuilder(data: BuilderData) { // 此处组件可根据业务需要自行扩展 Web({ src: data.url, controller: data.controller }) .cacheMode(CacheMode.Default) } export const businessWebview = wrapBuilder<BuilderData[]>(WebBuilder);
编写资源配置信息。
// Resource.ets import { 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.ets import { webview } from '@kit.ArkWeb'; import { NodeController } from '@kit.ArkUI'; import { createNode } from "./DynamicComponent" import { injectWebview } from "./InjectWebview" import { businessWebview } from "./BusinessWebview" @Entry @Component struct 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.ts import { 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.ts import { webview } from '@kit.ArkWeb'; @Entry @Component struct 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%') } }