前端緩存最佳實(shí)踐
來源:黑金團(tuán)隊(duì)——掘金
前言
緩存,這是一個(gè)老生常談的話題,也常被作為前端面試的一個(gè)知識(shí)點(diǎn)。
本文,重點(diǎn)在與探討在實(shí)際項(xiàng)目中,如何進(jìn)行緩存的設(shè)置,并給出一個(gè)較為合理的方案。
在介紹緩存的時(shí)候,我們習(xí)慣將緩存分為強(qiáng)緩存和協(xié)商緩存兩種。兩者的主要區(qū)別是使用本地緩存的時(shí)候,是否需要向服務(wù)器驗(yàn)證本地緩存是否依舊有效。顧名思義,協(xié)商緩存,就是需要和服務(wù)器進(jìn)行協(xié)商,最終確定是否使用本地緩存。
兩種緩存方案的問題點(diǎn)
強(qiáng)緩存
我們知道,強(qiáng)緩存主要是通過 http 請(qǐng)求頭中的 Cache-Control 和 Expire 兩個(gè)字段控制。Expire 是 HTTP1.0 標(biāo)準(zhǔn)下的字段,在這里我們可以忽略。我們重點(diǎn)來討論的 Cache-Control 這個(gè)字段。
一般,我們會(huì)設(shè)置 Cache-Control 的值為 “public, max-age=xxx”,表示在xxx秒內(nèi)再次訪問該資源,均使用本地的緩存,不再向服務(wù)器發(fā)起請(qǐng)求。
顯而易見,如果在xxx秒內(nèi),服務(wù)器上面的資源更新了,客戶端在沒有強(qiáng)制刷新的情況下,看到的內(nèi)容還是舊的。如果說你不著急,可以接受這樣的,那是不是完美?然而,很多時(shí)候不是你想的那么簡單的,如果發(fā)布新版本的時(shí)候,后臺(tái)接口也同步更新了,那就gg了。有緩存的用戶還在使用舊接口,而那個(gè)接口已經(jīng)被后臺(tái)干掉了。怎么辦?
協(xié)商緩存
協(xié)商緩存最大的問題就是每次都要向服務(wù)器驗(yàn)證一下緩存的有效性,似乎看起來很省事,不管那么多,你都要問一下我是否有效。但是,對(duì)于一個(gè)有追求的碼農(nóng),這是不能接受的。每次都去請(qǐng)求服務(wù)器,那要緩存還有什么意義。
最佳實(shí)踐
緩存的意義就在于減少請(qǐng)求,更多地使用本地的資源,給用戶更好的體驗(yàn)的同時(shí),也減輕服務(wù)器壓力。所以,最佳實(shí)踐,就應(yīng)該是盡可能命中強(qiáng)緩存,同時(shí),能在更新版本的時(shí)候讓客戶端的緩存失效。
在更新版本之后,如何讓用戶第一時(shí)間使用最新的資源文件呢?機(jī)智的前端們想出了一個(gè)方法,在更新版本的時(shí)候,順便把靜態(tài)資源的路徑改了,這樣,就相當(dāng)于第一次訪問這些資源,就不會(huì)存在緩存的問題了。
偉大的 webpack 可以讓我們?cè)诖虬臅r(shí)候,在文件的命名上帶上 hash 值。
entry:{
main: path.join(__dirname, ./main.js ),
vendor: [ react , antd ]
},
output:{
path:path.join(__dirname, ./dist ),
publicPath: /dist/ ,
filname: bundle.[chunkhash].js
}
綜上所述,我們可以得出一個(gè)較為合理的緩存方案:
HTML:使用協(xié)商緩存。
CSS&JS&圖片:使用強(qiáng)緩存,文件命名帶上hash值。
哈希也有講究
webpack 給我們提供了三種哈希值計(jì)算方式,分別是 hash、chunkhash 和 contenthash。那么這三者有什么區(qū)別呢?
hash:跟整個(gè)項(xiàng)目的構(gòu)建相關(guān),構(gòu)建生成的文件hash值都是一樣的,只要項(xiàng)目里有文件更改,整個(gè)項(xiàng)目構(gòu)建的hash值都會(huì)更改。
chunkhash:根據(jù)不同的入口文件(Entry)進(jìn)行依賴文件解析、構(gòu)建對(duì)應(yīng)的chunk,生成對(duì)應(yīng)的hash值。
contenthash:由文件內(nèi)容產(chǎn)生的hash值,內(nèi)容不同產(chǎn)生的contenthash值也不一樣。
顯然,我們是不會(huì)使用第一種的。改了一個(gè)文件,打包之后,其他文件的 hash 都變了,緩存自然都失效了。這不是我們想要的。
那 chunkhash 和 contenthash 的主要應(yīng)用場(chǎng)景是什么呢?
在實(shí)際在項(xiàng)目中,我們一般會(huì)把項(xiàng)目中的 css 都抽離出對(duì)應(yīng)的 css 文件來加以引用。如果我們使用 chunkhash,當(dāng)我們改了 css 代碼之后,會(huì)發(fā)現(xiàn) css 文件 hash 值改變的同時(shí),js 文件的 hash 值也會(huì)改變。這時(shí)候,contenthash 就派上用場(chǎng)了。
ETag計(jì)算
Nginx
Nginx 官方默認(rèn)的 ETag 計(jì)算方式是為"文件最后修改時(shí)間16進(jìn)制-文件長度16進(jìn)制"。
例:ETag:“59e72c84-2404”
Express
Express 框架使用了 serve-static 中間件來配置緩存方案,其中,使用了一個(gè)叫 etag 的 npm 包來實(shí)現(xiàn) etag 計(jì)算。從其源碼可以看出,有兩種計(jì)算方式:
方式一:使用文件大小和修改時(shí)間
function stattag (stat) {
var mtime = stat.mtime.getTime().toString(16)
var size = stat.size.toString(16)
return " + size + - + mtime + "
}
方式二:使用文件內(nèi)容的hash值和內(nèi)容長度
function entitytag (entity) {
if (entity.length === 0) {
// fast-path empty
return "0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"
}
// compute hash of entity
var hash = crypto
.createHash( sha1 )
.update(entity, utf8 )
.digest( base64 )
.substring(0, 27)
// compute length of entity
var len = typeof entity === string
? Buffer.byteLength(entity, utf8 )
: entity.length
return " + len.toString(16) + - + hash + "
}
ETag 與 Last-Modified 誰優(yōu)先
協(xié)商緩存,有 ETag 和 Last-Modified 兩個(gè)字段。那當(dāng)這兩個(gè)字段同時(shí)存在的時(shí)候,會(huì)優(yōu)先以哪個(gè)為準(zhǔn)呢?
在 Express 中,使用了 fresh 這個(gè)包來判斷是否是最新的資源。主要源碼如下:
function fresh (reqHeaders, resHeaders) {
// fields
var modifiedSince = reqHeaders[ if-modified-since ]
var noneMatch = reqHeaders[ if-none-match ]
// unconditional request
if (!modifiedSince && !noneMatch) {
return false
}
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://tools.ietf.org/html/rfc2616#section-14.9.4
var cacheControl = reqHeaders[ cache-control ]
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
// if-none-match
if (noneMatch && noneMatch !== * ) {
var etag = resHeaders[ etag ]
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === W/ + etag || W/ + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
// if-modified-since
if (modifiedSince) {
var lastModified = resHeaders[ last-modified ]
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
return true
}
我們可以看到,如果不是強(qiáng)制刷新,而且請(qǐng)求頭帶上了 if-modified-since 和 if-none-match 兩個(gè)字段,則先判斷 etag,再判斷 last-modified。當(dāng)然,如果你不喜歡這種策略,也可以自己實(shí)現(xiàn)一個(gè)。
后端需要怎么設(shè)置
上文主要說的是前端如何進(jìn)行打包,那后端怎么做呢?我們知道,瀏覽器是根據(jù)響應(yīng)頭的相關(guān)字段來決定緩存的方案的。所以,后端的關(guān)鍵就在于,根據(jù)不同的請(qǐng)求返回對(duì)應(yīng)的緩存字段。以 nodejs 為例,如果需要瀏覽器強(qiáng)緩存,我們可以這樣設(shè)置:
res.setHeader( Cache-Control , public, max-age=xxx );
如果需要協(xié)商緩存,則可以這樣設(shè)置:
res.setHeader( Cache-Control , public, max-age=0 );
res.setHeader( Last-Modified , xxx);
res.setHeader( ETag , xxx);
總結(jié)
在做前端緩存時(shí),我們盡可能設(shè)置長時(shí)間的強(qiáng)緩存,通過文件名加 hash 的方式來做版本更新。在代碼分包的時(shí)候,應(yīng)該將一些不常變的公共庫獨(dú)立打包出來,使其能夠更持久的緩存。
??愛心三連擊 1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長指北,回復(fù)「1」加入高級(jí)前端交流群!「在這里有好多 前端 開發(fā)者,會(huì)討論 前端 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
