如何通过Proxy实现JSBridge模块化封装


Posted in Javascript onOctober 22, 2020

最近公司在做一个项目,通过把我们自己的Webview植入第三方APP,然后我们的业务全部通过H5实现。至于为什么不直接用第三方APP WebView,主要是身处金融行业,需要做一些风控相关功能。

由于是Hybrid APP的性质,所以web与Native的通信是无法避免的;而为什么我要封装jsBridge,主要在于下面两点:

公司APP的JSBridge提供了数据的序列化和全局函数的注入,而我们这次由于包大小考虑,这一块需要H5自己来实现;

原生提供的接口协议太多,记住麻烦;

回调的写法不太人性化,期望Promise;

由于本次项目只涉及到Andriod,所以没有关于ios的处理,但我自认为他们只是协议的不同,Web的处理可以相同。

原理浅谈

如何通过Proxy实现JSBridge模块化封装

看上图的通信实现(图片来源于文章开头的文章),简单说一下通信过程;

Webview加载时会将原生提供的JSBridge方法注入到window对象上,比如:window.JSBridge.getDeviceInfo就是原生提供的可以读取一些设备标识信息的接口;

H5通过window调用原生接口,基本都需要传参,比如这次处理成功或则处理失败的结果回调的,还有一些参数设置,拿上面给的方法来举例:

window.JSBridge.getDeviceInfo({
 token: '*&^%$$#*',
 onOk(data) {
  save(data);
 },
 onError(error) {
  console.log(error.message);
 }
});

原生响应H5的调用成功或失败后,就执行H5传递过来的回调函数;

过程结束;

看上面的通信过程,貌似很简单。但这里面存在一些协议的问题:

首先H5与原生端的通信消息,是只支持字符串的,如果要传JS对象,那就先序列化;

序列化带来的后果又是,对象中的函数就无法传递;

而就算函数传过去了,也是存在问题的,由于安全的限制,webview和js的执行没有在一个容器中,回调这种局部函数是找不到的,所以是需要将回调函数注册到全局;

所以下面就来解决这些问题

一步一步的具体实现

接口协议封装

什么意思喃?看下面的图:

如何通过Proxy实现JSBridge模块化封装

由于APP端协议及分包问题, 存在多个Bridge, 比如MBDevice、MBControl、MBFinance,上面列出来的只是一小部分,对于web来说记忆这些接口是一件很费事的事;还有就是以前我调APP的JSBridge, 总有下面这样的代码:

window.JSBridge && window.JSBridge.getDeviceInfo && window.JSBridge.getDeviceInfo({ ... })

至于上面,所以加了一层封装,实现的核心就是Proxy和Map,具体实现看下面的伪代码:

const MBSDK = {
};

// sdk 提供的方法白名单
const whiteList = new Map([
 ['setMaxTime', 'MBVideo'],
 ['getDeviceInfo', 'MBDevice.getInfo'],
 ['close', 'MBControl'],
 ['getFinaceInfo', 'MBFinance.getInfo'],
]);

const handler = {
 get(target, key) {
  if (!whiteList.has(key)) {
   throw new Error('方法不存在');
  }
  const parentKey = whiteList.get(key);
  function callback() {
   return [...parentKey.split('.'), key];
  }
  return new Proxy(callback, applyHandler); // funcHandler后面再展开
 },
};
export default new Proxy(MBSDK, handler);

基于上面的封装,调用时,代码就是下面这样

sdk.setMaxTime({
   maxTime: 10,
  }).then(() => {
   console.log('设置成功');
  }, () => {
   window.alert('调用失败');
  });

序列化与回调注册

上面已经列了为什么需要回调函数全局注册和序列化,这里主要说一下实现原理,总得来说分两步;

回调函数剥离,全局注册;

参数序列化;

回调函数剥离和参数序列化

其实很好实现,直接展开运算符搞定:

const { onOk, onError, ...others } = params; // 回调函数剥离
const str = JSON.stringify(others); // 参数序列化

函数全局注册

看了很多文章的一些实现,思路基本一致,比如下面这样

window.bridgeCallbacks = {};
const callBacks = window.bridgeCallbacks;
const { onOk, onError, ...others } = params; // 回调函数剥离

const callbackId = generateId(); // 产生一个唯一的随机数Id

callBacks[`success_${callbackId}`] = onOk;
callBacks[`onError${callbackId}`] = onError;

others.success = `window.bridgeCallbacks.success_${callbackId}`
// ....
// 调用jdk代码

这是一种很容易想到的问题,但却存在一些问题,比如:

bridgeCallbacks全局会注册很多属性,因为Native调用并没有清理,而onOk这种很多时候是一个闭包,由于有引用,最后导致的问题就是内存泄露;

就算处理了第一步的问题,webview无响应怎么办,那回调就会被一直挂起,确少超时响应逻辑

callbackId的唯一性不好保证;

基于以上考虑,我换了一个方案,采用回调队列,因为APP端说过,回调是按顺序的,不会插队;

class CallHeap {
 constructor() {
  this.okQueue = [];
  this.errorQueue = [];
 }
 success = (args) => {
  // 成对弹出回调:成功时,不止要处理成功的回调,失败的也要同时弹出,
  const target = this.okQueue.shift();
  this.errorQueue.shift();
  target && target(args);
 }
 error = (args) => {
  const target = this.errorQueue.shift();
  this.okQueue.shift();
  target && target(args);
 }
 addQueue(onOk = Null, onError = Null) {
  this.okQueue.push(onOk);
  this.errorQueue.push(onError);
 }
}

window.bridgeCallbacks = {};
const callBacks = window.bridgeCallbacks;

function applyhandler() {
 const { onOk, onError, ...others } = params; // 回调函数剥离
 if (onOk || onError) {
   const callKey = transferKey || key; // transferKey || key后面会提到
   // 如果全局未注册,则先注册对应的调用域
   if (!callbacks[callKey]) {
    callbacks[callKey] = new CallHeap();
   }
   // 添加回调
   callbacks[callKey].addQueue(onOk, onError);

   others.success = `callBacks.${callKey}.success`;
   others.error = `callBacks.${callKey}.error`;
  }
  // 调用jdk代码
}

基于以上的实现,就可以保证发起多个Native请求,并保证有序回调;如果成功,成功回调被响应时,响应的失败回调也会被弹出,因为回调函数式存在数组中的,所以执行完后,引用就不会再存在。

完整实现

看了上面的代码实现,但核心好像还没有提及,那就是调用参数的拦截。前面我们用Proxy的get优雅的实现了SDK方法的拦截,这里会接着采用Proxy的apply方法来拦截方法调用的传参,直接看代码吧:

// 结合最上面接口协议封装的代码一起看
const applyHandler = {
 apply(target, object, args) {
  // transferKey 用于getFinaceInfo与getDeviceInfo这种数据命名重复的
  const [parentKey, key, transferKey] = target();
  console.log('res', parentKey, key);
  const func = (SDK[parentKey] || {})[key];

  const { onOk, onError, ...params } = args[0] || {};

  if (onOk || onError) {
   const callKey = transferKey || key;
   if (!callbacks[callKey]) {
    callbacks[callKey] = new CallHeap();
   }
   callbacks[callKey].addQueue(onOk, onError);

   others.success = `callBacks.${callKey}.success`;
   others.error = `callBacks.${callKey}.error`;
  }

  return func && (window[parentKey][key])(JSON.stringify(params));;
 }
};

Promise 封装

前面吹过的牛逼还有两个没实现,比如:

promise支持

超时调用

首先来复习一下,怎么封装一个支持Promise的setTimeout函数:

function promiseTimeOut(time) {
 return new Promise((resolve, reject) => {
  setTimeout(resolve, time);
 });
}

promiseTimeOut(1000).then(() => {
 console.log('time is ready');
})

如果对上面这个封装不陌生,那基于回调函数的Promise化就变得简单了

talk is cheap, show me your code

完整实现:

const MBSDK = {
};

// sdk 提供的方法白名单
const whiteList = new Map([
 ['setMaxTime', 'MBVideo'],
 ['getDeviceInfo', 'MBDevice.getInfo'],
 ['close', 'MBControl'],
 ['getFinaceInfo', 'MBFinance.getInfo'],
]);

const applyHandler = {
 apply(target, object, args) {
  // transferKey 用于getFinaceInfo与getDeviceInfo这种数据命名重复的
  const [parentKey, key, transferKey] = target();
  // FYX 编程
  const func = (window[parentKey] || {})[key];
  // 设置一个默认的超时参数,支持配置
  const { timeout = 5000, ...params } = args[0] || {};

  return new Promise((resolve, reject) => {
   const callKey = transferKey || key;
   if (!callbacks[callKey]) {
    callbacks[callKey] = new CallHeap();
   }
   const timeoutId = setTimeout(() => {
    // 超时,主动发起错误回调
    window.callBacks[callKey].error({ message: '请求超时' });
   }, timeout);
   callbacks[callKey].addQueue((data) => {
    clearTimeout(timeoutId);
    resolve(data);
   }, (data) => {
    clearTimeout(timeoutId);
    reject(data);
   });
   params.success = `callBacks.${callKey}.success`;
   params.error = `callBacks.${callKey}.error`;
   func && (window[parentKey][key])(JSON.stringify(params));
  }).catch((error) => {
   console.log('error:', error.message);
  });
 }
};

const handler = {
 get(target, key) {
  if (!whiteList.has(key)) {
   throw new Error('方法不存在');
  }
  const parentKey = whiteList.get(key);
  function callback() {
   return [...parentKey.split('.'), key];
  }
  return new Proxy(callback, applyHandler); // funcHandler后面再展开
 },
};

export default new Proxy(MBSDK, handler);

而调用时,基本上,就可以这样玩了:

sdk.setMaxTime({
   maxTime: 10,
  }).then(() => {
   console.log('设置成功');
  }, () => {
   window.alert('调用失败');
  });

解惑

- func.call(null, JSON.stringify(params)) // 以前的

+ func && (window[parentKey][key])(JSON.stringify(params)); // 现在的

开始函数的调用是采用func.call来实现的,当时我本地mock过,没有问题。但在webview中就弹出了下面这样一个错误:

java bridge method can't be invoked on a non-injected object

经过各种goggle,百度,查到的都是一条关于Andriod的注入漏洞。而至于我这里通过JS的方式把bridge指向的函数地址,赋值给一个变量名,然后再通过变量名来调用就会报上面这个错误,我个人的猜测有两个:一是协议这样规定的;二是this指向问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
原生javascript获取元素样式
Dec 31 Javascript
TypeOf这些知识点你了解吗
Feb 21 Javascript
bootstrap下拉列表与输入框组结合的样式调整
Oct 08 Javascript
AngularJS  双向数据绑定详解简单实例
Oct 20 Javascript
EasyUI学习之DataGird分页显示数据
Dec 29 Javascript
从零学习node.js之文件操作(三)
Feb 21 Javascript
jQuery实现多张图片上传预览(不经过后端处理)
Apr 29 jQuery
JS实现获取汉字首字母拼音、全拼音及混拼音的方法
Nov 14 Javascript
收集前端面试题之url、href、src
Mar 22 Javascript
详解React的回调渲染模式
Sep 10 Javascript
vue Cli 环境删除与重装教程 - 版本文档
Sep 11 Javascript
javascript函数式编程基础
Sep 15 Javascript
微信小程序canvas动态时钟
Oct 22 #Javascript
vue-cli —— 如何局部修改Element样式
Oct 22 #Javascript
微信小程序入门之绘制时钟
Oct 22 #Javascript
微信小程序入门之指南针
Oct 22 #Javascript
微信小程序实现拼图小游戏
Oct 22 #Javascript
Vue select 绑定动态变量的实例讲解
Oct 22 #Javascript
在Vue中使用Select选择器拼接label的操作
Oct 22 #Javascript
You might like
Syphon 虹吸式咖啡壶冲煮–拨动法
2021/03/03 冲泡冲煮
php smarty 二级分类代码和模版循环例子
2011/06/01 PHP
解析如何去掉CodeIgniter URL中的index.php
2013/06/25 PHP
PHP return语句的另一个作用
2014/07/30 PHP
php简单生成一组与多组随机字符串的方法
2017/05/09 PHP
php中输出json对象的值(实现方法)
2018/03/07 PHP
php集成开发环境详解
2019/09/24 PHP
prototype.js的Ajax对象
2006/09/23 Javascript
asp.net和asp下ACCESS的参数化查询
2008/06/11 Javascript
jQuery lazyLoad图片延迟加载插件的优化改造方法分享
2013/08/13 Javascript
jquery实现的下拉和收缩效果示例
2014/08/21 Javascript
js交换排序 冒泡排序算法(Javascript版)
2014/10/04 Javascript
javascript实现通过表格绘制颜色填充矩形的方法
2015/04/21 Javascript
JS简单获取客户端IP地址的方法【调用搜狐接口】
2016/09/05 Javascript
jQuery动态移除与增加onclick属性的方法详解
2018/06/07 jQuery
ES6 系列之 Generator 的自动执行的方法示例
2018/10/19 Javascript
使用 Vue 实现一个虚拟列表的方法
2019/08/20 Javascript
vue项目中使用particles实现粒子背景效果及遇到的坑(按钮没有点击响应)
2020/02/11 Javascript
vue从零实现一个消息通知组件的方法详解
2020/03/16 Javascript
浅谈pyhton学习中出现的各种问题(新手必看)
2017/05/17 Python
浅谈python3.6的tkinter运行问题
2019/02/22 Python
django一对多模型以及如何在前端实现详解
2019/07/24 Python
Python解析json时提示“string indices must be integers”问题解决方法
2019/07/31 Python
对python中的*args与**kwgs的含义与作用详解
2019/08/28 Python
通过Python实现一个简单的html页面
2020/05/16 Python
python Timer 类使用介绍
2020/12/28 Python
配置H5的滚动条样式的示例代码
2018/03/09 HTML / CSS
娇韵诗法国官网:Clarins法国
2019/01/29 全球购物
全球最受追捧的运动服品牌领先数字目的地:Stylerunner
2020/11/25 全球购物
大专学生推荐信范文
2013/11/19 职场文书
四年大学自我鉴定
2014/02/17 职场文书
幼儿园大班开学寄语
2014/08/02 职场文书
房地产经营管理专业自荐信
2014/09/02 职场文书
承兑汇票转让证明怎么写?
2014/11/30 职场文书
人事行政部各岗位职责说明书!
2019/07/15 职场文书
使用kubeadm命令行工具创建kubernetes集群
2022/03/31 Servers