node強緩存和協(xié)商緩存實戰(zhàn)示例
前言
瀏覽器緩存是性能優(yōu)化非常重要的一個方案,合理地使用緩存可以提高用戶體驗,還能節(jié)省服務(wù)器的開銷。掌握好緩存的原理和并合理地使用無論對前端還是運維都是相當(dāng)重要的。
什么是瀏覽器緩存
瀏覽器緩存(http 緩存) 是指瀏覽器在本地磁盤對用戶最近請求過的文檔進行存儲,當(dāng)訪問者再次訪問同一頁面時,瀏覽器就可以直接從本地磁盤加載文檔。
優(yōu)點
減少了冗余的數(shù)據(jù)傳輸,節(jié)省帶寬,減少服務(wù)器壓力
加快了客戶端加載速度,提升用戶體驗。
強緩存
強緩存不會向服務(wù)器發(fā)送請求,而是直接從緩存中讀取資源,強緩存可以通過設(shè)置兩種 HTTP Header 實現(xiàn):Expires 和 Cache-Control,這兩個頭部分別是HTTP1.0和HTTP1.1的實現(xiàn)。
Expires
Expires是HTTP1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由服務(wù)器返回。
Expires 受限于本地時間,如果修改了本地時間,就會造成緩存失效。
Cache-Control
Cache-Control 出現(xiàn)于 HTTP/1.1,常見字段是max-age,單位是秒,很多web服務(wù)器都有默認配置,優(yōu)先級高于Expires,表示的是相對時間。
例如Cache-Control:max-age=3600 代表資源的有效期是 3600 秒。取的是響應(yīng)頭中的 Date,請求發(fā)送的時間,表示當(dāng)前資源在 Date ~ Date +3600s 這段時間里都是有效的。Cache-Control 還擁有多個值:
- no-cache 不直接使用緩存,也就是跳過強緩存。
- no-store 禁止瀏覽器緩存數(shù)據(jù),每次請求資源都會向服務(wù)器要完整的資源。
- public 可以被所有用戶緩存,包括終端用戶和 CDN 等中間件代理服務(wù)器。
- private 只允許終端用戶的瀏覽器緩存,不允許其他中間代理服務(wù)器緩存。
要注意的就是no-cache和no-store的區(qū)別,no-cache是跳過強緩存,還是會走協(xié)商緩存的步驟,而no-store是真正的完全不走緩存,所有資源都不會緩存在本地
協(xié)商緩存
當(dāng)瀏覽器對某個資源的請求沒有命中強緩存,就會發(fā)一個請求到服務(wù)器,驗證協(xié)商緩存是否命中,如果協(xié)商緩存命中,請求響應(yīng)返回的http狀態(tài)為304并且會顯示一個Not Modified的字符串。
協(xié)商緩存用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】這兩對Header來管理的。
注意!!協(xié)商緩存需要配合強緩存使用,使用協(xié)商緩存需要先設(shè)置Cache-Control:no-cache或者pragma:no-cache來告訴瀏覽器不走強緩存
Last-Modified、If-Modified-Since
這兩個Header是HTTP1.0版本提出來的,兩個字段配合使用。
Last-Modified 表示本地文件最后修改日期,瀏覽器會在請求頭帶上If-Modified-Since(上次返回的Last-Modified的值),服務(wù)器會將這個值與資源修改的時間匹配,如果時間不一致,服務(wù)器會返回新的資源,并且將 Last-Modified 值更新,作為響應(yīng)頭返回給瀏覽器。如果時間一致,表示資源沒有更新,服務(wù)器返回 304 狀態(tài)碼,瀏覽器拿到響應(yīng)狀態(tài)碼后從本地緩存中讀取資源。
但Last-Modified有幾個問題。
- 文件雖然被修改了,但最終的內(nèi)容沒有變化,這樣文件修改時間還是會被更新
- 有的文件修改頻率在秒以內(nèi),這時候以秒粒度來記錄就不夠了
- 有的服務(wù)器無法精確獲取文件的最后修改時間。
所以出現(xiàn)了ETAG。
ETag、If-None-Match
在HTTP1.1版本中,服務(wù)器通過 Etag 來設(shè)置響應(yīng)頭緩存標(biāo)識。Etag 的值由服務(wù)端生成。在第一次請求時,服務(wù)器會將資源和 Etag 一并返回給瀏覽器,瀏覽器將兩者緩存到本地緩存數(shù)據(jù)庫。在第二次請求時,瀏覽器會將 Etag 信息放到 If-None-Match 請求頭去訪問服務(wù)器,服務(wù)器收到請求后,會將服務(wù)器中的文件標(biāo)識與瀏覽器發(fā)來的標(biāo)識進行對比,如果不相同,服務(wù)器返回更新的資源和新的 Etag ,如果相同,服務(wù)器返回 304 狀態(tài)碼,瀏覽器讀取緩存。
流程總結(jié)
總結(jié)這幾個字段:
- Cache-Control —— 請求服務(wù)器之前
- Expires —— 請求服務(wù)器之前
- If-None-Match (Etag) —— 請求服務(wù)器
- If-Modified-Since (Last-Modified) —— 請求服務(wù)器
node實踐
本文用koa來做例子,因為koa是更輕量級的、更純凈的,本身并沒有捆綁任何中間件,相比express自帶了很多router、static等多種中間件函數(shù),koa更適合本文來做示例。
koa啟動服務(wù)
秉著學(xué)習(xí)和更容易理解的宗旨,不使用koa-static和koa-router中間件,用koa簡易實現(xiàn)web服務(wù)器來驗證之前的結(jié)論。
創(chuàng)建項目
# 創(chuàng)建并進入一個目錄并新建index.js文件 mkdir koa-cache cd koa-cache touch index.js # 初始化項目 git init yarn init # 將 koa 安裝為本地依賴 yarn add koa
koa代碼
/*app.js*/ const Koa = require('koa') const app = new Koa() app.use(async (ctx) => { ctx.body = 'hello koa' }) app.listen(3000, () => { console.log('starting at port 3000') })
啟動服務(wù)
node index.js
這樣一個koa服務(wù)就起來了,訪問localhost:3000可以就看到hello koa。
為了方便調(diào)試,修改代碼不用重新啟動,推薦使用nodemon或者pm2啟動服務(wù)。
原生koa實現(xiàn)簡易靜態(tài)資源服務(wù)
實現(xiàn)一個靜態(tài)資源服務(wù)器關(guān)鍵點就是根據(jù)前端請求的地址來判斷請求的資源類型,設(shè)置返回的Content-Type,讓瀏覽器知道返回的內(nèi)容類型,瀏覽器才能決定以什么形式,什么編碼來讀取返回的內(nèi)容。
定義資源類型列表
const mimes = { css: 'text/css', less: 'text/css', gif: 'image/gif', html: 'text/html', ico: 'image/x-icon', jpeg: 'image/jpeg', jpg: 'image/jpeg', js: 'text/javascript', json: 'application/json', pdf: 'application/pdf', png: 'image/png', svg: 'image/svg+xml', swf: 'application/x-shockwave-flash', tiff: 'image/tiff', txt: 'text/plain', wav: 'audio/x-wav', wma: 'audio/x-ms-wma', wmv: 'video/x-ms-wmv', xml: 'text/xml', }
解析請求的資源類型
function parseMime(url) { // path.extname獲取路徑中文件的后綴名 let extName = path.extname(url) extName = extName ? extName.slice(1) : 'unknown' return mimes[extName] }
fs讀取文件
const parseStatic = (dir) => { return new Promise((resolve) => { resolve(fs.readFileSync(dir), 'binary') }) }
koa處理
app.use(async (ctx) => { 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.body = await parseStatic(path.relative('/', url)) } })
這樣基本也就完成了一個簡單的靜態(tài)資源服務(wù)器。然后在根目錄下新建一個html文件和static目錄,并在static下放一些文件。這時候的目錄應(yīng)該是這樣的:
|-- koa-cache |-- index.html |-- index.js |-- static |-- css |-- color.css |-- ... |-- image |-- soldier.png |-- ... ... ...
這時候就可以通過localhost:3000/static訪問具體的資源文件了。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>test cache</title> <link rel="stylesheet" href="/static/css/index.css" rel="external nofollow" /> </head> <body> <div id="app">測試css文件</div> <img src="/static/image/soldier.png" alt="" /> </body> </html>
css/color.css
#app { color: blue; }
這時候打開localhost:3000,就能看到如下效果:
到這里基本的環(huán)境就都搭好了。接下來進入驗證階段。
強緩存驗證
在沒有任何配置之前,可以看下network:
這時候無論是首次還是第幾次,都會向服務(wù)器請求資源。
注意!?。≡陂_始實驗之前要把network面板的Disable cache勾選去掉,這個選項表示禁用瀏覽器緩存,瀏覽器請求會帶上Cache-Control: no-cache和Pragma: no-cache頭部信息,這時候所有的請求都不會走緩存
設(shè)置Expire
修改index.js中的app.use代碼段。
app.use(async (ctx) => { const url = ctx.request.url if (url === '/') { // 訪問根路徑返回index.html ctx.set('Content-Type', 'text/html') ctx.body = await parseStatic('./index.html') } else { const filePath = path.resolve(__dirname, `.${url}`) ctx.set('Content-Type', parseMime(url)) // 設(shè)置過期時間在30000毫秒,也就是30秒后 ctx.set('Expires', new Date(Date.now() + 30000)) ctx.body = await parseStatic(filePath) } })
用ctx.set(‘Expires’, new Date(Date.now() + 30000)),設(shè)置過期時間為當(dāng)期時間的30000毫秒,也就是30秒后(后面的設(shè)置頭部信息都是這里修改)。
再訪問下localhost:3000,可以看到多了Expires這個Header。
后面在30秒之內(nèi)訪問都可以看到network的Size,css文件顯示的是disk cache,而image資源顯示的是from memory cache。這時候瀏覽器是直接讀的瀏覽器緩存,并沒有請求服務(wù)器,可以嘗試把css和圖片文件改名稱或者刪除驗證下,頁面顯示正常,說明之前的結(jié)論是沒錯的。
Cache-Control
ctx.set(‘Cache-Control’, ‘max-age=300’)設(shè)置300秒有效期,驗證方式同上。
協(xié)商緩存驗證
Last-Modified,If-Modified-Since
HTTP1.0協(xié)商緩存關(guān)鍵點就是根據(jù)客戶端請求帶的ifModifiedSince字段的時間和請求的資源對應(yīng)的修改時間來判斷資源是否有更新。
首先設(shè)置Cache-Control: no-cache, 使客戶端不走強緩存,再判斷客戶端請求是否有帶ifModifiedSince字段,沒有就設(shè)置Last-Modified字段,并返回資源文件。如果有就用fs.stat讀取資源文件的修改時間,并進行對比,如果時間一樣,則返回狀態(tài)碼304。
ctx.set('Cache-Control', 'no-cache') const ifModifiedSince = ctx.request.header['if-modified-since'] const fileStat = await getFileStat(filePath) if (ifModifiedSince === fileStat.mtime.toGMTString()) { ctx.status = 304 } else { ctx.set('Last-Modified', fileStat.mtime.toGMTString()) ctx.body = await parseStatic(filePath) }
etag、If-None-Match
etag的關(guān)鍵點在于計算資源文件的唯一性,這里使用nodejs內(nèi)置的crypto模塊來計算文件的hash值,并用十六進制的字符串表示。cypto的用法可以看nodejs的官網(wǎng)。
crpto不僅支持字符串的加密,還支持傳入buffer加密,作為nodejs的內(nèi)置模塊,在這里用來計算文件的唯一標(biāo)識再合適不過。
ctx.set('Cache-Control', 'no-cache') const fileBuffer = await parseStatic(filePath) const ifNoneMatch = ctx.request.headers['if-none-match'] 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 }
效果如下圖,第二次請求瀏覽器會帶上If-None-Match,服務(wù)器計算文件的hash值再次比較,相同則返回304,不同再返回新的文件。而如果修改了文件,文件的hash值也就變了,這時候兩個hash不匹配,服務(wù)器則返回新的文件并帶上新文件的hash值作為etag。
小結(jié)
通過以上代碼實踐了每個緩存字段的效果,代碼僅作為演示,生產(chǎn)的靜態(tài)資源服務(wù)器會更加復(fù)雜,例如etag不會每次都重新獲取文件來計算文件的hash值,這樣太費性能,一般都會有響應(yīng)的緩存機制,比如對資源的 last-modified 和 etag 值建立索引緩存。
總結(jié)
通常web服務(wù)器都有默認的緩存配置,具體的實現(xiàn)可能也不大相同,像nginx、tomcat、express等web服務(wù)器都有相應(yīng)的源碼,有興趣的可以去閱讀學(xué)習(xí)。
合理的使用強緩存和協(xié)商緩存具體需要看項目的使用場景和需求。像目前常見的單頁面應(yīng)用,因為通常打包都是新生成html與相應(yīng)的靜態(tài)資源依賴,所以可以對html文件配置協(xié)商緩存,而打包生成的依賴,例如js、css這些文件可以使用強緩存。或者只對第三方庫使用強緩存,因為第三方庫通常版本更新較慢,可以鎖定版本。
node示例完整代碼 https://github.com/chen-junyi/code/blob/main/node/cache/koa2.js
以上就是node強緩存和協(xié)商緩存實戰(zhàn)示例的詳細內(nèi)容,更多關(guān)于node強緩存協(xié)商緩存的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Node.js視頻流應(yīng)用創(chuàng)建之后端的全過程
這篇文章主要給大家介紹了關(guān)于創(chuàng)建Node.js視頻流應(yīng)用之后端的相關(guān)資料,文中通過實例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-03-03nodejs?實現(xiàn)簡單的文件上傳功能(示例詳解)
這篇文章主要介紹了nodejs?實現(xiàn)簡單的文件上傳功能,文件上傳方式分為三種,本文通過實例代碼給大家詳細介紹,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02實戰(zhàn)node靜態(tài)文件服務(wù)器的示例代碼
本篇文章主要介紹了實戰(zhàn)node靜態(tài)文件服務(wù)器的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03node異步方法的異步調(diào)用與同步調(diào)用實現(xiàn)方法示例
這篇文章主要介紹了node異步方法的異步調(diào)用與同步調(diào)用實現(xiàn)方法,結(jié)合實例形式分析了node.js異步操作類的封裝以及同步、異步兩種調(diào)用方式,需要的朋友可以參考下2023-05-05nodejs遍歷文件夾下并操作HTML/CSS/JS/PNG/JPG的方法
這篇文章主要介紹了nodejs遍歷文件夾下并操作HTML/CSS/JS/PNG/JPG的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11