今天我们来简单聊一下JSBridge

为什么要聊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请求即可。

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

  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 = messageQueuemessageQueue.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的windowwindow[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:天呐,我们原来活在一个虚拟的世界里!!!在鄙视链的最低端!!

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

我们生活的世界到底是真实的吗?

关于我们

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

转载于:https://juejin.im/post/5d425a16f265da03f564c1c3

【前端基础】Web与Native交互之The JSBridge FAQ相关推荐

  1. 前端基础 Web网页标准

    Web 标准是由W3C组织和其他标准化组织制定的一系列标准的集合.W3C(万维网联盟)是国际最著名的标准化组织. 为什么需要web标准: 遵循web标准除了可以让不同的开发人员写出页面更标准.更统一以 ...

  2. iframe跨域调用js_郑州Web前端基础学习之JS跨域知识梳理

    JS是Web前端开发三要素之一,是郑州Web前端基础学习中非常重要的知识点.JS涉及的知识点多且杂,很多同学反映不知如何下手,事实上,只要你认真记.多练习,就可以慢慢掌握它.今天千锋郑州Web前端培训 ...

  3. 零基础web前端学习路线【全新web前端入门视频教程】

    零基础怎么学web前端?下面就一起来看看吧! 想学好web前端,该从哪里入手学习呢?零基础学习web前端学习路线图从哪里可以找到呢?这里为大家整理完整的零基础 前端学习路线分享给大家. 适合零基础学员 ...

  4. web前端基础html,css,js,jquery

    目录 1 前端技术: 1.1 学习方法 1.1.1 前端要怎么学? 1.1.2 前端技术栈 1.2 HBuilderX 1.2.1 介绍 1.2.2 安装 1.2.3 主题 1.2.4 字体 1.2. ...

  5. web前端基础之HTML知识总结,了解后轻松做网页

    简介 HTML 是什么? htyper text markup language 即超文本标记语言. 超文本: 就是指页面内可以包含图片.链接,甚至音乐.程序等非文字元素. 标记语言: 标记(标签)构 ...

  6. 零基础web前端和python哪个好学一些?

    零基础的同学入行IT优先选择的两个语言就是web前端和Python,但更多的时候同学不知道到底应该选择哪一个,下面小千就来带大家去分析一下. Python开发 Python作为一门面向对象的编程语言, ...

  7. Web前端基础知识整理

    1. 前端基础知识 文件分类 XML(扩展标记语言) 装载有格式的数据信息,用于各个框架和技术的配置文件描述 特点: 扩展名为.xml 内容区分大小写 标签要成对出现,形成容器,只能有一个 标签按正确 ...

  8. WEB前端基础(HTML+CSS+JavaScript)(上)最好看最详细的笔记~

    WEB前端基础 当前部分是基于HTML与CSS总结,后续有JavaScript更新~ 文章目录 WEB前端基础 一.HTML概述(阶段1 入门概述) 1.HTML 二.第一个HTML页面 1.HTML ...

  9. web前端基础与CSS入门

    web前端基础 1.嵌套列表 注:列表之间可以互相嵌套,形成多层级的列表. <!DOCTYPE html> <html lang="en"> <hea ...

最新文章

  1. [转载] 钢铁是怎样炼成的——第一部第二章
  2. java学习笔记2022.1.17
  3. c++如何对结构体作为形参设置默认值
  4. 计算机函数公式一等奖怎么算,信息技术应用 用计算机画函数图象教案设计(一等奖)...
  5. C和指针之动态内存分配堆、栈、全局区(静态区)、常量区对比总结学习笔记
  6. codeforce C. Okabe and Boxes
  7. 《恋上数据结构第1季》动态数组实现栈
  8. 2020年CSDN最后一波上车机会,快来~~
  9. 【转】总结oninput、onchange与onpropertychange事件的用法和区别
  10. error C2011: “Font”:“struct”类型重定义
  11. Redis缓存穿透、缓存雪崩和缓存击穿理解
  12. php mysqli分页,PHP使用Mysqli类库实现完美分页效果的方法_PHP
  13. pip 清华大学镜像_pip源很慢,更改成清华的镜像地址
  14. 通证指数:ChaiNext系列指数基金上线
  15. Android NFC标签读写 配置 过滤器总结 各类NFC数据类型NfcA NfcB IsoDep MifareClassic读取
  16. 小米8se怎么解屏幕锁_黔隆科技刷机教程小米5SPLUS忘记密码刷机解锁降级救砖解屏幕锁账户锁教程...
  17. 如何让用html制作404页面,网站404页面怎么做?
  18. VS或VC编译正常,但运行时出现Stack overflow
  19. 如何快速实现增长App用户量?
  20. 什么是I3C总线?它和I2C和SMBus是什么关系?

热门文章

  1. 新裝win7虚拟机设置记录-20180909
  2. decide your linux OS is GUI or not
  3. Codeforces Round #250 (Div. 1) D. The Child and Sequence 线段树 区间取摸
  4. mysql InnoDB 行锁分析
  5. 2006吴山庙会-怎么都是人啊?
  6. L1-053 电子汪-PAT团体程序设计天梯赛GPLT
  7. 蓝桥杯 BASIC-5 基础练习 查找整数
  8. 【软件测试】软件测试的基本流程(一般步骤)
  9. Java中Http连接的两种方式
  10. ArcGIS9.3 SDE安装