Offline Web applications

Agenda

  • Quick Start: What & How?
    • 什么是 Offline Web applications?
    • 快速 Demo: 把网页转成离线应用的三个步骤
    • 浏览器支持情况和调试,及缓存空间大小限制
  • manifest 语法,编写和更新
  • 实用缓存技术
    • 访问未缓存内容
    • 添加后备内容
    • 网络连接检测的几种方法
    • 缓存过程的事件流
    • 使用 JavaScript API: applicationCache
    • 特性检测
  • 相关资源

什么是 Offline Web application?

HTML5 提供的一种应用缓存机制可以让基于 Web 的应用离线运行。开发者可以指定让浏览器缓存哪些资源以便在离线状态下可用。这样即使在离线状态下,用户打开(或刷新)应用页面时,已经缓存过的应用程序还可以继续加载和运行。

使用应用程序缓存的三个好处:

  • 离线浏览:用户即使在离线状态下也可以访问网站
  • 速度:缓存的资源是保存在本地的,因此加载起来更快速
  • 降低服务器负载:浏览器只从服务器下载变更了的内容

快速 Demo

包含一个 html,一个 css和一个 js 文件可以显示当前时间的一个单页面应用,我们对它应用 applicationCache 之后将可以离线运行。

  • clock.html
  • clock.css
  • clock.js
  • Description

clock.html

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Clock</title>
<script src="js/clock.js"></script>
<link rel="stylesheet" href="css/clock.css">
</head>

<body>
    <p>当前时间是:<output id="clock"></output></p>
</body>
</html>      

clock.css

/* clock.css */
output { 
    font: 2em sans-serif; 
}      

clock.js

/* clock.js */
setInterval(function () {
    document.getElementById('clock').value = new Date().toLocaleTimeString();
}, 1000);      

创建离线应用的三个步骤

  • 创建 manifest 文件
  • 修改网页,引用描述文件
  • 配置 Web 服务器

创建 manifest 文件

manifest 文件是一个文本文件,告诉浏览器需要缓存的内容。

新建一个 manifest 文件,命名为clock.manifest

CACHE MANIFEST

clock.html
css/clock.css
js/clock.js      

使用 manifest 文件

<html> 元素增加 manifest 属性

<html manifest="clock.manifest">      

Web 服务器配置

manifest 文件的 mimetype 类型是 text/cache-manifest, 因此 Web Server 的 mimetype 配置必须包含这个配置。例如 Apachemime.types 文件:

Apache mimetypes

考虑关闭对 manifest 文件的传统缓存机制

Let's Go!!!

浏览器支持情况

  • IE 10+
  • FF 3.5+
  • Chrome 4.0+
  • Safari 4.0+
  • Opera 10.6+
  • iOS 3.2+
  • Android 2.1+
  • BB 7.0+
  • Firefox Android 15.0+
  • Can I use...

Chrome 查看缓存数据

使用开发者工具

View -> Developer -> Developer Tools -> Resources -> Application Cache

Chrome查看应用缓存数据

在 Chrome 中查看缓存数据

使用内部页面

在地址栏中输入:chrome://appcache-internals

Chrome查看应用缓存数据

在 Firefox 中查看缓存数据

使用内部页面

在 Firefox 的地址栏中输入 about:cache

Firefox查看应用缓存数据

在移动版的 Safari 上查看缓存数据

[Demo]如何使用 Web Inspect(检查器) 调试移动版 Safari?

  • iOS 6+ (设备或模拟器)
  • Mac OS X(10.7.4+
  • Safari 6.0+
在移动版的 Safari 上查看缓存数据

缓存空间大小

各个浏览器情况不太一样,桌面版和移动版也不太一样;基本上都是针对每个站点有限制(per site)。

  • Chrome:大概30兆(测试版本:25.0 beta)
  • Firefox:超过50兆会提示用户,最大512兆。(测试版本:18.0.1)
  • 移动版 Safari:超过25MB会提示用户,最多50MB。(测试版本:iOS6)

Chrome 缓存空间限制的测试

Developer Tool 的 console 可以查看缓存过程

Chrome 缓存过程

Firefox 在缓存超过50MB时会提醒用户

Firefox 提示用户缓存超过50MB

Mobile Safari 缓存提示界面

也就是说在手机上我们都可以至少缓存几十兆,通过缓存文件提高性能(把常用的大文件下载并长期保存下来)的想法逐渐变得可行。

Safari 提示用户

Manifest 语法,编写和更新

编写 manifest

manifest 在服务器上必须使用 text/cache-manifest MIME type; 同样所有使用 text/cache-manifest 类型的资源必须遵循 application cache manifest 的语法。

manifest 语法

CACHE MANIFEST
# 每个缓存清单文件都从上面这一行开始
# 可以在文件的任何位置放置单行或者多行注释,都会被忽略掉

# 空行也会被忽略掉
# 下面这些文件是要被缓存的,没有加头部声明的,默认都是要缓存的。
images/sound-icon.png
images/background.png
# 注意:每个文件要放在单独的一行

# NETWORK: 区段表示的是在线白名单 - 不会被缓存,
# 并且到这些文件的引用都会绕过缓存,总是访问网络。(离线状态下访问这些文件会报错)
NETWORK:
comm.cgi

# 最好给要缓存的文件加上CACHE: 头部声明
CACHE:
style/default.css
      

相对路径?绝对路径?

manifest 除了可以使用绝对路径外,甚至还可以使用“绝对” URL:

CACHE MANIFEST

/main/home
/main/app.js
/settings/home
/settings/app.js
http://img.example.com/logo.png
http://img.example.com/check.png
http://img.example.com/cross.png      

访问未缓存的文件

假设我们有一个名为 network.html 的启用了应用缓存的一个页面,用于显示两张图片。

<!doctype html>
<html manifest="network.manifest">
<head>
<meta charset="UTF-8">
<title>NETWORK</title>
</head>

<body>
    <h1>访问未缓存的文件</h1>
  <img src="img/001.jpg">
  <img src="img/002.jpg">
</body>
</html>
  

network.html

network 的 manifest 文件

但是我们的 manifest 文件只缓存了一张图片,比方说:img/001.jpg

CACHE MANIFEST

img/001.jpg
  
  • 在线第一次访问的时候,两张图片肯定都会被显示,就像前面的图片看到的那样;同时 001.jpg 会被缓存起来。
  • 离线状态下访问,就只会显示一张缓存过的 001.jpg, 因为 002.jpg 没有被缓存起来。
  • 那么在线状态下,后续的对 network.html 的访问,002.jpg 会显示吗?
network.html

Chrome 调试

通过 network 查看 002.jpg 的加载过程,可以看到加载失败的消息。(加载都是 ‘from cache’)

Firefox 调试

Note: no 'Remote IP'

network2.html 的 manifest 文件

002.jpg 在线状态下也不会得到显示,因为浏览器在缓存了某个页面之后,就不会再向 Web 服务器发出请求。从前面两个浏览器的截图我们可以看到,对于离线页面所包含的资源(001.jpg,002.jpg)也是同样的处理,无论是否缓存了这些资源(如没有缓存的002.jpg)。

这时候我们需要使用 NETWORK: 告诉浏览器。这样,如果在线的时候,浏览器就会从 Web 服务器请求 002.jpg, 然后在离线时忽略它。

CACHE MANIFEST

img/001.jpg

NETWORK:
img/002.jpg
  

再查看 network2.html 的 network 控制台消息

Note: 002.jgp 是从 Remote IP 获得的。

为什么需要把不想缓存的文件都列出来?

  • 缓存空间有限,不想缓存大文件;
  • 动态内容不能缓存;
  • 注意,NETWORK: 下面可以使用通配符。比如:*.jpg, *。但是缓存文件列表不支持通配符(防止不小心把整个网站缓存下来。)

    FALLBACK:

    使用 manifest 文件,我们除了告诉浏览器哪些资源要缓存,哪些资源要从 Web 服务器获取之外,还支持一个 "FALLBACK" 的区块。

    FALLBACK: 区块为那些由于某些原因无法被缓存或者缓存失败的资源指定替代资源,每行列出一对文件来。第一个是在线时使用的,第二个是离线后备文件名。支持通配符,这里的通配符是 / 表示任意网页(和 NETWORK: 中的 * 不同):

    CACHE MANIFEST
    
    FALLBACK:
    / /offline.html
          

    浏览器在两种情况下会使用后备文件:

    • 在线状态下访问了不存在的文件(404)
    • 离线状态下访问了没有缓存的文件(不管是存在,还是不存在)

    FALLBACK: 示例

    fallback.html

    <!doctype html>
    <html manifest="fallback.manifest">
    <head>
    <meta charset="UTF-8">
    <title>FALLBACK</title>
    </head>
    <body>
      <img src="img/001.jpg">
        <h2>  <a href="yes.html">确实存在的 yes.html</a></h2>
        <h2>  <a href="no.html">不存在的 no.html</a></h2>
    </body>
    </html>
          

    fallback.manifest

    CACHE MANIFEST
    
    img/001.jpg
    FALLBACK:
    / offline.html      

    针对单个文件设置 fallback

    我们可以单独针对 yes.html 设置一个 fallback 页面。

    fallback2.html

    <html manifest="fallback.manifest">
          

    fallback2.manifest

    CACHE MANIFEST
    
    img/001.jpg
    
    FALLBACK:
    yes.html offline-yes.html
    / offline.html
    

    在线和离线检测

    几种离线检测的方法

  • navigator.onLine
  • 利用 FALLBACK
  • 利用 application cache 的 error 事件
  • 利用 XMLHttpRequest
  • 阶段性的发起网络请求
  • navigator.onLine

    navigator.onLine 维护了一个 truefalse 的布尔值,通过检测这个值可以主动判断浏览器是否在线。另外还可以通过对 onlineoffline 事件的监听来获得离线状态通知。isonline.html

    <!doctype html>
    <html manifest="isonline.manifest">
    <head>
    <meta charset="UTF-8">
    <title>浏览器是否在线?</title>
    <script type="text/javascript">
        window.addEventListener("online", function(e) {
            alert('上线!');
        }, true);
        window.addEventListener("offline", function(e) {
            alert('离线!');
        }, true);
        if (navigator.onLine) {
            alert('浏览器在线!');
        } else {
            alert('浏览器离线');
        }
    </script>
    </head>
    <body>
    </body>
    </html>
          

    利用 FALLBACK

    我们可以利用前面提到的 FALLBACK: 让浏览器根据应用是否在线分别加载 JavaScript 函数的不同版本。

    isonline-fallback.manifest

    CACHE MANIFEST
    
    img/AirPort4.png
    img/AirPortError.png
    
    FALLBACK:
    js/online.js js/offline.js      

    online.js & offline.js

    两个 js 文件都包含一个名为 isSiteOnline() 的简单函数。

    online.js

    function isSiteOnline() {
    	return true;
    }			

    如果浏览器没有下载到 online.js, 就会使用 offline.js, 后者包含一个同名函数,但是返回值是 false。

    function isSiteOnline() {
    	return false;
    }      

    isonline-fallback.html

    isonline-fallback.html

    <!doctype html>
    <html manifest="isonline-fallback.manifest">
    <head>
    <meta charset="UTF-8">
    <title>isonline - using fallback</title>
    <script src="js/online.js"></script>
    <script>
        function check() {
            var airport = document.getElementById('airport');
            if (isSiteOnline()) {
                airport.setAttribute('class', 'airportonline');
            } else {
                airport.setAttribute('class', 'airportoffline');
            }
        }
    </script>
    </head>
    <body onLoad="check();">
    <div id="airport">
    </div>
    </body>
    </html>
    

    使用 applicationCache API 的 error 事件

    applicationCache 总是会请求 manifest 文件来检查是否有更行。请求失败的话,就会触发 error 事件。这一般会是两种情况:

    • 一种是这个 manifest 文件不在 host 在服务器上了
    • 另外一种就是无法访问网络了
    window.applicationCache.addEventListener("error", function(e) {
      alert("Error fetching manifest: a good chance we are offline");
    });

    使用 XMLHttpRequest 的错误事件

    var fetch = function(url, callback) {
      var xhr = new XMLHttpRequest();
      var noResponseTimer = setTimeout(function() {
        xhr.abort();
        if (!!localStorage[url]) {
          callback(localStorage[url]);
          return;
        }
      }, maxWaitTime);
      xhr.onreadystatechange = function() {
        if (xhr.readyState != 4) {
          return;
        }
        if (xhr.status == 200) {
          clearTimeout(noResponseTimer);
          localStorage[url] = xhr.responseText;
          callback(xhr.responseText);
        }
        else {
          if (!!localStorage[url]) {
            // We have some data cached, return that to the callback.
            callback(localStorage[url]);
            return;
          }
        }
      };
      xhr.open("GET", url);
      xhr.send();
    };

    定期访问网络资源

    同样可以通过使用异步请求定期访问一个文件,比如:favicon.cion 的方法。

    setTimeout(function() { fetch("favicon.ico"); } , 30000);
    

    事件流

    当浏览器访问一个指向缓存清单文件的页面时,window.applicationCache 对象上的一系列事件会被浏览器触发:

    • 一旦浏览器访问的页面<html>元素设置了manifest属性, checking 事件就会被触发。(不管之前已经访问过该页面,还是访问过指向同一个缓存清单文件的其他页面,checking事件都会被触发)
    • 如果浏览器从未访问过该缓存清单,那么:
      • 浏览器会触发 downloading 事件,然后去下载缓存清单中的资源。
      • 在下载资源过程中,浏览器会阶段性地触发 process事件,通过该事件可以获取当前下载进度信息(当前已经下载了多少文件,以及还有多少文件在下载队列中)
      • 当所有的资源都下载成功完毕后,浏览器会最后触发 cached 事件。表示当前 Web application 所有资源已经下载完毕,准备好在离线状态下使用了。
    • 另一方面,如果之前浏览过该页面或者访问过指向同一个缓存清单文件的其他页面,那么就意味着浏览器访问过该缓存文件,而且有可能已经将部分,甚至全部资源文件添加到了应用缓存中。那么就要根据该缓存清单在上一次访问后有没有发生变动?
      • 如果未发生变动,浏览器就只会立即触发一个 noupdate 事件。
      • 如果发生了变动,浏览器就会触发 downloading 事件,并开始将缓存清单中的资源重新下载一遍。同样在下载过程中会阶段性的触发 process 事件...当下载完毕后,浏览器会触发 updateready 事件。

    首次访问的事件流

    通过 Chrome 的 console 可以比较方便的观察 applicationCache 的事件流。注意最后一步触发的是 cached 事件。

    manifest 没有变化的事件流

    最后一步触发的是 noUpdate 事件。

    manifest 发生变化

    会把缓存文件全部重新下载一遍。最后一步触发的是 updateReady 事件,这个是我们最关心的。

    处理 updateReady 事件

    updateReady 事件表示新版本应用已经下载下来了,但是浏览器窗口显示的还是旧版本内容。使用这个事件我们可以提醒用户有新的版本可用,让用户刷新页面以浏览新版本的内容。 clock with updateReady

    <!doctype html>
    <html manifest="clock-updateready.manifest">
    <head>
    <meta charset="UTF-8">
    <title>Clock test updateReady</title>
    <script src="js/clock.js"></script>
    <link rel="stylesheet" href="css/clock.css">
    </head>
    <body>
    <p>当前时间是:  <output id="clock"></output>
    </p>
    <script>
            function bindEvent() {
                if (window.applicationCache) {
                    applicationCache.onupdateready = function() {
                        if (confirm("新版本就绪,是否现在加载?")) {
                            window.location.reload();
                        } // end of if
                    } // end of function
                } // end of if
            } // end of function
            window.addEventListener("load", bindEvent, false);
        </script>
    </body>
    </html>
          

    error事件 - 事件流

    如果前面的过程发生了错误,浏览器就会触发error事件并停止。下面是可能发生的错误:

    • 访问缓存清单文件时发生HTTP 404(找不到页面)或者HTTP 410(永远不可用)错误。
    • 访问到缓存清单文件,同时该文件也未发生变动,但是指向该文件的HTML页面下载失败。
    • 访问到缓存清单文件,该文件发生变动。但是无法下载缓存清单中列出的资源。

    检测过程

    浏览器检测缓存清单文件是否发生变动的过程:

    • 通过普通的HTTP语义检测缓存清单文件是否已过期
    • 如果缓存清单文件已经过期,浏览器会向服务器发送一个HTTP请求,请求头中包含了缓存清单文件最后一次修改事件的信息。服务器对获取到的时间和服务器上最新缓存的文件的最后修改时间做一个比较,计算出该文件是否发生变化。如果没有发生变动,就简单的返回304(没有修改)状态码。(这也是所有的Web资源通常的处理方式)
    • 如果服务器计算出缓存清单文件发生变化,就会返回200 ok,同时会将新文件内容和新的canche-control以及最后一次修改时间的HTTP Response头信息,返回给浏览器。浏览器下载完新的缓存文件后,会去读取里面的内容,如果内容和前一个版本一致,没有变化,就不会去重新下载清单文件中的资源。

    Manifest Debug

    • 静态资源缓存的问题(过期时间内修改了缓存清单)
    • 改变了其中一个已经被缓存到应用缓存中的资源文件,但是资源文件清单中的该资源文件的URL并没有变(比如改变了一个CSS文件的内容,这时候要手动修改缓存清单文件)

    特性检测

    使用 window.applicationCache 进行检测:

          if (window.applicationCache) {
          	// 浏览器支持离线应用
          }
          

    或使用 Modernizer

          if (Modernizer.applicationCache) {
          	// . . . 
          }
          

    applicationCache 的其他两个方法

    • update() 让浏览器主动去检测。适用于一打开就是一整天的页面,比如坐席人员使用的。
    • swapCache() 告诉浏览器开始使用新的资源 - 如果已经下载下来的话;不影响当前显示的内容,但是此后加载的新内容会从新缓存获得(可能会引起一些混乱的问题,可以不考虑使用。)

    相关资源

    <Thank you!>

    Feel free to contact me if you have any question.