JsBridge是什么

JsBridge作为 Hybrid 应用的一个利器,H5页面其实运行在Navtive的webView中,webView是移动端提供的JavaScript的环境,他是一种嵌入式浏览器原生应用可以用它来展示网络内容。可与页面JavaScript交互,实现混合开发。所以jsBridge自然也运行在webView中的JS Content中,这与原生有运行环境的隔离,所以需要有一种机制实现Native端和Web端的双向通信,这就是JsBridg。

以JavaScript引擎或Webview容器作为媒介,通过协定协议进行通信,实现Native端和Web端双向通信的一种机制。

JsBridge用途

JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 Native 和 H5。它在 APP 内方便地让 Native 调用 JS,JS 调用 Native ,是双向通信的通道。JSBridge 主要提供了 JS 调用 Native 代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。

JsBridge与Native通信的原理

jsBridge就像它的名字的意义一样,是作为Native端(Ios端, Android端等)与非 Native 之间(这里指H5页面)的桥梁,它的核心作用是构建两端之间相互通信的通道。
JsBridge(前端的角度) - 图1

在H5中JavaScript调用Native的方式有两种:

  1. - **注入API**:注入一个全局对象到JavaScriptwindow对象中(可以类比于RPC调用)
  2. - **拦截URL Schema**:客户端拦截WebView的请求并做相应的操作(可类比为JSONP

注入API

注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的,使用该方式时,JS 需要等到 Native 执行完对应的逻辑后才能进行回调里面的操作。
Android 的 Webview 提供了 addJavascriptInterface 方法,支持 Android 4.2 及以上系统:

  1. gpcWebView.addJavascriptInterface(new JavaScriptInterface(), 'nativeApiBridge');
  2. public class JavaScriptInterface {
  3. Context mContext;
  4. JavaScriptInterface(Context c) {
  5. mContext = c;
  6. }
  7. public void share(String webMessage){
  8. // Native 逻辑
  9. }
  10. }

上面代码的作用就是webView中绑定一个全局的对象(桥对象),然后将nativeApiBridge对象中的方法映射到Javascript中的nativeApiBridge的方法。
前端调用方式:

  1. window.nativeBridge.postMessage(message);

拦截URL Scheme

先简单的了解一下什么是URL Scheme?URL Scheme是一种类似url的链接,是为了方便app直接互相调用设计的,形式和普通的url近似 ,主要区别是schemel和host一般是自定义的:
如普通的url:
url.PNG
而url scheme类似这样:

  1. kcnative://go/url?query

拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 通过(shouldOverrideUrlLoading()方法)拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。

那么调用 Native 功能时 Callback 怎么实现的?

对于 JSBridge 的 Callbac,可以用JSONP的机制实现:

当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。

1.在H5中注入一个callback方法,放在window对象或者与Native端相绑定的对象中

  1. /**
  2. *
  3. * @param callbackId
  4. * @param obj
  5. * 客户端通知webviw的callback
  6. */
  7. const onCallback = function (callbackId: number, obj: any) {
  8. if (ApiBridge.callbackCache[callbackId]) {
  9. console.log('onCallback调用',callbackId);
  10. console.log(ApiBridge.callbackCache[callbackId]);
  11. ApiBridge.callbackCache[callbackId](obj)
  12. }
  13. }
  14. //预先把callback存到一个callbackCache数组或者对象中,通过自增的方式确定callbackId

2.然后把callback对应的id通过Url Schema传到Native端

  1. const callNative = function (clz: string, method: string, args: any, callback?: any) {
  2. let msgJson = {
  3. clz,
  4. method,
  5. args,
  6. }
  7. if (args != undefined) msgJson.args = args
  8. if (callback) {
  9. const callbackId = getCallbackId()
  10. ApiBridge.callbackCache[callbackId] = callback
  11. if (msgJson.args) {
  12. msgJson.args.callbackId = callbackId.toString()
  13. } else {
  14. msgJson.args = {
  15. callbackId: callbackId.toString(),
  16. }
  17. }
  18. }
  19. if (browser.versions.ios) {
  20. if (ApiBridge.bridgeIframe == undefined) {
  21. bridgeCreate()
  22. }
  23. ApiBridge.msgQueue.push(msgJson)
  24. if (!ApiBridge.processingMsg) ApiBridge.bridgeIframe!.src = 'kcnative://go'
  25. window.initJSBridge = true
  26. } else if (browser.versions.android) {
  27. var ua = window.navigator.userAgent.toLowerCase()
  28. window.initJSBridge = true
  29. // android
  30. return (ua.indexOf('caiji') > -1 || ua.indexOf('caijinetgame') > -1 || ua.indexOf('chikii') > -1) && prompt(JSON.stringify(msgJson))
  31. }
  32. }

Native通过shouldOverrideUrlLoading(),拦截到WebView的请求,并通过与前端约定好的Url Schema判断是否是JSBridge调用。
3.Native解析出前端带上的callback,并使用下面方式调用callback

  1. webView.loadUrl(String.format("javascript:callback_1(%s)", isChecked)); // 可以带上相应的参数

通过上面几步就可以实现JavaScript到Native的通信

核心代码的实现

index.ts

  1. import { bridge,jsBridgeClient } from './bridgeInterface'
  2. import {
  3. _Configs,
  4. ApiBridge,
  5. bridgeCreate,
  6. prepareProcessingMessages,
  7. fetchMessages,
  8. getLanguage,
  9. getNativeData,
  10. setNativeData,
  11. doAction,
  12. onCallback,
  13. onBridgeInitComplete,
  14. } from './nativeBridge'
  15. (function (window) {
  16. let global = window
  17. let apiBridge:bridge = Object.assign({
  18. bridgeCreate,
  19. prepareProcessingMessages,
  20. fetchMessages,
  21. onBridgeInitComplete,
  22. onCallback,
  23. }, ApiBridge)
  24. let kerkee:jsBridgeClient = {
  25. getNativeData,
  26. setNativeData,
  27. doAction,
  28. }
  29. global.ApiBridge = apiBridge
  30. global.jsBridgeClient = kerkee
  31. onBridgeInitComplete(function (aConfigs: any) {
  32. //判断 JSBridge 是否已经注入成功的log。
  33. if (aConfigs) {
  34. if (aConfigs.hasOwnProperty('isOpenJSLog')) {
  35. _Configs.isOpenJSLog = aConfigs.isOpenJSLog
  36. }
  37. if (aConfigs.hasOwnProperty('isOpenNativeXHR')) {
  38. _Configs.isOpenNativeXHR = aConfigs.isOpenNativeXHR
  39. }
  40. }
  41. })
  42. })(window)

nativeBridge.ts

  1. import { bridge,reportDataOptions,callBackOption } from './bridgeInterface'
  2. import { GetLanguageRes } from 'UTILS/types/utils'
  3. const browser = {
  4. versions: (function () {
  5. const u = navigator.userAgent,
  6. app = navigator.appVersion
  7. return {
  8. trident: u.indexOf('Trident') > -1, //IE
  9. presto: u.indexOf('Presto') > -1, //opera
  10. webKit: u.indexOf('AppleWebKit') > -1, //apple&google kernel
  11. gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //firfox
  12. mobile: !!u.match(/AppleWebKit.*Mobile.*/), //is Mobile
  13. // ipad13.3系统用这个有问题
  14. // ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //is ios
  15. ios: !!u.match(/Mac OS X/), //is ios
  16. android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android
  17. iPhone: u.indexOf('iPhone') > -1, //iPhone or QQHD
  18. iPad: u.indexOf('iPad') > -1, //iPad
  19. iPod: u.indexOf('iPod') > -1, //iPod
  20. webApp: u.indexOf('Safari') == -1, //is webapp,no header and footer
  21. weixin: u.indexOf('MicroMessenger') > -1, //is wechat
  22. qq: u.match(/\sQQ/i)?.includes('qq'), //is qq
  23. }
  24. })(),
  25. language: (navigator.language).toLowerCase(),
  26. }
  27. let ApiBridge: bridge = {
  28. msgQueue: [],// 消息队列
  29. callbackCache: [], //存储callback的数组
  30. callbackId: 0,
  31. processingMsg: false,
  32. }
  33. const bridgeCreate = function () {
  34. ApiBridge.bridgeIframe = document.createElement('iframe')
  35. ApiBridge.bridgeIframe.style.display = 'none'
  36. document.documentElement.appendChild(ApiBridge.bridgeIframe)
  37. }
  38. const prepareProcessingMessages = function () {
  39. ApiBridge.processingMsg = true
  40. }
  41. const fetchMessages = function () {
  42. if (ApiBridge.msgQueue.length > 0) {
  43. const messages = JSON.stringify(ApiBridge.msgQueue)
  44. ApiBridge.msgQueue.length = 0
  45. return messages
  46. }
  47. ApiBridge.processingMsg = false
  48. return null
  49. }
  50. const callNative = function (clz: string, method: string, args: any, callback?: any) {
  51. let msgJson = {
  52. clz,
  53. method,
  54. args,
  55. }
  56. if (args != undefined) msgJson.args = args
  57. if (callback) {
  58. const callbackId = getCallbackId()
  59. ApiBridge.callbackCache[callbackId] = callback
  60. if (msgJson.args中·) {
  61. msgJson.args.callbackId = callbackId.toString()
  62. } else {
  63. msgJson.args = {
  64. callbackId: callbackId.toString(),
  65. }
  66. }
  67. }
  68. if (browser.versions.ios) {
  69. if (ApiBridge.bridgeIframe == undefined) {
  70. bridgeCreate()
  71. }
  72. ApiBridge.msgQueue.push(msgJson)
  73. if (!ApiBridge.processingMsg) ApiBridge.bridgeIframe!.src = 'kcnative://go'
  74. window.initJSBridge = true
  75. } else if (browser.versions.android) {
  76. var ua = window.navigator.userAgent.toLowerCase()
  77. window.initJSBridge = true
  78. // android
  79. return (ua.indexOf('caiji') > -1 || ua.indexOf('caijinetgame') > -1 || ua.indexOf('chikii') > -1) && prompt(JSON.stringify(msgJson))
  80. }
  81. }
  82. const getCallbackId = function (): number {
  83. return ApiBridge.callbackId++
  84. }
  85. /**
  86. *
  87. * @param callbackId
  88. * @param obj
  89. * 客户端通知webviw的callback
  90. */
  91. const onCallback = function (callbackId: number, obj: any) {
  92. if (ApiBridge.callbackCache[callbackId]) {
  93. console.log('onCallback调用',callbackId);
  94. console.log(ApiBridge.callbackCache[callbackId]);
  95. ApiBridge.callbackCache[callbackId](obj)
  96. }
  97. }
  98. // const webviewCallback = function (callbackId: string, options:callBackOption){
  99. // }
  100. const onBridgeInitComplete = function (callback: any) {
  101. callNative('ApiBridge', 'onBridgeInitComplete', {}, callback)
  102. }
  103. // 获取是否装了微信等相关信息
  104. const initPcGoData = function (callback:any) {
  105. callNative('jsBridgeClient', 'initPcGoData', {}, callback)
  106. }
  107. let _Configs = {
  108. isOpenJSLog: false,
  109. sysLog: {},
  110. isOpenNativeXHR: true,
  111. sysXHR: {},
  112. }
  113. /**
  114. * getNativeData(method:string,params:{},callback) 从客户端获取数据
  115. * 参数详情请看 https://confluence.caijiyouxi.com/pages/viewpage.action?pageId=11763717
  116. */
  117. const getNativeData = function (method:string,callback:any,params?:any){
  118. const pms = params || {}
  119. callNative('jsBridgeClient',method,pms,callback)
  120. }
  121. /**
  122. * setNativeData(method:string,params:{key:value})H5告诉客户端一些数据 ,客户端执行相应操作
  123. * 参数详情请看 https://confluence.caijiyouxi.com/pages/viewpage.action?pageId=11763717
  124. */
  125. const setNativeData = function (method:string,params: any){
  126. const pms = params || {}
  127. callNative('jsBridgeClient',method,pms)
  128. }
  129. /**
  130. * doAction(method:string,params:{})H5调用客户端组件或方法
  131. */
  132. const doAction = function (method:string,params?:any){
  133. const pms = params || {}
  134. callNative('jsBridgeClient',method,pms)
  135. }
  136. /**
  137. *
  138. * steamEpicApi(method:string,params:{},callback)steam/epic相关交互
  139. */
  140. const steamEpicApi = function (method:string,params?:any,callback?:any){
  141. const pms = params || {}
  142. callNative('jsBridgeClient',method,pms,callback)
  143. }
  144. export {
  145. browser,
  146. ApiBridge,
  147. _Configs,
  148. bridgeCreate,
  149. getLanguage,
  150. getNativeData,
  151. setNativeData,
  152. doAction,
  153. steamEpicApi,
  154. prepareProcessingMessages,
  155. fetchMessages,
  156. reportData,
  157. log,
  158. getCallbackId,
  159. onCallback,
  160. onBridgeInitComplete,
  161. initPcGoData,
  162. openJSLog,
  163. closeJSLog,
  164. }

对JsBridge接口的抽象

getNativeData(method:string,params:{},callback) 从客户端获取数据

参数 类型 是否必须
callback function
method string
params Object

获取一些如获取手机状态栏高度、网络状态、用户信息等功能
使用示例:

  1. const getTitleHeight = () => {
  2. console.log('getTitleHeight')
  3. jsBridge.getNativeData('getTitleHeight', (data: any) => {
  4. console.log(data)
  5. })
  6. }
  7. const getUserInfo = () => {
  8. console.log('getUserInfo')
  9. jsBridge.getNativeData('getUserInfo', (data: any) => {
  10. console.log(data)
  11. })
  12. }

setNativeData(method:string,params:{key:value})H5告诉客户端一些数据 ,客户端执行相应操作

参数 类型 是否必须
method string
params Object

使用实例:

  1. const setWebViewTitle = () => {
  2. console.log('setWebViewTitle')
  3. jsBridge.setNativeData('setWebViewTitle', { title: 'JsBrige' })
  4. }
  5. const setWebBackColor = () => {
  6. jsBridge.setNativeData('setWebBackColor', { back_color: 'red' })
  7. }

doAction(method:string,params:{},calllback:any)H5调用客户端组件或方法

参数 类型 是否必须
method string
params Object
callback Function

使用示例:

  1. const buyGoods = () => {
  2. console.log('buyGoods')
  3. jsBridge.doAction('buyGoods', { goods_info: state.goodsInfo, buy_num: 1 })
  4. }
  5. const showShareDialog = () => {
  6. console.log('showShareDialog')
  7. jsBridge.doAction('showShareDialog', {
  8. is_share: 1,
  9. share_type: 1,
  10. share_url: '分享链接',
  11. thumb: '项目链接'
  12. content: '分享内容',
  13. share_title: '分享标题!',
  14. })
  15. }

以上抽象的方法大多能够满足大部分的需求

总结:JsBridge原理总结.png