跨端技能必备之JSBridge

3,695 阅读5分钟

JSBridge & Fusion

JSBridge

在移动端H5开发中,我们常会调用一些(Android/iOS)端上的功能,这些功能有的是用H5无法实现的,有的纯粹是懒得再用H5开发一遍。JSBridge的目标即是在H5中以某种方式唤起这些端上的方法。

H5与Native通信

话不多说,先上代码,这段代码实现的功能即是在H5中唤起Native方法:

// bridgeCall.js
function schemeJump (url) { // 通过iframe子窗口执行URL
  let iframe = document.createElement('iframe')
  iframe.src = url
  iframe.style.display = 'none'
  document.documentElement.appendChild(iframe)
  setTimeout(() => {
    document.documentElement.removeChild(iframe)
  }, 0)
}

export default function bridgeCall (type, module, method, args) {
  let url = `fusion://${type}?`
  if (module) {
    url += `module=${module}&`
  }
  url += `method=${method}&`
  let param = args.map((arg) => {
    return encodeURIComponent(JSON.stringify(arg))
  })
  url += `arguments=%5B${param}%5D&`
  url += `origin=${window.location.hostname}`
  schemeJump(url)
}

在解释这段代码之前,我们先来了解一些背景知识。

WebView

在Android中,有个名为android.webkit.WebView的组件,它继承自android.widget.AbsoluteLayout,允许开发者在里面加载和展示一些H5页面。假如将WebView铺满整个页面,所有的视图表现都由H5来实现,那么Android端所需要提供的就仅仅是一个WeView容器。与此同时,一份代码完全可以跨浏览器和Android双端运行,大大降低了开发成本。从iOS 8.0起,iOS也实现了WKWebView,它与其他WebView的特性和用法相似。自此,基于WebView的移动端H5开发也就成为了跨多端(浏览器、Android、iOS等)的最佳实践。

WebView作为承载H5页面的容器,有一个特性是非常重要,即 它可以捕捉到所有在容器中发起的网络请求。其实想要 JS唤起Native 的方法,只要建立起 JS与Native通信 的桥梁即可,而这一点正好被WebView的这一特性所实现。

传递消息

我们可以通过 发起网络请求来向Native端传递消息,如在上述代码中通过子窗口iframe.src来发起请求。当然,使用location.href也可以发起请求,不过由于location.href作为当前页面的地址,所以并不推荐使用。

function schemeJump (url) { // 通过iframe子窗口发起网络请求
  let iframe = document.createElement('iframe')
  iframe.src = url
  iframe.style.display = 'none' // 不显示iframe
  document.documentElement.appendChild(iframe)
  setTimeout(() => {
    document.documentElement.removeChild(iframe)
  }, 0)
}

在与Native通信的时候,可以将一些参数传入,在当前场景中,有通信类型、模块名、方法名和入参等。

/**
 * @param type 通信类型
 * @param module 模块名
 * @param method 方法名
 * @param args 入参
 */
function bridgeCall (type, module, method, args) {
  let url = `fusion://${type}?` // 约定的协议
  if (module) {
    url += `module=${module}&`
  }
  url += `method=${method}&`
  let param = args.map((arg) => {
    return encodeURIComponent(JSON.stringify(arg))
  })
  url += `arguments=%5B${param}%5D&`
  url += `origin=${window.location.hostname}`
  schemeJump(url)
}

Native端在捕捉到这种协议头的请求时,会进行解析,伪代码如下:

IF url 匹配 "fusion://"
  DO 解析参数 type,module,method,args
  IF type === "invokeNative"
    DO 执行模块方法 FUNCS[module][method](args)
  END IF
END IF

我们也可以使用ajax来发送网络请求,只要Native端与H5同步一套解析规则即可。不过在实际开发中,由于ajax用于和服务端进行交互,所以最好还是使用iframe子窗口来发送请求。

执行回调

在唤起Native方法后,往往还需要执行一些回调,由于客户端无法直接执行JS代码,但可以获取WebView中的 全局变量,因此可以将回调方法挂载在全局变量上,之后客户端调用全局变量上的回调方法就可以了。

我们可以在全局设置一个单例的管理者,用其管理所有Native调用后的回调,以便于处理同时调用多个Native方法的复杂情况。

// manager.js
function isFunction (func) {
  return Object.prototype.toString.call(callback) === '[object Function]'
}

export default {
  GLOBAL_INSTANCE: {
    callbacks: [],
    callbackId: 0
  },
  callbackJs (callbackId, args) {
    let callback = this.globalInstance.callbacks[callbackId]
    if (callback && isFunction(callback)) {
      callback()
    }
  }
}

接着对Native方法唤起进一步封装,顺便将其回调记录下来,这里用到了闭包(装饰器)模式。

// jsBridge.js
import manager from './manager.js'
import bridgeCall from './bridgeCall.js'

window.manager = manager // 挂载到全局,以便端上调用
function isFunction (func) {
  return Object.prototype.toString.call(callback) === '[object Function]'
}

export default class JSBridge {
    constructor () {}
    invokeNative (module, method) {
        return function (...args) { // 持有形参变量module, method
            for (var i = 0; i < args.length; i++) {
                if (isFunction(args[i])) {
                    args = args.slice(0, +i + 1)
                    manager.GLOBAL_INSTANCE.callbacks.push(args[i]) // 记录回调方法
                    args[i] = manager.GLOBAL_INSTANCE.callbackId++ // 将回调索引传到端上
                    break
                }
            }
            bridgeCall('invokeNative', module, method, args)
        }
    }
}

调用示例如下:

import JSBridge from './jsBridge.js'
let bridge = new JSBridge()
let requestLogin = bridge.invokeNative('COMMON_MODULE', 'requestLogin')
requestLogin({ saveSession: true }, function (res) {
    // do sth
})

在唤起Native方法的时候,我们需要知道客户端到底支持哪些方法,端上应该给出一个API列表。

Fusion

在大型平台中,往往会有多个客户端,端与端之间是独立开发的,每个客户端各有一套独立的接口,这些接口可能名称不一样、入参不一样,也可能回调参数不一样。

如果一个H5页面在不同的端内唤起Native方法时需要书写不同的代码,那就毫无复用性可言。此外,学习各个端实现同一功能的不同API对开发者来说也是一种很大的负担和浪费。

在这种情况下,平台往往会推出一个组件用来 整合各个环境的bridge,允许开发者以一种统一的形式来调用不同端的实现相同功能的方法,这个组件一般被称为 Fusion(聚合物)

假如有一个功能为选择本地图片的方法:

模块(非必需) 名称 参数 回调参数
环境A common_module photograph opts: { type } Blob
环境B local_module chooseImage opts: { suffix } dataUrl
环境C other_module openAlbum opts: {} dataUrl
  • 在环境A中,photograph的传入属性type值为1时表示开启摄像头,值为2时表示打开相册,为 必填项;回调参数为Blob对象格式的拍摄或被选图片。
  • 在环境B中,chooseImage的功能为打开相册,入参属性suffix用于限制可选图片的后缀,非必填,不配置时默认允许选择所有后缀的图片;回调参数为dataUrl字符串格式的被选图片。
  • 在环境C中,openAlbum的功能为打开相册,无入参,回调参数为Blob对象格式的被选图片。

本着最大化兼容的原则构造一下统一接口,得到的结果应该为

名称 参数 回调参数
chooseImage { type, suffix } dataUrl
  • 在被调用时,方法内部首先将进行环境的判断,根据环境唤起不同端上的Native方法。
  • 由于属性type只在环境A中起作用,因此作为统一接口的参数时它是非必填的。当判断是环境A时,传入一个默认值。
  • 由于dataUrl格式是常用到的,所以这里使用window.URL.createObjectURL(blob)将blob对象转化为dataUrl字符串。在处理其他方法也应根据习惯来做兼容。

桥接模式

讲到这里,有必要提一下这种结构所使用的模式,见名知意,JSBridge与桥接模式脱不了干系。

桥接模式用于将抽象与实现解耦,使得二者可以独立变化,属于结构型模式。

这种模式涉及到一个作为桥接的接口,使得功能实现可以独立于接口实现,这两种类型的类可被结构化改变而互不影响。

在这里,抽象指的是JSBridge对接口的抽象,而实现则指的是客户端上对功能的实现。H5只需要了解如何调用JSBridge的方法即可,无需关心客户端上是如何实现这些功能的,也无需关心这些功能的实现是否有所变化。只要JSBridge可以维持住调用方式(名称、传入参数、回调参数等)不发生变化,H5和客户端两端代码就可以独立于彼此随便修改,这是一种十分松散的结构。

下图简单演示了JSBridge的中间作用。