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%')

}

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。