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

ANFOUNNYSOUL發表於2018-06-11

在日常開發中,開發者用得最多的大概是前三種吧,cookie、Session/Local,對後兩種運用的較少,話不多少,直接正文:

cookie

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

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

我們先來看一下在日常操作中是怎麼接收到這個cookie的。

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

開始之前,將本地cookie的值全部清除。

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

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

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


第一次觸發的是focus(),這時候Request Header請求頭是沒有任何Cookie,查詢欄位wd沒有引數,歷史記錄Hisdata讀取的是LocalStorage裡客戶端的歷史查詢與時間戳。看第二次請求本地儲存Cookie、Storage、indexDB、ServiceWork離線訪問網站

伺服器傳回了7條Set-Cooie,客戶端拿到資料後,進行儲存

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

這時候可以看到Cookie中有伺服器返回的所有cookie以及相對應的值和其他配置;

每次客戶端第一次請求,伺服器接收到請求後為該客戶端設定cookie,伺服器端向客戶端傳送Cookie是通過HTTP響應報文實現的,在Set-Cookie中設定需要向客戶端傳送的cookie,瀏覽器接收到響應快取cookie到記憶體或者硬碟(視瀏覽器機制),之後每次傳送請求都會攜帶上cookie

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

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資料庫資訊,瀏覽器每次新建、關閉都是直接導致當前視窗的資料庫新建和銷燬。相容性如下:

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

Local作用於當前瀏覽器,即使你開2個Chrome瀏覽器(不是2個視窗)也還是共用一個路徑地址。永遠不會自動刪除,所以如果我們要用LocalStorage儲存敏感重要資訊的時候也要注意不要放在Local裡,而是放在Session裡,關閉後進行清除,否則攻擊者可以通過XSS攻擊進行資訊竊取。相容性如下:

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

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

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

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裡可以有很多物件。

首先來看相容性

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

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的物件。

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

裡面有各個狀態的回撥引數,初始化的時候都是null,需要手動去掛載自定義的回撥引數,從而實現window.indexedDB.open函式的狀態回撥控制,再去控制檯Appliation的indexedDB檢視

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

我們已經成功新增該名稱為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物件狀態控制的回撥方法。這些方法仍然需要自己定義。

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

怎麼關閉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,我們到控制檯去看下

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


by_hero表示在建立索引的時候,通過createObjectStore('by_hero','hero',{unique:true})的時候,通過key值為hero的物件,進行索引篩選的資料。再去by_author看下,

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

同理,通過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)
}複製程式碼

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

獲取成功,拿到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});複製程式碼

有一點要注意,新增重複資料會更新。新增完畢後,去控制檯看下

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

不對啊,肯定有的,重新整理無數遍後,終於找到了解決辦法。這可能是Chrome的一個BUG吧。

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

刪除資料

//獲取資料是get,刪除資料是delete
targetObjectStore.delete(345678);
複製程式碼

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

同樣 ,需要輸入篩選資料才會觸發重新整理,不過在日常中已經足夠我們使用。

更新資料

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);
    }
}複製程式碼

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

又要篩選一遍.

當你需要便利整個儲存空間中的資料時,你就需要使用到遊標。遊標使用方法如下:

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回撥本地儲存Cookie、Storage、indexDB、ServiceWork離線訪問網站

上面提到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萬之間的資料,搜尋出一條。

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

好了,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,是現在沒有網路的環境中也能夠提供基本的頁面訪問,不會出現‘未連線到網際網路’,可以優化網頁渲染及網路資料訪問,並且可以新增到手機桌面,和普通應用一樣有全屏狀態和訊息推送的功能。

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


這是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的網站本地儲存Cookie、Storage、indexDB、ServiceWork離線訪問網站

你也可以開啟控制檯,到Application,點選serviceWork這一欄,本地儲存Cookie、Storage、indexDB、ServiceWork離線訪問網站

那怎麼才能體驗到service Work呢,我們以Vue官網為例,首先開啟https://cn.vuejs.org/,等待載入完成,現在關掉你的WiFi和所有能連上網際網路的工具。再重新整理位址列頁面

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


是不是感覺很新奇,怎麼做到呢,繼續往下看。需要執行本地環境,各種方法自行百度,我使用的是自己購買的騰訊雲伺服器,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物件,它可以擴充套件時間的生命週期,延長事件的壽命從而阻止瀏覽器在事件中的非同步操作完成之前終止服務工作執行緒。它可以擴充套件時間的生命週期,延長事件的壽命從而阻止瀏覽器在事件中的非同步操作完成之前終止服務工作執行緒。

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

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

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

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

本地儲存Cookie、Storage、indexDB、ServiceWork離線訪問網站現在我們去試試有沒有成功!

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

啊哈,漂亮!這樣就實現了離線訪問,但是在實際專案中,儘量不要快取servicework.js檔案,可能無法及時生效,進行後續修改。我們去控制檯看下本地儲存Cookie、Storage、indexDB、ServiceWork離線訪問網站

已經安裝好了,並且在執行中。

整體大概的流程如下

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

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 事件。

整個流程圖為:

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

一個版本的快取建立好之後,我們也可以設定多個快取,那怎去刪除不在白名單中的快取呢

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;
      })
    })
  );
});複製程式碼

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

更詳細的其他方法運用可以參考這篇文章.

本文只講本地快取,之後會將地理圍欄、訊息推送相關資訊更新在部落格,有興趣的朋友可以收藏本文章,更具體規範的程式碼內容可以到這檢視

service work(PWA)缺點:

  •     快取的問題,要定期清理。超出的時候會出現 Uncaught (in promise) DOMException: Quota exceeded. 異常。清理後必須要重啟瀏覽器才生效。
  •     瀏覽器相容,頭疼的問題。IE和safari不相容

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

優點:

    如上文所述,有著訊息推送、網路攔截代理、後臺運算、離線快取、地理圍欄等很實用的一些技術。

本文參考了很多大神的程式碼,不喜勿噴,誠心學習請指教。



相關文章