注:本篇研究重点不在于某个离线方案的具体使用,而在于对方案的优缺点分析、探究和选型,以及一些我个人的看法。
最早接触离线包的概念要追溯到16年初,项目迎来大改版,其中重点项目之一就是离线包方案的制定与实施。这几年关于这方面的工作比较多,尤其在JS和Native
的混合开发上,积累了一部分经验。离线包顾名思义就是将H5/CSS/JS和资源文件
打包提前下发到App中,这样App在加载网页的时候实际上加载的是本地的文件,减少网络请求来提高网页的渲染速度,并实现动态更新效果。
就目前情况来看,离线包的方案也是层出不穷的,在文末参考部分,总结了一些文章入口,如果有这方面的需求的也可以进行参考。
回归主题,本篇将从三个角度进行探讨分析,目的在于探索一个最佳方案来实施我们的离线包功能,也希望对你能有所帮助。
image
对于离线包平台的构建是十分有必要的,建议如果实施离线包方案,那就同步实施离线包平台构建,负责离线包的增删改功能,当然这就不单单是客户端的事情了。
1.将所有的h5文件都放入一个文件夹中。
2.将这个文件夹以相对路径的方式倒入到工程代码中。
3.获取本地的文件路径。
这个方案就是将部署在服务器上面的前端代码直接解压到本地沙盒。加载js的时候直接加载本地沙盒中的html进行离线加载。将每个前端的模块都定义为一个应用,打上id下发给客户端,当用户点击对应模块的时候根据id去沙盒查找对应的离线资源进行加载实现秒开。
file:///.../index.html
。这是在使用file协议
访问html,有些html样式并不支持file协议,在样式和功能上会有缺失,还会有一些api上的差异,前端开发好的代码可能部署到沙盒里导致有些资源无法使用,产生一些适配问题。file协议&http协议:file协议主要用于访问本地计算机中的文件,好比通过资源管理器打开文件一样,针对本地的,即file协议是访问你本机的文件资源。http协议访问本地html是在本地起了一台http服务器,然后你访问自己电脑上的本地服务器,http服务器再去访问你本机的文件资源。
浏览器对两种协议的处理有时会不同,譬如某些网页中直接调用file协议来打开图片,这样的功能会被浏览器的安全设置阻挡,因为默认上,html是运行于客户端的超文本语言,从安全性上来讲,服务端不能对客户端进行本地操作。即使有一些象
cookie
这类的本地操作,也是需要进行安全级别设置的。倘若你需要载入外部cdn的资源,比如livereload、browserSync
等工具的使用,由于浏览器的同源策略,从本地文件系统载入外部文件将会失败,会抛出安全性异常。
总的来说,这个方案会对前端产生严重的入侵,限制了前端只能通过相对路径对js,css,image
等资源的加载,还有file协议的跨域问题
导致无法引入外部cdn,这样会限制前端开发,虽然用起来最简单,但这并不是一个好的方案。
既然直接加载本地html不是最好方案,那我们将加载html后的所以资源请求全部拦截,映射为本地资源重新组装request进行http请求并加载,是否可行呢?当然可行了,但是往下看:
在UIWebView
上,protocol拦截确实是我们的首选方案,创建个子类,在子类里面实现protocol的代理方法即可实现对所有请求的拦截,当然也包括html里面对资源加载
的请求。
那么在WKWebView
上,这个方案是行不通的,关于这方面的解释已经很多了,WKWebView在独立于app进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在WKWebView上直接使用 NSURLProtocol 无法拦截请求。当然通过私有api可以解决问题,但依然存在缺陷,post请求body数据被清空。由于WKWebView在独立进程里执行网络请求。一旦注册http(s) scheme后,网络请求将从Network Process发送到App Process,这样 NSURLProtocol 才能拦截网络请求。在webkit2
的设计里使用MessageQueue进行进程之间的通信,Network Process会将请求encode成一个Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode的时候HTTPBody和HTTPBodyStream这两个字段被丢弃掉了。
此段引自:《WKWebView》
关于丢Body的解决方案可以戳上文详细查看。如果使用Get请求拦截离线资源可以通过设置自定义的customScheme:进行拦截,例如customScheme://.../.css
,拦截后加载本地离线资源。但是使用私有api又会面临另外一个风险:被拒
。
说一点题外话,目前据我所了解到百度App安卓就是采用的请求拦截方式,但是,是安卓,看下图:
image
图片来源《百度APP-Android H5首屏优化实践》
通过上图可以分析第11、12步
,WebView对html解析的时候可以发现资源请求并拦截,返回对应的缓存资源并渲染。实际上这个方案在iOS上是行不通
的,安卓可以使用自家浏览器,可以魔改浏览器,比如支付宝的UC,百度的T7等。iOS应用内是不允许使用魔改浏览器的,很遗憾,也就是说苹果爸爸开放了什么,我们才能使用什么。
总结来说,这个方案并不会对前端产生入侵,但对于body的拦截和对私有api的使用,产生上架被拒风险,另外protocol是一个全局的拦截,可能也并不是我们想要的,所以这个方案仍然不推荐。
WKURLSchemeHandler是iOS11就推出的,用于处理自定义请求的方案,不过并不能处理Http、Https等常规scheme。
WKWebViewConfiguration开放了setURLSchemeHandler:forURLScheme:
函数,需要指定一个自定义的scheme和一个用来处理WKURLSchemeHandler回调
的自定义对象。
根据注释来看,如果注册了一个无效的scheme或者使用WebKit内部已经处理的scheme将会引发异常。我们最好使用WKWebView的handlesURLScheme:
类方法来检查给定scheme的可用性,以免带来一些未知问题。
WKURLSchemeHandler提供了两个回调函数由上面自定义的对象来处理:
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask;
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id )urlSchemeTask;
通过urlSchemeTask
的request
对象可以拿到请求对应的url
,如果是我们自定义的scheme就去拦截它,并加载本地资源。
实际上这个方案解决了资源拦截的问题,但是它依然有很多短板:
所以这样来看,WKURLSchemeHandler的拦截方案也并不是特别友好。真的是感觉到了苹果爸爸对于离线包的恶意???
根据支付宝的文章《支付宝移动端动态化方案实践》对离线包的描述:
当 H5 容器发出资源请求时,其访问本地资源或线上资源所使用的 URL 是一致的。H5 容器会先截获该请求,截获请求后,发生如下情况:
1.如果本地有资源可以满足该请求的话,H5 容器会使用本地资源。
2.如果没有可以满足请求的本地资源,H5 容器会使用线上资源。 因此,无论资源是在本地或者是线上,WebView 都是无感知的。
可以看出,支付宝并不是采用的上述三种方案,因为上述方案除了protocol拦截以外,都无法做到让WebView无感知
,我相信支付宝也不可能采用protocol拦截的方案处理离线包,原因我上面说的很清楚。那么不出意外,支付宝应该采用的是起本地服务器方案
。这样做能够最大程度的让前端对于离线包“无感”,也能忽略掉拦截api的平台差异导致的框架实现差异。
优点:同网络服务器加载的样式和功能完全一致,不入侵前端,前端并不用关心当前页面是离线还是非离线,做到最大无感知。
缺点:
这个方案的实施可以参考:《基于 LocalWebServer 实现 WKWebView 离线资源加载》的处理,但是文末也提到了几个问题:
这些问题对于我来说也是未知的。如果大家对搭建本地服务有成熟的方案,希望能留言探讨,如果能分享出来感激不尽。
题外话:从上面提到的支付宝文章来看,还有一段我们可以分析一下:
为了解决离线包不可用的场景,fallback 技术应运而生。每个离线包发布的时候,都会同步在 CDN 发布一个对应的线上版本,目录结构和离线包结构一致。fallback 地址会随离线包信息下发到本地。在离线包没有下载好的场景下,客户端会拦截页面请求,转向对应的 CDN 地址, 实现在线页面和离线页面随时切换。
这个不可用场景
应该就是离线包不可用,未更新,资源有损坏,md5不匹配或者验签不通过等等,关于这种情况,可以在bang's
的博客里面找到一些总结:
第三种方案应该就是支付宝的fallback 技术
,可以解决上述问题。当然前两种方案也不是不可取,还是要看需求和场景。
关于这四种方案,都有优劣,关于选型,还是要参照自己的需求,就应用来说,都是可以的。当然对于一个优秀的Hybird
框架,这些还是远远不够的,不管是从支付宝的方案还是手百的方案来看,需要做的优化还有很多,不管是手Q的动态直出
,还是支付宝的Nebula
,都还有很多东西需要我们探讨学习。不知道大家有没有发现,不只是手百,包括头条,腾讯新闻,在页面没有全部push出之前就已经渲染完毕了,说明都存在对h5页面进行预加载
的处理,当然这一块还要视具体需求和人力来定了。关于离线包的处理,这是我目前能想到的所有方案,对于他们的优劣也有总结,如果你有什么建议或者更好的方案,欢迎留言。
本篇为原创,转载注明出处。
框架demo已开源:《SHRMJavaScriptBridge》(暂时未引入离线包)
《支付宝移动端动态化方案实践》
《WKWebView》
《基于 LocalWebServer 实现 WKWebView 离线资源加载》
《移动 H5 首屏秒开优化方案探讨》
《iOS app秒开H5优化总结》
《iOS 11:WKWebView内容过滤规则详解》