緩存一直以來都是用來提升性能的一項必不可少的技術 , 利用這項技術能夠很好地提升web的性能。 緩存能夠頗有效地下降網絡的時延,同時也會減小大量請求對於服務器的壓力。 接下來這篇文章將會詳細地介紹在web領域中緩存的一些知識點和應用。javascript
因爲整個網絡服務都是基於http協議 的,所以先來介紹一下HTTP協議當中定義的緩存機制。HTTP協議主要是經過請求頭當中的一些字段來和服務器進行通訊,從而採用不一樣的緩存策略。css
通常來講,對於一個完整的HTTP GET請求緩存過程會包含七個主要的步驟:①從接收網絡請求開始,②客戶端會讀取請求報文而且對報文進行解析, 進而提取URL和各類首部,③而後將會查詢是否在本地有副本,若是本地沒有副本就會從服務器上獲取一份副本而且保存在本地。④接着會進行查看副本是否足夠新鮮(新鮮度檢測), 若是緩存已經失效就會詢問服務器是否有任何更新,⑤服務器就會用新的首部和已緩存的主體來構建一條響應報文,⑥最後發送給客戶端。⑦根據服務器的不一樣,會可選地選擇建立日誌記錄該過程。html
具體的流程能夠看下面這張圖(該圖來自HTTP權威指南):前端
根據緩存處理方式的不一樣,接着又會分爲兩類:強緩存和協商緩存。java
強緩存主要是採用響應頭中的Cache-Control和Expires兩個字段進行控制的。其中Expires是HTTP 1.0中定義的,它指定了一個絕對的過時時期。而Cache-Control是HTTP 1.1時出現的緩存控制字段。Cache-Control:max-age定義了一個最大使用期,就是從第一次生成文檔到緩存再也不生效的合法生存日期。因爲Expires是HTTP1.0時代的產物,所以設計之初就存在着一些缺陷,若是本地時間和服務器時間相差太大,就會致使緩存錯亂。這兩個字段同時使用的時候Cache-Control的優先級給更高一點。 這兩個字段的效果是相似的,客戶端都會經過對比本地時間和服務器生存時間來檢測緩存是否可用。若是緩存沒有超出它的生存時間內,客戶端就會直接採用本地的緩存。若是生存日期已通過了,這個緩存也就宣告失效。接着客戶端將再次與服務器進行通訊來驗證這個緩存是否須要更新。jquery
強緩存機制若是檢測到緩存失效,就須要進行服務器再驗證。這種緩存機制也稱做協商緩存。瀏覽器在第一次獲取請求的時候,就會在響應頭中攜帶上資源的上次服務器修改日期(Last-Modified)或者資源的標籤(Etag)。後續的請求服務器會根據請求頭上的If-Modified-Since(對應Last-Modified)和(If-None-Match)字段來判斷資源是否失效,一旦資源過時,則服務器會從新發送新的資源到客戶端上,從而保證資源的有效性。webpack
其中Last-Modified字段對應的是資源最後修改時間,例如:`Last-Modified:git
Sat, 30 Dec 2017 20:18:56 GMT` ,當客戶端再次請求該資源的時候,會在其請求頭上附帶上If-Modified-Since字段,值就是以前返回的Last-Modified值。若是資源未過時,命中緩存,服務器就直接返回304狀態碼,客戶端直接使用本地的資源。不然,服務器從新發送響應資源。github
另一種協商緩存的校驗方式的經過校驗碼而不是時間,這樣就保證了在文件內容不變的狀況下不會重複佔用網絡資源。響應頭中Etag字段是服務器給資源打上的一個標記,利用這個標記就能夠實現緩存的更新。後續發起的請求,會在請求頭上附帶上If-None-Match字段,其值就是這個標記的值。web
須要注意的是當響應頭中同時存在Etag和Last-Modified的時候,會先對Etag進行比對,隨後纔是Last-Modified。
上面介紹了網絡協議層面的緩存方案,接下來從前端的角度來看一下瀏覽器中幾種經常使用的緩存技術。
原本HTTP協議的緩存方案很美好了,不過當用戶主動觸發頁面刷新內容,如:F5等,就會使瀏覽器的強緩存失效,進而轉變成協商緩存。而利用LocalStorage能夠無視用戶主動刷新行爲,而且能夠存儲較大致積的資源(2M以上)。
localStorage的使用也較爲簡單:
const key = 'scq000';
const value = 'hello world';
// 存
localStorage.setItem(key, value);
// 取
localStorage.getItem(key);
複製代碼
雖然說localStorage通常是用來存儲應用數據的,可是也能夠利用其存儲js和css等靜態資源。
<script id="testJs" src="example.js"></script>
複製代碼
// 以js爲例
var lsKey = 'loadJSv1.0'; // 做爲localStorage存取的key;
// 獲取要緩存或者執行的源碼內容
function getScriptContent(url, callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200) {
// 獲取代碼內容
var codeStr = httpRequest.responseText;
callback && callback(codeStr);
}
}
};
httpRequest.open('GET', url);
httpRequest.send();
}
// 第一次運行的時候緩存
function cacheJs(url) {
// 獲取代碼內容
getScriptContent(url, function(codeStr) {
console.log(codeStr);
// 執行代碼並緩存
var script = document.createElement('script');
script.innerHTML = codeStr;
localStorage.setItem(lsKey, codeStr);
});
}
// 加載源碼
function loadJs(url) {
// 讀取緩存
var cacheStr = localStorage.getItem(lsKey);
if(cacheStr) {
// 插入瀏覽器中,或者也能夠直接使用eval執行
var script = document.createElement('script');
script.innerHTML = cacheStr;
console.log("使用緩存成功");
} else {
// 沒有緩存,就會從服務器獲取源碼並緩存到本地
cacheJs(url);
}
}
// 第一次執行的時候,會直接執行並緩存到localhost中去,第二次進入的時候,會直接使用緩存
loadJs('http://code.jquery.com/jquery-3.2.1.min.js')
複製代碼
上面只是一個簡單的demo,若是真的要使用這種方案,還須要考慮到更新處理問題。
做爲一種性能優化的方案,這種方法也曾被大量應用於移動端的網頁中。不過缺點也很明顯,因爲localStorage是保存在本地中的,因此很容易致使xss注入攻擊。若是要使用這種方案,必定要作好對應的安全措施。在這裏推薦一篇文章:使用 SRI 加強 localStorage 代碼安全。
HTML5曾經提供了一個應用程序緩存機制, 使得基於web的應用程序能夠離線運行。這就是App Cache(採用mainfest文件進行緩存), 因爲方案目前正在從web標準中刪除,因此在這裏只作簡單的介紹。
<!DOCTYPE html>
<html manifest="index.appcache">
<head>
<title></title>
</head>
<body>
</body>
</html>
複製代碼
CACHE MANIFEST
# v1 - 2017-11-11
# 緩存版本號
# 指定須要被緩存的文件
CACHE:
index.html
script.js
# 指定須要和服務器鏈接的白名單,將不進行緩存
NETWORK:
style.css
# 回退頁面,當資源沒法訪問,瀏覽器將採用該頁面
FALLBACK:
index_bak.html
複製代碼
這個方案一個比較很差的地方,是須要和服務器進行配合,mainfest文件的版本更新也是一個問題,同時資源還不支持部分更新。若是你想了解更多,能夠訪問Using the application cache
做爲AppCache的替代方案,Service Worker 是一個相對來講比較新的技術,其目的也主要是爲了提升web app的用戶體驗,能夠實現離線應用消息推送等等一系列的功能, 從而進一步縮小與原生應用的差距。 Service Worker能夠看作是一個獨立於瀏覽器的Javascript代理腳本,經過JS的控制,可以使應用先獲取本地緩存資源(Offline First),從而在離線狀態下也能提供基本的功能。 出於安全性的考慮,Service Worker 只能在https協議下使用,不過爲了便於開發,如今的瀏覽器默認支持localhost使用Service Worker。
Service Worker整個的使用過程包括了註冊,安裝,激活,睡眠銷燬等等一系列的狀態。
首先須要在頁面中註冊一個Service Worker。須要寫在入口文件中:
if(‘serviceWorker' in navigator) {
navigator.serviceWorker.register('./testSW.js', {scope: '/src'}).then(reg => {
console.log('service worker is working', reg);
}).catch(e => console.log('register service worker failed'));
}
複製代碼
因爲兼容性的問題,須要在代碼開始作瀏覽器特性檢測處理。註冊時候,scope參數是可選的,用來限制SW的工做範圍的。
// 用來標記緩存
const CACHE_FILE = 'my-sw-demo-v1';
let filesToCache = [
'/',
'/index.html',
'/scripts/main.js',
'/styles/main.css'
];
// 安裝
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_FILE)
.then(cache => cache.addAll(filesToCache));
);
});
// 添加fetch事件監聽
self.addEventListener('fetch', event => {
event.responseWith(
caches.match(event.request)
.then(response => response)
.catch(() => fetch(event.request));
);
});
複製代碼
當用戶首次訪問頁面的時候,會觸發SW的安裝事件,addAll方法接收須要被緩存文件的url列表,並會自動獲取這些文件存入緩存中。 接下來註冊的fetch事件監聽器會在每次SW被控制的資源請求時觸發,攔截請求並在緩存中匹配對應資源。若是緩存命中,則直接返回資源,不然去發起fetch請求。 固然,若是你想更進一步,能夠在緩存沒有命中的時候,獲取資源而後將獲取到資源加入緩存中。另外,在網絡不可用的時候,提供一種回退方案。上面的代碼能夠改寫成這樣:
// 添加fetch事件監聽
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).catch(() => {
return fetch(event.request).then(response => {
return caches.open('v1').then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
}).catch(() => {
// 回退資源
return caches.match('/fallback.html');
})
);
});
複製代碼
若是應用中SW已經安裝,可是刷新的時候檢測到有新版保持可用,就會自動安裝。可是須要注意的是,只有當再也不有任何已加載頁面在使用舊版SW的時候,新版本的SW纔會被激活。
我們把上面的版本號更改一下:
const CACHE_FILE = 'my-sw-demo-v2';
複製代碼
此時刷新頁面,當install
事件發生的時候,前一個版本(my-sw-demo-v1)若是還在被其它頁面使用,則這個新版本不會被激活,當全部頁面都再也不使用v1的時候,v2就會激活並開始響應請求。
做爲緩存的完整生命週期來講,提供刪除功能必不可少。咱們有時候須要手動刪除舊版本的緩存,以便釋放有限的瀏覽器緩存空間。此時,能夠利用activate
事件和waitUntil
這樣一個方法來清理緩存。
// 清理緩存操做
self.addEventListener('activate', event => {
// 設置白名單,不須要刪除的緩存key
const cacheWhiteList = ['v2'];
event.waitUntil(
cache.keys().then(keyList => {
return Promise.all(keyList.map(key => {
if (!cacheWhiteList.includes(key)) {
// 若是不在白名單裏面,就刪除該緩存
return cache.delete(key);
}
}));
});
)
});
複製代碼
調試的時候,能夠在谷歌瀏覽器中輸入chrome://serviceworker-internals/
查看各個頁面SW腳本的工做狀況。也能夠在開發者工具中查看當前頁的SW腳本狀況:
SW目前仍是一個草案,在PC端上各個瀏覽器的支持度並非很高,可是在手機端已大部分可以實現支持了。做爲PWA的一種核心技術,谷歌對SW提供不少頗有用的工具,如:Sw-precache, Sw-toolbox,感興趣的能夠去研究一番。 下面收集了一些比較有用的工具和參考文章,若是須要深刻學習,能夠一閱: serviceworker-webpack-plugin
https://www.npmjs.com/package/workbox-webpack-plugin
http://air.ghost.io/using-workbox-webpack-to-precache-with-service-worker/
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers
https://ivweb.io/topic/5876d4ee441a881744b0d6d6
https://x5.tencent.com/tbs/guide/serviceworker.html
https://foio.github.io/
https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/
最後,做爲2018年的開篇之做,但願各位讀者在新的一年裏都能工做順利,生活快樂!