好好了解一下Cookie(強(qiáng)烈推薦)
Cookie的誕生
由于HTTP協(xié)議是無狀態(tài)的,而服務(wù)器端的業(yè)務(wù)必須是要有狀態(tài)的。Cookie誕生的最初目的是為了存儲web中的狀態(tài)信息,以方便服務(wù)器端使用。比如判斷用戶是否是第一次訪問網(wǎng)站。目前最新的規(guī)范是RFC 6265,它是一個(gè)由瀏覽器服務(wù)器共同協(xié)作實(shí)現(xiàn)的規(guī)范。
Cookie的處理分為:
服務(wù)器像客戶端發(fā)送cookie
瀏覽器將cookie保存
之后每次http請求瀏覽器都會將cookie發(fā)送給服務(wù)器端
服務(wù)器端的發(fā)送與解析
發(fā)送cookie
服務(wù)器端像客戶端發(fā)送Cookie是通過HTTP響應(yīng)報(bào)文實(shí)現(xiàn)的,在Set-Cookie中設(shè)置需要像客戶端發(fā)送的cookie,cookie格式如下:
Set-Cookie: "name=value;domain=.domain.com;path=/;expires=Sat, 11 Jun 2016 11:29:42 GMT;HttpOnly;secure"
其中name=value是必選項(xiàng),其它都是可選項(xiàng)。Cookie的主要構(gòu)成如下:
name:一個(gè)唯一確定的cookie名稱。通常來講cookie的名稱是不區(qū)分大小寫的。
value:存儲在cookie中的字符串值。最好為cookie的name和value進(jìn)行url編碼
domain:cookie對于哪個(gè)域是有效的。所有向該域發(fā)送的請求中都會包含這個(gè)cookie信息。這個(gè)值可以包含子域(如:
yq.aliyun.com),也可以不包含它(如:.aliyun.com,則對于aliyun.com的所有子域都有效).
path: 表示這個(gè)cookie影響到的路徑,瀏覽器跟會根據(jù)這項(xiàng)配置,像指定域中匹配的路徑發(fā)送cookie。
expires:失效時(shí)間,表示cookie何時(shí)應(yīng)該被刪除的時(shí)間戳(也就是,何時(shí)應(yīng)該停止向服務(wù)器發(fā)送這個(gè)cookie)。如果不設(shè)置這個(gè)時(shí)間戳,瀏覽器會在頁面關(guān)閉時(shí)即將刪除所有cookie;不過也可以自己設(shè)置刪除時(shí)間。這個(gè)值是GMT時(shí)間格式,如果客戶端和服務(wù)器端時(shí)間不一致,使用expires就會存在偏差。
max-age: 與expires作用相同,用來告訴瀏覽器此cookie多久過期(單位是秒),而不是一個(gè)固定的時(shí)間點(diǎn)。正常情況下,max-age的優(yōu)先級高于expires。
HttpOnly: 告知瀏覽器不允許通過腳本document.cookie去更改這個(gè)值,同樣這個(gè)值在document.cookie中也不可見。但在http請求張仍然會攜帶這個(gè)cookie。注意這個(gè)值雖然在腳本中不可獲取,但仍然在瀏覽器安裝目錄中以文件形式存在。這項(xiàng)設(shè)置通常在服務(wù)器端設(shè)置。
secure: 安全標(biāo)志,指定后,只有在使用SSL鏈接時(shí)候才能發(fā)送到服務(wù)器,如果是http鏈接則不會傳遞該信息。就算設(shè)置了secure 屬性也并不代表他人不能看到你機(jī)器本地保存的 cookie 信息,所以不要把重要信息放cookie就對了服務(wù)器端設(shè)置
cookie示例如下:
var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { res.setHeader('status', '200 OK'); res.setHeader('Set-Cookie', 'isVisit=true;domain=.yourdomain.com;path=/;max-age=1000'); res.write('Hello World'); res.end(); }).listen(8888); console.log('running localhost:8888')
直接設(shè)置Set-Cookie過于原始,我們可以對cookie的設(shè)置過程做如下封裝:
var serilize = function(name, val, options) { if (!name) { throw new Error("coolie must have name"); } var enc = encodeURIComponent; var parts = []; val = (val !== null && val !== undefined) ? val.toString() : ""; options = options || {}; parts.push(enc(name) + "=" + enc(val)); // domain中必須包含兩個(gè)點(diǎn)號 if (options.domain) { parts.push("domain=" + options.domain); } if (options.path) { parts.push("path=" + options.path); } // 如果不設(shè)置expires和max-age瀏覽器會在頁面關(guān)閉時(shí)清空cookie if (options.expires) { parts.push("expires=" + options.expires.toGMTString()); } if (options.maxAge && typeof options.maxAge === "number") { parts.push("max-age=" + options.maxAge); } if (options.httpOnly) { parts.push("HTTPOnly"); } if (options.secure) { parts.push("secure"); } return parts.join(";"); }
需要注意的是,如果給cookie設(shè)置一個(gè)過去的時(shí)間,瀏覽器會立即刪除該cookie;此外domain項(xiàng)必須有兩個(gè)點(diǎn),因此不能設(shè)置為localhost:
something that wasn't made clear to me here and totally confused me for a while was that domain names must contain at least two dots (.),hence 'localhost' is invalid and the browser will refuse to set the cookie!
服務(wù)器端解析cookie
cookie可以設(shè)置不同的域與路徑,所以對于同一個(gè)name value,在不同域不同路徑下是可以重復(fù)的,瀏覽器會按照與當(dāng)前請求url或頁面地址最佳匹配的順序來排定先后順序
所以當(dāng)前端傳遞到服務(wù)器端的cookie有多個(gè)重復(fù)name value時(shí),我們只需要最匹配的那個(gè),也就是第一個(gè)。服務(wù)器端解析代碼如下:
var parse = function(cstr) { if (!cstr) { return null; } var dec = decodeURIComponent; var cookies = {}; var parts = cstr.split(/\s*;\s*/g); parts.forEach(function(p){ var pos = p.indexOf('='); // name 與value存入cookie之前,必須經(jīng)過編碼 var name = pos > -1 ? dec(p.substr(0, pos)) : p; var val = pos > -1 ? dec(p.substr(pos + 1)) : null; //只需要拿到最匹配的那個(gè) if (!cookies.hasOwnProperty(name)) { cookies[name] = val; }/* else if (!cookies[name] instanceof Array) { cookies[name] = [cookies[name]].push(val); } else { cookies[name].push(val); }*/ }); return cookies; }
客戶端的存取
瀏覽器將后臺傳遞過來的cookie進(jìn)行管理,并且允許開發(fā)者在JavaScript中使用document.cookie來存取cookie。但是這個(gè)接口使用起來非常蹩腳。它會因?yàn)槭褂盟姆绞讲煌憩F(xiàn)出不同的行為。
當(dāng)用來獲取屬性值時(shí),document.cookie返回當(dāng)前頁面可用的(根據(jù)cookie的域、路徑、失效時(shí)間和安全設(shè)置)所有的字符串,字符串的格式如下:
"name1=value1;name2=value2;name3=value3";
當(dāng)用來設(shè)置值的時(shí)候,document.cookie屬性可設(shè)置為一個(gè)新的cookie字符串。這個(gè)字符串會被解釋并添加到現(xiàn)有的cookie集合中。如:
document.cookie = "_fa=aaaffffasdsf;domain=.dojotoolkit.org;path=/"
設(shè)置document.cookie并不會覆蓋cookie,除非設(shè)置的name value domain path都與一個(gè)已存在cookie重復(fù)。
由于cookie的讀寫非常不方便,我們可以自己封裝一些函數(shù)來處理cookie,主要是針對cookie的添加、修改、刪除。
var cookieUtils = { get: function(name){ var cookieName=encodeURIComponent(name) + "="; //只取得最匹配的name,value var cookieStart = document.cookie.indexOf(cookieName); var cookieValue = null; if (cookieStart > -1) { // 從cookieStart算起 var cookieEnd = document.cookie.indexOf(';', cookieStart); //從=后面開始 if (cookieEnd > -1) { cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd)); } else { cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, document.cookie.length)); } } return cookieValue; }, set: function(name, val, options) { if (!name) { throw new Error("coolie must have name"); } var enc = encodeURIComponent; var parts = []; val = (val !== null && val !== undefined) ? val.toString() : ""; options = options || {}; parts.push(enc(name) + "=" + enc(val)); // domain中必須包含兩個(gè)點(diǎn)號 if (options.domain) { parts.push("domain=" + options.domain); } if (options.path) { parts.push("path=" + options.path); } // 如果不設(shè)置expires和max-age瀏覽器會在頁面關(guān)閉時(shí)清空cookie if (options.expires) { parts.push("expires=" + options.expires.toGMTString()); } if (options.maxAge && typeof options.maxAge === "number") { parts.push("max-age=" + options.maxAge); } if (options.httpOnly) { parts.push("HTTPOnly"); } if (options.secure) { parts.push("secure"); } document.cookie = parts.join(";"); }, delete: function(name, options) { options.expires = new Date(0);// 設(shè)置為過去日期 this.set(name, null, options); } }
緩存優(yōu)點(diǎn)
通常所說的Web緩存指的是可以自動(dòng)保存常見http請求副本的http設(shè)備。對于前端開發(fā)者來說,瀏覽器充當(dāng)了重要角色。除此外常見的還有各種各樣的代理服務(wù)器也可以做緩存。當(dāng)Web請求到達(dá)緩存時(shí),緩存從本地副本中提取這個(gè)副本內(nèi)容而不需要經(jīng)過服務(wù)器。這帶來了以下優(yōu)點(diǎn):
緩存減少了冗余的數(shù)據(jù)傳輸,節(jié)省流量
緩存緩解了帶寬瓶頸問題。不需要更多的帶寬就能更快加載頁面
緩存緩解了瞬間擁塞,降低了對原始服務(wù)器的要求。
緩存降低了距離延時(shí), 因?yàn)閺妮^遠(yuǎn)的地方加載頁面會更慢一些。
緩存種類
緩存可以是單個(gè)用戶專用的,也可以是多個(gè)用戶共享的。專用緩存被稱為私有緩存,共享的緩存被稱為公有緩存。
私有緩存
私有緩存只針對專有用戶,所以不需要很大空間,廉價(jià)。Web瀏覽器中有內(nèi)建的私有緩存——大多數(shù)瀏覽器都會將常用資源緩存在你的個(gè)人電腦的磁盤和內(nèi)存中。如Chrome瀏覽器的緩存存放位置就在:C:\Users\Your_Account\AppData\Local\Google\Chrome\User Data\Default中的Cache文件夾和Media Cache文件夾。
公有緩存
公有緩存是特殊的共享代理服務(wù)器,被稱為緩存代理服務(wù)器或代理緩存(反向代理的一種用途)。公有緩存會接受來自多個(gè)用戶的訪問,所以通過它能夠更好的減少冗余流量。
下圖中每個(gè)客戶端都會重復(fù)的向服務(wù)器訪問一個(gè)資源(此時(shí)還不在私有緩存中),這樣它會多次訪問服務(wù)器,增加服務(wù)器壓力。而使用共享的公有緩存時(shí),緩存只需要從服務(wù)器取一次,以后不用再經(jīng)過服務(wù)器,能夠顯著減輕服務(wù)器壓力。
事實(shí)上在實(shí)際應(yīng)用中通常采用層次化的公有緩存,基本思想是在靠近客戶端的地方使用小型廉價(jià)緩存,而更高層次中,則逐步采用更大、功能更強(qiáng)的緩存在裝載多用戶共享的資源。
緩存處理流程
而對于前端開發(fā)者來說,我們主要跟瀏覽器中的緩存打交道,所以上圖流程簡化為:
下面這張圖展示了某一網(wǎng)站,對不同資源的請求結(jié)果,其中可以看到有的資源直接從緩存中讀取,有的資源跟服務(wù)器進(jìn)行了再驗(yàn)證,有的資源重新從服務(wù)器端獲取。
注意,我們討論的所有關(guān)于緩存資源的問題,都僅僅針對GET請求。而對于POST, DELETE, PUT這類行為性操作通常不做任何緩存
新鮮度限值
HTTP通過緩存將服務(wù)器資源的副本保留一段時(shí)間,這段時(shí)間稱為新鮮度限值。這在一段時(shí)間內(nèi)請求相同資源不會再通過服務(wù)器。HTTP協(xié)議中Cache-Control和 Expires可以用來設(shè)置新鮮度的限值,前者是HTTP1.1中新增的響應(yīng)頭,后者是HTTP1.0中的響應(yīng)頭。二者所做的事時(shí)都是相同的,但由于Cache-Control使用的是相對時(shí)間,而Expires可能存在客戶端與服務(wù)器端時(shí)間不一樣的問題,所以我們更傾向于選擇Cache-Control。
Cache-Control
下面我們來看看Cache-Control都可以設(shè)置哪些屬性值:
max-age(單位為s)指定設(shè)置緩存最大的有效時(shí)間,定義的是時(shí)間長短。當(dāng)瀏覽器向服務(wù)器發(fā)送請求后,在max-age這段時(shí)間里瀏覽器就不會再向服務(wù)器發(fā)送請求了。
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta http-equiv="X-UA-Compatible" content="IE=EDGE" /> <title>Web Cache</title> <link rel="shortcut icon" href="./shortcut.png"> <script> </script> </head> <body class="claro"> <img src="./cache.png"> </body> </html> var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { if (req.url === '/' || req.url === '' || req.url === '/index.html') { fs.readFile('./index.html', function(err, file) { console.log(req.url) //對主文檔設(shè)置緩存,無效果 res.setHeader('Cache-Control', "no-cache, max-age=" + 5); res.setHeader('Content-Type', 'text/html'); res.writeHead('200', "OK"); res.end(file); }); } if (req.url === '/cache.png') { fs.readFile('./cache.png', function(err, file) { res.setHeader('Cache-Control', "max-age=" + 5);//緩存五秒 res.setHeader('Content-Type', 'images/png'); res.writeHead('200', "Not Modified"); res.end(file); }); } }).listen(8888)
當(dāng)在5秒內(nèi)第二次訪問頁面時(shí),瀏覽器會直接從緩存中取得資源
public 指定響應(yīng)可以在代理緩存中被緩存,于是可以被多用戶共享。如果沒有明確指定private,則默認(rèn)為public。
private 響應(yīng)只能在私有緩存中被緩存,不能放在代理緩存上。對一些用戶信息敏感的資源,通常需要設(shè)置為private。
no-cache 表示必須先與服務(wù)器確認(rèn)資源是否被更改過(依靠If-None-Match和Etag),然后再?zèng)Q定是否使用本地緩存。
如果上文中關(guān)于cache.png的處理改成下面這樣,則每次訪問頁面,瀏覽器都需要先去服務(wù)器端驗(yàn)證資源有沒有被更改。
fs.readFile('./cache.png', function(err, file) { console.log(req.headers); console.log(req.url) if (!req.headers['if-none-match']) { res.setHeader('Cache-Control', "no-cache, max-age=" + 5); res.setHeader('Content-Type', 'images/png'); res.setHeader('Etag', "ffff"); res.writeHead('200', "Not Modified"); res.end(file); } else { if (req.headers['if-none-match'] === 'ffff') { res.writeHead('304', "Not Modified"); res.end(); } else { res.setHeader('Cache-Control', "max-age=" + 5); res.setHeader('Content-Type', 'images/png'); res.setHeader('Etag', "ffff"); res.writeHead('200', "Not Modified"); res.end(file); } } });
no-store 絕對禁止緩存任何資源,也就是說每次用戶請求資源時(shí),都會向服務(wù)器發(fā)送一個(gè)請求,每次都會下載完整的資源。通常用于機(jī)密性資源。
關(guān)于Cache-Control的使用,見下面這張圖(來自大額)
客戶端的新鮮度限值
Cache-Control不僅僅可以在響應(yīng)頭中設(shè)置,還可以在請求頭中設(shè)置。瀏覽器通過請求頭中設(shè)置Cache-Control可以決定是否從緩存中讀取資源。這也是為什么有時(shí)候點(diǎn)擊瀏覽器刷新按鈕和在地址欄回車,在NetWork模塊中看到完全不同的結(jié)果
Expires
不推薦使用Expires,它指定的是具體的過期日期而不是秒數(shù)。因?yàn)楹芏喾?wù)器跟客戶端存在時(shí)鐘不一致的情況,所以最好還是使用Cache-Control.
服務(wù)器再驗(yàn)證
瀏覽器或代理緩存中緩存的資源過期了,并不意味著它和原始服務(wù)器上的資源有實(shí)際的差異,僅僅意味著到了要進(jìn)行核對的時(shí)間了。這種情況被稱為服務(wù)器再驗(yàn)證。
如果資源發(fā)生變化,則需要取得新的資源,并在緩存中替換舊資源。
如果資源沒有發(fā)生變化,緩存只需要獲取新的響應(yīng)頭,和一個(gè)新的過期時(shí)間,對緩存中的資源過期時(shí)間進(jìn)行更新即可。
HTTP1.1推薦使用的驗(yàn)證方式是If-None-Match/Etag,在HTTP1.0中則使用If-Modified-Since/Last-Modified。
Etag與If-None-Match
根據(jù)實(shí)體內(nèi)容生成一段hash字符串,標(biāo)識資源的狀態(tài),由服務(wù)端產(chǎn)生。瀏覽器會將這串字符串傳回服務(wù)器,驗(yàn)證資源是否已經(jīng)修改,如果沒有修改,過程如下(圖片來自淺談Web緩存):
上文的demo中我們見到過服務(wù)器端如何驗(yàn)證Etag:
由于Etag有服務(wù)器構(gòu)造,所以在集群環(huán)境中一定要保證Etag的唯一性
If-Modified-Since與Last-Modified
這兩個(gè)是HTTP1.0中用來驗(yàn)證資源是否過期的請求/響應(yīng)頭,這兩個(gè)頭部都是日期,驗(yàn)證過程與Etag類似,這里不詳細(xì)介紹。使用這兩個(gè)頭部來驗(yàn)證資源是否更新時(shí),存在以下問題:
有些文檔資源周期性的被重寫,但實(shí)際內(nèi)容沒有改變。此時(shí)文件元數(shù)據(jù)中會顯示文件最近的修改日期與If-Modified-Since不相同,導(dǎo)致不必要的響應(yīng)。
有些文檔資源被修改了,但修改內(nèi)容并不重要,不需要所有的緩存都更新(比如代碼注釋)
關(guān)于緩存的更新問題,請大家看看這里張?jiān)讫埖幕卮?,本文就不詳?xì)展開了。
本文demo代碼如下:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta http-equiv="X-UA-Compatible" content="IE=EDGE" /> <title>Web Cache</title> <link rel="shortcut icon" href="./shortcut.png"> <script> </script> </head> <body class="claro"> <img src="./cache.png"> </body> </html> var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { if (req.url === '/' || req.url === '' || req.url === '/index.html') { fs.readFile('./index.html', function(err, file) { console.log(req.url) //對主文檔設(shè)置緩存,無效果 res.setHeader('Cache-Control', "no-cache, max-age=" + 5); res.setHeader('Content-Type', 'text/html'); res.writeHead('200', "OK"); res.end(file); }); } if (req.url === '/shortcut.png') { fs.readFile('./shortcut.png', function(err, file) { console.log(req.url) res.setHeader('Content-Type', 'images/png'); res.writeHead('200', "OK"); res.end(file); }) } if (req.url === '/cache.png') { fs.readFile('./cache.png', function(err, file) { console.log(req.headers); console.log(req.url) if (!req.headers['if-none-match']) { res.setHeader('Cache-Control', "max-age=" + 5); res.setHeader('Content-Type', 'images/png'); res.setHeader('Etag', "ffff"); res.writeHead('200', "Not Modified"); res.end(file); } else { if (req.headers['if-none-match'] === 'ffff') { res.writeHead('304', "Not Modified"); res.end(); } else { res.setHeader('Cache-Control', "max-age=" + 5); res.setHeader('Content-Type', 'images/png'); res.setHeader('Etag', "ffff"); res.writeHead('200', "Not Modified"); res.end(file); } } }); } }).listen(8888)
好了,本文關(guān)于cookie的介紹到此結(jié)束了,希望大家能夠喜歡。
相關(guān)文章
JavaScript?Hoisting變量提升機(jī)制實(shí)例解析
這篇文章主要為大家介紹了JavaScript變量提升Hoisting機(jī)制實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11JavaScript的document對象和window對象詳解
JavaScript的document對象和window對象詳解,js經(jīng)常用得到的知識,了解下。2010-12-12JavaScript學(xué)習(xí)筆記(二) js對象
JavaScript學(xué)習(xí)筆記(二) js對象學(xué)習(xí),學(xué)習(xí)js的朋友可以參考下。2011-10-10