【快取】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);
}
}
本地儲存使用場景
- 本地資料儲存,減少網路傳輸
- 在弱網路的環境下,會發生高延遲,低頻寬,應該儘量把資料(如指令碼、樣式)本地化;
我們來看一張圖,顯示的是本地儲存和網路拉取耗時的對比:
IndexedDB
概念
IndexedDB,是一種能做瀏覽器中持久地儲存結構化資料的資料庫,並且為web應用提供了豐富的查詢能力;
支援情況
chrome11+\opera不支援\firefox 4+\IE 10+,移動端瀏覽器支援能力弱
儲存結構
IndexedDB是按域名分配獨立空間,一個獨立域名下可以建立多個數據庫,每個資料庫可以建立對個物件儲存空間(表/table),一個物件儲存空間可以儲存多個物件資料;
如圖:
使用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
檢測是否線上;
原理
如圖:
解釋:
(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快取我們就介紹到這裡。