緩存&PWA 實(shí)踐
一潭辈、背景
從上一篇《前端動(dòng)畫實(shí)現(xiàn)與原理分析》,我們從 Performance 進(jìn)行動(dòng)畫的性能分析澈吨,并根據(jù) Performance 分析來(lái)優(yōu)化動(dòng)畫。但谅辣,前端不僅僅是實(shí)現(xiàn)流暢的動(dòng)畫修赞。ToB 項(xiàng)目會(huì)經(jīng)常與數(shù)據(jù)的保存屈藐、渲染打交道。例如開發(fā)中搓扯,為了提高用戶體驗(yàn)公壤,遇到了一些場(chǎng)景换可,其實(shí)就是在利用某些手段,來(lái)進(jìn)行性能優(yōu)化确憨。
- Select 下拉:做前端分頁(yè)展示 → 避免一次性渲染數(shù)據(jù)造成瀏覽器的假死狀態(tài)译荞;
- indexedDB:存儲(chǔ)數(shù)據(jù) → 用戶下一次進(jìn)入時(shí)瓤的,保存上一次編輯的狀態(tài) ……
那不免引發(fā)思考,我們從緩存與數(shù)據(jù)存儲(chǔ)來(lái)思考吞歼,該如何優(yōu)化圈膏?
二、 HTTP 緩存
是什么篙骡?
Http 緩存其實(shí)就是瀏覽器保存通過 HTTP 獲取的所有資源稽坤,
是瀏覽器將網(wǎng)絡(luò)資源存儲(chǔ)在本地的一種行為。
請(qǐng)求的資源在哪里糯俗?
- 6.8kB + 200 狀態(tài)碼: 沒有命中緩存尿褪,實(shí)際的請(qǐng)求,從服務(wù)器上獲取資源叶骨;
- memory cache: 資源緩存在內(nèi)存中,不會(huì)請(qǐng)求服務(wù)器茫多,一般已經(jīng)加載過該資源且緩存在內(nèi)存中祈匙,當(dāng)關(guān)閉頁(yè)面時(shí)忽刽,此資源就被內(nèi)存釋放掉了;
- disk cache: 資源緩存在磁盤中夺欲,不會(huì)請(qǐng)求服務(wù)器跪帝,但是該資源也不會(huì)隨著關(guān)閉頁(yè)面就釋放掉;
- 304 狀態(tài)碼:請(qǐng)求服務(wù)器些阅,發(fā)現(xiàn)資源沒有被更改伞剑,返回 304 后,資源從本地取出市埋;
- service worker: 應(yīng)用級(jí)別的存儲(chǔ)手段黎泣;
HTTP 緩存分類
1. 強(qiáng)緩存
- 瀏覽器加載資源時(shí),會(huì)先根據(jù)本地緩存資源的 header 中的信息判斷是否命中強(qiáng)緩存缤谎。如果命中抒倚,則不會(huì)像服務(wù)器發(fā)送請(qǐng)求,而是直接從緩存中讀取資源坷澡。
- 強(qiáng)緩存可以通過設(shè)置 HTTP Header 來(lái)實(shí)現(xiàn):
http1.0 → Expires: 響應(yīng)頭包含日期/時(shí)間托呕, 即在此時(shí)候之后,響應(yīng)過期频敛。
http1.1 → Cache-Control:max-age=: 設(shè)置緩存存儲(chǔ)的最大周期项郊,超過這個(gè)時(shí)間緩存被認(rèn)為過期(單位秒)。與Expires
相反斟赚,時(shí)間是相對(duì)于請(qǐng)求的時(shí)間
?? Cache-control
- cache-control: max-age=3600 :表示相對(duì)時(shí)間
- cache-control:no-cache → 可以存儲(chǔ)在本地緩存的着降,只是在原始服務(wù)器進(jìn)行新鮮度在驗(yàn)證之前,緩存不能將其提供給客戶端使用
- cache-control: no-store → 禁止緩存對(duì)響應(yīng)進(jìn)行復(fù)制拗军,也就是真正的不緩存數(shù)據(jù)在本地任洞;
- catch-control:public → 可以被所有用戶緩存(多用戶共享)厌殉,包括終端、CDN 等
- cache-control: private → 私有緩存
2. 協(xié)商緩存
- 當(dāng)瀏覽器對(duì)某個(gè)資源的請(qǐng)求沒有命中強(qiáng)緩存侈咕,就會(huì)發(fā)一個(gè)請(qǐng)求到服務(wù)器公罕,驗(yàn)證協(xié)商緩存是否命中,如果協(xié)商緩存命中耀销,請(qǐng)求返回的 http 狀態(tài) 304楼眷,并且會(huì)顯示 Not Modified 的字符串;
- 協(xié)商緩存通過【last-Modified熊尉,if-Modified-Since】與【ETag, if-None-Match】來(lái)管理的罐柳。
- 「Last-Modified、If-Modified-Since」
last-Modified : 表示本地文件最后修改的日期狰住,瀏覽器會(huì)在請(qǐng)求頭中帶上 if-Modified-since(也是上次返回的 Last-Modified 的值)张吉,服務(wù)器會(huì)將這個(gè)值與資源修改的時(shí)間進(jìn)行匹配,如果時(shí)間不一致催植,服務(wù)器會(huì)返回新的資源肮蛹,并且將 Last-modified 值更新,并作為響應(yīng)頭返回給瀏覽器创南。如果時(shí)間一致伦忠,表示資源沒有更新,服務(wù)器會(huì)返回 304 狀態(tài)稿辙,瀏覽器拿到響應(yīng)狀態(tài)碼后從本地緩存中讀取資源昆码。
- 「ETag、If-None-Match」
http 1.1 中邻储, 服務(wù)器通過 Etag 來(lái)設(shè)置響應(yīng)頭緩存標(biāo)示赋咽。Etag 是由服務(wù)器來(lái)生成的。
第一次請(qǐng)求時(shí)吨娜,服務(wù)器會(huì)將資源和 ETag 一并返回瀏覽器脓匿,瀏覽器將兩者緩存到本地緩存中。
第二次請(qǐng)求時(shí)萌壳,瀏覽器會(huì)將 ETag 的值放到 If-None-Match 請(qǐng)求頭去訪問服務(wù)器亦镶,服務(wù)器接收請(qǐng)求后,會(huì)將服務(wù)器中的文件標(biāo)識(shí)與瀏覽器發(fā)來(lái)的標(biāo)識(shí)進(jìn)行比對(duì)袱瓮,如果不同缤骨, 服務(wù)器會(huì)返回更新的資源和新的 Etag,如果相同尺借,服務(wù)器會(huì)返回 304 狀態(tài)碼绊起,瀏覽器讀取緩存。
?? 思考為什么有了 Last-Modified 這一對(duì)兒燎斩,還需要 Etag 來(lái)標(biāo)識(shí)是否過期進(jìn)行命中協(xié)商緩存
- 文件的周期性更改虱歪,但是文件的內(nèi)容沒有改變蜂绎,僅僅改變了修改時(shí)間,這個(gè)時(shí)候笋鄙,并不希望客戶端認(rèn)為該文件被修改了师枣,而重新獲取。
- 如果文件文件頻繁修改萧落,比如 1s 改了 N 次践美,If-Modified-Since 無(wú)法判斷修改的,會(huì)導(dǎo)致文件已經(jīng)修改但是獲取的資源還是舊的找岖,會(huì)存在問題陨倡。
- 某些服務(wù)器不能精確得到文件的最后修改時(shí)間,導(dǎo)致資源獲取的問題许布。
?? 如果 Etag 與 Last-Modified 同時(shí)存在兴革,服務(wù)器會(huì)先檢查 ETag,然后在檢查 Last-Modified, 最終確定是返回 304 或 200蜜唾。
HTTP 緩存實(shí)踐
測(cè)試環(huán)境: 利用 Koa杂曲,搭建一個(gè) node 服務(wù),用來(lái)測(cè)試如何命中強(qiáng)緩存還是協(xié)商緩存
當(dāng) index.js 沒有配置任何關(guān)于緩存的配置時(shí)灵妨, 無(wú)論怎么刷新 chrome解阅,都沒有緩存機(jī)制的落竹;
- 注意??:在開始實(shí)驗(yàn)之前要把 network 面板的 Disable cache 勾選去掉泌霍,這個(gè)選項(xiàng)表示禁用瀏覽器緩存,瀏覽器請(qǐng)求會(huì)帶上 Cache-Control: no-cache 和 Pragma: no-cache 頭部信息述召。
1. 命中強(qiáng)緩存
app.use(async (ctx) => {
// ctx.body = 'hello Koa'
const url = ctx.request.url;
if(url === '/'){
// 訪問跟路徑返回 index.html
ctx.set('Content-type', 'text/html');
ctx.body = await parseStatic('./index.html')
}else {
ctx.set('Content-Type', parseMime(url))
ctx.set('Expires', new Date(Date.now() + 10000).toUTCString()) // 實(shí)驗(yàn)1
ctx.set('Cache-Control','max-age=20') // 實(shí)驗(yàn)2
ctx.body = await parseStatic(path.relative('/', url))
}
})
app.listen(3000, () => {
console.log('starting at port 3000')
})
2. 命中協(xié)商緩存
/**
* 命中協(xié)商緩存
* 設(shè)置 Last-Modified, If-Modified-Since
*/
ctx.set('cache-control', 'no-cache'); // 關(guān)閉強(qiáng)緩存
const isModifiedSince = ctx.request.header['if-modified-since'];
const fileStat = await getFileStat(filePath);
if(isModifiedSince === fileStat.mtime.toGMTString()){
ctx.status = 304
}else {
ctx.set('Last-Modified', fileStat.mtime.toGMTString())
}
ctx.body = await parseStatic(path.relative('/', url))
/**
* 命中協(xié)商緩存
* 設(shè)置 Etag, If-None-Match
*/
ctx.set('cache-control', 'no-cache');
const ifNoneMatch = ctx.request.headers['if-none-match'];
const fileBuffer = await parseStatic(filePath);
const hash = crypto.createHash('md5');
hash.update(fileBuffer);
const etag = `"${hash.digest('hex')}"`
if (ifNoneMatch === etag) {
ctx.status = 304
} else {
ctx.set('Etag', etag)
ctx.body = fileBuffer
}
}
三朱转、 瀏覽器緩存
1.Cookies
- MDN 定義: 是服務(wù)器發(fā)送到用戶瀏覽器并報(bào)訊在本地的一小塊數(shù)據(jù),他會(huì)在瀏覽器下次想統(tǒng)一服務(wù)器再次發(fā)送請(qǐng)求時(shí)被攜帶并發(fā)送到服務(wù)器上积暖。
- 應(yīng)用場(chǎng)景:
- 會(huì)話狀態(tài)管理【用戶登陸狀態(tài)藤为,購(gòu)物車,游戲分?jǐn)?shù)或其他需要記錄的信息】
- 個(gè)性化設(shè)置(如用戶自定義設(shè)置夺刑、主題等)
- 瀏覽器行為跟蹤(如跟蹤分析用戶行為等)
- cookie 的讀取與寫入:
class Cookie {
getCookie: (name) => {
const reg = new RegExp('(^| )'+name+'=([^;]*)')
let match = document.cookie.match(reg); // [全量缅疟,空格,value遍愿,‘存淫;’]
if(match) {return decodeURI(match[2])}
}
setCookie:(key,value,days,domain) => {
// username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/
let d = new Date();
d.setTime(d.getTime()+(days*24*60*60*1000));
let expires = "; expires="+d.toGMTString();
let domain = domain ? '; domain='+domain : '';
document.cookie = name + '=' + value + expires + domain + '; path=/'
}
deleteCookie: (name: string, domain?: string, path?: string)=> {
// 刪除cookie,只需要將時(shí)間設(shè)置為過期時(shí)間沼填,而無(wú)需刪除cookie的value
const d = new Date(0);
domain = domain ? `; domain=${domain}` : '';
path = path || '/';
document.cookie =
name + '=; expires=' + d.toUTCString() + domain + '; path=' + path;
},
}
- 存在的問題: 由于通過 Cookie 存儲(chǔ)的數(shù)據(jù)桅咆,每次請(qǐng)求都會(huì)攜帶在請(qǐng)求頭。對(duì)與一些數(shù)據(jù)是無(wú)需交給提交后端的坞笙,這個(gè)不免會(huì)帶來(lái)額外的開銷岩饼。
2.WebStorage API
瀏覽器能以一種比使用 Cookie 更直觀的方式存儲(chǔ)鍵值對(duì)
localStorage
為每一個(gè)給定的源(given origin)維持一個(gè)獨(dú)立的存儲(chǔ)區(qū)域荚虚,瀏覽器關(guān)閉,然后重新打開后數(shù)據(jù)仍然存在籍茧。
sessionStorage
為每一個(gè)給定的源(given origin)維持一個(gè)獨(dú)立的存儲(chǔ)區(qū)域版述,該存儲(chǔ)區(qū)域在頁(yè)面會(huì)話期間可用(即只要瀏覽器處于打開狀態(tài),包括頁(yè)面重新加載和恢復(fù))寞冯。
3.indexedDB 與 webSQL
webSQL
基本操作與實(shí)際數(shù)據(jù)庫(kù)操作基本一致院水。
最終的數(shù)據(jù)去向,一般只是做臨時(shí)存儲(chǔ)和大型網(wǎng)站的業(yè)務(wù)運(yùn)行存儲(chǔ)緩存的作用简十,頁(yè)面刷新后該庫(kù)就不存在了檬某。而其本身與關(guān)系數(shù)據(jù)庫(kù)的概念比較相似。
indexedDB
隨著瀏覽器的功能不斷增強(qiáng)螟蝙,越來(lái)越多的網(wǎng)站開始考慮恢恼,將大量數(shù)據(jù)儲(chǔ)存在客戶端,這樣可以減少?gòu)姆?wù)器獲取數(shù)據(jù)胰默,直接從本地獲取數(shù)據(jù)〕“撸現(xiàn)有的瀏覽器數(shù)據(jù)儲(chǔ)存方案,都不適合儲(chǔ)存大量數(shù)據(jù)牵署;
IndexedDB 是瀏覽器提供的本地?cái)?shù)據(jù)庫(kù)漏隐, 允許儲(chǔ)存大量數(shù)據(jù),提供查找接口奴迅,還能建立索引青责。這些都是 LocalStorage 所不具備的。就數(shù)據(jù)庫(kù)類型而言取具,IndexedDB 不屬于關(guān)系型數(shù)據(jù)庫(kù)(不支持 SQL 查詢語(yǔ)句)脖隶,更接近 NoSQL 數(shù)據(jù)庫(kù)。
四暇检、應(yīng)用程序緩存
Service Worker
在提及 Service Worker 之前产阱,需要對(duì) web Worker 有一些了解;
webWorker : Web Worker 是瀏覽器內(nèi)置的線程所以可以被用來(lái)執(zhí)行非阻塞事件循環(huán)的 JavaScript 代碼块仆。 js 是單線程构蹬,一次只能完成一件事,如果出現(xiàn)一個(gè)復(fù)雜的任務(wù)悔据,線程就會(huì)被阻塞庄敛,嚴(yán)重影響用戶體驗(yàn), Web Worker 的作用就是允許主線程創(chuàng)建 worker 線程蜜暑,與主線程同時(shí)進(jìn)行铐姚。worker 線程只需負(fù)責(zé)復(fù)雜的計(jì)算,然后把結(jié)果返回給主線程就可以了。簡(jiǎn)單的理解就是隐绵,worker 線程執(zhí)行復(fù)雜計(jì)算并且頁(yè)面(主線程)ui 很流暢之众,不會(huì)被阻塞。
Service Worker 是瀏覽器和網(wǎng)絡(luò)之間的虛擬代理依许。其解決了如何正確緩存往后網(wǎng)站資源并使其在離線時(shí)可用的問題棺禾。
Service Worker 運(yùn)行在一個(gè)與頁(yè)面 js 主線程獨(dú)立的線程上,并且無(wú)權(quán)訪問 DOM 結(jié)構(gòu)峭跳。他的 API 是非阻塞的膘婶,并且可以在不同的上下文之間發(fā)送和接收消息。
他不僅僅提供離線功能蛀醉,還可以做包括處理通知悬襟、在單獨(dú)的線程上執(zhí)行繁重的計(jì)算等事務(wù)。Service Workers 非常強(qiáng)大拯刁,因?yàn)樗麄兛梢钥刂凭W(wǎng)絡(luò)請(qǐng)求脊岳,修改網(wǎng)絡(luò)請(qǐng)求,返回緩存的自定義響應(yīng)或者合成響應(yīng)垛玻。
2.PWA
?? PWA割捅,全稱 Progressive web apps,即漸進(jìn)式 Web 應(yīng)用帚桩。PWA 技術(shù)主要作用為構(gòu)建跨平臺(tái)的 Web 應(yīng)用程序亿驾,并使其具有與原生應(yīng)用程序相同的用戶體驗(yàn)。
?? PWA 的核心: 最根本账嚎、最基本的莫瞬,就是 Service Worker 以及在其內(nèi)部使用的 Cache API。只要通過 Service Worker 與 Cache API醉锄,實(shí)現(xiàn)了對(duì)網(wǎng)站頁(yè)面的緩存乏悄、對(duì)頁(yè)面請(qǐng)求的攔截、對(duì)頁(yè)面緩存的操縱 恳不。
為什么使用 PWA:
傳統(tǒng)的 Web 存在的問題:
- 缺乏直接入口,需要記住他的域名开呐,或者是保存在收藏夾烟勋,尋找起來(lái)不夠方便;
- 依賴于網(wǎng)絡(luò)筐付。只要客戶端處于斷網(wǎng)的狀態(tài)卵惦,整個(gè) web 就處于癱瘓狀態(tài),客戶端無(wú)法使用瓦戚;
- 無(wú)法像 Native APP 推送消息沮尿。
傳統(tǒng) Native APP 存在的問題:
- 需要安裝與下載。哪怕只是使用 APP 的某個(gè)功能,也是需要全盤下載的畜疾;
- 開發(fā)成本高赴邻,一般需要兼容安卓與 IOS 系統(tǒng);
- 發(fā)布需要審核啡捶;
- 更新成本高……
PWA 的存在姥敛,就是為了解決以上問題所帶來(lái)的麻煩:
優(yōu)勢(shì):
- 桌面入口,打開便捷瞎暑;
- 離線可用彤敛,不用過度依賴網(wǎng)絡(luò);
- 安裝方便了赌;
- 一次性開發(fā)墨榄,無(wú)需審核,所有平臺(tái)可用勿她;
- 能夠進(jìn)行消息推送
- Web App Manifest Web App Manifest(Web 應(yīng)用程序清單)概括地說(shuō)是一個(gè)以 JSON 形式集中書寫頁(yè)面相關(guān)信息和配置的文件渠概。
{
"short_name": "User Mgmt",
"name": "User Management",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".", // 調(diào)整網(wǎng)站的起始鏈接
"display": "standalone", // 設(shè)定網(wǎng)站提示模式 : standalone 表示隱藏瀏覽器的UI
"theme_color": "#000000", // 設(shè)定網(wǎng)站每個(gè)頁(yè)面的主題顏色
"background_color": "#ffffff" // 設(shè)定背景顏色
}
- ServiceWorker.js 代碼
/* eslint-disable no-restricted-globals */
// 確定哪些資源需要緩存
const CACHE_VERSION = 0;
const CACHE_NAME = 'cache_v' + CACHE_VERSION;
const CACHE_URL = [
'/',
'index.html',
'favicon.ico',
'serviceWorker.js',
'static/js/bundle.js',
'manifest.json',
'users'
]
const preCache = () => {
return caches
.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(CACHE_URL)
})
}
const clearCache = () => {
return caches.keys().then(keys => {
keys.forEach(key => {
if (key !== CACHE_NAME) {
caches.delete(key)
}
})
})
}
// 進(jìn)行緩存
self.addEventListener('install', (event) => {
event.waitUntil(
preCache()
)
})
// 刪除舊的緩存
self.addEventListener('activated', (event) => {
event.waitUntil(
clearCache()
)
})
console.log('hello, service wold');
self.addEventListener('fetch', (event) => {
console.log('request:', event.request.url)
event.respondWith(
fetch(event.request).catch(() => { // 優(yōu)先網(wǎng)絡(luò)請(qǐng)求,如果失敗嫂拴,則從緩存中拿資源
return caches.match(event.request)
})
)
})
- PWA 調(diào)試
當(dāng)離線的時(shí)候依然拿到緩存的資源播揪,并且正常展示,可以看出資源被 serviceWorker 緩存筒狠。
借助開發(fā)者工具:
chrome devtools : chrome://inspect/#service-workers 猪狈,可以展示當(dāng)前設(shè)備上激活和存儲(chǔ)的 service worker
五、總結(jié)與思考
參考優(yōu)秀網(wǎng)站:
- 語(yǔ)雀: https://www.yuque.com/dashboard 辩恼;
- PWA 例子: https://mdn.github.io/pwa-examples/js13kpwa/