查看原文
其他

HarmonyOS鸿蒙应用开发,原生与H5通信框架

菜鸟同学vlog 郭霖
2024-07-22


/   今日科技快讯   /

近日,工业和信息化部今天向中国电信、中国移动、中国联通颁发许可,批复在广西南宁、山东青岛、云南昆明、海南海口设立国际通信业务出入口局。

今天,广西南宁、山东青岛、云南昆明、海南海口新增设6个国际通信业务出入口局,这是30年来首次增设,建成后将显著提升国际网络通信能力,更好推动基础设施互联互通、数据跨境流动和国际数字贸易发展,促进更高水平开放,为构建新发展格局塑造新动能新优势。

/   作者简介   /

本篇文章来自菜鸟同学vlog的投稿,文章主要分享了如何开发鸿蒙中原生与 H5 通信的框架,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

菜鸟同学vlog的博客地址:
https://juejin.cn/user/782508007891048/posts

/   介绍   /

HarmonyOS版的DSBridge,通过本库可以在鸿蒙原生与JavaScript完成交互,可以相互调用彼此的功能。

目前兼容Android、iOS第三方DSBridge库的核心功能,基本保持原来的使用方式,后续会持续迭代保持与Android库相同的功能,减少前端和客户端的适配工作。

特性:

  • 已适配鸿蒙NEXT版本;
  • 原生同步方法内支持串行并发异步任务,同步等待异步结果,是根据鸿蒙特点设计的需求;
  • 支持以类的方式集中统一管理API;
  • 支持同步和异步调用;
  • 支持进度回调/回传:一次调用,多次返回;
  • 支持API是否存在检测;
  • 支持Javascript关闭页面的监听与拦截,
  • 支持命名空间API。

/   正文   /

安装

安装库

ohpm install @hzw/ohos-dsbridge

或者安装本地har包

ohpm install ../libs/library.har

使用

Native

1、在原生新建一个类JsBridge,实现业务API , 通过类来集中统一管理API,方法用@JavaScriptInterface()标注,是不是很眼熟呢,加一个@JavaScriptInterface()标注主要为了使用规范,是自定义的装饰器,与Android保持一致性。

export class JsBridge{
  private cHandler: CompleteHandler = null

  /**
   * 同步
   * @param p
   * @returns
   */
  @JavaScriptInterface(false)
  testSync(p: string): string {
    LogUtils.d("testSync: " + JSON.stringify(p))
    return "hello native"
  }

  /**
   * 异步
   * @param p
   * @param handler
   */
  @JavaScriptInterface()
  testAsync(p: string, handler: CompleteHandler) {
    LogUtils.d("testAsync: " + JSON.stringify(p))
    this.cHandler = handler
  }
}

原生同步方法不支持用async/await声明,如果需要在同步方法内执行异步任务,可以使用taskWait()函数来加持完成,下面会介绍基本用法;异步方法的形参CompleteHandler,可用于结果异步回调。

2、在原生Web组件初始化时,通过WebViewControllerProxy类来获取WebviewController实例来实现JS注入,然后将其关联到Web组件中,接着将API管理类(JsBridge)关联到WebViewControllerProxy中。

private controller: WebViewControllerProxy = WebViewControllerProxy.createController()

aboutToAppear() {
  this.controller.addJavascriptObject(new JsBridge())
}


Web({ src: this.localPath, controller: this.controller.getWebViewController() })
  .javaScriptAccess(true)
  .javaScriptProxy(this.controller.getJavaScriptProxy())
  .onAlert((event) => {
    // AlertDialog.show({ message: event.message })
    return false
  })

)

3、在JavaScript中通过dsBridge对象调用原生API,第一个参数是原生方法名称,第二参数是原生方法接收的参数,异步方法有第三个参数是回调函数,会接收CompleteHandler异步回调结果。

// 同步
let msg = dsBridge.call('testSync', JSON.stringify({data: 100}))

// 异步
dsBridge.call('testAsync', JSON.stringify({data: 200}), (msg) => {
  updateMsg(msg)
})

JavaScript

1、在JavaScript中初始化dsBridge,通过cdn或者npm安装都可以。

如果项目没有历史包袱,建议直接用m-dsbridge包。

npm i m-dsbridge
// 或者cdn引入
<script src="https://cdn.jsdelivr.net/npm/m-dsbridge/dsBridge.js"></script>

也支持直接用原Android或iOS的DSBridge库的JS脚本。

https://cdn.jsdelivr.net/npm/dsbridge/dist/dsbridge.js

2、通过dsBridge对象注册Js函数,供原生调用。

// 注册同步函数
dsBridge.register('showAlert', function (a, b, c) {
        // return "原生调用JS showAlert函数"
         alert("原生调用JS showAlert函数" + a + " " + b + " " + c)
        return true
    })

// 注册异步函数
dsBridge.registerAsyn('showAlertAsync', function (a, b, c, callback) {
  let counter = 0
  let id = setInterval(() => {
    if (counter < 5) {
      callback(counter, false)
      alert("原生调用JS showAlertAsync函数" + a + " " + b + " " + c + " " + counter)
      counter++
    } else {
      callback(counter, true)
      alert("原生调用JS showAlertAsync函数" + a + " " + b + " " + c + " " + counter)
      clearInterval(id)
    }
  }, 1000)

})

其中异步的callback函数,如果最后一个参数返回true则完成整个链接的调用,false则可以一直回调给原生,这个就是JavaScript端的一次调用,多次返回。比如需要将JavaScript端进度数据不间断同步到原生,这时就可以派上用场了。

3、原生通过WebViewControllerProxy实例来调用JavaScript所注册的函数。

Button("调用js函数-同步")
        .onClick(() => {
          this.controller.callJs("showAlert", [1, 2, '666'], (v) => {
            this.msg = v + ""
          })
        })

      Button("调用js函数-异步")
        .onClick(() => {
          this.controller.callJs("showAlertAsync", [1, 2, '666'], (v) => {
            this.msg = v + ""
          })
        })
    }

callJs()方法有三个形参,第一个是Js注册的函数名称,第二个是Js接收函数的参数,是一个数组类型,第三个是监听Js函数返回结果的函数。另外也提供了与Android库一样调用函数callHandler()。

进度回调(一次调用,多次返回)

前面提到了JavaScript端的一次调用,多次回调的情况,在原生端也是支持的,还是有应用场景的,比如将原生的下载进度实时同步到js中,可以通过CompleteHandler#setProgressData()方法来实现。

  @JavaScriptInterface()
  testAsync(p: string, handler: CompleteHandler) {
    LogUtils.d("testAsync: " + JSON.stringify(p))
    this.cHandler = handler
    let counter = 0
    setInterval(() => {
      if (counter < 5) {
        counter++
        handler.setProgressData("异步返回的数据--" + counter)
      } else {
        this.cHandler.complete("异步返回的数据--结束")
        this.cHandler.complete("异步返回的数据--结束2")
      }
    }, 1000)

JavaScript:

dsBridge.call('testAsync', JSON.stringify({data: 200}), (msg) => {
  updateMsg(msg)
})

监听或拦截Javascript关闭页面

Js调用close()函数可以关闭当前页面,原生可以设置监听观察是否拦截。

  aboutToAppear() {
    this.controller.setClosePageListener(() => {
      return true; // false 会拦截关闭页面
    })
  }

在回调函数中如果返回false,会拦截掉关闭页面的事件。

销毁结束任务

如果异步任务还在执行中,比如setProgressData,此时关闭页面返回就会闪退,为了避免这种情形,建议在组件的生命周期函数aboutToDisappear()中结束任务。

  aboutToDisappear(){
    this.jsBridge.destroy()
  }

命名空间

命名空间可以帮助你更好的管理API,这在API数量多的时候非常实用,支持你通过命名空间将API分类管理,不同级之间只需用'.' 分隔即可。支持同步与异步方式使用。

原生API命令空间

原生用WebViewControllerProxy#addJavascriptObject 指定一个命名空间名称:

this.controller.addJavascriptObject(new JsBridgeNamespace(), "namespace")

在JavaScript中,用命名空间名称.对应的原生函数。

    const callNative6 = () => {
        let msg =  dsBridge.call('namespace.testSync',{msg:'来自js命名空间的数据'})
        updateMsg(msg)
    }

      const callNative7 = () => {
          dsBridge.call('namespace.testAsync', 'test', (msg) => {
            updateMsg(msg)
        })
    }

JavaScript API命令空间

用dsBridge对象注册js函数的命名空间。

// namespace
dsBridge.register('sync', {
    test: function (a, b) {
        return "namespace:  " + (a + b)
    }
})

dsBridge.registerAsyn("asny",{
    test: function (a,b ,callback) {
        callback("namespace:  " + (a + b))
    }
})

第一个参数命名空间的名称,比如sync,第二个参数是API业务对象实例,支持字面量对象和Class类实例。

在原生调用方式:

this.controller.callJs("sync.test", [1, 2], (value: string) => {
  this.msg = value
})

this.controller.callJs("asny.test", [3, 2], (value: string) => {
  this.msg = value
})

原生同步方法内执行串行异步并发任务

原生同步方法:

  /**
   * 同步模版
   * @param p
   * @returns
   */
  @JavaScriptInterface(false)
  testSync(p: string): string {
    LogUtils.d("testSync: " + JSON.stringify(p))
    return "原生同步testSync方法返回的数据"
  }

如果要在同步方法内执行异步任务,并将异步结果立即返回给h5,上面的设计显然是无法满足需求的;在鸿蒙中异步任务基本与Promise和async/await有关联,然而桥接函数是不支持使用async/await声明(这点为了考虑兼容Android/iOS项目而因此设计)。

对此,设计了一个taskWait()函数来满足上述的需求,可以通过taskWait()函数在主线程同步方法内执行串行异步并发任务,主线程会同步等待异步结果。

  /**
   * 同步方法中执行异步并发任务
   * @param args
   * @returns
   */
  @JavaScriptInterface(false)
  testTaskWait(args: string): number {
    let p = new Param(100, 200)
    let p1 = new Param(100, 300)
    taskWait(p)
    p1.tag = "Param"
    taskWait(p1)
    LogUtils.d(`testTaskWait sum: ${p.sum}  ${p1.sum}`)
    return p.sum + p1.sum
  }

其中Param类需要继承BaseSendable,同时用@Sendable装饰器声明,任务放在run()方法中执行。

@Sendable
export class Param  extends BaseSendable{
  private a : number = 0
  private b : number = 0
  public sum: number = 0
  public enableLog: boolean = true

  constructor(a: number, b: number) {
    super();
    this.a = a;
    this.b = b;
  }
  // 异步任务执行
  async run(): Promise<void> {
    this.sum =  await this.add()
  }

  async add(){
    return this.a + this.b
  }
}

taskWait()函数是一个轻量级的同步等待函数,不建议执行耗时过长的任务,如果在3s内没有完成任务,会自动结束等待将结果返回,可能会存在数据丢失的情况;对于特别耗时的任务建议使用异步桥接函数。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Android分屏功能原理(基于Android12L)
使用 ML Kit 文档扫描器 API 轻松将文档扫描功能添加至您的应用

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存