0°

前端点滴(Node.js)(五)—- 构建 Web 应用(一)基础功能

Node.js

通过对异步IO、异步编程、网络编程的学习,就是在为构建Web打下坚实的基础。接下来就开始深入地学习如何构建Web应用。

一、构建 Web 应用

1. 基础功能

(1)请求方法

在Web应用中除了常见的GET请求,POST请求外还有HEAD、DELETE、PUT、CONNECT等方法。请求方法存在于报文的第一行的第一个单词,通常是一个大写,报文示例如下:


1
2
3
4
5
6
7
1> GET / HTTP/1.1
2> Host: 127.0.0.1:8000
3> User-Agent: curl/7.55.1
4> Accept: */*
5>
6
7

HTTP_Parser在解析请求报文时,会将报文头取出,设置为req.method。通常我们只需处理GET与POST两类请求方法,但是在RESTful类Web服务中请求方法十分重要,应为它会决定资源的操作。

  • PUT:新建一个资源。
  • POST:更新一个资源。
  • GET:获取一个资源。
  • DELETE:删除一个资源。

(2)路径分析

除了请求会被解析出来外,最常见的请求判断莫过于对路径的判断。路径存在于报文请求方法后,上述示例的路径为/。
完整的url格式应该是这样的:
在这里插入图片描述
浏览器会将这个地址解析成报文,将路径和查询部分放在报文的第一行,需要注意的是,hash部分会被丢弃,不会在于报文的任何地方。
最常见的就是根据路径进行业务处理的应用是静态问及那服务器,它会根据路径去查找磁盘中的文件,然后将其响应给客户端,如下所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1var http = require('http');
2var fs = require('fs');
3var url = require('url');
4
5var server = http.createServer();
6server.on('request',function(req,res){
7    var pathname = url.parse(req.url).pathname;
8    console.log(pathname);  // /folder/content/test.txt
9   fs.readFile('.'+pathname,function(err,datas){
10      if(err){
11          res.writeHead(404);
12          res.end('找不到文件');
13          return;
14      }
15      res.writeHead(200);
16      res.end(datas);
17  });
18});
19server.listen(8000,function(){
20    console.log('server is create')
21});
22
23//./folder/content/test.txt 内容:123
24
25

效果:
使用 postman 进行客户端模拟:
在这里插入图片描述

(3)查询字符串

查询字符串位于路径之后,在地址栏中路径后的?foo=bar&baz=val字符串就是查询字符串。
这个字符串会跟随在路径之后,形成请求报文首行的第二部分。这部分内容经常被业务逻辑所用到,Node提供了两种方法处理:

1)querystring模块用于处理这部分数据。
如下所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1var http = require('http');
2var fs = require('fs');
3var url = require('url');
4var querystring = require('querystring');
5
6var server = http.createServer();
7server.on('request',function(req,res){
8    var query = querystring.parse(url.parse(req.url).query);
9   console.log(query);  // { foo: 'bar', baz: 'val' }
10  res.end();
11});
12server.listen(8000,function(){
13    console.log('server is create')
14});
15
16

2)更加简洁的方法就是在url.parse()添加第二个参数。
修改 query变量 如下所示:


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

值得注意的是: 如果查询字符串中的键值出现多次,那么它的值就会以一个数组出现。因此,业务的判断一定要检查值是数组还是字符串,否则会出现TypeError异常。

(4)Cookie

cookie 的实现流程

由于http是一个无状态的协议,现实中的业务却是需要一定的状态的,否则无法区分用户之间的身份。如何标识和认证一个用户,最早的方案就是Cookie。

Cookie的处理分为如下几步:

  • 服务器向客户端发送Cookie
  • 浏览器将Cookie保存
  • 之后每次浏览器都会将Cookie发向服务端

因此HTTP_Parser会将所有的报文字段解析到req.headers上,那么Cookie就是req.headers.cookie。根据规范中的定义,Cookie值的格式是key=value;key2=value2形式,如果有需要Cookie,解析它也很方便。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1var http = require('http');
2
3var server = http.createServer();
4server.on('request', function (req, res) {
5/* 将cookie挂载在req对象上 */
6    req.cookies = parseCookie(req.headers.cookie);
7    /* 业务代码 */
8    var handle = function (req, res) {
9        res.writeHead(200);
10        if (!req.cookies.isVisit) {
11            res.end('first')
12        } else {
13            res.end('second')
14        }
15    }
16    handle(req, res);
17});
18server.listen(8000, function () {
19    console.log('server is create')
20});
21
22var parseCookie = function (cookie) {
23    var cookies = {};
24    if (!cookie) {
25        return cookies;
26    }
27    var list = cookie.split(';');
28    for (var i = 0; i < list.length; i++) {
29        var pair = list[i].split('=');
30        cookies[pair[0].trim()] = pair[1];
31    }
32    return cookies;
33}
34
35

使用命令
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。
  • Expires和Max-Age:用来告诉浏览器这个Cookie何时过期,如果不设置该选项,在关闭浏览器时会丢失这个Cookie。Expires的值是一个UTC格式的时间字符串,告诉浏览器此Cookie何时将会过期,Max-Age则告诉浏览器此Cookie多久过期。前者一般不会存在问题,但是如果服务端的时间和客户端的时间不匹配,这种时间设置就会存在偏差,所以需要使用Max-Age来告诉浏览器这条Cookie多久之后过期,而不是一个具体的时间。
  • HttpOnly:告诉浏览器不允许通过脚本document.cookie去更改这个Cookie值。
  • Secure:当Secure值为true时,在HTTP中无效,在HTTPS中才有效,表示创建Cookie只能通过HTTPS连接中被浏览器传递到服务器端进行会话验证,如果是HTTP连接则不会传递给信息,所以很难被窃听到。

将Cookie序列化成符合规范的字符串:

将上述的业务处理替换成:


1
2
3
4
5
6
7
8
9
10
11
12
1var handle = function (req, res) {
2        if (!req.cookies.isVisit) {
3            res.setHeader('Set-Cookie', serialize('isVisit', '1'));
4            res.writeHead(200);
5            res.end('first');
6        } else {
7            res.writeHead(200);
8            res.end('second');
9        }
10    };
11
12

添加:
Cookie规范功能函数:


1
2
3
4
5
6
7
8
9
10
11
12
13
1var serialize = function (name, val, opt) {
2    var pairs = [name + '=' + encodeURI(val)];
3    opt = opt || {};
4    if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge);
5    if (opt.domain) pairs.push('Domain=' + opt.domain);
6    if (opt.path) pairs.push('Path=' + opt.path);
7    if (opt.expires) pairs.push('Expires=' + opt.expires.toUTCString());
8    if (opt.httpOnly) pairs.push('HttpOnly');
9    if (opt.secure) pairs.push('Secure');
10    return pairs.join('; ');
11};
12
13

结果如下:
在这里插入图片描述
客户端收到这个带有Set-Cookie的响应后,在之后的请求时就会在Cookie字段中带上这个值。
值得注意的是,Set-Cookie是较少的,在报头中可能存在多个字段。为此res.setHeader的第二个参数可以是一个数组,修改res.setHeader看看结果:


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

结果:
在这里插入图片描述
方法二:
直接按照Cookie式规范创建Cookie:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1// 原生中操作 cookie
2const http = require("http");
3
4// 创建服务
5http.createServer((req, res) => {
6    if (req.url === "/read") {
7        // 读取 cookie
8        console.log(req.headers.cookie);
9        res.end(req.headers.cookie);
10    } else if (req.url === "/write") {
11        // 设置 cookie
12        res.setHeader("Set-Cookie", [
13            "name=Errrl; domain=panda.com; path=/write; httpOnly=true",
14            `age=28; Expires=${new Date(Date.now() + 1000 * 10).toGMTString()}`,
15            `address=${encodeURIComponent("广州番禺")}; max-age=10`
16        ]);
17        res.end("isDone");
18    } else {
19        res.end("Not Found");
20    }
21}).listen(8000,()=>{
22  console.log('server is create')
23});
24
25

效果:
在这里插入图片描述

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的数据只保存在服务器中,客户端无法修改,这样的数据安全性才能得到保障,并且数据也无须在协议中重复传递。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1// 原生中使用 session
2const http = require("http");
3const uuid = require('uuid/v1'); // 生成随字符串,npm install uuid
4const querystring = require("querystring");
5
6// 存放 session ,注:正常的Session是放在数据库或者缓存中,为了方便操作先用一个对象模拟。
7const session = {};
8
9// 创建服务
10http.createServer((req, res) => {
11    if (req.url === "/user") {
12        // 取出 cookie 存储的用户 ID
13        let userId = querystring.parse(req.headers["cookie"], "; ")["study"];
14
15        if (userId) {
16            if (session[userId].studyCount === 0) res.end("次数已用尽");
17            session[userId].studyCount--;
18        } else {
19            // 生成 userId
20            userId = uuid();
21            // 将用户信息存入 session
22            session[userId] = { studyCount: 30 };
23            // 设置 cookie
24            res.setHeader("Set-Cookie", [`study=${userId}`]);
25        }
26        // 响应信息
27        res.end(`
28            当前用户 ID 为 ${userId},
29            剩余次数为:${session[userId].studyCount}
30        `);
31    } else {
32        res.end("Not Found");
33    }
34}).listen(8000,()=>{
35    console.log('server is create')
36});
37
38

结果:
在这里插入图片描述

(6)缓存

传统的客户端在安装后的应用过程中仅仅需要传输数据,Web应用还需传输构成界面的组件(HTML、JavaScript、CSS文件)。这部分内容在大多数的场景下并不经常改变,却需要在每次的应用中像客户端传递,如果不进行处理,那么他将会造成不必要的宽带浪费。如果网络网速较差,就需要花费更多的时间来打开页面,对于用户来说就是体验性极差。因此节省不必要的传输,对用户和对服务器来说都是一种好处。

缓存规则:

  • 添加Expires或者Cache-Control到报文中。
  • 配置ETags。
  • 使用Ajax进行缓存。

对于请求来说:
通常来说,POST、DELETE、PUT这种行为性的请求操作一般不会进行任何缓存,大多数缓存之应用在GET请求中。

缓存策略
在这里插入图片描述
简单来说就是,本地没有文件时,浏览器必然会请求服务器端的内容,并将这部分把内容缓存到本地的某个缓存文件之中。第二次请求时,它将会对本地文件进行一次地毯式搜索,如果不能确定这份本地文件是否可以直接使用,它将会再一次发起请求。所谓的条件请求,就是普通的GET请求报文中附带的If-Modified-Since字段,比如:


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

它将询问服务器端是否有更新的版本,本地文件的最后一次修改时间。如果服务器端没有新的版本,只需响应一个304状态码,客户端使用本地缓存文件,如果服务器端存在更新了的版本,就将新的内容响应给客户端返回状态码200。

测试:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1var http = require('http');
2var fs = require('fs');
3var server = http.createServer();
4server.on('request', (req, res) => {
5    fs.stat('./test2.txt', function (err, stat) {
6        var lastModified = stat.mtime.toUTCString();
7        if (lastModified === req.headers['if-modified-since']) {
8            res.writeHead(304, "Not Modified");
9            res.end();
10        } else {
11            fs.readFile('./test2.txt', function (err, file) {
12                var lastModified = stat.mtime.toUTCString();
13                res.setHeader("Last-Modified", lastModified);
14                res.writeHead(200, "Ok");
15                res.end(file);
16            });
17        }
18    })
19}).listen(8000, () => {
20    console.log('server is create')
21})
22
23

结果:
在这里插入图片描述
刷新、关闭浏览器再打开,观测结果:
在这里插入图片描述
修改文件内容:
TXT:(./test2.txt)后观察结果:
在这里插入图片描述
这里的条件请求采用的就是时间戳的方式实现,但是时间戳有一定的缺陷:

  • 文件时间戳改动但内容并不一定改动。
  • 时间戳只能精确到秒级,更新频繁的内容可能就无法适应,生效。

针对上面的问题,ETag就是来解决这个问题的,ETag由服务器端生成,服务器端可以决定它的生成规则。如果根据文件内容生成散列值,那么条件请求将不会受到时间戳的改动造成的宽带浪费,下面是根据内容生成散列值的方法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1/* ETag */
2var crypto = require('crypto');
3var http = require('http');
4var fs = require('fs');
5var getHash = function (str) {
6    var shasum = crypto.createHash('sha1');
7    return shasum.update(str).digest('base64')
8}
9var server = http.createServer();
10server.on('request', function (req, res) {
11    fs.readFile('./test2.txt', function (err, file) {
12        var hash = getHash(file);
13        var noneMatch = req.headers['if-none-match'];
14        if (hash === noneMatch) {
15            res.writeHead(304, "Not Modified");
16            res.end();
17        } else {
18            res.setHeader("ETag", hash);
19            res.writeHead(200, "Ok");
20            res.end(file);
21        }
22    });
23}).listen(8000,()=>{
24    console.log('server is create')
25})
26
27

效果:
在这里插入图片描述
第二次请求:
在这里插入图片描述
也就是说,浏览器在收到Etag:ORPXO/IXlZYoBHRcMrpTyyB2fnk=这样的响应后,在下一次请求时,会将其放置在请求头中:If-None-Match: ORPXO/IXlZYoBHRcMrpTyyB2fnk=。

尽管条件请求可以在文件内容没有被修改的情况下节省宽带,但是它依然会发起一个HTTP请求,使得客户端依然会花一定的时间来等待响应,可见最好的方案就是连条件请求都不用发起。那如何使浏览器知晓是否能直接使用本地版本呢?答案就是服务器端在响应时,让浏览器明确地将内容缓存起来,所以这就是上面YSlow提到的Expires和Cache-Control。浏览器就是根据该值进行缓存的。
两者的区别如下:

Expires


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1/* Expires */
2var http = require('http');
3var fs = require('fs');
4
5var server = http.createServer();
6server.on('request', function (req, res) {
7    fs.readFile('./test2.txt', function (err, file) {
8        var expires = new Date();
9        expires.setTime(expires.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
10        res.setHeader("Expires", expires.toUTCString());
11        res.writeHead(200, "Ok");
12        res.end(file);
13    });
14}).listen(8000, () => {
15    console.log('server is create')
16})
17
18

效果:
在这里插入图片描述
Expires是一个GMT格式的字符串,浏览器在接到这个过期值后,只要本地还存在这个缓存文件,在到期时间之前她就不会再次发起请求。上述Expires设置了10年。但是Expires的缺陷在于浏览器与服务器之间的时间可能不一致,这可能会带来一些问题。比如文件提前过期,或者到期后没有被删除,这种情况,Cache-Control以更丰富的形式,实现相同的功能:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1/* Cache-Control */
2var http = require('http');
3var fs = require('fs');
4
5var server = http.createServer();
6server.on('request', function (req, res) {
7    fs.readFile('./test2.txt', function (err, file) {
8        res.setHeader("Cache-Control", "max-age=" + 10 * 365 * 24 * 60 * 60 * 1000);
9        res.writeHead(200, "Ok");
10        res.end(file);
11    });
12}).listen(8000, () => {
13    console.log('server is create')
14})
15
16

效果:
在这里插入图片描述
上面的代码为Cache-Control设置了max-age值,它比Expires优秀的地方在于,Cache-Control能够避免服务器端与浏览器端时间不同步带来的不一致性问题,只要进行类似的倒计时的方式计算过期时间即可。除此之外,Cache-Control的值还有public、private、no-cache、no-store等能够更加精细控制缓存的选项。

值得注意的是:由于HTTP1.0时还不支持max-age,如今的服务器端在模块的支持下多半同时对Expires和Cache-Control进行支持。在浏览器中如果两个值同时存在,且被同时支持时,max-age会覆盖Expires。

清除缓存
出现情况:缓存被设置,服务器意外更新内容,却无法通知客户端进行更新。这使得我们在使用缓存时也要同时为其设置版本号,所幸的是浏览器会根据URL进行缓存,难么一旦内容有所更新,我们就会让浏览器发起新的URL请求,使得新内容能够被用户端更新,更新机制如下:

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
安全运维