【前端基础】Web与Native交互之The JSBridge FAQ

今天咱们来简单聊一下JSBridgejavascript

为何要聊JSBridge?

不为何前端

好吧,JSBridge虽然也算比较古老了,但关于JSBridge的原理也是一个目前做为一名前端开发人员须要了解掌握的知识。java

现现在,在作移动端H5开发时,少不了与Native之间进行交互,这里的Native,包括了传统意义上的App和现现在各类各样的坑王小程序们。 一般,各大公司都会封装一套本身的js sdk,用于提供给Web开发人员,来实现与Native之间的交互,他们可能有本身的名字,但他们统称为JSBridgegit

网上有不少介绍关于JSBridge原理的文章,充斥着大量OC 或Java代码,对于没有作过移动端Native开发但想了解JSBridge原理的同窗来讲都不是很友好。 因此,这篇文章主要就来给没有Native开发经验的同窗们介绍一下H5页面在WebView中是如何经过JSBridge与Native进行交互的。同时,为了不见到没必要要的Native代码,这里不过多介绍经过JavaScriptCore API来实现交互的方式了,只介绍最初经典的方案。github

什么是JSBridge?

简单的来讲,JSBridge是一种H5页面与Native之间异步双向的通讯的方式,它与咱们平常接触到的最多见的HTTP这种通讯方式,本质上没什么区别。web

JSBridge存在的意义是啥?

为了生活变的更美好json

为了体验更好小程序

为了让用户分不清他用的究竟是Web App仍是Native App数组

为了效率app

搞开发的嘛,确定都是为了提升工做效率,同时不下降太多用户体验的状况下,复用,复用这个复用那个,统一这个统一那个的。一款App产品要作好几个端,重复的页面扔给H5算了!啥!须要一些奇特的功能?作个bridge接口吧!

JSBridge的通讯过程是什么样的?

image

简单来讲,这个通讯过程与在餐厅吃饭时厨师上菜的流程相似,厨师通常不会亲自为你上菜(土豪私厨请忽略),厨师们每作好一道菜后就会按一下取餐铃,服务员听到铃声后就会过来取餐并为客人上菜。

一般状况下,按一下铃就会有服务员过来把菜取走,有些时候,厨师们可能同时作好了N道菜,按了N次铃,这时,服务员也会很敬业的将这些菜所有打包取走并处理。

那么它是如何实现的?

接下来该进入正题了,咱们来看一下JSBridge的基本原理

  • 先从web 向 native发起通讯开始:

前面咱们了解到JSBridge的本质就是一种通讯方式,因此,这里就比较容易理解了,H5调用Native的本质就是请求拦截

当H5的世界想与外面交流时,他只须要(也只能)发送一个请求,好比发送一个简单的GET请求便可。

咱们会想到三种发请求的方式:

  1. 使用带有src属性标签发送请求,如iframe...
const iframe = document.createElement('iframe')
    iframe.src = "xxx"
复制代码

这种方式也是各大Hybrid框架经常使用的方式,重复大量发送也不须要担忧消息的丢失问题

  1. 使用location.href发送请求
location.href = "xxx"
复制代码

这种方式比较适用于一些一次性调用的场景,例如H5中某个操做须要跳转至App的某一个页面,经过这种方式重复发送大量请求会形成请求消息的丢失,只接受最后一次。

  1. 使用Ajax的方式来发送请求
const url = 'xxx'
    fetch(url, { ... })
        .then()
        .catch()
复制代码

这种方式写起来比较麻烦,但性能上略好于前两种

著名的Cordova.js使用的方式:

execIframe = document.createElement('iframe')
execIframe.style.display = 'none'
execIframe.src = 'gap://ready'
document.body.appendChild(execIframe)
复制代码

iOS中的一个叫WebViewJavascriptBridge的库一样使用的是相似的方式:

const messagingIframe = document.createElement('iframe')
messagingIframe.src = 'https://__wvjb_queue_message__'
body.appendChild(messagingIframe)
复制代码

注意到Cordova的src与下面的WebViewJavascriptBridge中的src不一样的地方在于,Cordova使用了自定义URL Scheme的方式,嗯,这种方式用来唤起本地安装的App更常见些。

  • 须要带点参数

多数状况下,咱们更但愿给Native带一些参数过去,因此接下来,咱们来看参数传递的问题

可能你会想到,在发送请求的时候直接把参数放在请求地址后面,经过query的形式拼接起来就能够了,好比像这样:

execIframe.src = 'gap://ready?p1=v1&p2=v2&p3=v3…'
复制代码

但这种方式存在着一些问题

  • 首先,它的长度是有限的,虽然限制很长,但这终归会是一个隐患

  • 其次,若是是本身公司的App临时作一个jsbridge的接口,确实能够这样写,可是若是做为一个通用的工具来封装,这样去实现的话就与业务耦合的太紧密了。

在典型的JSBridge实现方案中,关于参数的处理是这样实现的:

  • 首先,任什么时候候,H5中JS须要调用Native时,发送请求的url是固定不变的,好比gap://ready

  • 其次,在window上定义一个全局的数组变量,名叫messageQueue,初始化时为空,当H5须要给Native发送消息时,先建立一个对象,并把全部相关的参数放在这个对象中,而后将这个对象插入messageQueue数组队尾,用代码来解释就是这样:

const messageQueue = []
window.messageQueue = messageQueue

messageQueue.push(JSON.stringify({
	message: 'xxx',
	params: 'xxx'
}))
// 发一个请求,按一下铃,戳一下Native~~
execIframe.src = 'gap://ready'
复制代码
  • 当native收到gap://ready的请求后,就知道H5有新消息,就会执行一段神奇的代码,进入到WebView中,并将定义在window上的全局变量messageQueue数组中所有数据打包取走,并将messageQueue清空,取走后逐条解析执行。咱们用eval函数来充当这段神奇的代码来解释这里的逻辑:
// 当Native拦截到'gap://ready'请求后执行的magic code
const messageQueue = eval('window.messageQueue')
const messages = JSON.parse(messageQueue)
for (const message in messages) {
     doSomeThingWithMessage(message)
     …
}
eval('window.messageQueue = []')
复制代码

这样,菜就被服务员端走了, 消息就被Native取走了

  • 接下来看看Native如何将处理结果告诉H5

若是餐厅的一个负责任的厨师须要让他的客户快要吃完一道前菜时告诉他,以便他去及时准备主菜,只须要在上菜时放上本身的名片,让客户快吃完的时候把厨师的名片交给服务员就能够了。

一样,若是H5须要Native执行完某一条指令时通知到H5,那么H5只须要在window上准备一个回调函数,在里面作该作的事,并将这个回调函数的名字在上一步建立消息对象时,放进这个对象中:

messageQueue.push(JSON.stringify({
	message: 'xxx',
	params: 'xxx',
	callBackName: 'xxx',
}))
复制代码

这样,在Native执行完你须要的指令后会再次执行那段神奇的代码进入WebView的世界,执行定义在window上名为callbackName的方法,并把native执行的结果传给这个方法。就像这样:

const messageQueue = eval('window.messageQueue')
const messages = JSON.parse(messageQueue)
for (const message in messages) {
     const result = doSomeThingWithMessage(message)
     eval(`window[${message.callbackName}](${result})`)
     …
}
eval('window.messageQueue = []')
复制代码

同时,这也就揭露了Native是如何给H5发送消息的,直接执行window上定义好的一个方法便可。

固然,为了代码更规范,保证H5不胡乱的建立callBackName,Native并非直接执行window上的callbackName方法,而是会调用一个大概叫handleMessageFromNative的方法,这个方法是H5这边提早准备并定义在window上的方法,在这个方法中对消息的处理进行了收口,在里面调用window上的callbackName方法,执行完成后,将callbackName方法从window上删除掉 ,整个流程的代码大概是这样的:

// H5
function handleMessageFromNative (message) {
	if (typeof message.callbackName === 'function') {
		window[callbackName](message.result)
		delete window[callbackName]
	}
}

window.handleMessageFromNative = handleMessageFromNative

复制代码
// Native
const messageQueue = eval('window.messageQueue')
const messages = JSON.parse(messageQueue)

for (const message in messages) {
     const result = doSomeThingWithMessage(message)
     
     const messageFromNative = JSON.stringify({
        result,
        callbackName: message.callbackName
     })
     
     eval(`window.handleMessageFromNative(${messageFromNative})`)
     …
}
eval('window.messageQueue = []')
复制代码

这里关于callbackName的生成也有一点规则,感兴趣能够去撸一下相关源码。大概和jsonp的规则相似。

接下来,为了方便他人使用,将以上的流程整理封装完善一下,H5和Native同时暴露两个接口,便成了以下的样子:

// H5与Native同时增长以下两个接口供对方使用:
// ≈ function addEventListener(eventName, callback)
function registerHandler (handlerName, block) {
    window.handlers[handlerName] = block
    …
}

// Web或Native调用对方接口的方式
// ≈ dispatchEvent(eventName, data, callback)
function callHandler (handlerName, message, callback) {
    window.handlers[handlerName](message)
    …
}
复制代码

这样就能够很方便的使用了,例如要实现一个扫描二维码的功能:

// Native
// 注册了一个扫描二维码的方法
registerHanlder('scanQRCode', () => {
    // ...
    Camera.open().scanQRCode()
    // ...
})
复制代码
// H5
// 调用扫描二维码的方法
callHanlder('scanQRCode', { type: 'qrcode' }, result => {
    console.log('扫码结果:', result)
})
复制代码

喜欢的话能够用Promise封装一下:

// 为H5封装好的bridge-sdk.js,在H5中使用
/** * 扫描二维码并返回结果 * ... * @memberOf Camera * @async * @returns {Promise} 能够在then中接受扫码结果`result`,参数为 { code: 'xxxxxx' } * ... */
export async function scanQRCode () {
    return new Promise((resolve, reject) => {
        callHanlder('scanQRCode', { type: 'qrcode' }, result => {
            console.log('扫码结果:', result)
            resolve(result)
        })
    })
}
复制代码

好了,以上就是经典的JSBridge的实现方案,看起来很是的简单,且没有兼容性问题。

既然Native有神奇的代码,有没有更完全些的办法呢?

有!!!Native中有另外一个神奇的API,咱们暂且称它为defineFunc函数吧,它能够直接将Native的代码注入到H5的载体WebView中,并挂在WebView的window上。

// define 翻译过来大概就是下面的这个意思
function defineFunc (funcName, func) {
    const window = webView.window ... // 经过一些Native的API拿到WebView的window
    window[funcName] = func // 这里的func 是Native的func,执行的是纯Native的代码
}

// Native
defineFunc('callSomeNativeFunction', () => {
    // 这些是由Native的代码翻译成javascript的伪代码
    const file = io.readFile('/path/to/file')
    ...
    // 作一些H5作不到的事情
    file.write('/path/to/file', 'content')
    ...
})

复制代码

这就是利用如iOS中JavaScriptCore的API来实现交互的原理,安卓也有相似的方式,对系统版本有些许的要求,能够忽略不计。这里就不讨论了。

什么??Native能够随意到WebView中执行代码?这个bug是否是Native乱搞搞出来的?细思极恐啊!

H5:天呐,咱们原来活在一个虚拟的世界里!!!在鄙视链的最低端!!

是的!让我想起了《黑客帝国》,啥?没看过?暴露年龄了?

咱们生活的世界究竟是真实的吗?

关于咱们

快狗打车前端团队专一前端技术分享,按期推送高质量文章,欢迎关注点赞。

公众号二维码
相关文章
相关标签/搜索