HarmonyOS NEXT - ArkWeb管理网页加载与浏览记录
兔子不吃饭 2024-10-11 16:33:01 阅读 53
使用Web组件加载页面
页面加载是Web组件的基本功能。根据页面加载数据来源可以分为三种常用场景,包括加载网络页面、加载本地页面、加载HTML格式的富文本数据。
页面加载过程中,若涉及网络资源获取,需要配置ohos.permission.INTERNET网络访问权限。
加载网络页面
开发者可以在Web组件创建时,指定默认加载的网络页面 。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用loadUrl()接口加载指定的网页。Web组件的第一个参数变量src不能通过状态变量(例如:@State)动态更改地址,如需更改,请通过loadUrl()重新加载。
在下面的示例中,在Web组件加载完“www.example.com”页面后,开发者可通过loadUrl接口将此Web组件显示页面变更为“www.example1.com”。
<code>// 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 资源文件路径
应用侧代码。
<code>// 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>code>
</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>code>
</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">code>
</head>
<body>
<h1> 网络拦截测试demo</h1>
<a href="https://www.example.com/video.html">拦截视频资源请求,读取本地mp4文件</a><br/>code>
<a href="https://www.example.com/csp_bypassing.html">测试三方协议忽略csp检查,并成功拦截</a><br/>code>
<a href="https://www.example.com/isolated.html">测试拦截设置ISOLATED属性的三方协议</a><br/>code>
<a href="https://www.example.com/local.html">测试拦截设置LOCAL属性的三方协议</a><br/>code>
<a href="https://www.example.com/service_worker.html">测试拦截service worker触发的请求</a><br/>code>
<a href="https://www.example.com/post_data.html">测试读取blob类型http body stream</a><br/>code>
<a href="https://www.example.com/chunked_post_stream.html">测试读取chunked类型http body stream</a>code>
</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>code>
main/resources/rawfile/csp_bypassing.html
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">code>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; media-src 'self'">code>
</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>code>
</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">code>
</head>
<body>
<p>scheme: custom-isolated</p>
<p>options: ARKWEB_SCHEME_OPTION_DISPLAY_ISOLATED</p>
<div id="isolated_test">isolated_script.js 被拦截</div>code>
<script src="custom-isolated://www.example.com/isolated_script.js"></script>code>
</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">code>
</head>
<body>
<p>scheme: custom-local</p>
<p>options: ARKWEB_SCHEME_OPTION_LOCAL</p>
<div id="local_test">local_script.js 被拦截</div>code>
<script src="custom-local://www.example.com/local_script.js"></script>code>
</body>
</html>
main/resources/rawfile/post_data.html
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">code>
<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>code>
<div onclick="textPutXhr('https://www.example.com/xhr')">test xhr put</div>code>
</body>
</html>
main/resources/rawfile/service_worker.html
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">code>
<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">code>
</head>
<body>
<video width="400" height="400" controls>code>
<source src="https://www.example.com/test.mp4" type="video/mp4">code>
</video>
</body>
</html>
main/resources/rawfile/chunked_post_stream.html
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">code>
</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>code>
</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">code>
</head>
<body>
<!-- 页面资源请求 -->
<a href="https://www.example.com/test.html">intercept test!</a>code>
</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">code>
</head>
<body>
<div id="div-1">this is a test div</div>code>
<div id="div-2">this is a test div</div>code>
<div id="div-3">this is a test div</div>code>
<div id="div-4">this is a test div</div>code>
<div id="div-5">this is a test div</div>code>
<div id="div-6">this is a test div</div>code>
<div id="div-7">this is a test div</div>code>
<div id="div-8">this is a test div</div>code>
<div id="div-9">this is a test div</div>code>
<div id="div-10">this is a test div</div>code>
<div id="div-11">this is a test div</div>code>
<script src="https://www.example.com/test.js"></script>code>
</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"}code>
]
}
}
]
在页面中使用。
// 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" />方式使用,提供额外的响应头code>
{ 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">code>
<head></head>
<body>
<img src="https://www.example.com/path1/request.png" />code>
<img src="https://www.example.com/path2/request.png" />code>
<script src="https://www.example.com/example.js" crossorigin="anonymous"></script>code>
</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%')
}
}
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。