鸿蒙Web与JSBridge使用

1,475 阅读3分钟

鸿蒙Web与JSBridge使用

前言

最近好久好久没写文章了,又谈了一场失败的恋爱,又是帮请假一个月的同事干活,又是搞鸿蒙,学习写文章的激情下降的比较厉害,这几天打算收一收,写点鸿蒙的文章,做个记录吧。

我这也不想写鸿蒙的教程,只是把开发过程中我觉得有必有记录的东西写一写,而且基本上算新手,内容也比较讲的。

这篇文章就来写下Web与JSBridge使用,因为我们这APP就是套壳H5的。

官方文档

首先还是看官方文章吧,网上关于鸿蒙的文章还是比较少,尽量看API吧,虽然也要授权才能看:

Webview API

Webview的使用

下面就用个最简单的例子看下鸿蒙Webview的使用:

// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
  controller: web_webview.WebviewController = new web_webview.WebviewController();

  build() {
    Column() {
      // 组件创建时,通过$rawfile加载本地文件local.html
      Web({ src: $rawfile("local.html"), controller: this.controller })
        .onPageEnd((event) => {
          if (event) {

          }
        })
        .domStorageAccess(true)
        .javaScriptAccess(true)
        .fileAccess(true)
        .geolocationAccess(true)
        .domStorageAccess(true)
        .onlineImageAccess(true)
    }
  }
}

这里就是一个Web组件加载本地html,"local.html"放在resources的rawfile目录下。

Web组件还需要传入一个WebviewController,它负责处理Web组件的一些功能,Web组件还可以直接设置WebAttribute,类似安卓的WebSettings吧。这里好像没有WebViewClient和WebChromeClient那样的东西。

JSBridge使用

注册JSBridge

这里JSBridge有两种写法,一个是在Web组件上设置:

// xxx.ets
import web_webview from '@ohos.web.webview';

class testClass {
  constructor() {
  }

  test(): string {
    return 'ArkTS Hello World!';
  }
}

@Entry
@Component
struct WebComponent {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();
  // 声明需要注册的对象
  @State testObj: testClass = new testClass();

  build() {
    Column() {
      // web组件加载本地index.html页面
      Web({ src: $rawfile('index.html'), controller: this.webviewController})
        // 将对象注入到web端
        .javaScriptProxy({
          object: this.testObj,
          name: "testObjName",
          methodList: ["test"],
          controller: this.webviewController
        })
    }
  }
}

另一个就是通过WebviewController设置,下面丢一段官方例子吧:

// xxx.ets
import web_webview from '@ohos.web.webview';
import business_error from '@ohos.base';

class testClass {
  constructor() {
  }

  test(): string {
    return "ArkUI Web Component";
  }

  toString(): void {
    console.log('Web Component toString');
  }
}

@Entry
@Component
struct Index {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();
  @State testObj: testClass = new testClass();

  build() {
    Column() {
      Button('refresh')
        .onClick(() => {
          try {
            this.webviewController.refresh();
          } catch (error) {
            let e: business_error.BusinessError = error as business_error.BusinessError;
            console.error(`ErrorCode: ${e.code},  Message: ${e.message}`);
          }
        })
      Button('Register JavaScript To Window')
        .onClick(() => {
          try {
            this.webviewController.registerJavaScriptProxy(this.testObj, "testObjName", ["test", "toString"]);
          } catch (error) {
            let e: business_error.BusinessError = error as business_error.BusinessError;
            console.error(`ErrorCode: ${e.code},  Message: ${e.message}`);
          }
        })
      Web({ src: $rawfile('index.html'), controller: this.webviewController })
    }
  }
}

区别就是Web组件上只能注册一个JSBridge,而webviewController上可以注册多个,不过有个坑就是:

使用registerJavaScriptProxy()接口注册方法时,注册后需调用refresh()接口生效。

这个就很迷,所以我选择了简单又方便的Web组件注册JSBridge。

方法调用

这里建议看下这个官方文档吧,里面写的挺清楚的:

前端页面调用应用侧函数

我这一般就用两种形式的方法调用,一种是返回Promise,一种是传入回调,这样写可以在方法里面做一些耗时操作,有兴趣的可以参考下:

class UtilsBridge {
    
  /** 页面上下文 **/
  context: Context = null!
  
  // 单次调用示例
  singleCall(args: Array<string | number | boolean | object>): Promise<string> {
    return new Promise((resolve, reject) => {
      try {
        let str: string = args.get(0) as string;
         
      } catch (e) {
        LogUtil.e(e)
        reject(e)
      }
    });
  }

  // 多次调用示例
  multiCall(args: Array<string | number | boolean | object>, resolve: Function, reject: Function) {

  }
}

export default UtilsBridge

在html里面调用:

function singleCall() {
    harmony.singleCall([  ]).then(res => {
        
    }).catch(error => {
        
    });
}

function multiCall() {
    harmony.startPcmRecord(["test"], function(param){
       
    },function(param){
        
    });
}

如果JSBridge的方法需要用到context,可以通过下面方式传递进去(不知道会不会内存泄漏-_-||):

  Web({ src: $rawfile('index.html'), controller: this.webviewController})
    .onPageEnd((event) => {
      if (event) {

        // 向UtilsBridge传入context
        this.utilsBridge.context = getContext(this)
      }
    })

Message传递

除了JSBridge外,我这常用的还有就是Message传递了,html可以通过注册listener来监听原生消息,下面封装了一个类:

import { BusinessError } from '@kit.BasicServicesKit';
import { webview } from '@kit.ArkWeb';
import LogUtil from '../utils/LogUtil'

/**
 * 消息通知类,向H5发送message
 *
 * @author lfq 2024-06-24
 */
class MessageBridge {
  constructor() {}

  // 和H5通信的Ports
  private ports: webview.WebMessagePort[] = null!

  /**
   * 创建和H5通信的Ports
   *
   * @param controller webview控制器
   */
  createPorts(controller: webview.WebviewController) {
    try {
      // 1、创建两个消息端口。
      let ports = controller.createWebMessagePorts();
      this.ports = ports
      // 2、在应用侧的消息端口(如端口1)上注册回调事件。
      ports[1].onMessageEvent((result: webview.WebMessage) => {
        if (typeof (result) == "string") {
          LogUtil.d("onMessageEvent:" + result);
        } else {
          LogUtil.d("not support");
        }
      })
      // 3、将另一个消息端口(如端口0)发送到HTML侧,由HTML侧保存并使用。
      controller.postMessage('__init_port__', [ports[0]], '*');
      // 4、使用应用侧的端口给另一个已经发送到html的端口发送消息。
      // ports[1].postMessageEvent('onDeviceReady');
    } catch (error) {
      LogUtil.e(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
    }
  }

  /**
   * 向webview发送onDeviceReady事件
   */
  onDeviceReady() {
    if (this.ports && this.ports[1]) {
      this.ports[1].postMessageEvent('onDeviceReady');
    }
  }

  /**
   * 向webview发送resume事件
   */
  resume() {
    if (this.ports && this.ports[1]) {
      this.ports[1].postMessageEvent('resume');
    }
  }

  /**
   * 向webview发送pause事件
   */
  pause() {
    if (this.ports && this.ports[1]) {
      this.ports[1].postMessageEvent('pause');
    }
  }

  /**
   * 向webview发送online事件
   */
  online() {
    if (this.ports && this.ports[1]) {
      this.ports[1].postMessageEvent('online');
    }
  }

  /**
   * 向webview发送offline事件
   */
  offline() {
    if (this.ports && this.ports[1]) {
      this.ports[1].postMessageEvent('offline');
    }
  }

  /**
   * 主动调用,释放资源
   */
  onDestroy() {

  }
}

export default MessageBridge;

我这给H5发送了网页加载完成、页面显示、隐藏、网络联网、断网几个消息,还是挺有用的,看下Index里的写法:

// xxx.ets
import web_webview from '@ohos.web.webview';

class testClass {
  constructor() {
  }

  test(): string {
    return 'ArkTS Hello World!';
  }
}

@Entry
@Component
struct Index {
  controller = new webView.WebviewController();
  @State messageBridge: MessageBridge = new MessageBridge();
  @State utilsBridge: UtilsBridge = new UtilsBridge();
  currentNet = connection.createNetConnection()

  // 页面即将显示时调用
  onPageShow() {
    this.messageBridge.resume()
  }

  // 页面即将消失时调用
  onPageHide() {
    this.messageBridge.pause()
  }
  
  build() {
    Column() {
      // web组件加载本地index.html页面
      Web({ src: $rawfile('index.html'), controller: this.webviewController})
        .onPageEnd((event) => {
          if (event) {
            // 发送onDeviceReady事件
            LogUtil.d('onPageEnd:' + event.url);
            this.messageBridge.createPorts(this.controller)
            this.messageBridge.onDeviceReady()

            // 向UtilsBridge传入context
            this.utilsBridge.context = getContext(this)

            // 开启网络监听
            this.startListenNetwork()
          }
        })
    }
  }
  
  // 启动网络监听
  private startListenNetwork() {
    this.currentNet.register((error) => {
      LogUtil.d(error ? "开启监听失败" : "开启监听成功")
    })

    this.currentNet.on("netAvailable", () => {
      LogUtil.d("netAvailable")
      this.messageBridge.online()
      this.utilsBridge.netWorkState = 0
    })

    this.currentNet.on("netLost", () => {
      LogUtil.d("netLost")
      this.messageBridge.offline()
      this.utilsBridge.netWorkState = 1
    })

    // 实测不生效
    this.currentNet.on("netUnavailable", () => {
      LogUtil.d("netUnavailable")
      this.messageBridge.offline()
      this.utilsBridge.netWorkState = 1
    })
  }
}

这样就能在html里面监听了:

//test.js
var h5Port;
var output = document.querySelector('.output');
window.addEventListener('message', function (event) {
    if (event.data == '__init_port__') {
        if (event.ports[0] != null) {
            h5Port = event.ports[0];
            // 1. 保存从ets侧发送过来的端口
            h5Port.onmessage = function (event) {
                // 2. 接收ets侧发送过来的消息.
                var msg = '';
                var result = event.data;
                if (typeof(result) == "string") {
                    msg = msg + result;
                } else {
                    console.log("not support");
                }
                output.innerHTML = output.innerHTML + msg + '<Br/>';
            }
        }
    }
})

// 3. 使用h5Port往ets侧发送消息.
function PostMsgToEts(data) {
    if (h5Port) {
        h5Port.postMessage(data);
    } else {
        console.error("h5Port is null, Please initialize first");
    }
}

比较坑的一点就是addEventListener不能直接监听我们发送的"online"、"offline"这样的消息,还得h5Port.onmessage处理下。

小结

简单整理了下鸿蒙里面Web的使用、JSBridge使用以及postMessage的使用,比较初级,算是记录吧。