大家好,我是编程小6,很高兴遇见你,有问题可以及时留言哦。
行走四方,广结善缘,不为四方知己,但求一心平静。
在移动端H5开发中,我们常会调用一些(Android/iOS)端上的功能,这些功能有的是用H5无法实现的,有的纯粹是懒得再用H5开发一遍。JSBridge的目标即是在H5中以某种方式唤起这些端上的方法。
话不多说,先上代码,这段代码实现的功能即是在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)
}
在解释这段代码之前,我们先来了解一些背景知识。
在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列表。
在大型平台中,往往会有多个客户端,端与端之间是独立开发的,每个客户端各有一套独立的接口,这些接口可能名称不一样、入参不一样,也可能回调参数不一样。
如果一个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 |
本着最大化兼容的原则构造一下统一接口,得到的结果应该为
名称 | 参数 | 回调参数 |
---|---|---|
chooseImage | { type, suffix } | dataUrl |
window.URL.createObjectURL(blob)
将blob对象转化为dataUrl字符串。在处理其他方法也应根据习惯来做兼容。讲到这里,有必要提一下这种结构所使用的模式,见名知意,JSBridge与桥接模式脱不了干系。
桥接模式用于将抽象与实现解耦,使得二者可以独立变化,属于结构型模式。
这种模式涉及到一个作为桥接的接口,使得功能实现可以独立于接口实现,这两种类型的类可被结构化改变而互不影响。
在这里,抽象指的是JSBridge对接口的抽象,而实现则指的是客户端上对功能的实现。H5只需要了解如何调用JSBridge的方法即可,无需关心客户端上是如何实现这些功能的,也无需关心这些功能的实现是否有所变化。只要JSBridge可以维持住调用方式(名称、传入参数、回调参数等)不发生变化,H5和客户端两端代码就可以独立于彼此随便修改,这是一种十分松散的结构。
下图简单演示了JSBridge的中间作用。
上一篇
已是最后文章