Cordova深度定制的H5容器实际应用

2,539 阅读11分钟

前言

上一篇关于Cordova的文章简单介绍了Cordova的主要实现代码和Cordova对web容器和插件相关的处理,本篇将着重介绍Cordova在平台化应用中的深度定制化。目前原生开发者持续不断的在受到前端开发者的冲击,前有Cordova相关的hybrid类APP,后有weex、RN等框架的出现,当然我觉得weex、RN类技术是更应该被提倡的,因为他们兼顾了JS开发,原生渲染的特点。但是Cordova类框架也有它们的优点,微应用完全可以使用任何前端框架来编写,并且可控性强,虽然在渲染层面和weex、RN有一定的差距,但是目前我们使用react.js这种单页面应用配合离线包策略,也一定程度上降低了它们在渲染层的差距,有效快速的解决发版周期之间的矛盾、跨平台开发、实时发布等一些问题,而且有效地保证了发布质量和线上问题的及时修复。

框架对外提供

1.基于Cordova交互插件化,打包提供给H5使用,H5只需要引入一个文件就可以使用native提供的各种原生功能,包括但不限于fetch,map,IO操作,文件操作等。

2.提供“离线包”策略,提升渲染速度和避免不必要的网络请求,进行构建微应用。

3.提供三方授权服务,进行微应用的单点登录授权功能。

web容器优化方案

首先,手机要通过无线网络协议,从基站获得无线链路分配,才能跟网络进行通讯。 无线网络基站、基站控制器这方面,会给手机进行信号的分配,已完成手机连接和交互。 获得无线链路后,会进行网络附着、加密、鉴权,核心网络会检查你是不是可以连接在这个网络上,是否开通套餐,是不是漫游等。核心网络有SGSN和GGSN,在这一步完成无线网络协议和有线以太网的协议转换。 再下一步,核心网络会给你进行APN选择、IP分配、启动计费。 再往下面,才是传统网络的步骤:DNS查询、响应,建立TCP链接,HTTP GET,RTTP RESPONSE 200 OK,HTTP RESPONSE DATA,LAST HTTP RESPONSE DATA,开始UI展现。

可见,通过运营商的网络上网,情况比较复杂,经过的节点太多;运营商的网络信号强度变化频繁,连接状态切换快;网络延迟高、丢包率高;网络建立连接的代价高,传输速度快慢不等(从2G到4G,相差很大)。

1.压缩html, js, css

压缩代码,尤其是js和css资源,压缩后的大小可以降低至原来的1/3以下,有效节约流量。

2.避免30*/40*/50*的http status

200是一个正常的response,我们在浏览器中打开一个网页(后面会讲如何针对移动端进行调试),还会看到304,即命中浏览器缓存。这两种状态是正常的http status。

302、301跳转是常见的跳转,尤其前一种,在我们进行鉴权的时候有时会用到,但这个做法要尽可能地优化,一个页面访问,最多只进行一次302跳转即可,切忌频繁地跳转。

404、500,我们对自己开发的代码比较注意,一般不会发生,但是有的时候,加载第三方库,尤其是第三方库中有自己load组件的操作,这时,404和500错误可能会在你不知不觉的时候发生。

3.资源的版本更新

业务的js和css可能经常会有更新,如果命中浏览器缓存,可能会让一些新的特性不能及时展现,甚至可能导致逻辑上的冲突。因此对于这些js、css的资源引入,最好用版本号或者更新时间来作为后缀,这样的话,后缀不变,命中缓存;后缀改变,浏览器自动更新最新的代码。

4.接口数据缓存

缓存接口数据,在一些数据新旧敏感性不高的场景下很有作用,在非首次加载数据时候优先使用上次请求来的缓存数据,可以让页面更加快速地渲染出来,而不用等待一个新的http请求结束之后再渲染。

5.单页应用

钉钉的审批微应用,使用的就是单页架构。在这种架构下,基本不存在页面跳转的等待时间,只需要执行js逻辑触发界面变化,最多进行一次网络请求,获得服务端数据,其他资源均不需要再次请求。

6.资源离线

再快的网络交互,毕竟也是跨越了数个网络节点,因此一张图片、一个js,优化到了极致,也照样可能需要几百毫秒的时间来获得。因此想要打破这个极限,就要使用资源离线的策略。网页代码里面加载网络资源的需求,就变成了直接加载本地文件,速度自然得到再一次巨大的提升。

7.预加载机制

有时,我们能够通过用户的行为统计,预判出用户下一步可能进行的操作。假设,我们统计出来针对某个微应用,用户首页渲染完成之后,大部分会点击列表中的第一个项目查看详情。那么在首页渲染完成之后,我们就可以先预先加载第一个项目的部分内容,那么针对这部分用户,他们实际点击之后,立即就能看到新的页面中的内容。

在平台内H5页面是怎么加载JS插件的:

起初我们的H5代码都是随着应用一同发布的,这样不同平台的应用自然会将相应的cordova.js打包到应用中,加载相应的cordova_plugin.js,初始化js插件。由于应用逐渐走向平台化发展,那么我们的问题来了,怎样将我们的插件给其他三方应用使用?让其他系统可以方便的集成和使用?基于这样的需求,我们引入了两个自定义的js文件,cordova-dynamic-loading.jspm-cordova.js。cordova-dynamic-loading.js需要在业务端代码前引入,用于检查cordova环境,并且根据客户端系统,初始化不同的插件。而pm-cordova.js是js-bridge接口封装库。

架构图大致如下:

  • 1.H5应用层:目前基于react.js构建的单页面应用,取消页面跳转带来的不必要的网络加载。

  • 2.插件API接口层:平台对外输出插件接口。

  • 3.平台标识:实际上是为浏览器设置的唯一标识,区分不同平台,便于加载不同的js插件使用。

  • 4.动态加载:基于不同平台的userAgent,动态加载相应平台的js插件,其中cordova-dynamic-loading.js和cordova.js为具体实现代码。cordova.js还承担着原生与js交互对插件id,参数的传递,回调的存储等相关工作。

  • 5.平台:不同平台提供相应平台的js插件,供cordova.js加载。

具体这两个文件的实现代码由前端大神编写的,这里就不粘了,作为一个菜鸟iOS程序员,前端目前还尚处于学习中。当然框架内还提供了自定义的pm-cordova-mock.js可选引入,是pm-cordova.js的web端mock版本,本地开发业务进行测试时可以引入这个文件替换pm-cordova.js。除此之外,平台内也同样内置了一套相关代码,三方应用完全可以不必集成插件相关代码也可以与平台进行交互。

定制userAgent

UIWebView没有提供设置UserAgent的接口,但是可以通过cordova框架内CDVUserAgentUtil类,设置userAgent。

@interface CDVUserAgentUtil : NSObject
+ (NSString*)originalUserAgent;
+ (void)acquireLock:(void (^)(NSInteger lockToken))block;
+ (void)releaseLock:(NSInteger*)lockToken;
+ (void)setUserAgent:(NSString*)value lockToken:(NSInteger)lockToken;
@end
+ (void)setUserAgent:(NSString*)value lockToken:(NSInteger)lockToken
{
    //断言
    NSAssert(gCurrentLockToken == lockToken, @"Got token %ld, expected %ld", (long)lockToken, (long)gCurrentLockToken);
    //设置userAgent
    //必须在实例化UIWebView之前设置UserAgent。
    //它是按实例化读取的,所以它不会影响以前创建的视图。
    //除外!当一个PDF被加载时,所有当前活跃的uiwebview重新加载它们
    //来自NSUserDefaults的User-Agent在PDF bah的DidFinishLoad之后的一段时间!
    NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:value, @"UserAgent", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
}

通过设置NSUserDefaults中UserAgent的值来修改,但是这种设置方法有一个限制,需要在UIWebView的loadRequest之前调用才能生效(加载PDF比较特殊)。这是Cordova源码中关于这个问题的描述。

- (NSString*)userAgent
{
    if (_userAgent != nil) {
        return _userAgent;
    }

    //省略部分源代码
    _userAgent = [NSString stringWithFormat:@"%@ %@", _userAgent, @"[AWV/v1.0.0;AD/iOS]"];
    
    return _userAgent;
}

Cordova框架内提供对浏览器userAgent的设置相关代码,平台内对userAgent做了一些定制化的内容,比如自定义了userAgent参数,这里的userAgent实际上就是提供给上面cordova-dynamic-loading.js文件进行解析使用的。实际上通过Cordova源码可以看到还有一些包括对userAgent的释放等其他操作,这里不做具体分析。关于CDVUserAgentUtil可以参考Cordova源码解析(二)- 自定义UserAgent

实际上平台在js部分深度集成做了什么?

  • 1.对三方系统提供cordova-dynamic-loading.js文件,用于检查cordova环境。

  • 2.提供pm-cordova.js文件用与对js桥封装。

  • 3.提供pmCordova.platform.ready(实际上是对外提供的一个插件)函数,由于cordova插件异步进行加载和初始化,用于保证安全调用。

  • 4.提供高级配置,三方系统可按照自己的需求对插件进行配置,减少需要加载的plugin,提高性能。

  • 5.提供pm-cordova-mock.js文件,用于本地开发业务进行测试使用。

提供“离线包”策略,进行构建微应用

为什么要提供离线包,前言也有说明,这里再详细说一下:

  • 1.尽量使容器内H5接近native体验,将HTML,JabaScript,CSS压缩为离线包动态下发。

  • 2.减少网络环境对H5应用的影响。

  • 3.提升打开H5的速度,减少白屏渲染时间。

  • 4.实现微应用的动态更新。

通过应用管理平台,发布应用时会配置相应的版本号等信息,客户端启动后会后台比对版本等信息,检查应用是否需要更新等。关于离线包策略,为了防止用户阻塞,服务器会部署一套完全一样的代码,在离线包没有下载好的情况下,客户端会先去加载服务端的代码,待下次进入该应用的时候加载本地离线包内的代码,这一块的处理和前几天看到支付宝移动端动态化方案实践有些相似。

离线包统一由打包平台一键打包发布,从代码获取到压缩,到生成zip包,再到发布一气呵成,并相应的对版本号进行递增,目前平台客户端支持增量更新和全量更新,增量更新可以更节省流量,更新速度更快。全量更新可以保证版本更稳定,安全性更高。基于此,平台客户端实现了应用的动态发布与更新以及线上bug的修复,不需要再重新打包发布。

那么离线包功能客户端做了什么

  • 1.提供离线包安装路径:

客户端通过不同应用的应用id扩展不同的应用安装路径,用于应用的安装和打开。 实际上在客户端最终会有这样的目录结构:

  • 2.改造Cordova默认加载路径,指定为上图中的目标路径:
- (NSURL*)appUrl
{
    //省略了一部分代码
    NSURL* appURL = nil;
    if ([self.startPage rangeOfString:@"://"].location != NSNotFound) {
    } else if ([self.wwwFolderName rangeOfString:@"://"].location != NSNotFound) {
    } else if([self.wwwFolderName hasSuffix:@".bundle"]){
    } else {
        // CB-3005 strip parameters from start page to check if page exists in resources
        NSURL* startURL = [NSURL URLWithString:self.startPage];
        NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];
    }
    
    return appURL;
}

主要做的工作全部在NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];内实现,这行代码会生成我们预设好的目录文件路径。 Cordova内部实际上默认加载的路径为bundle,需要修改为我们指定的沙盒路径然后去加载沙盒里面相应的微应用进行UI展示。

提供三方授权服务,进行微应用的单点登录

应用有了,也提供了离线包策略,那么三方应用怎么登录平台?基于这个需求我们提供了Oauth2服务,进行三方登录授权。这部分不在web容器范畴内,后续会具体分析。