第六章 请求和响应对象
在用 Express 构建 Web 服务器时,大部分工作都是从请求对象开始,到响应对象终止。这 两个对象起源于 Node,Express 对其进行了扩展。
6.1 url的组成部分
- 协议 协议确定如何传输请求。我们主要是处理 http 和 https。其他常见的协议还有 file 和 ftp。
- 主机名 主机名标识服务器。运行在本地计算机(localhost)和本地网络的服务器可以简单地表 示,比如用一个单词,或一个数字 IP 地址。在 Internet 环境下,主机名通常以一个顶 级域名(TLD)结尾,比如 .com 或 .net。另外,也许还会有子域名作为主机名的前缀。子域名可以是任何形式的,其中 www 最为常见。子域名通常是可选的。
- 端口
每一台服务器都有一系列端口号。一些端口号比较“特殊”,如 80 和 443 端口。如果 省略端口值,那么默认 80 端口负责 HTTP 传输,443 端口负责 HTTPS 传输。如果不使 用 80 和 443 端口,就需要一个大于 10231 的端口号。通常使用容易记忆的端口号,如 3000、8080 或 8088。 - 路径
URL 中影响应用程序的第一个组成部分通常是路径(在考虑协议、主机名和端口的基 础上做决定很合理,但是不够好)。路径是应用中的页面或其他资源的唯一标识。 - 查询字符串
查询字符串是一种键值对集合,是可选的。它以问号(?)开头,键值对则以与号(&) 分隔开。所有的名称和值都必须是 URL 编码的。对此,JavaScript 提供了一个嵌入式的 函数 encodeURIComponent 来处理。例如,空格被加号(+)替换。其他特殊字符被数字 型字符替换。 - 信息片段
信息片段(或散列)被严格限制在浏览器中使用,不会传递到服务器。用它控制单页应 用或 AJAX 富应用越来越普遍。最初,信息片段只是用来让浏览器展现文档中通过锚点 标记()指定的部分。
6.2HTTP请求方法
HTTP 协议确定了客户端与服务器通信的请求方法集合(通常称为 HTTP verbs)。很显然, GET 和 POST 最为常见。
对于一个网站来说,大部分页面都响应 GET 请求。POST 请求通常用来提交信息到服务器后 台(例如表单处理)。服务器将请求中包含的所有信息(例如表单)处理完成之后,用以 响应的 HTML 通常与相应的 GET 请求是一样的。与服务器通信时,浏览器只使用 GET 和 POST 方法(如果没有使用 AJAX)。
6.3请求报头
请求包头会隐含很多其他的信息,比如:“用 户代理”信息(浏览器、操作系统和硬件设备)和其他一些信息,
。所有能够确保你了解请 求对象头文件属性的信息都将会作为请求报头发送,如果想查看浏览器发送的信息,可以 创建一个非常简单的 Express 路由来展示一下:
app.get('/headers', function(req,res){ res.set('Content-Type','text/plain'); var s = ''; for(var name in req.headers) s += name + ': ' + req.headers[name] + '\n'; res.send(s); });
6.4响应报头
正如浏览器以请求报头的形式发送隐藏信息到服务器,当服务器响应时,同样会回传一些 浏览器没必要渲染和显示的信息,通常是元数据和服务器信息。我们已经熟悉内容类型头 信息,它告诉浏览器正在被传输的内容类型(网页、图片、样式表、客户端脚本等)。特 别要注意的是,不管 URL 路径是什么,浏览器都根据内容类型报头处理信息。因此你可 以通过一个叫作 /image.jpg 的路径提供网页,也可以通过一个叫作 /text.html 的路径提供图 片。(这样做并不合情理,这里要讲的重点是路径是抽象的,浏览器只根据内容类型来决 定内容该如何渲染。)除了内容类型之外,报头还会指出响应信息是否被压缩,以及使用 的是哪种编码。响应报头还可以包含关于浏览器对资源缓存时长的提示。优化网站时需要 着重考虑这一点,我们将在第 16 章详细讨论。响应报头还经常会包含一些关于服务器的 信息,一般会指出服务器的类型,有时甚至会包含操作系统的详细信息。返回服务器信息 存在一个问题,那就是它会给黑客一个可乘之机,从而使站点陷入危险。非常重视安全的 服务器经常忽略此信息,甚至提供虚假信息。
禁用 Express 的 X-Powered-By 头信息很简单:
app.disable('x-powered-by');
6.8请求对象
请求对象(通常传递到回调方法,这意味着你可以随意命名,通常命名为 req 或 request)的生命周期始于 Node 的一个核心对象 http.IncomingMessage 的实例。Express 添加了一些 附加功能。我们来看看请求对象中最有用的属性和方法(除了来自 Node 的 req.headers 和 req.url,所有这些方法都由 Express 添加)。
- req.params 一个数组,包含命名过的路由参数。我们将在第 14 章进行详细介绍。
- req.param(name) 返回命名的路由参数,或者 GET 请求或 POST 请求参数。建议你忽略此方法。
- req.query 一个对象,包含以键值对存放的查询字符串参数(通常称为 GET 请求参数)。
- req.body 一个对象,包含 POST 请求参数。这样命名是因为 POST 请求参数在 REQUEST 正文中传 递,而不像查询字符串在 URL 中传递。要使 req.body 可用,需要中间件能够解析请求 正文内容类型,我们将在第 10 章进行详细介绍。
- req.route 关于当前匹配路由的信息。主要用于路由调试。
- req.cookies/req.singnedCookies 一个对象,包含从客户端传递过来的 cookies 值。
- req.cookies/req.singnedCookies 一个对象,包含从客户端传递过来的 cookies 值。
- req.headers 从客户端接收到的请求报头。
- req.accepts([types]);
一个简便的方法,用来确定客户端是否接受一个或一组指定的类型(可选类型可以是 单个的 MIME 类型,如 application/json、一个逗号分隔集合或是一个数组)。写公共 API 的人对该方法很感兴趣。假定浏览器默认始终接受 HTML。 - req.ip 客户端的IP地址
- req.path 请求路径(不包含协议、主机、端口或查询字符串)。
- req.host 一个简便的方法。用来返回客户端所报告的主机名。这些信息可以伪造,所以不应该用于安全目的。
- req.xhr 一个简便属性,如果请求由 Ajax 发起将会返回 true.
- req.protocol 用于标识请求的协议(http 或 https)。
- req.secure 一个简便属性,如果连接是安全的,将返回 true。等同于 req.protocol===‘https’
- req.url/req.originalUrl 有点用词不当,这些属性返回了路径和查询字符串(它们不包含协议、主机或端口)。 req.url 若是出于内部路由目的,则可以重写,但是 req.orginalUrl 旨在保留原始请求 和查询字符串。
- req.acceptedLanguages•
一个简便方法,用来返回客户端首选的一组(人类的)语言。这些信息是从请求报头中 解析而来的。
6.9响应对象
响应对象(通常传递到回调方法,这意味着你可以随意命名它,通常命名为 res、resp 或 response)的生命周期始于 Node 核心对象 http.ServerResponse 的实例。Express 添加了一 些附加功能
-
res.status(code) 设置 HTTP 状态代码。Express 默认为 200(成功),所以你可以使用这个方法返回状态 404(页面不存在)或 500(服务器内部错误),或任何一个其他的状态码。对于重定向 (状态码 301、302、303 和 307),有一个更好的方法:redirect。
-
res.set(name,value)
设置响应头。这通常不需要手动设置。 -
res.cookie(name,vaue,[options]),res.clearCookie(name,[options])•
设置或清除客户端 cookies 值。需要中间件支持 -
res.redirect([status],url)
重定向浏览器。默认重定向代码是 302(建立)。通常,你应尽量减少重定向,除非永 久移动一个页面,这种情况应当使用代码 301(永久移动)。 -
res.send(body),res.send(status,body)
向客户端发送响应及可选的状态码。Express 的默认内容类型是 text/html。如果你想改 为 text/plain,需要在 res.send 之前调用 res.set(‘Content-Type’,‘text/plain’)。如 果 body 是一个对象或一个数组,响应将会以 JSON 发送(内容类型需要被正确设置), 不过既然你想发送 JSON,我推荐你调用 res.json。 -
res.json(json),res.json(status,json)
向客户端发送 JSON 以及可选的状态码。 -
res.jsonp(json),req.jsonp(status,json)
向客户端发送 JSONP 及可选的状态码。 -
res.type(type)
一个简便的方法,用于设置 Content-Type 头信息。基本上相当于 res.set(‘ContentType’,‘type’),只是如果你提供了一个没有斜杠的字符串,它会试图把其当作文件的 扩展名映射为一个互联网媒体类型。比如,res.type(‘txt’) 会将 Content-Type 设为 text/plain。此功能在有些领域可能会有用(例如自动提供不同的多媒体文件),但是 通常应该避免使用它,以便明确设置正确的互联网媒体类型。 -
res.format(object)
这个方法允许你根据接收请求报头发送不同的内容。这是它在 API 中的主要用途,我们 将会在第 15 章详细讨论。这里有一个非常简单的例子:res.format({‘text/plain’:‘hi there’,‘text/html’:‘hi there’})。 -
res.attachment([filename]),res.download(path,[filename],[callback])
这两种方法会将响应报头 Content-Disposition 设为 attachment,这样浏览器就会选 择下载而不是展现内容。你可以指定 filename 给浏览器作为对用户的提示。用 res. download 可以指定要下载的文件,而 res.attachment 只是设置报头。另外,你还要将 内容发送到客户端。 -
res.sendFile(path,[option],[callback])
这个方法可根据路径读取指定文件并将内容发送到客户端。使用该方法很方便。使用静 态中间件,并将发送到客户端的文件放在公共目录下,这很容易。然而,如果你想根据 条件在相同的 URL 下提供不同的资源,这个方法可以派上用场。 -
res.links(links)
设置链接响应报头。这是一个专用的报头,在大多数应用程序中几乎没有用处。 -
res.locals,res.render(view,[locals],callback)
res.locals 是一个对象,包含用于渲染视图的默认上下文。res.render 使用配置的模
板引擎渲染视图(不能把 res.render 的 locals 参数与 res.locals 混为一谈,上下文 在 res.locals 中会被重写,但在没有被重写的情况下仍然可用)。res.render 的默认响 应代码为 200,使用 res.status 可以指定一个不同的代码。
内容渲染
大多数情况下,渲染内容用 res.render,它最大程度地根据布局渲染视图。如果想写一 个快速测试页,也许会用到 res.send。你可以使用 req.query 得到查询字符串的值,使用 req.session 得到会话值,或使用 req.cookie/req.singedCookies 得到 cookies 值。
基本用法:
// 基本用法 app.get('/about', function(req, res){ res.render('about'); });
200以外的响应码:
app.get('/error', function(req, res){ res.status(500); res.render('error'); }); // 或是一行…… app.get('/error', function(req, res){ res.status(500).render('error'); });
将上下文传递给视图(包括查询字符串、cookie、session值):
app.get('/greeting', function(req, res){ res.render('about', { message: 'welcome', style: req.query.style, userid: req.cookie.userid, username: req.session.username, }); });
没有布局的视图渲染:
// 下面的 layout 没有布局文件,即 views/no-layout.handlebars // 必须包含必要的 HTML app.get('/no-layout', function(req, res){ res.render('no-layout', { layout: null }); });
使用定制的试图渲染文件(handlerbars)
// 使用布局文件 views/layouts/custom.handlebars
请求和响应对象 | 57
app.get('/custom-layout', function(req, res){ res.render('custom-layout', { layout: 'custom' }); });
渲染纯文本输出:
app.get('/test', function(req, res){ res.type('text/plain'); res.send('this is a test'); });
添加错误处理程序:
// 这应该出现在所有路由方法的结尾 // 需要注意的是,即使你不需要一个 " 下一步 " 方法 // 它也必须包含,以便 Express 将它识别为一个错误处理程序 app.use(function(err, req, res, next){ console.error(err.stack); res.status(500).render('error'); });
添加一个404处理程序:
// 这应该出现在所有路由方法的结尾 app.use(function(req, res){ res.status(404).render('not-found'); });
处理表单
当你处理表单时,表单信息一般在 req.body 中(或者偶尔在 req.query 中)。你可以使用 req.xhr 来判断是 AJAX 请求还是浏览请求(第 8 章将深入讨论)。
基本表单处理
// 必须引入中间件 body-parser app.post('/process-contact', function(req, res){ console.log('Received contact from ' + req.body.name + ' <' + req.body.email + '>'); // 保存到数据库…… res.redirect(303, '/thank-you'); });
更强大的表单处理:
// 必须引入中间件 body-parser app.post('/process-contact', function(req, res){ console.log('Received contact from ' + req.body.name + ' <' + req.body.email + '>'); try { // 保存到数据库……
return res.xhr ?
res.render({ success: true }) : res.redirect(303, '/thank-you'); } catch(ex) { return res.xhr ? res.json({ error: 'Database error.' }) : res.redirect(303, '/database-error'); } });
6.11.3 提供API
数据示例:
var tours = [ { id: 0, name: 'Hood River', price: 99.99 }, { id: 1, name: 'Oregon Coast', price: 149.95 }, ];
简单的get请求,只返回JSON数据
app.get('/api/tours'), function(req, res){ res.json(tours); });
示例 6-12 根据客户端的首选项,使用 Express 中的 res.format 方法对其响应。
GET 节点,返回 JSON、XML 或 text :
app.get('/api/tours', function(req, res){ var toursXml = '<?xml version="1.0"?><tours>' + products.map(function(p){ return '<tour price="' + p.price + '" id="' + p.id + '">' + p.name + '</tour>'; }).join('') + '</tours>''; var toursText = tours.map(function(p){ return p.id + ': ' + p.name + ' (' + p.price + ')'; }).join('\n'); res.format({ 'application/json': function(){ res.json(tours); }, 'application/xml': function(){
请求和响应对象 | 59
res.type('application/xml'); res.send(toursXml); }, 'text/xml': function(){ res.type('text/xml'); res.send(toursXml); } 'text/plain': function(){ res.type('text/plain'); res.send(toursXml); } }); });
用于更新的PUT结点
//API 用于更新一条数据并且返回 JSON;参数在查询字符串中传递 app.put('/api/tour/:id', function(req, res){ var p = tours.some(function(p){ return p.id == req.params.id }); if( p ) { if( req.query.name ) p.name = req.query.name; if( req.query.price ) p.price = req.query.price; res.json({success: true}); } else { res.json({error: 'No such tour exists.'}); } });
最后,示例 6-14 展示了一个 DEL 节点。
用于删除的DEL结点
// API 用于删除一个产品 api.del('/api/tour/:id', function(req, res){ var i; for( var i=tours.length-1; i>=0; i-- ) if( tours[i].id == req.params.id ) break; if( i>=0 ) { tours.splice(i, 1); res.json({success: true}); } else { res.json({error: 'No such tour exists.'}); } });