本地存儲Cookie、Storage、indexDB、ServiceWork離線訪問網站

我的博客css

在平常開發中,cookie、Session/Local,對後兩種運用的較少。
html

cookie

cookie是客戶端的解決方案,最先是網景公司的前僱員Lou Montulli在1993年3月發明的。衆所周知,HTTP是一個無狀態的協議,客戶端發起一次請求,服務器處理來自客戶端的請求,而後給客戶端回送一條響應。在客戶端與服務器數據交換完畢後,服務器端和客戶端的鏈接就會被關閉,Web服務器幾乎沒有什麼信息能夠判斷是哪一個用戶發送的請求,也沒法記錄來訪用戶的請求序列,每次交換數據都須要創建新的鏈接,後來有了用戶,網站想要去了解用戶的需求,可是根據當時場景顯然沒法知足業務需求,cookie便孕育而出,它能夠彌補HTTP協議無狀態的部分不足。前端


咱們先來看一下在平常操做中是怎麼接收到這個cookie的。vue


開始以前,將本地cookie的值所有清除。nginx


輸入'掘金',百度監聽了focusinput事件,因此第一次觸發了2次請求,奇怪的是第一次沒有Set-cookie返回:git



第一次觸發的是focus(),這時候Request Header請求頭是沒有任何Cookie,查詢字段wd沒有參數,歷史記錄Hisdata讀取的是LocalStorage裏客戶端的歷史查詢與時間戳。看第二次請求github

服務器傳回了7條Set-Cooie,客戶端拿到數據後,進行儲存web


這時候能夠看到Cookie中有服務器返回的全部cookie以及相對應的值和其餘配置;chrome

每次客戶端第一次請求,服務器接收到請求後爲該客戶端設置cookie,服務器端向客戶端發送Cookie是經過HTTP響應報文實現的,在Set-Cookie中設置須要向客戶端發送的cookie,瀏覽器接收到響應緩存cookie到內存或者硬盤(視瀏覽器機制),以後每次發送請求都會攜帶上cookie數據庫


cookie格式以下:  

接收到的cookie格式

Set-cookie: name=value [; expires=date] [; path=path] [; domain=domain] [;secure=secure]

發送的cookie格式

Cookie: name1=value1 [; name2=value2]

  • name:一個惟一肯定的cookie名稱。一般來說cookie的名稱是不區分大小寫的。 
  • value:存儲在cookie中的字符串值。最好爲cookie的name和value進行url編碼 
  • domain:cookie對於哪一個域名下是有效的。全部向該域發送的請求中都會包含這個cookie信息。這個值能夠包含子域(如:m.baidu.com),也能夠不包含它(如:.baidu.com,則對於baidu.com的全部子域都有效). 
  • path: 表示這個cookie影響到的路徑,瀏覽器跟會根據這項配置,向指定域中匹配的路徑發送cookie。
  • expires:過時時間,表示cookie自刪除的時間戳。若是不設置這個時間戳,cookie就會變成會話Session類型的cookie,瀏覽器會在頁面關閉時即將刪除全部cookie,這個值是GMT時間格式,若是客戶端和服務器端時間不一致,使用expires就會存在誤差。 
  • max-age: 與expires做用相同,用來告訴瀏覽器此cookie多久過時(單位是秒),而不是一個固定的時間點。正常狀況下,max-age的優先級高於expires。 
  • HttpOnly: 告知瀏覽器不容許經過腳本document.cookie去更改這個值,一樣這個值在document.cookie中也不可見。但在http請求仍然會攜帶這個cookie。注意這個值雖然在腳本中不可獲取,但仍然在瀏覽器安裝目錄中以文件形式存在。這項設置一般在服務器端設置。 
  • secure: 安全標誌,指定後只有在使用SSL(https)連接時候纔會發送到服務器,若是是http連接則不會傳遞該值。可是也有其餘方法能在本地查看到cookie

咱們能夠手動設置一下cookie的返回,

var http = require('http');
var fs = require('fs');

http.createServer(function(request, responed) {
    responed.setHeader('status', '200 OK');
    responed.setHeader('Set-Cookie', 'userStatus=true;domain=.juejin.com;path=/;max-age=1000');
    responed.write('掘金');
    responed.end();
}).listen(8888);
複製代碼

而後啓動服務器,訪問的時候就能看到服務器返回

Set-Cookie:userStatus=true;domain=.juejin.com;path=/;max-age=1000    複製代碼

Cookie的優勢

  • cookie鍵值對形式,結構簡單
  • 能夠配置過時時間,不須要任何服務器資源存在於客戶端上,
  • 能夠彌補HTTP協議無狀態的部分不足
  • 無兼容性問題。

Cookie的缺點

  • 大小數量受到限制,每一個domain最多隻能有20條cookie,每一個cookie長度不能超過4096 字節,不然會被截掉。儘管在當今新的瀏覽器和客戶端設備開始支持8192字節。
  • 用戶配置可能爲禁用 有些用戶禁用了瀏覽器或客戶端設備接收 Cookie 的能力,所以限制了這一功能。
  • 增長流量消耗,每次請求都須要帶上cookie信息。
  • 安全風險,黑客能夠進行Cookie攔截、XSS跨站腳本攻擊和Cookie欺騙,歷史上由於Cookie被攻擊的網站用戶不在少數,雖然能夠對Cookie進行加密解密,但會影響到性能。

Cookie總結

在業務開發場景中,Cookie更多的是做爲一種標識,用來記錄用戶的行爲,而並不是用戶的身份信息,根據用戶登陸後生成特定Cookie,再次發起其餘請求的時候,服務器可以識別用戶是否可以繼續這次操做,不能則相應操做。若是將用戶的身份信息及其餘重要信息放在cookie中是十分危險的,並且對於大型流量網站來講,每個字節的消耗一年下來都是按幾十TB算的,

以谷歌爲例:
google的流量,佔到整個互聯網的40%2016年全球網路流量達到1.3ZB1ZB = 10^9TB),那麼google2016年的流量就是1.3ZB*40%,若是google1MB請求減小一個字節,每一年能夠節省近500TB。

Session/Local

SessionStorage簡稱會話存儲,和LocalStorage本地儲存是前端如今最爲普遍也是操做最簡單的本地存儲,都遵循同源策略(域名、協議、端口),存放空間很大,通常是5M,極大的解決了以前只能用Cookie來存儲數據的容量小、存取不便、容易被清除的問題。這個功能爲客戶端提供了極大的靈活性。

並不是是隻支持IE8以上,能夠經過MDN官方的方法,變相的存儲在Cookie內,不過這樣的話有失Storage對象存在的意義了。

Session只做用於當前窗口,不能跨窗口讀取其餘窗口的SessionStorage數據庫信息,瀏覽器每次新建、關閉都是直接致使當前窗口的數據庫新建和銷燬。兼容性以下:


Local做用於當前瀏覽器,即便你開2個Chrome瀏覽器(不是2個窗口)也仍是共用一個路徑地址。永遠不會自動刪除,因此若是咱們要用LocalStorage保存敏感重要信息的時候也要注意不要放在Local裏,而是放在Session裏,關閉後進行清除,不然攻擊者能夠經過XSS攻擊進行信息竊取。兼容性以下:


二者在PC端僅僅Chrome和Firefox有些許兼容誤差,移動端兼容性相同。


PS:當瀏覽器進入隱私瀏覽模式,會建立一個新的、臨時的數據庫來存儲local storage的數據;當關閉隱私瀏覽模式時,該數據庫將被清空並丟棄。

只是API名字不一樣。

localStorage.setItem('key', 'value');  // 設置
localStorage.getItem('key');  // 獲取
localStorage.removeItem('key'); // 刪除
localStorage.clear(); //清除全部複製代碼

不過在儲存和讀取數據的時候,須要將數據進行JSON.stringify和JSON.parse,不然會強制改變數據類型

var obj = {
    name:'掘金',
    url:'juejin.com'
    }
var arr = [1,2,3]

//錯誤方法
localStorage.setItem('object',obj);  //object:"[object Object]" 沒法讀取
localStorage.setItem('array',arr);  //array:"1,2,3" 變成string格式

//正確方法:Object
localStorage.setItem('object',JSON.stringify(obj));//存儲 object:"{"name":"掘金","url":"juejin.com"}"
JSON.parse(localStorage.getItem('object'));//讀取 {name: "掘金", url: "juejin.com"}

//正確方法:Array
localStorage.setItem('array',JSON.stringify(arr));  //存儲 array:"[1,2,3]"
JSON.parse(localStorage.getItem('array'));//讀取  [1,2,3]複製代碼
複製代碼

Session/Local優勢

  • 存儲數據量大,5MB。
  • 不會隨http請求一塊兒發送,有效的減小了請求大小
  • local跨窗口處理數據,可以減小至關一部分本地處理與維護狀態。

Session/Local缺點

  • 本質是在讀寫文件,寫入數據量大的話會影響性能(firefox是將localstorage寫入內存中的)
  • XSS攻擊竊取信息(爲了安全性仍是放session吧)
  • 兼容性,雖說IE6已經死了,可是我就看過好多掘金段友還在寫兼容IE6的文章....真是sun了dog,若是大家項目還在寫IE6兼容,我敬你是條漢子!
  • 不能被爬蟲讀取

Session與Local總結

原本是用來作cookie的解決方案,適合於作小規模的簡單結構數據儲存與狀態維護,不適宜儲存敏感信息。能夠運用在全部業務場景下。

IndexedDB

IndexedDB是HTML5規範裏新出現的瀏覽器裏內置的數據庫。跟NoSQL很像,提供了相似數據庫風格的數據存儲和使用方式。但IndexedDB裏的數據是永久保存,適合於儲存大量結構化數據,有些數據本應該存在服務器,可是經過indexedDB,能夠減輕服務器的大量負擔,實現本地讀取修改使用,以對象的形式存儲,每一個對象都有一個key值索引。

IndexedDB裏的操做都是事務性的。一種對象存儲在一個object store裏,object store就至關於關係數據庫裏的表。IndexedDB能夠有不少object store,object store裏能夠有不少對象。

首先來看兼容性


Window自帶瀏覽器也只是部分支持,window8.1及以上才支持IE11。

首先,咱們建立一個數據庫

//首先定義一個本身的版本
var my = {
    name:'juejin',
    version:'1',
    db:null
}
//打開倉庫,沒有則建立
var request = window.indexedDB.open(my.name);
//window.indexedDB.open()的第二個參數即爲版本號。在不指定的狀況下,默認版本號爲1.
//版本號不能是一個小數,不然會被轉化成最近的整數。同時可能致使不會觸發onupgradeneeded版本更新回調
console.log(request);複製代碼

返回的是一個名字爲IDBOpenDBRequest的對象。


裏面有各個狀態的回調參數,初始化的時候都是null,須要手動去掛載自定義的回調參數,從而實現window.indexedDB.open函數的狀態回調控制,再去控制檯Appliation的indexedDB查看


咱們已經成功添加該名稱爲juejin的db對象了,security origin表示安全起源(我是在我的博客控制檯進行建立的)

request.onerror = function(event){ //打開失敗回調
    console.log(`${my.name} open indexedDB is Fail`);
}
request.onsuccess = function(event){ //打開成功回調
    console.warn(`${my.name} open indexedDB is success`);
    //將返回的值賦給本身控制的db版本對象,下面兩種方法都能接收到。
    my.db = event.target.result|| request.result;
}
request.onupgradeneeded = function (event) {//版本變化回調參數,第一次設置版本號也會觸發
    console.log('indexDB version change');
}
console.log(my.db);複製代碼

返回的是一個db對象,裏面包含後續對該db對象狀態控制的回調方法。這些方法仍然須要本身定義。


怎麼關閉db對象和刪除db對象呢?關閉和刪除是兩個概念。

//關閉db對象,以後沒法對其進行插入、刪除操做。
my.db.close();

//而刪除方法則掛載在window.indexedDB下,刪除該db倉庫
window.indexedDB.deleteDatabase(my.db);複製代碼

這裏須要注意的一點是此onclose()方法非上面代碼調用的close()方法,my.db.close()調用的是__proto__原型內的方法。

知道如何創建和操做indexedDB以後,咱們對object store進行添加表的操做。上文咱們說到,indexedDB中沒有表的概念,而是object store,一個數據庫中能夠包含多個object store,object store是一個靈活的數據結構,能夠存放多種類型數據。也就是說一個object store至關於一張表,裏面存儲的每條數據和一個鍵相關聯。

咱們可使用每條記錄中的某個指定字段做爲鍵值(keyPath),也可使用自動生成的遞增數字做爲鍵值(keyGenerator),也能夠不指定。選擇鍵的類型不一樣,objectStore能夠存儲的數據結構也有差別。

建立object store對象只能從onupgradeneeded版本變化回調中進行。

//建立object store對象
request.onupgradeneeded = function() {    
    var db = request.result;    
    var objectStore = db.createObjectStore("LOL", {keyPath: "isbn"});    
    var titleIndex = objectStore.createIndex("by_hero", "hero", {unique: true});    
    var authorIndex = objectStore.createIndex("by_author", "author");
    objectStore.put({title: "亞索", author: "Roit", isbn: 123456});    
    objectStore.put({title: "提莫", author: "Roit", isbn: 234567});    
    objectStore.put({title: "諾手", author: "Hang", isbn: 345678});
};複製代碼

createObjectStore方法有2個參數,第一個表示該object store表的名稱,第二個是對象,keyPath爲存儲對象的某個屬性(做爲key值),options還有個參數:autoIncrement表明是否自增。接下來創建索引

var titleIndex = objectStore.createIndex("by_title", "title", {unique: true});    
var authorIndex = objectStore.createIndex("by_author", "author");複製代碼

  第一個參數是索引的名稱,第二個參數指定了根據存儲數據的哪個屬性來構建索引,第三個options對象,其中屬性unique的值爲true表示不容許索引值相等。第二個索引沒有options對象,接下來咱們能夠經過put方法添加數據了。

objectStore.put({hero: "亞索", author: "Roit", isbn: 123456});    
objectStore.put({hero: "提莫", author: "Roit", isbn: 234567});    
objectStore.put({hero: "諾手", author: "Hang", isbn: 345678});複製代碼

總體代碼寫上

var my = {//定義控制版本       
    name:'juejin',       
    version:'1',       
    db:null     
};
var request = window.indexedDB.open(my.name);  //建立打開倉庫     
request.onupgradeneeded = function() {//更新版本回調    
    var db = request.result;    
    var objectStore = db.createObjectStore("LOL", {keyPath: "isbn"});    
    var heroIndex = objectStore.createIndex("by_hero", "hero", {unique: true});    
    var authorIndex = objectStore.createIndex("by_author", "author");
    objectStore.put({hero: "亞索", author: "Roit", isbn: 123456});        
    objectStore.put({hero: "提莫", author: "Roit", isbn: 234567});        
    objectStore.put({hero: "諾手", author: "Hang", isbn: 345678});
};
request.onsuccess = function() {//成功回調    
    my.db = event.target.result|| request.result;    
    console.warn(`${my.name} indexedDB is success open Version ${my.version}`);
};
request.onerror = function() {//失敗回調    
    console.warn(`${my.name} indexedDB is fail open  Version ${my.version}`);
};
複製代碼

 注意只有在運行環境下才會進行一個存儲,本地打開靜態文件是不會儲存indexedDB的,雖然能彈出juejin indexedDB is success open。這樣咱們就成功建立了一個object store,咱們到控制檯去看下



by_hero表示在建立索引的時候,經過createObjectStore('by_hero','hero',{unique:true})的時候,經過key值爲hero的對象,進行索引篩選的數據。再去by_author看下,


同理,經過key值爲author的,進行索引的數據。這樣就可以儲存大量結構化數據。而且擁有索引能力,這一點比Storage強。固然,api也麻煩。接來下進行事務操做。

IndexedDB中,使用事務來進行數據庫的操做。事務有三個模式,默認只讀

  • readOnly只讀。
  • readwrite讀寫。
  • versionchange數據庫版本變化
//首先要建立一個事務,
var transaction = my.db.transaction('LOL', 'readwrite');
//獲取objectStore數據
var targetObjectStore = transaction.objectStore('LOL');
//對預先設置的keyPath:isbn進行獲取
var obj = targetObjectStore.get(345678);
//若是獲取成功,執行回調
obj.onsuccess = function(e){    
    console.log('數據成功獲取'+e.target.result)
}
//獲取失敗obj.onerror = function(e){    
    console.error('獲取失敗:'+e.target.result)
}複製代碼


獲取成功,拿到isbn爲345678的數據。

第一個參數爲須要關聯的object store名稱,第二個參數爲事務模式,選擇可讀可寫,與indexedDB同樣,調用成功後也會觸發onsuccess、onerror回調方法。能夠讀取了咱們嘗試去添加

targetObjectStore.add({hero: "蓋倫", author: "Yuan", isbn: 163632});        
targetObjectStore.add({hero: "德邦", author: "Dema", isbn: 131245});        
targetObjectStore.add({hero: "皇子", author: "King", isbn: 435112});複製代碼

有一點要注意,添加劇複數據會更新。添加完畢後,去控制檯看下


不對啊,確定有的,刷新無數遍後,終於找到了解決辦法。這多是Chrome的一個BUG吧。


刪除數據

//獲取數據是get,刪除數據是delete
targetObjectStore.delete(345678);
複製代碼


一樣 ,須要輸入篩選數據纔會觸發刷新,不過在平常中已經足夠咱們使用。

更新數據

var obj = targetObjectStore.get(123456);
//若是獲取成功,執行回調
obj.onsuccess = function(e){    
    console.log('數據成功獲取'+e.target.result);
    var data = e.target.result;
    data.hero = '亞索踩蘑菇掛了';
    //再put回去
    var updata = targetObjectStore.put(data);
    updata.onsuccess = function(event){
        console.log('更新數據成功'+event.target.result);
    }
}複製代碼


又要篩選一遍.

當你須要便利整個存儲空間中的數據時,你就須要使用到遊標。遊標使用方法以下:

var request = window.indexedDB.open('juejin');

request.onsuccess = function (event) {
    var db = event.target.result;
    var transaction = db.transaction('LOL', 'readwrite');
    //獲取object store數據
    var objectStore = transaction.objectStore('LOL');
    //獲取該數據的浮標
    var eachData = objectStore.openCursor();
        //openCursor有2個參數(遍歷範圍,遍歷順序)
    eachData.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor){    
            console.log(cursor);
            cursor.continue();
        }
    };

    eachData.onerror = function (event) {
        consoe.error('each all data fail reason:'+event.target.result);
    };
}複製代碼

這樣經過openCursor獲得的數據就相似於forEach輸出,當表中無數據,仍會書法一次onsuccess回調

上面提到openCursor的兩個參數,第一個是遍歷範圍,由indexedDB的 :IDBKeyRange的API進行實現,主要有如下幾個值

//區間向上匹配,第一個參數指定邊界值,第二個參數是否包含邊界值,默認false包含。
lowerBound('邊界值',Boolean);
var index = IDBKeyRange.lowerBound(1);//匹配key>=1
var index = IDBKeyRange.lowerBound(1,true);//匹配key>1

//單一匹配,指定參數值
only('值');
var index = IDBKeyRange.only(1);//匹配key===1;

//區間向下搜索,第一個參數指定邊界值,第二個參數是否包含邊界值,默認false包含。
upperBound('邊界值',Boolean);
var index = IDBKeyRange.upperBound(2);//匹配key<=2
var index = IDBKeyRange.upperBound(2,true);//匹配key<2

//區間搜索,第一個參數指定開始邊界值,第二個參數結束邊界值,
//        第三個指定開始邊界值是否包含邊界值,默認false包含。第四個指定結束邊界值是否包含邊界值,默認false
bound('邊界值',Boolean);
var index = IDBKeyRange.bound(1,10,true,false);//匹配key>1&&key<=10;複製代碼

openCursor第二個參數,遍歷順序,指定遊標遍歷時的順序和處理相同id(keyPath屬性指定字段)重複時的處理方法。改範圍經過特定的字符串來獲取。其中:

  • IDBCursor.next,從前日後獲取全部數據(包括重複數據)
  • IDBCursor.prev,從後往前獲取全部數據(包括重複數據)
  • IDBCursor.nextunique,從前日後獲取數據(重複數據只取第一條)
  • IDBCursor.prevunique,從後往前獲取數據(重複數據只取第一條)

咱們來試一下

var request = window.indexedDB.open('juejin');
request.onsuccess = function (event) {
    var db = event.target.result;
    var transaction = db.transaction('LOL', 'readwrite');
    //獲取object store數據
    var objectStore = transaction.objectStore('LOL');
    //bound('邊界值',Boolean);匹配key>22000&&key<=400000;
    var index = IDBKeyRange.bound(220000,400000,true,false);
    //獲取該數據的浮標,從前日後順序索引,包括重複數據
    var eachData = objectStore.openCursor(index,IDBCursor.NEXT);
    eachData.onsuccess = function (event) {
        var cursor = event.target.result;
        console.log(cursor);
        if (cursor) cursor.continue();
    };
    eachData.onerror = function (event) {
        consoe.error('each all data fail reason:'+event.target.result);
    };
}
複製代碼

搜索key值爲220000到40000萬之間的數據,搜索出一條。


好了,indexedDB基本和事務操做講的差很少了,如今說說它另外一方面:

目前爲止,所知道在IndexedDB中,鍵值對中的key值能夠接受如下幾種類型的值:

  • Number
  • String
  • Array
  • Object
  • Binary二進制

可是儲存數據千萬要注意的一點是,若是儲存了isbn相同的數據,是無效操做,甚至可能引發報錯。

keyPath可以接受的數據格式,示例中createObjectStore時設置的{KeyPath:'isbn'},爲主鍵

  • Blob
  • File
  • Array
  • String

至於value幾乎能接受全部數據格式。

indexedDB優勢

  • 替代web SQL,與service work搭配簡直無敵,實現離線訪問不在話下,
  • 數據儲存量無限大(只要你硬盤夠),Chrome規定了最多隻佔硬盤可用空間的1/3,能夠儲存結構化數據帶來的好處是能夠節省服務器的開支。

indexedDB缺點

  • 兼容性問題,只有ie11以上,根據業務場景慎重考慮需求。
  • 同源策略,部分瀏覽器如Safari手機版隱私模式在訪問IndexedDB時,可能會出現因爲沒有權限而致使的異常(LocalStorage也會),須要進行異常處理。
  • API相似SQL比較複雜,操做大量數據的時候,可能存在性能上的消耗。
  • 用戶在清除瀏覽器緩存時,可能會清除IndexedDB中相關的數據。

ServiceWork

什麼是ServiceWork?serviceWork是W3C 2014年提出的草案,是一種獨立於當前頁面在後臺運行的腳本。這裏的後臺指的是瀏覽器後臺,可以讓web app擁有和native app同樣的離線程序訪問能力,讓用戶可以進行離線體驗,消息推送體驗。service worker是一段腳本,與web worker同樣,也是在後臺運行。做爲一個獨立的線程,運行環境與普通腳本不一樣,因此不能直接參與web交互行爲。native app能夠作到離線使用、消息推送、後臺自動更新,service worker的出現是正是爲了使得web app也能夠具備相似的能力。

ServiceWork產生的意義

打開了如今瀏覽器單線程的革面,隨着前端性能愈來愈強,要求愈來愈高,咱們都知道在瀏覽器中,JavaScript是單線程執行的,若是涉及到大量運算的話,頗有可能阻礙css tree的渲染,從而阻塞後續代碼的執行運算速度,ServiceWork的出現正好解決了這個問題,將一些須要大規模數據運算和獲取  資源文件在後臺進行處理,而後將結果返回到主線程,由主線程來執行渲染,這樣能夠避免主線程被巨量的邏輯和運算所阻塞。這樣的大大的提高了JavaScript線程在處理大規模運算時候的一個能力, 這也是ServiceWork自己的巨大優點,好比咱們要進行WebGBL場景下3D模型和數據的運算,一個普通的數據可能高達幾MB,若是放在主線程進行運算的話,會嚴重阻礙頁面的渲染,這個時候就很是適合ServiceWork進行後臺計算,再將結果返回到主線程進行渲染。

ServiceWork的功能

service worker能夠:

  1. 消息推送、傳遞
  2. 在不影響頁面通訊以及阻塞線程的狀況下,後臺同步運算。
  3. 網絡攔截、代理,轉發請求,僞造響應
  4. 離線緩存
  5. 地理圍欄定位

說了這麼多,到底跟咱們實際工做中有什麼用處呢,這裏就要介紹google 的PWD(Progressive Web Apps),它是一種Web App新模型,漸進式的web App,它依賴於Service Work,是如今沒有網絡的環境中也可以提供基本的頁面訪問,不會出現‘未鏈接到互聯網’,能夠優化網頁渲染及網絡數據訪問,而且能夠添加到手機桌面,和普通應用同樣有全屏狀態和消息推送的功能。



這是Service Work的生命週期,首先沒有Service Work的狀況下會進行一個安裝中的狀態,返回一個promise實例,reject的會走到Error這一步,resolve安裝成功,當安裝完成後,進入Activated激活狀態,在這一階段,你還能夠升級一個service worker的版本,具體內容咱們會在後面講到。在激活以後,service worker將接管全部在本身管轄域範圍內的頁面,可是若是一個頁面是剛剛註冊了service worker,那麼它這一次不會被接管,到下一次加載頁面的時候,service worker纔會生效。當service worker接管了頁面以後,它可能有兩種狀態:要麼被終止以節省內存,要麼會處理fetch(攔截和發出網絡請求)和message(信息傳遞)事件,這兩個事件分別是頁面初始化的時候產生了一個網絡請求出現或者頁面上發送了一個消息。

目前有哪些頁面支持service Work呢?

在Chrome瀏覽器地址欄輸入chrome://inspect/#service-workers,能夠看到目前爲止你訪問過全部支持service work的網站

你也能夠打開控制檯,到Application,點擊serviceWork這一欄,

那怎麼才能體驗到service Work呢,咱們以Vue官網爲例,首先打開https://cn.vuejs.org/,等待加載完成,如今關掉你的WiFi和全部能連上互聯網的工具。再刷新地址欄頁面



是否是感受很新奇,怎麼作到呢,繼續往下看。須要運行本地環境,各類方法自行百度,我使用的是本身購買的騰訊雲服務器,nginx多開幾個端口,SFTP自動上傳。也能夠搭建本地localhost,切記不能夠用IP地址,ServiceWork不支持域名爲IP的網站,作好這些咱們開始。

首先建立一個文件夾,再建立index.htmlindex.cssapp.jsservicework.js這些文件咱們後續都要用到。

index.html

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <meta http-equiv="X-UA-Compatible" content="ie=edge">  
    <title>ServiceWork</title>
</head>
<body>
    <h1>ServiceWork</h1>
</body>
<script src="./app.js"></script>
<link rel="stylesheet" href="./index.css">
</html>複製代碼

引入了一個main.css文件和一個app.js

main.css

h1{  
    color:red;
    text-align:center;
}複製代碼

app.js

alert(1);複製代碼

測試成功彈出alert(1)後,咱們開始寫代碼。

首先要肯定是否支持ServiceWork

app.js

if (navigator.serviceWorker) {   
//先注入註冊文件,第二個參數爲做用域,爲當前目錄    
navigator.serviceWorker.register('./servicework.js', {
        scope: './'    
    }).then(function (reg) {
        console.warn('install ServiceWork success',reg)    
    }).catch(function (err) {        
        console.error(err)    
    })
} else {    
    //不支持serviceWork操做
}複製代碼

導入註冊配置文件,返回一個promise,觸發相應回調,而後再去修改servicework.js文件,

//self是serviceWorker的實例。
console.log(self)//
給實例監聽安裝事件,成功觸發回調self.addEventListener('install', function (e) {
    //ExtendableEvent.waitUntil()擴展事件的生命週期。    
    e.waitUntil(        
    //咱們經過打開名稱爲'app-v1'的緩存,將讀取到的文件儲存到cache裏
        caches.open('app-v1').then(function (cache) {
           console.log('caches staticFile success');
           //添加cache
           return cache.addAll([
               './app.js',
               './servicework.html',
               './servicework.js',
               './index.css'
           ]);
         })
    );
 });
複製代碼

ExtendableEvent.waitUntil()接受一個promise對象,它能夠擴展時間的生命週期,延長事件的壽命從而阻止瀏覽器在事件中的異步操做完成以前終止服務工做線程。它能夠擴展時間的生命週期,延長事件的壽命從而阻止瀏覽器在事件中的異步操做完成以前終止服務工做線程。


install的時候,它會延遲將安裝的works視爲installing,直到傳遞的Promise被成功地resolve。確保服務工做線程在全部依賴的核心cache被緩存以前都不會被安裝。

一旦 Service Worker 成功安裝,它將轉換到Activation階段。若是之前的 Service Worker 還在服務着任何打開的頁面,則新的 Service Worker 進入 waiting 狀態。新的 Service Worker 僅在舊的 Service Worker 沒有任何頁面被加載時激活。這確保了在任什麼時候間內只有一個版本的 Service Worker 正在運行。當進行

activited
的時候,它延遲將 active work視爲已激活的,直到傳遞的 Promise被成功地 resolve。確保功能時間不會被分派到 ServiceWorkerGlobalScope 對象。


更詳細的能夠到 MDN查看該API更多說明。

成功丟到緩存裏後,就可使用fetch進行網絡攔截了。


//一樣的方法,監聽fetch事件, 
self.addEventListener('fetch', function (event) {
    //respondWith方法產生一個request,response。
    event.respondWith(
      //利用match方法對event.request所請求的文件進行查詢
      caches.match(event.request).then(
        function (res) {
          console.log(res, event.request);
          //若是cache中有該文件就返回。
          if (res) {
            return res
          } else {
            //沒有找到緩存的文件,再去經過fetch()請求資源
            fetch(res.url).then(function (res) {
              if (res) {
                if (!res || res.status !== 200 || res.type !== 'basic') {
                  return res;
                }
                //再將請求到的數據丟到cache緩存中..
                var fetchRequest = event.request.clone();
                var fileClone = res.clone();
                caches.open('app-v1')
                  .then(function (cache) {
                    cache.put(event.request, fileClone);
                  });
              } else {
                //沒有請求到該文件,報錯處理
                console.error('file not found:' + event.reuqest + '==>' + res.url)
              }
            })
          }
        }
      )
    );
  });複製代碼

對於前端你們確定很熟悉requestresponse表明着什麼,event.respondWith()會根據當前控制的頁面產生一個requestrequest再去生成自定義的responsenetwork error 或者 Fetch的方式resolve


fetch()對網絡進行攔截,代理請求,先讀取本地文件,沒有資源再去請求,很大程度的節約了網絡請求消耗。

如今咱們去試試有沒有成功!


啊哈,漂亮!這樣就實現了離線訪問,可是在實際項目中,儘可能不要緩存servicework.js文件,可能沒法及時生效,進行後續修改。咱們去控制檯看下

已經安裝好了,而且在運行中。

總體大概的流程以下

service work.js 的更新

Service Work.js 的更新不只僅只是簡單的更新,爲了用戶可靠性體驗,裏面仍是有不少門道的。

  • 首先更新ServiceWork.js 文件,這是最主要的。只有更新 ServiceWork.js 文件以後,以後的流程才能觸發。ServiceWork.js 的更新也很簡單,直接改動 ServiceWork.js 文件便可。瀏覽器會自動檢查差別性(就算只有 1B 的差別也行),而後進行獲取。
  • 新的 ServiceWork.js 文件開始下載,而且 install 事件被觸發
  • 此時,舊的 ServiceWork 還在工做,新的 ServiceWork 進入 waiting 狀態。注意,此時並不存在替換
  • 如今,兩個 ServiceWork 同時存在,不過仍是之前的 ServiceWork 在掌管當前網頁。只有當 old service work 不工做,即,被 terminated 後,新的 ServiceWork 纔會發生做用。具體行爲就是,該網頁被關閉一段時間,或者手動的清除 service worker。而後,新的 Service Work 就度過可 waiting 的狀態。
  • 一旦新的 Service Work 接管,則會觸發 activate 事件。

整個流程圖爲:

                

一個版本的緩存建立好以後,咱們也能夠設置多個緩存,那怎去刪除不在白名單中的緩存呢

self.addEventListener('activate', function(event) {
    //上個版本,咱們使用的是'app-v1'的緩存,因此就須要進行清除,進行'app-v2'版本的緩存儲存
  var cacheWhitelist = ['app-v1'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});複製代碼

若是 new service work要當即生效呢,那就要用到skipWaiting,install 階段使用 self.skipWaiting(); ,由於上面說到 new Service Work 加載後會觸發 install 而後進入 waiting 狀態。那麼,咱們能夠直接在 install 階段跳過等待,直接讓 new Service Work 進行接管。

self.addEventListener('install',function(event) {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});複製代碼

上面的 service work 更新都是在一開始加載的時候進行的,那麼,若是用戶須要長時間停留在你的網頁上,有沒有什麼手段,在間隔時間檢查更新呢?

有的,可使用 registration.update() 來完成。

navigator.serviceWorker.register('./ServiceWork.js').then(function(reg){
  // sometime later…
  reg.update();
});複製代碼

另外,若是你一旦用到了 ServiceWork.js 而且肯定路由以後,請千萬不要在更改路徑了,由於,瀏覽器判斷 ServiceWork.js 是否更新,是根據 ServiceWork.js 的路徑來的。若是你修改的 ServiceWork.js 路徑,而之前的 ServiceWork.js 還在做用,那麼你新的 ServiceWork 永遠沒法工做。除非你手動啓用 update 進行更新。

你想要一個文件更新,只須要在 ServiceWork 的 fetch階段使用 caches 進行緩存便可。一開始咱們的 install 階段的代碼爲:

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('app-v1').then(function(cache) {
      return cache.addAll([
        './app.js',
        './servicework.html',
        './index.css'
      ]);
    })
  );
});


複製代碼

self.addEventListener('install', function (event) {
      var now = Date.now();
      // 事先設置好須要進行更新的文件路徑
      var urlsToPrefetch = [
        './index.css',
        './servicework.html'
      ];
      event.waitUntil(
        caches.open(CURRENT_CACHES.prefetch).then(function (cache) {
          var cachePromises = urlsToPrefetch.map(function (urlToPrefetch) {
            // 使用 url 對象進行路由拼接
            var url = new URL(urlToPrefetch, location.href);
            url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
            // 建立 request 對象進行流量的獲取
             var request = new Request(url, {
              mode: 'no-cors'
            });
            // 手動發送請求,用來進行文件的更新
            return fetch(request).then(function (response) {
              if (response.status >= 400) {
                // 解決請求失敗時的狀況
                     throw new Error('request for ' + urlToPrefetch +
                  ' failed with status ' + response.statusText);
              }
             // 將成功後的 response 流,存放在 caches 套件中,完成指定文件的更新。
              return cache.put(urlToPrefetch, response);
            }).catch(function (error) {
              console.error('Not caching ' + urlToPrefetch + ' due to ' + error);
            });
          });
          return Promise.all(cachePromises).then(function () {
            console.log('Pre-fetching complete.');
          });
        }).catch(function (error) {
             console.error('Pre-fetching failed:', error);
         })
        );
});複製代碼

傳送門Github查看該段代碼。


當成功獲取到緩存以後, ServiceWork 並不會直接進行替換,他會等到用戶下一次刷新頁面事後,使用新的緩存文件。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('app-v1').then(function(cache) {
      return cache.match(event.request).then(function(response) {
        var fetchPromise = fetch(event.request).then(function(res) {
          cache.put(event.request, res.clone());
          return res;
        })
        return response || fetchPromise;
      })
    })
  );
});複製代碼

               

更詳細的其餘方法運用能夠參考這篇文章.

本文只講本地緩存,以後會將地理圍欄、消息推送相關信息更新在博客,有興趣的朋友能夠收藏本文章,更具體規範的代碼內容能夠到這查看

service work(PWA)缺點:

  •     緩存的問題,要按期清理。超出的時候會出現 Uncaught (in promise) DOMException: Quota exceeded. 異常。清理後必需要重啓瀏覽器才生效。
  •     瀏覽器兼容,頭疼的問題。IE和safari不兼容


優勢:

    如上文所述,有着消息推送、網絡攔截代理、後臺運算、離線緩存、地理圍欄等很實用的一些技術。

本文參考了不少大神的代碼,不喜勿噴,誠心學習請指教。

相關文章
相關標籤/搜索