五、模板引擎和 Consolidate.js
模板引擎是允许我们使用不同模板语言的库(EJS,把手,Jade 等。).但是什么是模板语言呢?模板语言是一组特殊的指令(语法和控制结构),指示引擎如何处理数据。该语言特定于特定的模板引擎。模板中的说明通常用于以适合最终用户的更好格式呈现数据。在 web 应用的情况下,这样的最终表示格式是 HTML。所以基本上,我们有一些数据(JSON 或 JavaScript/Node.js 对象)和模板(EJS、把手、Jade 等)。).当它们被组合时,我们得到输出,这是很好的旧 HTML。
数据和模板结合的过程叫做渲染。一些模板引擎具有在渲染之前编译模板作为额外步骤的功能。编译类似于缓存,适合于频繁重用的优化。
"为什么要使用模板?"你可能会问你以前是否没有用过它们。使用模板比不使用模板有很多好处,其中最重要的是你可以重用代码——例如,菜单、页眉、页脚、按钮和其他表单元素等等。这样,如果您以后需要进行更改,您将只需要在一个地方更新代码,而不是在每个文件中进行更改。另一个优点是,根据您使用的库,您可以使模板更加动态。这意味着您可以向模板添加一些逻辑,使其更加智能(例如,一个for
循环来迭代表中的每一行)。
Jade 允许在其代码中使用几乎所有的 JavaScript/node . js;也就是说,开发人员可以利用模板中丰富的 JavaScript API 的全部功能!
这与 Handlebars 使用的方法形成了惊人的对比,handle bars 不允许在模板中使用 JavaScript/Node.js 函数。尽管 Handlebars 的理念是限制标准函数,但它允许在 JavaScript/Node.js 代码中注册自定义函数(即,在模板本身之外)。
嵌入式 JavaScript (EJS)是 Node.js 应用的另一个受欢迎的选择,当性能很重要时,它可能是一个更好的替代选择,因为在基准测试中,EJS 比 Jade 表现得更好。大多数模板引擎都适用于浏览器 JavaScript 和 Node.js。
在本章中,我们将讨论以下主题:
- 如何使用模板引擎:将不同的模板引擎插入 Express.js 项目
- 不常见的库:在 Express.js 中使用罕见的模板引擎
- 模板引擎选择:不同的独立模板引擎库
- Consolidate.js:几乎所有模板引擎与 Express.js 无缝集成的一站式库
如何使用模板引擎
前几章中的一些例子使用了这两个配置语句:
app.set('views', path);
app.set('view engine', name);
或者,使用值:
var path = require('path')
// ... Configurations
app.set('views', path.join(__dirname, 'templates'));
app.set('view engine', 'ejs');
其中path
是模板所在文件夹的路径,name
是模板文件扩展名和 NPM 库名称(如jade is both an extension and an NMP name
)。
这两行足以让 Express.js 呈现 EJS 或 Jade 模板。我们甚至不需要在app.js
文件中导入 Jade。(但是我们仍然需要在本地安装模块!)这是因为,在幕后,Express.js 库基于扩展导入库(其确切的工作方式将在本章的下一节描述):
require('jade');
或者
require('ejs');
有两种方法可以指定模板引擎扩展:
- 用
render()
功能 - 用
view engine
设置
通常文件扩展名是该模板引擎的 NPM 模块名称。以下是第一种方法的示例,在这种方法中,扩展名可以简单地放在 render 函数的参数中的文件名之后:
response.render('index.jade');
在路由请求处理程序内部调用 response.render。本章稍后将提供更多关于渲染和其他响应对象方法的详细信息。
如果我们使用这种方法(即带有扩展名的完整文件名),我们可以省略这一行:
app.set('view engine', 'jade');
您可以在一个 Express.js 应用中混合搭配不同的模板引擎。
当然,Express.js 调用的库需要安装在本地node_modules
文件夹中。例如,要安装jade
v1.5.0,我们必须在package.json
中定义它,然后运行:
$ npm install
这是来自ch4/package.json
的台词:
"jade": "1.5.0",
要使用任何其他模板引擎,确保用 NPM 安装该模块,最好也通过手动或者用名为 --save
的npm install
将其添加到package.json
。
有趣的是,Express.js 使用views
作为默认值。因此,如果在views
文件夹中有模板,可以省略这一行:
app.set('views', path.join(__dirname, 'views'));
您已经知道如何使用app.set()
来创建 EJS 和 Jade 模板,所以现在让我们介绍如何使用配置方法:app.engine()
来使用替代模板引擎。
app.engine()
app.engine()
方法是一种设置模板引擎的低级方法。Express.js 在幕后使用了这种方法。
默认情况下,Express.js 将试图要求一个基于所提供的扩展的模板引擎(模板引擎 NPM 模块名——这就是为什么我们使用这个名称作为扩展!).例如,当我们在路由的请求处理程序或中间件中以index.jade
文件名作为参数调用res.render('index.jade');
(稍后将详细介绍该方法)时,框架在内部调用require('jade')
。
Express.js 代码中的完整语句(您还不需要自己实现它)是这样的:app.engine('jade', require('jade').__express);
,其中__express
是模板库应该实现的约定。
比方说,你更喜欢使用*.html
或*.template
而不是*.jade
来存放你的 Jade 文件。在这种情况下,您可以使用app.set()
和app.engine()
来覆盖默认扩展名。例如,要使用*.html
,请编写以下语句:
app.set('view engine', 'html');
app.engine('html', require('jade').__express);
然后,在每条路线中,编写类似这样的内容来呈现index.html
:
response.render('index');
或者,对于'*.template'
示例,您可以使用另一种方法,不使用视图引擎,在请求处理程序中使用完整的文件名(基本上是复制内部 Express.js 代码):
app.engine('template', require('jade').__express);
以下是请求处理程序调用:
response.render('index.template');
这种覆盖对于车把和其他采用普通 HTML 的模板引擎来说尤其酷,因为您可以重用您的遗留 HTML 文件而不会有太多麻烦。
不常见的图书馆
现在让我们来看看不常见的模板引擎的使用。如果你打算只使用普通的库,比如 Jade 或 EJS,你可以安全地跳过这一节的其余部分。
不太常见的 Node.js 库选择需要公开_express
方法,这是表示模板库支持这种 Express.js 格式的常见约定。所以检查一下模板引擎是否在你用require()
导入的源文件上有__express()
。如果__express()
方法存在,那么贡献者使这个库与 Express.js 兼容。同样,大多数库已经配备了使用 Express.js 的功能,他们有__express()
。
如果你选择的库没有__express
怎么办?如果模板模块有一个签名类似于__express
方法签名的方法,你可以很容易地用app.engine
定义你的模板引擎的方法;比如在swig
( https://github.com/paularmstrong/swig
)中,就是renderFile()
法。因此,考虑到您选择的模板引擎库中的renderFile
支持带有这些参数的函数签名:
path
:模板文件的路径locals
:用于渲染 HTML 的数据callback
:回调函数
您可以编写这样的代码,将这个库作为 Express.js 中间件来应用:
*// ... Declare dependencies*
*// ... Instantiate the app*
*// ... Configure the app*
app.engine('swig', require('swig').renderFile);
*// ... Define the routes*
文件夹中的例子展示了如何使用多个模板引擎和各种扩展。这是app.js
报表的独家新闻:
*// ... Declare dependencies*
*// ... Instantiate the app*
*// ... Configure the app*
var jade = require('jade');
var consolidate = require('consolidate');
app.engine('html', jade.__express);
app.engine('template', jade.__express);
app.engine('swig', consolidate.swig);
*// ... Define the routes*
app.get('/', function(request, response){
response.render('index.html');
});
app.get('/template', function(request, response){
response.render('index.template');
});
app.get('/swig', function(request, response){
response.render('index.swig');
})
这个consolidate
库将在本章后面解释。
package.json
文件有以下依赖项(用npm install
安装它们):
{
"name": "template-app",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"consolidate": "⁰.10.0",
"errorhandler": "1.1.1",
"express": "4.8.1",
"jade": "1.5.0",
"morgan": "1.2.2",
"swig": "¹.4.2",
"serve-favicon": "2.0.1"
}
}
用$ node app
启动应用应该会启动服务器,当你进入主页时,它会呈现“嗨,我是来自 index.html 的 Jade”(见图 5-1 )。
图 5-1 。从 index.html 文件中渲染出的玉石模板
此外,当你转到/swig
(见图 5-2 )时,服务器应该呈现“嗨,我正在从 index.swig 中 Swig”。
图 5-2 。从 index.Swig 文件呈现的 swig 模板
最后,当你转到/template
(见图 5-3 )时,它应该呈现“嗨,我是 index.template 文件中的 Jade”。
图 5-3 。从 index.Jade 文件呈现的 jade 模板
这可能是一个夸张的例子,因为您很少会在一个 Express.js 应用中使用多个模板引擎。然而,很高兴知道该框架足够灵活,允许您只需几条配置语句就可以实现它。
值得注意的是,在我们的proexpressjs/ch5
例子中,Jade 文件index.html
和index.template
都通过include filename
使用所谓的自顶向下包含(没有引号)。这允许我们在lorem-ipsum.html
文件中重用该段落的 Lorem Ipsum 文本。
我们示例中的文件只是一个纯文本文件,但是它可以包含 Jade 模板内容。index.html
看起来是这样的:
h1 Hi, I'm Jade from index.html file
p
include lorem-ipsum.html
And the index.temlate is similar:
h1 hi, I'm Jade in index.template file
p
include lorem-ipsum.html
内含物、布局和局部
自顶向下包含是一种标准的继承模式,其中父对象命令在哪里以及对子对象(被包含的对象)做什么。例如,你有一个包含文件 B(一部分)的文件 A,文件 A 将对文件 B 做它想做的事情。这是您将在大多数模板语言中使用的。
自顶向下包含的替代方法是自底向上模式。并非所有语言都支持它。在这种情况下,文件 A 是一个更大、更高层次的实体(A 布局),文件 B 是拼图中较小的一块,但是文件 B 将决定它想要什么。
当您从父类扩展这些方法时,您也可以将自顶向下的方法视为覆盖子类中的一些方法,同时保持其他方法不变。
在 Jade 中,自底向上是通过一组extend
、layout
和block
语句实现的。关于玉石的深度报道,请参考 Practical Node.js (Apress,2014)。
模板引擎选择
本节简要介绍支持 Express.js 的库,无需任何修改。这个选择列表来自 Express.js wiki 页面上的列表:https://github.com/strongloop/express/wiki#template-engines
。
翡翠
Jade ( https://github.com/jadejs/jade
)是一个受 Haml 启发的模板引擎。它非常强大,因为它有两种类型的继承,支持所有 JavaScript/Node.js,并且由于将空白和缩进视为语言的一部分,所以需要最少数量的符号/字符。
汉姆·js
Haml.js ( https://github.com/tj/haml.js
)是一个 Haml 实现。Haml 是 Rails 开发者的标准选择。这种语言将空白和缩进视为语言的一部分,这使得代码更紧凑,更不容易出现打字错误,从而使编写起来更愉快。
EJS
EJS ( https://github.com/tj/ejs
)是一个嵌入式 JavaScript 模板引擎。根据一些基准性能测试,EJS 比 Jade 或 Haml 更快(例如,参见http://paularmstrong.github.io/node-templates/benchmarks.html
)。
Handlebars.js
Hbs ( https://github.com/donpark/hbs
)是 Handlebars.js 的适配器,是 Mustache.js 模板引擎的扩展。按照设计,Handlebars 禁止在模板中放置复杂的逻辑。相反,开发人员需要在模板之外编写函数并注册它们。这是最容易学习的模板引擎。它经常用在反应模板中。如果您熟悉(或计划使用)Angular.js、Meteor 或 DerbyJS,那么这个选择可能更适合您,因为它与它们所使用的类似。
替代适配器是express-hbs
( https://github.com/barc/express-hbs
),它是 Barc ( http://barc.com
)的 Express 3 的带有布局、部分和块的把手。
另一个适配器是express-handlebars
( https://github.com/ericf/express-handlebars
)。
Hogan.js 适配器
h4e ( https://github.com/tldrio/h4e
)是 Hogan.js 的适配器,支持部分和布局。Hulk-hogan ( https://github.com/quangv/hulk-hogan
)是 Twitter 的 Hogan.js (Mustache syntax)的适配器,支持 partials。
康恩. js
Combyne.js ( https://github.com/tbranyen/combyne.js
)是一个模板引擎,希望它能按照您预期的方式工作。而combynexpress
( https://github.com/tbranyen/combynexpress
)是 Combyne.js 的快递库
大喝
Swig ( https://github.com/paularmstrong/swig
)是一个快速的类似 Django 的模板引擎。
胡须
络腮胡 ( https://github.com/gsf/whiskers.js
)小而快,留着小胡子(看起来像车把或小胡子)。比杰德还快(按http://paularmstrong.github.io/node-templates/benchmarks.html
)。
叶片
Blade ( https://github.com/bminer/node-blade
)是一个 HTML 模板编译器,受 Jade 和 Haml 的启发,将空白视为语言的一部分。
汉默咖啡
Haml-Coffee ( https://github.com/netzpirat/haml-coffee
)提供了 Haml 模板,你可以在其中编写内嵌的 CoffeeScript。如果您将 CoffeeScript 用于 Node.js 代码,这是非常完美的(CoffeeScript 的好处在本演示中突出显示:http://www.infoq.com/presentations/coffeescript-lessons
)。
鹰眼
Webfiller ( https://github.com/haraldrudell/webfiller
)是一个普通的 HTML5 双面渲染引擎,具有自我配置的路径,有组织的源代码树。Webfiller 是 100% JS。
巩固. js
如果您选择的模板引擎没有提供一个__express()
方法,或者您不确定并且不想费心去寻找,可以考虑 consolidate 库(https://npmjs.org/package/consolidate
;GitHub: https://github.com/tj/consolidate.js
。
consolidate
库简化并概括了几十个模板引擎模块,因此它们可以“很好地”与 Express.js 一起使用。这意味着不需要查找源代码来搜索__express()
方法的存在。您需要做的只是整合,然后将您选择的引擎映射到扩展。
下面是一个 Consolidate.js 示例:
var express = require('express');
var consolidate = require('consolidate');
var app = express();
*// ... Configure template engine*
app.engine('html', consolidate.handlebars);
app.set('view engine', 'html');
app.set('views', __dirname + '/views');
就是这样;res.render()
准备使用车把!
在撰写本文时,Consolidate.js 支持的模板引擎如表 5-1 所示(编译自 Consolidate.js GitHub 页面:https://github.com/tj/consolidate.js/blob/master/Readme.md
)。
表 5-1 。Consolidate.js 支持的模板引擎
|模板引擎
|
开源代码库
|
网站(如果适用)
|
| — | — | — |
| ATP 的 | https://github.com/soywiz/atpl.js
| |
| 灰尘 | https://github.com/akdubya/dustjs
| http://akdubya.github.io/dustjs/
|
| 生态的 | https://github.com/sstephenson/eco
| |
| 电休克疗法 | https://github.com/baryshev/ect
| http://ectjs.com
|
| ejs | https://github.com/tj/ejs
| http://www.embeddedjs.com
|
| 低增生性急性髓细胞性白血病 | https://github.com/tj/haml.js
| http://haml.info
|
| 汉默咖啡 | https://github.com/9elements/haml-coffee
| http://haml.info
|
| handlebars.js | https://github.com/wycats/handlebars.js/
| http://handlebarsjs.com
|
| 霍根网 | https://github.com/twitter/hogan.js
| http://twitter.github.io/hogan.js
|
| 翡翠 | https://github.com/jadejs/jade
| http://jade-lang.com
|
| 爵士乐 | https://github.com/shinetech/jazz
| |
| jqtpl | https://github.com/kof/jqtpl
| |
| 仅仅 | https://github.com/baryshev/just
| |
| 酒 | https://github.com/chjj/liquor
| |
| 洛拉斯 | https://github.com/lodash/lodash
| https://lodash.com
|
| 髭 | https://github.com/janl/mustache.js
| http://mustache.github.io
|
| 努恩朱克斯 | http://mozilla.github.io/nunjucks/
| |
| QEJS | https://github.com/jepso/QEJS
| |
| 活跃的 | https://github.com/ractivejs/ractive
| |
| 大喝 | https://github.com/paularmstrong/swig
| http://paularmstrong.github.com/swig/
|
| 模板化的 | http://archan937.github.io/templayed.js/
| |
| 太妃糖 | https://github.com/malgorithms/toffee
| |
| 强调 | https://github.com/jashkenas/underscore
| http://documentcloud.github.io/underscore/
|
| 海象 | https://github.com/jeremyruppel/walrus
| http://documentup.com/jeremyruppel/walrus/
|
| 胡须 | https://github.com/gsf/whiskers.js/
| |
Jade 模板语言本身就相当广泛,超出了本书的范围。要了解每个功能以及 extend 和 include(自上而下和自下而上)之间的差异,请参考 Practical Node.js (Apress,2014),其中有一整章专门讨论 Jade 和手柄。
摘要
模板是现代 web 开发的主要部分。没有它们,开发人员将不得不编写更多的代码,维护将会非常痛苦。说到 Node.js,Jade——与 Ruby on Rails 的 Haml 非常接近——是一个强大的选择。这是由于它丰富的特性和优雅的风格(空白和缩进是语言的一部分)。但是不先学玉就不要企图写玉。可能会很痛苦。
Express.js 支持不同的方法来配置模板和文件扩展名的位置。此外,Express.js 在配置拼图的不同部分时大放异彩;更改模板引擎只需要几行代码。
NPM 用户区提供了大量的模板引擎选择——正如您在“Consolidate.js”一节中看到的,还有许多其他模板库可以轻松地与 Express.js 兼容。他们有不同的风格、设计和表演。例如,Swig、EJS 和其他一些库经常在基准测试中胜过 Jade。如果你习惯了车把和小胡子的{{...}}}
风格(例如,来自 angular . js)——或者你没有时间来适当地学习 Jade 那么你可以马上使用那些库!
本章总结了app.js
文件的配置部分。我们继续走路线。我们将从路由的定义和从 URL 中提取参数开始。
六、参数和路由
回顾一下,Express.js 应用的典型结构(通常是一个server.js
或app.js
文件)大致由这些部分组成,顺序如下:
- 依赖关系:导入依赖关系的一组语句
- 实例化:创建对象的一组语句
- 配置:配置系统和自定义设置的一组语句
- 中间件:为每个传入请求执行的一组语句
- Routes :定义服务器路由、端点和页面的一组语句
- Bootup :一组启动服务器并让它在特定端口监听传入请求的语句
本章包括第五类,路由和我们在路由中定义的 URL 参数。这些参数以及 app.param()中间件是必不可少的,因为它们允许应用访问 URL 中从客户端传递的信息(例如,books/proexpressjs)。这是 REST APIs 最常见的约定。例如,http://hackhall.com/api/posts/521eb002d00c970200000003
路由将使用 521 EB 002d 00 c 9702000000003 的值作为帖子 ID。
参数是在请求的 URL 的查询字符串中传递的值。如果我们没有 Express.js 或类似的库,只能使用核心的 Node.js 模块,我们就必须通过某种require('querystring').parse(url)
或require('url').parse(url, true)
函数“诡计”从HTTP.request
( http://nodejs.org/api/http.html#http_http_request_options_callback
)对象中提取参数
让我们仔细看看如何为特定的 URL 参数定义特定的规则或逻辑。
参数
从 URL 中提取参数的第一种方法是在请求处理程序(route)中编写一些代码。如果您需要在其他路由中重复这个片段,您可以抽象代码并手动将相同的逻辑应用到许多路由。(To abstract code 的意思是重构代码,以便可以在其他地方重用和/或更好地组织。这提高了代码的可维护性和可读性。)
例如,假设我们需要用户资料页面(/v1/users/azat
定义为/v1/users/:username
)和管理页面(/v1/admin/azat
定义为/v1/admin/:username
)上的用户信息。一种方法是定义一个查找用户信息的函数(findUserByUsername
),并在每条路线中调用这个函数两次。这是我们实现它的方式(示例ch6/app.js
):
var users = {
'azat': {
email: 'hi@azat.co',
website: 'http://azat.co',
blog: 'http://webapplog.com'
}
};
var findUserByUsername = function (username, callback) {
// Perform database query that calls callback when it's done
// This is our fake database
if (!users[username])
return callback(new Error(
'No user matching '
+ username
)
);
return callback(null, users[username]);
};
app.get('/v1/users/:username', function(request, response, next) {
var username = request.params.username;
findUserByUsername(username, function(error, user) {
if (error) return next(error);
return response.render('user', user);
});
});
app.get('/v1/admin/:username', function(request, response, next) {
var username = request.params.username;
findUserByUsername(username, function(error, user) {
if (error) return next(error);
return response.render('admin', user);
});
});
您可以使用$ node app
命令运行 ch6 文件夹中的应用。然后,打开一个新的终端选项卡/窗口,并使用以下内容来处理 GET 请求:
$ curl http://localhost:3000/v1/users/azat
To see this:
user profile</h2><p>http://azat.co</p><p>http://webapplog.com</p>
并且随着
$ curl http://localhost:3000/v1/admin/azat
要看这个:
admin: user profile</h2><p>hi@azat.co</p><p>http://azat.co</p><p>http://webapplog.com</p><div><Practical>Node.js is your step-by-step guide to learning how to build scalable real-world web applications, taking you from installing Express.js to writing full-stack web applications with powerful libraries such as Mongoskin, Everyauth, Mongoose, Socket.IO, Handlebars, and everything in between.</Practical></div>
注意 Windows 用户可以从
http://curl.haxx.se/download.html
.
下载 CURL
或者,你可以在http://bit.ly/JGSQwr
使用 Postman Chrome 扩展。或者,对于 GET 请求,您可以使用浏览器——只需转到 URL。浏览器不会发出上传或删除请求,只有当您提交表单时,它才会发出发布请求。
最后一种方法是使用 jQuery 发出 AJAX/XHR 请求,但是要注意跨源限制,这意味着在服务器上使用相同的域或 CORS 头。或者你可以在你的浏览器中简单地进入http://localhost:3000/v1/users/azat
(见图 6-1 )和http://localhost:3000/v1/admin/azat
(见图 6-2 )。
图 6-1 。用户名 URL 参数被解析并用于查找用户页面上显示的信息(例如 ch6)
图 6-2 。用户名 URL 参数被解析并用于查找显示在管理页面上的信息(例如 ch6)
admin.jade
模板 ( 图 6-2 )与user.jade
( 图 6-1 )的内容略有不同,以帮助您区分这两个页面/路径,因此您可以确保它们都能正确解析和使用参数。
即使在将大部分代码抽象成findUserByUsername()
函数 之后,我们仍然以笨拙的代码结束。如果我们使用中间件方法,代码会变得稍微好一点。想法是编写一个定制的中间件 findUserByUsernameMiddleware,并将其用于需要用户信息的每个路由。下面是如何重构相同的两条路由并使用/v2
前缀(前缀通常用于区分 REST API 版本):
var findUserByUsername = function (username, callback) {
// Perform database query that calls callback when it's done
// This is our fake database!
if (!users[username])
return callback(new Error(
'No user matching '
+ username
)
);
return callback(null, users[username]);
};
var findUserByUsernameMiddleware = function(request, response, next){
if (request.params.username) {
console.log('Username param was detected: ', request.params.username)
findUserByUsername(request.params.username, function(error, user){
if (error) return next(error);
request.user = user;
return next();
})
} else {
return next();
}
}
// The v2 routes that use the custom middleware
app.get('/v2/users/:username',
findUserByUsernameMiddleware,
function(request, response, next){
return response.render('user', request.user);
});
app.get('/v2/admin/:username',
findUserByUsernameMiddleware,
function(request, response, next){
return response.render('admin', request.user);
});
中间件 findUserByUsernameMiddleware 检查参数(request.params.username
)是否存在,如果存在,则继续获取信息。这是一个更好的模式,因为它保持了路由的精简和逻辑的抽象。然而,Express.js 有一个更好的解决方案。它类似于中间件方法,但是它通过自动执行参数存在检查(即检查参数是否在请求中)使我们的生活变得更简单。遇见app.param()
法!
app.param()
只要给定的字符串(例如,username
)出现在路由的 URL 模式中,并且服务器接收到与该路由匹配的请求,就会触发对app.param()
的回调。例如,使用app.param('username', function(req, res, next, username){...})
和app.get('/users/:username', findUser)
时,每次我们有一个请求/username/azat
或/username/tjholowaychuk
,就会执行app.param()
中的关闭(在findUser
之前)。
app.param()
方法与app.use()
非常相似,但是它提供值(在我们的例子中是username
)作为函数的第四个,也是最后一个参数。在这个代码片段中,用户名将具有来自 URL 的值(例如,'azat'
代表/users/azat
):
app.param('username', function (request, response, next, username) {
*// ... Perform database query and*
*// ... Store the user object from the database in the req object*
req.user = user;
return next();
});
不需要额外的代码行,因为我们有由app.param()
填充的req.user
对象:
app.get('/users/:username', function(request, response, next) {
*//... Do something with req.user*
return res.render(req.user);
});
这条路线也不需要额外的代码。我们免费得到req.user
,因为前面定义了app.param()
:
app.get('/admin/:username', function(request, response, next) {
*//... Same thing, req.user is available!*
return res.render(user);
});
下面是我们如何将 param 中间件插入我们的应用的另一个例子:
app.param('id', function(request, response, next, id){
*// Do something with id*
*// Store id or other info in req object*
*// Call next when done*
next();
});
app.get('/api/v1/stories/:id', function(request, response){
*// Param middleware will be executed before and*
*// We expect req objects to already have needed info*
*// Output something*
res.send(data);
});
提示如果你有一个大型应用,有很多版本的 API 和 routes (v1、v2 等。),那么最好用
Router
类/对象来组织这些路由的代码。您创建一个Router
对象,并将其挂载到一个路径上,比如/api
或/api/v1
。路由只是var app = express()
对象的精简版。关于Router
类的更多细节将在本章后面提供。
下面是一个将 param 中间件插入到一个应用中的例子,该应用在req.db
中有一个 Mongoskin/Monk 类型的数据库连接:
app.param('id', function(request, response, next, id){
req.db.get('stories').findOne({_id: id}, function (error, story){
if (error) return next(error);
if (!story) return next(new Error('Nothing is found'));
req.story = story;
next();
});
});
app.get('/api/v1/stories/:id', function(request, response){
res.send(req.story);
});
或者,我们可以使用多个请求处理程序,但概念保持不变:我们可以预期在执行这段代码之前会抛出一个req.story
对象或错误,因此我们抽象出获取参数及其各自对象的公共代码/逻辑。这里有一个例子:
app.get('/api/v1/stories/:id', function(request, response, next) {
*//do authorization*
},
*//we have an object in req.story so no work is needed here*
function(request, response) {
*//output the result of the database search*
res.send(story);
});
注意授权和输入卫生是驻留在中间件中的很好的候选者。关于 OAuth 和 Express.js 的广泛示例,请参考实用 node . js1(Apress,2014)。
param()
函数特别酷,因为我们可以在路线中组合不同的变量;例如:
app.param('storyId', function(request, response, next, storyId) {
*// Fetch the story by its ID (storyId) from a database*
*// Save the found story object into request object*
*request.story = story;*
});
app.param('elementId', function(request, response, next, elementId) {
*// Fetch the element by its ID (elementId) from a database*
*// Narrow down the search when request.story is provided*
*// Save the found element object into request object*
*request.element = element;*
});
app.get('/api/v1/stories/:storyId/elements/:elementId', function(request, response){
// Now we automatically get the story and element in the request object
res.send({ story: request.story, element: request.element});
});
app.post('/api/v1/stories/:storyId/elements', function(request, response){
// Now we automatically get the story in the request object
// We use story ID to create a new element for that story
res.send({ story: request.story, element: newElement});
});
总之,通过定义 app.param 一次,它的逻辑将为具有匹配 URL 参数名称的每个路由触发。您可能想知道,“它与编写自己的函数并调用它,或者与编写自己的定制中间件有什么不同?”它们都可以正确地执行代码,但是 param 是一种更好的方法。我们可以重构我们之前的例子来展示不同之处。
让我们回到ch6
项目。如果我们重构前面来自ch6/app.js
的示例,并使用v3
作为新的路由前缀,我们可能会得到如下优雅的代码:
app.param('v3Username', function(request, response, next, username){
console.log(
'Username param was is detected: ',
username
)
findUserByUsername(
username,
function(error, user){
if (error) return next(error);
request.user = user;
return next();
}
);
});
app.get('/v3/users/:v3Username',
function(request, response, next){
return response.render('user', request.user);
}
);
app.get('/v3/admin/:v3Username',
function(request, response, next){
return response.render('admin', request.user);
}
);
因此,提取参数很重要,但定义路线更重要。定义路由也是使用app.param()
从 URL 参数中提取值的一种替代方法——当一个参数只使用一次时,推荐使用这种方法。如果不止一次使用,param 是更好的模式。
在前五章中已经定义了许多路线。在下一节中,我们将更详细地探索如何定义各种 HTTP 方法,链中间件,抽象中间件代码,以及定义所有方法路由。
路由
Express.js 是一个 Node.js 框架,它提供了一种将路由组织成更小的子部分(路由—Router
类/对象的实例)的方法。在 Express.js 3.x 和更早的版本中,定义路由的唯一方式是使用app.VERB()
模式,我们将在接下来介绍。然而,从 Express.js v4.x 开始,使用新的Router
类是推荐的通过router.route(path)
定义路线的方式。我们将首先介绍传统方法。
app。动词()
每个路由都是通过一个应用对象上的方法调用定义的,第一个参数是 URL 模式(也支持正则表达式2);也就是app.METHOD(path, [callback...], callback)
。
例如,要定义一个 GET /api/v1/stories
端点:
app.get('/api/v1/stories/', function(request, response){
// ...
})
或者,为 POST HTTP 方法和相同的路由定义一个端点:
app.post('/api/v1/stories', function(request, response){
// ...
})
也支持 DELETE、PUT 和其他方法。更多信息,参见http://expressjs.com/api.html#app.VERB
。
我们传递给get()
或post()
方法的回调被称为请求处理程序(在第七章中有详细介绍),因为它们接受请求(req
),处理请求,并写入响应(res
)对象。例如:
app.get('/about', function(request, response){
res.send('About Us: ...');
});
我们可以在一个路由中有多个请求处理器。除了第一个和最后一个之外,它们都将处于流程的中间(它们被执行的顺序),因此得名中间件。它们接受第三个参数/函数next
,当被调用时(next()
,将执行流切换到下一个处理程序。例如,我们有三个执行授权、数据库搜索和输出的功能:
app.get('/api/v1/stories/:id', function(request, response, next) {
*// Do authorization*
*// If not authorized or there is an error*
*// Return next(error);*
*// If authorized and no errors*
return next();
}), function(request, response, next) {
*// Extract id and fetch the object from the database*
*// Assuming no errors, save story in the request object*
request.story = story;
return next();
}), function(request, response) {
*// Output the result of the database search*
res.send(response.story);
});
名称next()
是一个任意的约定,这意味着您可以使用任何您喜欢的名称来代替next()
。Express.js 使用函数中参数的顺序来确定它们的含义。故事的 ID 是 URL 参数,我们需要它在数据库中查找匹配的条目。
现在,如果我们有另一条路线/admin
呢?我们可以定义多个请求处理程序,它们执行资源的认证、验证和加载:
app.get('/admin',
function(request, response, next) {
*// Check active session, i.e.,*
*// Make sure the request has cookies associated with a valid user session*
*// Check if the user has administrator privileges*
return next();
}, function(request, response, next){
*// Load the information required for admin dashboard*
*// Such as user list, preferences, sensitive info*
return next();
}, function(request, response) {
*// Render the information with proper templates*
*// Finish response with a proper status*
res.end();
})
但是如果/admin
的一些代码,比如授权/认证,是从/stories
复制过来的呢?下面的代码完成了同样的事情,但是通过使用命名函数,更加简洁:
var auth = function (request, response, next) {
// ... Authorization and authentication
return next();
}
var getStory = function (request, response, next) {
// ... Database request for story
return next();
}
var getUsers = function (request, response, next) {
// ... Database request for users
return next();
}
var renderPage = function (request, response) {
if (req.story) res.render('story', story);
else if (req.users) res.render('users', users);
else res.end();
}
app.get('/api/v1/stories/:id', auth, getStory, renderPage);
app.get('/admin', auth, getUsers, renderPage);
另一个有用的技术是将回调作为数组的项来传递,这得益于arguments
JavaScript 机制的内部工作方式: 3
var authAdmin = function (request, response, next) {
// ...
return next();
}
var getUsers = function (request, response, next) {
// ...
return next();
}
var renderUsers = function (request, response) {
// ...
res.end();
}
var admin = [authAdmin, getUsers, renderUsers];
app.get('/admin', admin);
路由和中间件中的请求处理程序之间的一个明显区别是,我们可以通过调用next('route');
来绕过链中的其余回调。如果在前面使用/admin
路由的例子中,请求在第一次回调中认证失败,这可能会很方便,在这种情况下没有必要继续。如果有多条路线匹配同一个 URL,您还可以使用next()
跳转到下一条路线。
请注意,如果我们传递给app.VERB()
的第一个参数包含查询字符串(例如/?debug=true
,Express.js 将忽略该信息。例如,app.get('/?debug=true', routes.index);
将被完全视为app.get('/', routes.index);
。
以下是最常用的表述性状态转移(REST) 服务器架构 HTTP 方法及其在 Express.js 中的对应方法以及简要含义:
- GET:
app.get()
—检索实体或实体列表 - HEAD:
app.head()
—与 GET 相同,只是没有主体 - 发布:
app.post()
—提交新实体 - PUT:
app.put()
—通过完全替换来更新实体 - 补丁:
app.patch()
—部分更新实体 - 删除:
app.delete()
和app.del()
—删除现有实体 - 选项:
app.options()
—检索服务器的功能
提示HTTP 方法是每个 HTTP(S)请求的特殊属性,类似于它的头或主体。在浏览器中打开 URL 是 GET 请求,提交表单是 POST 请求。其他类型的请求,如 PUT、DELETE、PATCH 和 OPTIONS,只能通过 CURL、Postman 或定制的应用(前端和后端)等特殊客户端获得。
有关 HTTP 方法的更多信息,请参考 RFC 2616 ( http://tools.ietf.org/html/rfc2616
)及其“方法定义”部分(第九部分)。
app.all()
app.all()
方法允许在特定路径上执行指定的请求处理程序,而不管请求的 HTTP 方法是什么。在定义全局或名称空间逻辑时,这个过程可能是救命稻草,如下例所示:
app.all('*', userAuth);
...
app.all('/api/*', apiAuth);
尾随斜线
默认情况下,结尾带有斜杠的路径被视为与正常路径相同。要关闭此功能,请使用app.enable('strict routing');
或app.set('strict routing', true);
。你可以在第三章中了解关于设置选项的更多信息。
路由类别
Router
类是一个只有中间件和路由的 mini Express.js 应用。这对于根据它们执行的业务逻辑抽象某些模块很有用。例如,所有的/users/*
路由可以在一个路由中定义,而所有的/posts/*
路由可以在另一个路由中定义。好处是,在我们用router.path()
在路由中定义了 URL 的一部分之后(见下一节),我们不需要一遍又一遍地重复它,就像使用app.VERB()
方法一样。
以下是创建路由实例的示例:
var express = require('express');
var router = express.Router(options);
// ... Define routes
app.use('/blog', router);
其中options
是可以具有以下属性的对象:
caseSensitive
: Boolean,表示是否将名称相同但字母大小写不同的路由视为不同,默认为false
;例如,如果设置为false
,那么/Users
与/users
相同。strict
: Boolean,表示是否将名称相同但尾部有无斜杠的路由视为不同,默认为false
;例如,如果设置为false
,那么/users
与/users/
相同。
router.route(路径)
router.route(path)
方法用于链接 HTTP 动词方法。例如,在一个创建、读取、更新和删除(CRUD) 服务器中,对于/posts/:id
URL(例如/posts/53fb401dc96c1caa7b78bbdb
)有 POST、GET、PUT 和 delete 端点,我们可以如下使用Router
类:
var express = require('express');
var router = express.Router();
// ... Importations and configurations
router.param('postId', function(request, response, next) {
// Find post by ID
// Save post to request
request.post = {
name: 'PHP vs. Node.js',
url: 'http://webapplog.com/php-vs-node-js'
};
return next();
});
router
.route('/posts/:postId')
.all(function(request, response, next){
// This will be called for request with any HTTP method
})
.post(function(request, response, next){
})
.get(function(request, response, next){
response.json(request.post);
})
.put(function(request, response, next){
// ... Update the post
response.json(request.post);
})
.delete(function(request, response, next){
// ... Delete the post
response.json({'message': 'ok'});
})
Router.route(path)
方法提供了链接方法的便利,这是一种比为每条路线重新键入router
更有吸引力的结构化代码的方式。
或者,我们可以使用router.VERB(path, [callback...], callback)
来定义路线,就像我们使用app.VERB()
一样。同样,router.use()
和router.param()
方法的工作原理与app.use()
和app.param()
相同。
回到我们的示例项目(在ch6
文件夹中),我们可以用Router
实现v4/users/:username
和v4/admin/:username
:
router.param('username', function(request, response, next, username){
console.log(
'Username param was detected: ',
username
)
findUserByUsername(
username,
function(error, user){
if (error) return next(error);
request.user = user;
return next();
}
);
})
router.get('/users/:username',
function(request, response, next){
return response.render('user', request.user);
}
);
router.get('/admin/:username',
function(request, response, next){
return response.render('admin', request.user);
}
);
app.use('/v4', router);
如您所见,router.get()
方法没有提到v4
。通常,router.get()
和router.param()
方法被抽象成一个单独的文件。这样,主文件(在我们的例子中是app.js
)保持精简,易于阅读和维护——这是一个很好的遵循原则!
请求处理程序
Express.js 中的请求处理程序与核心 Node.js http.createServer()
方法中的回调惊人地相似,因为它们只是带有req
和res
参数的函数(匿名、命名或方法):
var ping = function(req, res) {
console.log('ping');
res.end(200);
};
app.get('/', ping);
此外,我们可以利用第三个参数next()
来控制流程。这与错误处理的主题密切相关,错误处理将在第九章的中介绍。下面是两个请求处理程序的简单例子,ping
和pong
,其中前者在打印一个单词 ping 后跳到后者:
var ping = function(req, res, next) {
console.log('ping');
return next();
};
var pong = function(req, res) {
console.log('pong');
res.end(200);
};
app.get('/', ping, pong);
当请求出现在/
路线上时,Express.js 调用ping()
,在这种情况下它充当中间件(因为它在中间!).Ping 完成后,用res.end()
调用 pong 完成响应。
return
关键词也很重要。例如,如果在第一个中间件中认证失败,我们不想继续处理请求:
*// Instantiate app and configure error handling*
*// Authentication middleware*
var checkUserIsAdmin = function (req, res, next) {
if (req.session && req.session._admin !== true) {
return next (401);
}
return next();
};
*// Admin route that fetches users and calls render function*
var admin = {
main: function (req, res, next) {
req.db.get('users').find({}, function(e, users) {
if (e) return next(e);
if (!users) return next(new Error('No users to display.'));
res.render('admin/index.html', users);
});
}
};
*// Display list of users for admin dashboard*
app.get('/admin', checkUserIsAdmin, admin.main);
关键字return
是必不可少的,因为如果我们不在next(e)
调用中使用它,即使有错误和/或我们没有任何用户,应用也会试图呈现(res.render()
)。例如,以下可能是一个坏主意,因为在我们调用next()
之后,这将在错误处理程序中触发适当的错误,流程继续并试图呈现页面:
var admin = {
main: function (req, res, next) {
req.db.get('users').find({}, function(e, users) {
if (e) next(e);
if (!users) next(new Error('No users to display.'));
res.render('admin/index.html', users);
});
}
};
我们应该使用这样的东西:
if (!users) return next(new Error('No users to display.'));
res.render('admin/index.html', users);
或者类似这样的东西:
if (!users)
return next(new Error('No users to display.'));
else
res.render('admin/index.html', users);
摘要
在本章中,我们介绍了 Express.js 应用典型结构的两个主要方面:定义路线和提取 URL 参数。我们探索了如何将它们从 URL 中取出并在请求处理程序中使用它们的三种不同方式(req.params
、定制中间件和app.param()
)。您了解了如何为各种 HTTP 方法定义路由。最后,我们深入研究了充当 mini Express.js 应用的Router
类,并使用Router
类为示例项目实现了另一组路由。
每次我们定义路由(或中间件)时,我们都在回调中使用匿名函数定义或命名函数来定义请求处理程序。请求处理器通常有三个参数:request
(或req
)、response
(或res
)和next
。在下一章中,您将了解更多关于这些对象的内容,以及在 Express.js 中,它们与核心 Node.js http
模块的request
和response
有何不同。了解这些差异将为您提供更多的特性和功能!
1
2
3 参见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments
七、Express.js 请求对象
Express.js 请求对象(简称为req
)是核心 Node.js http.request
对象的包装器,它是传入 HTTP(S)请求的 Node.js 表示。在 web 中,请求包含以下部分:
- 方法:获取、发布或其他
- URI:地点举例
http://hackhall.com/api/posts/
- 标题:主机:
www.hackhall.com
- body:URL encoded、JSON 或其他格式的内容
Express.js 请求对象有一些额外的简洁功能,但本质上它支持本机http.request
对象可以做的一切。
例如,Express.js 自动添加了对查询解析的支持,当系统需要访问以下格式(问号后)的 URL 中的数据时,这一点至关重要:http://webapplog.com/?name1=value&name2=value
。
这是我们将在本章中涉及的 Express.js 请求对象的方法和对象列表:
request.query
:查询字符串参数request.params
: URL 参数request.body
:请求体数据request.route
:路线路径request.cookies
: cookie 数据request.signedCookies
:已签名的 cookie 数据request.header()
和request.get()
:请求头
提示当你在代码中看到
request.
doSomething 时,不要把 Express.js 请求对象与 Mikeal Roger 的request
模块(https://github.com/mikeal/request
)或者与 core Node.js http 模块的请求(http://nodejs.org/api/http.html#http_event_request
)混淆。
为了更好地理解请求对象,让我们用 express . js 4 . 8 . 1 版创建一个全新的 Express.js app。这是项目的 package.json
文件(ch7/package.json
):
{
"name": "request",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "4.8.1",
"errorhandler": "1.1.1",
"jade": "1.5.0",
"morgan": "1.2.2",
"serve-favicon": "2.0.1",
"cookie-parser": "1.3.2",
"body-parser": "1.6.5",
"debug": "~0.7.4",
"serve-favicon": "2.0.1"
}
}
接下来,我们将带有 NPM 的模块安装到本地项目node_modules
文件夹中:
$ npm install
现在用$ node app
启动 app。它应该显示一个标准的 Express.js 生成器页面,带有文本“欢迎使用 Express”(在http://localhost:3000
上)。本章末尾提供了app.js
的完整源代码供参考。你可以在https://github.com/azat-co/proexpressjs
从 GitHub 下载。
请求.查询
查询字符串是给定 URL 中问号右侧的所有内容;例如,在 URL https://twitter.com/search?q=js&src=typd
中,查询字符串是q=js&src=typd. After the query string is parsed by Express.js, the resulting
JS 对象将是{q:'js', src:'typd'}
。这个对象被分配给请求处理程序中的req.query
或request.query
,这取决于您在函数签名中使用的变量名。
默认情况下,解析由qs
模块(http://npmjs.org/qs
)
完成,Express.js 通过express/lib/middleware/query.js
内部模块在后台使用该模块。这个设置可以通过query parser
设置来改变,这个你在第三章里学过(希望如此)。
request.query
的工作方式类似于body-parser
的json()
和cookie-parser
中间件,因为它在请求对象req
上放置了一个属性(在本例中为query
),该请求对象被传递给下一个中间件并进行路由。因此,如果没有某种查询解析,我们就无法访问request.query
对象。同样,Express.js 默认使用qs
解析器——我们不需要额外的代码。
为了举例说明request.query
,我们可以添加一个搜索路径,以查询数据格式打印输入的搜索词。本例中的数据为q=js
、q=nodejs
和q=nodejs&lang=fr
。服务器返回 JSON,其中包含我们发送给它的相同查询字符串数据。我们可以将这个路由添加到任何 Express.js 服务器,比如我们用 CLI 创建的服务器(即ch7/request
):
app.get('/search', function(req, res) {
console.log(req.query)
res.end(JSON.stringify(req.query)+'\r\n');
})
提示
\n
和\r
分别是 ASCII 码和 Unicode 码中的换行符和回车符。它们允许文本在新的一行开始。更多信息请参考http://en.wikipedia.org/wiki/Newline
和http://en.wikipedia.org/wiki/Carriage_return
。
保持服务器运行($ node app
来启动它),在另一个终端窗口中,用 CURL 发出以下 GET 请求:
$ curl -i "http://localhost:3000/search?q=js"
$ curl -i "http://localhost:3000/search?q=nodejs"
$ curl -i "http://localhost:3000/search?q=nodejs&lang=fr"
CURL GET 请求的结果如图图 7-1 所示,服务器输出的结果如图图 7-2 所示。
图 7-1 。使用查询字符串参数运行 CURL 命令的客户端结果
图 7-2 。使用查询字符串参数运行 CURL 命令的服务器端结果
请求参数
第六章讲述了如何建立中间件来处理来自请求 URL 的数据。然而,有时直接从特定的请求处理程序中获取这些值更方便。为此,有一个request.params
对象,它是一个包含键/值对的数组。
为了试验request.params
对象,我们可以向我们的ch7/request
应用添加一条新的路线。这个路由将定义 URL 参数,并在控制台中打印它们。添加以下路线到request/app.js
:
app.get('/params/:role/:name/:status', function(req, res) {
console.log(req.params);
res.end();
});
接下来,运行以下 CURL 终端命令,如图图 7-3 所示:
$ curl http://localhost:3000/params/admin/azat/active
$ curl http://localhost:3000/params/user/bob/active
图 7-3 。用 CURL 发送 GET 请求(客户端窗口)
如图 7-4 所示,我们看到request.params
对象的这些服务器日志:
[ role: 'admin', name: 'azat', status: 'active' ]
[ role: 'user', name: 'bob', status: 'active' ]
图 7-4 。处理请求的服务器结果。参数
请求.正文
request.body
对象是 Express.js 提供给我们的另一个神奇对象,它是通过应用body-parser
(express . js 3 . x 中的express.bodyParser()
)中间件函数填充的。主体解析器模块有两个功能/中间件:
json()
:用于将 HTTP(S)有效负载解析成 JavaScript/Node.js 对象urlencoded()
:用于将 URL 编码的 HTTP(S)请求数据解析成 JavaScript/Node.js 对象
在这两种情况下,结果对象和数据都被放入request.body
对象中——非常方便!
要使用request.body
,我们需要单独安装 body-parser(如果你使用的是ch7
,你可以跳过这一步,因为生成器为我们把它放在了package.json
):
$ npm install body-parser@1.0.0
然后我们需要导入并应用它:
var bodyParser = require('body-parser');
// ...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
您不必同时使用json()
和urlencoded()
方法。如果足够的话,只使用需要的那个。
为了说明request.body
的作用,让我们重用我们之前的项目,并添加下面的路径来看看request.body
对象是如何工作的,记住两个bodyParser()
中间件功能都已经应用到 Express.js 应用中,并包含在代码中:
app.post('/body', function(req, res){
console.log(req.body);
res.end(JSON.stringify(req.body)+'\r\n');
});
同样,使用 CURL 或类似工具提交几个 HTTP POST 请求:
$ curl http://localhost:3000/body -d 'name=azat'
$ curl -i http://localhost:3000/body -d 'name=azat&role=admin'
$ curl -i -H "Content-Type: application/json" -d '{"username":"azat","password":"p@ss1"}' http://localhost:3000/body
提示一个简短的提示:
-H
选项设置头,-d
传递数据,-i
启用详细日志记录。
前面的命令产生了request.body
对象,如图 7-5 中的客户端和图 7-6 中的服务器端所示:
{ name: 'azat' }
{ name: 'azat', role: 'admin' }
{ username: 'azat', password: 'p@ss1' }
图 7-5 。使用 CURL 发送 POST 请求(客户端日志)
图 7-6 。处理请求的结果。正文(服务器日志)
请求.路由
request.route
对象只包含当前路线的信息,例如:
path
:请求的原始 URL 模式method
:请求的 HTTP 方法keys
:URL 模式中的参数列表(即以:
为前缀的值)regexp
: Express.js 为路径生成的模式params
:request.params
对象
我们可以将上一节示例中的console.log(request.route)
;
语句添加到我们的request.params
路由中,如下所示:
app.get('/params/:role/:name/:status', function(req, res) {
console.log(req.params);
console.log(req.route);
res.end();
});
然后,如果我们发送 HTTP GET 请求
$ curl http://localhost:3000/params/admin/azat/active
我们应该得到request.route
对象的服务器日志,它有path
、stack
和methods
属性:
{ path: '/params/:role/:name/:status',
stack: [ { method: 'get', handle: [Function] } ],
methods: { get: true } }
当从中间件内部使用时,request.route
对象可能是有用的(即,在多个路由上使用),以找出当前使用的路由。
请求. cookie
cookie 解析器(在 Express.js 3.x 和更早的版本中以前是express.cookieParser()
)中间件(https://www.npmjs.org/package/cookie-parser
、https://github.com/expressjs/cookie-parser
)允许我们以 JavaScript/Node.js 格式访问请求的 cookie。快速会话中间件需要cookie-parser
,因为 web 会话通过将会话 ID 存储在浏览器 cookies 中来工作。
随着cookie-parser
的安装(用 NPM)、导入(用 ??)、应用(用 ??),我们可以通过request.cookies
对象访问 HTTP(S)请求 cookie(用户代理 cookie)。Cookies 自动呈现为 JavaScript 对象;例如,您可以使用以下命令提取会话 ID:
request.cookies['connect.sid']
警告出于安全考虑,不鼓励在浏览器 cookies 中存储敏感信息。此外,一些浏览器对 cookie 的大小施加了限制,这可能会导致错误(Internet Explorer!).我通常只用
request.cookie
来支持request.session
。
注关于如何安装和应用中间件的更多信息,请参考第四章。
可以使用response.cookie()
或res.cookie()
存储 cookie 信息。Express.js 响应对象包含在第八章的中。为了说明request.cookies
,我们可以实现一个/cookies
路由,它将增加一个计数器,改变 cookie 的值,并在页面上显示结果。这是您可以添加到ch7/request
中的代码:
app.get('/cookies', function(req, res){
if (!req.cookies.counter)
res.cookie('counter', 0);
else
res.cookie('counter', parseInt(req.cookies.counter,10) + 1);
res.status(200).send('cookies are: ', req.cookies);
})
提示
parseInt()
方法用于防止 JavaScript/Node.js 将数字值视为字符串,这将导致 0、01、011、0111 等。而不是 0,1,2,3 等等。建议将parseInt()
与基数/基数(第二个参数)一起使用,以防止数字被错误转换。
由于转到http://localhost:3000/cookies
并刷新几次,你应该看到计数器从 0 向上递增,如图图 7-7 所示。
图 7-7 。Cookie 值保存在浏览器中,并在每次请求时由服务器递增
检查 Chrome 开发者工具中的网络或资源标签会发现一个名为connect.sid
的 cookie 的存在(见图 7-7 )。浏览器窗口之间共享 cookies,因此即使我们打开一个新窗口,计数器也会从原始窗口中的值增加 1。
请求.已签名的预订
request.signedCookies
类似于request.cookies
,但是它是在将秘密字符串传递给express.cookieParser('some secret string');
方法时使用的。要填充request.signedCookies
,您可以使用带有标志signed: true
的response.cookie
。下面是我们如何修改之前的路线以切换到签名 cookies:
app.use(cookieParser('abc'));
// ... Other middleware
app.get('/signed-cookies', function(req, res){
if (!req.signedCookies.counter)
res.cookie('counter', 0, {signed: true});
else
res.cookie('counter', parseInt(req.signedCookies.counter,10) + 1, {signed: true});
res.status(200).send('cookies are: ', req.signedCookies);
});
// ... Server boot-up
因此,我们所做的就是将request.cookies
改为request.signedCookies
,并在响应上分配 cookie 值时添加signed: true
。签名 cookies 的解析是自动完成的,它们被放在普通的 JavaScript/Node.js 对象中。注意'abc'
是一个任意的字符串。你可以在 Mac OS X 上使用$ uuidgen
来生成一个随机密钥,给你的 cookies 或者 Random.org 之类的网络服务签名(http://bit.ly/1F1fbL8
)。
注意签署 cookie 不会隐藏或加密 cookie。这是通过应用私有值来防止篡改的简单方法。签名(或哈希)不同于加密。前者用于识别和防止篡改。后者用于对未授权的接收者隐藏内容(例如,参见
http://danielmiessler.com/study/encoding_encryption_hashing
)。您可以在服务器上加密您的 cookie 数据(并在读取时解密),但是,假设这仍然容易受到暴力攻击。漏洞的级别取决于您使用的加密算法。
request.header()和 request.get()
request.header()
和request.get()
方法是相同的,并且允许通过名称检索 HTTP(S)请求的头。幸运的是,标题命名不区分大小写:
request.get('Content-Type');
request.get('content-type');
request.header('content-type');
其他属性和方法
我们已经介绍了 Express.js 请求对象的最常用和最重要的方法和对象。在大多数情况下,它们应该足够了。但是清单并不止于此。为了方便起见,在 Express.js 请求中有大量的糖衣对象(见表 7-1 )。糖衣意味着这些对象的大部分功能可以用基础方法实现,但是它们比基础方法更有说服力。例如,request.accepts
可以替换为if/else
和request.get()
,这为我们提供了请求头。当然,如果您理解这些方法,您可以使用它们来使您的代码更优雅、更易读。
表 7-1 。Express.js 请求中的其他属性和方法
|属性/方法
|
条件/定义
|
应用接口
|
| — | — | — |
| request.accepts()
| true
如果传递的字符串(单个或逗号分隔的值)或 MIME 类型(或扩展)的数组与请求Accept
头匹配;false
如果没有匹配 | http://expressjs.com/api.html#req.accepts
|
| request.accepted
| 接受的 MIME 类型的数组 | http://expressjs.com/api.html#req.accepted
|
| request.is()
| true
如果传递的 MIME 类型字符串匹配Content-Type
头类型;false
如果没有匹配 | http://expressjs.com/api.html#req.is
|
| request.ip
| 请求的 IP 地址;参见第三章中的trust proxy
配置 | http://expressjs.com/api.html#req.ip
|
| request.ips
| 启用trust proxy
配置时的 IP 阵列 | http://expressjs.com/api.html#req.ips
|
| request.path
| 带有请求的 URL 路径的字符串 | http://expressjs.com/api.html#req.path
|
| request.host
| 请求的Host
报头中的值 | http://expressjs.com/api.html#req.host
|
| request.fresh
| true
如果请求是基于Last-Modified
和ETag
标题的新鲜;false
否则 | http://expressjs.com/api.html#req.fresh
|
| request.stale
| 与req.fresh
相反 | http://expressjs.com/api.html#req.stale
|
| request.xhr
| true
如果请求是通过 X- Requested-With
报头及其XMLHttpRequest
值的 AJAX 调用 | http://expressjs.com/api.html#req.xhr
|
| request.protocol
| 请求协议值(如http
或https
) | http://expressjs.com/api.html#req.protocol
|
| request.secure
| true
如果请求协议是https
| http://expressjs.com/api.html#req.secure
|
| request.subdomains
| 来自Host
头的子域数组 | http://expressjs.com/api.html#req.subdomains
|
| request.originalUrl
| 请求 URL 的不可更改值 | http://expressjs.com/api.html#req.originalUrl
|
| request.acceptedLanguages
| 请求的Accept-Language
头中的语言代码数组(如en-us, en
) | http://expressjs.com/api.html#req.acceptedLanguages
|
| request.acceptsLanguage()
| true
如果传递的语言代码在请求报头中 | http://expressjs.com/api.html#req.acceptsLanguage
|
| request.acceptedCharsets
| 请求的Accept-Charset
头中的字符集数组(如iso-8859-5
) | http://expressjs.com/api.html#req.acceptedCharsets
|
| request.acceptsCharset()
| true
如果传递的字符集在请求头中 | http://expressjs.com/api.html#req.acceptsCharset
|
在这一章中,我们一直在对ch7
项目做一些小的调整,所以现在是时候看看全貌了。因此,下面是来自ch7/app.js
文件的最终request
服务器的完整源代码(可在https://github.com/azat-co/proexpressjs
获得):
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('combined'));
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser('abc'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.get('/search', function(req, res) {
console.log(req.query);
res.end(JSON.stringify(req.query)+'\r\n');
});
app.get('/params/:role/:name/:status', function(req, res) {
console.log(req.params);
console.log(req.route);
res.end();
});
app.post('/body', function(req, res){
console.log(req.body);
res.end(JSON.stringify(req.body)+'\r\n');
});
app.get('/cookies', function(req, res){
if (!req.cookies.counter)
res.cookie('counter', 0);
else
res.cookie('counter', parseInt(req.cookies.counter,10) + 1);
res.status(200).send('cookies are: ', req.cookies);
});
app.get('/signed-cookies', function(req, res){
if (!req.signedCookies.counter)
res.cookie('counter', 0, {signed: true});
else
res.cookie('counter', parseInt(req.signedCookies.counter,10) + 1, {signed: true});
res.status(200).send('cookies are: ', req.signedCookies);
});
/// Catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
/// Error handlers
// Development error handler
// Will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// Production error handler
// No stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;
var debug = require('debug')('request');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});
摘要
理解和处理 HTTP 请求是 web 开发的基础。Express.js 处理请求的方式是添加对象和属性。开发人员在请求处理程序中使用它们。Express.js 在请求中提供了许多对象和方法,在它没有提供的地方,有许多第三方选项。
在下一章中,我们将讨论 Express.js 响应。响应对象是请求对象的对应对象。响应是我们实际上发送回客户端的东西。与 request 类似,Express.js 响应对象具有特殊的方法和对象作为其属性。我们将讨论最重要的,然后列出其余的内置属性。
八、Express.js 响应对象
Express.js 响应对象(简称res
)——它是请求处理程序回调中的一个参数——是老一套的 Node.js http.response
对象 1 。这是因为 Express.js 响应对象有新的方法。换句话说,Express.js 响应对象是http.response
类的扩展。
为什么有些人会使用这些额外的方法?的确,你可以使用response.end()
方法 2 和其他核心方法,但那样你就必须编写更多的代码。例如,您必须手动添加内容类型头。但是使用 Express.js 响应对象,它包含方便的包装器,如response.json()
和response.send()
,适当的内容类型会被自动添加。
在本章中,我们将详细介绍 Express.js 响应对象的以下方法和属性:
response.render()
response.``locals
response.set()
response.status()
response.send()
response.json()
response.jsonp()
response.redirect()
为了演示这些方法,我们在厨房水槽应用 ch8/app.js
中使用了它们。其他方法和属性及其含义将在表 8-1 中列出。在本章的最后,我们将介绍如何使用 streams 和 Express.js 响应。
从示例应用开始,用express-generator
和$ express response
终端命令创建一个全新的 Express.js 应用。显然,现在你需要运行$ cd response && npm install
来下载依赖项。最初的ch8/app.js
应用将与来自第七章的最初应用相同。
response.render()
response.render()
方法是 Express.js 的主食。从我们前面的例子和函数的名字,你可以猜测它与从模板(如 Jade、Handlebars 或 EJS)和数据生成 HTML 有关。
response.render(name, [data,] [callback])
方法有三个参数,但只有一个是强制的,这是第一个参数:name
,它是字符串格式的模板名称。其他参数是data
和callback
。如果你省略了data
,但是有callback
,那么callback
成为第二个参数。
模板名可以用或不用扩展名来标识。有关模板引擎扩展的更多信息,请参考第五章。
为了说明response.render()
最简单的用例,我们将创建一个页面,显示来自 Jade 模板的标题和段落。
首先,添加一条路线。下面是一个在response/app.js
文件中简单设置主页路径的例子:
app.get('/render', function(req, res) {
res.render('render');
});
然后,添加一个新的views/render.jade
文件,它现在看起来是静态的(即,它没有变量或逻辑):
extends layout
block content
h1= 'Pro Express.js'
p Welcome to the Pro Express.js Response example!
最后,用$ node app
启动响应应用,并在浏览器中转至http://localhost:3000
。你应该会看到如图图 8-1 所示的欢迎信息。
图 8-1 。不带参数的普通 response.render()调用的结果
注意 Jade 使用类似 Python/Haml 的语法,考虑到了空格和制表符。小心标记。我们可以使用
=
作为打印命令(h1
标签)或者什么都不用(p
标签)。欲了解更多信息,请访问官方文档(http://jade-lang.com/
)或查看Practical node . js(a press,2014)。 3
response.render()
除了必须的name
参数外,还有两个可选参数:data
和callback
。data
参数使模板比静态 HTML 文件更动态,并允许我们更新输出。例如,我们可以通过“标题”来覆盖默认值中的值:
app.get('/render-title', function(req, res) {
res.render('index', {title: 'Pro Express.js'});
});
index.jade
文件保持不变。它打印标题值,如下所示:
extends layout
block content
h1= title
p Welcome to #{title}
/render-title
路线的结果如图图 8-2 所示。h1
标题文本已更改为 Pro Express.js。
图 8-2 。带有数据参数的 response.render()示例具有标题属性
response.render()
callback
参数本身接受两个参数:error
和html
(一个输出的 HTML 字符串)。这个例子不在res/app.js
项目中,但是展示了如何向response.render()
传递回调:
app.get('/render-title', function(req, res) {
res.render('index', {title: 'Pro Express.js'}, function (error, html) {
*// Do something*
});
});
注意
data
参数的属性是模板中的局部变量。换句话说,如果您想访问模板中标题的值,数据对象必须包含一个键/值对。大多数模板引擎都支持嵌套对象。
因为 Express.js 能够确定参数的类型,所以callback
可以代替data
。这个例子不在response/app.js
中,但是展示了如何用我们的数据传递回调:
app.get('/render-title', function(req, res) {
res.render('index', function (error, html) {
*// Do something*
});
});
在后台,response.render()
调用response.send()
(这将在本章后面介绍)成功编译 HTML 字符串,或者调用req.next(error)
失败,如果没有提供回调,则调用*。换句话说,对response.render()
的默认回调是来自位于https://github.com/visionmedia/express/blob/3.3.5/lib/response.js#L753
的 GitHub 上 3.3.5 版本位置的代码:*
*// Default callback to respond*
fn = fn || function(err, str){
if (err) return req.next(err);
self.send(str);
};
查看这段代码,您会发现,只要响应有一个结尾(response.json
、response.send
或response.end
),就可以很容易地编写自己的回调函数来做任何事情。
响应.本地人
response.locals
对象是向模板传递数据的另一种方式,这样数据和模板都可以被编译成 HTML。您已经知道第一种方法是将数据作为参数传递给response.render()
方法,如前所述:
app.get('/render-title', function(req, res) {
res.render('index', {title: Pro Express.js'});
});
然而,有了response.locals
,我们可以实现同样的事情。我们的对象将在模板内部可用:
app.get('/locals', function(req, res){
res.locals = { title: 'Pro Express.js' };
res.render('index');
});
同样,index.jade
Jade 模板保持不变:
extends layout
block content
h1= title
p Welcome to #{title}
在图 8-3 中可以看到标题为 Pro Express.js 的网页。但是,如果什么都没有改变,那么response.locals
有什么好处呢?这样做的好处是,我们可以在一个中间件中公开(即传递给模板)信息,但是稍后在另一个请求处理程序中呈现实际的模板。例如,您可以在不渲染的情况下执行身份验证(这段代码不在ch8/app.js
中):
app.get('/locals',
function(req, res){
res.locals = { user: {admin: true}};
next();
}, function(req, res){
res.render('index');
});
图 8-3 。response.locals 示例呈现与 response.render()示例相同的页面
提示有时候,为了调试,查看特定 Jade 模板中所有可用变量的列表是很有用的。为此,只需插入以下日志语句:
- console.log(locals);
。更多关于翡翠的信息,请参考*实用 Node.js * (Apress,2014)。 4
response.set()
response.set(field, [value])
方法是response.header()
的别名(或者反过来),充当 Node.js http 核心模块的response.setHeader()
函数的包装器。 5 主要区别在于,Express.js’ response.set()
足够聪明,当我们以对象的形式传递多个头值对给它时,它会递归地调用自己。如果前面的句子对您没有多大意义,请参阅本节后面的 CSV 示例。
下面是一个来自ch8/app.js
的例子,它将单个Content-Type
响应头设置为text/html
,然后向客户端发送一些简单的 HTML:
app.get('/set-html', function(req, res) {
*// Some code*
res.set('Content-Type', 'text/html');
res.end('<html><body>' +
'<h1>Express.js Guide</h1>' +
'</body></html>');
});
你可以在 Chrome 开发者工具的“网络”标签中看到结果,在“标题”子标签下,显示为Content-Type: text/html
(参见图 8-4 )。如果我们没有带text/html
的response.set()
,那么响应仍然会有 HTML,但是没有标题的*。随意评论response.set()
,自己看。*
图 8-4 。使用 Content-Type: text/html 标题呈现 HTML 的 response.set()示例
当我们没有用response.set()
显式设置 Content-Type 时,它就消失了,因为 Express.js’ response.send()
会自动添加Content-Type
和其他头,但 core response.end()
不会。本章稍后将详细介绍response.send()
。
不过,我们的服务器通常需要提供不止一个头,以便所有不同的浏览器和其他 HTTP 客户端能够正确处理它。让我们探索一个向response.set()
方法传递多个值的例子。
假设我们正在构建的服务发出包含书名和标签的逗号分隔值(CVS)文件。这就是我们如何在ch8/app.js
文件中实现这条路线:
app.get('/set-csv', function(req, res) {
var body = 'title, tags\n' +
'Practical Node.js, node.js express.js\n' +
'Rapid Prototyping with JS, backbone.js node.js mongodb\n' +
'JavaScript: The Good Parts, javascript\n';
res.set({'Content-Type': 'text/csv',
'Content-Length': body.length,
'Set-Cookie': ['type=reader', 'language=javascript']});
res.end(body);
});
现在,如果你将 Chrome 转向http://localhost:3000/set-csv
,浏览器将识别 CSV MIME 类型并下载文件,而不是打开它(至少使用默认的 Chrome 设置,没有额外的扩展名)。您可以在图 8-5 的中看到标题。
图 8-5 。response.set()示例使用 CSV 数据呈现内容长度、内容类型和 Set-Cookie 头
response.status()
response.status()
方法接受一个 HTTP 状态码 6 号,并发送它作为响应。最常见的 HTTP 状态代码有:
200
:好的201
:已创建301
:永久移动401
:未经授权404
:未找到500
:内部服务器错误
你可以在第九章中找到一个更长的 HTTP 状态列表。其核心对应物 7 的唯一区别在于response.status()
是可链接的。状态代码对于构建 REST APIs 非常重要,因为它们使您能够标准化请求的结果。
让我们演示一下response.status()
如何在 pulse route 上工作,如果服务器还在运行,它将返回200
(OK)。这个路由不会故意发回任何文本或 HTML。我们使用response.end()
,因为response.send()
会自动添加正确的状态代码 200:
app.get('/status', function(req, res) {
res.status(200).end();
});
如果你去http://localhost:3000/status
,你会看到一个绿色的圆圈和数字 200,如图图 8-6 所示。
图 8-6 。response.status()示例响应
response.send()
response.send()
方法介于高级response.render()
和低级response.end()
之间。response.send()
方法使用自动生成的特有的 HTTP 头(如 Content-Length、ETag 或 Cache-Control)方便地输出任何数据应用(如字符串、JavaScript 对象,甚至缓冲区)。
由于其杂食(消耗任何输入)行为(由arguments.length
引起),response.send()
可以通过这些输入参数以无数种方式使用:
- 用文本/html 字符串 :
response.send('success');
- 用 JSON 表示的对象 :
response.send({message: 'success'});
或response.send({message: 'error'});
- 用 JSON 表示的数组 :
response.send([{title: 'Practical Node.js'}, {title: 'Rapid Prototyping with JS'}]);
- 缓冲器 :
response.send(new Buffer('Express.js Guide'));
与application/octet-stream
提示发送以
response.send(number)
为状态码的数字在 Express.js 4.x 中已被弃用,改用response.status(number).send()
。
状态代码和数据参数可以组合在一个链式语句中。例如:
app.get('/send-ok', function(req, res) {
res.status(200).send({message: 'Data was submitted successfully.'});
});
在添加新的send-ok
路由并重启服务器之后,当您转到/send-ok
时,您应该能够看到 JSON 消息。注意状态代码和Content-Type
标题。虽然 200 会自动添加,但建议为所有其他情况设置状态,如201
表示已创建,或404
表示未找到。
图 8-7。response.send() 200 状态代码示例响应
以下是将500
内部服务器错误状态代码与错误消息一起发送的示例(用于服务器错误):
app.get('/send-err', function(req, res) {
res.status(500).send({message: 'Oops, the server is down.'});
});
同样,当您在浏览器中检查这条路线时,有一个 JSON 内容类型,但现在您会看到一个红圈和数字500
。
图 8-8。响应状态(500)。send() 500 状态代码示例响应
如果之前明确指定,由response.send()
生成的标题可能会被覆盖。例如,缓冲区类型将把Content-Type
作为application/octet-stream
,但是我们可以用
app.get('/send-buf', function(req, res) {
res.set('Content-Type', 'text/plain');
res.send(new Buffer('text data that will be converted into Buffer'));
});
产生的内容类型和文本如图 8-9 所示。
图 8-9 。response.send()缓冲示例响应
注意几乎所有的核心 Node.js 方法(以及 Connect.js 方法)都可以在 Express.js 对象中找到。因此,我们可以访问 Express.js 响应 API 中的
response.end()
和其他方法。
response.json()
response.json()
方法是发送 JSON 数据的一种便捷方式。当传递的数据是数组或对象类型时,相当于response.send()
。在其他情况下,response.json()
用JSON.stringify()
强制数据转换。默认情况下,标题Content-Type
设置为application/json
,但可以在response.json()
之前用response.set()
覆盖。
如果你记得我们在第三章、json replacer
和json spaces
中的老朋友,那就是这些设定被考虑的地方。
response.json()
最常见的用法是使用适当的状态代码:
app.get('/json', function(req, res) {
res.status(200).json([{title: 'Practical Node.js', tags: 'node.js express.js'},
{title: 'Rapid Prototyping with JS', tags: 'backbone.js node.js mongodb'},
{title: 'JavaScript: The Good Parts', tags: 'javascript'}
]);
});
请注意图 8-10 中response.json()
生产的 JSON Content-Type
和Content-Length
头。
图 8-10 。使用 response.json()的结果:自动生成的头
注图 8-10 中的
response.json()
例子截图是在ch8/app.js
项目的ch8/app.js
文件中添加路线后拍摄的。我们鼓励你自己尝试这样做。
response.json()
的其他用法也是可能的——例如,没有状态代码:
app.get('/api/v1/stories/:id', function(req,res){
res.json(req.story);
});
假设req.story
是一个数组或一个对象,下面的代码将产生与前面的代码片段类似的结果(在这两种情况下都不需要将头设置为application/json
):
app.get('/api/v1/stories/:id', function(req,res){
res.send(req.story);
});
response.jsonp()
response.jsonp()
方法类似于response.json()
,但是提供了一个 JSONP 响应。也就是说,JSON 数据被包装在 JavaScript 函数调用中。例如,processResponse({...});
通常用于跨域呼叫支持。默认情况下,Express.js 使用一个callback
名称来提取回调函数的名称。可以用jsonp callback name
设置覆盖该值(更多信息见第三章)。如果请求的查询字符串中没有指定适当的回调(例如?callback=cb
,那么响应就是 JSON。
假设通过 JSONP 向前端请求提供 CSV 数据(status(200)
是可选的,因为默认情况下 Express 会自动添加正确的状态 200):
app.get('/', function (req, res) {
res.status(200).jsonp([{title: 'Express.js Guide', tags: 'node.js express.js'},
{title: 'Rapid Prototyping with JS', tags: 'backbone.js, node.js, mongodb'},
{title: 'JavaScript: The Good Parts', tags: 'javascript'}
]);
});
图 8-11 。response.jsonp()的结果和?callback=cb 是一个文本/javascript 头和 javascript 函数前缀
注图 8-11 中的
response.json()
示例截图是在 ch2/ cli-app/app.js
项目的index.js
文件中添加路线后拍摄的。我们鼓励你自己尝试这样做。
response.redirect()
有时我们只需要将用户/请求重定向到另一个路由。我们可以使用绝对、相对或完整路径:
res.redirect('/admin');
res.redirect('../users');
res.redirect('http://rapidprototypingwithjs.com');
默认情况下,response.redirect()
发送 302(找到/临时移动)状态代码。 8 当然,我们可以像response.send()
一样,根据自己的喜好进行配置;即,将第一个状态代码编号作为第一个参数进行传递(301
被永久移动):
res.redirect(301, 'http://rpjs.co');
其他响应方法和属性
在表 8-1 中列出的大多数方法和属性都是书中已经介绍过的方法的方便替代。换句话说,我们可以用 main 方法完成大部分逻辑,但是知道下面的快捷键可以让开发人员节省一些击键次数,提高可读性。例如,response.type()
是仅用于Content-Type
报头的response.header()
的一个特例。
表 8-1 。方法和属性备选方案
|
方法/属性
|
描述/条件
|
应用接口
|
| — | — | — |
| response.get()
| 传递的标头类型的响应标头的字符串值 | http://expressjs.com/api.html#res.get
|
| response.cookie()
| 接受 cookie 键/值对,并在响应时设置它 | http://expressjs.com/api.html#res.cookie
|
| response.clearCookie()
| 采用 cookie 键/名称和可选路径参数来清除 cookie | http://expressjs.com/api.html#res.clearCookie
|
| response.location()
| 将相对、绝对或完整路径作为字符串,并将该值设置为Location
响应头 | http://expressjs.com/api.html#res.location
|
| response.charset
| 响应的字符集值 | http://expressjs.com/api.html#res.charset
|
| response.type()
| 获取一个字符串并将其设置为Content-Type
头的值 | http://expressjs.com/api.html#res.type
|
| response.format()
| 将对象作为类型和响应的映射,并根据Accepted
请求头执行它们 | http://expressjs.com/api.html#res.format
|
| response.attachment()
| 将可选文件名作为字符串,并将Content-Disposition
(如果提供了文件名,Content-Type
)标题设置为attachment
并相应地设置文件类型 | http://expressjs.com/api.html#res.attachment
|
| response.sendfile()
| 获取服务器上文件的路径、各种选项和回调参数,并将文件发送给请求者 | http://expressjs.com/api.html#res.sendfile
|
| response.download()
| 取与response.sendfile()
相同的参数,设置Content-Disposition
并调用response.sendfile()
| http://expressjs.com/api.html#res.download
|
| response.links()
| 接受一个 URL 对象来填充Links
响应头 | http://expressjs.com/api.html#res.links
|
您可以在ch8
文件夹和 GitHub ( https://github.com/azat-co/proexpressjs
)上找到本章示例的完整源代码。清单 8-1 展示了ch8/app.js
文件的样子(包括其他例子)。
清单 8-1 。ch8/app.js 文件
var express = require('express');
var fs = require('fs');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var largeImagePath = path.join(__dirname, 'files', 'large-image.jpg');
var app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('combined'));
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser('abc'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.get('/render', function(req, res) {
res.render('render');
});
app.get('/render-title', function(req, res) {
res.render('index', {title: 'Pro Express.js'});
});
app.get('/locals', function(req, res){
res.locals = { title: 'Pro Express.js' };
res.render('index');
});
app.get('/set-html', function(req, res) {
// Some code
res.set('Content-Type', 'text/html');
res.end('<html><body>' +
'<h1>Express.js Guide</h1>' +
'</body></html>');
});
app.get('/set-csv', function(req, res) {
var body = 'title, tags\n' +
'Practical Node.js, node.js express.js\n' +
'Rapid Prototyping with JS, backbone.js node.js mongodb\n' +
'JavaScript: The Good Parts, javascript\n';
res.set({'Content-Type': 'text/csv',
'Content-Length': body.length,
'Set-Cookie': ['type=reader', 'language=javascript']});
res.end(body);
});
app.get('/status', function(req, res) {
res.status(200).end();
});
app.get('/send-ok', function(req, res) {
res.status(200).send({message: 'Data was submitted successfully.'});
});
app.get('/send-err', function(req, res) {
res.status(500).send({message: 'Oops, the server is down.'});
});
app.get('/send-buf', function(req, res) {
res.set('Content-Type', 'text/plain');
res.status(200).send(new Buffer('text data that will be converted into Buffer'));
});
app.get('/json', function(req, res) {
res.status(200).json([{title: 'Practical Node.js', tags: 'node.js express.js'},
{title: 'Rapid Prototyping with JS', tags: 'backbone.js node.js mongodb'},
{title: 'JavaScript: The Good Parts', tags: 'javascript'}
]);
});
app.get('/non-stream', function(req, res) {
var file = fs.readFileSync(largeImagePath);
res.end(file);
});
app.get('/non-stream2', function(req, res) {
var file = fs.readFile(largeImagePath, function(error, data){
res.end(data);
});
});
app.get('/stream1', function(req, res) {
var stream = fs.createReadStream(largeImagePath);
stream.pipe(res);
});
app.get('/stream2', function(req, res) {
var stream = fs.createReadStream(largeImagePath);
stream.on('data', function(data) {
res.write(data);
});
stream.on('end', function() {
res.end();
});
});
/// Catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
/// Error handlers
// Development error handler
// Will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// Production error handler
// No stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;
var debug = require('debug')('request');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});
流
至于在response.send()
和response.end()
之间发送非流式响应,您应该已经在前面的讨论中有所涉及。然而,对于流数据的返回,response.send()
是行不通的;相反,您应该使用响应对象(这是一个可写的流,继承自http.ServerResponse
):
app.get('/stream1', function(req, res) {
var stream = fs.createReadStream(largeImagePath);
stream.pipe(res);
});
或者,对data
和end
事件使用事件处理程序:
app.get('/stream2', function(req, res) {
var stream = fs.createReadStream(largeImagePath);
stream.on('data', function(data) {
res.write(data);
});
stream.on('end', function() {
res.end();
});
});
非流对等项可能如下所示:
app.get('/non-stream', function(req, res) {
var file = fs.readFileSync(largeImagePath);
res.end(file);
});
对于这个演示,我们使用一个相对较大的 5.1MB 的图像,它位于ch8/files/large-image.jpg
。请注意图 8-12 中的所示的分流和图 8-13 中的所示的非分流之间在等待时间上的巨大差异。非流式路由等待整个文件加载,然后将整个文件发送回来(大约 49 毫秒),而流式路由等待的时间要少得多(只有大约 7 毫秒)。我们在非流式示例中使用同步函数的事实并不重要,因为我们是串行加载页面的(一个接一个)。
图 8-12 。流式传输图像显示出比非流式传输更快的等待时间
图 8-13 。非流式图像显示的等待时间比流式图像慢
提示除了使用流进行响应之外,它们也可以用于请求。在处理大量数据(视频、二进制数据、音频等)时,流式传输非常有用。)因为流允许在没有完成传输的情况下开始处理。有关流的更多信息,请查看
https://github.com/substack/stream-handbook
和https://github.com/substack/stream-adventure
。
摘要
如果您已经了解了响应的每个属性,那么您可能比一般的 Express.js 开发人员了解得更多。恭喜你!理解请求和响应是 Express.js 开发的基础。
我们几乎完成了 Express.js 接口(也称为 API)。剩下的部分是错误处理和实际启动应用。
1
2
3
4
5
6
7
8