优秀的编程知识分享平台

网站首页 > 技术文章 正文

【HarmonyOS Next】dsbridge的跨端交互

nanyue 2025-03-19 15:02:16 技术文章 4 ℃
  • 本文所有代码案例均基于Api12。

对于已经上线的项目,和前端的JS交互往往成为兼容鸿蒙端的绊脚石。如果已经依赖了三方框架,好多人选择等已经使用的库推出支持鸿蒙端的方案再进入开发。

dsbridge作为一个轻量化的交互库,粉丝不少,我们在开发初期也遇到了让web端兼容的难题,现有的交互重度依赖dsbridge,让web端交互无缝过渡到鸿蒙成了一个关键节点。

经过研究,我们决定重写一个鸿蒙版本。我维护过两年手机浏览器项目,承担了这项工作的主要任务。

任务分成三个阶段:

1、dsbridge库改写成Ark TS版本

参照iOS版本转写成的鸿蒙代码,iOS版本的仓库在github上,直接clone到本地即可
转写后的代码会放在文末。

方案选择上,我选择了侵入式设计,让DsbridgeController继承了webview.WebviewController。也尝试过单独起一个控制器的方案,发现没必要。web组件已经提供了Controller的绑定,我们在实际项目中所有web都是一套的,也不会再单独给Controller添加别的功能,如果有特殊需要,再单独适配即可。

2、客户端已有的交互方法预埋

参照dsbridge自带的检测模型InternalApis,自己埋监听对象的时候,也一样,注入对象时,把"_dsb"换成自己的命名空间即可。

进入页面后,webController?.creat() 方法必须要调用,才可以注册自己实现的交互对象

aboutToAppear(): void {
    // 添加监听
    this.webController?.creat()
    this.webController?.addJavascriptObject(this.jsObject, "xxxx")
}

对象的类命名在本地可以任意,命名空间"xxxx"需要跟web端约定一致。

3、web获取鸿蒙端的识别标记

dsbridge分为web端和客户端,客户端已经接入dsbridge的消息,需要让web端识别到,查看web端源码,他们在UA中额外拼接了"_dsbridge"字段,客户端也要实现这个方案,在web组件绑定好Controller后,需要及时将UA更新:

    // web页面
    Web({ src: this.urlPath, controller: this.webController })
      .width(FULL_WIDTH)
      .height(FULL_HEIGHT)
      .onControllerAttached(() => {
        // 需要在这里注入UA
        this.webController?.insertWindowInfo()
      })

insertWindowInfo方法内容如下,这个方法写在文末的源码里

  /**
   * 给web追加UA的方法
   */
  insertWindowInfo() {
    // 获取UA信息
    let uaStr = this.getUserAgent()
    if (uaStr.includes(" _dsbridge")) {
      //如果已经包含,避免重复注入
      return
    }
    // 修改UA信息
    this.setCustomUserAgent(uaStr + " _dsbridge")
  }

还有一些额外要注意的,针对web容器,还需要额外补充如下代码:

    .onAppear(() => {
      if (this.webController != undefined) {
        this.webController.isBindUI = true
      }
    })
    .onDisAppear(() => {
      if (this.webController != undefined) {
        this.webController.isBindUI = false
      }
      // 销毁页面时需要清空timer,未清空可能会引起闪退
      this.webController?.clearTimer()
      this.webController = undefined
    })

这段代码是因为有timer延迟,会导致崩溃,额外加注的。


末尾:dsbridge类转写后的代码:

参照iOS版本转写成的鸿蒙代码

disableJavascriptDialogBlock因为没使用,所以就没实现

额外加了参数isBindUI和timerArray,加了方法clearTimer方法,离开页面后,dsbridgeController对象因为有延迟回调,加了这个处理方案。

dsbridge主要是用来数据交互,非代码注入工具,我们在项目中发生过严重卡顿,有一个交互方法通过dsbridge,向web注入长达30M的JSON,卡顿数秒,最后排查只能拆分处理。

import webview from '@ohos.web.webview'

export enum DSB_API {
  HASNATIVEMETHOD,
  CLOSEPAGE,
  RETURNVALUE,
  DSINIT,
  DISABLESAFETYALERTBOX
}

export class DSCallInfo {
  method?: string
  id?: number
  args?: Object[]
}

export class JSBUtil {
  static parseNamespace(method: string): string[] {
    let arr = method.split(".")
    if (arr.length > 1) {
      return [arr[0], arr[1]]
    } else {
      return ["", method]
    }
  }
}

// 注入的元交互模型
class InternalApis {
  webController?: DsbridgeController

  /**
   *  某个方法是否可执行
   * @param args
   * @returns
   */
  hasNativeMethod(args: Record): Object | undefined {
    return this.webController?.onMessage(args, DSB_API.HASNATIVEMETHOD) ?? undefined
  }

  /**
   * 关闭页面闭包,愿意的话自己实现闭包,不实现也没关系
   * @param args 只有 disable一个参数 bool类型
   * @returns
   */
  closePage(args: Record): Object | undefined {
    return this.webController?.onMessage(args, DSB_API.CLOSEPAGE) ?? undefined
  }

  /**
   * 通用的交互,包括调用某对象的方法,传参,返回结果等
   * @param args
   * @returns
   */
  returnValue(args: Record): Object | undefined {
    return this.webController?.onMessage(args, DSB_API.RETURNVALUE) ?? undefined
  }

  /**
   * 初始化完成后的交互,刚进入页面时会直接调用元模型的对象,然后触发客户端调用JS的未执行队列
   * @param args
   * @returns
   */
  dsinit(args: Record): Object | undefined {
    return this.webController?.onMessage(args, DSB_API.DSINIT) ?? undefined
  }

  /**
   * 弹窗模态设置,暂时不做拦截,web那边自己实现
   * @param args
   * @returns
   */
  disableJavascriptDialogBlock(args: Record): Object | undefined {
    return this.webController?.onMessage(args, DSB_API.DISABLESAFETYALERTBOX) ?? undefined
  }
}


export class DsbridgeController extends webview.WebviewController {
  // 持有关闭web窗口的实现闭包
  private javascriptCloseWindowListener: (() => void) | undefined = undefined
  // 回调闭包的自增key,方便查找
  private callId: number = 0
  // JS命名空间对象存储字典
  private javaScriptNamespaceInterfaces: Map = new Map()
  // 闭包的回调暂存数组
  private handerMap: Map = new Map()
  // 原生调用JS的模型暂存数组,在web加载初始化之前,需要先暂存到这里,初始化完再调用
  private callInfoList: DSCallInfo[] | undefined = []
  jsCache: string = ""
  lastCallTime: number = 0
  isPending: boolean = false
  // 存储timer的数组,离开页面时需要清空
  timerArray: number[] = []
  isBindUI: boolean = false

  /**
   * 离开页面时需要清空timer,未清空可能会引起闪退
   */
  clearTimer() {
    this.timerArray.forEach((timer) => {
      clearTimeout(timer)
    })
  }

  // 创建初始化交互
  creat() {
    // add internal Javascript Object
    let interalApis = new InternalApis()
    interalApis.webController = this
    this.addJavascriptObject(interalApis, "_dsb")
  }

  /**
   * 给web添加必要的注入信息
   */
  insertWindowInfo() {
    // 获取UA信息
    let uaStr = this.getUserAgent()
    if (uaStr.includes(" _dsbridge")) {
      //如果已经包含,避免重复注入
      return
    }
    // 修改UA信息
    this.setCustomUserAgent(uaStr + " _dsbridge")
  }

  //  原生调用js
  public callHandler(
    methodName: string,
    args?: Object[],
    completionHandler?: (value: Object) => void
  ) {
    // 将交互事件转换成model
    let callInfo: DSCallInfo = new DSCallInfo()
    callInfo.id = this.callId++
    callInfo.args = args == undefined ? [] : args
    callInfo.method = methodName

    if (completionHandler != undefined) {
      this.handerMap.set("" + callInfo.id, completionHandler)
    }

    if (this.callInfoList != undefined) {
      this.callInfoList.push(callInfo)
    } else {
      this.dispatchJavascriptCall(callInfo)
    }
  }

  // 设置JS调用关闭当前web的闭包
  public setJavascriptCloseWindowListener(callback: () => void) {
    this.javascriptCloseWindowListener = callback
  }

  /**
   * 添加命名空间的监听和对象
   * @param object
   * which implemented the javascript interfaces
   * @param namespace
   * if empty, the object have no namespace.
   **/
  public addJavascriptObject(object?: Object, namespace?: string) {
    if (namespace == undefined) {
      namespace = ""
    }
    if (object != undefined) {
      this.javaScriptNamespaceInterfaces.set(namespace, object)
    }
  }

  // 删除命名空间的监听
  public removeJavascriptObject(namespace: string) {
    if (namespace == undefined) {
      namespace = ""
    }
    this.javaScriptNamespaceInterfaces.delete(namespace)
  }

  // 检查JS方法是否可用
  public hasJavascriptMethod(handlerName: string, callback: (exist: boolean) => void) {
    this.callHandler(
      "_hasJavascriptMethod",
      [handlerName],
      (value: Object) => {
        callback(value > 0)
      }
    )
  }

  // 判断方法是否可用
  hasNativeMethod(args: Record): boolean {
    let argsname: string = args["name"] as string ?? ""
    let nameStr: string[] = JSBUtil.parseNamespace(argsname.trim())
    let JavascriptInterfaceObject: ESObject = undefined
    if (this.javaScriptNamespaceInterfaces.has(nameStr[0])) {
      JavascriptInterfaceObject = this.javaScriptNamespaceInterfaces.get(nameStr[0])
    }
    if (JavascriptInterfaceObject != undefined) {
      let funcname: string = nameStr[1] ?? ""
      let has: boolean = JavascriptInterfaceObject[funcname] != undefined
      return has
    }
    return false
  }

  // _dsb对象的消息转发方法
  public onMessage(msg: Record, type: DSB_API): Object | undefined {
    let ret: Object | undefined = undefined
    switch (type) {
      case DSB_API.HASNATIVEMETHOD:
        ret = this.hasNativeMethod(msg) ? 1 : 0
        break
      case DSB_API.CLOSEPAGE:
        this.closePage(msg)
        break
      case DSB_API.RETURNVALUE:
        ret = this.returnValue(msg)
        break
      case DSB_API.DSINIT:
        ret = this.dsinit(msg)
        break
      case DSB_API.DISABLESAFETYALERTBOX:
        let disable: boolean = msg["disable"] as boolean ?? false
        this.disableJavascriptDialogBlock(disable)
        break
      default:
        break
    }
    return ret
  }

  public disableJavascriptDialogBlock(disable: boolean) {
  }

  // 注册交互,并且把注册过的数组缓存清空
  dispatchStartupQueue() {
    if (this.callInfoList == undefined) {
      return;
    }
    for (let i = 0; i < this.callinfolist.length i this.dispatchjavascriptcallthis.callinfolisti this.callinfolist='undefined' _dsbridge=',包含就需要拦截,不包含就当成普通弹窗事件处理' canopendsbridgeprompt: string: boolean return prompt.includes_dsbridge=')   }    // JS调用原生解析的核心方法   call(prompt: string, argStr: string): string {     console.log(' prompt:='=='>"+prompt +",==>"+argStr)
    let method: string = prompt.slice(10)
    let nameStr: string[] = JSBUtil.parseNamespace(method.trim())
    let jsInterfaceObject: ESObject = this.javaScriptNamespaceInterfaces.get(nameStr[0])

    let result: Record = {
      "code": -1,
      "data": ""
    }

    if (jsInterfaceObject == undefined) {
      console.info("桥接方法已通过,但是没找到命名空间的对象, 需要排查注入代码")
    } else {
      // 参数
      let args: Record = JSON.parse(argStr)
      method = nameStr[1]
      let res: Object | undefined = undefined
      let arg: Object | undefined = args["data"]
      if (args != undefined && args["_dscbstub"] != undefined) {
        let cb: string = args["_dscbstub"] as string ?? ""
        // 如果包含 _dscbstub ,就需要生成闭包
        let completionHandler = (value: Object, complete: boolean) => {
          let del: string = ""
          result["code"] = 0
          if (value != undefined) {
            result["data"] = value
          }
          let resultvalue = encodeURIComponent(JSON.stringify(result)) ?? ""
          if (complete) {
            del = "delete window." + cb
          }
          let js = "try {"
            + cb
            + "(JSON.parse(decodeURIComponent(\""
            + resultvalue
            + "\")).data);"
            + del
            + "; } catch(e){};"

          let t: number = Number(new Date())

          this.jsCache = this.jsCache + js
          if (t - this.lastCallTime < 50 if this.ispending this.evaljavascript50 this.ispending='true' else this.evaljavascript0 if jsinterfaceobjectnamestr1 res='jsInterfaceObject[method](arg,' completionhandler else jsinterfaceobjectnamestr1 log if jsinterfaceobjectnamestr1 let res: object='jsInterfaceObject[method](arg)' resultcode='0' if res resultdata='res' return json.stringifyresult evaljavascriptdelay: number let timer='setTimeout(()'> {
      if (this.jsCache.length != 0 && this.isBindUI) {
        this.runJavaScript(this.jsCache)
        this.isPending = false
        this.jsCache = ""
        this.lastCallTime = Number(new Date())
      }
    }, delay * 1000)
    this.timerArray.push(timer)
  }

  // _dsb对象使用
  private closePage(args: Record): Record | undefined {
    if (this.javascriptCloseWindowListener != undefined) {
      this.javascriptCloseWindowListener()
    }
    return undefined
  }

  // _dsb对象使用
  private returnValue(args: Record): Record | undefined {
    let completionHandler: ((obj: Object) => void) | undefined = undefined
    if (this.handerMap.has(args["id"] as string)) {
      completionHandler = this.handerMap.get(args["id"] as string) as ((obj: Object) => void)
    }
    if (completionHandler != undefined) {
      completionHandler(args["data"])
      let complete: boolean = args["complete"] as boolean
      if (complete) {
        this.handerMap.delete(args["id"] as string)
      }
    }
    return undefined
  }

  // _dsb对象使用
  private dsinit(args: Record): undefined {
    this.dispatchStartupQueue()
    return undefined
  }

  // 将消息模型转发成可解析的js字符串到web
  private dispatchJavascriptCall(info: DSCallInfo) {
    let map: Record = {}
    map["method"] = info.method ?? ""
    map["callbackId"] = info.id ?? 0
    map["data"] = JSON.stringify(info.args)
    // 生成json串
    let json = JSON.stringify(map)
    // 调用方法
    this.runJavaScript("window._handleMessageFromNative(" + json + ")")
  }
}




最近发表
标签列表