JSBridge 学习笔记

在移动互联网的时代,开发移动端的前端页面是前端同学一项必不可少的技能了。而涉及到与原生移动端的交互,除了一些 WEEX、React Native 这种技术外,最常用也是最快捷的方式就是 JSBridge 了。所以,作为前端非常有必要了解一下 JSBridge 的运作原理。

移动端的浏览器控件

当下主流的移动端操作系统无疑是 Android 和 IOS。而这两者分别都提供了各自承载网页的控件 WebView(IOS 里面是 UIWebView 和 WKWebView)。下面简单介绍下浏览器控件。

Android WebView

Android 的 WebView 能够像其他的浏览器 APP 一样显示网页并对页面做一些基础的操作。由于历史原因,Android WebView 采用了两种不同的内核。

在 Android 4.4 以下(不包含 4.4)系统 WebView 底层实现是采用 WebKit(http://www.webkit.org/) 内核,而在 Android 4.4 及其以上 Google 采用了 chromium(http://www.chromium.org/) 作为系统WebView的底层内核支持。

所以,如果需要适配 4.4 以下机型需要做好样式和JS的适配兼容工作。

IOS 的 UIWebView 和 WKWebView

在 IOS 中也是由于历史原因,出现了两个 WebView 控件。 UIWebView 是自 IOS2 就有的,而 WKWebView 从 IOS8 才有。WKWebView 使用的是 Safari 浏览器内核,相比于笨重的 UIWebView 性能更佳。

如果需要支持 IOS8 之前的版本,只好去兼容 UIWebView ,而在 IOS8+ 的版本中,就有 UIWebView 和 WKWebView 两种选择了(具体看 native 端的实现选择)。

简单介绍了下两个操作系统的浏览器控件的情况,更佳具体的移动前端问题可以参考我之前整理的2018 年最新的移动前端资料整理(不断更新)
一文。

JSBridge 原理及实现

具体的原理在有赞前端的# H5与Native交互之JSBridge技术 一文中讲的非常清楚,不多赘述。简单说下:

IOS 调用 JavaScript 方法

在 IOS 中使用 stringByEvaluatingJavaScriptFromString 方法直接调用挂载在前端 window 下的方法,并获取方法返回的数据。

webview.stringByEvaluatingJavaScriptFromString("Math.random()")

JavaScript 调用 IOS 方法

有两种方法可以实现 JavaScript 调用 IOS 方法这个行为:

1. 在 JavaScript 中使用创建一个 iframe 请求 URL 的方式将带有 scheme 的 URL 传给 IOS。

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
    iframe.remove();
}, 100);

IOS 在 WebView 获取到网页 URL 请求时将 scheme 码的 URL 拦截下来去做 native 方法的处理。

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
    
            return false
        } else {
            return true
        }
    }

2. 对于 WKWebView 有另外的解决方案:iOS下OC与JS的交互(WKWebview-MessageHandler实现)

简单说下实现逻辑:在 IOS 中使用 addScriptMessageHandler 方法定义 MessageHandler。

[userContentController addScriptMessageHandler:self name:@"Share"];
[userContentController addScriptMessageHandler:self name:@"Camera"];

其中的 Share 和 Camera 是 MessageHandler 的 name 属性。在 JS 端的使用 MessageHandler 的方式如下:

window.webkit.messageHandlers.<name>.postMessage(<messageBody>)

最后回到 IOS 端根据 MessageHandler 的 name 获取它的 body 并处理执行相应操作行为。

if ([message.name isEqualToString:@"Share"]) {
    [self ShareWithInformation:message.body];    
} else if ([message.name isEqualToString:@"Camera"]) {
    [self camera];
}

Android 调用 JavaScript 方法

在 Android 中直接使用 loadUrl 方法即可调用 JavaScript 中 window 对象上的方法。

webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

JavaScript 调用 Android 方法

有 3 种方法:

  1. 通过 shouldOverrideUrlLoading 方法处理 URL 的 scheme 码,做法类似 IOS 的通信方式。
  2. Android 中有一个 JSInterface 类可以直接注入原生 JS 代码。
class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的 window 对象里注入了 AndroidJS 对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate
  1. 使用 prompt,console.log,alert 方式。

LJBridge 学习

m-base 源码简析

说了这么多,作为前端最关注必然不是 Native 端如何实现,而是 JavaScript 端如何与 Native 端通信啦。下面来看看链家的 JSBridge 源码方法:

function tryCatch(callback) {
  // try…catch 执行 Native 方法
}
function setBridgeInstance(bridge) {
  // 设置 JSBridge 实例
}
function setBridgeInstanceForWK(bridge) {
  // 设置 JSBridge 实例(适配 WKWebView)
}
function do_callback() {
  // 执行 ready 方法中的回调函数
}
function bindSysSchema(evt){
  // 绑定系统 scheme (打电话和发短信)
}
// 在 window 对象中定义 ljBridge 来使用 JSBridge
window.$ljBridge = {
  ready: function() {},
  webStatus: webStatus
}
// 初始化逻辑
if (isApp) {
  // 将 APP 数据保存 cookie
  // 获取 bridgeInstance
  // 设置 Title、RightButton、ShareConfig
} else {
  // 非 APP 环境调试
}

LJBridge 如何通信

基于一开始讲过的 JSBridge 通信原理,我们来理解下 m-base 源码中的通信。

所有在 H5与APP通信文档(JS bridge) 文档中接口方法都可以在 setBridgeInstancesetBridgeInstanceForWK 方法中看到。

JS 获取 Native 端数据

在这些方法中,像 getNetwork、getDeviceId 这类获取 Native 信息的方法都是直接获取注入的 window.HybridBridgeLJ 对象中的数据。

  // 获取APP注入的用户device id
  bridgeInstance.getDeviceId = function() {
    return objectValueFromPath(bridge, 'staticData.deviceId');
  };
  // 获取APP版本
  bridgeInstance.getAppVersion = function() {
    return objectValueFromPath(bridge, 'staticData.appVersion');
  };
  // 获取APP网络状态
  bridgeInstance.getNetwork = function() {
    return objectValueFromPath(bridge, 'staticData.network');
  };
  // 获取APP的协议
  bridgeInstance.getScheme = function() {
    return objectValueFromPath(bridge, 'staticData.scheme');
  };

往 Android 中注入 JS 上面已经介绍了,而往 IOS 中注入 JS 可以查看 WKWebview:与JS交互数据传值、Cookies的注入与清除 一文。

JS 调用 Native 端方法

setRightButtonsetCity 这些 JavaScript 调用 Native 方法的行为,有两种不同的方式:

第一种是直接调用注入的 window.HybridBridgeLJ 对象中的方法,原理上面提到过的 JavaScript 调用 Android 方法的第 2 条。

bridge.setShareConfigWithString(paramString);

第二种是针对 WKWebView 的 MessageHandler 形式进行 JS 到 Native 端的通信。

window.webkit.messageHandlers.lianjia.postMessage(JSON.stringify({
  "type": "setShareConfig",
  "param": paramString
}));

LJBridge 的 ready 方法

ready 是在 JSBridge 的 JS 文件加载完成后可以被调用,以获取 bridge 对象进行通信。

下面是 ready 方法的使用及其源码:

$ljBridge.ready((bridge, webStatus) => {
  // ...
})
// 可以定义 ready 方法和 web 状态值
window.$ljBridge = {
  ready: function(fn, context) {
    callbacks.push(arguments.length > 1 ? {
      fn: fn,
      context: context
    } : {
      fn: fn
    });
    requestAnimationFrame(do_callback);
  },
  webStatus: webStatus
}

var callbacks = [];

// 执行回调函数
function do_callback() {
  if (bridgeInstance === null) return
  var callback
  while (callback = callbacks.shift()) {
    var fn = callback.fn;
    try {
      if ('context' in callback) {
        fn.call(callback.context, bridgeInstance, webStatus);
      } else {
        fn(bridgeInstance, webStatus);
      }
    } catch (e) {
      if ('console' in window) {
        console.error ? console.error(e) : console.log(e);
      }
    }
  }
}

最后

最后总结一下:

  • Android 有一个 WebView 控件,注意 4.4 版本前后的浏览器内核差异。
  • IOS 有 UIWebView 和 WKWebView 两个浏览器,两者对于 JS 的通信方式是不同的。
  • JS 调用 Android 有 3 种方法
  • JS 调用 IOS 有 2 种方法,对应 UIWebView 和 WKWebView
  • Android 和 IOS 都可以往 JS 的 window 对象中注入数据对象。
  • 链家 JSBridge 也是使用了以上的方案来实现。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容