前言

API实现阶段之JS端的实现,重点描述这个项目的JS端都有些什么内容,是如何实现的。

不同于一般混合框架的只包含JSBridge部分的前端实现,本框架的前端实现包括JSBridge部分、多平台支持,统一预处理等等。

项目的结构

在最初的版本中,其实整个前端库就只有一个文件,里面只规定着如何实现JSBridge和原生交互部分。但是到最新的版本中,由于功能逐步增加,单一文件难以满足要求和维护,因此重构成了一整个项目。

整个项目基于ES6Airbnb代码规范,使用gulp + rollup构建,部分重要代码进行了Karma + Mocha单元测试

整体目录结构如下:

  1. quickhybrid
  2. |- dist // 发布目录
  3. | |- quick.js
  4. | |- quick.h5.js
  5. |- build // 构建项目的相关代码
  6. | |- gulpfile.js
  7. | |- rollupbuild.js
  8. |- src // 核心源码
  9. | |- api // 各个环境下的api实现
  10. | | |- h5 // h5下的api
  11. | | |- native // quick下的api
  12. | |- core // 核心控制
  13. | | |- ... // 将核心代码切割为多个文件
  14. | |- inner // 内部用到的代码
  15. | |- util // 用到的工具类
  16. |- test // 单元测试相关
  17. | |- unit
  18. | | |- karma.xxx.config.js
  19. | |- xxx.spec.js
  20. | |- ...

代码架构

项目代中将核心代码和API实现代码分开,核心代码相当于一个处理引擎,而各个环境下的不同API实现可以单独挂载(这里是为了方便其它地方组合不同环境下的API所以才分开的,实际上可以将native和核心代码打包到一起)

  1. quick.js
  2. quick.h5.js
  3. quick.native.js

这里需要注意,quick.xx环境.js中的代码是基于quick.js核心代码的(譬如里面需要用到一些特点的快速调用底层的方法)

而其中最核心的quick.js代码架构如下

  1. index
  2. |- os // 系统判断相关
  3. |- promise // promise支持,这里并没有重新定义,而是判断环境中是否已经支持来决定是否支持
  4. |- error // 统一错误处理
  5. |- proxy // API的代理对象,内部对进行统一预处理,如默认参数,promise支持等
  6. |- jsbridge // 与native环境下原生交互的桥梁
  7. |- callinner // API的默认实现,如果是标准的API,可以不传入runcode,内部默认采用这个实现
  8. |- defineapi // API的定义,API多平台支撑的关键,也约定着该如何拓展
  9. |- callnative // 定义一个调用通用native环境API的方法,拓展组件API(自定义)时需要这个方法调用
  10. |- init // 里面定义config,ready,error的使用
  11. |- innerUtil // 给核心文件绑定一些内部工具类,供不同API实现中使用

可以看到,核心代码已经被切割成很小的单元了,虽然说最终打包起来总共代码也没有多少,但是为了维护性,简洁性,这种拆分还是很有必要的

统一的预处理

在上一篇API多平台的支撑中有提到如何基于Object.defineProperty实现一个支持多平台调用的API,实现起来的API大致是这样子的

  1. Object.defineProperty(apiParent, apiName, {
  2. configurable: true,
  3. enumerable: true,
  4. get: function proxyGetter() {
  5. // 确保get得到的函数一定是能执行的
  6. const nameSpaceApi = proxysApis[finalNameSpace];
  7. // 得到当前是哪一个环境,获得对应环境下的代理对象
  8. return nameSpaceApi[getCurrProxyApiOs(quick.os)] || nameSpaceApi.h5;
  9. },
  10. set: function proxySetter() {
  11. alert('不允许修改quick API');
  12. },
  13. });
  14. ...
  15. quick.extendModule('ui', [{
  16. namespace: 'alert',
  17. os: ['h5'],
  18. defaultParams: {
  19. message: '',
  20. },
  21. runCode(message) {
  22. alert('h5-' + message);
  23. },
  24. }]);

其中nameSpaceApi.h5的值是api.runCode,也就是说直接执行runCode(...)中的代码

仅仅这样是不够的,我们需要对调用方法的输入等做统一预处理,因此在这里,我们基于实际的情况,在此基础上进一步完善,加上统一预处理机制,也就是

  1. const newProxy = new Proxy(api, apiRuncode);
  2. Object.defineProperty(apiParent, apiName, {
  3. ...
  4. get: function proxyGetter() {
  5. ...
  6. return newProxy.walk();
  7. }
  8. });

我们将新的运行代码变为一个代理对象Proxy,代理api.runCode,然后在get时返回代理过后的实际方法(.walk()方法代表代理对象内部会进行一次统一的预处理)

代理对象的代码如下

  1. function Proxy(api, callback) {
  2. this.api = api;
  3. this.callback = callback;
  4. }
  5. Proxy.prototype.walk = function walk() {
  6. // 实时获取promise
  7. const Promise = hybridJs.getPromise();
  8. // 返回一个闭包函数
  9. return (...rest) = >{
  10. let args = rest;
  11. args[0] = args[0] || {};
  12. // 默认参数的处理
  13. if (this.api.defaultParams && (args[0] instanceof Object)) {
  14. Object.keys(this.api.defaultParams).forEach((item) = >{
  15. if (args[0][item] === undefined) {
  16. args[0][item] = this.api.defaultParams[item];
  17. }
  18. });
  19. }
  20. // 决定是否使用Promise
  21. let finallyCallback;
  22. if (this.callback) {
  23. // 将this指针修正为proxy内部,方便直接使用一些api关键参数
  24. finallyCallback = this.callback;
  25. }
  26. if (Promise) {
  27. return finallyCallback && new Promise((resolve, reject) = >{
  28. // 拓展 args
  29. args = args.concat([resolve, reject]);
  30. finallyCallback.apply(this, args);
  31. });
  32. }
  33. return finallyCallback && finallyCallback.apply(this, args);
  34. };
  35. };

从源码中可以看到,这个代理对象统一预处理了两件事情:

  • 1.对于合法的输入参数,进行默认参数的匹配

  • 2.如果环境中支持Promise,那么返回Promise对象并且参数的最后加上resolvereject

而且,后续如果有新的统一预处理(调用API前的预处理),只需在这个代理对象的这个方法中增加即可

JSBridge解析规则

前面的文章中有提到JSBridge的实现,但那时其实更多的是关注原理层面,那么实际上,定义的交互解析规则是什么样的呢?如下

  1. // 以ui.toast实际调用的示例
  2. // `${CUSTOM_PROTOCOL_SCHEME}://${module}:${callbackId}/${method}?${params}`
  3. const uri = 'QuickHybridJSBridge://ui:9527/toast?{"message":"hello"}';
  4. if (os.quick) {
  5. // 依赖于os判断
  6. if (os.ios) {
  7. // ios采用
  8. window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(uri);
  9. } else {
  10. window.top.prompt(uri, '');
  11. }
  12. } else {
  13. // 浏览器
  14. warn(`浏览器中jsbridge无效, 对应scheme: ${uri}`);
  15. }

原生容器中接收到对于的uri后反解析即可知道调用了些什么,上述中:

  • QuickHybridJSBridge是本框架交互的scheme标识

  • modulemethod分别代表API的模块名和方法名

  • params是对于方法传递的额外参数,原生容器会解析成JSONObject

  • callbackId是本次API调用在H5端的回调id,原生容器执行完后,通知H5时会传递回调id,然后H5端找到对应的回调函数并执行

为什么要用uri的方式,因为这种方式可以兼容以前的scheme方式,如果方案切换,变动代价下(本身就是这样升级上来的,所以没有替换的必要)

UA约定

混合开发容器中,需要有一个UA标识位来判断当前系统。

这里Android和iOS原生容器统一在webview中加上如下UA标识(也就是说,如果容器UA中有这个标识位,就代表是quick环境-这也是os判断的实现原理)

  1. String ua = webview.getSettings().getUserAgentString();
  2. ua += " QuickHybridJs/" + getVersion();
  3. // 设置浏览器UA,JS端通过UA判断是否属于quick环境
  4. webview.getSettings().setUserAgentString(ua);
  1. // 获取默认UA
  2. NSString *defaultUA = [[UIWebView new] stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
  3. NSString *version = [[NSBundle mainBundle].infoDictionary objectForKey:@"CFBundleShortVersionString"];
  4. NSString *customerUA = [defaultUA stringByAppendingString:[NSString stringWithFormat:@" QuickHybridJs/%@", version]];
  5. [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":customerUA}];

如上述代码中分别在Android和iOS容器的UA中添加关键性的标识位。

API内部做了些什么

API内部只做与本身功能逻辑相关的操作,这里有几个示例

  1. quick.extendModule('ui', [{
  2. namespace: 'toast',
  3. os: ['h5'],
  4. defaultParams: {
  5. message: '',
  6. },
  7. runCode(...rest) {
  8. // 兼容字符串形式
  9. const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message', );
  10. const options = args[0];
  11. const resolve = args[1];
  12. // 实际的toast实现
  13. toast(options);
  14. options.success && options.success();
  15. resolve && resolve();
  16. },
  17. }, ...]);
  1. quick.extendModule('ui', [{
  2. namespace: 'toast',
  3. os: ['quick'],
  4. defaultParams: {
  5. message: '',
  6. },
  7. runCode(...rest) {
  8. // 兼容字符串形式
  9. const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message');
  10. quick.callInner.apply(this, args);
  11. },
  12. }, ...]);

以上是toast功能在h5和quick环境下的实现,其中,在quick环境下唯一做的就是兼容了一个字符串形式的调用,在h5环境下则是完全的实现了h5下对应的功能(promise也需自行兼容)

为什么h5中更复杂?因为quick环境中,只需要拼凑成一个JSBridge命令发送给原生即可,具体功能由原生实现,而h5的实现是需要自己完全实现的。

另外,其实在quick环境中,上述还不是最少的代码(上述加了一个兼容调用功能,所以多了几行),最少代码如下

  1. quick.extendModule('ui', [{
  2. namespace: 'confirm',
  3. os: ['quick'],
  4. defaultParams: {
  5. title: '',
  6. message: '',
  7. buttonLabels: ['取消', '确定'],
  8. },
  9. }, ...]);

可以看到,只要是符合标准的API定义,在quick环境下的实现只需要定义些默认参数就可以了,其它的框架自动帮助实现了(同样promise的实现也在内部默认处理掉了)

这样以来,就算是标准quick环境下的API数量多,实际上增加的代码也并不多。

关于代码规范与单元测试

项目中采用的Airbnb代码规范并不是100%契合原版,而是基于项目的情况定制了下,但是总体上95%以上是符合的

还有一块就是单元测试,这是很容易忽视的一块,但是也挺难做好的。这个项目中,基于Karma + Mocha进行单元测试,而且并不是测试驱动,而是在确定好内容后,对核心部分的代码都进行单测。

内部对于API的调用基本都是靠JS来模拟,对于一些特殊的方法,还需Object.defineProperty(window.navigator, name, prop)来改变window本身的属性来模拟。

本项目中的核心代码已经达到了100%的代码覆盖率。

具体的代码这里不赘述,可以参考源码

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】JS端的项目实现的更多相关文章

  1. 【quickhybrid】Android端的项目实现

    前言 前文中就有提到,Hybrid模式的核心就是在原生,而本文就以此项目的Android部分为例介绍Android部分的实现. 提示,由于各种各样的原因,本项目中的Android容器确保核心交互以及部 ...

  2. PC端Web项目开发流程

    从前一直再做前端,突然想到如果有一天领导让自己独立承担一个web 项目的话是否有足够的能力去接这个任务,要学会自己去搭建一些基础的工具信息.所有的这一切在心里都要有个大致的流程,不然真正做的时候难免会 ...

  3. 【前端】Vue.js经典开源项目汇总

    Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...

  4. Vue.js经典开源项目汇总

    Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...

  5. nuxt.js express模板项目IIS部署

    继续上一篇的nuxt/express项目部署,还是windows上要把nuxt的服务端渲染项目跑起来,这次的目的是用已经有的域名windows服务器上一个虚拟目录反向代理部署在其他端口nuxt项目. ...

  6. Vue.js经典开源项目汇总-前端参考资源

    Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...

  7. cometd的js端代码

    一:js端使用方式 CometD JavaScript的配置.整个API可以通过一个单一的原型名为org.cometd.Cometd的对象来调用.Dojo工具包中有一个名称为dojox.cometd的 ...

  8. 关于Web端即JS端编程

    主要的技术是 HTML/JS/CSS/XML Web就是JS/DOM编程. 页面的数据来源: XML, JSON, HTML, Text, 第三方页面或者数据. 不一定都要跟服务器进行交互. JS端 ...

  9. 基于开源SuperSocket实现客户端和服务端通信项目实战

    一.课程介绍 本期带给大家分享的是基于SuperSocket的项目实战,阿笨在实际工作中遇到的真实业务场景,请跟随阿笨的视角去如何实现打通B/S与C/S网络通讯,如果您对本期的<基于开源Supe ...

随机推荐

  1. 在不升级 mysql 的情况下直接使用 mysql utf8 存储 超过三个字节的 emoji 表情

    由于现在数据库的版本是5.5.2,但是看网上说要直接存储emoji表情,需要升级到5.5.3然后把字符集设置为utf8mb4,但是升级数据库感觉属于敏感操作. 考虑了多久之后直接考虑使用正则来替换,但 ...

  2. JDK(十)JDK1.7&1.8源码对比分析【集合】ConcurrentHashMap

    前言 在JDK1.7&1.8源码对比分析[集合]HashMap中我们对比分析了JDK1.7和1.8版本的HashMap源码,趁热打铁,这篇文章就来看看JDK1.7和1.8版本的Concurre ...

  3. BI之报表测试总结

    报表测试总结: 1.测试准备工作: 数据准备 保证足够多的有效数据 清楚报表中涉及到的算法.公式 清楚业务功能接口 2.报表测试点 基本测试点:界面.控件.格式.布局.明显的数据错误.js报错.报表标 ...

  4. iredmail 设置

    一些问题和修改 1.收邮件很慢安装完毕后,测试会发现 发送邮件都是秒到,但收邮件特别慢 长达十几分钟,这是因为iredmail的灰名单规则导致的(需要外部邮箱进行3次投递才接收,防止垃圾邮件),禁用灰 ...

  5. 三层架构搭建(asp.net mvc + ef)

    第一次写博客,想了半天先从简单的三层架构开始吧,希望能帮助到你! 简单介绍一下三层架构, 三层架构从上到下分:表现层(UI),业务逻辑层(BLL),数据访问层(DAL)再加上数据模型(Model),用 ...

  6. vue请求本地自己编写的json文件。

    1.第一步,这是目录结构 2.接下来是build/webpack.dev.conf.js文件需要配置的内容 代码: //vue配置请求本地json数据const express = require(' ...

  7. L2-025 分而治之(图)

    (这不会是我最后一天写算法题的博客吧...有点感伤...) 题目: 分而治之,各个击破是兵家常用的策略之一.在战争中,我们希望首先攻下敌方的部分城市,使其剩余的城市变成孤立无援,然后再分头各个击破.为 ...

  8. jquery 中的dom操作

    jquery DOM 分为元素操作.属性操作.样式操作. 一.元素操作 1.查找 ①工具:jQuery选择器 2.创建和添加 ①代码格式:变量 = $('要创建的元素'): 注意点: 1 要使用标准的 ...

  9. Spring第二天——IOC注解操作与AOP概念

    大致内容 spring的bean管理(注解实现) AOP原理 log4j介绍 spring整合web项目的演示 一.spring注解实现bean管理 注解: 代码中一些特殊的标记,使用注解也可以完成一 ...

  10. Centos 6.4 安装mysql-5.6.14-linux-glibc2.5-i686.tar.gz

    创建用户和组 创建链接 授权own和grp给mysql-5.5.8-linux2.6-i686文件夹,就是下面的BASE_DIR 执行的mysql_install_db的时候后面带参数 ./scrip ...