1. 程式人生 > >HTML5新特性之離線快取技術

HTML5新特性之離線快取技術

一、離線快取的起因。

     HTML5之前的網頁,都是無連線,必須聯網才能訪問,這其實也是web的特色,這其實對於PC是時代問題並不大,但到了移動網際網路時代,

裝置終端位置不再固定,依賴無線訊號,網路的可靠性變得降低,比如坐在火車上,過了一個隧道(15分鐘),便無法訪問網站,十分不便

而離線web應用允許我們在離線時與網站進行互動。

二、什麼是離線Web應程式?為什麼要開發離線的Web應用程式?

離線web應用程式是指:當客戶端本地與web應用程式的伺服器沒有建立連線時,也能正常在客戶端本地使用該web應用程式進行有關操作。

Web應用程式已經變的越來越複雜,很多領域都在利用Web應用程式。但是,它有一個致命的缺點:如果使用者沒有和Internet建立連線,他就

不能利用這個web應用程式了。因此H5新增了一個API,它使用一個本地快取機制很好的解決了這個問題,使離線應用程式的開發成為了可能。

要想使web應用程式在離線狀態的時候也能正常工作,就必須把所有構成web應用程式的資原始檔,如HTML檔案、CSS檔案、JavaScript指令碼

檔案等放在本地快取中,當伺服器沒有和Internet建立連線時,也可以利用本地快取中的資原始檔正常執行web應用程式。

三、什麼是本地快取,本地快取與瀏覽器網頁快取的區別

Web應用程式的本地快取與瀏覽器的網頁快取有許多方面都存在著明顯的區別。

1.本地快取為整個web應用程式服務的,而瀏覽器的網頁快取只服務於單個網頁

。任何網頁都具有網頁快取。而本地快取至快取那些指定的快取

的頁面。   

2.網頁快取不安全不可靠為我們不知道在網站中到底快取了哪些網頁,以及快取了網頁上的哪些資源。而本地快取可靠,我們可以控制對

哪些內容進行快取,

不對哪些內容進行快取,開發人員還可以利用程式設計的手段來控制快取的更新,利用快取物件的各種屬性、狀態和事件來開發出更加強大的離線

應用程式。

3.(有些)瀏覽器會主動儲存自己的快取檔案以加快網站載入速度。但是要實現瀏覽器快取必須要滿足一個前提,那就是網路必須要保持連線

。如果網路沒有連線,

即使瀏覽器啟用了對一個站點的快取,依然無法開啟這個站點。只會收到一條錯誤資訊。而使用離線web應用,我們可以主動告訴瀏覽器應該

從網站伺服器中獲取或快取哪些檔案,並且在網路離線狀態下依然能夠訪問這個網站。

四、如何實現HTML5應用程式快取?什麼是manifest檔案,在檔案中制定什麼內容需要進行本地快取,哪些內容不需要?

實現HTML5應用程式快取非常簡單,只需三步,並且不需要任何API。只需要告訴瀏覽器需要離線快取的檔案,並對伺服器和網頁做一些

簡單的設定即可實現。

4-1、建立一個 cache.manifest 檔案,並確保檔案具有正確的內容。

   4-2、在伺服器上設定內容型別。

   4-3、所有的HTML檔案都指向 cache.manifest

具體實現:

4-1:首先我們建立一個名為cache.manifest的檔案,Windows平臺下用記事本即可(也可用其他的IDE)。檔案內容如下:

CACHE MANIFEST
#version1
CACHE:
index.html
404.html
favicon.ico
robots.txt
humans.txt
apple-touch-icon.png
css/normalize.min.css
css/main.css
css/bootmetro-icons.min.css
img/pho-cat.jpg
img/pho-huangshan.jpg

FALLBACK:
online.js local.js

NETWORK:
*

注意事項:

1、第一行必須是”CACHE DMANIFEST”文字,以把本檔案的作用告知瀏覽器,即對本地快取中的資原始檔進行具體設定。

2、在manifest檔案中,可以加上註釋來進行一些必要說明或解釋。註釋行以”#”文字開頭

3、在CACHE之後的部分為列出我們需要快取的檔案

4、在FALLBACK之後的部分每一行中指定兩個資原始檔,第一個資原始檔為能夠線上訪問時使用的資原始檔,第二個資原始檔為

不能線上訪問時使用的備用資原始檔。

5、在NETWORK之後可以指定線上白名單,即列出我們不希望離線儲存的檔案,因為通常它們的內容需要網際網路訪問才有意義。

另外,在此部分我們可以使用快捷方式:萬用字元*。這將告訴瀏覽器,應用伺服器中獲取沒有在顯示部分中提到的任何檔案或URL。

4-2:伺服器上設定內容型別。

真正執行或測試離線web應用程式的時候,需要對伺服器進行配置,讓伺服器支援text/cache-manifest這個MIME型別(在h5中規定

manifest檔案的MIME型別是text/cache-manifest)。例如對Apache伺服器進行配置的時候,需要找到

{apache_home}/conf/mime.type這個檔案(.htaccess),並在檔案最後新增如下所示程式碼:

text/cache-manifest .manifest 。在微軟的IIS伺服器中的步驟如下所示:

(1).右鍵選擇預設網站或需要新增型別的網站,彈出屬性對話方塊

(2).選擇”http頭”標籤

(3).在MIME對映下,單擊檔案型別按鈕

(4).在開啟的MIME型別對話方塊中單擊新建按鈕

(5).在關聯副檔名文字中輸入”manifest”,在內容型別文字框中輸入”text/cache-manifest”,然後點選確定按鈕。

4-3:設定HTML檔案的指向。

<html manifest=”/cache.manifest” >

完成這一步後,就完成了web離線快取的所有步驟。由於瀏覽的檔案內容都沒有更改且儲存在本地,因此現在網頁的開啟速度會更快

(即使是線上狀態也如此)。 

注意事項:

1、網站的每一個html頁面都必須設定html元素的manifest屬性。Must to do;

2、在你的整個網站應用中,只能有一個cache.manifest檔案(建議放在網站根目錄下);

3、部分瀏覽器(如IE8-)不支援HTML5離線快取;

4、“#” 開頭的註釋行可滿足其他用途。應用的快取會在其 manifest 檔案更改時被更新。如果您編輯了一幅圖片,或者修改了一個 JavaScript 函式,

這些改變都不會被重新快取。更新註釋行中的日期和版本號是一種使瀏覽器重新快取檔案的辦法。

五、掌握進行本地快取的applicationCache物件及其屬性和事件:

(1)快取的更新

當一個web應用從快取中載入的時候,所有與之相關的檔案也是直接從快取中獲取。線上狀態下,瀏覽器會非同步地檢查清單檔案是否有更新

如果有更新,新的清單檔案以及清單中的列舉的所有檔案都會下載下來重新儲存到程式快取中。但要注意瀏覽器只是檢查清單檔案,而不會

檢查快取的檔案是否有更新,如果修改一個快取的js檔案,並且要想讓該檔案生效,就必須去更新下清單檔案。由於應用程式依賴的檔案列

表其實並沒有變化,因此最簡單的方式就是更新版本

程式碼如下:

CHCHE MANIFEST
CACHE:
#version<span style="color:#cc0000;">2</span> (更改這個數字以便讓瀏覽器重新下載)
 myapp.html
 myapp.css
 myapp.js

同樣“解除安裝“,就要在伺服器端刪除清單檔案,使得請求該檔案的時候返回404,同時,修改html檔案以便他們與該清單列表”斷開連結“。

注意:

①、瀏覽器檢查清單檔案以及更新快取的操作是非同步的,可能是在從快取中載入應用之前,也有可能是同時進行。因此對於簡單的web應用

而言,在更新清單檔案之後,使用者必須載入應用兩次才能保證最新的版本生效:第一次是從快取中載入老版本隨後更新快取;第二次才是

快取中載入最新的版本

②、瀏覽器在更新快取過程中會觸發一系列事件,可以通過註冊處理程式來跟蹤這個過程同時提供反饋使用者。

程式碼如下:

applicationCache.onupdateready= function(){
   var reload = confirm(“A new version of this application is available\n and will be used the next time you reload.\n”);
   if(reload)  location.reload();
}
該事件註冊在ApplicationCache物件上的,該物件是window的applicationCache屬性的值。支援應用程式快取的瀏覽器會定義該屬性。 

(2)處理應用快取相關事件:

//下面所有的事件處理程式都使用此函式來顯示狀態訊息
//由於都是通過呼叫status函式來顯示狀態,因此所有處理程式都返回false來阻止瀏覽器顯示其預設狀態訊息
  function status(msg){
     doucment.getElementById(“statusline”).innerHTML= msg;
     console.log(msg); //同時在控制檯輸出此訊息,便於除錯
  }
   //每當應用程式載入的時候,都會檢查該清單檔案
   //也總會首先觸發“checking”事件
   window.applicationCache.onchecking = function(){
      status(“checking for a new version.”);
      return false;
   }
   //如果沒有改動,同時應用程式也已經快取了
   //”noupdate”事件被觸發,整個過程結束
   window.applicationCache.onnoupdate = function(){
   }
    //如果還未快取應用程式,或者清單檔案有改動
    //那麼瀏覽器會下載並快取清單中的所有資源
    //觸發”downloading”事件,同時意味著下載過程開始
   window.applicationCache.ondownloading = function(){
        status(“Downloading new version”);
        window.progresscount = 0;
        return false;
   }
   //在下載過程中會間斷性觸發“progress“事件
   //通常是在每個檔案下載完畢的時候
   window.applicationCache.onprogress = function(e){
        varprogress = “”;
        if(e && e.lengthComputable)
               progress = “ ”+Math.round(100*e.loaded/e.total)+”%”
         else
               progress = “(“+(++progresscount)+”)”
         return false;
   }
    //當下載完成並且首次將應用程式下載到快取中時,瀏覽器會觸發“cached“事件
   window.applicationCache.oncached = function(e){
        status(“Thisapplication is now cached locally”);
        return false;
   }
 
    //當下載完成並將快取中的應用程式更新後,瀏覽器會觸發”updaterady”事件
    //要注意的是:觸發此事件的時候,使用者任然可以看到老版本的應用程式
   window.applicationCache.onupdateready = function(e){
        status(“Anew version has been downloaded. Reload to run it”);
        return false;
   }
 
    //如果瀏覽器處於離線狀態,檢查清單列表失敗,則會觸發“error“事件
    //當一個未快取的應用程式引用一個不存在的清單檔案,也會觸發此事件
   window.applicationCache.onerror = function(e){
        status(“Couldn’tload manifest or cache application”);
        return false;
   }
 
    //如果一個快取的應用程式引用一個不存在的清單檔案,會觸發“obsolete“
    //同時將應用從快取中移除之後不會從快取而是通過網路載入資源
   window.applicationCache.onobsolete = function(e){
        status(“Thisapplication is no longer cached. Reload to get the latest version from thenetwork.”);
        return false;
   }

每次載入一個設定了manifest屬性的html檔案,瀏覽器都會觸發”checking”事件。並通過網路載入該清單檔案。不過之後,會隨著

不同的情況觸發不同的事件。

事件列表:

(1).沒有可用更新

如果應用程式已經快取並且清單檔案沒有動,則瀏覽器會觸發noupdate事件

(2).有可用更新

如果應用程式已經快取並且清單元件有改動,則瀏覽器會觸發downloading事件並開始下載和快取清單檔案中列舉的所有資源。

隨著下載過程的進行瀏覽器還會觸發”progress”事件,在下載完成後,會觸發”updateready”事件

(3).首次載入新的應用程式

如果還未快取應用程式,如上所述downloading,progress事件都會觸發。但是,當下載完成後,瀏覽器會觸發”cached”事件

而不是updateready事件

(4).瀏覽器處於離線狀態

如果瀏覽器處於離線狀態,它無法檢查清單檔案,同時它會觸發“error”事件

如果一個未快取的應用程式引用了不存的清單檔案,瀏覽器也會觸發該事件

(5).清單檔案不存在

如果瀏覽器處理線上狀態,應用程式也已經快取起來,但是清單檔案不存在,瀏覽器會觸發obsolete事件,並將該應用程式

從快取中移除。


快取狀態:

快取的狀態可以通過window.applicationCache.status獲得,其狀態主要包括如下6種:

    const unsigned short UNCACHED=0;//未快取(應用程式沒有設定manifest屬性:未快取)
    const unsigned short IDLE=1;//空閒狀態(清單檔案已經檢查完畢,並且已經快取了最新的應用程式)  
    const unsigned short CHECKING=2;//檢查中(瀏覽器正在檢查清單檔案)  
    const unsigned short DOWNLOADING=3;//下載中(瀏覽器正在下載並快取清單中列舉的所有檔案)
    const unsigned short UPDATEREADY=4;//更新準備中(已經下載和快取了最新版的應用程式)
    const unsigned short OBSOLETE =5;//過期狀態(清單檔案不存在,快取將被清除)  
    readonly attribute unsigned short status; 

六、ApplicationCache物件還定義了兩個方法update()和swapCache():

(1).update

顯式呼叫了更新快取演算法以檢測是否有最新版本的的應用程式。這導致瀏覽器檢測同一個清單檔案(並觸發相同的事件),

這和第一次載入應用程式時的效果是一樣的。

(2).swapCache

它告訴瀏覽器可以棄用老快取,所有的請求都從新快取中獲取。注意,這並不會重新載入應用程式:所有已經載入的html檔案

、圖片、指令碼等資源都不會改變。但是,之後的請求將從最新的快取中獲取。這會導致“版本錯亂”的問題,因此一般不推薦使用

,除非應用程式設計得很好,確保這樣的方式沒有問題。只有ApplicationCache.UPDATEREADY和

ApplicationCache.ABSOLETE 時呼叫 swapCache()才有意義(當狀態OBSOLETE時,呼叫它可以立即棄用廢棄的快取,

讓之後所有的請求都通過網路獲取)。如果狀態屬性是其他數值的時候呼叫swapCache()方法,它就會丟擲異常。

七、如何判斷線上還是離線狀態?

離線web應用指的是將自己“安裝”在應用程式快取中的程式,使得哪怕在瀏覽器處於離線狀態時依然可訪問它。為了在離線狀態可用

Web應用需要可以告知別人自己是離線還是線上,同時當網路連線的狀態發生改變時候也能“感知”到。通過navigator.onLine屬性,

navigator.onLine是HTML5定義用來檢測裝置是線上還是離線。對應的值為false或true。但是不同瀏覽器表現並不一致。


IE 6+和Safari 5+能夠正確的檢測到網路已斷開,並將navigator.onLine設為flase。

Firefox 3+和Opera 10.6+也支援navigator.onLine。但需要手動講瀏覽器設定為離線模式才能讓瀏覽器正常工作。

Chrome 11及以上版本始終將navigator.onLine設為true。(不過作者的Chrome 21已經能正常使用了)

HTML5定義了online&offline事件用於監聽網路狀態變化。

window.addEventListener('online', callback); // 離線到上線
window.addEventListener('offline', callback); // 上線到離線

目前除了IE(IE只支援navigator.onLine屬性)外,其他最新瀏覽器都支援這個事件。

八、離線Web應用實戰。

通過一個簡單的記事本程式——PermaNote,來解釋如何使用。程式將使用者的文字儲存到localStorage中,並且在網路連線可用的時候,

將其上傳到伺服器,PermaNote只允許使用者編輯單個筆記。

PermaNote應用包含3個檔案,一個應用清單檔案、一個html頁面檔案,一個實現邏輯的js檔案。

Demo: http://xuanfengge.com/demo/201506/appcache/permanote.html

①.premanote.appcache部分:

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

②.permanote.html部分:

<!DOCTYPEHTML>
<html manifest= permanote.appcache”>
     <head>
          <title>PermaNote Editor</title>
          <script src=” permanote.js”></script>
           <style type=”text/css”>
                 #editor {width:100%;height:250px}
                 #statusline{width:100%}
            </style>
      </head>
       <body>
               <div id=”toobar”>
                    <button id=”savebutton”onclick = “save()”>save</button>
                    <button onclick = “sync()”>SyncNote</button>
                    <button onclick = “applicationCache.update()”>UpdateApplication</button>
                      <textarea id=”editor”></textarea>
                       <div id=”statusline”></div>
               </div>
      </body>
</html>

③.permanote.js部分

status()函式用於顯示狀態列訊息,save()函式將筆記本儲存到伺服器,sync()用於確保本地與伺服器文字的同步。

應用程式的時間處理程式解釋:

(1).onload

嘗試和伺服器同步,一旦有新版本的筆記並且完成同步後,就啟用編輯器視窗。

save()和sync()函式發出HTTP請求,並在XMLHttpRequest物件上註冊一個onload時間處理程式來獲取上傳或者

下載完成的提醒。

(2).onbeforeunload

在未上傳前,把當前版本的筆記資料儲存到伺服器上。

(3).oninput

每當textarea輸入框內容發生變化時,都將其內容儲存到localStorage中,並啟動一個計時器。當用戶停止編輯超過5秒

,將自動把資料儲存到伺服器。

(4).onoffline

當瀏覽器進入離線狀態時,在狀態列顯示離線訊息。

(5).ononline
當瀏覽器回到線上狀態時,同步伺服器,檢查是否有新版本的資料,並且儲存當前版本的資料。

(6).onupdateready

如果新版本的應用已快取,則在狀態列展示訊息告知使用者。

(7).onnoupdate

如果應用程式快取沒有發生變化,則同時使用者仍在運行當前版本。

// 定義全域性變數
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;

    // 查詢編輯器UI元素,並初始化全域性變數
    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
                                localStorage.note = editor.value;
                                localStorage.lastModified = Date.now();
                                // 重置閒置計時器
                                if (idletimer) clearTimeout(idletimer);
                                idletimer = setTimeout(save, 5000);
                                // 啟用儲存按鈕
                                savebutton.disabled = false;
                            },
                            false);

    // 每次載入應用程式時,嘗試同步伺服器
    sync();
};

// 離開頁面錢儲存資料到伺服器
window.onbeforeunload = function() {
    if (localStorage.lastModified > localStorage.lastSaved)
        save();
};

// 離線時,告知使用者
window.onoffline = function() { status("Offline"); }

// 再次返回線上狀態時,進行同步
window.ononline = function() { sync(); };

// 當有新版本應用的時候,提醒使用者
// 也可使用location.reload()放大來強制重新整理應用
window.applicationCache.onupdateready = function() {
    status("A new version of this application is available. Reload to run it");
};

// 當沒有新版本的時候也通知使用者
window.applicationCache.onnoupdate = function() {
    status("You are running the latest version of the application.");
};

// 狀態列顯示相關資訊提示
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) {
                var remoteModTime = xhr.getResponseHeader("Last-Modified");
                remoteModTime = new Date(remoteModTime).getTime();
            }

            if (remoteModTime > localStorage.lastModified) {
                status("Newer note found on server.");
                var useit =
                    confirm("There is a newer version of the note\n" +
                            "on the server. Click Ok to use that version\n"+
                            "or click Cancel to continue editing this\n"+
                            "version and overwrite the server");
                var now = Date.now();
                if (useit) {
                    editor.value = localStorage.note = xhr.responseText;
                    localStorage.lastSaved = now;
                    status("Newest version downloaded.");
                }
                else 
                    status("Ignoring newer version of the note.");
                localStorage.lastModified = now;
            }
            else
                status("You are editing the current version of the note.");

            if (localStorage.lastModified > localStorage.lastSaved) {
                save();
            }

            editor.disabled = false;  // 再次啟用編輯器
            editor.focus();           // 將游標定位到編輯器中
        }
    }
    else { // 離線狀態下,不能同步
        status("Can't sync while offline");
        editor.disabled = false;
        editor.focus();
    }
}