Web组件
- Web组件:提供具有网页显示能力的Web组件。
- @ohos.web.webview:提供web控制能力。 (Private)
生命周期
这个需要额外了解下,因为往往需要进行一些方法注入,拦截请求等操作:
aboutToAppear函数:在创建自定义组件的新实例后,在执行其build函数前执行。- 建议在此设置WebDebug调试模式、自定义协议URL的权限、Cookie等。
onControllerAttached事件:当Controller成功绑定到Web组件时触发该回调,且禁止在该事件回调前调用Web组件相关的接口,否则会抛出js-error异常。- 建议在此事件中注入JS对象、设置自定义用户代理,使用操作网页不相关的接口。
- 无法使用有关操作网页的接口,例如zoomIn、zoomOut等。
onLoadIntercept事件:当Web组件加载url之前触发该回调,用于判断是否阻止此次访问。默认允许加载。onInterceptRequest事件:当Web组件加载url之前触发该回调,用于拦截url并返回响应数据。onPageBegin事件:网页开始加载时触发该回调,且只在主frame触发。- 多frame页面可能同时加载,主frame加载结束时子frame可能仍在加载。同一页面导航或失败的导航不会触发该回调。
onProgressChange事件:告知开发者当前页面加载的进度。- 多frame页面或者子frame可能还在继续加载而主frame已经加载结束,所以在onPageEnd事件后仍可能收到该事件。
onPageEnd事件:网页加载完成时触发该回调,且只在主frame触发。- 建议在此回调中执行JavaScript脚本。
其它状态:
onOverrideUrlLoading事件:当URL将要加载到当前Web中时,让宿主应用程序有机会获得控制权,回调函数返回true将导致当前Web中止加载URL,而返回false则会导致Web继续照常加载URL。- 与
onLoadIntercept很像但触发时机也不同,onLoadIntercept事件在LoadUrl和iframe加载时触发,但onOverrideUrlLoading事件在LoadUrl和特定iframe加载时不会触发。
- 与
onPageVisible事件:Web回调事件。渲染流程中当HTTP响应的主体开始加载,新页面即将可见时触发该回调。此时文档加载还处于早期,因此链接的资源比如在线CSS、在线图片等可能尚不可用。onRenderExited事件:应用渲染进程异常退出时触发该回调,可以在此回调中进行系统资源的释放、数据的保存等操作。如果应用希望异常恢复,需要调用loadUrl接口重新加载页面。onDisAppear事件:组件卸载消失时触发此回调。

// xxx.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct WebComponent {
controller: webview.WebviewController = new webview.WebviewController();
responseWeb: WebResourceResponse = new WebResourceResponse();
heads: Header[] = new Array();
@State webData: string = "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
"<title>intercept test</title>\n" +
"</head>\n" +
"<body>\n" +
"<h1>intercept test</h1>\n" +
"</body>\n" +
"</html>";
aboutToAppear(): void {
try {
webview.WebviewController.setWebDebuggingAccess(true);
} catch (error) {
console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
}
}
build() {
Column() {
Web({ src: 'www.example.com', controller: this.controller })
.onControllerAttached(() => {
// 推荐在此loadUrl、设置自定义用户代理、注入JS对象等
console.info('onControllerAttached execute')
})
.onLoadIntercept((event) => {
if (event) {
console.info('onLoadIntercept url:' + event.data.getRequestUrl())
console.info('url:' + event.data.getRequestUrl())
console.info('isMainFrame:' + event.data.isMainFrame())
console.info('isRedirect:' + event.data.isRedirect())
console.info('isRequestGesture:' + event.data.isRequestGesture())
}
// 返回true表示阻止此次加载,否则允许此次加载
return false;
})
.onOverrideUrlLoading((webResourceRequest: WebResourceRequest) => {
if (webResourceRequest && webResourceRequest.getRequestUrl() == "about:blank") {
return true;
}
return false;
})
.onInterceptRequest((event) => {
if (event) {
console.info('url:' + event.request.getRequestUrl());
}
let head1: Header = {
headerKey: "Connection",
headerValue: "keep-alive"
}
let head2: Header = {
headerKey: "Cache-Control",
headerValue: "no-cache"
}
// 将新元素追加到数组的末尾,并返回数组的新长度。
let length = this.heads.push(head1);
length = this.heads.push(head2);
console.info('The response header result length is :' + length);
this.responseWeb.setResponseHeader(this.heads);
this.responseWeb.setResponseData(this.webData);
this.responseWeb.setResponseEncoding('utf-8');
this.responseWeb.setResponseMimeType('text/html');
this.responseWeb.setResponseCode(200);
this.responseWeb.setReasonMessage('OK');
// 返回响应数据则按照响应数据加载,无响应数据则返回null表示按照原来的方式加载
return this.responseWeb;
})
.onPageBegin((event) => {
if (event) {
console.info('onPageBegin url:' + event.url);
}
})
.onFirstContentfulPaint(event => {
if (event) {
console.info("onFirstContentfulPaint:" + "[navigationStartTick]:" +
event.navigationStartTick + ", [firstContentfulPaintMs]:" +
event.firstContentfulPaintMs);
}
})
.onProgressChange((event) => {
if (event) {
console.info('newProgress:' + event.newProgress);
}
})
.onPageEnd((event) => {
// 推荐在此事件中执行JavaScript脚本
if (event) {
console.info('onPageEnd url:' + event.url);
}
})
.onPageVisible((event) => {
console.info('onPageVisible url:' + event.url);
})
.onRenderExited((event) => {
if (event) {
console.info('onRenderExited reason:' + event.renderExitReason);
}
})
.onDisAppear(() => {
this.getUIContext().getPromptAction().showToast({
message: 'The web is hidden',
duration: 2000
})
})
}
}
}
异常监听
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebComponent {
needReloadWhenVisible: boolean = false ; // Web组件不可见时render退出后阻止重新加载页面,在可见时重新加载页面。
webIsVisible: boolean = false; // 判断Web组件是否可见。
// 此处是将子进程异常崩溃和其它异常原因做了区分,应用开发者可根据实际业务特点,细化对应异常的处理策略。
renderReloadMaxForCrashed: number = 5; // 设置因为异常崩溃后重新加载的最大重试次数,应用可根据业务特点,自行设置试错上限。
renderReloadCountForCrashed: number = 0; // 异常崩溃后重新加载的次数。
renderReloadMaxForOthers: number = 10; // 设置因为其它异常原因退出的最大重试次数,应用可根据业务特点,自行设置试错上限。
renderReloadCountForOthers: number = 0; // 其它异常原因退出后重新加载的次数。
// 创建Web组件。
controller: webview.WebviewController = new webview.WebviewController();
// 指定加载的页面。
url: string = "www.example.com";
build() {
Column() {
Web({ src: this.url, controller: this.controller })
.onVisibleAreaChange([0, 1.0], (isVisible) => {
this.webIsVisible = isVisible;
if (isVisible && this.needReloadWhenVisible) { // Web组件可见时重新加载页面。
this.needReloadWhenVisible = false;
this.controller.loadUrl(this.url);
}
})
// 应用监听渲染子进程异常退出回调,并进行异常处理。
.onRenderExited((event) => {
if (!event) {
return;
}
if (event.renderExitReason == RenderExitReason.ProcessCrashed) {
if (this.renderReloadCountForCrashed >= this.renderReloadMaxForCrashed) {
// 设置重试次数上限保护,避免必现问题导致页面被循环加载。
return;
}
console.info('renderReloadCountForCrashed: ' + this.renderReloadCountForCrashed);
this.renderReloadCountForCrashed++;
} else {
if (this.renderReloadCountForOthers >= this.renderReloadMaxForOthers) {
// 设置重试次数上限保护, 避免必现问题导致页面被循环加载。
return;
}
console.info('renderReloadCountForOthers: ' + this.renderReloadCountForOthers);
this.renderReloadCountForOthers++;
}
if (this.webIsVisible) {
// Web组件可见则立即重新加载。
this.controller.loadUrl(this.url);
return;
}
// Web组件不可见时不立即重新加载。
this.needReloadWhenVisible = true;
})
}
}
}
端侧调页面方法
应用侧可以通过runJavaScript()方法调用前端页面的JavaScript相关函数。参数为string类型,如果要传递ArrayBuffer,使用runJavaScriptExt()
前端页面
// 有参函数。
var param = "param: JavaScript Hello World!";
function htmlTestParam(param) {
document.getElementById('text').style.color = 'green';
console.log(param);
}
// 无参函数。
function htmlTest() {
document.getElementById('text').style.color = 'yellow';
}
// 假定端侧会写入一个changeColor方法
function callArkTS() {
changeColor();
}
function recharge() {
if(amount === null) {
return '请选择充值金额';
}
return document.getElementById('phone').value.length === 13 ? '充值成功' : '请输入正确的手机号';
}
ArkUI
// xxx.ets
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
aboutToAppear() {
// 配置Web开启调试模式
webview.WebviewController.setWebDebuggingAccess(true);
}
build() {
Column() {
Button('runJavaScriptParam')
.onClick(() => {
// 调用前端页面有参函数。
this.webviewController.runJavaScript('htmlTestParam(param)');
})
Button('runJavaScript')
.onClick(() => {
// 调用前端页面无参函数。
// this.webviewController.runJavaScript('htmlTest()');
this.webviewController.runJavaScript('recharge()')
.then((result) => {
promptAction.showToast({ message: result.replaceAll('"', '') });
});
})
Button('runJavaScriptCodePassed')
.onClick(() => {
// 传递runJavaScript侧代码方法。
this.webviewController.runJavaScript(`function changeColor(){document.getElementById('text').style.color = 'red'}`);
})
Web({ src: $rawfile('index.html'), controller: this.webviewController })
}
}
}
注意,上例中特别演示了有返回值的话,是用promise接的
页面调端侧方法
两个途径
- 调用Web组件的
javaScriptProxy()接口 - 在加载controller的事件里注册:
registerJavaScriptProxy()- 也可以提供一个注册按钮手动注册
注册了方法要
this.webviewController.refresh()一下, 但.javaScriptProxy()试注册的不需要
方法1:
// xxx.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
class TestClass {
constructor() {
}
test(): string {
return 'ArkTS Hello World!';
}
}
@Entry
@Component
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
// 声明需要注册的对象
@State testObj: TestClass = new TestClass();
build() {
Column() {
Button('deleteJavaScriptRegister')
.onClick(() => {
try {
this.webviewController.deleteJavaScriptRegister("testObjName");
this.webviewController.refresh();
} catch (error) {
console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
}
})
// Web组件加载本地index.html页面
Web({ src: $rawfile('index.html'), controller: this.webviewController})
// 将对象注入到web端
.javaScriptProxy({
controller: this.webviewController,
object: this.testObj, // 加载的对象
name: "testObjName", // 对前端暴露的名字
methodList: ["test"], // 方法列表
// 可选参数
asyncMethodList: [],
permission: '{"javascriptProxyPermission":{"urlPermissionList":[{"scheme":"resource","host":"rawfile","port":"","path":""},' +
'{"scheme":"e","host":"f","port":"g","path":"h"}],"methodList":[{"methodName":"test","urlPermissionList":' +
'[{"scheme":"https","host":"xxx.com","port":"","path":""},{"scheme":"resource","host":"rawfile","port":"","path":""}]},' +
'{"methodName":"test11","urlPermissionList":[{"scheme":"q","host":"r","port":"","path":"t"},' +
'{"scheme":"u","host":"v","port":"","path":""}]}]}}'
})
}
}
}
方法2:
this.webviewController.registerJavaScriptProxy(this.testObj, "testObjName", ["test", "toString"],[],)
// 两种注册方法的移除API是一样的
this.webviewController.deleteJavaScriptRegister("testObjName");
触发:
<button type="button" onclick="callArkTS()">Click Me!</button>
<script>
function callArkTS() {
let str = testObjName.test();
console.info('ArkTS Hello World! :' + str);
}
</script>
如果代码里只写了一个方法,注册的时候仍然要包成一个对象传进去: 比如这里就把
chooseContact包到了一个对象里。this.webviewController.registerJavaScriptProxy({ call: this.chooseContact }, 'jsbridgeHandle', ['call']);
复杂对象传递
- 有关permission相关请查阅文档
- Array/实体类 也可以作为注册对象方法的参数或返回值,在应用侧和前端页面之间传递。
- 如果传递的是类对象,它不包含任何在 ArkTS 类上定义的方法。它仅仅是数据的载体,实现了从 ArkTS 到 JS 的数据序列化/反序列化过程. 或者说,js接到的时候已经成了一个普通的javascript对象
- 在端侧接回来的时候要用
ESObject来接,可以理解为是any
- function也可以传递,但不能作为返回值,只能作为参数传递
传递Function的例子:
// 假定有个方法需要接收一个function作为参数
class TestClass {
constructor() {
}
test(param: Function): void {
param("call callback");
}
toString(param: String): void {
console.info('Web Component toString' + param);
}
}
// 正常注册
@State testObj: TestClass = new TestClass();
this.webviewController.registerJavaScriptProxy(this.testObj, "testObjName", ["test", "toString"]);
// 页面上这么用
testObjName.test(function(param){testObjName.toString(param)});
因为test方法接收的是一个function,所以前端页面在调用的时候传递的那个function里的代码是能在端侧执行的,这里一定要注意,比如上例中,便是执行了端侧的toString方法。但是这里面其实有个隐藏的转换,因为你是用testObjName来回传的,说明系统自动帮你转回了ArkTs对应的testObj了
上面讲的是传递复杂类型,即使是class和function,都是传递过程中的,如果我们要调用的是跨端的对象里的方法呢?需要配置methodNameListForJsProxy:
端侧调前端对象里的方法
// index.html
class Student {
constructor(nameList) {
this.methodNameListForJsProxy = nameList;
}
hello(param) {
testObjName.toString(param)
}
}
var st = new Student(["hello"])
// 或者换种写法:
function Obj1(){
this.methodNameListForJsProxy=["hello"]; // 给个默认值
this.hello=function(param){
testObjName.toString(param)
};
}
var st1 = Obj1()
// 使用
testObjName.test(st); // 期望这个st里是有一个能被发现的hello方法的
// xxxx.ets
test(param: ESObject): void {
param.hello("call obj func"); // 端侧调传入的对象里的hello方法
}
前端调端侧对象里的方法
这里我们需要调用的是ObjOther类里的testOther方法,跟上面一样,目标class里增加methodNameListForJsProxy属性并提供构造函数,然后注册:
class ObjOther {
methodNameListForJsProxy: string[]
constructor(list: string[]) {
this.methodNameListForJsProxy = list
}
testOther(json: string): void {
console.info(json)
}
}
class TestClass {
ObjReturn: ObjOther
constructor() {
// 注册methodNameListForJsProxy
this.ObjReturn = new ObjOther(["testOther"]);
}
test(): ESObject {
return this.ObjReturn
}
toString(param: string): void {
console.info('Web Component toString' + param);
}
}
// index.html
// 前端使用:
testObjName.test().testOther("call other object func");
以上两种需要维护
methodNameListForJsProxy表,有个硬编码的过程,很容易出错。事实上,你是把对象跨语言传递过去了,然后希望调用这个对象里的方法才需要这么做,而我们只是想调到这个方法的话,完全可以让调用发生在本端,比如让arkts自行调用testOther方法,而不是把ObjOther对象返给前端让前端自行调用,这样就大大简化了,也不需要维护这个表了。
Promise
如果端侧的方法是异步的(比如setTimeout),前端怎么处理呢?用Promise来包装一下。
test(): Promise<string> {
let p: Promise<string> = new Promise((resolve, reject) => {
setTimeout(() => {
console.info('执行完成');
reject('fail'); // resolve('success')也一样
}, 10000);
});
return p;
}
// index.html
testObjName.test()
.then((param)=>{testObjName.toString(param)})
.catch((param)=>{testObjName.toString(param)})
注意,用Web组件的
.javaScriptProxy注册方法,需要区别asyncMethodList和methodList,即同步还是异步,错误注册会出问题。但是用controller的.registerJavaScriptProxy注册方法是不区分的。
还有一种用法比较复杂,需要前端自行new Promise,具体查看文档。对于不能动端侧代码的场景,需要前端来自行处理。
调试
Backlinks