乐刻教练端业务是一个 Hybrid APP,通过原生 APP 提供调用原生能力的外壳,内部逻辑全部使用 H5 页面实现。 这种方式在早期产品需求快速迭代,客户端资源紧张的情况下,解决了很大一部分问题。但随着业务需要和使用人数的增加,教练端已经有点力不从心的。在 3 月份,已经对教练端进行过一次优化,主要集中在资源加载、初始数据、转场动画等。接下来,想要结合 HTML5 离线缓存机制,进行一些优化尝试。

HTML5 离线缓存

是什么

HTML5 的新增了很多浏览器本地存储的技术,Application Cache(简称 AppCache)使得基于 web 的应用程序可以离线运行,似乎是为支持 Web App 离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但 AppCache 是对浏览器缓存机制的补充,不是替代。

有什么用

  • 离线浏览: 用户可以在离线状态下浏览网站内容。
  • 更快的速度: 因为数据被存储在本地,所以速度会更快。
  • 减轻服务器的负载: 浏览器只会下载在服务器上发生改变的资源。

怎么用

通过在站点根目录维护一个 manifest 文件,在需要缓存的 html 页面中引入这个文件。manifest 相当于一个配置文件,配置需要缓存的文件等规则,下面会详细列出。

假如在根目录下新建一个名为 manifest.appcache 的文件(该文件名和后缀名可以自定义),配置内容如下:

CACHE MANIFEST
# version 1.0.0  以 # 声明注释,浏览器根据manifest文件变化来检测是否需要重新加载新文件。
# 注释:需要缓存的文件,无论在线与否,均从缓存里读取
/images/cached.png
/css/cached.css
/js/cached.js

# 需要特殊声明的缓存文件,也可以都在这里声明
CACHE:
/css/otherCached.css
/js/otherCached.js

# 注释:不缓存的文件,无论缓存中存在与否,均从新获取(如果与上面的缓存内容重复,依旧会缓存)
NETWORK:
*

# 注释:获取不到资源时的备选路径,如index.html访问失败,则返回404页面
FALLBACK:
index.html 404.html
  1、CACHE MANIFEST — 文件开头第一行必须声明 CACHE MANIFEST 字段标识,然后紧接着声明需要缓存的文件路径,作用是标识出哪些文件需要缓存,可以是相对路径也可以是绝对路径;
  2、# — #号开头的是注释,一般会在第二行写个版本号,用来在缓存的文件更新时,更改manifest的作用,可以是版本号,时间戳或者md5码等等;
  3、CACHE — 我们也可以在CACHE下面声明需要缓存的资源路径;
  4、NETWORK可选,这一部分是要直接读取的文件,可以使用通配符 * ;
  5、FALLBACK可选,指定了一个后备页面,当资源无法访问时,浏览器会使用该页面。

接下来只需要在 html 文件上,引入该配置文件即可

在文档的 <html> 标签中添加 manifest 属性,将 manifest.appcache 文件引入:

<html manifest="manifest.appcache">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"
    />
    <meta name="format-detection" content="telephone=no" />
    <title>乐刻教练</title>
  </head>
  <body></body>
</html>

这样浏览器在读取 HTML 页面时会将 manifest.appcache 中声明的资源下载并进行离线存储,下次请求直接使用离线存储中的资源,HTTP 请求表现为:200 (from disk cache)

注意:

一般情况下,manifest 文件需要在 web 服务器上配置正确的 MIME-type,即 “text/cache-manifest”,才能正常的被浏览器正常请求。

  • webpack 自动化配置

在使用 webpack 自动化构建的项目中,我们可以利用appcache-webpack-plugin这个插件轻松生成 manifest 文件,并将打包的资源路径更新到 manifest 文件中,配置如下

const AppCachePlugin = require("appcache-webpack-plugin");

plugins: [
  new AppCachePlugin({
    cache: ["otherAsset.png"],
    network: ["*"], // 除了声明的缓存资源其他都通过网络访问
    fallback: ["index.html", "error.html"],
    settings: ["prefer-online"],
    exclude: ["file.txt", /.*\.html$/], // Exclude file.txt and all .html files
    output: "/manifest.appcache",
  }),
];

什么效果

浏览器离线存储缓存机制示意图:

  • 用户在初次访问网站时,浏览器读取到 HTML 上的 manifest 属性声明的 mainfest.appcache 文件,根据文件内的配置项下载相应的资源进行离线缓存。在 chrome 中可以看到,被缓存的资源。(浏览器在下载 manifest 中的文件时,会一次性下载所有的文件,一旦因为某些原因某一个资源下载失败,会导致所有的资源更新失败,浏览器继续适应旧的资源。)

  • 已经缓存离线存储后,用户再次访问网站,浏览器(无论在线与否)将会使用已缓存的资源加载页面,然后浏览器会对比新的 manifest 文件,如果文件发生变化,就重新下载文件中的资源进行离线,如果没有变化,就不做改变。

  • 注意:

    • 站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存中访问。
    • 浏览器下载完新的更新资源后并不会立即使用,需要刷新页面后才有效果,解决办法:
var appCache = window.applicationCache;
appCache.onupdateready = function (e) {
  appCache.swapCache();
  window.location.reload();
};
  • 离线情况下,浏览器直接访问离线资源

  • 与传统浏览器缓存的区别

    • 离线缓存是针对整个应用,浏览器缓存是单个文件。
    • 离线缓存断网了还是可以打开页面,浏览器缓存不行。
    • 离线缓存可以通过代码主动通知浏览器更新资源。
  • chrome 浏览器下通过访问 chrome://appcache-internals/ 可以查看缓存在本地的资源文件.

API 介绍

applicationCache 对象

该对象是 window 对象的直接子对象 window.applicationCache

基类:DOMApplicationCache

事件列表

事件 接口 触发条件 后续事件
checking Event 用户代理检查更新或者在第一次尝试下载 manifest 文件的时候,本事件往往是事件队列中第一个被触发的 noupdate, downloading, obsolete, error
noupdate Event 检测出 manifest 文件没有更新
downloading Event 用户代理发现更新并且正在取资源,或者第一次下载 manifest 文件列表中列举的资源 progress, error, cached, updateready
progress ProgressEvent 用户代理正在下载资源 manifest 文件中的需要缓存的资源 progress, error, cached, updateready
cached Event manifest 中列举的资源已经下载完成,并且已经缓存
updateready Event manifest 中列举的文件已经重新下载并更新成功,接下来 js 可以使用 swapCache()方法更新到应用程序中
obsolete Event manifest 的请求出现 404 或者 410 错误,应用程序缓存被取消

另外还有一个 error 事件,触发条件如下:

  • manifest 的请求出现 404 或者 410 错误,更新缓存的请求失败;
  • 无 manifest 文件没有改变,但是页面引用的 manifest 文件没有被正确地下载;
  • 在取 manifest 列举的资源的过程中发生致命的错误;
  • 在更新过程中 manifest 文件发生变化用户代理会尝试立即再次获取文件。

属性

status 属性返回缓存的状态

可选值 匹配常量 描述
0 appCache.UNCACHED 未缓存
1 appCache.IDLE 闲置
2 appCache.CHECKING 检查中
3 appCache.DOWNLOADING 下载中
4 appCache.UPDATEREADY 已更新
5 appCache.OBSOLETE 失效

方法

方法名 描述
update() 发起应用程序缓存下载进程
abort() 取消正在进行的缓存下载
swapcache() 切换成本地最新的缓存环境

支持程度

不过在 H5 标准中, Offline Web applications 部分有如下描述:

This feature is in the process of being removed from the Web platform. (This is a long process that takes many years.) Using any of the offline Web application features at this time is highly discouraged. Use service workers instead. [SW]

因此后续我将在其他文章中继续介绍 service workers.

最佳实践

  1. 首次访问时会加载所有数据,如果缓存的数据很多的话,将拖慢加载速度(只使用一个 manifest 文件,缓存少量必要文件)
  2. 有时候会出现即使 manifest 文件改变后,应用还是不能及时更新(对于业务代码不使用 manifest 缓存,只缓存一些类库文件和不经常改变的文件)

总结

  • 在教练端 APP 上加载速率上没有明显变化,且在断网情况下 APP 有默认缺省图(在 chrome 浏览器上,加载速率提升明显)
  • H5 离线缓存机制 Manifest 将被废除,由 PWA 技术取代
  • 网上有讨论:会出现更新代码后,页面无法更新问题(没有复现)
  • 结合上述现象,决定不再继续深究此方案