由于 Webview 内嵌 H5 的性能/功能各种受限,于是有了各种的混合开发解决方案,例如 Hybrid、RN、WEEX、Flutter、小程序、快应用等等。

React Native 至今没有推出 1.0 版本,由于各种可能的坑,一些 hold 不住的团队可能会放弃。 Flutter 是否可替代 RN,真正实现两端统一,拭目以待,他从头到尾重写一套跨平台的 UI 框架,包括 UI 控件、渲染逻辑甚至开发语言。我本人之后会关注学习一下。 小程序 不用说太多了,大家都很熟悉了;微信、支付宝、百度都在用。除了第一次需要花点时间下载,体验上可以说是很不错了,但是封闭性是他很大的一个缺点。 快应用 目标是很好的,统一 API,但是还是要看各厂家的执行力度。

现在来总结一下我们团队目前使用的 Hybrid 方案。算是回顾一下,巩固基础,好记性不如烂笔头。

# 一、Hybrid 简介

Hybrid 可以说是上面提到的几种里最古老,最成熟的解决方案了。

缺点是明显的:H5 有的缺点他几乎都有,比如性能差、JS 执行效率低等等。

但是优点也很显著:随时发版,不受应用市场审核限制(当然这个前提是 Hybrid 对应 Native 的功能都已准备就绪);拥有几乎和 Native 一样的能力,eg:拍照、存储、加日历等等...

基本原理 Hybrid 利用 JSBridge 进行通信的基本原理网上一搜一大把,简单记录一下。

Native => JS 两端都有现成方法。谁让都在别人的地盘下面玩呢,Native 当然有办法来执行 JS 方法。 iOS

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];
1
2
3
4

Android

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//这里的 value 即为对应 JS 方法的返回值
}
});
1
2
3
4
5
6

JS => Native 对于 Webview 中发起的网络请求,Native 都有能力去捕获/截取/干预。所以 JSBridge 的核心就是设计一套 url 方案,让 Native 可以识别,从而做出响应,执行对应的操作就完事。 例如,正常的网络请求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png 我们可以自定义协议,改成 jsbridge://methodName?param1=value1&param2=value2。 Native 拦截 jsbridge 开头的网络请求,做出对应的动作。 最常见的做法就是创建一个隐藏的 iframe 来实现通信。

# 二、现成的解决方案

iOS WebViewJavascriptBridge Android JsBridge

基本原理都相同,项目的设计就决定了一个它的可扩展性&可维护性。良好的可扩展性&可维护性对于 JSBridge 尤为重要,他是后面一切业务的基石。

基础库简析 (下面都以 Android 为例)

# 1、 初始化

类似写普通 H5 页面需要监听 DOMContentLoaded 或者 onLoad 来决定开始执行脚本一样,JSBridge 需要一个契机去告诉 JS,我准备好了,你可以来调用我的方法了。

[前端] 执行监听 && 检测

if (window.WebViewJavascriptBridge) {
  //do your work here
} else {
  document.addEventListener(
    "WebViewJavascriptBridgeReady",
    function() {
      //do your work here
    },
    false
  );
}
1
2
3
4
5
6
7
8
9
10
11

[Native (埋在端里的 JS)] dispatchEvent 触发

var WebViewJavascriptBridge = (window.WebViewJavascriptBridge = {
  init: init,
  send: send,
  registerHandler: registerHandler,
  callHandler: callHandler,
  _fetchQueue: _fetchQueue,
  _handleMessageFromNative: _handleMessageFromNative
});

var readyEvent = doc.createEvent("Events");
readyEvent.initEvent("WebViewJavascriptBridgeReady");
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2、JS 调 Native 方法

先上代码,下面是埋在端内的,JSBridge.callHandler,用来实现 JS 调用 Native。

// 调用线程
function callHandler(handlerName, data, responseCallback) {
  _doSend(
    {
      handlerName: handlerName,
      data: data
    },
    responseCallback
  );
}

//sendMessage add message, 触发native处理 sendMessage
function _doSend(message, responseCallback) {
  if (responseCallback) {
    var callbackId = "cb_" + uniqueId++ + "_" + new Date().getTime();
    responseCallbacks[callbackId] = responseCallback;
    message.callbackId = callbackId;
  }

  sendMessageQueue.push(message);
  messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + "://" + QUEUE_HAS_MESSAGE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

jsbridge.callHandler 是 JS 调 Native 方法的核心。 handlerName 是前端与 Native 协商好的方法名称 data 参数 responseCallback 回调

回调函数绑在了一个内部对象中 var responseCallbacks = {},发送给 Native 的消息 message 中只包含了这个回调函数对应的 id,端上处理完成之后触发&销毁。

这个方法并不直接把消息全部推送走,而是存在一个队列中 sendMessageQueue。同时通知 Native,有新数据(message)需要处理。即上面代码的最后一行,他利用 iframe 的 src 通知端上的信息如下:

var CUSTOM_PROTOCOL_SCHEME = "sn";
var QUEUE_HAS_MESSAGE = "__sn__queue_message__";
1
2

上面提到的,JS 只是通知了端上有新消息,Native 调用获取时机暂时不考虑,就假设他收到一条就处理一次,极端高频情况下,两三条处理一次。Native 通过_fetchQueue 统一处理存储在 sendMessageQueue 中的数据:

// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
function _fetchQueue() {
  var messageQueueString = JSON.stringify(sendMessageQueue);
  sendMessageQueue = [];
  //android can't read directly the return data, so we can reload iframe src to communicate with java
  if (messageQueueString !== "[]") {
    bizMessagingIframe.src =
      CUSTOM_PROTOCOL_SCHEME +
      "://return/_fetchQueue/" +
      encodeURIComponent(messageQueueString);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

这些基本就是 JS 主动调用 Native 的流程,关于回调方法,下面统一说。

# 3、Native 调 JS 方法

虽说 Native 可以随意执行 JS,但是总是需要知道哪些 JS 方法是可执行的吧。registerHandler 就是用来执行注册。 registerHandler 在 Native 端定义(是 JSBridge 对象的一个方法),由前端来注册。

// 注册线程 往数组里面添加值
function registerHandler(handlerName, handler) {
  messageHandlers[handlerName] = handler;
}
1
2
3
4

Native 主动调用。 Native 主动调用分两种情况,1 是 Native 主动触发前端事件,例如通知前端页面可视状态变化。2 是前端调用 Native 的回调。JSBridge 是天生异步的,所以回调和主动调用归结到一类里面了。 如果是前端主动调用的方法,有 responseId,即有回调,直接调用执行即可。 否则就去注册的 messageHandlers 中寻找方法,调用。

//提供给native使用,
function _dispatchMessageFromNative(messageJSON) {
  setTimeout(function() {
    var message = JSON.parse(messageJSON);
    var responseCallback;
    //java call finished, now need to call js callback function
    // 前端主动调用的Callback
    if (message.responseId) {
      responseCallback = responseCallbacks[message.responseId];
      if (!responseCallback) {
        return;
      }
      responseCallback(message.responseData);
      delete responseCallbacks[message.responseId];
    } else {
      // Native主动调用
      //直接发送
      if (message.callbackId) {
        var callbackResponseId = message.callbackId;
        responseCallback = function(responseData) {
          _doSend({
            responseId: callbackResponseId,
            responseData: responseData
          });
        };
      }

      var handler = WebViewJavascriptBridge._messageHandler;
      if (message.handlerName) {
        handler = messageHandlers[message.handlerName];
      }
      //查找指定handler
      try {
        handler(message.data, responseCallback);
      } catch (exception) {
        if (typeof console != "undefined") {
          console.log(
            "WebViewJavascriptBridge: WARNING: javascript handler threw.",
            message,
            exception
          );
        }
      }
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

代码分析基本就到这里,盗一张图(地址放在最后了),把流程都画了出来,个人感觉没啥问题

# 三、业务封装

直接使用前面的库可以完成功能,但是不够优雅,代码不经过良好的设计可能会变得牵一发动全身,可维护性差。下面说说我们的设计,可能不是最好的,但是是很符合我们业务场景的。

事件基础类 EventClass 处理事件广播、订阅。 连接基础类 ConnectClass


- 创建和获取 jsbridge 基础类
- @class ConnectClass
- @extends EventsClass

  class ConnectClass extends EventsClass {

- 获取 jsbridge 实例,注入到 sncClass 上的 bridge 属性 `this.bridge`

  connect() {
  // 事件广播,通知开始建立连接,统计使用
  // 建立 JSBridge
  // 建立 JSBridge.then 1.注册 Native 主动调用的事件,对应上面的 bridge.registerHandler;2.广播 建立完成,统计使用
  }


// ... 其他的一些方法
// eg: 分平台初始化 JSBridge,处理差异性
// eg: bridge.registerHandler 回调的封装一层的统一处理函数

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

关于注册 Native 主动调用的事件(和下面会提到的 JS 主动调用事件),实现插件化,并同一封装。好处是可以明确代码执行步骤、方便业务同学调试(这不是我的锅,我已经执行调用了...)、方便性能统计。

业务类


class SncClass extends ConnectClass {
constructor(option){
// 监听 connect,监听首屏数据
// 建立连接 this.connect
// 挂载必备 API
}

// 初始化,根据参数决定挂载哪些 api
init(apis){
this.mountApi(apis);
}




- 挂载 api
- @param {Object} apis api 对象集合


mountApi(apis) {
// 1. 错误处理
// 2. 检测是否已经 jsb 建立连接 已连接则 直接执行真正挂载函数 return
// 3. bridge 未初始化时,定义方法预声明。执行的方法将会被储存在缓存队列里在 bridge 初始化后调用
// 4. 监听连接事件,执行真正挂载 loadMethods
}
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • 加载 API 到实例属性,标志着 api 的真正挂载

loadMethods(apis) {
// 1. 防止重复挂载 api,
// 2. 给插件初始化方法注入 ctx,让插件得以调用库内真正的初始化函数,即封装一层的上面提到的 callHandler
}

// ... 其他实例方法,比如 extend,得以在业务中和 Native 互相约定新的非通用 JSB,方便扩展
初始化
导出单例 appSNC,拥有的方法都在 appApis 中定义,如果有新的业务需求直接扩展此文件夹中内容即可。
import \* as apis from '../appApis'; // 方法集合
import SNC from './sdk'; // 上面的 SncClass
const option = {} ; // 一些配置
const appSNC = new SNC(option);

export default appSNC.init(apis);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

以上就是我们正在使用的方案,总结一下,不断积累。

# 参考

干货!移动端真机调试指南,对调试说easy (opens new window)