Web组件

生命周期

这个需要额外了解下,因为往往需要进行一些方法注入,拦截请求等操作:

  • 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也可以传递,但不能作为返回值,只能作为参数传递
{topage:ArkTS实体类JavaScript对象frompage:JavaScript对象EsObject\begin{cases} \tt \small to page: ArkTS实体类 \rightarrow JavaScript对象 \\ \tt \small from page: JavaScript对象 \rightarrow EsObject \end{cases}

传递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注册方法,需要区别asyncMethodListmethodList,即同步还是异步,错误注册会出问题。但是用controller的.registerJavaScriptProxy注册方法是不区分的。

还有一种用法比较复杂,需要前端自行new Promise,具体查看文档。对于不能动端侧代码的场景,需要前端来自行处理。

调试

查看文档


Backlinks