ExpressJS 高级教程(二)

原文:Pro Express.js

协议:CC BY-NC-SA 4.0

五、模板引擎和 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 安装该模块,最好也通过手动或者用名为 --savenpm 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.htmlindex.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 中,自底向上是通过一组extendlayoutblock语句实现的。关于玉石的深度报道,请参考 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.jsapp.js文件)大致由这些部分组成,顺序如下:

  1. 依赖关系:导入依赖关系的一组语句
  2. 实例化:创建对象的一组语句
  3. 配置:配置系统和自定义设置的一组语句
  4. 中间件:为每个传入请求执行的一组语句
  5. Routes :定义服务器路由、端点和页面的一组语句
  6. 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/:usernamev4/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()方法中的回调惊人地相似,因为它们只是带有reqres参数的函数(匿名、命名或方法):

var ping = function(req, res) {
  console.log('ping');
  res.end(200);
};

app.get('/', ping);

此外,我们可以利用第三个参数next()来控制流程。这与错误处理的主题密切相关,错误处理将在第九章的中介绍。下面是两个请求处理程序的简单例子,pingpong,其中前者在打印一个单词 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模块的requestresponse有何不同。了解这些差异将为您提供更多的特性和功能!


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.queryrequest.query,这取决于您在函数签名中使用的变量名。

默认情况下,解析由qs模块(http://npmjs.org/qs )完成,Express.js 通过express/lib/middleware/query.js内部模块在后台使用该模块。这个设置可以通过query parser设置来改变,这个你在第三章里学过(希望如此)。

request.query的工作方式类似于body-parserjson()cookie-parser中间件,因为它在请求对象req上放置了一个属性(在本例中为query),该请求对象被传递给下一个中间件并进行路由。因此,如果没有某种查询解析,我们就无法访问request.query对象。同样,Express.js 默认使用qs解析器——我们不需要额外的代码。

为了举例说明request.query,我们可以添加一个搜索路径,以查询数据格式打印输入的搜索词。本例中的数据为q=jsq=nodejsq=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/Newlinehttp://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对象的服务器日志,它有pathstackmethods属性:

{ 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-parserhttps://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: trueresponse.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/elserequest.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-ModifiedETag标题的新鲜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 | 请求协议值(如httphttps) | 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,它是字符串格式的模板名称。其他参数是datacallback。如果你省略了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参数外,还有两个可选参数:datacallbackdata参数使模板比静态 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 参数本身接受两个参数:errorhtml(一个输出的 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.jsonresponse.sendresponse.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/htmlresponse.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 replacerjson 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-TypeContent-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);
});

或者,对dataend事件使用事件处理程序:

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-handbookhttps://github.com/substack/stream-adventure

摘要

如果您已经了解了响应的每个属性,那么您可能比一般的 Express.js 开发人员了解得更多。恭喜你!理解请求和响应是 Express.js 开发的基础。

我们几乎完成了 Express.js 接口(也称为 API)。剩下的部分是错误处理和实际启动应用。


1

2

3

4

5

6

7

8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值