前端緩存最佳實(shí)踐
來自:掘金,作者:?黑金團(tuán)隊(duì)
https://juejin.im/post/5c136bd16fb9a049d37efc47
前言
緩存,這是一個(gè)老生常談的話題,也常被作為前端面試的一個(gè)知識點(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 請求頭中的 Cache-Control 和 Expire 兩個(gè)字段控制。Expire 是 HTTP1.0 標(biāo)準(zhǔn)下的字段,在這里我們可以忽略。我們重點(diǎn)來討論的 Cache-Control 這個(gè)字段。
一般,我們會設(shè)置 Cache-Control 的值為 “public, max-age=xxx”,表示在xxx秒內(nèi)再次訪問該資源,均使用本地的緩存,不再向服務(wù)器發(fā)起請求。
顯而易見,如果在xxx秒內(nèi),服務(wù)器上面的資源更新了,客戶端在沒有強(qiáng)制刷新的情況下,看到的內(nèi)容還是舊的。如果說你不著急,可以接受這樣的,那是不是完美?然而,很多時(shí)候不是你想的那么簡單的,如果發(fā)布新版本的時(shí)候,后臺接口也同步更新了,那就gg了。有緩存的用戶還在使用舊接口,而那個(gè)接口已經(jīng)被后臺干掉了。怎么辦?
協(xié)商緩存
協(xié)商緩存最大的問題就是每次都要向服務(wù)器驗(yàn)證一下緩存的有效性,似乎看起來很省事,不管那么多,你都要問一下我是否有效。但是,對于一個(gè)有追求的碼農(nóng),這是不能接受的。每次都去請求服務(wù)器,那要緩存還有什么意義。
最佳實(shí)踐
緩存的意義就在于減少請求,更多地使用本地的資源,給用戶更好的體驗(yàn)的同時(shí),也減輕服務(wù)器壓力。所以,最佳實(shí)踐,就應(yīng)該是盡可能命中強(qiáng)緩存,同時(shí),能在更新版本的時(shí)候讓客戶端的緩存失效。
在更新版本之后,如何讓用戶第一時(shí)間使用最新的資源文件呢?機(jī)智的前端們想出了一個(gè)方法,在更新版本的時(shí)候,順便把靜態(tài)資源的路徑改了,這樣,就相當(dāng)于第一次訪問這些資源,就不會存在緩存的問題了。
偉大的 webpack 可以讓我們在打包的時(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值都會更改。
chunkhash:根據(jù)不同的入口文件(Entry)進(jìn)行依賴文件解析、構(gòu)建對應(yīng)的chunk,生成對應(yīng)的hash值。
contenthash:由文件內(nèi)容產(chǎn)生的hash值,內(nèi)容不同產(chǎn)生的contenthash值也不一樣。
顯然,我們是不會使用第一種的。改了一個(gè)文件,打包之后,其他文件的 hash 都變了,緩存自然都失效了。這不是我們想要的。
那 chunkhash 和 contenthash 的主要應(yīng)用場景是什么呢?
在實(shí)際在項(xiàng)目中,我們一般會把項(xiàng)目中的 css 都抽離出對應(yīng)的 css 文件來加以引用。如果我們使用 chunkhash,當(dāng)我們改了 css 代碼之后,會發(fā)現(xiàn) css 文件 hash 值改變的同時(shí),js 文件的 hash 值也會改變。這時(shí)候,contenthash 就派上用場了。
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í)候,會優(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???????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)制刷新,而且請求頭帶上了 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ù)不同的請求返回對應(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ú)立打包出來,使其能夠更持久的緩存。
分享前端好文,點(diǎn)亮?在看?
