Web离线技术

注:本篇研究重点不在于某个离线方案的具体使用,而在于对方案的优缺点分析、探究和选型,以及一些我个人的看法。

前言

最早接触离线包的概念要追溯到16年初,项目迎来大改版,其中重点项目之一就是离线包方案的制定与实施。这几年关于这方面的工作比较多,尤其在JS和Native的混合开发上,积累了一部分经验。离线包顾名思义就是将H5/CSS/JS和资源文件打包提前下发到App中,这样App在加载网页的时候实际上加载的是本地的文件,减少网络请求来提高网页的渲染速度,并实现动态更新效果。

就目前情况来看,离线包的方案也是层出不穷的,在文末参考部分,总结了一些文章入口,如果有这方面的需求的也可以进行参考。

回归主题,本篇将从三个角度进行探讨分析,目的在于探索一个最佳方案来实施我们的离线包功能,也希望对你能有所帮助。

目录

  • 1. 构建离线包管理平台

  • 2. 离线包加载方案探究

  • 3. fallback 技术

  • 4. 总结

一、构建离线包管理平台

Web离线技术_第1张图片

image

  • 1.后端使用离线包管理平台把同一个业务模块相关的页面和资源打包成一个zip压缩包文件,同时对文件加密/签名。
  • 2.客户端根据本地数据库表,和服务端进行版本校验,做增量、全量更新操作,做解压/解密/校验等工作。
  • 3.根据数据库表,打开某个业务时转接到打开离线包的入口页面,即我们常见的index.html。
  • 4.拦截网络请求,对于离线包已经有的文件,直接读取离线包数据返回,否则走http协议缓存逻辑。

对于离线包平台的构建是十分有必要的,建议如果实施离线包方案,那就同步实施离线包平台构建,负责离线包的增删改功能,当然这就不单单是客户端的事情了。

本篇着重讨论第四步,拦截网络请求。

二、离线包加载方案探究

方案一:获取沙盒html路径直接加载index.html

  • 1.将所有的h5文件都放入一个文件夹中。

  • 2.将这个文件夹以相对路径的方式倒入到工程代码中。

  • 3.获取本地的文件路径。

这个方案就是将部署在服务器上面的前端代码直接解压到本地沙盒。加载js的时候直接加载本地沙盒中的html进行离线加载。将每个前端的模块都定义为一个应用,打上id下发给客户端,当用户点击对应模块的时候根据id去沙盒查找对应的离线资源进行加载实现秒开。

  • 优点:简单。
  • 缺点:
      1. 实际上我们在访问本地html的时候可以看到实际路径为file:///.../index.html。这是在使用file协议访问html,有些html样式并不支持file协议,在样式和功能上会有缺失,还会有一些api上的差异,前端开发好的代码可能部署到沙盒里导致有些资源无法使用,产生一些适配问题。
      1. 访问本地资源可能会导致资源路径泄漏产生安全问题。
      1. 还会有一些浏览器的安全设置无法通过。
      1. 无法实现跨域资源请求,会让前端开发人员无法访问外部cdn。

file协议&http协议:file协议主要用于访问本地计算机中的文件,好比通过资源管理器打开文件一样,针对本地的,即file协议是访问你本机的文件资源。http协议访问本地html是在本地起了一台http服务器,然后你访问自己电脑上的本地服务器,http服务器再去访问你本机的文件资源。

浏览器对两种协议的处理有时会不同,譬如某些网页中直接调用file协议来打开图片,这样的功能会被浏览器的安全设置阻挡,因为默认上,html是运行于客户端的超文本语言,从安全性上来讲,服务端不能对客户端进行本地操作。即使有一些象cookie这类的本地操作,也是需要进行安全级别设置的。倘若你需要载入外部cdn的资源,比如livereload、browserSync等工具的使用,由于浏览器的同源策略,从本地文件系统载入外部文件将会失败,会抛出安全性异常。

总的来说,这个方案会对前端产生严重的入侵,限制了前端只能通过相对路径对js,css,image等资源的加载,还有file协议的跨域问题导致无法引入外部cdn,这样会限制前端开发,虽然用起来最简单,但这并不是一个好的方案。

方案二:使用protocol拦截

既然直接加载本地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安卓就是采用的请求拦截方式,但是,是安卓,看下图:

 

Web离线技术_第2张图片

image

图片来源《百度APP-Android H5首屏优化实践》

通过上图可以分析第11、12步,WebView对html解析的时候可以发现资源请求并拦截,返回对应的缓存资源并渲染。实际上这个方案在iOS上是行不通的,安卓可以使用自家浏览器,可以魔改浏览器,比如支付宝的UC,百度的T7等。iOS应用内是不允许使用魔改浏览器的,很遗憾,也就是说苹果爸爸开放了什么,我们才能使用什么。

总结来说,这个方案并不会对前端产生入侵,但对于body的拦截和对私有api的使用,产生上架被拒风险,另外protocol是一个全局的拦截,可能也并不是我们想要的,所以这个方案仍然不推荐。

方案三:WKURLSchemeHandler

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;

通过urlSchemeTaskrequest对象可以拿到请求对应的url,如果是我们自定义的scheme就去拦截它,并加载本地资源。

实际上这个方案解决了资源拦截的问题,但是它依然有很多短板:

    1. 因为使用的自定义scheme,并不是http协议,所以它依然无法解决跨域问题。
    1. 由于自定义了scheme,对于前端来说,需要额外将scheme设置为我们自定义的,这又会给前端带来大量的改造,所以对前端还是产生了入侵。
    1. 上面提到在安卓完全不需要像iOS这样大费周章的绕弯路,所以安卓可能就不需要这个自定义的scheme,这样又会导致面临着与安卓差异化严重问题。

所以这样来看,WKURLSchemeHandler的拦截方案也并不是特别友好。真的是感觉到了苹果爸爸对于离线包的恶意???

方案四:起本地服务器加载离线资源

根据支付宝的文章《支付宝移动端动态化方案实践》对离线包的描述:

当 H5 容器发出资源请求时,其访问本地资源或线上资源所使用的 URL 是一致的。H5 容器会先截获该请求,截获请求后,发生如下情况:

1.如果本地有资源可以满足该请求的话,H5 容器会使用本地资源。

2.如果没有可以满足请求的本地资源,H5 容器会使用线上资源。 因此,无论资源是在本地或者是线上,WebView 都是无感知的。

可以看出,支付宝并不是采用的上述三种方案,因为上述方案除了protocol拦截以外,都无法做到让WebView无感知,我相信支付宝也不可能采用protocol拦截的方案处理离线包,原因我上面说的很清楚。那么不出意外,支付宝应该采用的是起本地服务器方案。这样做能够最大程度的让前端对于离线包“无感”,也能忽略掉拦截api的平台差异导致的框架实现差异。

  • 优点:同网络服务器加载的样式和功能完全一致,不入侵前端,前端并不用关心当前页面是离线还是非离线,做到最大无感知。

  • 缺点:

      1. 需要额外搭建本地服务器,html文件的路径需要做处理。
      1. 对于本地服务器的搭建存在成本问题,本地服务器的管理问题,例如服务器的打开、关闭时机等等。
      1. 对于本地服务器会不会带来其他问题对于我来说也是未知的,并不是所有团队都能像支付宝一样搭建一个自己的服务器来处理。

这个方案的实施可以参考:《基于 LocalWebServer 实现 WKWebView 离线资源加载》的处理,但是文末也提到了几个问题:

  • 资源访问权限安全问题。
  • APP前后台切换时,服务重启性能耗时问题。
  • 服务运行时,电量及CPU占有率问题。
  • 多线程及磁盘IO问题。

这些问题对于我来说也是未知的。如果大家对搭建本地服务有成熟的方案,希望能留言探讨,如果能分享出来感激不尽。

三、fallback 技术

题外话:从上面提到的支付宝文章来看,还有一段我们可以分析一下:

为了解决离线包不可用的场景,fallback 技术应运而生。每个离线包发布的时候,都会同步在 CDN 发布一个对应的线上版本,目录结构和离线包结构一致。fallback 地址会随离线包信息下发到本地。在离线包没有下载好的场景下,客户端会拦截页面请求,转向对应的 CDN 地址, 实现在线页面和离线页面随时切换。

这个不可用场景应该就是离线包不可用,未更新,资源有损坏,md5不匹配或者验签不通过等等,关于这种情况,可以在bang's的博客里面找到一些总结:

    1. 如果本地离线包没有或不是最新,就同步阻塞等待下载最新离线包。这种方案用户体验最差,因为离线包体积相对较大。
    1. 如果本地有旧包,用户本次就直接使用旧包,如果没有再同步阻塞等待,这种会导致更新不及时,无法确保用户使用最新版本。(据我所知微信小程序为此方案)
    1. 对离线包做一个线上版本,离线包里的文件在服务端有一一对应的访问地址,在本地没有离线包时,直接访问对应的线上地址,跟传统打开一个在线页面一样,这种体验相对等待下载整个离线包较好,也能保证用户访问到最新。

第三种方案应该就是支付宝的fallback 技术,可以解决上述问题。当然前两种方案也不是不可取,还是要看需求和场景。

四、总结

关于这四种方案,都有优劣,关于选型,还是要参照自己的需求,就应用来说,都是可以的。当然对于一个优秀的Hybird框架,这些还是远远不够的,不管是从支付宝的方案还是手百的方案来看,需要做的优化还有很多,不管是手Q的动态直出,还是支付宝的Nebula,都还有很多东西需要我们探讨学习。不知道大家有没有发现,不只是手百,包括头条,腾讯新闻,在页面没有全部push出之前就已经渲染完毕了,说明都存在对h5页面进行预加载的处理,当然这一块还要视具体需求和人力来定了。关于离线包的处理,这是我目前能想到的所有方案,对于他们的优劣也有总结,如果你有什么建议或者更好的方案,欢迎留言。

本篇为原创,转载注明出处。

框架demo已开源:《SHRMJavaScriptBridge》(暂时未引入离线包)

相关参考文章推荐,希望对你有用

《支付宝移动端动态化方案实践》

《WKWebView》

《基于 LocalWebServer 实现 WKWebView 离线资源加载》

《移动 H5 首屏秒开优化方案探讨》

《iOS app秒开H5优化总结》

《iOS 11:WKWebView内容过滤规则详解》

你可能感兴趣的:(html+前端框架的选择)