Web 应用存储和离线 Web 应用


HTML5 新增了「应用程序缓存」,允许 Web 应用将应用程序自身本地保存到用户浏览器中。不同于上面提到的存储数据,它是将应用程序自身保存起来,包括 HTML、CSS、JavaScript、图片等。

Web 应用缓存不会随着用户清除浏览器缓存被清除。

Web 应用缓存落脚点不在「缓存」,更好的叫法应该叫「Web 应用存储」。

让 Web 应用实现「本地安装」的目的是支持离线访问,原理是离线状态下通过 localStorage 访问/保存相关应用数据,同时还具备一套同步机制,再次回到在线状态时,能够将存储的数据传输给服务器。

应用程序缓存清单

要想将应用程序「安装」到应用程序缓存,需要创建一个包含应用程序依赖的所有 URL 列表清单,然后在应用程序主 HTML 页面的 <html> 标签中设置 manifest 属性,指向该清单文件即可。下面是一个简单的清单文件:

    CACHE MENIFEST
    
    # 下面是应用程序依赖的资源文件的URL
    myapp.html
    myapp.css
    myapp.js
    images/background.png

注:清单文件中的相对 URL 都是相对于清单文件而言。

应用程序缓存清单文件约定以 .appcache.manifest 作为文件扩展名,真正识别是通过 text/cache-manifest 这个 MIME 类型(通过响应头设置)。

对于更复杂的应用,无法将所有资源都缓存起来,这时候,我们需要创建一个更加复杂的缓存清单:

CACHE MANIFEST
    
CACHE:
myapp.html
myapp.css
myapp.js
    
FALLBACK:
videos/ offline.help.html
    
NETWORK:
cgi/

我们在清单文件中可以通过特殊的区域头来标识该头信息之后清单项的类型:

  • CACHE:默认区域,其后的清单项都会缓存起来
  • FALLBACK:清单项每行都包含两个URL,第一个URL是一个前缀,匹配该前缀的URL不会缓存,而是从网络载入,如果载入失败,则用第二个缓存资源替代
  • NETWORK:清单项都是URL前缀,匹配该前缀的URL都会从网络下载

缓存的更新

在线状态下,浏览器会异步检查清单文件是否有更新,如果有更新则将新的清单文件保存下来,这里需要注意的是浏览器只是检查清单文件,不会检查缓存文件。我们可以通过版本号来更新清单文件:

CACHE MENIFEST
    
# MyApp Version 1 (更改这个数字可以让浏览器重新下载这个文件)
myapp.html
myapp.css
myapp.js

要让 Web 应用从缓存中「卸载」,就要在服务器端删除这个清单文件,同时修改 HTML 文件以便它们与清单列表「断开连接」。

浏览器在更新缓存过程中会触发一系列事件,可以通过注册事件处理程序来跟踪这个过程同时给用户提供反馈:

applicationCache.onupdateready = function() {
    var reload = confirm("该应用有新版本发布了,刷新页面后生效,现在需要刷新吗?");
    if (reload)
        location.reload();
};

该事件处理程序注册在 ApplicationCache 对象上,该对象是 Window 对象的 applicationCache 属性值,支持应用程序缓存的浏览器会定义这个属性。除此之外,还有其他应用缓存相关事件:

// 处理应用缓存相关事件
    
// 显示状态消息
function status(msg) {
    document.getElementById("statusline");
    console.log(msg);
}
    
// 每次应用程序载入时,都会检查该清单文件
window.applicationCache.onchecking = function() {
    status("检查新版本");
    return false;
};
    
// 如果清单文件没有改动,同时应用程序也已经缓存了
window.applicationCache.onnoupdate = function() {
    status("已经是最新版本了");
    return false;
};
    
// 如果还未缓存应用程序,或者清单文件有改动
window.applicationCache.ondownloading = function() {
    status("下载新版本");
    window.progresscount = 0;
    return false;
};
    
// 在现在过程中会间断性的触发progress事件
window.applicationCache.onprogress = function (e) {
    var progress = "";
    if (e && e.lengthComputable)
        progress = " " + Math.round(100 * e.loaded / e.total) + "%";
    else 
        progress = " (" + ++progresscount + ")";
    
    status("下载新版本进度 " + progress);
    return false;
};
    
// 下载完成并首次将应用程序下载到缓存中时触发该事件
window.applicationCache.oncached = function () {
    status("应用已经缓存到本地");
    return false;
};
    
// 新在完成并将缓存中的应用程序更新后触发(首次载入时不触发该事件)
window.applicationCache.onupdateready = function () {
    status("新版本已经下载,刷新页面运行");
    return false;
};
    
// 浏览器处理离线状态,检查清单列表失败
window.applicationCache.onerror = function () {
    status("无法加载 mainfest 或缓存应用");
    return false;
};
    
// 如果一个缓存的应用程序引用一个不存在的清单文件时会触发
window.applicationCache.onobsolete = function () {
    status("该应用不再缓存,刷新页面在线获取最新版本");
    return false;
};

除了使用事件处理程序之外,还可以使用 applicationCache.status 属性来检查当前缓存的状态:

  • ApplicationCache.UNCACHED(0):未缓存
  • ApplicationCache.IDLE(1):清单检查完毕,已缓存最新应用
  • ApplicationCache.CHECKING(2):浏览器正在检查清单文件
  • ApplicationCache.DOWNLOADING(3):正在下载缓存清单中的文件
  • ApplicationCache.UPDATEREADY(4):已经下载并缓存了最新版的应用程序
  • ApplicationCache.OBSOLETE(5):清单文件不存在,缓存将被清除

此外,ApplicationCache 还提供了两个方法:

  • update() 方法显式调用更新缓存算法以检查是否有最新版本的应用程序
  • swapCache() 方法告诉浏览器它可以弃用老的缓存,所有请求都从新缓存中获取,但是并不需要重新载入应用程序,需要注意的是只有当状态属性是 ApplicationCache.UPDATEREADY 或 ApplicationCache.OBSOLETE 时,调用该方法才有意义,其他情况调用会抛异常

离线 Web 应用

离线应用的原理是通过 localStorage 存储应用数据,当在线的时候将数据上传到服务器。

离线应用需要感知是否在线,这可以通过 navigator.onLine 属性来检测,同时在 Window 对象上注册在线和离线事件的处理程序。

我们将实现一个简单的在线记事本程序 —— PermaNote 来演示离线应用的开发,它将用户数据保存到 localStorage,并在网络连接可用时将其上传到服务器,该应用包含三个文件:一个缓存清单文件,一个HTML文件,以及一个JavaScript脚本文件。

permanota.appcache 文件代码:

CACHE MANIFEST
# PermaNote v8
permanote.html
permanote.js
NETWORK:
note

permanote.html

<!DOCTYPE html>
<html manifest="permanote.appcache">
<head>
    <meta charset="UTF-8">
    <title>PermaNote Editor</title>
    <script src="permanote.js"></script>
    <style>
    
    </style>
</head>
<body>
    <div id="toolbar">
        <button id="savebutton" onclick="save()">Save</button>
        <button onclick="sync()">Sync Note</button>
        <button onclick="applicationCache.update()">Update Application</button>
    </div>
    <textarea id="editor"></textarea>
    <div id="statusline"></div>
</body>
</html>

permanote.js

var editor, statusline, savebutton, idletimer;
    
window.onload = function () {
    if (!localStorage.note === null)
        localStorage.note = "";
    if (localStorage.lastModified === null)
        localStorage.lastModified = 0;
    if (localStorage.lastSaved === null)
        localStorage.lastSaved = 0;
    
    editor = document.getElementById("editor");
    statusline = document.getElementById("statusline");
    savebutton = document.getElementById("savebutton");
    
    editor.value = localStorage.note;
    editor.disabled = true;  // 同步前禁止编辑
    
    editor.addEventListener("input", function (e) {
        localStorage.note = editor.value;
        localStorage.lastModified = Date.now();
        if (idletimer)
            clearTimeout(idletimer);
        idletimer = setTimeout(save, 5000); // 每隔5秒保存一次
        savebutton.disabled = false;
    }, false);
    
    // 每次载入应用时尝试同步服务器
    sync();
};
    
// 离开页面保存数据到服务器
window.onbeforeonload = function () {
    if (localStorage.lastModified > localStorage.lastSaved) {
        save();
    }
};
    
// 离线通知用户
window.onoffline = function () {
    status("已离线");
};
    
// 上线同步数据
window.ononline = function () {
    sync();
};
    
// 应用有新版本时告诉用户
window.applicationCache.onupdateready = function () {
    status("该应用有新版本,刷新页面运行");
};
    
window.applicationCache.onnoupdate = function () {
    status("当前运行的是应用最新版本");
};
    
// 在状态栏显示状态消息
function status(msg) {
    statusline.innerHTML = msg;
}
    
// 每当笔记内容更新后,用户停止更新超过5分钟就会自动将笔记上传到服务器
function save() {
    if (idletimer)
        clearTimeout(idletimer);
    idletimer = null;
    if (navigator.onLine) {
        var xhr = new XMLHttpRequest();
        xhr.open("PUT", "/note");
        xhr.send(editor.value);
        xhr.onload = function () {
            localStorage.lastSaved = Date.now();
            savebutton.disabled = true;
        };
    }
}
    
// 检查服务器端是否有新版本笔记,如果没有,将本地笔记保存到服务器
function sync() {
    if (navigator.onLine) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/note");
        xhr.send();
        xhr.onload = function () {
            var remoteModTime = 0;
            if (xhr.status === 200) {
                remoteModTime = xhr.getResponseHeader("Last-Modified");
                remoteModTime = new Date(remoteModTime).getTime();
            }
            if (remoteModTime > localStorage.lastModified) {
                status("服务器上有更新版本数据");
                var useit = confirm("服务器上有未同步数据,点击「确定」使用服务器数据,点击「取消」继续编辑");
                var now = Date.now();
                if (useit) {
                    editor.value = localStorage.note = xhr.responseText;
                    localStorage.lastSaved = now;
                    status("已下载最新版本笔记");
                } else {
                    status("忽略服务器版本笔记");
                }
                localStorage.lastModified = now;
            } else {
                status("正在编辑当前版本的笔记");
            }
    
            if (localStorage.lastModified > localStorage.lastSaved) {
                save();
            }
    
            editor.disabled = false;
            editor.focus();
        };
    } else {
        status("离线状态不能同步");
        editor.disabled = false;
        editor.focus();
    }
}

Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 利用 IE userData 持久化数据

>> 下一篇: 脚本化图片