Giter Site home page Giter Site logo

quickhybrid / quickhybrid Goto Github PK

View Code? Open in Web Editor NEW
441.0 441.0 90.0 446 KB

快速混合开发框架,JSBridge提供核心Android、iOS功能。多平台API支撑,部分兼容浏览器

License: BSD 3-Clause "New" or "Revised" License

JavaScript 100.00%

quickhybrid's Introduction

quickhybrid

快速混合开发框架,JSBridge提供核心Android、iOS功能。多平台API支撑,部分兼容浏览器

包括三大部分:

  • JS(前端)

  • Android原生

  • iOS原生

JS(前端)

大致实现内容

ES6 + Rollup + Eslint + Karma + Mocha

此入口项目即是前端项目

Android原生

地址:

quickhybrid-android

iOS原生

地址:

quickhybrid-ios

系列文章

segmentfault地址:

https://segmentfault.com/blog/dailc

官方网站地址:

https://quickhybrid.github.io/

也可直接在github issue中看到

https://github.com/quickhybrid/quickhybrid/issues

包括了api使用文档,原理讲解系列文档等等

讨论

  • gitter

  • QQ群(674789958

  • 也可以直接在github issue中讨论

注意,申请加入群时请添加验证信息,例如:quickhybrid运行遇到问题等等

quickhybrid's People

Contributors

dailc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

quickhybrid's Issues

【quickhybrid】iOS端的项目实现

【quickhybrid】iOS端的项目实现

前言

18年元旦三天内和朋友突击了下,勉强是将雏形做出来了,后续的API慢慢完善。(当然了,主力还是那个朋友,本人只是初涉iOS,勉强能看懂,修修改改而已)

大致内容如下:

  • JSBridge核心交互部分

  • uipage等部分常用API的实现(其它慢慢完善)

  • 组件(自定义)API拓展的实现

  • API的权限校验仅预留了一个入口,模拟最简单的实现

  • 其它如离线资源加载更新,底层优化等机制暂时不提供

项目的结构

这个项目中,为了方便,就没有分成多个静态库了(事实上是可以这样做的),而是全部都放在一个项目中

整体目录结构如下:

quickhybrid-ios
    |- AppDelegate          // 应用入口,分发进入对应的viewcontroller
    |- core                 // 核心工具类,放一些通用工具类
    |   |- ui
    |   |- util
    |   |- ...
    |- quickhybrid          // JSBridge实现的核心代码,定制viewcontroller,实现API等
    |   |- WebViewJavascriptBridge
    |   |- basecore
    |   |- quickcore
    |   |- api

代码架构

和Android一样,仍然是简单的三次架构:底层核心工具类->JSBridge桥接实现->app应用实现

其中,core和jsbridge有必要的话可以打包成静态库

core
    |- ui                           // 一些UI效果的定义与实现
    |- util                         // 通用工具类
    
quickhybird
    |- WebViewJavascriptBridge      // 第三方开源的jsbridge实现,里面进行了修改
    |- basecore                     // 定义基类viewcontroller
    |- quickcore                    // 定义quickhybrid中的viewcontroller实现
    |- api                          // 定义API,开放原生功能给H5
    
应用内
    |- AppDelegate                  // 应用入口,分发进入对应的viewcontroller
    |- MainViewController           // 入口界面
    |- TestPayApi                   // 定义的一个测试支付组件(自定义)API
    |- qhjsmodules.plist            // 内部定义模块的别名于路径关系的配置文件

权限配置

iOS可以直接在info.plist中配置权限,譬如

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>
    <key>NSCameraUsageDescription</key>
    <string>是否允许应用使用摄像头?</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>是否允许应用使用定位功能</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>是否允许应用使用麦克风?</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>是否允许访问相册</string>
    <key>UIFileSharingEnabled</key>
    ...

应用配置

Bundle Identifier: com.quickhybrid.quickhybriddemo
Version: 1.0.0

Deployment Target: 11.2(默认最新调试)
Devices: Universal

Signing: none

相比Android中一堆复杂的配置,iOS中无疑简单很多,直接用最新系统调试即可。。。

这里,到目前位置,这个项目还有很多API没有实现,后续预计是会引入部分静态库的。

当然,如果想要引入静态库,也很简单,直接如下:

项目配置->Build Phases->Link Binary With Libraries->+(添加)->然后需要用到的地方import即可

整个过程非常的轻松愉快。

一些关键代码

代码方面,也无法一一全部说明,这里仅列举一些比较重要的步骤实现,其余可参考源码

UA约定

前面的JS项目中就已经有提到UA约定,就是在加载对于webview时,统一在webview中加上如下UA标识

// 获取默认UA
NSString *defaultUA = [[UIWebView new] stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
        
NSString *version = @"1.0.0";
        
NSString *customerUA = [defaultUA stringByAppendingString:[NSString stringWithFormat:@" QuickHybridJs/%@", version]];
        
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":customerUA}];

监听JSBridge的触发

在创建webview时,QHBaseWebLoader里创建代理监听

    // 创建webView容器
    WKWebViewConfiguration *webConfig = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *userContentVC = [[WKUserContentController alloc] init];
    webConfig.userContentController = userContentVC;
    WKWebView *wk = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webConfig];
    
    [self.view addSubview:wk];
    self.wv = wk;
    self.wv.navigationDelegate = self;
    self.wv.UIDelegate = self;
    self.wv.translatesAutoresizingMaskIntoConstraints = NO;
    
    ...  
    
    self.bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.wv];
    [self.bridge setWebViewDelegate:self];
    
    [self.wv.configuration.userContentController addScriptMessageHandler:self.bridge name:@"WKWebViewJavascriptBridge"];

然后h5中通过以下调用:

window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);

然后WKWebViewJavascriptBridge内部,接受传递的信息,并自行解析

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"WKWebViewJavascriptBridge"]) {
        [self excuteMessage:message.body];
    }
}

其它

iOS中还有一点和Android不同就是,很多标准的HTML5内容无需额外兼容(譬如fileinput文件选择等)

其它内容,和Android实现中提到的一样,这里就不再赘述了,可以直接参考源码

另外,后续如果继续有容器优化等操作,也会单独整理,加入本系列。

前端页面示例

为了方便,直接集成到了res/中,入口页面默认会加载它,也可以直接看源码

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】H5和原生的职责划分

前言

在JSBridge实现后,前端网页与原生的交互已经通了,接下来就要开始规划API,明确需要提供哪一些功能来供前端调用。

但是在这之前,还有一点重要工作需要做:

明确H5与Native的职责划分,确定哪一些功能可以由H5实现,哪一些功能只能由原生实现

Native与H5职责划分

使用Hybrid模式,用H5开发页面的本质是:

减少工作量(一套代码,多个平台),以及快速的更新迭代(譬如线上更新),而且还需要考虑Native端的高性能以及系统API调用能力(否则直接用纯H5就可以了)

因此在进行职责划分时,就得充分的考虑前端渲染,JS语言以及原生渲染,Java/OC等语言的特性,基本总结如下:

  • 混合页面导航栏组件由原生实现

  • 一些重要的业务页面、带有复杂动画或交互的页面以及一些固定页面由原生实现

  • 系统级UI由原生统一实现

  • 页面切换的转场由原生实现

  • CPU密集型任务、底层的优化要由原生完成

  • 其它功能能用H5实现(并且效果不错)的就尽量不要用原生

导航栏组件由Native实现

尝试过,也对比过很多的混合开发框架,譬如Dcloud的HTML5+,钉钉里的DD API,自己也尝试过不同的方式,
最终发现导航栏的最好做法还是由原生提供,核心原因如下:

  • H5页面加载过程会有白屏问题(也别是弱网络情况),如果整个页面都是H5实现,那么白屏了就体验非常差,而且连基本的交互与操作都没了

仅基于这一点,就已经拍板了由Native导航栏组件+webview(加载H5)来组成页面,而原生提供一些API来供网页操控导航栏(譬如标题,按钮等)

整体页面布局如下:

而H5端可以通过原生提供的API来操控导航栏,以下举例为quick中规划的API:

// 仅提供一部分示例
quick.navigator.setTitle({
    title: '标题',
    subTitle: '子标题',
    success: function(result) {},
    error: function(error) {}
});

quick.navigator.setRightBtn({
    isShow: 1,
    text: '按钮右1',
    // 设置图片的优先级会较高
    //imageUrl: 'http://xxx/test.png',
    // 从右数起第几个
    which: 0,
    success: function(result) {
        /**
         * 按钮点击后回调
         */
    },
    error: function(error) {}
});

多tab页面也由原生提供

实际开发中Native导航栏组件+webview也就满足绝大部分的页面需求了,但是还有一些特殊页面是这种实现达不到的,譬如多Tab页面

上述这种内含多tab的页面,每一个tab里都是单独的页面,而且可以通过滑动等手势来切换,甚至tab还会有一些渐变动画,导航栏也配合改变等(常见于APP首页)

为了统一实现,这类页面的导航栏与底部tab均是由原生实现,由H5通过API打开这类原生页面,并将需要加载的网页地址传入,如下

quick.page.openLocal({
    className: '那种原生页面的标识,可以唯一查询到相应的界面',
    data: {
        // 需要加载的n个url
        url1: 'http://...',
        urln: 'http://...',
    },
    success: function(result) {},
    error: function(error) {}
});

然后,在每一个前端页面(webview里加载的内容),可以分别在对于页面的脚本里进行自己的交互控制

重要的业务页面由原生实现

对于一些重要的业务页面,如登陆,注册,支付等,处于安全性以及交互性的考虑(就是一个APP的门面),会采用完全由Native实现
(当然了,一般这些页面的变动频率也不大)

一些默认提示页面采用原生实现

webview加载网页时,一般情况原生都是会对加载情况进行监听的,比如是否网络异常。服务器响应异常,页面加载崩溃等,
为了防止APP假死,原生会提高一些默认提示页面

上述只是一个原型示例,实际上,很多情况都可以由原生提供统一提示页面,
如404,页面崩溃,网络错误等

交互性强、动画复杂的页面采用原生实现

除了关键性页面,还有一类,就是H5不好实现的(或者说达不到要求的、实现代价过大的),也应该由原生实现

譬如以某图像处理软件某个界面截图为例

这种页面涉及到了明显不太适合H5实现的图像处理,因此原生才是更佳的选择(当然了,实际上H5的canvas是由图像处理能力的)

系统级UI由原生统一实现

前面提到了页面的选择,但页面内的内容也是需要抉择的,比如一些UI显示控件(alert,toast等)

虽然H5完成可以实现这些UI控件,并且可以和原生模拟的一样,但是基于以下考虑,所有系统级的UI全部由原生实现并提供API:(原生和H5需统一风格)

  • 每一个合格的原生应用本身就会有一套自己风格的UI,因此不存在重复开发问题

  • H5本身可以实现这些组件,但是如果要模拟的和原生一摸一样的话代价并不小,而且体验并不能完全接近原生(比如遮罩无法覆盖导航栏)

  • 如果是原生提供的,更改风格时原生改掉就行了,其它无效变动,如果H5单独维护一套,那么就被迫一起同步,平白新增很多的工作量

  • 而且H5还会存在一些坐标、尺寸计算偏差问题

一般情况下H5通过如下API即可调用

quick.ui.toast('xxxx');
quick.ui.alert('xxxx');

quick.ui.alert({
    title: "标题",
    message: "信息",
    buttonName: "确定",
    success: function(result) {
        // 点击 alert的按钮后回调
    },
    error: function(err) {}
});

页面切换的转场由原生实现

一般PC浏览器中,页面之间的调整直接通过a标签完成(或者改变href跳转),
但是这种跳转有一个缺点:

无法使用转场动画,每次都是干巴巴的等浏览器加载进度条,体验很差

因此针对这种情况,原生需要提供特点的API来供页面调用,可以有原生转场动画,在新的webview中打开这个页面

quick.page.open({
    pageUrl: "./xxx.html",
    data: {
        // 额外传递的数据
        key1: 'value1'
    },
    success: function(result) {},
    error: function(error) {}
});

采用这种方式打开的页面不再是在本webview中跳转,而是直接用新的webview打开,有过渡动画,而且以前的页面仍然存在内存中,接近原生体验

譬如

页面A -> 页面B -> 页面C

可以看到,如果是直接调整,页面A和B是不存存在的,而是会被替换,但是采用原生webview打开后,三个页面同时存在

仍然支持第三方页面的href跳转

虽然说可以有API打开的增强方式,但是仍然需要支持href跳转,这在集成第三方页面时十分重要(将已经写好的第三方纯网页集成到容器中,作为某个子模块)

这里有一点需要注意:

这类页面一般由a标签或href跳转直接打开,没有转场动画,但是需要webview容器保存访问历史记录,
以避免多次跳转后一个后退就直接退出了整个模块

CPU密集型任务、底层的优化要由原生完成

当涉及到一些大量计算时,尽量避免直接在网页端完成,而是应该由原生提供API完成。

譬如对一张图片进行图像处理(曝光、水印、压缩等等),如果直接由网页完成的话会发现非常卡,发热也严重,而原生则没有这么多的问题

关于底层优化,其实整套混合开发框架中,底层容器的实现是核心部分

容器是否健壮,优化的如何,直接影响整个应用的体验

关于原生容器应该如何进行优化,后续会有专门的文章,这里不赘述,只是稍微提及一下:

  • 支持H5页面的离线访问(有线上版本和离线版本,通过本地路由表映射)

  • 离线资源动态更新(结合离线访问一起,比较复杂)

  • 资源缓存(如图片的缓存,脚本样式的缓存等)

  • 统一数据埋点采样等(手机应用使用数据)

  • ajax请求等等(还有很多,不一一列举)

能用H5实现的就尽量不要用原生

接下来就是在实际开发过程遵循的准则:

  • 能用H5实现的就尽量不要用原生

乍看之下可能和上述的有矛盾,但其实又是合理的,在排除了一些不适合H5实现的页面,剩余的绝大部分都是普通的业务页面,
这类页面基本可以毫无压力的采用H5。

所以,这时候,第一想法都是采用H5完成(因为一套代码可以在至少三个平台运行-浏览器,Android,iOS),
遇到一些比较困难的页面再去考虑原生实现(从开发效率上,维护代价上,更新方便上都比较麻烦)

那些H5开发中遇到最多的页面

最后,看下实际开发过程中遇到的最多的页面吧(以实际遇到的N个项目的总结)

  • 列表页面(下拉刷新,加载更多)

  • 纯详情展示页面(标题,关键字,内容)

  • 九宫格首页

  • 图片轮播(时常结合列表和九宫格)

  • 标准的表单提交页面

没错,80%都是上述这种可以算非常简单的页面。

譬如封装过一个下拉刷新组件,基本别人基于这个组件来开发,列表的代码几乎是千篇一律。(当然了,剥离了业务逻辑而言)

结束语

时至今日,Hybrid模式已经过了它最火的时候,市面上也出现如weexreact-native等直接写原生组件的框架,
但是,现在使用最多,应用最广的仍然要属这种传统的Hybrid模式,它已经进入了稳定期(可以说,传统H5开发(泛概念)不被APP淘汰,这种模式很难被挤下舞台)

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

附录

参考资料

【quickhybrid】API规划

前言

当一切就绪后,就要开始进行API规划,这一块是整个Hybrid框架中非常重要的内容,毕竟对于前端页面来说,只会通过JS API来调用功能。
基本上,API调用起来是否方便简洁影响着整个体验。

这里将内容细分为以下几点:

  • API约束(包括调用格式,传参格式,回调格式)

  • 功能规划(约定这个框架应该提供什么样的功能)

  • 权限校验(很重要的一块,校验后才能调用,包括权限校验的代码格式,校验一些什么内容,以及哪些API无需校验)

  • 模块化的API(按照模块划分,每一个模块可以作为单独的组件,便于拓展)

  • 其它优化(如在PC端调试API的页面,部分API支持Promise等)

API约束

API调用关乎着整个体验,我们约定所有API统一采用如下调用方式

quick.模块名.方法({
    参数1: "",
    参数2: "",
    success: fucntion(result) {
        // 成功回调
    },
    error: fucntion(error) {
        // 失败回调
    }
});

约束说明

  • 所有接口都为异步调用

  • 接收一个object类型的参数

  • 成功回调success

    • 通过result获取成功数据

    • 回调函数的触发时机由具体的API决定,有的API是调用时即可回调(短期),有的是某个事件触发后才被回调(长期)

  • 失败回调error,所有的API调用错误都会走失败回调

功能规划

混合开发框架最重要的一个功能就是__将原生功能以JS API形式提供给前端页面调用__

本框架的API规划如下:(这个项目中仅规划了部分功能,实际使用中自行拓展即可)

quick
    |- ui               // 系统ui组件
    |   |- toast        
    |   |- alert       
    |   |- confirm       
    |   |- prompt          
    |   |- showWaiting         
    |   |- closeWaiting          
    |   |- actionSheet       
    |   |- pickDate     
    |   |- pickTime
    |   |- pickDateTime
    |   |- popWindow
    |- page             // 页面(webview)管理
    |   |- open
    |   |- openLocal
    |   |- close
    |   |- reload
    |- navigator        // 导航栏控制
    |   |- setTitle
    |   |- setMultiTitle
    |   |- hookSysBack
    |   |- hookBackBtn
    |   |- setRightBtn
    |   |- setLeftBtn
    |   |- setRightMenu
    |- auth             // 权限认证相关
    |   |- getToken
    |- device           // 设备相关
    |   |- setOrientation
    |   |- getDeviceId
    |   |- getNetWorkInfo
    |   |- getVendorInfo
    |   |- closeInputKeyboard
    |   |- vibrate
    |   |- callPhone
    |   |- sendMsg
    |- runtime          // 运行环境
    |   |- launchApp
    |   |- getAppVersion
    |   |- getQuickVersion
    |   |- getGeolocation
    |   |- clearCache
    |   |- clipboard
    |   |- openUrl
    |- util             // 其它工具
    |   |- scan
    |   |- selectImage
    |   |- cameraImage
    |   |- selectFile
    |   |- openFile

上述规划的是最常用到的功能,具体每一个API的介绍,传入参数传出参数等会在框架的API文档中提到

权限校验

如果整套框架要对外开放(如允许第三方按规范接入),那么权限认证是必不可少的!

如果没有权限认证?可以想象下,随便一个页面就能调用任意API,获取敏感信息。。。

那么权限认证应该是怎样的呢?根据不同需求,可以划分一个等级。

  • 平台级别的(像钉钉、微信这类对外开放的),需要配合后台,有完整的授权,签名,校验机制

  • 项目级别的(N个项目同一个框架,但业务各不相同),简单的应用内部配置,直接校验一些域名白名单信息即可

当然了,这个框架是后面的项目级别的,大批量的项目都采用的,因此直接简单配置即可(示例中是在原生预留了这个入口,但没有实现)

这里暂不谈具体实现(实现可参考源码),只说说权限认证的流程:(如钉钉、微信中的)

quick.error(function(error) {
    // 处理错误
});

quick.config({
    ...
});
quick.ready(function() {
    // TODO: 处理验证成功后的事情,例如调用api
});

可以看到,和其它框架一样,前端也是configreadyerror三步,
然后原生接收到config时,内部进行校验(校验内容可以是检测页面地址是否符合域名白名单等等...)

而且,如果config失败或者没有校验,那么敏感API都无法调用

无需校验就可用的API

并不是所有API都需校验后才能用,我们约定以下API默认就可用(这里是一个粗糙的划分,实际上可以精确到每一个API)

ui模块的所有API
page模块的所有API
navigator模块的所有API

这样做的好处是,如果不涉及到一些敏感数据,可以无需校验,提供效率(校验如果规则毕竟重的话对速度还是有影响的)

模块化的API

有一个全局变量quick,但API并不是直接绑定在全局变量下,而是按模块划分,譬如

quick.ui
quick.device
quick.page
...

这里说明一下,用模块化划分,除了前端调用会更清晰外,原生进行API定义与组件API拓展时也更方便。

组件API的拓展机制

关于组件API的拓展机制,这里也不具体描述如何实现(实现可参考源码),仅说说大概是一个什么样的东西。

默认情况下,框架会注册以下组件(前面已经规划的所有模块)

ui
page
navigator
auth
device
runtime
util

但是假设某项目中突然遇到了一个需求,要新增一个支付功能,并且要以API的形式提供给H5页面调用,该如何实现呢?

注意,这个框架设计的初衷是可以供N个项目使用,所以不可能所有的功能都集成进入框架中的,各个项目可以拓展自己的组件

这时候就需要规划这个拓展机制了,如下

// 1.前端config时,传入需要注册的组件别名
quick.config({
   jsApiList: ['pay', 'speech']
});
// 2.原生框架中,接收到config后,基于传入的别名,去对应项目配置文件中查询路径,然后将对应路径的API实现类注册
// 对应的组件的API实现类不是放框架中的,而是由各自的项目管理的,到时候框架就是一个固定的库,给各个项目引用
// 代码实现这里省略
...
// 3.前端中,通过一个固定的方法,调用刚注册的组件API中的功能
quick.callApi({
    name: 'xxx',
    mudule: 'pay',
    ...
    data: {},
    success: function(result) {},
    error: function(error) {},
});

通过这一套机制,可以保持框架的可拓展性,就算应用不同的项目中,N多的功能,也能通过这种方法拓展,保持一致的使用

其它优化

在大体功能都实现后,接下来还需要做一些优化功能,具体效果可以是简化调用,也可以是方便调试

部分API支持Promise

前面定义的API中都是基于普通的回调进行的,在多层回调嵌套时仍然会毕竟麻烦,因此可以拓展支持Promise调用方式

在拓展前,先定一个基调:所有短期回调都支持Promise调用,长期回调不建议使用Promise

因为长期回调涉及到多次回调(比如右上角按钮),所以不建议使用Promise,如果强行要使用,这些API调用完毕后马上就会进入then

具体实现参考源码,这里稍微对比下普通调用与Promise调用

quick.ui.alert({
    title: '提示',
    message: 'sd#ddd测试',
    success: function(result) {
        console.log('点击alert成功');
    },
    error: function(error) {
        console.error('失败:' + JSON.stringify(error));
    }
});
quick.ui.alert({
    title: '提示',
    message: 'sd#ddd测试',
}).then(function(result) {
    console.log('点击alert成功');
}).catch(function(error) {
    console.error('失败:' + JSON.stringify(error));
});

上述还只是没有嵌套的对比情况,有嵌套时,区别更大

PC端调试API

类似于钉钉的调试页面,这里也规划了一个在PC端调试API的页面(基于websocket),后续会有更详细的说明。

原理方面的源码可以先参考https://github.com/dailc/node-server-examples/tree/master/node-socketio-hybriddebug

可以先预览下效果

其它

其它优化后续再介绍

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【完善】统一的账号体系、Cookie与Token认证

前言

本文并不包含实现,大概描述下思路

基于的实际场景是:N个项目+PC+移动的账号体系

统一的账号体系?

还是基于一个实际场景来,大概某个公司的业务最初是这样的:

  • 有一个统一的框架模版,接到的每一个项目都是按照框架模版copy+修改

  • 于是乎,每一个项目都有自己的登陆注册。。。

  • 然后就造成了一个尴尬的场景:某一个需求是做一个平台级的App,要求将以前的n个App的功能集成进来,作为模块(或频道),
    ,然后发现傻眼了,因为每一个模块的账号体系都不一样,完全无法做到互相交流

这时候,就出现了一个刚需:需要一套统一的账号认证体系,同一条线上,一个账号可以在N个模块之间通用

所以,从此以后,就有了这样方案:sso(单点登录)机制

关于这套机制的原理,网上已经有很多相关资料了,这里不赘述,直接上一张图总结下web-sso流程(这种图部分描述可能有点不是很适当,但暂时懒得改了,大致上差不多)

加上了移动端后的账号体系?

可以看到,上图中描述的单点登陆场景是基于web的,采用的也是cookie来传递信息。

后来某天,加上了一个移动Navtive端的业务,突然间这套方案好似就不好用了:

  • 因为Navtive端都是通过纯http的接口形式请求,所以cookie并不总是合适

那用什么方案替代呢?这里用的是token机制

即通过接口进行登陆,登陆成功后返回一个token字段,然后每次请求中在http请求中默认带上token头部。

当然了,token还得和前面的统一认证体系关联,这里为了方便,也直接上一张图总结:

当然了上图中的token非法后可以是通知原生端,然后原生端重新刷新token或刷新失败后重新登陆

最终是选择cookie还是token?

回到最初的话题,在Hybrid混合开发中,由于是基于webview的,所以也是可以直接选择cookie方案的,当然也可以选择token方案。

那么最终应该如何抉择呢?这里说下quickhybrid中计划的实现。(这里优先考虑的是前端仍然使用ajax,容器自动注入机制)

  • 首先,在Android是完全可以做到自动写入cookie的(即登陆后,登陆由接口返回jsessionid,然后每次资源请求时自动写入到cookie中)

  • 然后,在iOS的UIWebview中,cookie也比较便于管理,可以做到自动写入cookie

  • 但是,在iOS的WKWebview中,相应开发人员有提到由于WKWebview的网络请求是系统进程,所以无法监听前端到的ajax,也就是说,不能在ajax中注入cookie。。。

而且,由于某些原因,采用了最新的WKWebview来作为iOS容器,因此就放弃了cookie方案了,转而采用token方案

所以已知采用的是token,下面仍然会将两种方案的大致流程都描述。

PS:这里优先考虑的是前端仍然用ajax请求,配置cors解决跨域,token是由前端框架主动注入(先通过api获取然后注入)的,
这样容器就减少了工作。当然实际上,完全可以由容器提供代理方法请求网络的,这样连token注入等操作都无需前端关心了。
不同的方案都可行,仅仅是抉择问题

如何用cookie进行身份验证

前文中有提到在浏览器中时,后台可通过前端请求的cookie来验证身份,恰好在混合开发时,是基于webview,就等同于一个浏览器。
这时同样可以在请求中加入cookie来作为身份认证。

如何实现?简单的说就是前端页面仍然用H5的ajax之类手段请求接口,然后容器层面在每个请求时都自动写入cookie,从而完成正确的身份验证。

具体流程以结合Android原生为例:

1.有一个登陆页面,通过账号密码,登陆后接口字段中包含一个jsessionid字段(对于后台的session)

2.原生容器层面将这个字段保存下来(同时每次刷新时也确保字段合法)

3.原生容器监听页面的请求,然后往请求中加入cookie

    // <5.0
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        // 其它语句略去,版本判断略去
        setCookies(view.getContext(), url);
        ...
    }
    // 5.0+
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        // 同上
        ...
    }
    
    public static void setCookies(Context context, String url) {
        if (!TextUtils.isEmpty(url)) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                CookieSyncManager.createInstance(context);
            }
            CookieManager cookieManager = CookieManager.getInstance();
            cookieManager.setAcceptCookie(true);
            cookieManager.removeSessionCookie();
            // 右侧就是获取自己存储的jsessionid
            cookieManager.setCookie(url, "JSESSIONID=" + CommonInfo.getInstance().getToken().jsessionid);
            CookieSyncManager.getInstance().sync();
        }
    }

可以看到在Android中的关键就是CookieSyncManager来管理cookie。
当然了这里还有一个小关键,就是cookie一般是跨域的,因此前端需要主动在ajax中允许跨域cookie才能生效。

setting.xhrFields = {
    // 跨域带cookie
    withCredentials: true
};

同时,后台也得允许跨域cookie才能正常请求(注意跨域cookie的配置是不允许用*的,必须是特定的域名)

理论上来说,ios的UIWebview中也能做到cookie写入,但是在用上了WKWebview后,据相应的开发人员说,由于WK中的网络请求都是系统进程。
如果页面用ajax请求,容器是无法监听到的,也就是无法在请求中写入cookie(没有更权威的人员来背书,也就认为它确实不能了)。

所以这种方式被弃用了。而采用了下面的token方案。

如何用token进行身份验证

token来校验身份应该是移动端中最常用的一种做法了,抛开token的原理不谈,其实就移动端来说面,流程是挺简单的。

这是native中使用token的流程。

1.有一个登陆页面,通过账号密码,登陆后接口字段中包含token字段

2.原生容器层面将token字段保存下来(一般还会有一个refreshToken字段以确保token过期时刷新token)

3.原生进行接口http请求时,在把token加入到一个头部。然后后台读取这个头部里的token字段进行验证。

headers.Authorization = 'Bearer ' + token;

譬如token验证的头部大概长上面这样(主要是后端需要统一约定一个头部然后通信)

那么,在webview中的ajax请求呢?一样可以使用上面的方法。

在ajax请求中带上token头部即可(这一步一般有前端业务框架封装了)

不过注意,ajax请求时很多时候都会有跨域这个问题,需要一起配合解决。

当然,原生也可以提供一个代理网络请求的API,然后h5通过这个API来进行网络请求,这样token可以由原生统一添加,而且也不会有跨域问题。

所以,其实quickhybrid中,最终是可以采用这个token方案来进行身份验证的。

最后,还是得申明下,token方案的关键是token的生成(即上述的sso原理)。这里仅仅是提到而已。并没有展开将(因为展开的话整套后台逻辑并不简单,后续可以考虑单独结合后台来将)

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】Android端的项目实现

前言

前文中就有提到,Hybrid模式的核心就是在原生,而本文就以此项目的Android部分为例介绍Android部分的实现。

提示,由于各种各样的原因,本项目中的Android容器确保核心交互以及部分重要API实现,关于底层容器优化等机制后续再考虑完善。

大致内容如下:

  • JSBridge核心交互部分

  • uipagenavigator等部分常用API的实现

  • 组件(自定义)API拓展的实现

  • 容器h5支撑的部分完善(如支持fileinput文件选择,地理定位等-默认不生效的)

  • API的权限校验仅预留了一个入口,模拟最简单的实现

  • 其它如离线资源加载更新,底层优化等机制暂时不提供

项目的结构

基于AndroidStudio的项目,为了便于管理,稍微分成了几个模块,
而且由于主要精力已经偏移到了JS前端,已经不想再花大力气重构Android代码了,
因此仅仅是将代码从业务中抽取出来,留下了一些稍微精简的代码(也不是特别精简)。

所以如果发现代码风格,规范等不太合适,请先将就着。

整体目录结构如下:

quickhybrid-android
    |- app                  // application,应用主程序
    |   |- api/PayApi       // 拓展了一个组件API
    |   |- MainActivity     // 入口页面
    |- core                 // library,核心工具类模块,放一些通用工具类
    |   |- baseapp
    |   |- net
    |   |- ui
    |   |- util
    |- jsbridge             // library,JSBridge模块,混合开发的核心实现
    |   |- api
    |   |- bean
    |   |- bridge
    |   |- control
    |   |- view

代码架构

简单的三次架构:底层核心工具类->JSBridge桥接实现->app应用实现

core
    |- application                  // 应用流程控制,Activity管理,崩溃日志等
    |- baseapp                      // 一些基础Activity,Fragment的定义
    |- net                          // 网络请求相关
    |- ui                           // 一些UI效果的定义与实现
    |- util                         // 通用工具类
    
jsbridge
    |- api                          // 定义API,开放原生功能给H5
    |- bean                         // 放一些实体类
    |- bridge                       // 桥接的定义以及核心实现
    |- control                      // 控制类,包括回调控制,页面加载控制,文件选择控制等
    |- view                         // 定义混合开发需要的webview和fragment实现
    
app
    |- api                          // 拓展项目需要的自定义组件API
    |- AppApplication.java          // 应用的控制
    |- MainActivity.java            // 入口界面的控制

权限配置

原生应用中,不可逃避的就是打包后的权限问题,没有权限,很多功能都使用不了,
简单起见,这里将应用中用的权限都列了出来(基于多种考虑,并没有遵循最小原则)

<!-- ===============================权限配置声明=============================== -->
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.READ_CALL_LOG" />
    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.READ_LOGS" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.READ_OWNER_DATA" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.READ_PROFILE" />

注意,6.0之上需要动态权限,请确保已经给应用开了对应的权限

Gradle配置

AndroidStudio中项目要正确运行起来,需要有一个正确的Gradle配置。

这里也就几个关键性的配置作说明,其余的可以参考源码

gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-all.zip

如果遇到gradle编译不动,可以像上述一样,把这个文件的gradle版本修改为本地用的版本
(否则的话,没有科学上网就很有可能卡住)

setting.gradle

include ':app', ':jsbridge', ':core'

里面很简单,就是一行代码,将三个用到的模块都引用进来

build.gradle(core)

仅挑选了部分进行说明

apply plugin: 'com.android.library'

android {
    compileSdkVersion 25

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        ...
    }
    ...
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:support-v4:25.3.1'
    compile 'com.android.support:design:25.3.1'
    compile 'com.android.support:recyclerview-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.jakewharton:butterknife:8.6.0'
    compile 'com.google.code.gson:gson:2.8.0'
    compile 'com.journeyapps:zxing-android-embedded:3.5.0'
    compile 'com.liulishuo.filedownloader:library:1.5.5'
    compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
    compile 'me.iwf.photopicker:PhotoPicker:0.9.10@aar'
    compile 'com.github.bumptech.glide:glide:4.1.1'
    ...
}

上述的关键信息有几点:

  • apply plugin: 'com.android.library'代表是模块而不是主应用

  • minSdkVersion 16代表最低兼容4.1的版本

  • targetSdkVersion 25是编译版本,targetSdkVersion 22提供向前兼容的作用,22时不需要动态权限,
    主要作用是某些API在不同版本中使用不一样,或者根本就在低版本中没有。

  • versionNameversionCode进行版本控制

  • dependencies中是依赖信息,首先compile fileTree添加了libs下的所有离线依赖(里面有离线依赖包),
    然后compile一些必须的依赖(譬如用到了gson,自动注解,文件下载等等)

为什么这里没用implementation添加依赖,而是用compile?因为implementation不具有传递性,这样引用core的jsbridge就用不到了,
而我们需要确保jsbridge中也用到,所以就用了compile。

build.gradle(jsbridge)

一部分类似的代码就没有贴出来了

apply plugin: 'com.android.library'

...

dependencies {
    implementation project(':core')
    ...
}

这里和core不同之处在于,内部依赖于core模块,使用了implementation project
这样在jsbridge内部就能使用core的源码了。

需要注意的是,implementation不具有传递性(core只会暴露给jsbridge,不会传递下去)

build.gradle(app)

一部分类似的代码就没有贴出来了

apply plugin: 'com.android.application'

android {
    defaultConfig {
        applicationId "com.quick.quickhybrid"
        versionCode 1
        versionName "1.0"
    }
   ...
}

dependencies {
    implementation project(':core')
    implementation project(':jsbridge')
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // butterknife8.0+版本支持控件注解必须在可运行的model加上
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
    ...
}

与之前相比,有几点关键信息

  • apply plugin: 'com.android.application'代表是主应用而不是模块

  • applicationId定义了应用id

  • 同样有自己的版本控制,但是注意,这里是容器版本号,前面的如jsbridge中是quick的版本号,有区别的

  • implementation依赖了前面两个模块,同时,后面引入了应用中可能需要的依赖

  • annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0',这行代码是为了使得butterknife自动注解生效的配置

targetSdkVersion说明

配置中使用的版本是22,因为在这个版本以上会有动态权限问题,比较麻烦,需要更改部分逻辑。因此就暂时未修改了。

譬如操作私有文件的权限问题等等

一些关键代码

代码方面,也无法一一全部说明,这里仅列举一些比较重要的步骤实现,其余可参考源码

UA约定

前面的JS项目中就已经有提到UA约定,就是在加载对于webview时,统一在webview中加上如下UA标识

WebSettings settings = getSettings();
String ua = settings.getUserAgentString();
// 设置浏览器UA,JS端通过UA判断是否属于Quick环境
settings.setUserAgentString(ua + " QuickHybridJs/" + BuildConfig.VERSION_NAME);

一些关键的webview设置

// 设置支持JS
settings.setJavaScriptEnabled(true);
// 设置是否支持meta标签来控制缩放
settings.setUseWideViewPort(true);
// 缩放至屏幕的大小
settings.setLoadWithOverviewMode(true);
// 设置内置的缩放控件(若SupportZoom为false,该设置项无效)
settings.setBuiltInZoomControls(true);
// 设置缓存模式
// LOAD_DEFAULT 根据HTTP协议header中设置的cache-control属性来执行加载策略
// LOAD_CACHE_ELSE_NETWORK 只要本地有无论是否过期都从本地获取
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setDomStorageEnabled(true);
// 设置AppCache 需要H5页面配置manifest文件(官方已不推介使用)
String appCachePath = getContext().getCacheDir().getAbsolutePath();
settings.setAppCachePath(appCachePath);
settings.setAppCacheEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 强制开启android webview debug模式使用Chrome inspect(https://developers.google.com/web/tools/chrome-devtools/remote-debugging/)
    WebView.setWebContentsDebuggingEnabled(true);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    CookieManager.getInstance().setAcceptThirdPartyCookies(this, true);
}

上述的一系列配置下去才能让H5页面的大部分功能正常开启,如localstorage,cookie,viewport,javascript等

支持H5地理定位

在继承WebChromeClient的QuickWebChromeClient

    @Override
    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
        callback.invoke(origin, true, false);
        super.onGeolocationPermissionsShowPrompt(origin, callback);
    }

需要重新才支持地理定位,否则纯h5定位无法获取地理位置(或者被迫使用了网络定位)

支持文件选择

同样在继承WebChromeClient的QuickWebChromeClient

    /**
     * Android 4.1+适用
     *
     * @param uploadMsg
     * @param acceptType
     * @param capture
     */
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
        loadPage.getFileChooser().showFileChooser(uploadMsg, acceptType, capture);
    }

    /**
     * Android 5.0+适用
     *
     * @param webView
     * @param filePathCallback
     * @param fileChooserParams
     * @return
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        loadPage.getFileChooser().showFileChooser(webView, filePathCallback, fileChooserParams);
        return true;
    }

上述的操作是主动监听文件的选择,然后自动调用原生中的处理方案,譬如弹出一个通用的选择框,进行选择等。
如果不实现,无法正常通过FileInput选择文件,而实际上,FileInput又是一个很常用的功能。

监听JSBridge的触发

同样在继承WebChromeClient的QuickWebChromeClient

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        result.confirm(JSBridge.callJava(loadPage.getFragment(), message,loadPage.hasConfig()));
        return true;
    }

为了方便,直接使用onJsPrompt来作为交互通道,前文中也相应提到过

其它

在直接提供API前,还有很多需要做的基础工作,譬如浏览历史记录管理,监听附件下载,页面加载报错处理等等,这里不再赘述,可以直接参考源码

最后,关于一些JSBridge实现,API实现,由于本系列的其它文中或多或少都已经提到,这里就不再赘述了,可以直接参考源码

另外,后续如果继续有容器优化等操作,也会单独整理,加入本系列。

前端页面示例

为了方便,直接集成到了app/assets/中,入口页面默认会加载它,也可以直接看源码

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】JS端的项目实现

前言

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

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

项目的结构

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

整个项目基于ES6Airbnb代码规范,使用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对象并且参数的最后加上resolvereject

而且,后续如果有新的统一预处理(调用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标识

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

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

  • callbackId是本次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/quickhybrid

【quickhybrid】架构一个Hybrid框架

前言

虽然说本系列中架构篇是第一章,但实际过程中是在慢慢演化的第二版中才有这个概念,
经过不断的迭代,演化才逐步稳定

明确目标

首先明确需要做成一个什么样的框架?

大致就是:

  • 一套API规范(统一AndroidiOS),所有API异步调用(防止阻塞)

  • 提供大部分原生功能的API(包括很多常用的功能给H5使用)

  • 原生需要能调用到H5中注册的方法(用关于原生主动通知)

  • 部分API需要支持H5环境(譬如alert需要在AndroidiOS、浏览器中同时运行)

  • API类别需要包括事件监听(如网络变化),页面跳转(如打开页面,关闭通过回调回传值),UI显示(调用后立即执行)等

整体架构

其中:

  • quick API指的就是quick hybrid框架提供给H5调用的JS API

  • 最外层的统一JSAPI规范就是quick API

  • 多平台支持的意思是-譬如调用了quick.ui.alert,在quick hybrid容器中会有响应(原生的弹窗),
    同时在浏览器中也会有响应(H5实现的弹窗),或者在其它容器中(如DD)也会有响应(其它容器实现的弹窗)

  • 多平台支持并不是所有API都会支持,而是指一些常用的API在多个平台下都有实现(比如UI类API一般都会支持,但是原生设备相关就不会在浏览器支持)

【目标分析】需要哪些工作

根据quick hybrid的整体架构与目标,我们需要先分析需要实现哪一些内容:

  • 【核心工作】制定quick平台下前端和原生容器的交互规则(JSBridge

  • 【核心工作】前端和原生(Android/iOS)分别实现JSBridge交互(包括互相调用,回调等机制)

  • 【核心工作】完成前端调用多平台的支撑(API在不同平台下有不同实现,并会根据不同环境自动转换)

  • 【重要工作】规划功能API(需要提供哪些功能,并且每一个功能应该在哪些平台下有实现)

  • 【重要工作】前端和原生(Android/iOS)分别实现这些功能API(第一步根据二八原则实现重点API即可)

  • 【重要工作】处理好短期API(即调即用,立即回收),长期API(一个页面中能被多次触发,如导航了按钮监听),事件监听API(整个应用生命周期内监听,如网络变化)等不同类型

  • 【优化完善】原生API实现的优化,前端代码的优化,权限认证,本地资源等等

然后就可以基于这些目标,逐步完成每一个规划的内容

【分解目标】总体规划

  • 【quick hybrid】JSBridge的实现

  • 【quick hybrid】H5和原生的职责划分

  • 【quick hybrid】API的分类:短期API、长期API

  • 【quick hybrid】API规划

拓展:

  • 【quick hybrid】H5和Native交互原理

【分解目标】API的实现

  • 【quick hybrid】API多平台支撑的实现

  • 【quick hybrid】组件(自定义)API的实现

  • 【quick hybrid】JS端的项目实现

  • 【quick hybrid】Android端的项目实现

  • 【quick hybrid】iOS端的项目实现

【分解目标】优化与完善

  • 账号体系、Cookie还是Token?

  • hybrid容器的优化与完善

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】API的分类:短期API、长期API

前言

一切就绪,开始规划API,这里在规划前对API进行了一次分类:短期API、长期API

首先申明下,这个是在实际框架演变过程中自创的一个概念,其它混合框架可能也会有这个概念,但应该是会在原生底层来实现,而不是前端实现。。
而这里由于是前端驱动,所以相比其它混合框架,前端多了一个处理引擎(包括多平台适配,API处理等等)

划分的凭据

根据API回调的实际执行次数来划分,执行一次自动回收的是短期API,可以执行多次的是长期API

譬如,短期回调包括:

ui
    alert,toast等等
page
    open,reload等等
...

譬如,长期回调包括:

navigator
    hookBackBtn,setRightBtn
event
    registerEvent:'resume','netChange'等等
...

而且规定,短期API执行完一次后必须自动删除引用,避免无法及时回收,长期API则不会自动回收,会一直监听直到主动取消或页面关闭

如下图

在代码层次两者有何区别?

API设计的一个原则就是尽量简单优雅,所以__在H5的调用层次来看,这两种不会有任何区别__,比如

quick.navigator.setTitle({
    title: '测试标题',
    subTitle: '子标题',
    success: function(result) {},
    error: function(err) {}
});

quick.navigator.setRightBtn({
    isShow: 1,
    text: '按钮右1',
    which: 0,
    success: function(result) {
        /**
         * 按钮点击后回调
         */
    },
    error: function(error) {}
});

这两个API从定义上一个是长期回调,一个是短期回调,但是在调用者看来,写法不会有区别,仍然是同一种风格的,也不会要求传递额外参数

在框架定义层次,需要区分

虽然在调用层次来看,并无区别,但是在框架内部,还是有所不同的

譬如

// 在原生调用完对应的方法后,会执行对应的回调函数id,并删除
const responseCallbacks = {};
// 长期存在的回调,调用后不会删除
const responseCallbacksLongTerm = {};

长期API在调用后,回调会添加到responseCallbacksLongTerm池中,短期AP会添加到responseCallbacks

// 这里规定,原生执行方法完毕后准备通知h5执行回调时,回调函数id是responseId
responseCallback = responseCallbacks[responseId];
// 默认先短期再长期
responseCallback = responseCallback || responseCallbacksLongTerm[responseId];
// 执行本地的回调函数
responseCallback && responseCallback(responseData);
delete responseCallbacks[responseId];

在回调执行阶段,如果是短期回调,会自动删除引用,方便垃圾处理器自动回收(里面会保证短期id和长期id不重复)

在原生容器层面两者的区别?

在H5框架层次,长期API和短期API的差距也不是很大,但是在原生层面,差距明显变大(这也是为什么会把这两种单独划分成两个概念)

为了尽可能的减少冗余信息,我们仅以navigator.setRightBtnsetTitleAPI为例,它们的前端调用我们在前面已经看到了,那么它在原生容器中的实现如下

原生中长期API是会先行监听的(考虑过调用后监听和调用前监听两种模式,最终采取了调用前监听-结合了原生原有代码考虑)

以下的伪代码基于前面的JSBridge介绍文章,这里假设已经了解了,不再赘述

注意:这里的setTitle为短期API,但是如果想要给它加上点击回调的话是需要变成长期API的,我们仅以短期的状态为例

Android容器中的对比

public class NavigatorApi implements IBridgeImpl {
    public static void setTitle(..., JSONObject params, Callback callback) {
        // 获取了参数后直接改变了导航栏的标题
        ...
        // 然后开始回调
        callback.apply(...);
    }
    
    public static void setRightBtn(..., JSONObject params, Callback callback) {
        // 解析参数,然后将回调信息和监听函数添加到webview控制器中,这个控制器默认监听了很多事件。但只有这些回调信息添加后才会执行
        // 里面也包括了更改按钮的文字或图标
        ...
        WebloaderControl.addPort(callback.port, 'xxx右侧按钮监听事件');
    }
}

WebloaderControl中的作用是监听各种事件(如利用一些天然的resume事件等),然后当对应事件触发时,只有回调池中(addPort添加进的)存在,才会执行

其中'xxx右侧按钮监听事件'是在webview自行监听的,这里就不再赘述了

只需直到,WebloaderControl.addPort,右侧监听事件中才会执行添加进去的回调,否则是没有什么操作的

可以看到短期API和长期API的形态是不一样的,一个是调用后立即执行,一个是已经监听了,就等着你的回调

iOS容器中的对比

@implementation NavigatorApi
- (void)registerHandlers {
    [self registerHandlerName:@"setTitle" handler:^(id data, WVJBResponseCallback responseCallback) {
        // 同样,在执行完功能后进行回调
        ...
        responseCallback(...);
    }
    
    [self registerHandlerName:@"setRightBtn" handler:^(id data, WVJBResponseCallback responseCallback) {
        // 里面也包括了更改按钮的文字或图标等一些步骤
        ...
        // 同样,这里是将回调添加到缓存池中,然后监听到对于按钮点击时就会执行回调
        [self cacheCallback:responseCallback handlerName:@"setRightBtn"];
    }
}

iOS和Android中相比,虽然实现代码不一样,但是逻辑是一致的,同样在监听后才会执行对应回调

结束语

短期和长期概念只是用来更好的划分和管理API,有了这个概念后,接下来就可以开始对API进行统一规划了

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】JSBridge的实现

前言

本文介绍quick hybrid框架的核心JSBridge的实现

由于在最新版本中,已经没有考虑iOS7等低版本,因此在选用方案时没有采用url scheme方式,而是直接基于WKWebView实现

交互原理

具体H5和Native的交互原理可以参考前文的H5和Native交互原理

交互原理图如下:

预计的最终效果

如果一步一步来分析,最后再看效果,可能会很枯燥,甚至还有点化简为繁的样子。(感觉直接看代码应该是最简单的,奈何每次写成文章时都得加一大堆的描述)

因此,先来看看最终完成后应该是什么样的。

// 调用ui中alert的示例
callHandler({
    // 模块名,本文中的API划分了模块
    module: 'ui',
    // 方法名
    name: 'alert',
    // 需要传递给native的请求参数
    data: {
        message: 'hello',
    },
    callback: function(res) {
        /**
         * 调用后的回调,接收原生传递的回调数据
         * alert如果成功,可以点击后再回调
         {
            // 1成功/0失败
            code: 1,
            message: '描述',
            // 数据
            data: {},
         }
         */
    }
});

架构

从头开始实现一个JSBridge,很容易两眼一抹黑,无从下手。

因此我们需要先从大方向上把功能交互确定好,然后再开始构建细节,编码实现

功能分析与确认

根据核心架构,规划需要实现的功能:

  • H5桥接对象的设计(JSBridge)

    • 短期回调池,需自动回收

    • 长期回调池,可多次使用

    • 调用Native方法的通道,桥接对象上原生注册的接收方法

    • 接收Native调用的通道,桥接对象上H5注册的接收方法

    • H5可以注册主动给原生调用的方法

  • 原生桥接对象的设计

    • 长期方法池,每一个长期调用都会存储在回调池中,可以多次使用

    • 短期立即执行,每一个短期调用都是立即执行

    • 调用H5方法的通道,桥接对象上H5注册的接收方法

    • 接收H5调用的通道,桥接对象上原生注册的接收方法,底层自动解析,然后执行对应API

    • 回调对象,底层基于调用H5的通道,每次执行完毕后都通过回调对象回调给H5

    • 主动调用H5,不同于回调对象只能被动响应,这个可以主动调用H5中注册的方法

  • API的设计

    • H5中的API,供前端调用,底层通过调用Native方法的通道,然后将预处理后的参数发送给原生

    • Native中的API,真正的功能实现

接下来就是JSBridge的实现

全局通信对象的确认

最重要的,是先把H5和Native通信时的几个全局桥接对象确定:

  • JSBridge,H5端的桥接对象,对象中绑定了接收原生调用的方法_handleMessageFromNative,以及内部有对回调函数等进行管理

  • webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage,iOS端的桥接对象,这个方法接收H5的调用

  • prompt,Android端的桥接对象,为了方便,直接重写了WebChromeClient中的onJsPrompt

// H5端的内部逻辑处理
window.JSBridge = {...}

// 接收原生的调用,有回调以及主动调用两种
JSBridge._handleMessageFromNative = function() {...}
// H5主动调用原生
if (os.ios) {
    // ios采用
    window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);
} else {
    window.top.prompt(...);
}

JSBridge对象的实现

H5就依靠这个对象与Native通信,这里仅介绍核心的逻辑

JSBridge = {
    // 本地注册的方法集合,原生只能主动调用本地注册的方法
    messageHandlers: {},
    // 短期回调函数集合,在原生调用完对应的方法后会自动删除回收
    responseCallbacks: {},
    // 长期存在的回调集合,可以多次调用
    responseCallbacksLongTerm: {},
    
    _handleMessageFromNative: function(messageJSON) {
        // 内部的处理:
        
        **
          如果是回调函数:
          如果是短期回调responseCallbacks中查询回调id,并执行,执行后自动销毁
          如果是短期回调responseCallbacksLongTerm中查询回调id,并执行
          *
          
          **
          如果是Native的主动调用:
          去本地注册的方法池messageHandlers中搜索,并执行
          *
    },
    
    callHandler: function(...) {
        // 底层分别调用Android或iOS的原生接收方法
        
        // 如果是短期回调,会将回调添加到responseCallbacks中
        // 如果是长期回调,会将回调添加到responseCallbacksLongTerm中
        
        // 省略若干逻辑
        ...
        
        if (os.ios) {
            // ios采用
            window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);
        } else {
            window.top.prompt(...);
        }
    },
    
    registerHandler: function(handlerName, handler) {
        // H5在本地注册可供原生调用的方法
    },
    
    ...
};

Android中桥接对象的实现

Android中的核心就是JSBridge,其余都是围绕这个来的,以下是伪代码,列举主要的逻辑

public class JSBridge {
    // 缓存所有的API模块(注册时添加进去)
    static exposedAPIModlues =  new HashMap<>();
    
    static register(String apiModelName, Class<? extends IBridgeImpl> clazz) {
        // 注册时会自动寻找所有的框架API模块,然后添加到缓存exposedAPIModlues,每一个模块中可以有若干API
        // 每一个模块都需要实现IBridgeImpl接口
        ...
    }
    
    static callAPI(...) {
        // 首先会解析参数(H5中传递的),解析出调用了哪一个API,传递了些什么,解析结果包括如下
        // port:H5传递的回调id,是responseCallbacks或responseCallbacksLongTerm中的key
        // moduleName:调用的API的模块名,用来检索exposedAPIModlues中注册的模块
        // name:调用的API的方法名,在对于找到的模块中去查找API
        // 其他:包括传递的参数等等
        
        // 然后会根据H5的回调端口号,生成一个回调对象(用来回调通知H5)
        Callback callback = new Callback(port);
        
        // 之后,根据解析的参数寻找API方法
        // java.lang.reflect.Method;
        Method method = searchMethodBy(moduleName, name);
        
        // 没有找到方法会回调对于错误信息
        // 否则执行对于的method,传递解析出的参数
        // 并且在method内部执行完毕后主动回调给H5对于信息
        method.invoke(..., callback);
    }
}

callback类伪代码如下:

public class Callback {
    apply(...) {
        // 先解析拼装参数,然后将参数组装成javascript代码,参数中包含Callback对于的port值(回调id)
        ...
        String js = javascript:JSBridge._handleMessageFromNative(对于的json参数);
        
        callJS(js);
    }
    callHandler(...) {
        // 主动调用H5,封装的参数中不再是回调id,而是handleName
        ...
        callJS(js);
    }
    callJS(js) {
        // 底层通过loadUrl执行
        ...
        webviewContext.loadUrl(js);
    }
}

IBridgeImpl接口是空的,只是一个抽象定义,以下以某个实现这个接口的API为例

// 为了清晰,以ui.alert为例
public class xxxApi implements IBridgeImpl {
    // 定义一个注册的模块别名,方便查找,譬如ui
    static RegisterName = "ui";
    
    // 模块中的某个API,譬如alert
    public static void alert(..., Callback callback) {
        // 接下来就是在这个API中实现对于的逻辑
        ...
        // 最后,通过触发callback通知H5即可
        callback.apply(...);
    }
}

最后可以看到,在webview中,重新了WebChromeClientonJsPrompt来接收H5的调用

并且在webview加载时就会调用JSBridgeregister

public class XXXWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(..., JsPromptResult result) {
        // 内部触发JSBridge.callJava
        result.confirm(JSBridge.callJava(...));
        return true;
    }
}

以上几个就是Andorid中JSBridge核心实现,其他的如长期回调,短期回调,细节实现等优化不是核心逻辑,就列举,详情可以参考最后的源码

iOS中桥接对象的实现

这里仍然是OC实现的,主要参考的marcuswestin/WebViewJavascriptBridge实现

核心仍然是WKWebViewJavascriptBridge,其余一切都是通过它来分发代理

@implementation WKWebViewJavascriptBridge {
    // 内部基于一个WebViewJavascriptBridgeBase基类(基类中定义交互方法)
    WebViewJavascriptBridgeBase *_base;    
}
/**
 * API
 */
- (void)callHandler:(NSString *)handlerName data:(id)data {
    // 主动调用H5的方法
    // 底层调用_base的sendData,发送数据给H5
}

- (void)registerModuleFrameAPI {
    // 注册模块API,模块用到了别名代理
    [self registerHandlersWithClassName:@"UIApi" moduleName:@"ui"];
    
    // 其中registerHandlersWithClassName就是将模块示例化注册到全局中的作用,不赘述
}

- (void)excuteMessage:(NSString *)message {
    // 内部执行API的实现,这里会解析API解析出来的数据,如
    // module.name,port(callbackid)等
    ...
    // 然后底层调用_base的excuteMsg(它内部会根据注册的API,找到相对应的,然后执行原生功能,最后通过回调通知H5)
}

#pragma mark - WKScriptMessageHandler其实就是一个遵循的协议,它能让网页通过JS把消息发送给OC
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    // 监听到对于API调用时,底层会调用excuteMessage
    if ([message.name isEqualToString:@"WKWebViewJavascriptBridge"]) {
        [self excuteMessage:message.body];
    }
}

然后看看它基类WebViewJavascriptBridgeBase的实现

@implementation WebViewJavascriptBridgeBase

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    // 底层将接收到的数据组装成js代码执行
    ...
    NSString* javascriptCommand = [NSString stringWithFormat:@"JSBridge._handleMessageFromNative('%@');", messageJSON];
    
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
}

- (void)excuteMsg:(NSString *)messageQueueString moduleName:(NSString *)moduleName {
    // 底层根据对于的模块,API名,找到注册的handler
    ...
    
    // 然后创建一个回调对象
    WVJBResponseCallback responseCallback = (通过sendData通知H5回调数据);
    
    // 然后执行这个handler
    handler(message[@"data"], responseCallback);
}

接下来是API的定义

定义API模块之前,需要先了解RegisterBaseClass,所有模块必须实现的基类,定义了如何注册

@implementation RegisterBaseClass
#pragma mark - 注册api的统一方法
- (void)registerHandlers {
    // 子类重写改方法实现自定义API注册
}

#pragma mark - handler存取
- (void)registerHandlerName:(NSString *)handleName
                    handler:(WVJBHandler)handler {
    // 注册某个模块下的某个API
}

- (WVJBHandler)handler:(NSString *)handlerName {
    // 通过名称获取对应的API
}

要定义一个API模块,则需继承RegisterBaseClass然后重写registerHandlers(为了清晰,以ui.alert为例)

@implementation UIApi
- (void)registerHandlers {
    [self registerHandlerName:@"alert" handler:^(id data, WVJBResponseCallback responseCallback) {
        // 同样,在接收到数据,并处理后,通过responseCallback通知H5
        ...
        responseCallback(...);
    }
}

webview加载时就会调用WKWebViewJavascriptBridgeregisterModuleFrameAPI,对于模块名ui与别名UIApi,可以在注册时看到,它们之间是有一一对应关系的

然后在webview创建时,会进行监听,userContentController

WKWebViewConfiguration * webConfig = [[WKWebViewConfiguration alloc] init];
WKUserContentController * userContentVC = [[WKUserContentController alloc] init];
webConfig.userContentController = userContentVC;
WKWebView * wk = [[WKWebView alloc] initWithFrame: CGRectZero configuration: webConfig];

self.wv = wk;
...

// 代理
self.bridge = [WKWebViewJavascriptBridge bridgeForWebView: self.wv];
[self.bridge setWebViewDelegate: self];

// 添加供js调用oc的桥梁。这里的name对应WKScriptMessage中的name,多数情况下我们认为它就是方法名。
[self.wv.configuration.userContentController addScriptMessageHandler: self.bridge name: @"WKWebViewJavascriptBridge"];

同样,iOS中的长期回调等其它一些非核心内容也暂时隐藏了

API的设计

按照上述的实现,可以构建出一个完整的JSBridge交互流程,H5和Native的交互已经通了

接下来就是设计API真正给外界调用

准确的来说,API的设计已经脱离了JSBridge交互内容,属于混合框架框架应用层次,因此后续会有单独的章节介绍quick hybrid中的API

API如何实现?可以参考上文中Android的继承IBridgeImpl法以及iOS的继承RegisterBaseClass然后重写registerHandlers

至于该规划些什么API,这与实际的需求有关,不过一般情况下,像ui.alert等等一般都是必须的

更多详情请待后续章节

结束语

最后再来一张图巩固下把

至此,整个JSBridge交互就已经完成了

其实在总结文章时,考虑过很多种形式,发现,
如果是全文字描述,十分枯燥,很难坚持读下来,
如果是各种原理都用绘图+描述,发现会化简为繁,硬生生把难度提高了几个level,
所以最终采用的是伪代码(半伪半真)展示形式(剔除一些无效信息,提取关键,而且还不和最终的代码冲突)

虽然说,这整套流程都没有特别难的地方,涉及的知识点都不是特别深。但是却包含了前端,Android,iOS三个领域。
因此如果要将整套工作做的比较好的化最好还是有分工的好,比较一个人的精力有限,真正专精多个领域的人还是比较少的,
而且后续各个优化的内容也不少(API,优化,等等...)

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

附录

参考资料

【quickhybrid】API多平台支撑的实现

前言

在框架规划时,就有提到过这个框架的一些常用功能需要支持H5环境下的调用,也就是需要实现API的多平台支撑

为什么要多平台支撑?核心仍然是复用代码,比如在微信下,在钉钉下,在quick容器下,
如果没有多平台支撑,那么quick.ui.alert只能用于quick容器下,钉钉和微信下就得分别用其它代码实现,
代码复用率低,如果实现了多平台支撑。那么三个平台中同一个功能的代码则是一样的。

什么样的多平台支撑

当然了,本框架实现的多平台支撑和一般框架的有点区别。

一般的框架中支持多个平台更多的是一个polyfill,譬如

// 假设以前不支持h5
const oldToast = quick.ui.toast;

quick.ui.toast = function(...) {
    if (os.h5) {
        // 做一些h5中做的
        ...
    } else {
        oldToast(...);
    }
};

这就是垫片实现,如果是新的环境,用新的实现,否则用老的实现

而__本框架中的多平台实现是直接内置到了框架核心中__,也就是说框架本身就支持多平台API的设置

quick.extendModule('ui', [{
    namespace: 'toast',
    os: ['h5'],
    defaultParams: {
        message: '',
    },
    runCode(...rest) {
        // 定义h5环境中的做法
        ...
    },
}, ...];

quick.extendModule('ui', [{
    namespace: 'toast',
    os: ['quick'],
    defaultParams: {
        message: '',
    },
    runCode(...rest) {
        // 定义quick环境中的做法
        ...
    },
}, ...];

在框架内部定义API时,不再是直接的quick.ui.alert = xxx,而是通过特定的API单独给某个环境下定义实现

而且,框架中的定义,每一个API都是有quickh5环境下的实现的。

多平台支撑的核心

从上述的介绍中也可以看到,多平台支撑主要是前端的实现,与原生API,原生API在这里面只能算一个环境下的实现

核心就是基于:Object.defineProperty,重写set和get

Object.defineProperty(apiParent, apiName, {
    configurable: true,
    enumerable: true,
    get: function proxyGetter() {
        // 需要根据不同的环境,返回对应下的内容
        ...
    },
    set: function proxySetter() {
        // 可以提示禁止修改API
    },
});

本框架中的多平台实现代码可以参考源码,这里不赘述,下文中会介绍如何简单的实现一个多平台支撑API

实现一个多平台支撑API

我们先预设最终的结果:

quick.os.quick = true;
quick.ui.alert('hello'); // quick-hello

quick.os.quick = false;
quick.ui.alert('hello'); // h5-hello

quick.ui.alert = 11; // 提示:不允许修改quick API

那么要达到上述的要求,应该如何做呢?

写一个雏形

最简单的,先假设这些实现都已经存在,然后直接基于defineProperty返回

function alertH5(message) {
    alert('h5-' + message);
}
function alertQuick(message) {
    alert('quick-' + message);
}
const quick = {};

quick.ui = {};
quick.os = {
    quick: false,
};

Object.defineProperty(quick.ui, 'alert', {
    configurable: true,
    enumerable: true,
    get: function proxyGetter() {
        // 需要根据不同的环境,返回对应下的内容
        if (quick.os.quick) {
            return alertQuick;
        } else {
            return alertH5;
        }
    },
    set: function proxySetter() {
        // 可以提示禁止修改API
        alert('不允许修改quick API');
    },
});

那么,它的调用结果是

quick.os.quick = true;
quick.ui.alert('hello'); // quick-hello

quick.os.quick = false;
quick.ui.alert('hello'); // h5-hello

quick.ui.alert = 11; // 提示:不允许修改quick API

虽然效果和预设的一样,但是很明显还需优化完善

增加拓展API的方法

拓展方式的定义如下

const quick = {};

quick.os = {
    quick: false,
};
/**
 * 存放所有的代理 api对象
 * 每一个命名空间下的每一个os都可以执行
 * proxyapi[namespace][os]
 */
const proxysApis = {};
// 支持的所有环境
const supportOsArray = ['quick', 'h5'];

function getCurrProxyApiOs(currOs) {
    for (let i = 0, len = supportOsArray.length; i < len; i += 1) {
        if (currOs[supportOsArray[i]]) {
            return supportOsArray[i];
        }
    }

    // 默认是h5
    return 'h5';
}

// 如获取quick.ui.alert
function getModuleApiParentByNameSpace(module, namespace) {
    let apiParent = module;
    // 只取命名空间的父级,如果仅仅是xxx,是没有父级的
    const parentNamespaceArray = /[.]/.test(namespace) ? namespace.replace(/[.][^.]+$/, '').split('.') : [];

    parentNamespaceArray.forEach((item) = >{
        apiParent[item] = apiParent[item] || {};
        apiParent = apiParent[item];
    });

    return apiParent;
}

function proxyApiNamespace(apiParent, apiName, finalNameSpace) {
    // 代理API,将apiParent里的apiName代理到Proxy执行
    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');
        },
    });
}

function extendApi(moduleName, apiParam) {
    if (!apiParam || !apiParam.namespace) {
        return;
    }

    if (!quick[moduleName]) {
        quick[moduleName] = {};
    }

    const api = apiParam;
    const modlue = quick[moduleName];
    const apiNamespace = api.namespace;
    const apiParent = getModuleApiParentByNameSpace(modlue, apiNamespace);
    // 最终的命名空间是包含模块的
    const finalNameSpace = moduleName + '.' + apiNamespace;
    // 如果仅仅是xxx,直接取xxx,如果aa.bb,取bb
    const apiName = /[.]/.test(apiNamespace) ? api.namespace.match(/[.][^.]+$/)[0].substr(1) : apiNamespace;

    // 这里防止触发代理,就不用apiParent[apiName]了,而是用proxysApis[finalNameSpace]
    if (!proxysApis[finalNameSpace]) {
        // 如果还没有代理这个API的命名空间,代理之,只需要设置一次代理即可
        proxyApiNamespace(apiParent, apiName, finalNameSpace);
    }

    // 一个新的API代理,会替换以前API命名空间中对应的内容
    const apiRuncode = api.runCode;
    const oldProxyNamespace = proxysApis[finalNameSpace] || {};

    proxysApis[finalNameSpace] = {};

    supportOsArray.forEach((osTmp) = >{
        if (api.os && api.os.indexOf(osTmp) !== -1) {
            // 如果存在这个os,并且合法,重新定义
            proxysApis[finalNameSpace][osTmp] = apiRuncode;
        } else if (oldProxyNamespace[osTmp]) {
            // 否则仍然使用老版本的代理
            proxysApis[finalNameSpace][osTmp] = oldProxyNamespace[osTmp];
        }
    });
}

function extendModule(moduleName, apis) {
    if (!apis || !Array.isArray(apis)) {
        return;
    }
    if (!quick[moduleName]) {
        quick[moduleName] = [];
    }
    for (let i = 0, len = apis.length; i < len; i += 1) {
        extendApi(moduleName, apis[i]);
    }

}

quick.extendModule = extendModule;

上述代码中增加了些复杂度,有一个统一管理所有代理调用的池,然后每次会更新对于环境下的代理

基于上述的方式可以如下拓展对于环境下的API

quick.extendModule('ui', [{
    namespace: 'alert',
    os: ['h5'],
    defaultParams: {
        message: '',
    },
    runCode(message) {
        alert('h5-' + message);
    },
}]);

quick.extendModule('ui', [{
    namespace: 'alert',
    os: ['quick'],
    defaultParams: {
        message: '',
    },
    runCode(message) {
        alert('quick-' + message);
    },
}]);

最终的调用如下(结果和预期一致)

quick.os.quick = true;
quick.ui.alert('hello'); // quick-hello
quick.os.quick = false;
quick.ui.alert('hello'); // h5-hello
quick.ui.alert = 11; // 提示:不允许修改quick API

虽然就一两个API来说,这类拓展方式看起来很复杂,但是当API一多,特别是还需批量预处理时(如默认参数,Promise支持等),它的优势就出来了

多平台支撑在quick中的应用

quick hybrid框架中,默认支持quickh5有种环境,核心代码就是上述列举的(当然,内部增加了一些代理,默认参数处理等,会稍微复杂一点)。

基于这个核心,然后可以将框架的定义和API定义分开打包

quick.js
quick.h5.js

这样,最终看起来h5下的API定义就是一个拓展包,是没有它也不会影响quick环境下的使用,而且,如果增加一个新的环境(比如dd),
只需要再新增另一个环境的拓展包而已,各种写法都是一样的,这样便于了统一维护

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】H5和Native交互原理

前言

Hybrid架构的核心就是JSBridge交互,而实现这个交互的前提是弄清楚H5和Native端的交互

本文主要介绍Native端(Android/iOS)和H5端(泛指前端)的交互原理
(之前也整理过类似的文章,本系列重新梳理)

NativeH5交互的两种方式

原生和前端的交互有两种方式:url scheme以及JavaScriptCore(在Android中是addJavascriptInterface

url scheme适用于所有的系统设备(低版本Android和低版本iOS都适用)

但是url scheme毕竟是通过url拦截实现的,在大量数据传输,以及效率上都有影响

另一种方法则在低版本中会有这样或那样的问题

如JavaScriptCore不支持iOS7以下,addJavascriptInterface在4.2以前有风险漏洞

当然了,时至今日,这些低版本造成的影响已经慢慢不再

url scheme交互

这个是最广为流传的交互方式,起因是因为在hybrid刚出来时,很多低版本都需要兼容,因此几乎都用的这种

一些概念:

  • 一般清空下,url scheme是一种类似于url的链接,是为了方便app直接互相调用设计的

    • 具体为,可以用系统的OpenURI打开一个类似于url的链接(可拼入参数),
      然后系统会进行判断,如果是系统的url scheme,则打开系统应用,
      否则找看是否有app注册这种scheme,打开对应app

    • 需要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为(weixin://)

  • 而本文中混合开发交互的url scheme则是仿照上述的形式的一种方式

    • 具体为,由前端页面通过某种方式触发scheme(如用iframe.src),
      然后Native用某种方法捕获对应的url触发事件,然后拿到当前的触发url,
      根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行等

    • 协议类似于:quickhybrid://xxx

    • 一般这种交互的url没有必要在原生app配置中注册

  • 注意⚠️: ️iOS10以后,urlscheme必须符合url规范,否则会报错,

基本原理:

H5 -> 触发一个url(每一个功能代表的url都不同)-> Native端捕获到url

-> Native端分析属于哪一个功能并执行 -> Native端调用H5中的方法将执行结果回调给H5

如下图:

相比于其它方案的优点:

  • Android4.2以下,addJavascriptInterface方式有安全漏掉

  • iOS7以下,JavaScriptCore无法使用

所以如果需要兼容这类型低版本的机型,url scheme方案是不二选择

H5直接与Native交互

分别包括Android,iOS中H5和原生互相调用,总结如下:

  • H5调Android-原生通过addJavascriptInterface注册,然后H5直接调用

  • Android调H5-原生通过loadUrl来调用H5,4.4及以上还可以通过evaluateJavascript调用

  • H5调iOS-原生通过JavaScriptCore注册(需ios7以上),然后H5直接调用

  • iOS调H5-通过stringByEvaluatingJavaScriptFromString

H5调Android:

首先,原生webview需要先注册可供前端调用的JS函数

 WebSettings webSettings = mWebView.getSettings();  
 // Android容器允许JS脚本,必须要
webSettings.setJavaScriptEnabled(true);
// Android容器设置侨连对象
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
// Android4.2版本及以上,本地方法要加上注解@JavascriptInterface,否则会找不到方法。
private Object getJSBridge(){  
    Object insertObj = new Object(){  
        @JavascriptInterface
        public String foo(){  
            return "foo";  
        }  

        @JavascriptInterface
        public String foo2(final String param){  
            return "foo2:" + param;  
        }  

    };  
    return insertObj;  
}

然后H5中即可调用原生中注册的函数

// 调用方法一
window.JSBridge.foo(); // 返回:'foo'
// 调用方法二
window.JSBridge.foo2('test'); // 返回:'foo2:test'

注意:

  • 在Android4.2以上(api17后),暴露的api要加上注解@JavascriptInterface,否则会找不到方法。

  • 在api17以前,addJavascriptInterface有风险,hacker可以通过反编译获取Native注册的Js对象,
    然后在页面通过反射Java的内置静态类,获取一些敏感的信息和破坏

Android调H5:

4.4版本之前

// 即当前webview对象     
mWebView = new WebView(this);       
mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')"); 

// ui线程中运行
runOnUiThread(new Runnable() {  
        @Override  
        public void run() {  
            mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");  
            Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();  
        }  
});

4.4及以后(包括)

// 异步执行JS代码,并获取返回值    
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            // 这里的value即为对应JS方法的返回值
        }
});

注意:

  • 4.4之前Native通过loadUrl来调用JS方法,只能让某个JS方法执行,但是无法获取该方法的返回值

  • 4.4及之后,通过evaluateJavascript异步调用JS方法,并且能在onReceiveValue中拿到返回值

  • mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");
    函数需在UI线程运行,因为mWebView为UI控件(但是有一个坏处是会阻塞UI线程)

H5调iOS:

OC为例

首先,需要引入JavaScriptCore

#import <JavaScriptCore/JavaScriptCore.h>

然后原生需要注册API

//webview加载完毕后设置一些js接口
-(void)webViewDidFinishLoad:(UIWebView *)webView{
    [self hideProgress];
    [self setJSInterface];
}

-(void)setJSInterface{

    JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    // 注册名为foo的api方法
    context[@"foo"] = ^() {

        //获取参数
        NSArray *args = [JSContext currentArguments];
        NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
        //做一些自己的逻辑
        //返回一个值  'foo:'+title
        return [NSString stringWithFormat:@"foo:%@", title];
    };
    
}

之后前端就可以调用了

// 调用方法,用top是确保调用到最顶级,因为iframe要用top才能拿到顶级
window.top.foo('test'); // 返回:'foo:test'

注意:

  • 引入官方提供的JavaScriptCore库(iOS7中出现的),然后可以将api绑定到JSContext上
    (然后Html中JS默认通过window.top.*(iframe中时需加top)可调用)

  • iOS7之前,js无法直接调用Native,只能通过urlscheme方式间接调用

iOS调H5:

// 可以取得JS函数执行的返回值
// 方法必须是Html页面绑定在最顶层的window上对象的
// 如window.top.foo
// Swift
webview.stringByEvaluatingJavaScriptFromString("方法名(参数)")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];

注意:

  • Native调用JS方法时,能拿到JS方法的返回值

  • 有iframe时,需要获取顶层窗口的引用

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】如何实现一个Hybrid框架

章节目录

一些感慨

踏入前端领域满打满算也两年多了。到现在,主要方向已经是由Android原生转到了偏前端领域。

期间,不提自己的技术进步、视野拓宽,最大的产出之一应该就是从0开始构建了一个Hybrid框架了。

正值最近开始进行技术梳理,因此就准备写一系列文章沉淀起来。

本系列包含的内容清单

  • Hybrid框架的原理以及架构系列

  • JavaScript部分的原理以及源码系列(包括部分API的多容器的兼容)

  • Android部分的原理以及源码系列(仅覆盖核心实现以及API部分,不包含实际业务代码)

  • iOS部分的部分原理(一些坑会特别提出,理论上根据原理应该可以还原出)

    • 由于本人没写过iOS应用,因此目前没有直接提供源码,后续有时间可以考虑进一步提供

什么样的Hybrid框架?

核心宗旨:H5页面基于该框架可以替代80%以上的原生业务页面。

更详细一点:

  • 适用于需要开发大量项目级APP的场景

  • 不是用于完全替代原生开发,而是替代里面的80%原生业务页面(模式是: 原生部分 + H5部分)

  • 框架人员至少需要一名Android原生,一名iOS原生,一名前端架构(如果全栈,可以考虑合一)

  • 部分API(如UI显示类)考虑到了H5的兼容

  • 并没有做到产品级别的优化(需求优先级别较低)

之所以不基于第三方框架而是自己重新实现,是由具体的环境与需求决定的。譬如要求自己必须完全掌握源码,某些功能必须通过特定安全检测等。

另外,本系列不与任何市面上的其他框架进行比较,仅是自己的经验总结。

此框架是否有实践经验?

此框架不是平地起高楼而来的,而是在接近两年的项目实战中慢慢演化出的,内部已经迭代过多个版本

另外,它已经在一个项目型公司全面推广使用了。(N+级别)

这里要说明下:

  • 实际项目中,Hybrid框架仅仅是其中的一部分,还会包括一些原生通用组件,业务模块等

  • 但是本系列仅止步于Hybrid框架(处于诸多因素考虑,包括核心实现以及API实现)

如何应用与自己的项目中?

最后的源码部分仅提供核心实现以及API部分,对于一些简单项目来说,其实也就够用了,
但是如果功能较复杂的,肯定需要进一步封装自己的原生功能。

实际上推荐使用以下人员配置:

  • 一名资深Android原生(负责Android容器)

  • 一名资深iOS原生(负责iOS容器)

  • 一名资深前端(前端部分不要小觑,要配合排查问题的)

  • 总架构(推荐是以上三人中的一人担任,譬如本系列是由前端来统一架构的-但前提是必须懂点原生原理,否则抓瞎)

因为每一个人精力有限,所以除非特别厉害和全能,否则不建议一人担任两职
(譬如像我转入前端后,以前的Android就遗忘的很快,但是如果重点兼顾Android,前端水准肯定无法快速提升)

N+项目时的模式大致如下:

  • 三名框架人员负责核心框架容器部分(框架还需要提供一些通用模块与组件)

  • 各个业务线的APP中可以专门分配不同的原生人员负责打包APP(1对N,协助排查各自可能的业务问题)

  • 每一个APP中可以有若干H5业务开发人员(由不同的复杂度而定,主要业务都是线上的H5形式)

  • 三名对于的框架人员负责处理过滤后的真正框架BUG(由业务负责人过滤)

注意,以上是最小配置。(譬如可以分配更多的框架人员,优化提升等)

最后,以上是实际的经验总结,仅做参考。

框架更新与迭代

实际上不同框架的更新迭代方式都是不一样的,比如本系列中就是基于需求迭代

也就是说遇到问题才修复,优化,累积一段时间后开始考虑下一代的优化提升(迫于投入的窘迫性)

一般来说,整体的交互架构以及API是由对于的负责人规划的,然后安排给对于的容器实现

版本号的化仍然是以下经典形式:

大版本.小版本.修正版

譬如本框架在两年内迭代了个大版本(涉及到底层),
使用起来变化较大就会变动小版本,
平时个别API新增和修复是修正版

这里因人而异,比如有的喜欢将API新增也变为小版本更新

借鉴与不足

本框架中在实现是吸取了不少市面上已有框架的经验,譬如:

  • 钉钉(API设计上,可惜无法看到它底层实现...)

  • phonegap,html5+,apicloud,appcan等都有接触过(但参考的不多)

  • 一些github开源库,譬如marcuswestin/WebViewJavascriptBridge

另外,在文章总结时,参考了一些博文,包括我以前写的文章(会在参考来源中)

源码

github上这个框架的实现

quickhybrid/quickhybrid

【quickhybrid】组件(自定义)API的实现

前言

前文在API规划时就已经有提到过组件API这个概念,本文将会介绍它的原理以及实现

理解组件API这个概念

quick.ui.xxx
quick.page.xxx

在quick hybrid中,API是按模块划分的,如uipage等都是不同模块,而模块的另一个名称则是组件

为什么叫组件?可以这样理解,模块更多的是H5前端的叫法(因为在前端看来不同API分别属于不同的模块下),
而组件则是原生那边加强的理解概念(因为,每一个组件都是可以在项目中单独存在的,譬如项目A中有组件pay,但项目B却不一定集成有)

框架API和组件API

回到最初,quick hybrid的使命就是服务于N个项目,那么会遇到一个问题-N个项目中可能会有非常多的需要以API方式提供的需求,但是考虑到体积以及通用性,并不是所有的都适合直接集成到框架中

此时,就需要对框架内容和项目内容进行区分,于是就有了框架API和组件API的概念(此时可以认为原生中框架文件是单独打成一个静态包给项目引用的,项目无法直接修改)

框架API

  • 直接打包到框架文件中(前端的quick.native.js,原生框架包中的API都会包含)

  • 使用的时候直接quick.xx模块.xx功能即可调用(因为前端会将框架API都默认封装)

  • config配置时无需单独注册(因为默认情况会注册好)

  • 部分框架API会有H5下的实现(如部分系统级API都是有H5下的实现的)

组件API

  • 框架中不会包含,由各自的项目自行开发或集成(如某项目单独集成一个个性化语音组件)

  • 使用的时候必须用quick.callAPi(...)并传入合适参数(因为框架不会集成,需要通过这个万金油方法调用)

  • config配置时必须注册(需要传入组件别名注册,因为框架内部不知道这些新组件的)

  • 所有组件API都只是quick环境下的实现(一般都是一些原生中集成的拓展功能)

项目中如何拓展组件API

项目中默认只会打包框架API,但是框架的功能是有限的(只会集成一些最常用的功能),如果遇到一些个性化的需求(如支付,语音等等),则需要项目拓展组件API,总体步骤如下:

  • 1.原生引入框架,并实现对应的API接口,编写API的功能代码

  • 2.原生在项目配置文件中(不是框架配置文件)声明对应的别名和路径关系

  • 3.H5页面初始化时,config,并传入对应需要注册的组件的别名

  • 4.容器接收到config方法后,去配置文件中根据别名找路径,然后注册对应路径下的API类

  • 5.注册成功后,H5页面中通过callAPi来调用新注册的组件API

原生实现API接口

原生中API的定义如下(以pay组件为示例)

Android中

public class PayApi implements IBridgeImpl {

     public static void payCustom(..., JSONObject param, final Callback callback) {
        // 做对应的支付工作,做完后回调
        ...
        callback.apply(...);
    }
}

iOS中

@implementation PayApi
- (void)registerHandlers {
    [self registerHandlerName:@"payCustom" handler:^(id data, WVJBResponseCallback responseCallback) {
        // 做对应的支付工作,做完后回调
        ...
        responseCallback(...);
    }];
}

声明别名与路径的关系

需要注意的是,Android和iOS中别名请保持一致,一般情况下键值对也可

譬如以示例项目为例,

Android在app模块下的assets/modules.properties

pay = com.quick.quickhybrid.api.PayApi
...

同理iOS中也类似,只不过右侧的路径值可以换为iOS中的,如

pay = PayApi

可以看到,Android和iOS中的别名名称相同,但是路径不一致(因为各种的包机制不一样)

H5中config注册

H5中需要在config注册拓展的组件,需要传入别名(别名有对应的文档说明-一般情况下同类型组件的别名是固定的)

quick.config({
   jsApiList: ['pay']
});

// error代表发生错误
quick.error(...);

// ready中是注册成功
quick.ready(...);

原生容器注册组件API

原生容器接收到config请求后就开始注册组件,如下

// RegisterName: ui,page,pay之类的组件(模块)名
// RegisterNclass: 对应的路径,Android中和iOS中不一致

// RegisterNclass: 如com.quick.quickhybrid.api.PayApi
JSBridge.register(RegisterName, RegisterNclass);
// RegisterNclass: 如PayApi
[self registerHandlersWithClassName:@"RegisterNclass" moduleName:@"RegisterName"];

H5中调用组件API

注册成功后,H5中通过特定方法调用

quick.callApi({
    name: 'testPay',
    mudule: 'pay',
    // 额外参数经常都需要
    data: {...},
    success: function(result) {
        quick.ui.toast(JSON.stringify(result));
    },
    error: function(error) {},
});

结束语

实际情况下,当项目足够多时,拓展组件API是一种非常常见的场景,因此制定规范是很有必要的。

另外,一般情况下,很多相同功能的组件都是可以一起积累,多个项目复用的(比如支付,特定业务组件等等)

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.