【架构拾集】移动应用 WebView 的 bridge 设计

最近的一个项目,是一个关于在移动应用中嵌入 WebView,并在其中实现 JavaScript 能调用原生代码。虽然已经有 Cordova 这样的成熟方案,但是它太 “重” 了——直接在一个拥有大量代码的项目上,修改起来并不容易。

原先,我并不打算花费笔墨来记录相关的实现。毕竟,JavaScript Bridge 相关的方案相当的成熟——到处可风,但是考虑到其中的一些设计思想相当不错。我还是想存个档下来,方便自己以后查阅。它之所以成为了我的第 701 篇博客,也不是没有理由的,毕竟年纪一大记忆力就不好了。

广告时间:作为一个前端开发人员,我更推荐在底层可以使用 React Native,可以节省开发成本。也可以使用我之前写的基于 React Native 实现的 WebView 容器 Dore,这样能节省大量的开发成本。

技术远景

作为项目的开发人员,我们希望能提供一个 WebView 的 APP 壳,它可以提供 WebView 访问原生组件的能力。

适用场景

关键因素描述
对于想通过 JavaScript 调用原生代码的开发人员
我们的方案JS Bridge 接口
是一个轻量级 JavaScript Bridge
它可以在 APP 内提供原生代码调用 JavaScript、JavaScript 调用原生代码的接口
但他不同于Cordova
它的优势是代码量低、维护成本低

方案对比

对于这个项目来说,在最开始的时候,我们设计了两个方案:

  • 同步数据流,使用常规的 HTTP 来请求数据,在请求的 promise 里返回相应的结果。

  • 异步回调,通常 iframe 的方式创建 HTTP 请求,在原生代码执行成功后,调用相应的 JavaScript 来返回对应的结果。

之所以有两种方案的原因,也是因为两种方案各有优缺点

同步数据流

第一版里,我们截获 HTTP 数据流,而后调用相应的响应结果:

 
 
  1. this.http.get("bridge://picture").then((res: any) => {

  2.  console.log(res.json());

  3. });

这样做的过程中,遇到了一个问题,即 HTTP 请求的 Timeout。如果我们的请求超时了,那么我们的方法调用就失败了。在 APP 中有诸多这样的场景,如用户选择相册的时候,或者用户拍照的时候。

异步调用

第二个版本里,我们则使用在原生代码里直接调用 JavaScript 。这是最常见的一种方式,知名的混合应用框架 Cordova 就是使用这种方式来实现的。

其对应的逻辑便是:

  1. 从 JavaScript 代码中发起对原生代码的调用

  2. 生成对应的 callbackId 和函数的映射,并绑定到全局的 window 变量中

  3. 原生代码执行完后,调用 JavaScript 代码

  4. JavaScript 代码通过传回的 Callback ID 找到、调用对应的函数

  5. 完成后,按需要删除相应的 Callback ID

(PS:Callback ID 用于识别不同的函数)

这个时候,我们就需要手动提供 JavaScript 的接口,相应的调用逻辑也就变成了:

 
 
  1. jsbridge.get("bridge://picture").then((res: any) => {

  2.  console.log(res.json());

  3. });

两种方式的区别并不是太大。不过这样做有一个好处是,避免与 HTTP 请求之间的混淆。

方案调研

由于 WebView 的 JavaScript ,因此我们不妨先大致了解一下现有的几种设计。

  • 覆写原生 WebView 的方法来捕获请求,如 WebView 中的 prompt 弹出对话框方法

  • 在 WebView 中创建相应调用的数组,而后通过轮询数组来获取方法

  • 通过 iframe 来创建 HTTP 请求,在 APP 壳对对应的请求进行处理

下面则是关于 Cordova 一些方案的设计。

JavaScript 调用原生

对于 JavaScript 调用原生方面的实现,做得比较成功的莫过于 Cordova 框架了,于是我们可以先简单地了解一下其中的实现。

Cordova 版本: Android 实现

由于早期 Android 版本(即 JellyBean,4.1 之前)不支持使用 addJavascriptInterface,所以提供了两种方式实现:

 
 
  1. jsToNativeModes = {

  2.  PROMPT: 0,

  3.  JS_OBJECT: 1

  4. }

PROMPT 即通过覆写 DOM 中的对话框方法来接收参数, JS_OBJECT 的方式是通过 addJavascriptInterface 来实现。在 addJavascriptInterface 不支持的情况下,自动转换为使用 prompt 方式实现。

对应的调用原生代码逻辑,如下所示:

 
 
  1. function androidExec(success, fail, service, action, args) {

  2.  ...

  3.  var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);

  4.  ...

  5. }

如果其通过 JS_OBJECT 调用失败,则会使用 PROMPT 方法实现,其调用 prompt 的实现如下所示:

 
 
  1. module.exports = {

  2.  exec: function(bridgeSecret, service, action, callbackId, argsJson) {

  3.  return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));

  4.  },

  5.  setNativeToJsBridgeMode: function(bridgeSecret, value) {

  6.  prompt(value, 'gap_bridge_mode:' + bridgeSecret);

  7.  },

  8.  retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) {

  9.  return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret);

  10.  }

  11. };

而后在 Android 的 Java 代码中覆写 onJsPrompt 方法,

 
 
  1. @Override

  2. public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {

  3.  // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.

  4.  String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);

  5.  ...

  6.  return true;

  7. }

逻辑大体上就是这样。

Cordova 版本:iOS 实现

iOS 端在实现上,就没有 Android 这么复杂,则是先往 commandQueue 中 push 数据:

 
 
  1. var command = [callbackId, service, action, actionArgs];

  2. commandQueue.push(JSON.stringify(command));

然后通过创建 iframe,告知 iOS 原生代码可以处理数据了:

 
 
  1. if (execIframe && execIframe.contentWindow) {

  2.  execIframe.contentWindow.location = 'gap://ready';

  3. } else {

  4.  execIframe = document.createElement('iframe');

  5.  execIframe.style.display = 'none';

  6.  execIframe.src = 'gap://ready';

  7.  document.body.appendChild(execIframe);

  8. }

原理大致是这样的:

Callback ID 生成与绑定

对应的生成 ID 的逻辑比较简单:

 
 
  1. var cordova = {

  2.  ...

  3.  callbackId: Math.floor(Math.random() * 2000000000),

  4.  callbacks: {}

  5. }

代码绑定相应的逻辑便是:

 
 
  1. function androidExec(success, fail, service, action, args) {

  2.  ...

  3.  if (successCallback || failCallback) {

  4.  callbackId = service + cordova.callbackId++;

  5.  cordova.callbacks[callbackId] =

  6.  {success:successCallback, fail:failCallback};

  7.  }

  8.  ...

  9. }

原生代码触发 JavaScript

不论是 iOS 还是 Android,最后都可以直接调用相应的 JavaScript 函数。于是,在被调用之后我们只需要进行对应的处理即可:

 
 
  1. callbackFromNative: function (callbackId, isSuccess, status, args, keepCallback) {

  2.  try {

  3.  var callback = cordova.callbacks[callbackId];

  4.  if (callback) {

  5.  if (isSuccess && status === cordova.callbackStatus.OK) {

  6.  callback.success && callback.success.apply(null, args);

  7.  } else if (!isSuccess) {

  8.  callback.fail && callback.fail.apply(null, args);

  9.  }

  10.  if (!keepCallback) {

  11.  delete cordova.callbacks[callbackId];

  12.  }

  13.  }

  14.  } catch (err) {

  15.  ...

  16.  }

  17. }

相应的逻辑便是,根据 callbackId 获取对应的 callback 方法。如果存在 callback 方法,则调用相应的成功或者失败回调,再根据需要决定是否删除对应的回调方法。

架构设计方案

为了便于 Android 和 iOS 统一,并且能快速实现,我们直接使用了 iframe 来创建请求。目前市面上的一些混合应用框架,采用的都是类似的形式,如豆瓣的 rexxar-web,相关的代码如下:

 
 
  1. export function callUri(uri) {

  2.  let iframe = document.createElement('iframe');

  3.  iframe.src = uri;

  4.  iframe.style.display = 'none';

  5.  document.documentElement.appendChild(iframe);

  6.  setTimeout(() => document.documentElement.removeChild(iframe), 0);

  7. }

即创建了一个 iframe,随后添加到 WebView 中就会创建对应的 HTTP 请求,然后立马删除了这个 iframe。过程中,原生 APP 就能捕获到对应的请求,并进行处理。由于在某些版本的 Android 系统中,timeout = 0 太短,需要改长一些。

对于 callbackId 的处理,则相似的,创建一个数组,然后添加值进去。

 
 
  1. if (typeof callback == 'function') {

  2.  callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();

  3.  callbackArray[callbackId] = callback;

  4. }

再把 callbackId 添加到 URL 中即可。对应的,我们要调用的方式也比较简单:

 
 
  1. ...

  2. callbackArray[callbackId](result.result);

  3. ...

由于 callback 在 Angular 框架上,不能被 Zone.js 监测到。我们添加了 Promise 的支持,即当:

 
 
  1. if (typeof callback == 'function') {

  2.  ...

  3. } else {

  4.  return new Promise(resolve: any, reject: any) => {

  5.  ...

  6.  });

  7. }

结论

对于一个多方协作,跨域不同平台的系统设计来说,这并不是一件容易的事。在这个方案里,是 Android 和 iOS 对于不同请求方式的处理,是一个比较复杂的地方。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SQLAlchemy 是一个 SQL 工具包和对象关系映射(ORM)库,用于 Python 编程语言。它提供了一个高级的 SQL 工具和对象关系映射工具,允许开发者以 Python 类和对象的形式操作数据库,而无需编写大量的 SQL 语句。SQLAlchemy 建立在 DBAPI 之上,支持多种数据库后端,如 SQLite, MySQL, PostgreSQL 等。 SQLAlchemy 的核心功能: 对象关系映射(ORM): SQLAlchemy 允许开发者使用 Python 类来表示数据库表,使用类的实例表示表中的行。 开发者可以定义类之间的关系(如一对多、多对多),SQLAlchemy 会自动处理这些关系在数据库中的映射。 通过 ORM,开发者可以像操作 Python 对象一样操作数据库,这大大简化了数据库操作的复杂性。 表达式语言: SQLAlchemy 提供了一个丰富的 SQL 表达式语言,允许开发者以 Python 表达式的方式编写复杂的 SQL 查询。 表达式语言提供了对 SQL 语句的灵活控制,同时保持了代码的可读性和可维护性。 数据库引擎和连接池: SQLAlchemy 支持多种数据库后端,并且为每种后端提供了对应的数据库引擎。 它还提供了连接池管理功能,以优化数据库连接的创建、使用和释放。 会话管理: SQLAlchemy 使用会话(Session)来管理对象的持久化状态。 会话提供了一个工作单元(unit of work)和身份映射(identity map)的概念,使得对象的状态管理和查询更加高效。 事件系统: SQLAlchemy 提供了一个事件系统,允许开发者在 ORM 的各个生命周期阶段插入自定义的钩子函数。 这使得开发者可以在对象加载、修改、删除等操作时执行额外的逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值