前端點滴(Node.js)(五)---- 構建 Web 應用(一)基礎功能

Node.js

通過對異步IO、異步編程、網絡編程的學習,就是在爲構建Web打下堅實的基礎。接下來就開始深入地學習如何構建Web應用。

一、構建 Web 應用

1. 基礎功能

(1)請求方法

在Web應用中除了常見的GET請求,POST請求外還有HEADDELETEPUTCONNECT等方法。請求方法存在於報文的第一行的第一個單詞,通常是一個大寫,報文示例如下:

> GET / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.55.1
> Accept: */*
>

HTTP_Parser在解析請求報文時,會將報文頭取出,設置爲req.method。通常我們只需處理GET與POST兩類請求方法,但是在RESTful類Web服務中請求方法十分重要,應爲它會決定資源的操作。

  • PUT:新建一個資源。
  • POST:更新一個資源。
  • GET:獲取一個資源。
  • DELETE:刪除一個資源。

(2)路徑分析

除了請求會被解析出來外,最常見的請求判斷莫過於對路徑的判斷。路徑存在於報文請求方法後,上述示例的路徑爲/
完整的url格式應該是這樣的:
在這裏插入圖片描述
瀏覽器會將這個地址解析成報文,將路徑和查詢部分放在報文的第一行,需要注意的是,hash部分會被丟棄,不會在於報文的任何地方。
最常見的就是根據路徑進行業務處理的應用是靜態問及那服務器,它會根據路徑去查找磁盤中的文件,然後將其響應給客戶端,如下所示:

var http = require('http');
var fs = require('fs');
var url = require('url');

var server = http.createServer();
server.on('request',function(req,res){
    var pathname = url.parse(req.url).pathname;
    console.log(pathname);  // /folder/content/test.txt
	fs.readFile('.'+pathname,function(err,datas){
		if(err){
			res.writeHead(404);
			res.end('找不到文件');
			return;
		}
		res.writeHead(200);
		res.end(datas);
	});
});
server.listen(8000,function(){
    console.log('server is create')
});

//./folder/content/test.txt 內容:123

效果:
使用 postman 進行客戶端模擬:
在這裏插入圖片描述

(3)查詢字符串

查詢字符串位於路徑之後,在地址欄中路徑後的?foo=bar&baz=val字符串就是查詢字符串。
這個字符串會跟隨在路徑之後,形成請求報文首行的第二部分。這部分內容經常被業務邏輯所用到,Node提供了兩種方法處理:

1)querystring模塊用於處理這部分數據。
如下所示:

var http = require('http');
var fs = require('fs');
var url = require('url');
var querystring = require('querystring');

var server = http.createServer();
server.on('request',function(req,res){
    var query = querystring.parse(url.parse(req.url).query);
	console.log(query);  // { foo: 'bar', baz: 'val' }
	res.end();
});
server.listen(8000,function(){
    console.log('server is create')
});

2)更加簡潔的方法就是在url.parse()添加第二個參數。
修改 query變量 如下所示:

var query = url.parse(req.url,true).query;

值得注意的是: 如果查詢字符串中的鍵值出現多次,那麼它的值就會以一個數組出現。因此,業務的判斷一定要檢查值是數組還是字符串,否則會出現TypeError異常。

(4)Cookie

cookie 的實現流程

由於http是一個無狀態的協議,現實中的業務卻是需要一定的狀態的,否則無法區分用戶之間的身份。如何標識和認證一個用戶,最早的方案就是Cookie

Cookie的處理分爲如下幾步:

  • 服務器向客戶端發送Cookie
  • 瀏覽器將Cookie保存
  • 之後每次瀏覽器都會將Cookie發向服務端

因此HTTP_Parser會將所有的報文字段解析到req.headers上,那麼Cookie就是req.headers.cookie。根據規範中的定義,Cookie值的格式是key=value;key2=value2形式,如果有需要Cookie,解析它也很方便。

var http = require('http');

var server = http.createServer();
server.on('request', function (req, res) {
/* 將cookie掛載在req對象上 */
    req.cookies = parseCookie(req.headers.cookie);
    /* 業務代碼 */
    var handle = function (req, res) {
        res.writeHead(200);
        if (!req.cookies.isVisit) {
            res.end('first')
        } else {
            res.end('second')
        }
    }
    handle(req, res);
});
server.listen(8000, function () {
    console.log('server is create')
});

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;
}

使用命令
curl -v -H "Cookie: foo=bar; baz=val" "http://127.0.0.1:1337/path?foo=bar&foo=baz"查看結果:
在這裏插入圖片描述
但是值得注意的是,如果Cookie值沒有isVisit,都會收到first這樣的響應。這裏就提出了一個問題,如果識別到用戶(客戶端)沒有訪問過我們的站點(服務器),那麼我們的站點(服務器)是否有義務告述用戶(客戶端)已經訪問過了的表示呢?鑑於性能方面想這個問題,這個操作是必然的。告述客戶端的方式是通過響應報文實現的,響應的Cookie值在Set-Cookie字段中。 它的格式上述Cookie的格式大不相同,規範對於它的定義如下所示:
Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
其中name=value是必須包含的部分,其餘皆是可選參數。這些參數將會影響後續瀏覽器發送Cookie到服務器的行爲,參數說明如下:

  • path:表示Cookie影響到的路徑,當前路徑不滿足該匹配時,瀏覽器不會發送這個Cookie
  • ExpiresMax-Age:用來告訴瀏覽器這個Cookie何時過期,如果不設置該選項,在關閉瀏覽器時會丟失這個CookieExpires的值是一個UTC格式的時間字符串,告訴瀏覽器此Cookie何時將會過期,Max-Age則告訴瀏覽器此Cookie多久過期。前者一般不會存在問題,但是如果服務端的時間和客戶端的時間不匹配,這種時間設置就會存在偏差,所以需要使用Max-Age來告訴瀏覽器這條Cookie多久之後過期,而不是一個具體的時間。
  • HttpOnly:告訴瀏覽器不允許通過腳本document.cookie去更改這個Cookie值。
  • Secure:當Secure值爲true時,在HTTP中無效,在HTTPS中才有效,表示創建Cookie只能通過HTTPS連接中被瀏覽器傳遞到服務器端進行會話驗證,如果是HTTP連接則不會傳遞給信息,所以很難被竊聽到。

Cookie序列化成符合規範的字符串:

將上述的業務處理替換成:

var handle = function (req, res) {
        if (!req.cookies.isVisit) {
            res.setHeader('Set-Cookie', serialize('isVisit', '1'));
            res.writeHead(200);
            res.end('first');
        } else {
            res.writeHead(200);
            res.end('second');
        }
    };

添加:
Cookie規範功能函數:

var serialize = function (name, val, opt) {
    var pairs = [name + '=' + encodeURI(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('; ');
};

結果如下:
在這裏插入圖片描述
客戶端收到這個帶有Set-Cookie的響應後,在之後的請求時就會在Cookie字段中帶上這個值。
值得注意的是,Set-Cookie是較少的,在報頭中可能存在多個字段。爲此res.setHeader的第二個參數可以是一個數組,修改res.setHeader看看結果:

res.setHeader('Set-Cookie', [serialize('foo', 'bar'), serialize('baz', 'val')]);

結果:
在這裏插入圖片描述
方法二:
直接按照Cookie式規範創建Cookie

// 原生中操作 cookie
const http = require("http");

// 創建服務
http.createServer((req, res) => {
    if (req.url === "/read") {
        // 讀取 cookie
        console.log(req.headers.cookie);
        res.end(req.headers.cookie);
    } else if (req.url === "/write") {
        // 設置 cookie
        res.setHeader("Set-Cookie", [
            "name=Errrl; domain=panda.com; path=/write; httpOnly=true",
            `age=28; Expires=${new Date(Date.now() + 1000 * 10).toGMTString()}`,
            `address=${encodeURIComponent("廣州番禺")}; max-age=10`
        ]);
        res.end("isDone");
    } else {
        res.end("Not Found");
    }
}).listen(8000,()=>{
	console.log('server is create')
});

效果:
在這裏插入圖片描述

cookie 的性能優化

由於Cookie的實現機制,一旦服務器向客戶端發送設置Cookie的意圖,除非Cookie過期,否則客戶端每次請求都會發送這些Cookie到服務器端,一旦設置過多,將會導致報頭較大。大多數的Cookie並不需要每次都用上,一次會造成帶寬的部分浪費。所以在YSlow的性能優化中有一條:

  • 減少Cookie的大小

更嚴重的是,如果域名的根節點設置了Cookie,那麼幾乎所有的子路徑下請求都會帶上這些Cookie,這些Cookie在某些情況下有用某些情況下無用。其中以靜態文件最爲典型。解決辦法:

  • 爲靜態組件使用不同的域名

簡而言之就是,爲不需要Cookie的組件換個域名可以實現減少無效Cookie的傳輸,所以很多網站的靜態文件會有特別的域名,使得業務相關的Cookie不再影響靜態資源。

總結: 目前,廣告和在線統計領域是最爲依賴Cookie,通過嵌入的第三方廣告和統計腳本,將Cookie和當前頁面綁定,這樣就可以標識用戶,得到用戶的瀏覽行爲。儘管這樣的行爲很可怕,但是從Cookie的原理來說,它只能做到標記,而不能做到具有破壞性的事情。所以如果擔心自己站點的用戶被記錄下行爲,那就不要掛任何的第三方腳本。

(5)Session

通過Cookie,瀏覽器和服務器可以實現狀態記錄,但是Cookie並不是非常完美,上述已經提及Cookie體積過大就是一個顯著的問題,最爲嚴重的問題是Cookie可以在前後端進行修改,因此數據就極易被篡改和僞造。

爲了解決Cookie的敏感數據問題,Session應運而生,應爲Session的數據只保存在服務器中,客戶端無法修改,這樣的數據安全性才能得到保障,並且數據也無須在協議中重複傳遞。

// 原生中使用 session
const http = require("http");
const uuid = require('uuid/v1'); // 生成隨字符串,npm install uuid
const querystring = require("querystring");

// 存放 session ,注:正常的Session是放在數據庫或者緩存中,爲了方便操作先用一個對象模擬。
const session = {};

// 創建服務
http.createServer((req, res) => {
    if (req.url === "/user") {
        // 取出 cookie 存儲的用戶 ID
        let userId = querystring.parse(req.headers["cookie"], "; ")["study"];

        if (userId) {
            if (session[userId].studyCount === 0) res.end("次數已用盡");
            session[userId].studyCount--;
        } else {
            // 生成 userId
            userId = uuid();
            // 將用戶信息存入 session
            session[userId] = { studyCount: 30 };
            // 設置 cookie
            res.setHeader("Set-Cookie", [`study=${userId}`]);
        }
        // 響應信息
        res.end(`
            當前用戶 ID 爲 ${userId},
            剩餘次數爲:${session[userId].studyCount}
        `);
    } else {
        res.end("Not Found");
    }
}).listen(8000,()=>{
    console.log('server is create')
});

結果:
在這裏插入圖片描述

(6)緩存

傳統的客戶端在安裝後的應用過程中僅僅需要傳輸數據,Web應用還需傳輸構成界面的組件(HTML、JavaScript、CSS文件)。這部分內容在大多數的場景下並不經常改變,卻需要在每次的應用中像客戶端傳遞,如果不進行處理,那麼他將會造成不必要的寬帶浪費。如果網絡網速較差,就需要花費更多的時間來打開頁面,對於用戶來說就是體驗性極差。因此節省不必要的傳輸,對用戶和對服務器來說都是一種好處。

緩存規則:

  • 添加Expires或者Cache-Control到報文中。
  • 配置ETags
  • 使用Ajax進行緩存。

對於請求來說:
通常來說,POSTDELETEPUT這種行爲性的請求操作一般不會進行任何緩存,大多數緩存之應用在GET請求中。

緩存策略
在這裏插入圖片描述
簡單來說就是,本地沒有文件時,瀏覽器必然會請求服務器端的內容,並將這部分把內容緩存到本地的某個緩存文件之中。第二次請求時,它將會對本地文件進行一次地毯式搜索,如果不能確定這份本地文件是否可以直接使用,它將會再一次發起請求。所謂的條件請求,就是普通的GET請求報文中附帶的If-Modified-Since字段,比如:

If-Modified-Since: Sun, 03 Feb 2013 06:01:12 GMT

它將詢問服務器端是否有更新的版本,本地文件的最後一次修改時間。如果服務器端沒有新的版本,只需響應一個304狀態碼,客戶端使用本地緩存文件,如果服務器端存在更新了的版本,就將新的內容響應給客戶端返回狀態碼200

測試:

var http = require('http');
var fs = require('fs');
var server = http.createServer();
server.on('request', (req, res) => {
    fs.stat('./test2.txt', 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('./test2.txt', function (err, file) {
                var lastModified = stat.mtime.toUTCString();
                res.setHeader("Last-Modified", lastModified);
                res.writeHead(200, "Ok");
                res.end(file);
            });
        }
    })
}).listen(8000, () => {
    console.log('server is create')
})

結果:
在這裏插入圖片描述
刷新、關閉瀏覽器再打開,觀測結果:
在這裏插入圖片描述
修改文件內容:
TXT:(./test2.txt)後觀察結果:
在這裏插入圖片描述
這裏的條件請求採用的就是時間戳的方式實現,但是時間戳有一定的缺陷:

  • 文件時間戳改動但內容並不一定改動。
  • 時間戳只能精確到秒級,更新頻繁的內容可能就無法適應,生效。

針對上面的問題,ETag就是來解決這個問題的,ETag由服務器端生成,服務器端可以決定它的生成規則。如果根據文件內容生成散列值,那麼條件請求將不會受到時間戳的改動造成的寬帶浪費,下面是根據內容生成散列值的方法:

/* ETag */
var crypto = require('crypto');
var http = require('http');
var fs = require('fs');
var getHash = function (str) {
    var shasum = crypto.createHash('sha1');
    return shasum.update(str).digest('base64')
}
var server = http.createServer();
server.on('request', function (req, res) {
    fs.readFile('./test2.txt', 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);
        }
    });
}).listen(8000,()=>{
    console.log('server is create')
})

效果:
在這裏插入圖片描述
第二次請求:
在這裏插入圖片描述
也就是說,瀏覽器在收到Etag:ORPXO/IXlZYoBHRcMrpTyyB2fnk=這樣的響應後,在下一次請求時,會將其放置在請求頭中:If-None-Match: ORPXO/IXlZYoBHRcMrpTyyB2fnk=

儘管條件請求可以在文件內容沒有被修改的情況下節省寬帶,但是它依然會發起一個HTTP請求,使得客戶端依然會花一定的時間來等待響應,可見最好的方案就是連條件請求都不用發起。那如何使瀏覽器知曉是否能直接使用本地版本呢?答案就是服務器端在響應時,讓瀏覽器明確地將內容緩存起來,所以這就是上面YSlow提到的ExpiresCache-Control。瀏覽器就是根據該值進行緩存的。
兩者的區別如下:

Expires

/* Expires */
var http = require('http');
var fs = require('fs');

var server = http.createServer();
server.on('request', function (req, res) {
    fs.readFile('./test2.txt', 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);
    });
}).listen(8000, () => {
    console.log('server is create')
})

效果:
在這裏插入圖片描述
Expires是一個GMT格式的字符串,瀏覽器在接到這個過期值後,只要本地還存在這個緩存文件,在到期時間之前她就不會再次發起請求。上述Expires設置了10年。但是Expires的缺陷在於瀏覽器與服務器之間的時間可能不一致,這可能會帶來一些問題。比如文件提前過期,或者到期後沒有被刪除,這種情況,Cache-Control以更豐富的形式,實現相同的功能:

/* Cache-Control */
var http = require('http');
var fs = require('fs');

var server = http.createServer();
server.on('request', function (req, res) {
    fs.readFile('./test2.txt', function (err, file) {
        res.setHeader("Cache-Control", "max-age=" + 10 * 365 * 24 * 60 * 60 * 1000);
        res.writeHead(200, "Ok");
        res.end(file);
    });
}).listen(8000, () => {
    console.log('server is create')
})

效果:
在這裏插入圖片描述
上面的代碼爲Cache-Control設置了max-age值,它比Expires優秀的地方在於,Cache-Control能夠避免服務器端與瀏覽器端時間不同步帶來的不一致性問題,只要進行類似的倒計時的方式計算過期時間即可。除此之外,Cache-Control的值還有publicprivateno-cacheno-store等能夠更加精細控制緩存的選項。

值得注意的是:由於HTTP1.0時還不支持max-age,如今的服務器端在模塊的支持下多半同時對ExpiresCache-Control進行支持。在瀏覽器中如果兩個值同時存在,且被同時支持時,max-age會覆蓋Expires

清除緩存
出現情況:緩存被設置,服務器意外更新內容,卻無法通知客戶端進行更新。這使得我們在使用緩存時也要同時爲其設置版本號,所幸的是瀏覽器會根據URL進行緩存,難麼一旦內容有所更新,我們就會讓瀏覽器發起新的URL請求,使得新內容能夠被用戶端更新,更新機制如下:

  • 每一次發佈,路徑中跟隨Web應用的版本號:http://url.com/?version=20200212
  • 每一次發佈,路徑中跟隨文件內容的hash值:http://url.com/?hash=afadfadwe。(推薦)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章