1. 程式人生 > >【快取】HTML5快取的那些事

【快取】HTML5快取的那些事

關於儲存

說到儲存,你可能會想到這是伺服器端的一種設定。

伺服器端的儲存介質大體上分為4種:

  • cache:快取,它可以讓從資料庫、磁碟上輸出的東西/資料放置在快取裡,從而減少資料庫或是磁碟的讀取與寫入(IO)操作;
  • 磁碟檔案:如,我們常常會將圖片、視訊等檔案存放在磁碟上;
  • 資料庫:mySql\mongoDB…關係\非關係資料庫;
  • 記憶體:通常放置頻繁要使用到的東西,能夠提高讀取效率;快取(cache)也是存放在記憶體裡的;

HTML的儲存-cookies

在HTML5出生之前,通常在瀏覽器(客戶端)使用cookies來儲存客戶端的內容;

cookies的特點:

  • 每次的http請求頭中,都會帶有cookies——缺點;
  • 每個域名只能儲存4K大小的cookies;
  • 主域名汙染:如果我們使用cookies儲存主域名的東西,那麼子域名下得Http請求都會帶上主域名的東西;

如果關聯上網路,那麼將帶來安全問題。

所以,通常我們會使用cookies用在如購物車、身份驗證等問題上。

下面,我們來看一下百度首頁的cookies在瀏覽器端的一個儲存形態:

如圖:

這裡寫圖片描述

HTTP這一列,如果在setCookie的時候,這裡就會打鉤,這與HTTPOnly相關。

HTTPOnly:

如果把HTTPOnly設定為true,那麼cookies只能被server伺服器端來讀取或是修改,客戶端沒有許可權進行讀取和修改。例如,我們在進行身份驗證的時候,就可以使用這個。

Secure:與安全相關,如果設定了,那麼請求只能是來自HTTP加密請求。

HTML的儲存-UserData

  • 只有IE支援,有微軟提供API,但不符合W3C標準;
  • 儲存在XML檔案中;

HTML5的儲存

針對以上問題,HTML5的出現,需要解決以下問題:

  • 解決4K的大小問題;
  • 解決請求頭常帶儲存資訊的問題;
  • 解決關係型儲存的問題;
  • 跨瀏覽器平臺問題;

HTML5儲存形式

  • 本地儲存——localstorage \sessionstorage
  • 離線快取——application cache
  • IndexedDB、Web SQL

本地儲存

API:

localstorage 、sessionstorage

儲存形式:

key–>value

過期時間:

localstorage:永久儲存,永不失效,除非手動刪除
sessionstorage:重新開啟頁面,或是關閉瀏覽器,sessionstorage才會消失;

儲存大小:
每個域名能存5M;

支援情況:

IE8+,safari3.2+,chrome,firefox等主流瀏覽器都支援;

使用方法——localstorage\sessionstorage

主要涉及到5個方法:

  • getItem:獲取localstorage\sessionstorage
  • setItem:設定localstorage\sessionstorage
  • removeItem:移除localstorage\sessionstorage
  • key:獲取某一個位置上的key值,按值從0開始索引;
  • clear:全部清除localstorage\sessionstorage

例如:我們開啟www.baidu.com

在控制檯Console輸出面板,輸入:

localStorage.setItem("test1","test");
那麼在Resources面板的Local Storage下,將出現Key=test1,value=test的記錄

localStorage.getItem("test1"); //輸出test

localStorage.key(0);//輸出BDSUGSTORED

sessionstorage的API與localstorage一樣,但是你要注意一點:

sessionStorage需要在瀏覽器關閉或是重新開啟頁面,才會消失;

本地儲存可以儲存什麼?

  • 陣列(需要將其序列化為字串才能儲存);
  • json資料——將其轉化為字串儲存;
  • 圖片
  • 指令碼、樣式檔案:通過ajax

只要能被轉化為字串的資料,都能被localstorage儲存;

本地儲存如何儲存圖片

先來看一段程式碼:

    var src="demo.jpg";

    function set(key){
        var img=document.createElement('img');

        img.addEventListener("load",function(){
            //建立一個canvas
            var imgCanvas=document.createElement("canvas"),
            imgContext=imgCanvas.getContext("2d");
            //確保canvas元素的大小和圖片的尺寸一致
            imgCanvas.width=this.width;
            imgCanvas.height=this.height;
            //渲染圖片到canvas中,使用canvas的drawImage()方法
            imgContext.drawImage(this,0,0,this.width,this.height);
            //用canvas的dataUrl的形式取出圖片,imgAsDataURL是一個base64的字串
            var imgAsDataURL=imgCanvas.toDataURL("image/png");
            //儲存到本地儲存中
            //使用try-catch()檢視是否支援localstorage
            try{
                localStorage.setItem(key,imgAsDataURL);//將取出的圖片存放到localStorage 
            }
            catch(e) {
             console.log("Storage failed:"+e);//儲存失敗
            }

        },false);
        img.src=src;
    }   
    function get(key) {//從本地快取獲取圖片並且渲染
    var srcStr=localStorage.getItem(key);//從localStorage中取出圖片
    var imgObj=document.createElement('img');//建立一個img標籤
    imgObj.src=srcStr;
    document.body.appendChild(imgObj);
    }

        註釋:
        (1)、這個比較適合用在不常更改的圖片,但是如果圖片的base64大小比較大的話,將比較耗費localStorage的資源;

        (2)、canvas有一個安全策略的問題:如果圖片和你本身請求的域名不在同一個域名下,瀏覽器會報出一個安全問題,這個時候我們要給我們的伺服器加一個“允許跨域”訪問的響應頭————Access Orign=*,這樣來保證你的圖片可進行跨域被canvas來畫;

HTML5本地儲存需要注意的:

  • 使用前判斷瀏覽器是否支援localStorage;(IOS瀏覽器在無痕模式瀏覽下,是無法開啟localStorage;以及,其他奇葩瀏覽器,在儲存localstorage的時候報錯)

做法:根據前面程式碼,我們在檢查是否支援,先進行setItem()一次,然後對setItem進行異常捕獲;

  • 寫資料的時候,需要異常處理,避免超出容量丟擲錯誤;
    localStorage本身只有5M;

  • 避免把敏感的資訊存入localStorage;

  • key的唯一性;重複寫,將會覆蓋之前的key;

HTML5本地儲存使用限制:

  • 儲存更新策略,過期控制:localStorage是永不過期的,業務上如果想實現一些過期策略,需要在localStorage上加一層處理過期的機制;
  • 各個子域名之間不能共享儲存資料;(藉助H5的postMessage()這個API做一些跨域上得處理)
  • 超出儲存大小之後如何儲存——使用一些如LRU、FIFO的演算法去淘汰一些舊的資料;
  • server端如何取到資料——使用post/get引數

處理過期控制

先來看一下程式碼:

function set(key,y){
        var curTime=new Date().getTime();
        //儲存一個當時儲存時候的時間
        localStorage.setItem(key,JSON.stringify({data:v,time:curTime}));

    }
    function get(key,exp) {
        var data=localStorage.getItem(key);
        var dataObj=JSON.parse(data);
        if(new Date().getTime()-dataObj.time>exp) {//get出來的時間減去當時儲存的時間大於過期時間,那麼就認為過期
           console.log("過期");
        }else {
        //否則,返回值
         console.log("data="+dataObj.data);
        }
    }

本地儲存使用場景

  • 本地資料儲存,減少網路傳輸
  • 在弱網路的環境下,會發生高延遲,低頻寬,應該儘量把資料(如指令碼、樣式)本地化;

我們來看一張圖,顯示的是本地儲存和網路拉取耗時的對比:

alt text

IndexedDB

概念

IndexedDB,是一種能做瀏覽器中持久地儲存結構化資料的資料庫,並且為web應用提供了豐富的查詢能力;

支援情況

chrome11+\opera不支援\firefox 4+\IE 10+,移動端瀏覽器支援能力弱

儲存結構

IndexedDB是按域名分配獨立空間,一個獨立域名下可以建立多個數據庫,每個資料庫可以建立對個物件儲存空間(表/table),一個物件儲存空間可以儲存多個物件資料;

如圖:

alt text

使用IndexedDB實現離線資料庫

這裡我們主要從IndexedDB 的四大功能入手:

  • 增刪改
  • 事務處理
  • 遊標
  • 索引

下面我們通過一段程式碼來講解,請關注裡面的註釋:

<!DOCTYPE html>
    <html>

    <div class="form-group">
        <label for="name">姓名:</label><input type="text" id="name" value="" />   
        <label for="phone">電話:</label><input type="text" id="phone" value="" /> 
        <label for="address">地址:</label><input type="text" id="address" value="" /> 
        <input type="button" id="seletBtn" value="查詢" />    
        <input type="button" id="add" value="新增" /> 
        <input type="button" id="deleteDB" value="刪除資料庫" /> 
    </div>


    <script type="text/javascript">
        var db;
        var arrayKey=[];
        var openRequest;
        var lastCursor;
        var indexedDB=window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;//indexedDB在不同的瀏覽器下不同

        var dbName="person";//資料庫名稱
        var tableName="testTable";//表名稱
        function init() {
            openRequest=indexedDB.open(dbName);//頁面載入時先開啟一個DB,如果該DB存在,則開啟;不存在,則新建

            //觸發事件——當一個“新的資料庫”被建立或者資料庫的“版本號”被更改時觸發
            openRequest.onupgradeneeded=function(e){
                console.log("onupgradeneeded");
                var thisDb=e.target.result;
                console.log(thisDb.version);

                //檢查這個資料庫中是否包含我們要查詢的表
                if(!thisDb.objectStoreNames.contains(tableName)){
                    //不包含——建立一個表
                    console.log("需要建立一個objectStore");
                    //keyPath:主鍵,autoIncrement:主鍵自增
                    var objectStore=thisDb.createObjectStore(tableName,{keyPath:"id",autoIncrement:true});
                    //建立表的時候,指定哪些欄位是能被索引的
                    objectStore.createIndex("name","name",{unique:false});//建立索引
                    objectStore.createIndex("phone","phone",{unique:false});
                }

            }
            //觸發事件——成功開啟一個數據庫時觸發
            openRequest.onsuccess=function(e){
                db=e.target.result;
                console.log(db.version);
                db.onerror=function(event){
                    alert("資料庫錯誤:"+event.target.errorCode);
                    console.dir(event.target);
                };
                //判斷該資料庫中有沒有這個表
                if(db.objectStoreNames.contains(tableName)){
                    //存在這個表
                    console.log("包含表:"+tableName);
                    //通過事物機制操作一個表的讀寫,從而保證資料的一致性和可靠性
                    var transaction=db.transaction([tableName],"readwrite");
                    //事物的事件
                    transaction.oncomplete=function(event){
                        console.log("完成");
                    };

                    transaction.onerror=function(event){
                        console.dir(event);
                    };
                    var objectStore=transaction.objectStore(tableName);//通過事物獲取表中一個objectStore物件,即表的物件

                    //遍歷表的記錄——遊標-openCursor,這是indexedDb的重點
                    objectStore.openCursor().onsuccess=function(event){
                        var cursor=event.target.result;
                        if(cursor){
                            console.log(cursor.key);
                            console.dir(cursor.value);
                            render({key:cursor.key,name:cursor.value["name"],phone:cursor.value["phone"],address:cursor.value["address"]});
                            lastCursor=cursor.key;//如果不設定lastCursor,那麼遊標預設是下一條接著下一條來遍歷;設定了lastCursor,遊標將迴圈遍歷
                            cursor.continue();
                        }else {
                            console.log("請使用遊標來搞定");
                        }
                    };
                    objectStore.openCursor().onerror=function(event){
                        console.dir(event);
                    };

                }

            }


            //新增新記錄
            document.querySelector("#add").addEventListener("click",function(){
                var name=document.querySelector("#name").value();
                var phone=document.querySelector("#phone").value();
                var address=document.querySelector("address").value();
                var person={"name":name,"phone":phone,"address":address};//設定物件
                //通過事務——操作表
                var transaction=db.transaction([tableName],"readwrite");
                transaction.oncomplete=function(event){
                    console.log("事務處理完成");
                };
                transaction.onerror=function(event){
                    console.dir(event);
                };
                var objectStore=transaction.objectStore(tableName);//建立一個表物件
                objectStore.add(person);//新增物件到表中——add()
                //將新增的記錄顯示處理
                objectStore.openCursor().onsuccess=function(event){
                    cursor=event.target.result;
                    var key;
                    if(lastCursor==null){
                        key=cursor.key;
                        lastCursor=key;
                    }else {
                        key=++lastCursor;
                    }
                    render({key:key,name:name,phone:phone,address:address});
                    console.log("成功新增新記錄:"+key);
                    console.dir(person);
                }

            });

            //刪除指定ID
            function deleteRecord(id){
                var transaction=db.transaction([tableName],"readwrite");
                transaction.oncomplete=function(event){
                    console.log("事務處理完成");
                };
                transaction.onerror=function(event){
                    console.dir(event);
                };
                var objectStore=transaction.objectStore(tableName);
                var removeKey=parseInt(id);
                var getRequest=objectStore.get(removeKey);//獲取索引值---get()
                getRequest.onsuccess=function(e){
                    var result=getRequest.result;
                    console.dir(result);
                }
                var request=objectStore.delete(removeKey);//刪除——delete
                request.onsuccess=function(e){
                    console.log("刪除成功");
                };
                request.onerror=function(e){
                    console.log("刪除錯誤"+e);
                };
                //隱藏刪除的DOM
                document.getElementById(removeKey).style.display="none";
            }

            //查詢記錄
            document.querySelector("#seletBtn").addEventListener("click",function(){
                var curName=document.getElementById("selname").value;
                var transaction=db.transaction([tableName],"readwrite");
                transaction.oncomplete=function(event){
                    console.log("事務處理完成");
                };
                transaction.onerror=function(event){
                    console.dir(event);
                };
                var objectStore=transaction.objectStore(tableName);
                var boundKeyRange=IDBKeyRange.only(curName);//生成一個表示範圍的Range物件---IDBKeyRange,有4個方法,only\lowerBound\upperBound\bound
                objectStore.index("name").openCursor(boundKeyRange).onsuccess=function(event){
                    //從indexedDb中找到name
                    var cursor=event.target.result;
                    if(!cursor){
                        return;
                    }
                    var rowData=cursor.value;
                    console.log(rowData);
                    document.getElementById('content').innerHTML="";
                    render({key:cursor.value.id,name:cursor.value["name"],phone:cursor.value["phone"],address:cursor.value["address"]});
                    cursor.continue();
                };

            });
            //刪除資料庫
            document.querySelector("#deleteDB").addEventListener("click",function(){
                //使用deleteDatabase()
                var deleteDB=indexedDB.deleteDatabase(dbName);
                var content=document.querySelector("#content");
                while(content.firstChild){
                    content.removeChild(content.firstChild);
                }
                deleteDB.onsuccess=function(event){
                    console.log("刪除成功");
                };
                deleteDB.onerror=function(event){
                    console.dir(event.target);
                };
            });

            //渲染
            function render(opt){
                var child_node = document.createElement("div");
                var child_node_child1 = document.createElement("div");
                var child_node_child2 = document.createElement("div");
                var child_node_child3 = document.createElement("div");
                var child_node_child4 = document.createElement("div");
                child_node_child1.setAttribute("class","table_child");
                child_node_child2.setAttribute("class","table_child");
                child_node_child3.setAttribute("class","table_child");
                child_node_child4.setAttribute("class","table_child");
                child_node_child1.setAttribute("style","float:left");
                child_node_child2.setAttribute("style","float:left");
                child_node_child3.setAttribute("style","float:left");
                child_node_child4.setAttribute("style","float:left");
                child_node_child1.innerHTML = name;
                child_node_child2.innerHTML = opt.phone;
                child_node_child3.innerHTML = opt.address;
                child_node_child4.innerHTML = "<input type='button' value='刪除'>"
                child_node.appendChild(child_node_child1);
                child_node.appendChild(child_node_child2);
                child_node.appendChild(child_node_child3);
                child_node.appendChild(child_node_child4);
                child_node.setAttribute("class","table_tr");
                child_node.setAttribute("id",opt.key);
                var content = document.getElementById('content');
                content.appendChild(child_node)

            }

        }
    </script>

    </body>
    </html>

離線快取——application Cache

何為離線快取

它是能讓web應用在離線的情況下繼續使用,通過一個叫manifest的檔案指明需要快取的資源;你可以通過navigator.online檢測是否線上;

原理

如圖:

alt text

解釋:

(1)使用者通過瀏覽器(browser)去訪問應用,首先檢測瀏覽器是否有一個叫做“App cache”的東西存在,如果存在,則從中檢索出app cache所要快取的list,然後把資源(快取在瀏覽器中)拉取出來,返回給使用者;

(2)在訪問的同時,會檢查server上一個叫做manifest的檔案,如果該檔案有更新,就把manifest指定的檔案從server端重新拉取一次,然後把這些快取在瀏覽器中,並更新相應的app cache檔案;如果manifest這個檔案沒有更新,那麼就啥也不做。

從上圖,我們總結2點:

  • 快取機制的改變,會更新app cache.但是,使用者訪問,會返回上一次的結果。這樣一來,會有一個麻煩,即如果你的業務發生更改,你就需要去更新一次manifest。

注意:更改完,第一次是不生效的,只有第二次重新整理才會生效;

  • 如果有一個檔案要更新,你就要去更新manifest,而更新manifest檔案,它會把server上的檔案全部重新拉取一次,而非只是拉取你需要更改的那個檔案,這就會造成損耗;

瀏覽器支援情況

safari on ios 3.2+\android 1,5+\window phone 9+

應用

例子:cache.appcache

CACHE MANIFEST

    #version 1.0

    CACHE:

    #需要快取的檔案
    /css/a.css
    /js/a/js
    /images/a.png

    NETWORK:

    #每次重新拉取的檔案

    *

    FALLBACK

    #離線狀況下代替的檔案

    /404.html

在頁面上引入manifest檔案:

<html manifest="cache.appcache">

在伺服器新增mime-type text/cache-manifest

如果在伺服器上新增:

找到你的xampp/apache/conf目錄,找到mime.types檔案,在最後面新增一條記錄:

text/cache-manifest appcache (appcache是字尾名,你可以選擇其他的)

我們來看一個例子:

<html lang="en" manifest="cache.appcache">
      <head>
        <meat charset="utf-8" />
      </head>
      <body>
         <h1><demo1/h1>

      <script type="text/javascript">
        window.addEventListener('load',function(e){
        //監聽app cache的updateready事件
         window.applicationCache.addEventListener('updateready',function(e)){

            console.log(window.applicationCache.status);
            if(window.applicatioinCache.status==window.applicationCache.UPDATEREADY){
            //application cache的版本號發生改年
              window.applicationCache.swapCache();
              window.location.reload();
            }else {
              console.log("manifest沒有更改");
            }
        },false);
      },false);
      </script>
      </body>

    </html>

注意:

  • app cache會自動地將本頁當做一個靜態頁快取;

  • 如果你要更新,請更新server端的manifest檔案的版本;

  • 如果你不想啟用app cache,或者說現在app cache不適合你現在的應用,那麼有一個做法:

更改server端上manifest檔案的名稱,例如cache1.appcache,這個時候再去重新整理瀏覽器,首先,瀏覽器還是會從app cache快取中讀取快取,到第二次重新整理的時候,瀏覽器會到server端查詢manifest檔案,發現這個檔案不存在,那麼瀏覽器會走網路從Server上重新拉取檔案;

app cache優勢:

  • 完全離線
  • 資源快取,載入更快
  • 降低伺服器負載

app cache缺陷:

  • 含有manifest屬性的當前請求頁無論如何都會被快取;
  • 更新需要建立在manifest檔案的更新,檔案更新後是需要頁面再次重新整理的,並且在第2次重新整理才能獲取新資源;
  • 更新是全域性性的,無法單獨更新某個檔案;
  • 對於連結的引數變化的敏感的,任何一個引數的修改都會被重新快取,例如:index.html和index.html?v=1會被認為是不同檔案,分別快取;

app cache適用場景

  • 單地址頁面
  • 對實時性要求不要的業務
  • 離線web應用

總結

在實際應用中,我們需要根據業務的需要來採取相應的快取措施,如上所述,html5的幾種快取都有各自的優缺點和適用場景,有時我們也需要組合使用。

關於HTML5快取我們就介紹到這裡。

參考