2月阅读周·深入浅出的Node.js | 构建Web应用,追逐应用化发展的潮流
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效。
已读完书籍:《架构简洁之道》。
当前阅读周书籍:《深入浅出的Node.js》。
构建Web应用
基础功能
对于一个Web应用而言,在具体的业务中,可能有如下这些需求:
- 请求方法的判断。
- URL的路径解析。
- URL中查询字符串解析。
- Cookie的解析。
- Basic认证。
- 表单数据的解析。
- 任意格式文件的上传处理。
请求方法
在Web应用中,最常见的请求方法是GET和POST,除此之外,还有HEAD、DELETE、PUT、CONNECT等方法。
可以通过请求方法来决定响应行为,如下所示:
function fn(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);
}
}
路径解析
除了根据请求方法来进行分发外,最常见的请求判断莫过于路径的判断了。
最常见的根据路径进行业务处理的应用是静态文件服务器,它会根据路径去查找磁盘中的文件,然后将其响应给客户端,如下所示:
function fn(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('找不到相关文件。- -');
return;
}
res.writeHead(200);
res.end(file);
});
}
查询字符串
查询字符串位于路径之后,在地址栏中路径后的?foo=bar&baz=val字符串就是查询字符串。
这个字符串会跟随在路径后,形成请求报文首行的第二部分。这部分内容经常需要为业务逻辑所用,Node提供了querystring模块用于处理这部分数据,如下所示:
var url = require('url');
var querystring = require('querystring');
var query = querystring.parse(url.parse(req.url).query);
Cookie
Cookie可以标识和认证一个用户。
1、Cookie的处理分为如下几步:
- 服务器向客户端发送Cookie。
- 浏览器将Cookie保存。
- 之后每次浏览器都会将Cookie发向服务器端。、
2、Cookie的性能影响
一旦设置的Cookie过多,将会导致报头较大。大多数的Cookie并不需要每次都用上,因为这会造成带宽的部分浪费。
在YSlow的性能优化规则中有:减小Cookie的大小、为静态组件使用不同的域名、减少DNS查询。
Session
Session的数据只保留在服务器端,客户端无法修改,这样数据的安全性得到一定的保障,数据也无须在协议中每次都被传递。
1、如果想将每个客户和服务器中的数据一一对应起来,常见的两种实现方式:
- 基于Cookie来实现用户和数据的映射。
- 通过查询字符串来实现浏览器端和服务器端数据的对应。
2、Session与内存
Session数据直接存在变量sessions中,它位于内存中。
为了解决性能问题和Session数据无法跨进程共享的问题,常用的方案是将Session集中化,将原本可能分散在多个进程里的数据,统一转移到集中的数据存储中。目前常用的工具是Redis、Memcached等。
3、Session与安全
Session的口令依然保存在客户端,这里会存在口令被盗用的情况。
一种方案是将这个口令通过私钥加密进行签名,使得伪造的成本较高。
另外一种方案是将客户端的某些独有信息与口令作为原值,然后签名,这样攻击者一旦不在原始的客户端上进行访问,就会导致签名失败。
缓存
节省不必要的传输,对用户和对服务提供者来说都有好处。
为了提高性能,YSlow中也提到几条关于缓存的规则:
- 添加Expires或Cache-Control到报文头中。
- 配置ETags。
- 让Ajax可缓存。
1、清除缓存
缓存也需要有更新机制,一般的更新机制有如下两种:
- 每次发布,路径中跟随Web应用的版本号:http://url.com/?v=20130501。
- 每次发布,路径中跟随该文件内容的hash值:http://url.com/?hash=afadfadwe。
这两种方式,以文件内容形成的hash值更精准。
Basic认证
Basic认证是当客户端与服务器端进行请求时,允许通过用户名和密码实现的一种身份认证方式。这里简要介绍它的原理和它在服务器端通过Node处理的流程。
数据上传
在业务中,我们往往需要接收一些数据,比如表单提交、文件提交、JSON上传、XML上传等。
通过报头的Transfer-Encoding或Content-Length即可判断请求中是否带有内容,携带的内容部分需要用户自行接收和解析。
表单数据
最为常见的数据提交就是通过网页表单提交数据到服务器端:
<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>;
然后解析它也很简单:
var handle = function (req, res) {
if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
req.body = querystring.parse(req.rawBody);
}
todo(req, res);
};
其他格式
除了表单数据外,常见的提交还有JSON和XML文件等,判断和解析他们都是依据Content-Type中的值决定,其中JSON类型的值为application/json, XML的值为application/xml。
1、JSON文件
Node对于处理JSON内容不需要额外的库:
var handle = function (req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch (e) {
// 异常内容,响应Bad request
res.writeHead(400);
res.end('Invalid JSON');
return;
}
}
todo(req, res);
};
2、XML文件
解析XML文件需要用到将XML文件到JSON对象转换的库,比如xml2js模块:
var xml2js = require('xml2js');
var handle = function (req, res) {
if (mime(req) === 'application/xml') {
xml2js.parseString(req.rawBody, function (err, xml) {
if (err) {
// 异常内容,响应Bad request
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
});
}
};
附件上传
在前端HTML代码中,特殊表单与普通表单的差异在于该表单中可以含有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>;
接收是可以使用formidable模块,它基于流式处理解析报文,将接收到的文件写入到系统的临时文件夹中,并返回对应的路径:
var formidable = require('formidable');
function fn(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);
}
}
数据上传与安全
在Web应用中,需要重视与数据上传相关的安全问题。比如内存和CSRF。
1、内存限制
在解析表单、JSON和XML部分,我们采取的策略是先保存用户提交的所有数据,然后再解析处理,最后才传递给业务逻辑。这种策略存在潜在的问题是,它仅仅适合数据量小的提交请求,一旦数据量过大,将发生内存被占光的情况。
要解决这个问题主要有两个方案:
- 限制上传内容的大小,一旦超过限制,停止接收数据,并响应400状态码。
- 通过流式解析,将数据流导向到磁盘中,Node只保留文件路径等小数据。
2、CSRF
用户通过浏览器访问服务器端的Session ID是无法被第三方知道的,但是CSRF的攻击者并不需要知道Session ID就能让用户中招。
解决CSRF攻击的方案有添加随机值的方式,攻击者构造出相同的随机值的难度相当大,所以我们只需要在接收端做一次校验就能轻易地识别出该请求是否为伪造的。
路由解析
对于不同的业务,我们还是期望有不同的处理方式,这带来了路由的选择问题。
文件路径型
1、静态文件
这种方式的路由,URL的路径与网站目录的路径一致,无须转换,非常直观。这种路由的处理方式也十分简单,将请求路径对应的文件发送给客户端即可。
2、动态文件
动态文件的处理原理是Web服务器根据URL路径找到对应的文件,如/index.asp或/index.php。Web服务器根据文件名后缀去寻找脚本的解析器,并传入HTTP请求的上下文。
MVC
MVC模型是分层模型,它的主要思想是将业务逻辑按职责分离。
分层模型的路由解析方式是根据URL寻找到对应的控制器和行为。
根据URL做路由映射,有两个分支实现。一种方式是通过手工关联映射,一种是自然关联映射。
RESTful
REST的全称是Representational State Transfer,中文含义为表现层状态转化。符合REST规范的设计,我们称为RESTful设计。它的设计哲学主要将服务器端提供的内容实体看作一个资源,并表现在URL上。
中间件
引入中间件的目的是简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。
中间件的行为比较类似Java中过滤器(filter)的工作原理,就是在进入具体的业务处理之前,先让过滤器处理。它的工作模型如下图:
异常处理
想要知晓某个中间件是否发生了错误的一种方法是为next()方法添加err参数,并捕获中间件直接抛出的同步异常。实现代码如下:
var handle = function (req, res, stack) {
var next = function (err) {
if (err) {
return handle500(err, req, res, stack);
}
// 从stack数组中取出中间件并执行
var middleware = stack.shift();
if (middleware) {
// 传入next()函数自身,使中间件能够执行结束后递归
try {
middleware(req, res, next);
} catch (ex) {
next(err);
}
}
};
// 启动执行
next();
};
中间件与性能
为了让业务逻辑提早执行,尽早响应给终端用户,可以从下面两个方面进行提升:
- 编写高效的中间件。
- 合理利用路由,避免不必要的中间件执行。
页面渲染
HTTP响应实现的技术细节,主要包含内容响应和页面渲染两个部分。
内容响应
客户端在接收到相应报文后,正确的处理过程是通过gzip来解码报文体中的内容,用长度校验报文体内容是否正确,然后再以字符集UTF-8将解码后的脚本插入到文档节点中。
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');
视图渲染
Web应用最终呈现在界面上的内容,都是通过一系列的视图渲染呈现出来的。在动态页面技术中,最终的视图是由模板和数据共同生成出来的。
模板是带有特殊标签的HTML片段,通过与数据的渲染,将数据填充到这些特殊标签中,最后生成普通的带数据的HTML片段。通常将渲染方法设计为render(),参数就是模板路径和数据,如下所示:
res.render = function (view, data) {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
// 实际渲染
var html = render(view, data);
res.end(html);
};
模板
模板技术的实质就是将模板文件和数据通过模板引擎生成最终的HTML代码。
1、形成模板技术的也就如下4个要素:
- 模板语言。
- 包含模板语言的模板文件。
- 拥有动态数据的数据对象。
- 模板引擎。
2、模板技术
模板技术使得网页中的动态内容和静态内容变得不互相依赖,数据开发者与模板开发者只要约定好数据结构,两者就不用互相影响了:
Bigpipe
Bigpipe的提出主要是为了解决重数据页面的加载速度问题。
Node通过异步已经将多个数据源的获取并行起来了,最终的页面输出速度取决于两个数据请求中响应时间慢的那个。在数据响应之前,用户看到的是空白页面,这是十分不友好的用户体验。
Bigpipe的解决思路则是将页面分割成多个部分(pagelet),先向用户输出没有数据的布局(框架),将每个部分逐步输出到前端,再最终渲染填充框架,完成整个网页的渲染。
Bigpipe有几个重要的点。
- 页面布局框架(无数据的)。
- 后端持续性的数据输出。
- 前端渲染。
总结
我们来总结一下本篇的主要内容:
- 在Web应用的整个构建过程中,从处理请求到响应请求的整个过程都有原理性阐述。
- RESTful模式以其轻量的设计,得到广大开发者的青睐。对于多数的应用而言,只需要构建一套RESTful服务接口,就能适应移动端、PC端的各种客户端应用。
- 中间件是Connect的经典模式。中间件机制使得Web应用具备良好的可扩展性和可组合性,可以轻易地进行数据增删。
- 模板技术的出现,将业务开发与HTML输出的工作分离开来,它的设计原理就是单一职责原理。
- Bigpipe将网页布局和数据渲染分离,使得用户在视觉上觉得网页提前渲染好了,其随着数据输出的过程逐步渲染页面,使得用户能够感知到页面是活的。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)