【quickhybrid】JS端的项目实现
前言
API实现阶段之JS端的实现,重点描述这个项目的JS端都有些什么内容,是如何实现的。
不同于一般混合框架的只包含JSBridge部分的前端实现,本框架的前端实现包括JSBridge部分、多平台支持,统一预处理等等。
项目的结构
在最初的版本中,其实整个前端库就只有一个文件,里面只规定着如何实现JSBridge和原生交互部分。但是到最新的版本中,由于功能逐步增加,单一文件难以满足要求和维护,因此重构成了一整个项目。
整个项目基于ES6
、Airbnb代码规范
,使用gulp + rollup
构建,部分重要代码进行了Karma + Mocha
单元测试
整体目录结构如下:
quickhybrid
|- dist // 发布目录
| |- quick.js
| |- quick.h5.js
|- build // 构建项目的相关代码
| |- gulpfile.js
| |- rollupbuild.js
|- src // 核心源码
| |- api // 各个环境下的api实现
| | |- h5 // h5下的api
| | |- native // quick下的api
| |- core // 核心控制
| | |- ... // 将核心代码切割为多个文件
| |- inner // 内部用到的代码
| |- util // 用到的工具类
|- test // 单元测试相关
| |- unit
| | |- karma.xxx.config.js
| |- xxx.spec.js
| |- ...
代码架构
项目代中将核心代码和API实现代码分开,核心代码相当于一个处理引擎,而各个环境下的不同API实现可以单独挂载(这里是为了方便其它地方组合不同环境下的API所以才分开的,实际上可以将native和核心代码打包到一起)
quick.js
quick.h5.js
quick.native.js
这里需要注意,quick.xx环境.js
中的代码是基于quick.js
核心代码的(譬如里面需要用到一些特点的快速调用底层的方法)
而其中最核心的quick.js
代码架构如下
index
|- os // 系统判断相关
|- promise // promise支持,这里并没有重新定义,而是判断环境中是否已经支持来决定是否支持
|- error // 统一错误处理
|- proxy // API的代理对象,内部对进行统一预处理,如默认参数,promise支持等
|- jsbridge // 与native环境下原生交互的桥梁
|- callinner // API的默认实现,如果是标准的API,可以不传入runcode,内部默认采用这个实现
|- defineapi // API的定义,API多平台支撑的关键,也约定着该如何拓展
|- callnative // 定义一个调用通用native环境API的方法,拓展组件API(自定义)时需要这个方法调用
|- init // 里面定义config,ready,error的使用
|- innerUtil // 给核心文件绑定一些内部工具类,供不同API实现中使用
可以看到,核心代码已经被切割成很小的单元了,虽然说最终打包起来总共代码也没有多少,但是为了维护性,简洁性,这种拆分还是很有必要的
统一的预处理
在上一篇API多平台的支撑
中有提到如何基于Object.defineProperty
实现一个支持多平台调用的API,实现起来的API大致是这样子的
Object.defineProperty(apiParent, apiName, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
// 确保get得到的函数一定是能执行的
const nameSpaceApi = proxysApis[finalNameSpace];
// 得到当前是哪一个环境,获得对应环境下的代理对象
return nameSpaceApi[getCurrProxyApiOs(quick.os)] || nameSpaceApi.h5;
},
set: function proxySetter() {
alert('不允许修改quick API');
},
});
...
quick.extendModule('ui', [{
namespace: 'alert',
os: ['h5'],
defaultParams: {
message: '',
},
runCode(message) {
alert('h5-' + message);
},
}]);
其中nameSpaceApi.h5
的值是api.runCode
,也就是说直接执行runCode(...)
中的代码
仅仅这样是不够的,我们需要对调用方法的输入等做统一预处理,因此在这里,我们基于实际的情况,在此基础上进一步完善,加上统一预处理
机制,也就是
const newProxy = new Proxy(api, apiRuncode);
Object.defineProperty(apiParent, apiName, {
...
get: function proxyGetter() {
...
return newProxy.walk();
}
});
我们将新的运行代码变为一个代理对象Proxy
,代理api.runCode,然后在get时返回代理过后的实际方法(.walk()
方法代表代理对象内部会进行一次统一的预处理)
代理对象的代码如下
function Proxy(api, callback) {
this.api = api;
this.callback = callback;
}
Proxy.prototype.walk = function walk() {
// 实时获取promise
const Promise = hybridJs.getPromise();
// 返回一个闭包函数
return (...rest) = >{
let args = rest;
args[0] = args[0] || {};
// 默认参数的处理
if (this.api.defaultParams && (args[0] instanceof Object)) {
Object.keys(this.api.defaultParams).forEach((item) = >{
if (args[0][item] === undefined) {
args[0][item] = this.api.defaultParams[item];
}
});
}
// 决定是否使用Promise
let finallyCallback;
if (this.callback) {
// 将this指针修正为proxy内部,方便直接使用一些api关键参数
finallyCallback = this.callback;
}
if (Promise) {
return finallyCallback && new Promise((resolve, reject) = >{
// 拓展 args
args = args.concat([resolve, reject]);
finallyCallback.apply(this, args);
});
}
return finallyCallback && finallyCallback.apply(this, args);
};
};
从源码中可以看到,这个代理对象统一预处理了两件事情:
1.对于合法的输入参数,进行默认参数的匹配
2.如果环境中支持Promise,那么返回Promise对象并且参数的最后加上
resolve
,reject
而且,后续如果有新的统一预处理(调用API前的预处理),只需在这个代理对象的这个方法中增加即可
JSBridge解析规则
前面的文章中有提到JSBridge的实现,但那时其实更多的是关注原理层面,那么实际上,定义的交互解析规则是什么样的呢?如下
// 以ui.toast实际调用的示例
// `${CUSTOM_PROTOCOL_SCHEME}://${module}:${callbackId}/${method}?${params}`
const uri = 'QuickHybridJSBridge://ui:9527/toast?{"message":"hello"}';
if (os.quick) {
// 依赖于os判断
if (os.ios) {
// ios采用
window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(uri);
} else {
window.top.prompt(uri, '');
}
} else {
// 浏览器
warn(`浏览器中jsbridge无效, 对应scheme: ${uri}`);
}
原生容器中接收到对于的uri后反解析即可知道调用了些什么,上述中:
QuickHybridJSBridge
是本框架交互的scheme标识module
和method
分别代表API的模块名和方法名params
是对于方法传递的额外参数,原生容器会解析成JSONObjectcallbackId
是本次API调用在H5端的回调id,原生容器执行完后,通知H5时会传递回调id,然后H5端找到对应的回调函数并执行
为什么要用uri的方式,因为这种方式可以兼容以前的scheme方式,如果方案切换,变动代价下(本身就是这样升级上来的,所以没有替换的必要)
UA约定
混合开发容器中,需要有一个UA标识位来判断当前系统。
这里Android和iOS原生容器统一在webview中加上如下UA标识(也就是说,如果容器UA中有这个标识位,就代表是quick环境-这也是os判断的实现原理)
String ua = webview.getSettings().getUserAgentString();
ua += " QuickHybridJs/" + getVersion();
// 设置浏览器UA,JS端通过UA判断是否属于quick环境
webview.getSettings().setUserAgentString(ua);
// 获取默认UA
NSString *defaultUA = [[UIWebView new] stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSString *version = [[NSBundle mainBundle].infoDictionary objectForKey:@"CFBundleShortVersionString"];
NSString *customerUA = [defaultUA stringByAppendingString:[NSString stringWithFormat:@" QuickHybridJs/%@", version]];
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":customerUA}];
如上述代码中分别在Android和iOS容器的UA中添加关键性的标识位。
API内部做了些什么
API内部只做与本身功能逻辑相关的操作,这里有几个示例
quick.extendModule('ui', [{
namespace: 'toast',
os: ['h5'],
defaultParams: {
message: '',
},
runCode(...rest) {
// 兼容字符串形式
const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message', );
const options = args[0];
const resolve = args[1];
// 实际的toast实现
toast(options);
options.success && options.success();
resolve && resolve();
},
}, ...]);
quick.extendModule('ui', [{
namespace: 'toast',
os: ['quick'],
defaultParams: {
message: '',
},
runCode(...rest) {
// 兼容字符串形式
const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message');
quick.callInner.apply(this, args);
},
}, ...]);
以上是toast功能在h5和quick环境下的实现,其中,在quick环境下唯一做的就是兼容了一个字符串形式的调用,在h5环境下则是完全的实现了h5下对应的功能(promise也需自行兼容)
为什么h5中更复杂?因为quick环境中,只需要拼凑成一个JSBridge命令发送给原生即可,具体功能由原生实现,而h5的实现是需要自己完全实现的。
另外,其实在quick环境中,上述还不是最少的代码(上述加了一个兼容调用功能,所以多了几行),最少代码如下
quick.extendModule('ui', [{
namespace: 'confirm',
os: ['quick'],
defaultParams: {
title: '',
message: '',
buttonLabels: ['取消', '确定'],
},
}, ...]);
可以看到,只要是符合标准的API定义,在quick环境下的实现只需要定义些默认参数就可以了,其它的框架自动帮助实现了(同样promise的实现也在内部默认处理掉了)
这样以来,就算是标准quick环境下的API数量多,实际上增加的代码也并不多。
关于代码规范与单元测试
项目中采用的Airbnb代码规范
并不是100%
契合原版,而是基于项目的情况定制了下,但是总体上95%
以上是符合的
还有一块就是单元测试,这是很容易忽视的一块,但是也挺难做好的。这个项目中,基于Karma + Mocha
进行单元测试,而且并不是测试驱动,而是在确定好内容后,对核心部分的代码都进行单测。
内部对于API的调用基本都是靠JS来模拟,对于一些特殊的方法,还需Object.defineProperty(window.navigator, name, prop)
来改变window本身的属性来模拟。
本项目中的核心代码已经达到了100%
的代码覆盖率。
具体的代码这里不赘述,可以参考源码
返回根目录
源码
github
上这个框架的实现
【quickhybrid】JS端的项目实现的更多相关文章
- 【quickhybrid】Android端的项目实现
前言 前文中就有提到,Hybrid模式的核心就是在原生,而本文就以此项目的Android部分为例介绍Android部分的实现. 提示,由于各种各样的原因,本项目中的Android容器确保核心交互以及部 ...
- PC端Web项目开发流程
从前一直再做前端,突然想到如果有一天领导让自己独立承担一个web 项目的话是否有足够的能力去接这个任务,要学会自己去搭建一些基础的工具信息.所有的这一切在心里都要有个大致的流程,不然真正做的时候难免会 ...
- 【前端】Vue.js经典开源项目汇总
Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...
- Vue.js经典开源项目汇总
Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...
- nuxt.js express模板项目IIS部署
继续上一篇的nuxt/express项目部署,还是windows上要把nuxt的服务端渲染项目跑起来,这次的目的是用已经有的域名windows服务器上一个虚拟目录反向代理部署在其他端口nuxt项目. ...
- Vue.js经典开源项目汇总-前端参考资源
Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...
- cometd的js端代码
一:js端使用方式 CometD JavaScript的配置.整个API可以通过一个单一的原型名为org.cometd.Cometd的对象来调用.Dojo工具包中有一个名称为dojox.cometd的 ...
- 关于Web端即JS端编程
主要的技术是 HTML/JS/CSS/XML Web就是JS/DOM编程. 页面的数据来源: XML, JSON, HTML, Text, 第三方页面或者数据. 不一定都要跟服务器进行交互. JS端 ...
- 基于开源SuperSocket实现客户端和服务端通信项目实战
一.课程介绍 本期带给大家分享的是基于SuperSocket的项目实战,阿笨在实际工作中遇到的真实业务场景,请跟随阿笨的视角去如何实现打通B/S与C/S网络通讯,如果您对本期的<基于开源Supe ...
随机推荐
- c++——默认参数、函数占位参数
2 默认参数 /*1 C++中可以在函数声明时为参数提供一个默认值, 当函数调用时没有指定这个参数的值,编译器会自动用默认值代替 */ void myPrint(int x = 3) { printf ...
- 常用命令 tcl & shell
TCL 常用命令: 1. 当前时间 [exec date +%m%d_%H%M] (实际是调用shell命令 date),比如在 icc 中保存cell 时可以用:save_mw_cel ...
- 删除iptables nat 规则
删除FORWARD 规则: iptables -nL FORWARD --line-numberiptables -D FORWARD 1 删除一条nat 规则 删除SNAT规则 iptables ...
- 内部元素一一相应的集合的算法优化,从list到hashmap
说是算法优化,基本上是在吹牛,仅仅只是算是记录下,我写代码时候的思路.毕竟还是小菜鸟. 我要开一个party,与会者都是情侣,可是情侣并非一起过来的,而是有先有后,可是每位与会者来的时候都拿着一束鲜花 ...
- 一、Delphi中Cxgrid表格滚动条粗细设置
1.Delphi VCL新版本的Cxgrid滚动条默认是触屏模式(如下图),很细的滚动条,在电脑版显示非常不方便. 2.如果需要改成传统的滚动条模式,需要设置一下LookAndFeel里面的Scrol ...
- Hadoop(18)-MapReduce框架原理-WritableComparable排序和GroupingComparator分组
1.排序概述 2.排序分类 3.WritableComparable案例 这个文件,是大数据-Hadoop生态(12)-Hadoop序列化和源码追踪的输出文件,可以看到,文件根据key,也就是手机号进 ...
- uip.h 笔记
想了解uip,可以从uip.h开始,他对主体函数有详细的说明,和案例 初始化 1 设定IP网络设定 2 初始化uip 3 处理接收包 4 ARP包处理 5 周期处理,tcp协议处理 uip_proce ...
- 1.Variables-变量(Dart中文文档)
初次翻译,部分内容并非按字面翻译,是按本人理解进行了内容重组.如有错误望指正. 如下是变量定义和赋值的示例 var name = 'Bob'; 变量存储的是一个引用地址.如上的变量name指向了一个值 ...
- 2017-2018-1 20155220 《信息安全系统设计基础》课下实践——实现mypwd
学习pwd命令 输入pwd命令 于是man 1 pwd查看pwd详细 然后查看pwd实现需要的系统调用man -k; grep 在这发现了一个功能相同的内核函数getcwd 到这步就很简单了,先查看这 ...
- 20155231 邵煜楠《网络对抗技术》实验一 PC平台逆向破解
20155231 邵煜楠<网络对抗技术>实验一 PC平台逆向破解 实验内容 直接修改程序机器指令,改变程序执行流程: 通过构造输入参数,造成BOF攻击,改变程序执行流: 注入Shellco ...