持久化存儲與HTTP緩存

本文主要學習一下一些高級的HTTP知識,例如Session LocalStorage Cache-Control Expires ETag

其實主要就是涉及到了持久化存儲與緩存的技術

在此之前已經學習了Cookie的相關知識,其中Cookie有個缺點可以人為修改,有一定的安全隱患。

所以,針對這個缺點,誕生了Session

Session

一般來說Session是基於Cookie實現的,它利用一個sessionId把用戶的敏感數據隱藏起來,除非暴力窮舉才有可能獲得敏感數據。

sessionId

我們使用Cookie的時候,一般是伺服器給用戶一個響應頭,設置Cookie

response.setHeader(Set-Cookie, sign_in_email=...;HTTPOnly)

既然Session還是基於Cookie實現的,那麼還是應該在Set-Cookie上搞事情。

//預先在伺服器端預留對象準備存儲各種sessionlet sessions = {}...let sessionId = Math.random() * 100000sessions[sessionId] = {sign_in_email: email}response.setHeader(Set-Cookie, `sessionId=${sessionId};HTTPOnly`)

使用隨機數來做sessionId,最終只是把這串隨機數暴露給外界,而真正的信息卻保存在了伺服器端的sessions對象裡面。它就像一個密碼簿一樣,有效的信息與sessionId一一對應,這是伺服器的事,保證了安全性。

當下次用戶訪問該網站的其他頁面的時候,就會帶著登錄時伺服器給的這個sessionId,伺服器獲得這個sessionId後,然後一轉化就知道是正確的用戶了。

let sessions = { sessionId: { sign_in_email: ... }}

持久化存儲

在HTML裡面js文件裡面的變數或對象,每當網頁刷新的時候,就會死掉,又重新生成,雖然還是那個a,但是刷新後已經是另一塊內存了。既然它也沒變,我們為什麼不把它一直保留著呢,即使刷新了a還是那個a,也就是持久化存儲的意義。以前使用Cookie做這個功能,不過Cookie每次發請求會把Cookie裡面的所有東西都帶著去伺服器,加重內存的負擔,而且請求響應時間長,所以html5給了一個新的API localStorage

關於Cookie如何工作的,我發現這篇文章寫得特別好

聊一聊 cookie - WEB前端路上踩過的坑兒 - SegmentFault 思否segmentfault.com圖標

LocalStorage

它本質上還是個hash,不過是存在於瀏覽器端的,不同於session存在與伺服器端的hash。一般存儲的都是沒有用的或者不敏感的信息。

localStorage是window的全局屬性,常用的有三個方法

//1. 添加鍵、值localStorage.setItem(a, ...)//2. 獲得鍵、值localStorage.getItem(a)//3.清空localStoragelocalStorage.clear()

注意,它存的值全是字元串,即使你寫的像對象也沒有卵用。

如果想存儲字元串需要用到JSON.stringify( )

一個實際應用

很簡單的一個例子:網站進行更新了,用戶登錄進來了,想提示用戶一下---我有新東西啦,這個提示並不應該在每次刷新的時候反覆告訴用戶,只是在第一次用戶進來的時候告訴他即可。

let already = localStorage.getItem(已經提示過了) if (!already) { alert(我們的網站新進了一些貨物,您看一下有沒有您需要的啊O(∩_∩)O~) localStorage.setItem(已經提示過了, true) } else {}

當第一次訪問的時候,already為null,所以進入if代碼片段,提示用戶一次,接著把already設為true,不會進入if,也就不再提示了。

不基於Cookiesession

學習了localStorage,就可以搞一些黑科技了,前面說了,session一般是基於Cookie的,那麼有沒有例外呢。

有的。利用查詢參數和localStorage可是實現sessionId`。

小結一下

  1. Cookie的特點

  • 伺服器通過 Set-Cookie 頭給客戶端一串字元串
  • 客戶端每次訪問相同域名的網頁時,必須帶上這段字元串
  • 客戶端要在一段時間內保存這個Cookie
  • Cookie 默認在用戶關閉頁面後就失效,後台代碼可以任意設置 Cookie 的過期時間。比如max-age和後面要講的Expires
  • 大小大概在 4kb 以內
  1. Session的特點

  • 將 SessionID(隨機數)通過 Cookie 發給客戶端
  • 客戶端訪問伺服器時,伺服器讀取 SessionID
  • 伺服器有一塊內存(哈希表)保存了所有 session
  • 通過 SessionID 我們可以得到對應用戶的隱私信息,如 id、email
  • 這塊內存(哈希表)就是伺服器上的所有 session
  1. LocalStorage的特點

  • LocalStorage 跟 HTTP 無關
  • 也就是說發送任何請求都不會帶上 LocalStorage 的值
  • 只有相同域名的頁面才能互相讀取 LocalStorage(沒有同源那麼嚴格)
  • 每個域名 localStorage 最大存儲量為 5Mb 左右(每個瀏覽器不一樣)
  • 常用場景:記錄有沒有提示過用戶(沒有用的信息,不能記錄密碼等敏感信息)
  • LocalStorage 永久有效,除非用戶清理緩存

SessionStorage

會話存儲主要特點與localStorage基本相同,最大的不同是SessionStorage在用戶關閉頁面(會話結束)後就失效。

HTTP緩存技術三兄弟

假如說我們要訪問的的文件比較大,我們請求完之後,下載需要花很長時間,當我們刷新頁面的時候,雖然文件沒有任何更新,但是我們又從伺服器端下載了一遍大文件,導致每次響應時間依然很長。

通過上圖的實驗可以看到localhost的請求響應很快,10ms;而default.cssmain.js文件較大,響應時間是localhost的25倍,而jq文件使用了cdn加速,是從內存的緩存中獲得的,幾乎瞬間。如果每次都這樣的話,用戶體驗肯定很差。


那麼我們能不能在第一次響應完畢之後,如果資源沒有更新,就不去伺服器端下載,而是去某個地方獲得呢?

答案是肯定的,可以實現,通過緩存,正如上圖的jq實現的方法一樣。

這部分可以作為web性能優化的一個方法。

Cache-Control

通過max-age設置緩存的有效時間(持續時間)

if (path === /css/default.css){ let string = fs.readFileSync(./css/default.css, utf8) response.setHeader(Content-Type, text/css;charset=utf-8) response.setHeader(Cache-Control, max-age=1000000) response.write(string) response.end() }

在響應頭裡面加上Cache-Control,表示在100000秒內不要再去向伺服器要這個資源了,就從我的內存緩存裡面獲得。

雖然使用了緩存技術,不過有一點疑惑的就是有時候從硬碟的緩存裡面獲得,這個速度提升並不大,但是仍然避免了向伺服器再次發起請求獲得資源的過程;有時候從內存的緩存裡面獲得,這個就特別快了。大概是因為內存的緩存特別快吧。

通常我們把Cache-Control的有效時間設的很長。

以經常逛得知乎為例。

如果一個文件長期不變,把它設為從緩存裡面獲得,知乎設置了32596169秒的有效時間,超過了1年=31536000秒的時間。

首頁盡量不用緩存技術

我們刷一些論壇性質的或者新聞性質的網站,注重時效性,一般會把爆炸性的、高質量的內容放到首頁去,如果我們看了一會,想刷新看看新的更新的內容,而你設了緩存,看到的還是10分鐘之前的首頁,那就太尷尬了?……

所以首頁盡量不用緩存技術,只對那些長期不變的文件、圖片等使用緩存技術。

還是以知乎為例。

對於知乎的Cache-Control的寫法我是比較懵逼的。

MDN的語法

MDN推薦關閉緩存的寫法是Cache-Control: no-cache, no-store, must-revalidate

那麼如果有的資源確實被更新了,如何去更新緩存呢。

更新緩存

通過伺服器端代碼server.js我們可以發現

if (path === /js/main.js) { ... response.setHeader(Cache-Control, max-age=1000000) ...} else if (path === /css/default.css){ ... response.setHeader(Cache-Control, max-age=1000000) ...}

只要當URL符合要求的時候,會使用緩存技術,不去發起請求重新下載資源。

所以當文件確實被更新了之後,我們可以改變URL,那麼就會去重新下載新的文件了。

既然我們的網頁入口是html,可以在這裡面動手腳

...<script src="./js/main.js?V2"></script>...

當你更新代碼之後,理論上只需要在URL上添加查詢參數?V2即可。


我們還是去知乎看看他們的例子。

可以看到知乎也是把URL改了,只不過比我那種高級,它在文件名字動了手腳,大概是用了什麼框架或者處理工具吧,不過更新緩存的思路上是一樣的。文件變了,知乎就把文件緩存的URL填點東西;沒變的話,就緩存一年,在你的硬碟某處睡一年^_^。

小結一下

使用緩存就用response.setHeader(Cache-Control, max-age=100000),當你想更新的時候就改變文件的URL

當然,緩存存多了,你的硬碟估計就爆了,瀏覽器會去權衡這些的,應該優先清楚哪些緩存,是瀏覽器的事。

俗話說得好啊,吃井不忘挖井人啊,要學會憶苦思甜啊,我們現在用的可爽的Cache-Control也不是憑空冒出來的,是有歷史原因的,以前呢,是用Expires實現緩存的技術。

Expires

Expires的英文是到期的意思,很明顯是與緩存有關的技術,不過從其英文意思也能看出它是到某個時間點截止的意思,不是Cache-Control的有效時間。

從語法和示例可以看出它是基於格林威治時間的。

我們還要處理一下時間

var d = new Date() //Sat Feb 10 2018 11:18:54 GMT+0800 (CST)d.toGMTString() //"Sat, 10 Feb 2018 03:18:54 GMT"

能看出來,這個響應頭的最大的弊端在於,時間戳是與你的本地時間關聯的

如果本地電腦的時間系統錯亂了,而且這種毛病還真的時常發生,那你的緩存就毫無作用了。maybe這就是HTTP要升級這個響應頭的原因吧O(∩_∩)O~

Cache-ControlExpires共同存在的時候

如果還有一個 設置了 "max-age" 或者 "s-max-age" 指令的<code>Cache-Control</code>響應頭,那麼 Expires 頭就會被忽略。

關於緩存的技術,還有最後一個兄弟ETag,在搞定它之前,先來學習一下它的小跟班MD5

MD5

MD5是一個摘要演算法。經常用於比較兩個文件是否完全一樣,如果有一點不一樣,誤差會放大。例如我們經常重裝系統的話,有良心的系統提供者會給你一個對應的MD5值,當你下載完畢後,查看你下載的系統的MD5值是否與官方提供給你的一樣,確保是否會因為網路原因導致你下載的東西不完整。

Linux系統裡面使用md5sum指令進行MD5校驗

第一個紅框裡面就是1.txt文件(內容設定為123456)的MD5值,第二個紅框裡面就是1-copy文件(內容被我改為了123460)的MD5值。

nodejs裡面如何使用呢,Google後發現有npm的MD5。

npm install md5...//在server.js引入var md5 = require(md5);

準備工作做完,可以搞ETag了。

ETag

The ETag HTTP response header is an identifier for a specific version of a resource.It allows caches to be more efficient, and saves bandwidth, as a web server does not need to send a full response if the content has not changed. On the other side, if the content has changed, etags are useful to help prevent simultaneous updates of a resource from overwriting each other ("mid-air collisions").

If the resource at a given URL changes, a new Etag value must be generated. Etags are therefore similar to fingerprints and might also be used for tracking purposes by some servers. A comparison of them allows to quickly determine whether two representations of a resource are the same, but they might also be set to persist indefinitely by a tracking server.

  • 這個響應頭是特定資源版本的標識符。
  • 如果給定URL中的資源更改,則一定要生成新的Etag值。因此Etags類似於指紋,也可能被某些伺服器用於跟蹤。 比較etags能快速確定此資源是否變化,但也可能被跟蹤伺服器永久存留。

可以看出ETag應該是一串值,此時上一節的MD5就派上用場了,我們使用MD5來比較前後兩次請求文件的內容。

當某個URL來訪問伺服器的資源的時候,如果伺服器設置了響應頭ETag:一串md5值,那麼

現在沒有什麼其他變化,如果第二次刷新的話,你會發現

請求頭多了一個If-None-Match:一串MD5值

比較上述兩圖,我的main.js沒有改變過,發現ETag:一串md5值If-None-Match:一串MD5值的一樣,稍微一思考的話,就能明白,第二次刷新的時候如果我的main.js變了的話,那麼

第二次向伺服器發起請求,下載的main.jsETag的MD5值必然不同了。

根據這個現象,然後結合MDN文檔

ETag頭的另一個典型用例是緩存未更改的資源。 如果用戶再次訪問給定的URL(設有ETag欄位),顯示資源過期了且不可用,客戶端就發送值為ETag的<code>If-None -Match</code> header欄位:

If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

伺服器將客戶端的ETag(作為If-None-Match欄位的值一起發送)與其當前版本的資源的ETag進行比較,如果兩個值匹配(即資源未更改),伺服器將返回不帶任何內容的<code>304</code>未修改狀態,告訴客戶端緩存版本可用(新鮮)。

可以推理出如下的代碼了:

if (path === /js/main.js) { let string = fs.readFileSync(./js/main.js, utf8) response.setHeader(Content-Type, application/javascript;charset=utf-8) let fileMd5 = md5(string) response.setHeader(ETag, fileMd5) if (request.headers[if-none-match] === fileMd5) { response.statusCode = 304 } else { response.write(string) } response.end() }

304狀態碼的含義

HTTP 304 說明無需再次傳輸請求的內容,也就是說可以使用緩存的內容。這通常是在一些安全的方法(safe),例如<code>GET</code> 或<code>HEAD</code> 或在請求中附帶了頭部信息: <code>If-None-Match</code>或<code>If-Modified-Since</code>。

304和緩存的區別:

  1. 緩存不會發起請求了,直接從內存或者硬碟中獲得
  2. 304依然會發起請求與響應,只不過響應的第四部分不用再次下載了,因為沒有更改,所以還是第一次下載的資源。

幾個常見的考題

Cookie和Session的區別

  1. Cookie是存放在瀏覽器端的數據,每次都隨請求發送給 Server。存儲cookie是瀏覽器提供的功能。cookie 其實是存儲在瀏覽器中的純文本,瀏覽器的安裝目錄下會專門有一個 cookie 文件夾來存放各個域下設置的cookie
  2. 而Session是存放在伺服器端的內存中,其 Session ID 是通過 Cookie 發送給客戶端的,這個Session ID每次都隨請求發送給 Server。

Cookie 和 LocalStorage 的區別

  1. Set-Cookie之後,用戶的每次訪問伺服器,請求裡面都會帶著Cookie到伺服器上,與HTTP有關,而LocalStorage不用發到伺服器端,它是存儲在瀏覽器裡面的,與HTTP無關,是瀏覽器的屬性,window.localStorage
  2. Cookie一般比較小,大約4k左右,而LocalStorage大約能用5M
  3. Cookie默認會在用戶關閉頁面後失效,不過後端可以設置保存時間,而LocalStorage永久有效,除非用戶手動清理。

LocalStorage 和 SessionStorage 的區別

  1. LocalStorage永久有效,除非用戶手動清理localStorage.clear()。不會自動過期
  2. 但是SessionStorage在會話結束後就會失效,也就是用戶關閉了頁面,就失效了。會自動過期

Cookie 如何設置過期時間?如何刪除 Cookie?

  1. 設置過期時間:Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>

data是格林威治時間,響應頭裡面應該這麼寫代碼

response.setHeader(Expires, Fri, 09 Feb 2018 11:29:48 GMT)

也就是說Cookie在格林威治時間的2018年2月9號的11點29分48秒失效。

  1. 設置cookie過期時間小於當前時間,那麼就會刪除該cookie。

function deleteCookie(name) { document.cookie = name + =; expires=Thu, 01 Jan 1970 00:00:01 GMT;}

Cache-Control: max-age=1000 緩存 與 ETag 的「緩存」有什麼區別?

  1. Cache-Control: max-age=1000的緩存 是直接不發請求的,1000秒內相同URL的用戶請求資源的時候,不會再去發請求訪問伺服器了,直接從本地內存的緩存裡面獲取
  2. ETag的緩存是不管怎麼樣都要發起請求,第二次訪問的是時候會多一個請求頭If-None-Match : md5值,如果兩次請求之間的MD5值相同就不會去下載新的文件,響應體是第一次下載的;如果MD5值變了,就要去下載新的文件。

推薦閱讀:

前端數據流哲學
八個例子講解現代前端框架前置知識(講義)
【前端資訊】TypeScript 2.7 發布

TAG:前端工程師 | 前端入門 | 前端開發 |