Nodejs 构建Web应用

1,062 阅读9分钟

前言

前一章我们已经知道了答题的服务器和客户端的工作流程,大体的请求头和响应头的结构如下图

在具体的业务中,我们可能的需求有:


* 请求方法的判断
* URL的路径解析
* URL中查询字符串解析
* Cookie的解析
* Basic认证
* 表单数据的解析
* 任意格式文件的上传处理
* session会话

基础功能

请求方法

在web应用中最常见的是GET 和 POST 请求。
除此之外,还有HEAD、DELETE、PUT、CONNECT等方法
请求方法一般位于报文的第一行的第一个单词,通常是大写
> GET /?a=1&b=2 HTTP/1.1

HTTP_Parse在解析报文的时候,将报文头取出来,设置为req.method

路径解析

路径通常存储于报文的第一个行的第二部分
> GET /?a=1&b=2 HTTP/1.1
HTTP_Parse在解析报文的时候,设置为req.url :
http://127.0.0.1:1111/?a=1&b=2

查询字符串

查询字符串通常存储于报文的第一个行的第二部分
> ?a=1&b=2
Node提供了 queryString 模块处理这部分数据
var url = require('url');
var queryString = require('queryString');
var  query = queryString.parse(url.parse(req.url).query);
会将 a=1&b=2 解析为一个JSON对象
{
    a: 1,
    b: 2
}

Cookie

Cookie用于认证用户,一般的处理分为以下几步:
服务器向客户端发送Cookie
浏览器存储Cookie
之后每次浏览器都会将Cookie发向服务器

HTTP_Parse会将所有的报文文字解析到req.headers上。 Cookie 就可以通过 req.headers.cookie 格式为 key=value;key2=value2形式

设置cookie有以下几个选项
path:表示这个cookie影响到的路径
Expirse:和Max-age:用来告诉浏览器何时过期
httpOnly:告知浏览器不允许通过脚本设置cooikie这个值,且设置该属性后,document.cookie是获取达不到的 但在http请求中还是会发送到服务器重的
Secure:当为true时,在http中无效。

cookie对性能的影响
一但cookie过多,就会造成带宽的浪费,所以
1.减小cookie的大小
2.为静态组建使用不同的域名
3.减少DNS解析

Session

Session数据只存在于服务器端,客户端无法修改
相当于与cookie 而言,cookie对于敏感数据的保存是无效的
将客户端的数据和服务器中的数据一一对应起来,有两种方式
第一种。基于cookie来实现用户和数据的映射
第二种。通过查询字符串来实现浏览器端和服务端数据的对应

缓存

web应用需要传输构成页面的组建(HTML、JavaScirpt、CSS文件等),这部分内容在大多数情境下不需要经常变更,却需要每次在应用中向客户端传递,如果不禁想处理,将导致不必要的带宽浪费,如果网络速度差,就需要花费更长的事件打开页面,对于用户体验不是很好。所以,为了提高性能YSlow中提到几条关于缓存的规则:
1.添加Expirse 或者 Cache-Control到报文头中
2.配置ETags
3.让ajax可以缓存

清除缓存

虽然知道了如何设置缓存,以达到节省网络宽带的目的,但是一旦缓存设立,当服务器意外更新内容时候,却无法通知客户端进行刷新。所幸得是浏览器是根据url进行缓存的,只要重新发起新的URL,就可以是的新内容能够被客户端更新,一般的更新机制有以下两种
1.每次发布,路径跟随web应用的版本号,http://url.com/?v=6797897890
2.每次发布,路径中跟随文件内容更新的hash值: http://url.com/?hash=dsgfjsdfds

Basic 认证

当客户端与服务器进行请求时,允许通过用户名和密码实现的一种身份认证方式,
他会检查报文重的Autorization字段,该字段的值由认证方式和加密值构成

数据上传

在业务中,我们往往需要接收一下数据:表单提交、文件提交、JSON上传、XML文件上传等

表单数据

默认的表单提交,请求头中的Content-Type字段值为application/x-www-form-urlencided
由于他的报文体内容跟查询字符串相同 a=1&b=2
所以解析它十分简单 req.body就可以得到

其他格式

除了表单之外,常见的提交还有JSON 和 XML文件。
JSON 的 Content-Type字段值为application/json
XML 的 Content-Type字段值为application/xml

附件上传

通常的表单。可以通过urlencoded的方式编码内容形成报文体,在发送给服务器端,但是业务场景往往需要用户直接提交文件。
特殊表单与普通表单的差异在于该表中可以含有file类型的控件。以及需要制定表单属性enctype为multipart/form-data
报文是一下这种形式的
Contetn-Type: multipart/form-data; boundary=Aabo3x
Content-Length: 17689
boundary值得是每部分内容的分界。Content-Length报文的长度

数据长传与安全

1.内存限制
    解析表单时候,先保存用户的提交的所有数据,然后再解析处理,最后才传递给业务逻辑。潜在的隐患是,仅仅适合数量小的请求,一旦数据量过大,就会导致内存益处,攻击者只要伪造大量的请求,就可以把内存吃光。要解决这个问题有两个方法
    a:限制文件上传的大小,一旦超出限制,停止接收数据并返回400状态码
    b:通过流式解析,将数据流导向到磁盘中,Node中只保留文件路径等小数据
2.CSRF 跨站请求伪造

路由解析

通常我们对于不同的业务有不同的处理方式,执行不同的模块或者页面,这就引入了路由的问题。MVC、RESTful等路由的方式

文件路径型

1.静态文件
这种方式的路由,其网站目录的路径与URL路径一致,无需转换,
2.动态文件
在MVC模式流行起来之前,根据文件路径执行动态脚本,也是基本的路由方式,服务器根据文件名称后缀去寻找脚本的解析器,并且传入HTTP请求的上下文

MVC

MVC模型的主要思想是将业务逻辑按指责分离,主要分为以下几种
1.控制器(Controller),一组行为的集合
2.模型(Model),数据相关演的操作和封装
3.视图(view),视图的渲染
工作模式大概:
路由解析:根据URl寻找对应的控制的器的行文
行为调用相关的模型,进行数据操作
数据操作结束后,调用视图和相关的数据进行页面渲染,输出的客户端

1.手工映射
手工映射除了需要手工配置路由比较原始以外,他对URL的要求非常灵活
/user/setting
/settting/user
路径解析的方式:正则解析,参数解析
//这里假设已经拥有一个处理设置用户信息的控制器
exports.setting = function (req, res) {
    //TODO
}

//再添加一个映射的方法就可以了,这个方法名字叫做 use()
var routers = [];
var use = function(path, action) {
    routers.push([path, action]);
}

//入口程序判断url,执行对相应的逻辑,于是就完成了基本的路由映射过程
function(req, res) {
    var pathname = url.parse(req.url).pathname;
    for(var i=0; i < routers; i++) {
        var route = routers[i];
        if (pathname = route[0]) {
            var action = routep[1];
            action(req, res);
            return;
        }
    }
    //处理404请求
    handle404(req, res)
}

user('/user/setting', exports.setting);
2.自然映射
RESTful 将HTTP的方法也加入了路由的操作,通过accept决定资源的表现形式
app.post(url, addFn);
app.delete(url2,deletFn);

中间件 Connect

Connect 是一个 node 中间件框架。Express 就是基于 Connect 开发的。
如果把一个 HTTP 处理过程比作是污水处理,中间件就像是一层层的过滤网,过滤网有各自不同的作用。

Connect 中间件就是 JavaScript 函数。函数一般有三个参数:
req(请求对象)
res(响应对象)
next(回调函数)
一个中间件完成自己的工作后,如果要执行后续的中间件,需要调用 next 回调函数
const connect = require('connect');

// 输出 HTTP 请求的方法和 URL 并调用 next() 执行后续中间件
function logger(req, res, next) {
  console.log('%s %s', req.method, req.url);
  next();
}

// 响应 HTTP 的请求
function hello(req, res, next) {
  res.setHeader('Content-Type', 'text/plain');
  res.end('hello world');
}

// 按顺序组合中间件
connect()
  .use(logger)
  .use(hello)
  .listen(3000);

注意组合中间件的顺序很重要,如果某个中间件不调用next() ,那么在它后面的中间件就不会被调用

中间件如何进行错误处理?
Connect 有一种用来处理错误的中间件变体,比常规中间件多了一个错误对象参数。
必须有四个参数:err 、req 、res 、next
错误处理中间件有两种工作机制:

用 Connect 的默认错误处理器(自动处理)
自行处理

当 Connect 遇到错误时,会跳过常规中间件,只去调用错误处理中间件。比如:
connect()
    .use(mw1) // 出错
    .use(mw2) // 跳过
    .use(mw3) // 跳过
    .use(errorHandler) // 执行 

页面渲染

内容响应

内容响应的过程中,响应报文头重的Content-* 字段非常重要
下列响应的报文中告诉客户端内容是以gzip编码的,其中内容长度为21170字节,内容类型为JS,字符集为utf-8
Content-Encoding: Gzip;
Content-Length: 21170;
Content-Type: Text/JavaScript; Charset=Utf-8;  (不同的文件有不同的MIME值)

附件下载

Content-Disposition字段   inline/即时查看  attcahment/附件查看

响应JSON

res.setHeader('Content-type', 'application/json');

视图渲染

app.get('/error', function(req, res){
res.status(500);
res.render('error');
});

模版

express中间件

新建网页模板

新建模板文件的index.html。
<!-- views/index.html文件 -->

<h1>文章列表</h1>

{{#each entries}}
   <p>
      <a href="/article/{{id}}">{{title}}</a><br/>
      Published: {{published}}
   </p>
{{/each}}
模板文件about.html。

<!-- views/about.html文件 -->

<h1>自我介绍</h1>

<p>正文</p>
模板文件article.html。

<!-- views/article.html文件 -->

<h1>{{blog.title}}</h1>
Published: {{blog.published}}

<p/>

{{blog.body}}
可以看到,上面三个模板文件都只有网页主体。因为网页布局是共享的,所以布局的部分可以单独新建一个文件的layout.html。

<!-- views/layout.html文件 -->

<html>

<head>
   <title>{{title}}</title>
</head>

<body>

	{{{body}}}

   <footer>
      <p>
         <a href="/">首页</a> - <a href="/about">自我介绍</a>
      </p>
   </footer>

</body>
</html>

渲染模板

// app.js文件

var express = require('express');
var app = express();

var hbs = require('hbs');

// 加载数据模块
var blogEngine = require('./blog');

app.set('view engine', 'html');
app.engine('html', hbs.__express);
app.use(express.bodyParser());

app.get('/', function(req, res) {
   res.render('index',{title:"最近文章", entries:blogEngine.getBlogEntries()});
});

app.get('/about', function(req, res) {
   res.render('about', {title:"自我介绍"});
});

app.get('/article/:id', function(req, res) {
   var entry = blogEngine.getBlogEntry(req.params.id);
   res.render('article',{title:entry.title, blog:entry});
});

app.listen(3000);