为什么要聊JSBridge?
好吧,JSBridge虽然也算比较古老了,但关于JSBridge的原理也是一个目前作为一名前端开发人员需要了解掌握的知识。
现如今,在做移动端H5开发时,少不了与Native之间进行交互,这里的Native,包括了传统意义上的App和现如今各种各样的坑王小程序们。 通常,各大公司都会封装一套自己的js sdk,用于提供给Web开发人员,来实现与Native之间的交互,他们可能有自己的名字,但他们统称为JSBridge。
网上有很多介绍关于JSBridge原理的文章,充斥着大量OC 或Java代码,对于没有做过移动端Native开发但想了解JSBridge原理的你同学来说都不是很友好。 所以,这篇文章主要就来给没有Native开发经验的同学们介绍一下H5页面在WebView中是如何通过JSBridge与Native进行交互的。同时,为了避免见到不必要的Native代码,这里不过多介绍通过JavaScriptCore API来实现交互的方式了,只介绍最初经典的方案。
什么是JSBridge?
简单的来说,JSBridge是一种H5页面与Native之间异步双向的通信的方式,它与我们日常接触到的最常见的HTTP这种通信方式,本质上没什么区别。
JSBridge存在的意义是啥?
为了生活变的更美好
为了体验更好
为了让用户分不清他用的到底是Web App还是Native App
为了效率
搞开发的嘛,肯定都是为了提高工作效率,同时不降低太多用户体验的情况下,复用,复用这个复用那个,统一这个统一那个的。一款App产品要做好几个端,重复的页面扔给H5算了!啥!需要一些奇特的功能?做个bridge接口吧!
JSBridge的通信过程是什么样的?
简单来说,这个通信过程与在餐厅吃饭时厨师上菜的流程类似,厨师一般不会亲自为你上菜(土豪私厨请忽略),厨师们每做好一道菜后就会按一下取餐铃,服务员听到铃声后就会过来取餐并为客人上菜。
通常情况下,按一下铃就会有服务员过来把菜取走,有些时候,厨师们可能同时做好了N道菜,按了N次铃,这时,服务员也会很敬业的将这些菜全部打包取走并处理。
那么它是如何实现的?
接下来该进入正题了,我们来看一下JSBridge的基本原理
- 先从web 向 native发起通信开始:
前面我们了解到JSBridge的本质就是一种通信方式,所以,这里就比较容易理解了,H5调用Native的本质就是请求拦截
当H5的世界想与外面交流时,他只需要(也只能)发送一个请求,比如发送一个简单的GET请求即可。
我们会想到三种发请求的方式:
- 使用带有src属性标签发送请求,如
iframe...
const iframe = document.createElement('iframe') iframe.src = "xxx"
这种方式也是各大Hybrid框架常用的方式,重复大量发送也不需要担心消息的丢失问题
- 使用location.href发送请求
location.href = "xxx"
这种方式比较适用于一些一次性调用的场景,例如H5中某个操作需要跳转至App的某一个页面,通过这种方式重复发送大量请求会造成请求消息的丢失,只接受最后一次。
- 使用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', }))
