离线web-ApplicationCache

https://www.html5rocks.com/en/tutorials/appcache/beginner/

http://diveintohtml5.info/offline.html#fallback

介绍

  离线web程序变得非常重要。所有的浏览器都支持长期缓存页面资源,但浏览器会为了腾出缓存空间而清除掉一些之前的缓存。h5中通过ApplicationCache(后面简称appCache)来解决这个问题。appCache可以允许开发者指定某些文件应该保存到本地,用于离线访问。它有三个优点:

  1. 让用户可以离线访问页面
  2. 直接从本地硬盘而不是网络获取资源,速度更快
  3. 当我们的服务器因为某些原因宕机时,用户依然可以访问我们的页面,这使我们的服务更加健壮

清单文件

  这是一个文本文件,用于列出浏览器应该缓存的资源,来离线访问。通过在页面中引用这个清单文件,来启用离线缓存:

<html manifest="example.appcache">
  ...
</html>

  每个需要被离线访问的页面都需要像上面那样引用清单文件,如果html标签没有manifest属性,则浏览器不会缓存这个页面。页面地址后有不同参数,会被AppCache认为是不同的页面,会被分开缓存,所以AppCache最好用于无参数(或固定参数,唯一的)地址的页面

  通过在chrome中访问chrome://appcache-internals/来管理被appCache缓存的页面。

  manifest所指向的清单文件地址必须与当前页面同源。清单文件可以使任意后缀,但被服务器输出时必须指定其媒体类型(content-type)为text/cache-manifest。这个规定在较新版本的chrome、ff、Safari被废弃了,但是对于IE11以及其他老浏览器,仍然有这个规定。

  一个简单的清单文件结构如下:

CACHE MANIFEST
index.html
stylesheet.css
images/logo.png
scripts/main.js
http://cdn.example.com/scripts/main.js

  以上清单文件会缓存4个文件,需要注意的是:

  1. CACHE MANIFEST必须在第一行
  2. 被缓存的文件可以来自其他域(CDN)
  3. 一些浏览器对于缓存的数量有限制,在chrome中,appCache使用共享缓存(与其他离线API共享这片区域)
  4. 如果manifest指向的地址返回了404或410,则缓存会被删除
  5. 如果清单文件或者指定被缓存的资源下载失败,则缓存会更新失败,浏览器会使用旧的缓存。

  浏览器会下载所有清单文件中指定的文件,而不管这些文件有没有在当前页面中有用到。如果有多个页面,则每个页面都需要指向相同的、代表了整个应用程序的清单文件。如果app只有一个html,则保证这个html引用了清单文件即可,而不需要把这个html添加到清单文件中,因为浏览器会认为这个页面属于app的一部分,而自动对这个html进行缓存(但还是推荐显式地加上),利用html文件会被自动缓存的这特点可以实现一个“懒缓存”,H5说明书提供了一个例子:

CACHE MANIFEST
FALLBACK:
/ /offline.html
NETWORK:
*

  对于一个有成千上万个页面的站点,我们不可能也不想去下载整个站点,但可以将其中一部分做成离线的,但怎么决定哪些页面应该被缓存呢?假设这个巨大的站点支持离线,我们访问的每个页面都会被下载和缓存。这就是以上清单文件所能做的事情。假设这个巨大的站点中每个html都引用了上面的清单文件,当访问里面的html时,浏览器会认为这个html是一个离线app的一部分,如果浏览器没有下载过这个清单文件,则会创建一个新的离线缓存,然后下载清单文件中所有的资源并且缓存到刚刚创建的离线缓存中,然后把当前页面也添加到缓存中。这样一来,任何被访问过的页面都会添加进同一个离线缓存中。换个说法就是所有html都引用同一个清单文件,但CACHE那一块不需要列举html文件,因为会自动缓存,但里面的资源需要列举,因为里面的资源不会被自动缓存。

  以上的fallback仅仅只有一行,第一个斜杠是一个url匹配模式,会匹配所有页面。当我们离线访问一个页面时,这个页面在缓存中,则显示缓存的页面;如果不在缓存中,则显示错误信息,并且显示/offline.html这个页面。

  下面来看一个更复杂的例子( # 用于注释一行):

CACHE MANIFEST
# 2010-06-18:v2

# Explicitly cached 'master entries'.
CACHE:
/favicon.ico
index.html
stylesheet.css
images/logo.png
scripts/main.js

# Resources that require the user to be online.
NETWORK:
*

# static.html will be served if main.py is inaccessible
# offline.jpg will be served in place of all images in images/large/
# offline.html will be served in place of all other .html files
FALLBACK:
/main.py /static.html
images/large/ images/offline.jpg

事件流

  当我们访问了一个页面引用了清单文件,会有一系列的事件在window.applicationCache对象上触发(事件监听器要设置在这个对象上)。

  当浏览器发现html标签上有一个manifest属性时,首先会触发一个checking事件,这个事件总会触发,不管浏览器之前有没有遇到过这个清单文件(再次访问当前页面或者访问过其他页面指向了相同的清单文件)

  假如浏览器第一次遇到这个清单文件:

  1. 触发一个downloading事件,然后下载里面的资源
  2. 正在下载的时候,会持续触发progress事件,可以知道文件的下载情况
  3. 当所有文件都下载完,会触发一个cached事件,这就意味着资源已经缓存完成,可以离线了。

  如果浏览器之前就遇到过这个清单文件,则可能有文件已经缓存好了。那问题就是自上次检查完后,这个清单文件本身有没有发生变化?

  • 如果没有发生变化,则触发一个noupdate事件
  • 如果有,则触发一个downloading事件,然后重新下载里面所有的资源。正在下载时,持续触发progress事件。当全部资源已经重新下载完毕了,触发updateready事件,这意味着一个全新版本的资源已经缓存完成,可以离线了。但新的资源并没有被应用上去,需要调用swapCache函数,然后手动刷新页面才会生效。

  如果以上更新流程出现问题,则触发一个error事件并且停止更新。可能出错的地方:

  1. 可以联网时,以上的协商检查返回了404或410(清单文件找不到或者无权访问清单文件)
  2. 重新绑定关联时,新的清单下载失败
  3. 当更新过程中,服务器的清单文件发生变化
  4. 重新下载所有资源时,资源下载失败。

  appCache暴露了一些事件让我们去观察appCache的状态:

function handleCacheEvent(e) {
  //...
}

function handleCacheError(e) {
  alert('Error: Cache failed to update!');
};

// Fired after the first cache of the manifest.
appCache.addEventListener('cached', handleCacheEvent, false);

// Checking for an update. Always the first event fired in the sequence.
appCache.addEventListener('checking', handleCacheEvent, false);

// An update was found. The browser is fetching resources.
appCache.addEventListener('downloading', handleCacheEvent, false);

// The manifest returns 404 or 410, the download failed,
// or the manifest changed while the download was in progress.
appCache.addEventListener('error', handleCacheError, false);

// Fired after the first download of the manifest.
appCache.addEventListener('noupdate', handleCacheEvent, false);

// Fired if the manifest file returns a 404 or 410.
// This results in the application cache being deleted.
appCache.addEventListener('obsolete', handleCacheEvent, false);

// Fired for each resource listed in the manifest as it is being fetched.
appCache.addEventListener('progress', handleCacheEvent, false);

// Fired when the manifest resources have been newly redownloaded.
appCache.addEventListener('updateready', handleCacheEvent, false);
 

调试的艺术

  以上提到清单文件中的资源下载失败时,会触发一个error事件,但我们无法得知具体的错误是什么,这使离线应用调试起来让人沮丧。

  浏览器到底是怎么检查被缓存的清单文件是否发生了修改呢?分成三步:

  1. 通过http协议,类似于其他http资源,浏览器会检查缓存的清单文件是否过期,在http响应头中包含了文件的元信息,但不是强缓存(Expires、Cache-Control)。
  2. 假如缓存的清单文件过期了(通过http头检查),浏览器会问服务器是否有一个新的版本可以下载,是则下载。为了实现这一点浏览器会发起一个http请求,里面包含了缓存的清单文件的修改时间,这个时间也就是上次清单文件下载的时间。如果web服务器认为这个文件没有被修改,则返回304.
  3. 如果web服务器认为这个文件发生修改了,则返回200,浏览器从这个响应中读取新的清单文件内容.

  总结以上三个步骤就是缓存的清单文件是协商缓存。

  假如发布了一个清单文件,10分钟后,往上面加了一行,再次发布。会发生这样的事情:刷新页面,结果什么也没发生。这是因为浏览器始终认为这个缓存没有发生变化,这可能是因为服务器设置了强缓存(Cache-Control)。所以有一件事情绝对要去做:取消(不设置)清单文件的强缓存。

  只要清单文件没有发生变化,则即使里面的资源发生了变化,浏览器也不会发现。如一个css文件已经重新发布了,但运行起来没有任何变化,因为清单文件没有发生变化。解决办法就是:只要离线资源发生了变化,就去修改清单文件,简单随便改里面的一个字符即可。最方便就是通过 # 注释往里面标记一个版本号,修改版本号即可

CACHE MANIFEST
# rev 43
clock.js
clock.css

  html标签可以以相对路径引用清单文件,清单文件内以相对于清单文件的路径来引用其他资源。

缓存更新

 可以理解为清单文件也会被缓存,是协商缓存。表现为

  1. 第一次访问页面时,下载并且把清单内容缓存到本地,浏览器再将这个清单文件的地址与页面的地址进行关联(绑定关联)
  2. 再次访问时,浏览器根据页面地址找到对应关联的清单地址(获取关联),然后检查这个地址是否与当前页面的清单地址一致(校验关联),不一致则重新下载新的清单(重新绑定关联)。最后根据这个地址检查本地的清单内容是否发生变化(协商检查),是则进入更新流程(重新下载所有资源),无法联网则使用缓存的清单文件。

  仅当清单文件内容发生变化时,才会触发浏览器去更新缓存。假如清单文件无变化,而仅仅修改了图片或者修改了一个JS函数,这些变化不会被缓存(不会被浏览器发现)。

  在一次更新中,清单文件会被检查(协商检查清单文件有没有发生变化)两次,开始的时候一次和所有缓存文件都更新完成后再检查一次。如果清单文件在更新的时候被修改,即两次检查的结果不一致,则更新失败。

  就算缓存被更新了,浏览器不会使用这些缓存,直到页面被刷新,因为缓存的更新发生在页面重新加载之后(加载的是当前版本的缓存,而不是新版版的缓存)。

  当清单文件或者清单文件内指定的资源下载失败时,整个更新过程就会失败。浏览器会继续使用旧的缓存

  缓存会一直有效,直到:

  1. 用户手动清空浏览器缓存
  2. 清单文件被修改。更新清单文件中的缓存列表,并不意味着浏览器会对缓存进行更新,清单文件本身必须要修改

缓存的状态

  通过window.applicationCache可以以编程方式来访问浏览器中的appCache,对象的status属性用于检查当前缓存的状态

var appCache = window.applicationCache;

switch (appCache.status) {
  case appCache.UNCACHED: // UNCACHED == 0
    return 'UNCACHED';
    break;
  case appCache.IDLE: // IDLE == 1
    return 'IDLE';
    break;
  case appCache.CHECKING: // CHECKING == 2
    return 'CHECKING';
    break;
  case appCache.DOWNLOADING: // DOWNLOADING == 3
    return 'DOWNLOADING';
    break;
  case appCache.UPDATEREADY:  // UPDATEREADY == 4
    return 'UPDATEREADY';
    break;
  case appCache.OBSOLETE: // OBSOLETE == 5
    return 'OBSOLETE';
    break;
  default:
    return 'UKNOWN CACHE STATUS';
    break;
};

  编程方式来更新缓存首先要调用update方法,这会尝试去更新缓存(但需要清单文件已经发生变化),当status进行UPDATEREADY时,可以使用swapCache函数来将新的缓存替换旧的缓存:

var appCache = window.applicationCache;

appCache.update(); // Attempt to update the user's cache.

...

if (appCache.status == window.applicationCache.UPDATEREADY) {
  appCache.swapCache();  // The fetch was successful, swap in the new cache.
}

  以上缓存替换完成后,需要刷新页面,新替换进去的缓存才会生效,可以这么做:

  window.addEventListener('load', function(e) {

  window.applicationCache.addEventListener('updateready', function(e) {
    if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
      // Browser downloaded a new app cache.
      if (confirm('A new version of this site is available. Load it?')) {
        window.location.reload();
      }
    } else {
      // Manifest didn't changed. Nothing new to server.
    }
  }, false);

}, false);

清单文件的结构

  清单文件可以分成三个部分(书写顺序无影响,一个文件中可以同一个部分出现多次):CACHE、NETWORK和FALLBACK。

CACHE:

  这是默认的部分,里面列出的文件(或者直接在CACHE MANIFEST底下的文件)会被下载后第一时间进行缓存

NETWORK:

  里面列出的文件如果不在缓存中,则从网络上下载;否则如果某些资源地址不在这里,就算网络通畅,也不会去下载文件。所以一般指定一个 * 即可。

FALLBACK:

  这是可选的,里面第一个url指定为资源,第二个url为当资源无法访问时就访问这个url。两个url必须与当前页面同源。这里指定资源url时,可以指定一个前缀来批量地指定多个url的访问失败时的反馈页面。

  以下例子定义了一个全资源的错误页面(offline.html),当无网络时,用户访问站点根目录或者访问其他所有资源时,则这个页面就会显示出来

CACHE MANIFEST
# 2010-06-18:v3

# Explicitly cached entries
index.html
css/style.css

# offline.html will be displayed if the user is offline
FALLBACK:
/ /offline.html

# All other resources (e.g. sites) require the user to be online. 
NETWORK:
*

# Additional resources to cache
CACHE:
images/logo1.png
images/logo2.png
images/logo3.png

appCache是一个逗比(douchebag)

http://alistapart.com/article/application-cache-is-a-douchebag

  里面形容appCache就是一颗洋葱,当你一层一层剥开,你会流泪的。

1.文件总是来自于appCache,即使网络可用

  当缓存更新完成,updateready事件会被触发,但这时候我们不能刷新页面,因为用户可能正在进行交互。这不是什么大问题,因为旧的缓存也可能足够好用了,如果实在需要新的缓存,则弹框提醒用户是否要刷新界面来启用新的缓存,是则刷新页面。

2.appCache缓存仅当清单文件本身发生变化时才更新

  http协议是有缓存的,我们可以为每个文件定义缓存的方式(总是不缓存、协商缓存和强缓存)。假如清单文件中有50个html,每次我们访问里面的页面,浏览器就要创建50个请求来检查他们是否需要更新。

  浏览器仅当清单文件本身发生变化了,才去检查内部的资源是否需要更新,只要清单文件的一个字节发生变化,就会触发检查更新。

  这对一直不会变化的静态资源来说非常好。当资源的路径发生变化,就意味着清单文件的内容发生变化。最简单的改变清单文件的方式是修改里面的注释(如# v1 改成 # v2),这可以通过构建工具来完成,类似于为每个文件都创建一个唯一标识符ETAG,然后通过注释写到清单文件中,这样一来,文件内容发生变化,则清单文件发生变化

  然而,清单文件被更新,不意味着里面的资源会更新。

3.appCache属于额外的缓存,不能替换http缓存

  大部分appCache被更新时,浏览器会发出http请求。这符合一般的缓存流程:假如资源已经被强缓存下来了,则appCache更新,浏览器发出请求,发现这个强缓存没有到期,则不会去访问服务器了,更新就这样结束了。这是可以的,因为当清单文件发生变化时,我们降低了向服务器请求的次数。

  所以所有资源都要正确地设置好http缓存头,这比清单文件更加重要

4.永远不要去强缓存清单文件

  强缓存一个清单文件的话,页面每次获取到的清单文件总是旧的,根据清单文件获取的缓存也是旧的。

5.一个缓存页上不会加载没被缓存的资源

  当缓存了index.html,却没有缓存cat.jpg,则这个图片不会显示在index.html上,即使网络可用。解决这个问题可以将NETWORK版块设置为一个 * 。这意味着浏览器在展示一个缓存页时,没有被缓存的资源就从网络中获取。

配合iframe使用

案例1

  在线访问一个普通页面A(没有引用清单文件),内嵌了一个隐藏的iframe,指向另一个页面B(引用了清单文件),可以离线访问B

案例2

  在案例1的基础上,B中的清单文件配置了Fallback,然后离线访问A,B中的fallback生效了。这是因为B在上次已经被缓存,然后离线访问A,A中通过一个隐藏的iframe访问了B,就相当于离线访问了B,B的fallback就生效了。

fallback:

CACHE MANIFEST
FALLBACK:
/ fallback.html
/assets/imgs/avatars/ assets/imgs/avatars/default-v1.png

  以上当请求失败,就显示fallback.html。除非前缀为/assert/ims/avatars/,就返回一个默认的图片。

使用localstorage来动态离线管理

  我们不可能缓存所有东西,因为内容太多了。我们希望用户自己去选择可离线访问的资源,保存到localstorage中。实现如下:

  1.首先以地址映射页面内容,地址映射标题(标题都保存到index指向的json字符串中)

// Get the page content
var pageHtml = document.body.innerHTML;
var urlPath = location.pathName;
// Save it in local storage
localStorage.setItem( urlPath, pageHtml );
// Get our index
var index = JSON.parse( localStorage.getItem( 'index' ) );
// Set the title and save it
index[ urlPath ] = document.title;
localStorage.setItem( 'index', JSON.stringify( index ) );

  2.通过清单指定一个fallback页面,页面中读取localstorage:

var pageHtml = localStorage.getItem( urlPath );
if ( !pageHtml ) {
 document.body.innerHTML = 'Page not available';
}else {
 document.body.innerHTML = localStorage.getItem( urlPath );
 document.title = localStorage.getItem( 'index' )[ urlPath ];
}

6.再见,条件下载

  对于响应式下载(媒体查询加载不同的图片,如source标签),浏览器会根据清单文件的内容来下载,而不管其他地方是否有媒体查询,这会导致浏览器下载多套不同的资源。同样的道理也会作用于字体

7.我们不知道什么时候会显示fallback页面

  根据说明(spec),当源请求被重定向到其他域、遇到4xx或5xx状态码、或网络错误时会显示fallback页面(显示的方式类似于服务器端跳转,即浏览器地址不变)。但是当用户没有网络的时候,fallback也没办法显示了。

8.重定向到其他域也会被认为是个错误

  如果页面访问一个url,服务器对这个url的请求进行重定向,这会马上显示一个对应url的fallback页面,因为appCache不允许。

  这个规则是好的,因为假设我们连了wifi上网,访问网站则重定向到了这个wifi的付费页面,相对于这点,显示fallback页面是好的。把这些需要重定向的url添加到NETWORK是无效的,但使用JS或者meta-redirect是可以的

 其他阅读

posted @ 2018-01-23 14:56  HelloHello233  阅读(645)  评论(0编辑  收藏  举报