Node - 構(gòu)建web應(yīng)用

基礎(chǔ)功能

對(duì)一個(gè)web應(yīng)用而言弥喉,具體的業(yè)務(wù)中捷兰,我們可能有如下需求:

1.請(qǐng)求方法的判斷

2.URL的路徑解析

3.URL中查詢字符串解析

4.Cookie的解析

5.Session的使用

6.Basic認(rèn)證

7.表單數(shù)據(jù)的解析

8.任意格式文件的上傳處理

請(qǐng)求方法

客戶端向服務(wù)端發(fā)送報(bào)文饲化,服務(wù)端解析報(bào)文,發(fā)現(xiàn)HTTP請(qǐng)求頭時(shí),調(diào)用http_parser模塊解析請(qǐng)求報(bào)文,并將屬性解析出來定義到ServerRequest對(duì)象上嗤朴,其中請(qǐng)求方法被設(shè)置為req.method。在RESTful類web服務(wù)中請(qǐng)求方法十分重要虫溜,使用PUT雹姊、DELETE、POST衡楞、GET來分別決定對(duì)資源的操作行為吱雏。

路徑解析

http_parser將路徑解析為req.url,不包括hash部分寺酪。

查詢字符串

var url = require('url');
var queryString = require('querystring');

var str = 'https://www.iconfont.cn/search/index?q=504&page=3';

console.log(url.parse(str));
// {
//   protocol: 'https:',
//   slashes: true,
//   auth: null,
//   host: 'www.iconfont.cn',
//   port: null,
//   hostname: 'www.iconfont.cn',
//   hash: null,
//   search: '?q=504&page=3',
//   query: 'q=504&page=3',
//   pathname: '/search/index',
//   path: '/search/index?q=504&page=3',
//   href: 'https://www.iconfont.cn/search/index?q=504&page=3' 
// }
console.log(url.parse(str).query); // q=504&page=3
console.log(url.parse(str, true).query) //傳值true 序列化 -> { q: '504', page: '3' }
console.log(queryString.parse(url.parse(str).query)); // { q: '504', page: '3' }

要注意的點(diǎn)是坎背,如果查詢字符串中的鍵出現(xiàn)多次,那么它的值會(huì)是一個(gè)數(shù)組寄雀。

// foo=bar&foo=baz
{
  foo: ['bar', 'baz']
}

cookie

HTTP是一個(gè)無狀態(tài)的協(xié)議,現(xiàn)實(shí)中的業(yè)務(wù)卻是需要一定的狀態(tài)的陨献,否則無法區(qū)分用戶之間的身份盒犹,如何標(biāo)識(shí)和認(rèn)證一個(gè)用戶,最早的方案就是cookie了眨业。

cookie的處理分為如下幾步:

1.服務(wù)器向客戶端發(fā)送cookie

2.瀏覽器將cookie保存

3.之后每次瀏覽器都會(huì)將cookie發(fā)向服務(wù)器

http_parser將cookie解析為ServerRequest對(duì)象的req.headers.cookie急膀。cookie值的格式是:key1=value1;key2=value2

服務(wù)端如何向客戶端發(fā)送cookie,響應(yīng)的cookie值在set-cookie字段中龄捡。

Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

path: 表示這個(gè)cookie影響到的路徑卓嫂,當(dāng)前訪問路徑不滿足時(shí),不發(fā)送這個(gè)cookie聘殖。cookie配置path會(huì)向下傳遞晨雳,配置根路徑行瑞,則所有頁面都會(huì)帶上這個(gè)cookie。

ExpiresMax-Age:表示cookie的過期時(shí)間餐禁,Expires是格林威治時(shí)間血久,指的是過期時(shí)間,Max-age指這條cookie多久后過期帮非,單位為毫秒

HttpOnly告知瀏覽器不允許通過腳本document.cookie去修改cookie的值氧吐,設(shè)置之后,這個(gè)值在document.cookie中不可見末盔,但是在請(qǐng)求時(shí)依然會(huì)發(fā)送到服務(wù)器筑舅。

Secure:當(dāng)Secure值為true時(shí),在HTTP中是無效的陨舱,在HTTPS中才有效

Domain:可以使多個(gè)web服務(wù)器共享cookie翠拣,默認(rèn)是創(chuàng)建cookie的網(wǎng)頁所在的主機(jī)名,不能將一個(gè)cookie的域設(shè)置成服務(wù)器所在域之外的域隅忿。如果a.example.com的頁面創(chuàng)建的cookie把自己的path屬性設(shè)置為“/”心剥,把domain屬性設(shè)置成“.example.com”,那么所有位于a.example.com的網(wǎng)頁和所有位于b.example.com的網(wǎng)頁背桐,以及位于example.com域的其他服務(wù)器上的網(wǎng)頁都可以訪問這個(gè)cookie优烧。

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('; '); 
};

// 服務(wù)器發(fā)送
res.setHeader('Set-Cookie', serialize('isVisit', '1'));
// Set-cookie: isVisit=1;

// 發(fā)送多個(gè)值
res.setHeader('Set-Cookie', [serialize('isVisit', '1'),serialize('user_id', '999')]);
// 這樣在響應(yīng)報(bào)文中會(huì)形成兩條Set-Cookie字段
// Set-Cookie: isVisit=1; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com; 
// Set-Cookie: user_id=999; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com

Cookie的性能影響

  • 減小Cookie的大小:如果在根路徑設(shè)置Cookie链峭,那么幾乎所有子路徑都會(huì)帶上這些Cookie畦娄,但并不是所有頁面都需要,所以一定要合理利用Path弊仪。

  • 為靜態(tài)組件使用不同的域名:為不需要Cookie的靜態(tài)組件換個(gè)域名可以減少無效的Cookie的傳輸熙卡,而且主機(jī)名的不同就能增加瀏覽器并行下載的數(shù)量,但是主機(jī)名不是越多越好励饵,因?yàn)槊慷嘁粋€(gè)域名就多一次DNS查詢驳癌。

Cookie除了可以通過后端添加協(xié)議頭的字段設(shè)置外,在前端瀏覽器中也可以通過JavaScript進(jìn)行修改役听,瀏覽器將Cookie通過document.cookie暴露給JavaScript颓鲜,前端在修改Cookie之后,后續(xù)的網(wǎng)絡(luò)請(qǐng)求中就會(huì)攜帶上修改后的值典予。

Session

通過Cookie甜滨,瀏覽器和服務(wù)器可以實(shí)現(xiàn)狀態(tài)的記錄,但是Cookie是有大小限制瘤袖,而且最大的問題衣摩,是不安全,前后端都能修改捂敌,甚至用戶能直接通過瀏覽器修改Cookie艾扮,為了解決安全問題既琴,Session應(yīng)運(yùn)而生,Session的數(shù)據(jù)只保留在服務(wù)端栏渺,客戶端無法修改呛梆,也無須在協(xié)議中每次都被傳遞。

但是如何將客戶和服務(wù)器中的數(shù)據(jù)一一對(duì)應(yīng)起來磕诊,有兩種實(shí)現(xiàn)方式

基于Cookie來實(shí)現(xiàn)用戶和數(shù)據(jù)的映射

將數(shù)據(jù)放在Session中填物,將口令放在Cookie中,因?yàn)槿绻蛻舳舜鄹牧丝诹罹褪チ擞成潢P(guān)系霎终,也就無法訪問和修改服務(wù)端中的數(shù)據(jù)滞磺,并且session的有效期通常較短,通常為20分鐘莱褒,如果20分鐘客戶端和服務(wù)端沒有交互產(chǎn)生击困,服務(wù)端就會(huì)將數(shù)據(jù)刪除。

一旦服務(wù)器啟用了session广凸,就會(huì)約定一個(gè)鍵作為Session的口令阅茶,比如'session_id',一旦服務(wù)器檢查到用戶請(qǐng)求的Cookie中沒有該值谅海,它就會(huì)為之生成一個(gè)值脸哀,且這個(gè)值是唯一且不重復(fù)的,并且設(shè)置超時(shí)時(shí)間扭吁。

var sessions = {};
var key = 'session_id';
var EXPIRES = 20 * 60 * 1000;
var generate = function (res) {
  var session = {};
  session.id = (new Date()).getTime() + Math.random(); 
  session.cookie = {
    expires: (new Date()).getTime() + EXPIRES 
  };
  sessions[session.id] = session;
  res.setHeader('Set-Cookie', `${key}=${session.id}`) // session_id=1573096605319.127
  return session; 
};

function (req, res) {
  var id = req.cookies[key]; // 獲取瀏覽器發(fā)送的cookie有沒有鍵為session_id的
  // 沒有就生成一個(gè)  session {id: 1573096605319.127, cookie: { expires: 1573097914736} }撞蜂,
  // 并存儲(chǔ)在全局的sessions中  sessions { '1573096605319.127':  {id: 1573096605319.127, cookie: { expires: 1573097914736} }}
  if (!id) { 
    req.session = generate(res); 
  } else {
    var session = sessions[id];  // 根據(jù)id從全局sessions中取出session
    if (session) {
      // 如果session存在 查收過期了
      if (session.cookie.expires > (new Date()).getTime()) {
        // 未過期 設(shè)置新的過期時(shí)間
        session.cookie.expires = (new Date()).getTime() + EXPIRES;
        // 設(shè)置新的session
        req.session = session;
      } else {
        // 從全局sessions中刪除舊的數(shù)據(jù),重新生成
        delete sessions[id];
        req.session = generate(res);
      }
    } else {
      // 根據(jù)id沒有取到session侥袜,重新生成
      req.session = generate(res); 
    }
  }
  handle(req, res); 
}

通過查詢字符串來實(shí)現(xiàn)瀏覽器和服務(wù)器端數(shù)據(jù)的對(duì)應(yīng)

檢查請(qǐng)求的查詢字符串蝌诡,如果沒有值,會(huì)先生成新的URL去重定向枫吧;

var redirect = function (url) {
  res.setHeader('Location', url); 
  res.writeHead(302);
  res.end();
};

Session與內(nèi)存

如果我們都將Session數(shù)據(jù)存在全局變量中浦旱,即位于內(nèi)存中,這樣將會(huì)帶來隱患九杂,如果用戶增多闽寡,很可能就接觸到了內(nèi)存限制的上限,并且內(nèi)存中的數(shù)據(jù)量加大尼酿,必然會(huì)引起垃圾回收的頻繁掃描,引起性能問題植影。

另一個(gè)問題是我們可能會(huì)為了利用多核CPU而啟動(dòng)多個(gè)進(jìn)程裳擎,用戶請(qǐng)求的連接將可能分配到各個(gè)進(jìn)程中,Node的進(jìn)程與進(jìn)程之間是不能直接共享內(nèi)存的思币,用戶的Session可能會(huì)引起錯(cuò)亂鹿响。

為了解決性能問題和Session數(shù)據(jù)無法跨進(jìn)程共享的問題羡微,常用方案就是將Session集中化,將原本可能分散在多個(gè)進(jìn)程里的數(shù)據(jù)惶我,統(tǒng)一轉(zhuǎn)移到集中的數(shù)據(jù)存儲(chǔ)中妈倔。比如Redis,通過這些高效的緩存绸贡,Node進(jìn)程無須在內(nèi)部維護(hù)數(shù)據(jù)對(duì)象盯蝴,垃圾回收問題和內(nèi)存限制問題可以迎刃而解。

采用第三方緩存來存儲(chǔ)Session會(huì)引起網(wǎng)絡(luò)訪問听怕,相比訪問本地磁盤中的數(shù)據(jù)的速度要慢捧挺,盡管如此,還是會(huì)采用第三方高速存儲(chǔ)尿瞭,是因?yàn)椋?/p>

1.Node與緩存服務(wù)保持長(zhǎng)連接闽烙,握手導(dǎo)致的延遲只影響初始化。

2.高速存儲(chǔ)直接在內(nèi)存中進(jìn)行數(shù)據(jù)存儲(chǔ)和訪問声搁。

3.緩存服務(wù)通常與Node進(jìn)程運(yùn)行在相同的機(jī)器上或者相同的機(jī)房里黑竞,網(wǎng)絡(luò)速度受到的影響較小

// 獲取存儲(chǔ)在緩存中的Session數(shù)據(jù),是異步的疏旨。
// 取
store.get(id, function(err, data){})
// 保存
store.save(req.session);

Session與安全

將口令的值加密

const crypto = require('crypto');
const SECRET = '1dmpoqjdfpoje1p2dq,w[dk1';
const key = 'session_id';

function sign(str, secret) {
  return crypto.createHach('md5')
        .update(str + secret)
        .digest('base64');
}

// 加入到cookie中
var val = sign(req.sessionID, SECRET); 
res.setHeader('Set-Cookie', cookie.serialize(key, val));

這樣只要不知道私鑰的值很魂,就無法偽造簽名信息,以此實(shí)現(xiàn)對(duì)Session的保護(hù)充石。

緩存

傳統(tǒng)客戶端在安裝后的應(yīng)用過程中僅僅需要傳輸數(shù)據(jù)莫换,Web應(yīng)用還需要傳輸構(gòu)成界面的組件(HTML,CSS,JS文件),這部分內(nèi)容在大多數(shù)場(chǎng)景下并不經(jīng)常變更骤铃,卻需要在每次的應(yīng)用中向客戶端傳遞拉岁,所以對(duì)這一部分需要使用緩存。

{{% notice info %}}
參考:緩存惰爬、ETag
{{% /notice %}}

協(xié)商緩存: If-Modified-Since/Last-Modified 和 If-None-Match/ETag

強(qiáng)制緩存:Expires 和 Cache-Control

Basic認(rèn)證

Basic認(rèn)證是當(dāng)客戶端與服務(wù)端進(jìn)行請(qǐng)求時(shí)喊暖,允許通過用戶名和密碼實(shí)現(xiàn)的一種身份認(rèn)證方式。

如果一個(gè)頁面需要Basic認(rèn)證撕瞧,它會(huì)檢查請(qǐng)求報(bào)文中的Authorization字段陵叽,該字段的值由認(rèn)證方式和加密值構(gòu)成。

<!-- 請(qǐng)求頭 -->
Authorization: Basic dXNlcjpwYXNz

在Basic認(rèn)證中丛版,它會(huì)將用戶和密碼部分組合:username:password巩掺,然后進(jìn)行Base64編碼。

var encode = function (username, password) {
  return new Buffer(username + ':' + password).toString('base64');
};

如果用戶首次訪問該頁面页畦,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)) {
    // 響應(yīng)頭中的WWW-Authenticate字段告知瀏覽器采用什么樣的認(rèn)證和加密方式
    // 未認(rèn)證會(huì)有交互框彈出
    res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');  
    res.writeHead(401);
    res.end();
  } else { 
    handle(req, res);
  } 
}

Basic認(rèn)證有太多的缺點(diǎn),使用Base64編碼加密后在網(wǎng)絡(luò)中傳送独令,近乎于明文傳輸端朵。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市燃箭,隨后出現(xiàn)的幾起案子冲呢,更是在濱河造成了極大的恐慌,老刑警劉巖招狸,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敬拓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡瓢颅,警方通過查閱死者的電腦和手機(jī)恩尾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挽懦,“玉大人翰意,你說我怎么就攤上這事⌒攀粒” “怎么了冀偶?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)渔嚷。 經(jīng)常有香客問我进鸠,道長(zhǎng),這世上最難降的妖魔是什么形病? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任客年,我火速辦了婚禮,結(jié)果婚禮上漠吻,老公的妹妹穿的比我還像新娘量瓜。我一直安慰自己,他們只是感情好途乃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布绍傲。 她就那樣靜靜地躺著,像睡著了一般耍共。 火紅的嫁衣襯著肌膚如雪烫饼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天试读,我揣著相機(jī)與錄音杠纵,去河邊找鬼。 笑死钩骇,一個(gè)胖子當(dāng)著我的面吹牛淡诗,可吹牛的內(nèi)容都是我干的骇塘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼韩容,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了唐瀑?” 一聲冷哼從身側(cè)響起群凶,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哄辣,沒想到半個(gè)月后请梢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡力穗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年毅弧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片当窗。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡够坐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出崖面,到底是詐尸還是另有隱情元咙,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布巫员,位于F島的核電站庶香,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏简识。R本人自食惡果不足惜赶掖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望七扰。 院中可真熱鬧奢赂,春花似錦、人聲如沸戳寸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疫鹊。三九已至袖瞻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拆吆,已是汗流浹背聋迎。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枣耀,地道東北人霉晕。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牺堰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拄轻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 構(gòu)建web應(yīng)用會(huì)遇到的問題 請(qǐng)求方法的判斷 URL路徑的解析 URL中查詢字符串的解析 Cookie的解析 表單數(shù)...
    TaoGeNet閱讀 422評(píng)論 0 0
  • 基礎(chǔ)功能 之前我們通過http模塊創(chuàng)建了一個(gè)簡(jiǎn)單的服務(wù)器,但是對(duì)于一個(gè)網(wǎng)絡(luò)應(yīng)用來說肯定是遠(yuǎn)遠(yuǎn)不夠的伟葫,在聚義的業(yè)務(wù)中...
    exialym閱讀 878評(píng)論 1 22
  • 數(shù)據(jù)上傳 單純的頭部報(bào)文無法攜帶大量的數(shù)據(jù)恨搓,在業(yè)務(wù)中,我們往往需要接收一些數(shù)據(jù)筏养,比如表單提交斧抱、文件提交、JSON上...
    Upcccz閱讀 314評(píng)論 0 0
  • http協(xié)議有http0.9渐溶,http1.0辉浦,http1.1和http2三個(gè)版本,但是現(xiàn)在瀏覽器使用的是htt...
    一現(xiàn)_閱讀 1,865評(píng)論 0 3
  • 一根K線主要包括三個(gè)部分:實(shí)體茎辐、上影線和下影線宪郊。實(shí)體即開盤價(jià)與收盤價(jià)之間的部分;上影線是實(shí)體以上的部分荔茬,即實(shí)體上...
    稻花香擺鐘閱讀 537評(píng)論 0 1