ExpressJS-高级教程-全-

ExpressJS 高级教程(全)

原文:Pro Express.js

协议:CC BY-NC-SA 4.0

零、简介

如果你正在考虑是否要买这本书,这份介绍将帮助你确定它完全适合你的专业水平和需求。如果你已经买了这本书,那么恭喜你!现在,您已经做好了充分的准备,可以更深入地研究增长最快的平台的最流行的 web 框架。

随着初创公司和大公司逐渐意识到采用 Node.js 的好处,Node.js 和 Express.js 中的技能需求正处于快速增长的边缘。新技术的早期采用者和主流采用者之间总是有差距,Node.js 的主流采用者正在迅速接近。对于开发人员来说,这意味着现在是精通 Node.js 的最佳时机。您的技能将会大受欢迎!

为什么写这本书

几年前我开始写关于 Express.js 的文章,当时我写了 Express.js 指南。该书于 2013 年自助出版,是第一本专门介绍 Express.js 的书籍,express . js 是迄今为止最受欢迎的 Node.js web 框架(截至 2014 年 12 月撰写本文时)。在我写 Express.js 指南的时候,Express.js 官方网站(http://expressjs.com)只提供了一点点见解,而且是为高级 Node.js 程序员准备的。我遇到过很多人——包括通过 Hack Reactor 程序和我在 General Assembly 和 pariSOMA 上的 Node.js 课程——他们对一本权威手册感兴趣,这本手册将涵盖 Express.js 的所有不同组件在现实生活中如何协同工作。Express.js Guide 的目标就是成为这样的资源。

Express.js Guide 成为 Amazon.com 同类图书中的第一畅销书后,一家出版社联系我写这本书,以扩展该材料并接受专业技术编辑的正式审查。 Pro Express.js 远不止是 Express.js 指南的修订或更新。这是一个完整的翻拍,增加了评论,描述,例子和额外的东西。 Pro Express.js 拥有经过更好审核的代码和文本,以及最新版本的库(例如 Express.js v4.8.1)。

在写这两本书的过程中,很多事情都发生了变化。Node.js 在 io.js 处分叉,Express.js 的创建者 TJ Holowaychuk 不再积极参与 Node.js,现在 StrongLoop ( http:/strongloop.com)维护框架的知识库。Express.js 上的发展一如既往的迅猛。它更稳定,更安全。我只看到 Express.js 和 Node.js 更光明的未来!

谁应该拥有这本书

本书面向已经精通编程和前端 JavaScript 的软件工程师和 web 开发人员。为了从 Pro Express.js 中获得最大的好处,您应该熟悉 Node.js 的基本概念,例如进程和全局,还应该知道核心模块,包括流、集群和缓冲区。

如果您正在考虑开始一个 Node.js 项目或者重写一个现有的项目,并且您选择的武器是 Express.js,那么这个指南就是为您准备的!它将回答你的大多数“如何”和“为什么”的问题。

这本书是什么

Pro Express.js 是一本关于某个特定图书馆的详尽书籍。与覆盖许多库的 Practical Node.js (Apress,2014)不同,Pro Express.js 专注于单个模块 Express.js。当然,在有必要覆盖其他相关库的地方,如中间件,这本书也会涉及这些库,但不像框架本身那样广泛。

Pro Express.js 涵盖中间件、命令行界面和脚手架、渲染模板、从动态 URL 中提取参数、解析有效负载和 cookies、管理会话认证、错误处理以及为生产准备应用。

Pro Express.js 由四个不同的部分组成,各章节在目录中列出:

  1. 入门:让您体验框架的快速入门演练
  2. 深度 API 参考:作为 Express.js v4.8.1 API 参考,当您需要关于某些方法的信息时,您可以完整阅读或浏览
  3. 解决常见和抽象的问题:代码组织和模式的最佳实践——使用 Express.js 时需要了解的主题
  4. 教程和示例:现实世界的教程(精心描绘的编码练习)和示例(对更复杂应用的不太详细的解释)

这本书不是什么

这本书不是对 Node.js 的介绍,也不是一本非常详细地涵盖构建现代 web 应用的所有方面的书,例如 WebSockets、数据库和(当然)前端开发。你不会在这里找到学习编程或 JavaScript 基础知识的帮助,因为这不是一本初学者的书。

关于 Node.js、MongoDB 和 Backbone.js 前端开发的介绍,你可能想看看我的书用 js 快速原型化:敏捷 JavaScript 开发 ( http://rapidprototypingwithjs.com)或者考虑亲自或在线参与 Node 计划(http://nodeprogram.com)。

在现实世界中——尤其是在 Node.js 开发中,由于它的模块化理念——我们很少只使用一个框架。然而,在本书中,我试图坚持使用 Express.js,并尽可能地省略其他内容,而不影响示例的有用性。因此,我有意省略了 web 开发的一些重要部分——例如,数据库、认证和测试。虽然这些元素出现在教程和例子中,但没有详细解释。如果你想了解更多关于这些话题的内容,附录 A 列出了一些你可能想查阅的相关书籍。

例子

Pro Express.js 充满了代码片段和可运行的例子。一些例子是一步一步的,精心解释的教程,鼓励你在阅读本书时自己复制。其他的是一些简短的代码示例,目的是为了说明某一点。

大部分源代码可以在 GitHub 资源库的https://github.com/azat-co/proexpressjs文件夹ch1ch18下获得(对应于第一章–第十八章)。第四部分中的例子更加广泛,并且存在于它们自己的库中。可以在以下网址找到它们:

  • Instagram Gallery : https://github.com/azat-co/sfy-gallery
  • 全部应用:??]
  • REST API: https://github.com/azat-co/rest-api-express
  • 骇客厅:??

所提供的示例是仅用给定的特定版本的依赖项编写和测试的。因为 Node.js 及其模块生态系统正在快速开发中,请关注新版本是否有突破性变化。以下是我用过的版本列表:

  • Express.js v4.8.1
  • Node.js v0.10.12
  • NPM v1.2.32 版
  • mongodb v 2 . 6 . 3 版
  • Redis v2.6.7
  • 触控笔 v0.47.3
  • 杰德 v1.5.0
  • 领班 v0.75.0
  • 谷歌浏览器版本 39.0.2171.7

勘误表和联系人

如果你被困在一个练习中,一定要检查 GitHub 库。在 GitHub 问题版块(https://github.com/azat-co/proexpressjs/issues)中可能会有更新的代码和答案。此外,通过提交您自己的问题,您可以帮助您的程序员同事获得更好的体验。

至于令人讨厌的错别字,我敢肯定,无论我检查手稿多少次,其中一些仍然会存在,请将它们提交给出版社(通过www.apress.com/9781484200384的勘误表)或 GitHub Issues。

最后,让我们在网上做朋友吧!孤立地编码是孤独的。以下是联系我以及与其他开发人员交流的一些方法:

  • 写一篇 Amazon.com 评论:http://amzn.to/1D6qiqk
  • 加入 HackHall.com,一个面向程序员、黑客和开发者的社区
  • 在 Twitter 上发布你的 Node.js 问题:@azat_co
  • 跟随我上脸书:http://facebook.com/1640484994
  • 访问 Pro Express.js 网站:http://proexpressjs.com
  • 访问我的网站:http://azat.co
  • 启动 Pro Express.js GitHub 库:https://github.com/azat-co/proexpressjs
  • 直接给我发邮件:hi@azat.co
  • 注册博客时事通讯:http://webapplog.com

既然你已经到了介绍的结尾,让 Twitter 上的每个人都知道你即将通过 Pro Express.js : http://ctt.ec/91iHS开始学习 Express.js。在你有机会阅读这本书之后,请写一篇 Amazon.com 评论,让其他人知道你对这本书的看法。

一、从 Express.js 开始

Express.js 是基于核心 Node.js http模块 1 并连接 2 组件的 web 框架。这些组件被称为中间件。它们是框架理念的基石,即配置优于约定。一些熟悉 Ruby 的开发人员将 Express.js 与 Sinatra 相比较,后者与 Ruby on Rails 框架有着非常不同的方法,后者更倾向于约定而不是配置。换句话说,开发人员可以自由选择他们需要的特定项目的库。这种方法为他们提供了高度定制项目的灵活性和能力。

如果您只使用核心 Node.js 模块编写过任何严肃的应用,您很可能会发现自己在重复编写相同的代码来完成类似的任务,例如:

  • 解析 HTTP 请求正文
  • 解析 cookies
  • 管理会话
  • 根据请求的 URL 路径和 HTTP 方法,用一系列if条件组织路由
  • 基于数据类型确定正确的响应头
  • 处理错误
  • 提取 URL 参数(如/messages/3233)

后来,您可能创建了自己的库来重用代码,但是您的库不会像社区支持的最好的库那样经过彻底的测试。此外,维护将完全由您和您的团队承担。因此,我的建议是使用社区模块,如果它适合你的需要。这个建议同样适用于使用小型库和 web 框架。

Express.js 解决了这些和其他许多问题。它提供了优雅地重用代码的方法,并为您的 web 应用提供了类似于模型-视图-控制器(MVC) 的结构。模型(M)部分需要由一个额外的数据库驱动库提供(例如,mongose3)。这些应用可以是各种各样的,从准系统、仅后端 REST APIs 到成熟的、全栈的、实时的 web 应用,还有额外的库,如jade-browser ( https://npmjs.org/package/jade-browser)和socket.io ( http://socket.io)。

为了让你快速开始使用 Express.js,并且不要太深入地钻研它的 API,我们将在本章中讨论这些主题:

  • Express.js 如何工作
  • Express.js 安装
  • Express.js 发电机安装

Express.js 如何工作

Express.js 是一个 Node 包管理器(NPM 或npm)模块,它依赖于您的应用。这意味着每个用/on Express.js 构建的项目都需要在本地node_modules文件夹中有框架的源文件(不是全局的!).为此,您可以像安装任何其他 NPM 模块一样安装 Express.js,使用$ npm install,例如$ npm install express@4.2.0

现在,我们可以概述一个 Express.js 应用的典型结构。假设您的应用在一个server.js文件中,您计划用$ node server.js启动您的应用。在这种情况下,您需要在server.js文件中要求并配置 Express.js。该文件通常包含完成以下任务的语句:

  1. 包括第三方依赖项以及您自己的模块,如控制器、实用程序、助手和模型
  2. Express.js 对象和其他对象的实例化
  3. 连接到 MongoDB 4 、Redis 5 或 MySQL 6 等数据库
  4. 配置 Express.js 应用设置,例如模板引擎及其文件扩展名
  5. 定义中间件,如错误处理程序、静态文件夹、cookies 和其他解析器
  6. 定义路由及其请求处理程序
  7. 启动将在特定主机和端口上启动服务器的应用

当然,如果您的应用很大,您将有多个文件来配置您的 Express.js 应用,而不仅仅是单个的server.jsapp.js文件。原因是更好的代码组织。例如,在一个文件中,您将配置会话,在另一个身份验证中,在另一个路由中,等等。

Image 提示在应用开发的高级阶段(通常导致部署到生产环境中),您可能希望使用forever ( https://npmjs.org/package/forever )模块和 Upstart 来实现更好的应用正常运行时间。你也可以利用第十三章中概述的cluster模块来产生多个工人。

第三方依赖关系

定义第三方依赖关系很简单:

var name = require('name');

依赖关系通常包括 Express.js 库本身,以及必要的中间件,如body-parser。定义多个依赖项的另一种方法是在每个定义后面跟一个逗号:

var express = require('express'),
  compression = require('compression'),
  bodyParser = require('body-parser'),
  mongo = require('mongoskin');

实例化

要使用 Express.js,需要实例化它。同时,实例化任何其他对象也是一个很好的做法:

var app = express();
var db = mongo.db('mongodb://localhost:27017/integration_tests', {native_parser: true});

Image 提示你不必命名 Express.js 模块express或者命名 Express.js 应用app。变量名可以是任意的。然而,本书中的大多数例子都使用了expressapp来避免混淆。

连接到数据库

连接到数据库的语句不必在开头,只要它们在第 7 步“启动应用”之前(来自本节前面的列表)——除非我们将数据库用作会话存储。例如:

var session = require('express-session');
var RedisStore = require('connect-redis')(session);
app.use(session({
  store: new RedisStore(options),
  secret: 'Pro Express.js rocks!'
}));

大部分数据库驱动,如 Mongoskin 7 和 mongose8都支持查询的缓冲;这样,如果服务器在连接建立之前正在运行,数据库查询将被缓冲以供以后执行(当数据库连接建立时)。

配置 Express.js 应用设置

简单来说,配置 Express.js app 设置就是用app.set()给字符串键设置一些值。其中一些键由 Express.js 使用并增强其行为,而其他键是任意的。例如,如果您正在使用 Jade 模板引擎和*.jade文件,使用'view engine'让 Express.js 知道它需要寻找*.jade文件:

app.set('view engine', 'jade');

在接下来的章节中,你会发现更多关于配置设置的信息。

有时我们希望在服务器对象上存储一个自定义值,以供将来参考。例如,我们可以将port赋给环境变量PORT中的一个值,或者,如果没有定义,赋给3000,这样我们就可以在所有源代码中使用这个值:

app.set('port', process.env.PORT || 3000);

定义中间件

中间件是一个特殊的功能,允许更好的代码组织和重用。一些中间件被打包成第三方(NPM)模块,可以开箱即用。其他时候,我们可以编写自己的自定义中间件。在这两种情况下,语法都是app.use():

app.use(bodyParser.json());

定义路线

路由可以是好的旧网页,也可以是 REST API 端点。在这两种情况下,语法是相似的:我们使用app.VERB(),其中VERB()是一个 HTTP 方法,比如 GET、POST、DELETE、PUT、OPTIONS 或 PATCH。例如,我们可以将主页(根)路由定义为

app.get('/', renderHomePage);

启动应用

最后,在配置好一切之后,我们可以用server.listen(portNumber)启动服务器,其中server是用app对象创建的核心http server对象:

var server = http.createServer(app);
var boot = function () {
  server.listen(app.get('port'), function(){
    console.info('Express server listening on port ' + app.get('port'));
  });
};
var shutdown = function() {
  server.close();
};

如果这个文件包含在另一个文件中(例如,一个测试),我们可能想要导出服务器对象,而不是引导它。我们执行检查的方式是使用require.main === module;如果这是真的,那么这个文件没有被其他任何东西包含。测试将使用我们导出的方法boot()自动启动服务器。我们还出口shutdown()port:

if (require.main === module) {
  boot();
} else {
  console.info('Running app as a module');
  exports.boot = boot;
  exports.shutdown = shutdown;
  exports.port = app.get('port');
}

当 Express.js 应用运行时,它会监听请求。每个传入的请求都根据定义的中间件链和路由进行处理,从上到下进行处理。这一点很重要,因为它允许您控制执行流程。

例如,我们可以有多个函数来处理每个请求,其中一些函数位于中间(因此被称为中间件):

  1. 解析 cookie 信息,完成后进入下一步。
  2. 解析 URL 中的参数,完成后进入下一步。
  3. 如果用户被授权(cookie/session),则根据参数值从数据库中获取信息,如果匹配,则进入下一步。
  4. 显示数据并结束响应。

Express.js 安装

Express.js 是一个依赖模块,应该安装在本地(项目)node_modules文件夹:$ npm install express@4.8.1

Image 提示 NPM 查找node_modules文件夹或package.json文件。如果这是一个全新的文件夹,既没有文件夹也没有文件,你可以用$ mkdir node_modules创建node_modules,或者用$ npm init创建package.json

对于作为依赖项的本地 Express.js 模块安装,让我们创建一个新文件夹,$ mkdir proexpressjs。这将是本书的项目文件夹。现在,我们可以用$ cd proexpressjs打开它。

Image 提示为了方便起见,大部分示例都位于 GitHub 资源库 azat-co/proexpressjs ( http://github.com/azat-co/proexpressjs)中。但是,我强烈建议您键入书中的代码,并使用您的文本、名称和自定义逻辑对其进行修改。不要复制/粘贴代码,甚至更糟——只需运行我们的 GitHub 示例。使用书中提供的完整源代码,GitHub 仅在您遇到困难或在您阅读完这本书后需要为您的项目使用一些 reciepe 时作为参考。这个建议是基于大量的研究,这些研究表明写作或打字的人比那些只看内容的人记忆和学习更有效。还要做笔记!

一旦我们进入项目文件夹,我们可以在文本编辑器中手动创建package.json或者使用$ npm init终端命令。当你使用这个命令时,会要求你提供项目名称、描述等细节,如图图 1-1 所示。

9781484200384_Fig01-01.jpg

图 1-1 。运行$ npm init 的结果

这是图 1-1 中带有普通$ npm init选项的package.json文件示例:

{
  "name": "ch1",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

最后,要使用 NPM 安装模块:

$ npm install express

前面的命令将获取 Express.js 的最新版本。但是,对于本书,建议您使用指定的版本,因此改为运行以下命令:

$ npm install express@4.8.1 --save

--save标志是可选的。它在package.json中为这个模块创建一个条目。

如果您想为现有项目安装 Express.js 并将依赖关系保存到package.json文件中(聪明的做法!)—已经存在于该项目的文件夹中—运行:

 $ npm install express --save.

Image 注意如果你试图在没有package.json文件或node_modules文件夹的情况下运行前面提到的$ npm install express命令,智能 NPM 将遍历目录树找到有这两个文件的文件夹。这种行为有点模仿 Git 的逻辑。有关 NPM 安装算法的更多信息,请参考https://npmjs.org/doc/folders.html的官方文档。

如果你的项目是空白的,使用$ npm init然后$ npm install express@4.8.1 --save的方法是好的。因此,该命令将下载 express 及其依赖项,并列出它们(如图图 1-2 ):

pg9.jpg

9781484200384_Fig01-02.jpg

图 1-2 。运行 npm install express@4.8.1 的结果-保存

信息express@4.8.1 node_modules/express很重要,因为它告诉我们the express被放置在哪里(在node_modules)。

或者,如果项目不为空,并且package.json已经包含了所有有用的信息,我们可以在package.json文件中添加我们的新依赖项及其版本号或一些意外的组合(不推荐生产应用使用),例如"express": "4.8.x""express": "4.8.1", or "*",然后运行$ npm install

添加了 Express.js v4.8.1 依赖项的package.json文件如下所示:

{
  "name": "proexpress",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "express": "4.8.1"
  },
  "author": "",
  "license": "BSD"
}

$ npm install

为了仔细检查 Express.js 的安装及其依赖项,我们可以运行一个$ npm ls命令(参见图 1-3 ,它列出了这个本地项目中的所有模块:

pg11.jpg

9781484200384_Fig01-03.jpg

图 1-3 。运行$ npm ls 和$ ls node_modules 的结果

命令$ npm ls列出了这个项目(当前文件夹)的本地依赖项。此时,我们至少应该看到express@4.8.1(如图图 1-3 )。另一种检查方法是使用$ ls node_modules(也显示在图 1-3 中),它列出了node_modules中的文件夹。

Express.js 发电机安装

Express.js 生成器(express-generator)是一个独立的模块(从 express . js 4 . x 版开始,之前都是捆绑的)。它允许快速创建应用,因为它的脚手架机制接受命令行选项并基于它们生成 Express.js 应用。

要安装 Express.js Generator,一个用于搭建的命令行工具,从 Mac/Linux 机器上的任何地方运行$ npm install -g express-generator。(对于 Windows 用户,路径会有所不同。检查NODE_PATH环境变量,如果您想要一个不同的全局位置,请更新它。)安装命令将下载并链接$ express终端命令到正确的路径,以便我们以后创建新应用时可以访问它的命令行界面(CLI)。

当然,我们可以更具体一点,告诉 NPM 使用$ npm install -g express@4.2.0 .版本安装 4.2.0 版本的expressexpress-generator模块不必(通常也不会)匹配,因为它们是独立的模块。因此,您需要根据变更日志和 GitHub 自述文件来了解什么是兼容的。

Image 提示当将–g标志与npm install一起使用时,一个好的经验法则是将其用于命令行工具(例如,node-devmochasupervisorgrunt),而不是用于项目的依赖项。Node.js 中的依赖项必须本地安装到项目的node_modules文件夹中。

在图 1-4 中,我们尝试了express命令没有成功,然后安装express-generator,进入express命令行工具。

9781484200384_Fig01-04.jpg

图 1-4 。用-g 和$ express -V 运行 NPM 的结果

Image 注意最有可能的情况是,您的系统需要 root/administrator 权限才能写入该文件夹。在这种情况下,您将需要$ sudo npm install -g express-generator

注意图 1-4 中的路径/usr/local/lib/node_modules/express-generator。这与本地安装的express路径截然不同。这是发现错误的方法之一,如果你试图安装某个东西,但它不可用——检查位置!

是的,对于一些精通 Node.js 的读者来说,这可能是微不足道的,但我见过太多次了,一些来自 Python、Ruby 和 Java 背景的人试图全局安装express(依赖项),以便他们可以在所有项目中使用它。请不要这样做。

摘要

本章为理解 Express.js 的工作原理奠定了基础。通过讲述一个典型的 Express.js 应用结构,您已经了解了配置、中间件和路由等概念,我们将在整本书中更深入地讨论这些概念。最重要的是,您学习了如何将 Express.js 本地安装到一个空白文件夹和现有项目中,以及如何安装 Express.js Generator。

在下一章中,我们将构建我们的第一个 Express.js 应用,典型的 Hello World,并探索生成器/脚手架选项。


1

2

3

4

5

6

7

8

二、Hello World 示例

在本章中,为了帮助您使用 Express.js,我们将构建一个典型的编程示例 Hello World 应用。如果你已经构建了这个 Express.js 应用的一些变体(也许通过遵循在线教程),请随意跳到下一章,或者去第三章获取 API 方法和对象,或者去第十九章 - 22 获取示例。本章涵盖的主题如下:

  • 入门:从头开始创建 minimal Express.js 应用
  • 生成器命令:Express.js 生成器的命令行选项
  • MVC 结构和模块:组织 Express.js 应用代码的常用方法
  • 监视文件更改:开发技巧

入门指南

我一直喜欢自下而上的教学方法,从最基本的概念开始,向更复杂的概念发展。此外,我注意到,当开发人员学会如何从头开始创建一些东西,而不仅仅是修改现有的项目或使用样板文件时,他们会获得更多的信心。

Image 注意我鼓励读者键入所有代码,因为这样可以提高学习效率。然而,作为参考,对于那些仍然喜欢复制和粘贴的人来说,本章(和其他章节)的代码在位于https://github.com/azat-co/proexpressjs的 GitHub 库中。

首先,您将编写一个在端口 3000 上本地运行的 web 服务器。所以,当你在浏览器中打开http://localhost:3000位置时,你应该会看到 Hello World。3000 是 Express.js 应用事实上的标准端口号。

Image 提示当你导航到某个页面时,浏览器会发出 GET 请求,所以至少你的服务器应该处理 GET 请求。GET 和其他类型的请求可以用 CURL (Mac 1 或 Windows 2 )或类似的工具来执行。

在文件夹proexpressjs/ch2中,创建一个hello.js文件。使用你喜欢的文本编辑器,比如 Vim ( www.vim.org)、Emacs ( www.gnu.org/software/emacs/)、Sublime Text 2 ( www.sublimetext.com)或者 TextMate ( http://macromates.com)。文件hello.js服务器将利用 Express.js 因此,让我们包括这个库:

var express = require('express');

现在我们可以创建一个应用(例如,实例化一个 Express.js 对象):

var app = express();

web 服务器将在本地端口 3000 上运行,所以让我们在这里定义它:

var port = 3000;

接下来,让我们用app.get()函数定义通配符路由 ( *):

app.get('*', function(request, response){
  response.end('Hello World');
});

app.get()函数接受字符串格式的 URL 模式的正则表达式 3 。在我们的例子中,我们通过指定通配符*来处理所有的 URL。

Image 注意正则表达式在许多编程语言中广泛使用,并且工作方式类似,所以如果您已经在 Perl、PHP、Java 等语言中使用过它们,那么您已经知道如何在 JavaScript/Node.js 中使用它们中的大多数。例如,这是电子邮件正则表达式之一:/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

对于 Express.js,您可以在 routes 中使用 RegExps 来动态定义复杂的 URL 模式。要了解更多关于正则表达式的信息,请查看位于https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions的文档。

使用请求处理程序

app.get()方法的第二个参数是一个请求处理程序 。典型的 Express.js 请求处理程序类似于我们作为回调传递给 native/core Node.js http.createServer()方法的处理程序。对于那些不熟悉核心http模块的人来说,请求处理器是一个每当服务器接收到一个特定请求时就会执行的功能,通常由 HTTP 方法(例如GET)和 URL 路径(即没有协议、主机和端口的 URL)定义。Express.js 请求处理程序至少需要两个参数— request,或简称为req,和response,或res(稍后在第九章中会有更多相关内容)。类似于它们的核心对应物,我们可以通过response.pipe()和/或response.on('data', function(chunk) {...})利用可读和可写的流接口(http://nodejs.org/api/stream.html)。

输出终端消息

最后,我们启动 Express.js web 服务器,并在回调中输出一条用户友好的终端消息:

app.listen(port, function(){
  console.log('The server is running, ' +
    ' please, open your browser at http://localhost:%s',
    port);
});

为了运行这个脚本,我们从项目文件夹中执行$ node hello.js。你会看到“服务器正在运行,请在http://localhost:3000”打开你的浏览器,如图图 2-1 。

9781484200384_Fig02-01.jpg

图 2-1 。运行$ node hello.js 的结果

现在,如果您在http://localhost:3000(与http://127.0.0.1:3000http://0.0.0.0:3000http://localhost:3000/)相同)打开浏览器,无论 URL 路径是什么,您都应该会看到 Hello World 消息(参见图 2-2 )。URL 路径是域和端口后的字符串,所以对于http://localhost:3000/,是/,对于http://localhost:3000/messages/,是/messages/。当您在路线定义中使用“*”时,此路径无关紧要。

9781484200384_Fig02-02.jpg

图 2-2 。浏览器在http://localhost:3000/ads打开

这里提供了hello.js文件的完整代码供您参考:

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

app.get('*', function(request, response){
  resquest.end('Hello World');
});

app.listen(port, function(){
  console.log('The server is running, ' +
    ' please open your browser at http://localhost:%s',
     port);
});

增强应用

我们可以通过回显提供给服务器的名称以及“Hello”短语,使我们的示例更具交互性。为此,我们可以使用$ cp hello.js hello-name.js复制hello.js文件,并在前面示例中的无所不包的路线(all.get('*', ...))之前添加以下路线:

app.get('/name/:user_name', function(req,res) {
  res.status(200);
  res.set('Content-type', 'text/html');
  res.send('<html><body>' +
    '<h1>Hello ' + req.params.user_name + '</h1>' +
    '</body></html>'
  );
});

/name/:name_route路由内部,我们设置适当的 HTTP 状态码 ( 200表示 OK)和 HTTP 响应头,并将动态文本包装在 HTML bodyh1标签中。

Image 注意 response.send() 是一个特殊的 Express.js 方法,它方便地超越了核心 http 模块response.end() 中我们的老朋友所做的事情。例如,前者会自动为我们添加一个Content-Length HTTP 头。它还根据提供给它的数据扩充了Content-Type—第八章中提供了更多详细信息。

下面提供了hello-name.js文件的完整源代码(也可以在本书的可下载源代码中找到):

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

app.get('/name/:user_name', function(request,response) {
  response.status(200);
  response.set('Content-Type', 'text/html');
  response.end('<html><body>' +
    '<h1>Hello ' + req.params.user_name + '</h1>' +
    '</body></html>'
  );
});

app.get('*', function(request, response){
  response.end('Hello World');
});

app.listen(port, function(){
  console.log('The server is running, ' +
    ' please open your browser at http://localhost:%s',
     port);
});

关闭之前的服务器并启动hello-name.js脚本后,您将能够看到动态响应;例如,在您的浏览器中输入http://localhost:3000/name/azat会产生如图图 2-3 所示的屏幕。

9781484200384_Fig02-03.jpg

图 2-3 。动态 Hello 用户示例

到目前为止,我们已经从头开始创建了两个 Express.js 应用。它们中的每一个都只有几行代码。这应该给你信心,并说明用 Express.js 和 Node.js 创建 web 服务器是多么容易!但是有一种更快的方法——express . js 生成器。让我们来看看它的命令和选项。

发电机命令

与 Ruby on Rails 和许多其他 web 框架相比,Express.js 附带了一个命令行界面(CLI ),用于启动您的开发过程。CLI 为最常见的情况生成了一个基本基础。与 Rails 或 Sails 不同,Express generator 不支持添加路线/模型(在撰写本文时)。

如果你遵循了第一章中的全球安装说明,你应该能看到版本号,如果你在你机器上的任何地方运行$ express -V表单的话。如果您键入$ express -h$ express --help,您将获得可用选项及其用法的列表。在本书中,我们使用的是最新(截至本文撰写时)版本 4.2.0,它与 Express.js 4.x 兼容(参见**图 2-4 )。

9781484200384_Fig02-04.jpg

图 2-4 。检查 Express.js 生成器版本

要生成 skeleton Express.js app,我们需要运行一个终端命令 : express [options] [dir|appname](如express cli-app),选项如下:

  • -e--ejs增加 EJS 发动机支持(www.embeddedjs.com)。默认使用 Jade ( http://jade-lang.com/tutorial/)。
  • -H--hogan增加 hogan.js 引擎支持。
  • -c <engine> or --css <engine>为 Less ( http://lesscss.org)、Stylus ( http://learnboost.github.io/stylus)或 Compass ( http://compass-style.org)添加样式表<engine>支持;默认情况下,使用普通 CSS。
  • -f--force强制在非空目录上生成应用。

当然,这些选项是可选的,所以你只需运行express cli-app就能得到一个默认设置的应用。

如果省略了dir / appname选项,Express.js 将使用当前文件夹作为项目的基础来创建文件。否则,应用将位于指定的目录下。

生成 Skeleton Express.js 应用

为了进行试验,我们运行这个命令:$ express -e -c less -f cli-app。生成器工具将输出创建的文件,并建议运行命令来启动服务器。图 2-5 显示了输出。

9781484200384_Fig02-05.jpg

图 2-5 。运行$ express -e -c less -f cli-app 的结果

如您所见,Express.js 提供了一个健壮的命令行工具来快速生成样板文件。缺点是 Express.js 生成器方法不太容易配置。例如,当你手动创建应用时,可以使用 Handlebars 模板引擎(以及许多其他工具,不仅仅是由 CLI 提供的 Hogan、Jade、JSHTML 或 EJS ),但是 Express.js Generator 没有这个选项(在撰写本文时)。你会在第五章中找到更多关于使用 Express.js 不同模板引擎的信息。

接下来,我们将检查生成器创建了什么——换句话说,脚手架使用了什么应用结构。

检查应用的结构

让我们简单地检查一下应用的结构。项目的根文件夹包括两个非常重要的文件,app.jspackage.js,如图 2-5 中所示:create: cli-app/package.jsoncreate: cli-app/app.jsapp.js文件是主文件(如前一章所述),它连接所有其他部分。package.json文件拥有所有需要的依赖项(至少是express)。然后,我们有三个文件夹:

  • public:静态资产,如图像、浏览器 JavaScript 和 CSS 文件
  • views:模板文件,如*.jade,或本例中的*.ejs
  • 请求处理程序被抽象成独立的文件/内部模块

public文件夹在express-generator生成项目时有三个自己的文件夹:

  • images:用于存储图像
  • javascripts:对于前端 JavaScript 文件
  • stylesheets:对于 CSS,或者在我们的例子中,对于更少的文件(-c less选项)

routes文件夹有两个文件:index.js,处理主页(root 或/),和users.js,处理/users路线。

public文件夹中的文件夹不是强制的,你可以创建任意的文件夹,这些文件夹将通过express.static()中间件暴露在你的服务器的/路径上。比如public/img的内容会在http://localhost:3000/img有。我个人更喜欢imgjscss而不是imagesjavascriptsstylesheets。当您重命名/public中的文件夹时,您不需要在 Express.js 配置中做任何额外的更改。

您也可以重命名viewspublic文件夹本身,但是您需要在配置语句中进行一些额外的更改,即更改设置。我们将在第三章的中介绍这些设置。

App.js

在您喜欢的文本编辑器中打开主 web 服务器文件app.js。我们将简要介绍一下自动生成的代码以及它的功能,然后再深入探讨每一种配置(第三章)。

我们包括以下模块依赖关系:

var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');

接下来,我们创建 Express.js 应用对象:

var app = express();

然后,我们定义配置设置。现在,你大概可以根据它们的名字猜出它们的意思。即从哪里获取模板文件(views)以及使用什么模板引擎(view engine)。关于这些参数的更多细节在第三章中提供,所以现在让我们跟随app.js文件:

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

我们接下来定义中间件(在第四章中详细讨论)来服务 favicon、记录事件、解析请求体、支持旧浏览器的 HTTP 方法、解析 cookies 和利用路由:

app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());

以下中间件负责将较少的样式编译成 CSS 样式,并提供给浏览器:

app.use(require('less-middleware')(path.join(__dirname, 'public')));

我们将文件夹(public)作为参数传递,让它递归地扫描这个文件夹中的任何*.less文件。CSS 文件名和 Less 文件名需要匹配,所以如果我们用/css/style.css(e.g., in HTML or Jade),就需要有/public/css/style.less。对于每个请求,Express.js 将编译得更少。

该语句负责从public文件夹中提供静态资产:

app.use(express.static(path.join(__dirname, 'public')));

路由在一个单独的文件中被定义为一个模块,所以我们只是传递函数的表达式,而不是在这里将它们定义为匿名请求处理程序:

app.get('/', routes.index);
app.get('/users', user.list);

Express.js 从process.env.NODE_ENV获取它的环境变量,例如,当服务器启动时或者在机器的配置中,它被作为NODE_ENV=production传递。有了这个条件,我们为开发环境启用了一个更明确的错误处理程序。生成器为我们提供了一个404(未找到)中间件,和两个 500(内部服务器错误)错误处理程序,一个用于开发(更详细),一个用于生产:

app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});
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
        });
    });
}
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

与 Hello World 示例不同,此服务器不会立即启动,而是导出:

module.exports = app;

/bin/www中,服务器从app.js文件导入:

#!/usr/bin/env node
var debug = require('debug')('cli-app');
var app = require('../app');

该语句设置了一个名为port的自定义设置,以便稍后在服务器启动时使用:

app.set('port', process.env.PORT || 3000);

最后,用熟悉的listen()启动服务器:

var server = app.listen(app.get('port'), function() {
  debug('Express server listening on port ' + server.address().port);
});

以下是app.js的完整代码,供您参考:

var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(require('less-middleware')(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
app.use('/users', users);

/// 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;

如果您导航($ cd cli-app)到项目文件夹并运行$ npm install,您应该观察到基于package.json条目的 NPM 模块的安装。安装完成后,运行npm start$ ./bin/www。当您在浏览器中导航到http://localhost:3000时,您应该会看到如图图 2-6 所示的响应。

9781484200384_Fig02-06.jpg

图 2-6 。由生成器创建的样板 Express.js 应用

Express.js 生成器并不是一个非常强大的工具,因为它在样板文件创建之后并没有做太多事情。然而,它为开发人员提供了可用于快速原型制作的样板/框架。此外,初学者还可以了解推荐的结构和代码组织,以及学习如何包含 Less 和express.static()等一些中间件。

下一节总结了关于 Express.js 应用的文件夹结构的一些常见模式和约定。

MVC 结构和模块

Express.js 是一个高度可配置的框架,这意味着我们可以应用任何我们认为合适的结构。正如我们在上一节中观察到的,生成器工具立即为我们生成了几个文件夹:publicviewsroutes。为了坚持通用的 MVC 范式,所缺少的是模型。如果您使用类似 mongose, 4 的东西,您可能想要创建一个名为models的文件夹,并将模型和/或模式对象(mongose 模式)放在那里。更高级的应用可能有一个嵌套的文件夹结构,如下所示:

pg28.jpg

最佳实践是将静态资产放在一个特殊的文件夹下。这些资产也可以用 CoffeeScript 等可编译语言编写。

如果您更喜欢重命名文件夹,只需确保更新您的app.js文件(或者其他主脚本文件,如果您正在用不同的文件名从头开始创建应用)中的相应代码。例如,如果我想从文件夹controllers中提供与我的用户相关的路线,我会像这样更新我的app.js文件:

var user = require('./controllers/user');

模块的模式不必很复杂。在主文件中,我们包含了带有require()函数的对象,在该模块文件中,我们应用了一个exports全局关键字来附加一个我们想要公开的方法(稍后在主文件中使用):

exports.list = function(req, res){
  res.send("respond with a resource");
};

这里有一个警告——或者说一个特性,取决于你如何看待它——如果我们省略文件名并需要一个文件夹,比如我们前面例子中的var routes = require('./routes');, node . js 将从那个文件夹中获取index.js文件,如果存在的话。当声明一些您可能希望在该特定文件夹的文件间共享的助手或实用函数时,这可能会很方便。

在第十三章中,我们将讨论如何使我们的应用本身成为一个模块,这样我们就可以在一个生产环境中产生多个进程(例如,workers)。

类似的方法也适用于模板文件夹。如果我们决定用templates代替views,我们需要将设置行改为

app.set('views', path.join(__dirname, 'views'));

在这一行中,app.set()函数的第一个参数是设置的名称views,第二个参数是以全局__dirname变量为前缀的动态值。?? 5T7__dirname变量返回正在执行的模块的系统路径。

监视文件更改

这个主题超出了 Express.js 的范围,但是我相信它非常重要,值得一提。Node.js 应用存储在内存中,如果我们对源代码进行更改,我们需要重新启动该进程—正在运行的 Node.js 程序。在 Mac OS X 上,这是通过在终端中按下以下组合键来实现的:Ctrl+C、向上箭头、Enter。

以下出色的文件查看工具可以利用来自核心 Node.js fs模块的watch()方法 6 ,并在我们保存来自编辑器的更改时重启我们的服务器。出于本书及其示例的目的,这些工具几乎是相同的,因此没有给出任何偏好;随便挑一个。

  • forever:在生产服务器上使用(https://npmjs.org/package/forever)
  • node-dev : 描述(https://npmjs.org/package/node-dev;GitHub: https://github.com/fgnass/node-dev
  • nodemon:支持 coffee script(https://npmjs.org/package/nodemon);GitHub: https://github.com/remy/nodemon
  • supervisor:由《NPM》的创作者之一https://npmjs.org/package/supervisor撰写;GitHub: https://github.com/isaacs/node-supervisor
  • up:由编写 Express.js 的团队编写(https://npmjs.org/package/up;GitHub: https://github.com/LearnBoost/up

Image 提示因为默认情况下,Express.js 会为每个新请求重新加载一个模板文件,所以不需要重启服务器。但是,我们可以通过启用view cache设置来缓存模板。这个和其他设置将在本书第三章的中详细介绍。

摘要

到目前为止,我们已经从头开始创建了一些应用,使用了 Express.js Generator 并探索了它的选项。然后我们看了一种构建 Express.js 应用和组织代码的方法。Express.js 生成器不是一个非常通用的工具,但是通过使用它,您可以使用不同的 CSS 库和模板引擎快速生成一些样板代码。

该框架的可配置性是 Express.js 的主要卖点之一,也是其越来越受欢迎的一个因素。这意味着 Express.js 的方式是提供默认设置,所以最起码你的服务器在没有额外配置的情况下也能正常工作(ch2/hello.js只有 13 行)。但与此同时,Express.js 允许熟练的开发人员以合理的方式轻松配置设置。谁会反对知道这些设定呢,对吧?因此,在第三章中,我们将探索 Express.js 中最重要的配置设置


1

2 http://www.confusedbycode.com/curl/#downloadshttp://curl.haxx.se/download.html

3

4

5

6

三、配置、设置和环境

本章介绍了配置 Express.js 设置的不同方法。正如在第一章中提到的,Express.js 将自己定位为一个超越常规框架的配置。因此,当谈到 Express.js 中的配置时,您几乎可以配置任何东西!为此,您需要使用配置语句并了解设置。

有哪些设定?可以将设置视为通常以全局或应用范围的方式起作用的键值对。设置可以增强服务器的行为,将信息添加到响应中,或者在以后用作参考。

有两种类型的设置:框架在后台使用的 Express.js 系统设置,以及开发人员为自己的代码使用的任意设置。前者有默认值,所以如果你不配置它们,应用仍然可以正常运行!所以,如果你不知道或者不使用 Express.js 的一些设置,也没什么大不了的。出于这个原因,不要觉得你必须记住所有的设置才能构建快速应用。如果您对特定方法或系统设置有任何疑问,请随时参考本章内容。

为了从简单到复杂,本章组织如下:

  • 配置 :设定设定值和获取设定值的方法
  • 设置 :设置的名称、默认值、影响以及如何增加值的示例
  • 环境 :确定一个环境并将应用置于该模式是任何严肃应用的一个重要方面。

本章的例子可以在ch3/app.js项目中找到,该项目位于http://github.com/azat-co/proexpressjs的 GitHub 存储库中。

配置

在使用设置之前,您需要了解如何在 Express.js 应用上应用它们。最常见和最通用的方法是使用app.set定义一个值,并使用app.get根据设置的键/名称检索该值。

其他配置方法不太通用,因为它们仅适用于基于其类型(布尔值)的某些设置:app.enable()app.disable()

app.set()和 app.get()

方法app.set(name, value)接受两个参数:namevalue。正如您可能猜到的,它设置了名称的值。例如,我们经常想要存储我们计划启动服务器的端口的值:

app.set('port', 3000);

或者,对于一个更高级和更现实的用例,我们可以从系统环境变量PORT (process.env.PORT)中获取端口。如果PORT环境变量未定义,我们就回到硬编码的值3000:

app.set('port', process.env.PORT || 3000);

前面的代码更短,相当于使用了一个if else语句:

if (process.env.PORT) {
  app.set(process.env.PORT);
} else {
  app.set(3000);
}

name值可以是 Express.js 设置或任意字符串。要获得该值,我们可以使用带有单个参数的app.set(name),或者我们可以使用更显式的方法app.get(name),如下例所示:

console.log('Express server listening on port ' + app.get('port'));

app.set()方法还将变量暴露给应用范围内的模板;例如,

app.set('appName', 'HackHall');

将在所有模板中可用,这意味着该示例在 Jade 模板布局中有效:

doctype 5
html
  head
    title= appName
  body
    block content

app.enable()和 app.disable()

有些 system Express.js 设置的类型为布尔值 true 和 false,而不是字符串类型,它们只能设置为布尔值 false 或 true。对于这样的标志,有简写版本;例如,作为app.set(name, true)app.set(name, false)函数的替代,您可以相应地使用简洁的app.enable(name)app.disable(name)调用。我推荐使用app.set(),因为不管设置的类型是什么,它都能保持代码的一致性。

例如,etag Express.js 设置是一个布尔值。它为浏览器缓存打开和关闭 ETag 头(稍后将详细介绍etag)。要用app.disable()关闭缓存,写一个语句:

app.disable('etag');

app.enabled()和 app.disabled()

为了检查上述值是真还是假,我们可以调用方法app.enabled(name)app.disabled(name)。例如,

app.disable('etag');
console.log(app.disabled('etag'));

会在 Express.js app 的上下文中输出true

设置

有两类设置:

  • Express.js 系统设置 :这些设置被框架用来确定某些配置。它们中的大多数都有默认值,所以省略了配置这些设置的基本应用将会工作得很好。
  • 自定义设置 :您可以存储任意名称作为设置,以备后用。这些设置是为您的应用定制的,您首先需要定义它们来使用。

系统设置的范围是 Express.js 文档中最难理解的部分之一,因为有些设置根本没有文档记录(在撰写本文时)。Express.js 足够灵活,你不必为了写应用而知道所有的设置。但是当你了解了所有的设置并开始使用你需要的设置后,你会对配置你的服务器更有信心。您将更好地理解框架的内部工作方式。

在本节中,您将了解以下设置:

  • env
  • view cache
  • view engine
  • views
  • trust proxy
  • jsonp callback name
  • json replacerjson spaces
  • case sensitive routing
  • strict routing
  • x-powered-by
  • etag
  • query parser
  • subdomain offset

为了说明实际的设置,我们写了一个ch3/app.js例子。为了避免混淆,我们现在不展示整个文件,而是在本节末尾提供源代码以供参考。

包封/包围(动词 envelop 的简写)

这个变量用于存储这个特定 Node.js 进程的当前环境模式。该值由 Express.js 从process.env.NODE_ENV开始自动设置(通过执行机器上的环境变量提供给 Node.js ),如果没有设置,则设置为development值。

env设置的其他最常见值如下:

  • development
  • test
  • stage
  • preview
  • production

Express.js 使用“production”和“development”值作为某些设置的默认值(view cache就是其中之一)。其他值只是约定俗成的,意味着只要保持一致,你可以随意使用。例如,你可以用qa代替stage

我们可以通过在代码中添加app.set('env', 'preview');process.env.NODE_ENV=preview来增加env的设置。不过,更好的方法是用$ NODE_ENV=preview node app启动一个 app,或者在机器上设置NODE_ENV变量。

了解应用运行的模式非常重要,因为与错误处理、样式表编译和模板呈现相关的逻辑可能会有很大的不同。显然,数据库和主机名因环境而异。

app.get('env')设置在ch3/app.js示例中显示为

console.log(app.get('env'));

这一行输出

"development"

当我们用$ NODE_ENV=development node app.js启动过程时,如果NODE_ENV被设置为development,或者当NODE_ENV未被设置时,前面的行被打印。在后一种情况下,使用“development”值的原因是 Express.js 在未定义时默认设置为“development”。

查看缓存

这个标志,如果设置为false,允许无痛开发,因为每次服务器请求模板时都会读取它们。另一方面,如果view cache被设置为true,它有助于模板编译缓存,这是生产中期望的行为。如果env设置为production,则view cache 默认开启。否则设置为false

查看引擎

view engine 设置保存模板文件扩展名(例如,'ext''jade'),以便在文件扩展名未被传递给请求处理器内部的res.render()函数时使用。

例如,如图 3-1 中的所示,如果我们从上一章的ch2/cli-app/app.js示例中注释掉这一行:

// app.set('view engine', 'ejs');

9781484200384_Fig03-01.jpg

图 3-1 。没有合适的模板扩展集的结果

服务器将无法定位该文件,因为我们在cli-app/routes/index.js中的指令太不明确:

exports.index = function(req, res){
  res.render('index', { title: 'Express' });
};

我们可以通过给cli-app/routes/index.js文件添加扩展名来解决这个问题:

exports.index = function(req, res){
  res.render('index.ejs', { title: 'Express' });
};

有关如何应用不同模板引擎的更多信息,请参考第五章。

视图

views 设置有一个指向模板目录的绝对路径(在 Mac 和 Unix 上以/开头)。该设置默认为项目根目录下views文件夹的绝对路径(主应用文件,如app.js所在的位置)。

正如在第二章的的“MVC 结构和模块”一节中提到的,改变模板文件夹名是很简单的。通常,当我们在app.js中为views设置自定义值时,我们使用path.join()__dirname全局变量——这给了我们app.js所在文件夹的绝对路径。例如,如果你想使用文件夹templates使用这个配置语句:

app.set('views', path.join(__dirname, 'templates'));

信任代理

如果您的 Node.js 应用在 Varnish 或 Nginx 等反向代理后面工作,请将trust proxy 设置为true。这将允许信任X-Forwarded-*报头,例如X-Forwarded-Proto ( req.protocol)或X-Forwarder-For ( req.ips)。默认情况下,trust proxy设置被禁用。

如果您想打开它(当您有代理服务器时),您可以使用以下语句之一:

app.set('trust proxy', true);
app.enable('trust proxy');

jsonp 回调名称

如果您正在构建一个应用(REST API 服务器),为来自托管在不同域上的前端客户端的请求提供服务,那么在进行 XHR/AJAX 调用时,您可能会遇到跨域限制。换句话说,浏览器请求仅限于同一个域(和端口)。解决方法是在服务器上使用跨源资源共享(CORS) 头。

如果您不想将 CORS 头文件应用到您的服务器上,那么带前缀的 JavaScript 对象文字符号(JSONP)是一个不错的选择。Express.js 有一个res.jsonp()方法,使得使用 JSONP 变得轻而易举。

Image 提示要了解更多关于 CORS 的信息,请前往http://en.wikipedia.org/wiki/Cross-origin_resource_sharing

默认的回调名称是 JSONP 响应的前缀,通常在请求的查询字符串中提供,名称为callback;比如?callback=updateView。但是,如果你想使用不同的东西,只需将设置jsonp callback name设置为该值即可;例如,对于带有查询字符串 param ?cb=updateView的请求,我们可以使用这个设置:

app.set('jsonp callback name', 'cb');

这样,我们的响应将被包装在updateView JavaScript 代码中(当然,带有适当的Content-Type头),如图图 3-2 所示。

9781484200384_Fig03-02.jpg

图 3-2 。使用 cb 作为回调的查询字符串名称

在大多数情况下,我们不想改变这个值,因为默认的callback值在某种程度上被 jQuery $标准化了。ajax JSONP 函数。

如果我们在 Express.js 设置配置中将jsonp callback name设置为cb,但是用不同的属性进行请求,比如callback,那么路由不会输出 JSONP。它会默认为 JSON 格式,如图图 3-3 所示,没有函数调用的前缀,我们在图 3-2 中看到。

9781484200384_Fig03-03.jpg

图 3-3 。如果没有合适的回调参数,JSONP 默认为 JSON

json 替换器和 json 空间

同样,当我们使用 Express.js 方法res.json()时,我们可以应用特殊的参数:replacerspaces。这些参数被传递给应用范围内的所有JSON.stringify()功能 1JSON.stringify()是一个广泛使用的函数,用于将原生 JavaScript/Node.js 对象转换成字符串。

replacer参数就像一个过滤器。这个函数有两个参数:键和值。如果返回undefined,那么该值被省略。为了让键值对成为最终的字符串,我们需要返回值。你可以在 Mozilla 开发者网络(MDN)上阅读更多关于replacer的内容。22

Express.js 使用null作为json replacer的默认值。当我需要打印漂亮的 JSON 时,我经常使用JSON.stringify(obj, null, 2)

spaces参数本质上是一个缩进尺寸。它的值在开发中默认为2,在生产中默认为0。在大多数情况下,我们不去管这些设置。

在我们的示例应用ch3/app.js中,我们有一个/json路由,它向我们发回一个包含一本书信息的对象。我们将一个replacer参数定义为一个从对象中省略折扣代码的函数(我们不想公开这个信息)。并且spaces参数被设置为4,这样我们可以看到 JSON 被很好地格式化,而不是一些混乱的代码。/json路线的最终响应如图 3-4 中的所示。

9781484200384_Fig03-04.jpg

图 3-4 。设置了替换符和空格的 JSON 输出

以下是示例应用中使用的语句:

app.set('json replacer', function(key, value){
  if (key === 'discount')
    return undefined;
  else
    return value;
});
app.set('json spaces', 4);

如果我们移除json spaces,应用将产生图 3-5 中所示的结果。

9781484200384_Fig03-05.jpg

图 3-5 。未设置空格的 JSON 输出

区分大小写的路由

case sensitive routing 标志应该是不言自明的。当它是默认值false时,我们不考虑 URL 路径的大小写,当该值设置为 true 时,我们不考虑大小写。比如我们有app.enable('case sensitive routing');,那么/users/Users就不一样了。为了避免混淆,最好禁用此选项。

严格路由

下一个设置(或者一个标志,因为它有布尔意义)严格路由处理 URL 中尾部斜杠的情况。在strict routing 使能的情况下,比如app.set('strict routing', true');,路径会被区别对待;例如,/users/users/将是完全独立的航线。在示例ch3/app.js中,我们有两条相同的路由,但其中一条有一个尾随斜杠。它们发回不同的字符串:

app.get('/users', function(request, response){
  response.send('users');
})
app.get('/users/', function(request, response){
  response.send('users/');
})

因此,浏览器对于/users/users/会有不同的消息,如图图 3-6 所示。

9781484200384_Fig03-06.jpg

图 3-6 。启用严格路由时,/users 和 users/是不同的路由

默认情况下,该参数设置为false,这意味着尾部斜杠被忽略,带有尾部斜杠的路由将被视为与不带尾部斜杠的路由相同。我的建议是保留默认值;也就是说,将带斜线的路由视为与不带斜线的路由相同。如果您的 API 架构要求区别对待它们,那么这个建议就不适用。

x 供电的

x-powered- by 选项将 HTTP 响应报头X-Powered-By设置为Express值。该选项默认启用,如图 3-7 中的所示。

9781484200384_Fig03-07.jpg

图 3-7 。X-Powered-By Express 已启用(默认)

如果你想禁用x-powered-by(从响应中删除它)——这是出于安全原因推荐的,因为如果你的平台未知,就更难找到漏洞——那么应用app.set('x-powered-by', false)app.disable('x-powered-by'),这将删除 X-Powered-By 响应头(如示例ch3/app.js和图 3-8 所示的)。

9781484200384_Fig03-08.jpg

图 3-8 。X-Powered-By Express 被禁用,没有响应头

电子标签

ETag 3 (或实体标签)是一个缓存工具。它的工作方式类似于给定 URL 上内容的唯一标识符。换句话说,如果特定 URL 上的内容没有变化,ETag 将保持不变,浏览器将使用缓存。图 3-7 和图 3-8 包括一个 ETag 响应头的例子。这个例子的代码可以在ch3/app.js中找到。

如果有人不知道 etag 是什么,也不知道如何使用它,那么最好让 Express.js 默认的 ETag 设置保持原样,即 on (boolean true)。否则,要禁用 ETag,请使用app.disable('etag');,这将消除 ETag HTTP 响应头。

默认情况下,Express.js 使用“弱”ETag。其他可能的值有false(无 ETag)true(弱 ETag)strong(强 ETag)。Express.js 提供的最后一个选项(对于高级开发人员)是使用您自己的 ETag 算法:

app.set('etag', function (body, encoding) {
  return customEtag(body, encoding); // you define the customEtag function
})

如果您不熟悉弱或强的含义,下面是这些类型的 ETag 之间的差异的简短解释:相同的强 ETag 保证响应是完全相同的,而相同的弱 ETag 表示响应在语义上是相同的。因此,对于弱 ETags 和强 ETags,您将获得不同级别的缓存。当然,这是一个非常简短和模糊的解释。如果这个主题对你的项目很重要,请自己做研究。

查询分析器

一个查询字符串是在 URL 中问号后面发送的数据(例如,?name=value&name2=value2)。这种格式需要解析成 JavaScript/Node.js 对象格式才能使用。为了方便起见,Express.js 自动包含了这个查询解析。这是通过启用query parser设置来实现的。

query parser 的默认值是extended,它使用了qs模块的功能。 4 其他可能值有

  • false:禁用解析
  • true:使用qs
  • simple:使用核心querystring模块的功能(http://nodejs.org/api/querystring.html)

可以将您自己的函数作为参数传递,在这种情况下,您的自定义函数将用于解析,而不是解析库。如果您传递自己的函数,那么您的自定义解析函数必须接受一个字符串参数,并返回一个 JavaScript/Node.js 对象,该对象类似于来自核心querystring模块的parse函数的签名。 5

以下是我们将query parser设置为使用querystring、无解析和自定义解析函数的示例:

app.set('query parser', 'simple');
app.set('query parser', false);
app.set('query parser', customQueryParsingFunction);

子域偏移

subdomain offset 设置控制req.subdomains属性返回的值。当应用部署在多个子域上时,如http://ncbi.nlm.nih.gov,此设置非常有用。

默认情况下,主机名/URL 中的最后两个“子域”(最右边的两个部分)被删除,其余的在req.subdomains中以相反的顺序返回;所以对于我们的http://ncbi.nlm.nih.gov的例子,得到的req.subdomains['nlm', 'ncbi']

但是,如果 app 已经通过app.set('subdomain offset', 3);subdomain offset设置为3req.subdomains的结果将只是['ncbi'],因为 Express.js 会从右边开始丢弃三(3)个部分(nlmnihgov)。

环境

正如你们许多人所知,大多数应用不能在单一环境中运行。这些环境通常至少包括开发、测试和生产。每种环境都对应用提出了不同的要求。例如,在开发中,应用的错误消息需要尽可能详细,而在生产中,它需要对用户友好,并且不会将任何系统或用户的个人身份信息(PII 6 )数据泄露给黑客。

代码需要适应不同的环境,而不需要我们这些开发人员在每次部署到不同的环境时都必须修改它。

当然,我们可以根据process.env.NODE_ENV值写一些if else语句;例如:

if ('development' === process.env.NODE_ENV) {

如果上面的那行对你来说很奇怪,请记住它与process.env.NODE_ENV === 'development'完全相同。或者,您可以使用process.env.NODE_ENV == 'development',它会在比较之前将NODE_ENV转换为字符串(如果由于某种原因它还不是字符串)。

  *// Connect to development database*
} else if ('production' === process.env.NODE_ENV) {
  *// Connect to production database*
 }; *// Continue for staging and preview environments*

或者使用 Express.js env param(参考本章前面的“env”部分):

*// Assuming that app is a reference to Express.js instance*
if ('development' === app.get('env')) {
  *// Connect to development database*
} else if ('production' === app.get('env')) {
  *// Connect to production database*
 }; *// Continue for staging and preview environments*

app.get('env')的另一个例子是 skeleton Express.js Generator 应用中的一个。与生产或任何其他环境相比,它为开发环境应用了更详细的错误处理程序(从err对象发送整个 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
        });
    });
}

如果环境不是development,Express.js 将使用这个没有向用户泄露堆栈跟踪的错误处理程序,而不是上面的错误处理程序:

app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

APP。配置

允许更优雅的环境配置的app.configure()方法在 Express.js 4.x 中被弃用,但是,您应该仍然知道它是如何工作的,因为您可能会在旧项目中遇到它。

当用一个参数调用 app.configure()方法时,它会将回调应用到所有的环境。例如,如果您想要为任何环境设置一个作者电子邮件和应用名称,那么您可以编写:

app.configure(function(){
app.set('appName', 'Pro Express.js Demo App');
app.set('authorEmail', 'hi@azat.co');

但是,如果我们传递两个(或更多)参数,第一个是环境,最后一个仍然是函数,那么只有当应用处于这些环境模式(例如,开发、生产)时,才会调用代码。

例如,您可以为开发设置不同的dbUri值(数据库连接字符串),并使用这些回调进行准备:

app.configure('development', function() {
app.set('dbUri', 'mongodb://localhost:27017/db');
});
app.configure('stage', 'production', function() {
app.set('dbUri', process.env.MONGOHQ_URL);

Image 提示 Express.js 经常使用输入参数数量和类型的差异来指导函数的行为。因此,请密切注意如何调用方法。

现在您已经熟悉了设置,下面是演示厨房水槽应用。在其中,我们收集了所有上述设置来说明示例。在检查代码时,请注意文件中配置语句的顺序!它们必须在var app实例化之后,但在中间件和路由之前。下面是示例服务器ch3/app.js的完整源代码:

var book = {name: 'Practical Node.js',
  publisher: 'Apress',
  keywords: 'node.js express.js mongodb websocket oauth',
  discount: 'PNJS15'
}
var express = require('express'),
  path = require('path');

var app = express();

console.log(app.get('env'));

app.set('view cache', true);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set('port', process.env.PORT || 3000);

app.set('trust proxy', true);
app.set('jsonp callback name', 'cb');
app.set('json replacer', function(key, value){
  if (key === 'discount')
    return undefined;
  else
    return value;
});
app.set('json spaces', 4);

app.set('case sensitive routing', true);
app.set('strict routing', true);
app.set('x-powered-by', false);
app.set('subdomain offset', 3);
// app.disable('etag')

app.get('/jsonp', function(request, response){
  response.jsonp(book);
})
app.get('/json', function(request, response){
  response.send(book);
})
app.get('/users', function(request, response){
  response.send('users');
})
app.get('/users/', function(request, response){
  response.send('users/');
})
app.get('*', function(request, response){
  response.send('Pro Express.js Configurations');
})

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
        });
    });
}
var server = app.listen(app.get('port'), function() {
  console.log('Express server listening on port ' + server.address().port);
});

摘要

在本章中,我们介绍了如何使用 app.set()、app.disable()和 app.enable()等方法配置 Express.js 系统设置。您学习了如何使用 app.get()和 app.enabled()和 app.disabled()获取设置值。然后,我们讨论了所有重要的 Express.js 设置,它们的意义和价值。您还看到,设置可以是任意的,并用于存储特定于应用的自定义信息(例如,端口号或应用名称)。

如果你还记得第一章中的应用结构,中间件在主 Express.js 应用文件的配置部分之后。第三方中间件和定制中间件都可以与 Express.js 一起使用。当你编写自己的中间件时,这是一种重用和组织代码的方法。

NPM 上有大量的第三方 Express.js 中间件模块。它们可以完成从解析到认证的许多任务。通过使用第三方中间件,您可以增强和定制应用的行为。所以中间件可以被认为是它自己的一种配置(类固醇上的配置!).继续阅读,掌握最常用的中间件!


1

2

3

4

5

6

四、使用中间件

中间件是一种非常有用的模式,它允许开发人员在他们的应用中重用代码,甚至以 NPM 模块的形式与他人共享。中间件的本质定义是一个有三个自变量的函数:request(或req)、response ( res)、next。如果您正在编写自己的中间件,您可以使用任意的名称作为参数,但是最好坚持通用的命名约定。下面是一个如何定义自己的中间件的例子:

var myMiddleware = function (req, res, next) {
  // Do something with req and/or res
  next();
};

自己写中间件的时候,别忘了调用next()回调函数。否则,请求将挂起并超时。对于后续的中间件,请求(req)和响应(res)对象是相同的,因此您可以向它们添加属性(例如req.user = 'Azat')以便以后访问它们。

在本章中,我们将讨论以下主题:

  • 应用中间件:如何在 Express.js 应用中使用中间件
  • 必备中间件:最常用的中间件,Connect.js 中间件,4.x 版本之前是 Express.js 一部分的中间件
  • 其他中间件:最有用最流行的第三方中间件

与描述如何构建一个大型项目的传统技术书籍章节不同,本章广泛描述了最流行和最常用的中间件模块。类似于第三章,这一章有点类似于参考。为了向您演示中间件的特性,在ch4文件夹中有一个厨房水槽,这意味着它有许多不同的东西。像往常一样,代码将在书中列出,并在https://github.com/azat-co/proexpressjs的 GitHub repo 中提供。

应用中间件

为了设置中间件,我们使用来自 Express.js API 的app.use()方法。这适用于第三方中间件和内部中间件。

方法app.use()有一个可选的字符串参数路径和一个强制的函数参数回调。例如,为了实现一个带有日期、时间、请求方法和 URL 的日志记录器,我们使用了console.log()方法:

*// Instantiate the Express.js app*
app.use(function(req, res, next) {
  console.log('%s %s — %s', (new Date).toString(), req.method, req.url);
  return next();
});
*// Implement server routes*

另一方面,如果我们想要给中间件加上前缀,也称为 mounting ,我们可以使用path参数,该参数将这个特定中间件的使用限制为只有具有这样前缀的路由。例如,为了将日志记录限制为仅管理仪表板路由/admin,我们可以编写

*// Instantiate the Express.js app*
app.use('/admin', function(req, res, next) {
  console.log('%s %s — %s', (new Date).toString(), req.method, req.url);
  return next();
});
*// Actually implement the /admin route*

从头开始编写所有的东西,甚至像静态文件的日志记录和服务这样琐碎的事情,显然不是很有趣。因此,我们可以利用express.static()morgan中间件功能,而不是实现我们自己的模块。这里有一个使用express.static()morgan中间件的例子:

var express = require('express');
var logger = require('morgan');
*// Instantiate and configure the app*
app.use(logger('combined'));
app.use(express.static(__dirname + '/public'));
*// Implement server routes*

Image 注意在 Express.js 3 . x 版及更早版本(即 4.x 版之前)中,logger 是 express . js 的一部分,可以用express.logger()调用。

Static 是唯一一个仍然捆绑在 Express.js version 4.x 中的中间件,它的 NPM 模块是serve-static。静态中间件支持对静态资产的直通请求。这些资产通常存储在public文件夹中(有关推荐文件夹结构的更多信息,请参考第二章)。

下面是一个更高级的静态中间件示例,它将资产限制在各自的文件夹中。这称为挂载,通过向app.use()提供两个参数来实现:路由路径和中间件功能;

app.use('/css', express.static(__dirname + '/public/css'));
app.use('/img', express.static(__dirname + '/public/images'));
app.use('/js', express.static(__dirname + '/public/javascripts'));

全局路径避免了歧义,这就是我们使用__dirname的原因。

当您编写自己的中间件时,静态中间件在幕后使用的模式是另一个很好的技巧。它是这样工作的:如果你仔细观察,express.static()接受一个文件夹名作为参数。这使得中间件能够动态地改变其行为或模式。这种模式被称为单子,尽管熟悉函数式编程的人可能会认为单子是不同的东西。总之,这里的主要思想是,我们有一个存储数据并返回另一个函数的函数。

这种模式在 JavaScript/Node.js 和类似于serve-static的模块中的实现方式是使用return关键字。这里有一个例子,一个定制的myMiddleware函数接受一个参数,根据参数 deep 是否等于(===)到 A,返回不同的中间件 A 或者默认的中间件:

var myMiddleware = function (param) {
  if (param === 'A') {
    return function(req, res, next) { // <---Middleware A
      // Do A stuff
      return next();
    }
  } else {
    return function(req, res, next) { // The default middleware
      // Do default stuff
      return next();
  }
}

接下来显示的ch4/app.js示例演示了如何应用(app.use())中间件staticmorgan和其他。示例中使用的每个中间件的参数和路由都包含在各自的章节中。

ch4/app.js的完整源代码,演示如何应用中间件(并为您提供其他中间件模块的工作内容):

// Import and instantiate dependencies
var express = require('express'),
  path = require('path'),
  fs = require('fs'),
  compression = require('compression'),
  logger = require('morgan'),
  timeout = require('connect-timeout'),
  methodOverride = require('method-override'),
  responseTime = require('response-time'),
  favicon = require('serve-favicon'),
  serveIndex = require('serve-index'),
  vhost = require('vhost'),
  busboy = require('connect-busboy'),
  errorhandler = require('errorhandler');

var app = express();
// Configure settings
app.set('view cache', true);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set('port', process.env.PORT || 3000);
app.use(compression({threshold: 1}));
app.use(logger('combined'));
app.use(methodOverride('_method'));
app.use(responseTime(4));
app.use(favicon(path.join('public', 'favicon.ico')));
// Apply middleware
app.use('/shared', serveIndex(
  path.join('public','shared'),
  {'icons': true}
));
app.use(express.static('public'));
// Define routes
app.use('/upload', busboy({immediate: true}));
app.use('/upload', function(request, response) {
  request.busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
    file.on('data', function(data){
      fs.writeFile('upload' + fieldname + filename, data);
    });
    file.on('end', function(){
      console.log('File' + filename + 'is ended');
    });

  });
 request.busboy.on('finish', function(){
    console.log('Busboy is finished');
   response.status(201).end();
 })
});

app.get(
  '/slow-request',
  timeout('1s'),
  function(request, response, next) {
    setTimeout(function(){
      if (request.timedout) return false;
      return next();
    }, 999 + Math.round(Math.random()));
  }, function(request, response, next) {
    response.send('ok');
  }
);

app.delete('/purchase-orders', function(request, response){
  console.log('The DELETE route has been triggered');
  response.status(204).end();
});

app.get('/response-time', function(request, response){
  setTimeout(function(){
    response.status(200).end();
  }, 513);
});

app.get('/', function(request, response){
  response.send('Pro Express.js Middleware');
});
app.get('/compression', function(request, response){
  response.render('index');
})
// Apply error handlers
app.use(errorhandler());
// Boot the server
var server = app.listen(app.get('port'), function() {
  console.log('Express server listening on port' + server.address().port);
});

既然您已经知道如何应用第三方和内部中间件,下一步就是确定哪一个第三方中间件是必不可少的。以及开发人员可以获得什么,并允许他们将自己和队友从实现、维护和测试 NPM 模块提供的功能的“乐趣”中解救出来。

基本中间件

正如您在上一节中看到的,中间件只不过是一个接受reqres对象的函数。express . js 4 . x 版只提供了一个现成的中间件功能:express.static()。大多数中间件需要安装和导入。本质中间件通常源于 Sencha 的 Connect 库:http://www.senchalabs.org/connect/(NPM:https://npmjs.org/package/connect;GitHub: https://github.com/senchalabs/connect)。

使用中间件时要记住的主要事情是,中间件函数应用于app.use()函数的顺序关系到,因为这是它们执行的顺序。换句话说,开发人员需要小心中间件语句的顺序(在app.js中),因为这个顺序将决定每个请求通过相应中间件功能的顺序。

你已经困惑了吗?看看这个例子:一个会话(express-session)必须跟随一个 cookie ( cookie-parser),因为任何 web 会话都依赖 cookie 来存储会话 ID(它是由cookie-parser提供的)。如果我们移动它们,会议将无法进行!另一个例子是需要express-session的跨站点请求伪造中间件csurf

为了使这一点完全清楚,出于完全相同的原因,中间件语句放在路由之前。如果您将静态(express.static()serve-static)中间件放在路由定义之后,那么框架将通过响应来完成请求流,静态资产(例如,来自/public)将不会被提供给客户端。

让我们更深入地了解以下中间件:

  • compression
  • morgan
  • body-parser
  • cookie-parser
  • express-session
  • csurf
  • express.staticserve-static
  • connect-timeout
  • errorhandler
  • method-override
  • response-time
  • serve-favicon
  • serve-index
  • vhost
  • connect-busboy

压缩

compression 中间件(NPM: http://npmjs.org/compression ) gzips 传输数据。Gzip 或 GNU zip 是一个压缩工具。要安装compression v1.0.11,在您的终端项目的根文件夹中运行这个命令:

$ npm install compression@1.0.11 --save

您还记得中间件语句的顺序很重要吗?这就是为什么compression中间件通常被放在 Express.js 应用配置的最开始,这样它就在其他中间件和路由之前。利用compression()方法进行压缩:

var compression = require('compression');
// ... Typical Express.js set up...
app.use(compression());

Image 提示您需要将压缩 NPM 模块安装在项目(即本地)node_modules文件夹中。你可以通过$ npm install compression@1.0.10 --save或者将行"compression": "1.0.10"放入package.json文件并运行$ npm install来实现。

compression()方法不需要任何额外的参数,但是如果您是一名高级 Node.js 程序员,您可能希望使用 gzip 选项进行压缩:

  • threshold:以千位为单位的开始压缩的大小(即可以解压缩的最小大小,以千位为单位)
  • filter:过滤出要压缩的内容的功能;默认过滤器是compressible,可在https://github.com/expressjs/compressible获得。

Gzip 使用核心 Node.js 模块 zlib ( http://nodejs.org/api/zlib.html#zlib_options)并将这些选项传递给它:

  • chunkSize:要使用的块的大小(默认:16*1024)
  • windowBits:窗口大小
  • level:压缩等级
  • memLevel:要分配多少内存
  • strategy:应用什么 gzip 压缩算法
  • filter:默认测试Content-Type表头为jsontextjavascript的功能

有关这些选项的更多信息,请参见位于http://zlib.net/manual.html#Advanced的 zlib 文档。

ch4项目中,我们用一些虚拟文本创建了一个index.jade文件,然后将以下内容添加到app.js file中:

var compression = require('compression');
// ... Configurations
app.use(compression({threshold: 1}));

views/index.jade文件将使用一些 Lorem Ipsum 文本呈现h1p HTML 元素,如下所示:

h1 hi
p Lorem Ipsum is simply dummy text of ...

Image 提示要获得完整的 Jade 模板引擎教程,请查阅 Practical Node.js (Apress,2014)。

作为应用compression的结果,在 Chrome 浏览器开发者工具控制台,可以看到Content-Encoding: gzip响应头,如图图 4-1 所示。

9781484200384_Fig04-01.jpg

图 4-1 。内容编码是 gzip,使用压缩中间件

摩根

morgan 中间件(https://www.npmjs.org/package/morgan)根据指定的输出格式跟踪所有请求和其他重要信息。要安装morgan1 . 2 . 2 版,请使用

$ npm install morgan@1.2.2 --save

Morgan 要么接受一个选项对象,要么接受一个格式字符串(commondev等)。);例如,

var logger = require('morgan');
// ... Configurations
app.use(logger('common'));

或者

var logger = require('morgan');
// ... Configurations
app.use(logger('dev'));

或者

var logger = require('morgan');
// ... Configurations
app.use(logger(':method :url :status :res[content-length] - :response-time ms'));

传递给 morgan 函数的支持选项(上例中的logger())如下:

  • format:有输出格式的字符串;查看即将发布的令牌字符串和预定义格式列表。
  • stream:要使用的输出流默认为stdout,但也可以是其他任何东西,比如一个文件或另一个流。
  • buffer:缓冲间隔的毫秒数;如果未设置或不是数字,则默认为 1000 毫秒。
  • immediate:布尔值,当设置为true时,使记录器(morgan)根据请求而不是响应写日志行。

以下是可用的格式字符串参数或标记:

  • :req[header](例如:req[Accept])
  • :res[header](例如:res[Content-Length])
  • :http-version
  • :response-time
  • :remote-addr
  • :date
  • :method
  • :url
  • :referrer
  • :user-agent
  • :status

以下是 Morgan 附带的预定义格式/标记:

  • combined:同:remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"
  • common:同:remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length]
  • short:同:remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms
  • tiny:同:method :url :status :res[content-length] - :response-time ms
  • dev:简短彩色开发输出,带响应状态,同:method :url :status :response-time ms - :res[content-length]

你也可以定义你自己的格式。有关更多信息,请参考位于https://www.npmjs.org/package/morganmorgan文档。

正文分析器

body-parser模块 ( https://www.npmjs.org/package/body-parser)可能是所有第三方中间件模块中最本质的。它允许开发人员将传入的数据(如主体有效负载)处理成可用的 JavaScript/Node.js 对象。要安装body-parser v1.6.1,运行以下命令:

$ npm install body-parser@1.6.1

body-parser模块有以下独特的中间件:

  • json():处理 JSON 数据;例如{"name": "value", "name2": "value"}
  • urlencoded():处理 URL 编码的数据;例如name=value&name2=value2
  • raw():返回主体作为缓冲类型
  • text():以字符串类型返回正文

如果请求的 MIME 类型为application/json,那么json()中间件将尝试将请求有效负载解析为 JSON。结果将被放入req.body对象中,并传递给下一个中间件和路由。

我们可以将以下选项作为属性传递:

  • strict:布尔型truefalse;如果是true(默认),那么当第一个字符不是``或{时,400 状态错误(错误请求)将被传递给next()回调。
  • reviver:转换输出的JSON.parse()函数的第二个参数;更多信息请访问 MDN。 [1
  • limit:最大字节大小;默认情况下禁用。
  • inflate:给瘪了的身体充气;默认为true
  • type:要解析的内容类型;默认为json
  • verify:验证身体的功能。

例如,如果您需要跳过私有方法/属性(按照惯例,它们以下划线符号_开头),应用非严格解析,并且限制为 5,000 个字节,您可以输入以下内容:

var bodyParser = require('body-parser');
// ... Express.js app set up
app.use(bodyParer.json({
  strict: false,
  reviver: function(key, value) {
    if (key.substr(0,1) === '_') {
      return undefined;
    } else {
      return value;
    }
  },
  limit: 5000
}));
// ...Boot-up

urlencoded()

这个body-parser模块的urlencoded()中间件只解析带有x-ww-form-urlencoded头的请求。它利用 qs 模块的(https://npmjs.org/package/qs ) querystring.parse())函数,并将结果 JS 对象放入req.body

除了limittypeverifyinflateurlencoded()带一个extended布尔选项。extended选项是一个强制字段。当设置为true(默认值)时,body-parser使用qs模块(https://www.npmjs.org/package/qs)解析查询字符串。

如果将extended设置为false,body-parser 将使用核心 Node.js 模块querystring解析 URL 编码的数据。我建议将extended设置为true(即使用qs),因为它允许从 URL 编码的字符串中解析对象和数组。

如果您忘记了 URL 编码的字符串是什么样子,它是 URL 中问号(?)后面的name=value&name2=value2字符串。

我们也可以将limit参数传递给urlencoded()limit选项的工作方式类似于bodyParser.json()中间件中的limit,您可以在前面的代码片段中看到。例如,要将limit设置为 10,000:

var bodyParser = require('body-parser');
// ... Express.js set up
app.use(bodyParser.urlencoded({limit: 10000});

Image 注意在旧版本中,众所周知,bodyParser.multipart()中间件在处理大文件上传时容易出现故障。安德鲁·凯利在文章《不要将 bodyParser 与 Express.js 一起使用》中描述了确切的问题 2 当前版本的 Express.js v 4.x 对 bodyParser.multipart()的非捆绑支持。而是 Express.js 团队推荐使用卫生员、 3 威猛、 4 或者多方。 5

cookie 分析器

cookie-parser 中间件(https://www.npmjs.org/package/cookie-parser)允许我们从请求处理程序中的req.cookie对象访问用户 cookie 值。该方法采用一个字符串,该字符串用于对 cookies 进行签名。通常是一些巧妙的伪随机序列(如very secret string)。要安装cookie-parser v1.3.2,运行以下命令:

$ npm install cookie-parser@1.3.2

像这样使用它:

var cookieParser = require('cookie-parser');
// ... Some Express.js set up
app.use(cookieParser());

或者用秘密字符串(任意的随机字符串,通常存储在环境变量中):

app.use(cookieParser('cats and dogs can learn JavaScript'));

Image 注意 避免在 cookies 中存储任何敏感信息,尤其是与用户相关的信息(个人身份信息),比如凭据,或者他们的偏好。在大多数情况下,仅使用 cookies 来存储与服务器上的值相匹配的唯一且难以猜测的密钥(会话 ID)。这使您能够在后续请求中检索用户会话。

除了 secret 之外,cookieParser()还将这些选项作为第二个参数:

  • path:一个 cookie 路径
  • expires:cookie 的绝对截止日期
  • maxAge:cookie 的相对最长时间
  • domain:cookie 的网站域
  • secure:布尔值,表示 cookie 是否安全
  • httpOnly:布尔值,表示是否只有 HTTP

cookie-parser 有一些额外的方法:

  • JSONCookie(string):将字符串解析成 JSON 数据格式
  • JSONCookies(cookies):与JSONCookie(string)相同,但对象不同
  • signedCookie(string, secret):将 cookie 值解析为签名的 cookie
  • signedCookies(cookies, secret):与signedCookie(string, secret)相同,但对象不同

快速会话

express-session 中间件(https://www.npmjs.org/package/express-session)允许服务器使用 web 会话。这个中间件必须在其定义之前启用 cookie-parser(在app.js文件中更高)。要安装express-session v1.7.6,运行以下命令:

$ npm install express-session@1.7.6 --save

1.7.6 版中间件采用了这些选项:

  • key : Cookie 名称,默认为connect.sid
  • store:会话存储实例,通常是一个 Redis 对象(在第十二章中有详细介绍)
  • secret:用于对会话 cookie 进行签名,防止篡改;通常只是一个随机字符串
  • cookie:会话 cookie 设置,默认为{ path: '/', httpOnly: true, maxAge: null }
  • proxy:布尔值,表示在设置安全 cookies 时(通过"X-Forwarded-Proto")是否信任反向代理
  • saveUninitialized:强制保存新会话的布尔值(默认为真)
  • unset:用可能的值keepdestroy(默认为keep)控制在取消设置会话后是否要将会话保留在存储中
  • resave:强制保存未修改会话的布尔值(默认值为 true)
  • rolling:布尔值,在每次请求时设置一个新的 cookie,以重置到期时间(默认值为 false)
  • genid:生成会话 ID 的函数(默认为uid2 : https://www.npmjs.org/package/uid2https://github.com/coreh/uid2)

默认情况下,会话存储在内存中。然而,我们可以使用 Redis 来实现持久性,并在多台机器之间共享会话。有关 Express.js 会话的更多信息,请参考第三部分,尤其是第十二章。

-是吗

跨站点请求伪造(CSRF) 当客户端仍然拥有来自受保护网站(如银行网站)的会话信息,并且恶意脚本代表客户端提交数据(甚至可能是资金转账)时,就会发生这种情况。攻击之所以成功,是因为银行的服务器无法区分客户来自银行网站的有效请求和来自一些受损或不可信网站的恶意请求。浏览器有正确的会话,但用户不在银行网站的页面上!!!

为了防止 CSRF,我们可以通过在每个请求中使用一个令牌并根据我们的记录验证该令牌来启用 CSRF 保护。通过这种方式,我们知道我们服务了页面或资源,带有提交数据的后续请求来自该页面或资源。更多信息,请参考维基百科 CSRF 条目http://en.wikipedia.org/wiki/Cross-site_request_forgery

Express.js 通过在会话(req.session._csrf)中放置一个_csrf令牌并对照req.bodyreq.queryX-CSRF-Token头中的值验证该值,来处理csurf模块(https://www.npmjs.org/package/csurf)的 CSRF 保护。如果值不匹配,则返回 403 禁止的 HTTP 状态代码,这意味着资源被禁止(例如,参见http://en.wikipedia.org/wiki/HTTP_403)。默认情况下,csurf中间件不检查 GET、HEAD 或 OPTIONS 方法。要安装csurf v1.6.0,运行以下命令:

$ npm install csurf@1.6.0 --save

使用csurf v1.6.0 最简单的例子如下:

var csrf = require('csurf');
// ... Instantiate Express.js application
app.use(csrf());

csurf v1.6.0 采用以下附加参数:

  • value:将 request ( req)作为参数的函数,检查令牌是否存在,并返回值 true(找到)或 false(未找到)。看看下面的例子。
  • cookie:指定使用基于 cookie 的存储,而不是默认的基于会话的存储(不推荐)
  • ignoreMethods:在检查请求中的 CSRF 令牌时要忽略的 HTTP 方法的数组(默认值为['GET', 'HEAD', 'OPTIONS'])

您可以通过在value属性中传递回调函数来覆盖检查标记值存在的默认函数;例如,要使用不同的名称并只检查请求体,您可以使用

var csrf = require('csurf');
// ... Instantiate Express.js application
app.use(express.csrf({
  value: function (req) {
    return (req.body && req.body.cross_site_request_forgery_value);
  }
}));

csrf 中间件必须在 express- 会话cookie 解析器之后,并且可选地(如果你计划在请求体中支持令牌)在体解析器之后中间件:

var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var csrf = require('csurf');
// ... Instantiate Express.js application
app.use(bodyParser.json());
app.use(cookieParser());
app.use(session());
app.use(csrf());

express.static()

作为独立模块(https://www.npmjs.org/package/serve-static)的express.static()serve-static是 express . js 4 . x 版唯一附带的中间件,所以你不必安装它。换句话说,在引擎盖下,express.static()是一个serve-static模块:https://github.com/expressjs/serve-static。我们已经介绍了express. static (path, options)方法,该方法从指定的根路径向文件夹提供文件,例如:

app.use(express.static(path.join(__dirname, 'public')));

或者(不推荐,因为这可能在 Windows 上不起作用):

app.use(express.static(__dirname + '/public'));

相对路径也是一个选项:

app.use(express.static('public'));

express.static(path, options) v1.5.0(对于 Express.js v4.8.1)方法采用这些选项:

  • maxAge:为浏览器缓存 maxAge 设置的毫秒数,默认为0
  • redirect:布尔型truefalse(默认为true)表示当 URL 路径名为目录时,是否允许重定向到结尾斜杠(/)
  • dotfiles:表示如何处理隐藏的系统文件夹/文件(例如gitignore);可能的值有ignore(默认)、allowdeny
  • etag:布尔值,表示是否使用 ETag 缓存(默认为true)
  • extensions:布尔值,表示是否使用默认文件扩展名(默认为false)
  • index:标识索引文件;默认为index.html;一个数组、一个字符串和false(禁用)都是可能的值
  • setHeaders:设置自定义响应头的功能

下面是 express.static()高级用法的一个示例,其中包含一些任意值:

app.use(express.static(__dirname + '/public', {
  maxAge: 86400000,
  redirect: false,
  hidden: true,
  'index': ['index.html', 'index.htm']
}));

连接超时

connect-timeout模块 ( https://www.npmjs.org/package/connect-timeout)设置超时。建议仅在您怀疑可能比一般路线慢的特定路线(如'/slow-route')上使用该中间件。要使用connect-timeout v1.2.2,请安装:

$ npm install connect-timeout@1.2.2 --save

在您的服务器文件中,编写这些语句,如示例ch4/app.js所示:

var timeout = require('connect-timeout');
// ... Instantiation and configuration
app.get(
  '/slow-request',
  timeout('1s'),
  function(request, response, next) {
    setTimeout(function(){
      if (request.timedout) return false;
      return next();
    }, 999 + Math.round(Math.random()));
  }, function(request, response, next) {
    response.send('ok');
  }
);
// ... Routes and boot-up

$ node app运行服务器。然后,从单独的终端,用 CURL 发送几个 GET 请求:

$ curl http://localhost:3000/slow-request -i

响应应该超时大约一半时间,并显示 503 服务不可用状态代码。好的响应返回状态代码 200。两者如图 4-2 所示。可以在错误处理程序中定制消息。

9781484200384_Fig04-02.jpg

图 4-2 。超时中间件运行和不运行时的响应

errorhandler(错误处理程序)

errorhandler 中间件(https://www.npmjs.org/package/errorhandler)可以用于基本的错误处理。这在开发和原型制作中特别有用。这个模块不会做任何您自己不能用定制的错误处理中间件做的事情。然而,它会节省你的时间。对于生产环境,请考虑根据您的需要定制错误处理。

使用以下 NPM 命令完成errorhandler v1.1.1 模块安装:

$ npm install errorhandler@1.1.1 --save

我们在服务器文件中应用它,如下所示:

var errorHandler = require('errorhandler');
// ... Configurations
app.use(errorHandler());

或者,仅适用于开发模式:

if (app.get('env') === 'development') {
  app.use(errorhandler());
}

编写自己的错误处理程序是微不足道的。事实上,你已经在第二章第一节和第三章第三节中看到过了,在第一章中我们检查了由生成器生成的代码。例如,这是一个基本处理程序,它呈现一个带有错误消息的错误模板:

app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

如您所见,方法签名类似于请求处理程序或中间件,但它有四个参数,而不是像中间件那样有三个参数,或者像 core Node.js 请求处理程序那样有两个参数。这就是 Express.js 如何确定这是一个错误处理程序而不是中间件——函数定义中的四个参数:error ( err)、request ( req)、response ( res)和next

通过用错误对象调用next()从另一个中间件内部触发该错误处理程序;比如next(new Error('something went wrong'))。如果我们不带参数地调用next(),Express.js 会认为没有错误,并继续处理链中的下一个中间件。

方法覆盖

方法覆盖中间件(https://www.npmjs.org/package/method-override)使您的服务器能够支持客户机可能不支持的 HTTP 方法——例如,请求被限制为 GET 和 POST 的系统(比如浏览器中的 HTML 表单)。要安装method-override v2.1.3,运行:

$ npm install method-override@2.1.3 --save

method-override模块可以使用来自传入请求的X-HTTP-Method-Override=VERB报头:

var methodOverride = require('method-override');
// ... Configuratoins
app.use(methodOverride('X-HTTP-Method-Override'));

除了头,我们可以使用一个查询字符串。例如,用?_method=VERB支持请求:

var methodOverride = require('method-override');
// ... Configuratoins
app.use(methodOverride('_method'));

ch4/app.js中,在我们使用查询字符串方法和_method名称安装、导入和应用方法覆盖中间件之后,我们可以像这样定义一个删除路径:

app.delete('/purchase-orders', function(request, response){
  console.log('The DELETE route has been triggered');
  response.status(204).end();
});

在我们用$ node app 启动应用后,我们在一个单独的终端窗口中用 CURL 提交POST请求。在 URL 中,我们将_method指定为DELETE:

$ curl http://localhost:3000/purchase-orders/?_method=DELETE -X POST

Express.js 将这个 CURL 请求视为删除 HTTP 方法请求,我们将在服务器上看到以下消息:

The DELETE route has been triggered

对于 Windows 用户,CURL 可以从http://curl.haxx.se/download.html开始安装。或者,你可以使用 Chrome 开发者工具中的 jQuery 的$.ajax()函数。

响应时间

response-time 中间件(https://www.npmjs.org/package/response-time)在X-Response-Time报头中添加了从请求进入该中间件开始的时间(以毫秒为单位)。

要安装response-time v2.0.1,运行

$ npm install response-time@2.0.1 --save

response-time() 方法在需要包含在结果中的点之后取一些数字( 3 是缺省值)。让我们要求 4 位数:

var responseTime = require('response-time');
// ... Middleware
app.use(responseTime(4));

为了演示这个中间件的运行,用$ node app 运行ch4/app.js。服务器有这些关于响应时间中间件的陈述:

app.use(responseTime(4));
// ... Middleware
app.get('/response-time', function(request, response){
  setTimeout(function(){
    response.status(200).end();
  }, 513);
});

preceding /response-time 路线背后的想法是将响应延迟 513 ms。然后,在一个单独的终端窗口中,运行带有-icurl命令,以发出 GET 请求并输出响应信息:

$ curl http://localhost:3000/response-time -i

如图 4-3 所示,该标题出现在响应中:

X-Response-Time: 514.3193ms

9781484200384_Fig04-03.jpg

图 4-3 。带有显示响应时间的 X-Response-Time 标头的 HTTP 响应

serve-favicon

serve-favicon 中间件(https://www.npmjs.org/package/serve-favicon)可以让你把浏览器中默认的收藏夹图标改成自定义图标。

要安装static-favicon v2.0.1 模块,运行:

$ npm install serve-favicon@2.0.1 --save

要包含和应用中间件,运行

var favicon = require('serve-favicon');
// ... Instantiations
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));

serve-favicon v2.0.1 模块有两个参数:

  • path:收藏图标文件的路径,或者图标数据的缓冲区(缓冲区是 Node.js 二进制类型)
  • options : maxAge 以毫秒为单位——缓存收藏图标多长时间;默认值为 1 天

当你运行ch4/app.js时,你会在标签上看到 webapplog.com 的标志,如图图 4-4 所示。

9781484200384_Fig04-04.jpg

图 4-4 。使用 serve-favicon 中间件时最喜欢的图标

发球指数

serve-index 中间件(https://www.npmjs.org/package/serve-index)使您能够基于特定文件夹的内容创建目录列表。可以把它想象成一个终端$ ls命令(或者 Windows 上的dir)。您甚至可以用自己的模板和样式表定制外观(选项将在本节稍后讨论)。

要安装serve-index v1.1.6,运行:

$ npm install serve-index@1.1.6 --save

要应用中间件,请在您的服务器文件中写入以下几行:

var serveIndex = require('serve-index');
// ... Middleware
app.use('/shared', serveIndex(
  path.join('public','shared'),
  {'icons': true}
));
app.use(express.static('public'));

serveIndex语句中,指定'/shared'文件夹,并将path.join('public', 'shared');路径传递给项目目录中的public / shared文件夹。图标值true(icons: true)表示显示图标。需要静态中间件来显示实际的文件。

这几行代码取自ch4/app.js,如果你运行它并导航到http://localhost:3000/shared,你会看到一个带有文件夹名(shared)和文件名(abc.txt)的网络界面,如图图 4-5 所示。

9781484200384_Fig04-05.jpg

图 4-5 。带有文件夹和文件的默认服务器索引 web 界面

如果你把浏览器的尺寸调整到足够窄,界面应该会改变——响应性!此外,由于默认的serve-index界面,还有搜索栏。

点击文件名abc.txt应该会打开显示消息“秘密文本”的文件,如图图 4-6 所示。这是使用expsess.static()中间件而不是serve-index的结果。

9781484200384_Fig04-06.jpg

图 4-6 。由静态中间件服务的文本文件

serve-index中间件将 options 对象作为第二个参数(第一个是路径)。这些选项可以具有以下属性:

  • hidden : Boolean 表示是否显示隐藏(点)文件;默认为false
  • view:显示模式(tilesdetails);默认为tiles
  • icons : Boolean 表示是否在文件名/文件夹名旁边显示图标;默认为false
  • filter:过滤功能;默认为false
  • stylesheet:CSS 样式表的路径(可选);默认为内置样式表
  • template:HTML 模板的路径(可选);默认为内置模板

在模板中,您可以使用:{directory}作为目录名,{files}作为文件链接的无序列表(<ol>)的 HTML,{linked-path}作为目录链接的 HTML,以及{style}作为指定的样式表和嵌入的图像。

Image 注意不要在系统文件夹和机密文件上随意使用 serve-index。最好将其限制在某个子文件夹中,比如public

甚么东西

vhost 中间件(https://www.npmjs.org/package/vhost)使您能够基于域使用不同的路由逻辑。例如,我们可以有两个 Express.js 应用,apiweb,分别根据域、api.hackhall.com 或www.hackhall.com来组织不同路线的代码:

var app =express()
var api = express()
var web = express()
// ... Configurations, middleware and routes
app.use(vhost('www.hackhall.com', web))
app.use(vhost('api.hackhall.com', api))
app.listen(3000)

要安装vhost v2.0.0,运行:

$ npm install vhost@2.0.0 --save

vhost v2.0.0 中间件有两个参数(如前面的例子所示):

  • domain : String 或 RegExp 例如,*.webapplog.com
  • server:服务器对象(expressconnect);比如api或者web

连接-餐馆工

connect-busboy模块 ( https://www.npmjs.org/package/connect-busboy)是 connect.js/Express.js 中间件,被构建用于busboy表单解析器(https://www.npmjs.org/package/busboy)。busboy表单解析器基本上接受传入的 HTTP(S)请求的多部分主体,并允许我们使用它的字段、上传的文件等等。要安装已经包含 busboy 的中间件 v0.0.1,运行

$ npm install connect-busboy@0.0.1 --save

然后,在您的服务器文件(app.js)中,编写类似下面的内容,以在/upload路径上实现文件上传功能:

var busboy = require('connect-busboy');
// ... Configurations
app.use('/upload', busboy({immediate: true }));
app.use('/upload', function(request, response) {
  request.busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
    file.on('data', function(data){
      fs.writeFile('upload' + fieldname + filename, data);
    });
    file.on('end', function(){
      console.log('File' + filename + 'is ended');
    });

  });
 request.busboy.on('finish', function(){
    console.log('Busboy is finished');
   response.status(201).end();
 })
});

前面的示例将文件写入磁盘,并在完成后向客户端输出 201。在终端中,我们应该会看到带有“ended”字样的文件名。

要模拟没有网页表单的文件上传,我们可以使用我们的老朋友 CURL(一行命令):

$ curl -X POST -i -F name=icon -F filedata=@./public/favicon.ico http://localhost:3000/upload

我们上传的文件在ch4/public/favicon.ico中。这是早期 serve-favicon 示例中最受欢迎的图标。因此,项目文件夹中应该有一个名为uploadfiledatafavicon.ico的文件。并且在您的终端服务器窗口上,您应该看到消息:

File favicon.ico is ended
Busboy is finished

在你的终端客户端上(即 curl 窗口),你会看到 201 被创建。

Image 除了ch4的例子,请参见第四部分的章节,了解更多关于中间件的高级例子。

其他中间件

还有很多其他值得注意的模块兼容 Connect.js 和 Express.js,下面只是简单列举了一些目前比较流行的模块;每个月都会有更多的作品问世,其中一些已经停产或被放弃,所以请定期查看 NPM 的更新。您可以在https://www.npmjs.org/package/ package 名称中找到这些模块中的每一个,其中包名称是下面列表中模块的名称。

  • cookieskegrip :替代cookie-parser ( https://www.npmjs.org/package/cookieshttps://www.npmjs.org/package/keygriphttps://www.npmjs.org/package/cookie-parser)
  • cookie-session :基于 Cookie 的会话存储(https://www.npmjs.org/package/cookie-session)
  • raw-body :针对作为缓冲器的请求(https://www.npmjs.org/package/raw-body)
  • connect-multiparty :使用mutliparty,替代connect-busboy ( https://www.npmjs.org/package/connect-multipartyhttps://www.npmjs.org/package/multipartyhttps://www.npmjs.org/package/connect-busboy)
  • qs:替代查询和查询字符串 ( https://www.npmjs.org/package/qshttps://www.nodejs.org/api/querystring.html)
  • stconnect-staticstatic-cache:静态资产缓存 ( https://www.npmjs.org/package/sthttps://www.npmjs.org/package/connect-statichttps://www.npmjs.org/package/static-cache)
  • express-validator :传入数据验证/清理(https://www.npmjs.org/package/express-validator)
  • everyauthpassport??:认证授权中间件(https://www.npmjs.org/package/everyauthhttps://www.npmjs.org/package/passport
  • oauth2-server : OAuth2 服务器中间件(https://www.npmjs.org/package/oauth2-server)
  • helmet :安全中间件集合(https://www.npmjs.org/package/helmet)
  • connect-cors :支持 Express.js 服务器的跨产地资源共享(CORS)(https://www.npmjs.org/package/connect-cors)
  • connect-redis:express . js 会话的 Redis 会话存储(https://www.npmjs.org/package/connect-redis)

摘要

本章讲述了如何创建和应用您自己的定制中间件,以及如何安装和应用来自 NPM 的第三方中间件。您了解了最基本的中间件是如何工作的,它们的功能需要哪些参数,以及它们的行为方式。您可能已经注意到,在ch4示例项目中,我们为错误页面和压缩页面使用了一个小模板。

下一章是配置和中间件主题的延续。这些几乎是任何 Express.js 应用的不同部分,正如我们在第一章(或server.jsindex.js,意思是主 Express.js 文件)中讨论的高级app.js结构。下一个主题是关于配置由模板支持的视图。第四章深入探讨了我们如何使用不同的模板引擎。我们探索如何利用 Express.js 中最流行的选项,比如 Jade 和 Handlebars 以及其他库。


1

2

3

4

5

五、模板引擎和 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 )。

9781484200384_Fig05-01.jpg

图 5-1 。从 index.html 文件中渲染出的玉石模板

此外,当你转到/swig(见图 5-2 )时,服务器应该呈现“嗨,我正在从 index.swig 中 Swig”。

9781484200384_Fig05-02.jpg

图 5-2 。从 index.Swig 文件呈现的 swig 模板

最后,当你转到/template(见图 5-3 )时,它应该呈现“嗨,我是 index.template 文件中的 Jade”。

9781484200384_Fig05-03.jpg

图 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>

Image 注意 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 )。

9781484200384_Fig06-01.jpg

图 6-1 。用户名 URL 参数被解析并用于查找用户页面上显示的信息(例如 ch6)

9781484200384_Fig06-02.jpg

图 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);
});

Image 提示如果你有一个大型应用,有很多版本的 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);
});

Image 注意授权和输入卫生是驻留在中间件中的很好的候选者。关于 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()—检索服务器的功能

Image 提示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() :请求头

Image 提示当你在代码中看到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');
})

Image 提示\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 所示。

9781484200384_Fig07-01.jpg

图 7-1 。使用查询字符串参数运行 CURL 命令的客户端结果

9781484200384_Fig07-02.jpg

图 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

9781484200384_Fig07-03.jpg

图 7-3 。用 CURL 发送 GET 请求(客户端窗口)

如图 7-4 所示,我们看到request.params对象的这些服务器日志:

[ role: 'admin', name: 'azat', status: 'active' ]
[ role: 'user', name: 'bob', status: 'active' ]

9781484200384_Fig07-04.jpg

图 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

Image 提示一个简短的提示:-H选项设置头,-d传递数据,-i启用详细日志记录。

前面的命令产生了request.body对象,如图 7-5 中的客户端和图 7-6 中的服务器端所示:

{ name: 'azat' }
{ name: 'azat', role: 'admin' }
{ username: 'azat', password: 'p@ss1' }

9781484200384_Fig07-05.jpg

图 7-5 。使用 CURL 发送 POST 请求(客户端日志)

9781484200384_Fig07-06.jpg

图 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']

Image 警告出于安全考虑,不鼓励在浏览器 cookies 中存储敏感信息。此外,一些浏览器对 cookie 的大小施加了限制,这可能会导致错误(Internet Explorer!).我通常只用request.cookie来支持request.session

Image 关于如何安装和应用中间件的更多信息,请参考第四章。

可以使用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);
})

Image 提示parseInt()方法用于防止 JavaScript/Node.js 将数字值视为字符串,这将导致 0、01、011、0111 等。而不是 0,1,2,3 等等。建议将parseInt()与基数/基数(第二个参数)一起使用,以防止数字被错误转换。

由于转到http://localhost:3000/cookies并刷新几次,你应该看到计数器从 0 向上递增,如图图 7-7 所示。

9781484200384_Fig07-07.jpg

图 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)。

Image 注意签署 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 所示的欢迎信息。

9781484200384_Fig08-01.jpg

图 8-1 。不带参数的普通 response.render()调用的结果

Image 注意 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。

9781484200384_Fig08-02.jpg

图 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*
  });
});

Image 注意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');
});

9781484200384_Fig08-03.jpg

图 8-3 。response.locals 示例呈现与 response.render()示例相同的页面

Image 提示有时候,为了调试,查看特定 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(),自己看。

9781484200384_Fig08-04.jpg

图 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 的中看到标题。

9781484200384_Fig08-05.jpg

图 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 所示。

9781484200384_Fig08-06.jpg

图 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

Image 提示发送以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表示未找到。

9781484200384_Fig08-07.jpg

图 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

9781484200384_Fig08-08.jpg

图 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 所示。

9781484200384_Fig08-09.jpg

图 8-9 。response.send()缓冲示例响应

Image 注意几乎所有的核心 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头。

9781484200384_Fig08-10.jpg

图 8-10 。使用 response.json()的结果:自动生成的头

Image 图 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'}
  ]);
});

9781484200384_Fig08-11.jpg

图 8-11 。response.jsonp()的结果和?callback=cb 是一个文本/javascript 头和 javascript 函数前缀

Image 图 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 毫秒)。我们在非流式示例中使用同步函数的事实并不重要,因为我们是串行加载页面的(一个接一个)。

9781484200384_Fig08-12.jpg

图 8-12 。流式传输图像显示出比非流式传输更快的等待时间

9781484200384_Fig08-13.jpg

图 8-13 。非流式图像显示的等待时间比流式图像慢

Image 提示除了使用流进行响应之外,它们也可以用于请求。在处理大量数据(视频、二进制数据、音频等)时,流式传输非常有用。)因为流允许在没有完成传输的情况下开始处理。有关流的更多信息,请查看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

九、错误处理和运行应用

好的 web 应用必须有信息丰富的错误消息来通知客户端请求失败的确切原因。错误可能是由客户端(例如,错误的输入数据)或服务器(例如,代码中的错误)引起的。

客户端可能是一个浏览器,在这种情况下,应用应该显示一个 HTML 页面。例如,当找不到请求的资源时,应该显示 404 页面。或者客户端可能是另一个通过 REST API 消耗我们资源的应用。在这种情况下,应用应该以 JSON 格式(或者 XML 或其他支持的格式)发送适当的 HTTP 状态代码和消息。由于这些原因,在开发重要的应用时,定制错误处理代码总是最佳实践。

在典型的 Express.js 应用中,错误处理程序遵循以下路线。错误处理值得在这本书里有自己的章节,因为它不同于其他中间件。在错误处理程序之后,我们将介绍 Express.js 应用方法和启动 Express.js 应用的方式。因此,本章的主要议题如下:

  • 错误处理
  • 运行应用

错误处理

由于 Node.js 和回调模式的异步特性,捕捉和记录错误发生的状态以备将来分析并不是一项简单的任务。在第十七章中,我们将介绍 Express.js 应用中域名的使用。在 Express.js 中使用域进行错误处理是一种更高级的技术,对于大多数开箱即用的实现来说,框架的内置错误处理可能已经足够了(加上定制的错误处理中间件)。

我们可以从我们的ch2/cli-app例子中的基本开发错误处理程序开始。错误处理器抛出错误状态(500,内部服务器错误)、堆栈跟踪和错误消息。当应用处于开发模式时,仅通过此代码启用:

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
        });
    });
}

Image 提示 app.get('env')process.env.NODE_ENV的便捷方法;换句话说,前面一行可以用process.env.NODE_ENV === 'development'改写。

这是有意义的,因为错误处理通常在整个应用中使用。因此,最好将其实现为中间件。

对于定制的错误处理程序实现,除了多了一个参数error(或简称为err):之外,中间件与其他任何中间件都是一样的

*// Main middleware*
app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  console.error(err);
  res.status(500).send();
});
*// Routes*

我们可以使用res.status(500).end()来获得类似的结果,因为我们没有发送任何数据(例如,错误消息)。建议至少发送一条简短的错误消息,因为这将有助于出现问题时的调试过程。事实上,响应可以是任何东西:JSON、文本、静态页面的重定向或其他东西。

对于大多数前端和其他 JSON 客户端,首选格式当然是 JSON:

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  console.error(err);
  res.status(500).send({status:500, message: 'internal error', type:'internal'});
})

Image 注意开发者可以使用req.xhr属性或者检查Accept请求头是否有application/json值。

最简单的方法就是发一条短信:

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  console.error(err);
  res.status(500).send('internal server error');
})

或者,如果我们知道输出错误消息是安全的,我们可以使用下面的方法:

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  console.error(err);
  res.status(500).send('internal server error: ' + err);
})

为了简单地呈现一个名为500(模板是文件500.jade,引擎是 Jade)的静态错误页面,我们可以使用

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  console.error(err);
  *// Assuming that template engine is plugged in*
  res.render('500');
})

或者,如果我们想要覆盖文件扩展名,我们可以使用以下内容作为完整的文件名500.html:

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  console.error(err);
  *// Assuming that template engine is plugged in*
  res.render('500.html');
})

我们也可以使用res.redirect():

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  res.redirect('/public/500.html');
})

建议始终使用正确的 HTTP 响应状态,如401400500等。快速参考参见表 9-1 。

表 9-1 。主要 HTTP 状态代码

|

密码

|

名字

|

意义

|
| --- | --- | --- |
| Two hundred | 好 | 成功 HTTP 请求的标准响应 |
| Two hundred and one | 创造 | 请求已被满足。新资源已创建 |
| Two hundred and four | 没有内容 | 请求已处理。没有返回内容 |
| Three hundred and one | 永久移动 | 此请求和所有将来的请求都指向给定的 URI |
| Three hundred and four | 未修改 | 自上次请求后,资源未被修改 |
| four hundred | 错误的请求 | 由于语法错误,请求无法实现 |
| Four hundred and one | 未经授权的 | 身份验证是可能的,但是失败了 |
| Four hundred and three | 被禁止的 | 服务器拒绝响应请求 |
| Four hundred and four | 未发现 | 找不到请求的资源 |
| Five hundred | 内部服务器错误 | 服务器出现故障时的一般错误消息 |
| Five hundred and one | 未实施 | 服务器无法识别该方法或缺乏实现该方法的能力 |
| Five hundred and three | 服务不可用 | 服务器当前不可用 |

Image 提示关于可用 HTTP 方法的完整列表,请参考位于www.w3.org/Protocols/rfc2616/rfc2616-sec10.html的 RFC 2616。

这是我们发送状态500(内部服务器错误)而不发回任何数据的方式:

app.use(function(err, req, res, next) {
  *// Do logging and user-friendly error message display*
  res.end(500);
})

要从请求处理程序和中间件内部触发错误,我们只需调用

app.get('/', function(req, res, next){
  next(error);
});

或者,如果我们想要传递一个特定的错误消息,那么我们创建一个错误对象并将其传递给next():

app.get('/', function(req,res,next){
  next(new Error('Something went wrong :-('));
});

使用return关键字来处理多个容易出错的情况并结合前面的两种方法是一个好主意。例如,我们将数据库错误传递给next(),但是一个空的查询结果不会导致数据库错误(即error将是null,所以我们用!users : 检查这个条件

// A GET route for the user entity
app.get('/users', function(req, res, next) {
  // A database query that will get us any users from the collection
  db.get('users').find({}, function(error, users) {
    if (error) return next(error);
    if (!users) return next(new Error('No users found.'));
    *// Do something, if fail the return next(error);*
    res.send(users);
});

对于复杂的应用,最好使用多个错误处理程序。例如,一个用于 XHR/AJAX 请求,一个用于普通请求,一个用于通用的 catch-everything-else。使用命名函数(并将它们组织在模块中)而不是匿名函数也是一个好主意。

关于这种高级错误处理的例子,请参考第二十二章。

Image 提示在管理错误处理方面有一个简单的方法,特别适合开发目的。它被称为errorhandler ( https://www.npmjs.org/package/errorhandler),它拥有 Express.js/Connect.js.的默认错误处理程序,更多关于errorhandler的信息,请参考第四章。

运行应用

Express.js 类提供了一些应用范围内的对象和其对象上的方法,在我们的例子中是app。推荐使用这些对象和方法,因为它们可以改进代码重用和维护。例如,不用到处硬编码数字3000,我们只需用app.set('PORT', 3000);分配它一次。然后,如果我们以后需要更新它,我们只有一个地方需要改变。因此,我们将在本节中介绍以下属性和方法:

  • app.locals
  • app.render()
  • app.mountpath
  • app.on('mount', callback)
  • app.path()
  • app.listen()

app.locals

app.locals对象类似于res.locals对象(在第八章的中讨论过),它将数据暴露给模板。然而,有一个主要的区别:app.locals使它的属性在app呈现的所有模板中可用,而res.locals将它们限制为仅请求。因此,开发者需要小心不要通过app.locals泄露任何敏感信息。这方面的最佳用例是应用范围的设置,如位置、URL、联系信息等。例如:

app.locals.lang = 'en';
app.locals.appName = 'HackHall';

app.locals对象也可以像函数一样被调用:

app.locals([
  author: 'Azat Mardan',
  email: 'hi@azat.co',
  website: 'http://proexpressjs.com'
]);

app.render()

app.render()方法或者用视图名和回调调用,或者用视图名、数据和回调调用。例如,系统可能有一个用于“感谢您注册”消息的电子邮件模板和另一个用于“重置您的密码”的电子邮件模板:

var sendgrid = require('sendgrid')(api_user, api_key);

var sendThankYouEmail = function(userEmail) {
  app.render('emails/thank-you', function(err, html){
    if (err) return console.error(err);
    sendgrid.send({
      to: userEmail,
      from: app.get('appEmail'),
      subject: 'Thank you for signing up',
      html: html // The html value is returned by the app.render
    }, function(err, json) {
      if (err) { return console.error(err); }
      console.log(json);
    });
  });
};

var resetPasswordEmail = function(userEmail) {
  app.render('emails/reset-password', {token: generateResetToken()}, function(err, html){
    if (err) return console.error(err);
    sendgrid.send({
      to: userEmail,
      from: app.get('appEmail'),
      subject: 'Reset your password',
      html: html
    }, function(err, json) {
      if (err) { return console.error(err); }
      console.log(json);
    });
  });
};

Image 例子中使用的sendgrid模块在 NPM1 和 GitHub 都有。 2

app.mountpath

app.mountpath属性用于挂载/订阅的应用。挂载的应用是子应用,可以用于更好的代码重用和组织。属性返回安装了app的路径。

例如,在ch9/app-mountpath.js中有两个子应用:postcomment。帖子挂载在 app 的/post路径,评论挂载在帖子的/comment。作为日志的结果,mountpath 返回值/post/comment:

var express= require('express'),
  app = express(),
  post = express(),
  comment = express();

app.use('/post', post);
post.use('/comment', comment);

console.log(app.mountpath); // ''
console.log(post.mountpath); // '/post'
console.log(comment.mountpath); // '/comment'

app.on('mount ',函数(父){...})

当子应用被装载到父/主应用的特定路径上时,装载被触发。例如,在ch9/app-on-mount.js中,我们有两个带有装载事件监听器的子应用,它们打印父应用的装载路径。路径的值是post的父级(app)的/comment的父级(post)的/post:

var express= require('express'),
  app = express(),
  post = express(),
  comment = express();

post.on('mount', function(parent){
  console.log(parent.mountpath); // '/'
})
comment.on('mount', function(parent){
  console.log(parent.mountpath); // '/post'
})

app.use('/post', post);
post.use('/comment', comment);

app.path()

app.path()方法将返回 Express.js 应用的规范路径。如果您正在使用安装到不同路径的多个 Express.js 应用(为了更好地组织代码),这将非常有用。

例如,通过在post应用的/comment路径上安装comment应用,你可以获得帖子的评论资源(与评论相关的路径)。但是你仍然可以用comment.path()(来自ch9/app-path.js)获得“完整”路径:

var express= require('express'),
  app = express(),
  post = express(),
  comment = express();

app.use('/post', post);
post.use('/comment', comment);

console.log(app.path()); // ''
console.log(post.path()); // '/post'
console.log(comment.path()); // '/post/comment'

app.listen()

Express.js app.listen(port, [hostname,] [backlog,] [callback])方法类似于核心 Node.js http 模块中的server.listen() 3 。这个方法是启动 Express.js app 的方法之一。port是服务器应该接受传入请求的端口号。hostname是域名。当您将应用部署到云时,您可能需要设置它。backlog 是排队等待连接的最大数量。默认值为 511。而callback是一个异步函数,在服务器启动时被调用。

要在特定端口上直接启动 Express.js 应用(3000):

var express = require('express');
var app = express();
*// ... Configuration*
*// ... Routes*
app.listen(3000);

这种方法是由 Express.js 生成器在第二章 Hello World 示例中的ch2/hello.jsch2/hell-name.js示例中创建的。在这里,app.js文件不启动服务器,但是它用

module.exports = app;

我们也不使用$ node app.js运行app.js文件。相反,我们用$ ./bin/www启动了一个 shell 脚本www。shell 脚本的第一行有这个特殊的字符串:

#!/usr/bin/env node

上面的代码行将 shell 脚本转换成 Node.js 程序。这个程序从app.js文件中导入app对象,设置端口,用listen()和一个回调函数启动app服务器

var debug = require('debug')('cli-app');
var app = require('../app');

app.set('port', process.env.PORT || 3000);

var server = app.listen(app.get('port'), function() {
  debug('Express server listening on port ' + server.address().port);
});

当另一个过程需要您的服务器对象时,例如一个测试框架,将您的服务器对象导出为一个模块是必要的。在前面的例子中,主服务器文件(ch2/cli-app/app.js)导出了对象,没有办法用$ node app启动服务器。如果您不希望有一个单独的 shell 文件来启动服务器,但仍然希望在需要时导出服务器,您可以使用下面的技巧。这种方法的要点是检查模块是否是具有require.main === module条件的依赖。如果是真的,那么我们启动应用。如果不是,那么我们公开方法和app对象。

var server = http.createServer(app);
var boot = function () {
  server.listen(app.get('port'), function(){
    console.info('Express server listening on port ' + app.get('port'));
  });
}
var shutdown = function() {
  server.close();
}
if (require.main === module) {
  boot();
} else {
  console.info('Running app as a module');
  exports.boot = boot;
  exports.shutdown = shutdown;
  exports.port = app.get('port');
}

除了app.listen()之外的另一种启动服务器的方式是将 Express.js app 应用到核心 Node.js 服务器函数中。这对于生成具有相同代码库的 HTTP 服务器和 HTTPS 服务器非常有用:

var express = require('express');
var https = require('https');
var http = require('http');
var app = express();
var ops = require('conf/ops');
*//... Configuration*
*//... Routes*
http.createServer(app).listen(80);
https.createServer(ops, app).listen(443);

您可以创建一个自签名的 SSL 证书(例如,server.crt文件),通过运行以下命令使用OpenSSL在本地测试您的 HTTPS 服务器,以用于开发目的:

$ sudo ssh-keygen -f host.key
$ sudo openssl req -new -key host.key -out request.csr
$ sudo openssl x509 -req -days 365 -in request.csr -signkey host.key -out server.crt

OpenSSL 是安全套接字层(SSL)协议的开源实现,也是一个工具包。你可以在https://www.openssl.org找到更多相关信息。当你使用 OpenSSL 时,Chrome 和许多其他浏览器会抱怨一个关于自签名证书的警告——无论如何你可以通过点击继续忽略它(见图 9-1 )。

9781484200384_Fig09-01.jpg

图 9-1 。您可以忽略这个由自签名 SSL 证书引起的警告

Image?? 提示要在 Mac OS X 上安装OpenSSL,运行$ brew install OpenSSL。在 Windows 上,从http://gnuwin32.sourceforge.net/packages/openssl.htm下载安装程序。在 Ubuntu 上,运行apt-get install OpenSSL

server.crt准备好之后,像这样把它喂给https.createServer()方法(ch9/app.js文件):

var express = require('express');
var https = require('https');
var http = require('http');
var app = express();
var fs = require('fs');

var ops = {
    key: fs.readFileSync('host.key'),
    cert: fs.readFileSync('server.crt') ,
    passphrase: 'your_secret_passphrase'
};

app.get('/', function(request, response){
  response.send('ok');
});
http.createServer(app).listen(80);
https.createServer(ops, app).listen(443);

该密码是您在使用OpenSSL创建证书时使用的密码。如果您没有输入任何密码,请忽略它。要启动这个过程,您可能必须使用 sudo,比如$ sudo node app

如果一切正常,您应该会看到如图图 9-2 所示的正常信息。

9781484200384_Fig09-02.jpg

图 9-2 。使用自签名 SSL 证书进行本地开发

最后,如果您的应用执行大量的阻塞工作,您可能希望用cluster模块启动多个进程。这个话题在第十三章中有所涉及。

摘要

本章介绍了实现错误处理程序的多种方法、app对象接口以及启动 Express.js 服务器的方法。第二部分“深度 API 参考”到此结束希望您已经了解了 Express.js 框架对象的许多新属性和方法,例如响应、请求和应用本身。如果你对中间件有任何疑问,那么第四章消除了任何疑虑。最后但同样重要的是,我们讨论了路由、错误处理和模板利用主题。所有这些主题奠定了您的基础,因此您可以使用 Express.js 将这些知识应用于创建令人惊叹和激动人心的新应用

随着这本书的参考部分结束,我们正在进入更实际和复杂的主题,其中包括“如何使用 X”或“如何做 Y”的例子。继续第三部分,“解决常见和抽象的问题”


1

2

3

十、抽象

本章讨论代码组织。抽象通常意味着将整体逻辑分成几个部分。本章介绍的大多数技术可以应用于任何 Node.js 代码,而不仅仅是 Express.js 代码。

一个 Express.js app 通常有一个主文件(app.js或者server.js)。您应该尽量保持这个文件尽可能小,因为随着它变得越来越大,维护起来就越来越困难。最适合用来代替大型主文件的代码类型是中间件和路由。也可以抽象出配置语句,但是它们通常没有路由多,路由可能超过 200 到 300 条。

中间件

如第四章所述,中间件概念提供了灵活性。软件工程师可以使用匿名函数或命名函数作为中间件。使用匿名函数的方法如下所示:

app.use(function(request, response) {
// ...
});
app.get(function(request, response) {
//...
}, function(request, response) {
// ...
});

命名函数方法如下所示:

var middleware = function(request, response){
};
var middleware2 = function(request, response){
//...
};
var middleware3= function(request, response){
//...
};
app.use(middleware);
app.get(middleware2, middleware3);

出于代码重用的目的,命名函数方法更有吸引力。也就是说,如果命名了一个函数,就可以通过传递名称在多个路由中使用它。相反,使用匿名函数的方法只允许您在定义函数的地方使用中间件一次。

当使用命名函数时,随着应用变得越来越大,最佳实践是根据它们的功能将命名函数抽象到外部模块中,例如身份验证或数据库任务。

路线

假设我们有一个包含以下资源的 REST API:故事、元素和用户。我们可以相应地将请求处理程序分离到文件中,这样routes/stories.js就有了

module.exports.findStories = function(req, res, next) {
// ... Query to find stories from the database
};
module.exports.createStory = function(req, res, next) {
// ... Query to create a story in the database
};
// ....

routes/users.js文件保存用户实体的逻辑:

module.exports.findUser = function(req, res, next){
// ...
};
module.exports.updateUser = function(req, res, next){
// ...
};
module.exports.removeUser = function(req, res, next){
// ...
};

主服务器文件(app.jsserver.js)可以以这种方式使用前面的模块:

// ...
var stories = require('./routes/stories');
var users = require('./routes/users');
// ...
app.get('/stories', stories.findStories);
app.post('/stories', stories.createStory);
app.get('/users/:user_id', users.findUser);
app.put('/users/:user_id', users.updateUser);
app.del('/users/:user_id', users.removeUser);
// ...

在带有var stories = require('./routes/stories');的示例中,stories是带有省略(可选).js扩展名的文件stories.js

Image 提示'./routes/stories'中的句号(.)表示路径从当前文件夹开始。在 Windows 上,路径使用\而不是/,所以更好的方法是编写require(path.join('routes', 'stories'));,因为它对跨平台更友好。

请注意,代码在每一行/每一个模块中不断重复;也就是说,开发人员必须通过导入相同的模块(例如users)来复制代码。想象一下,我们需要在每个文件中包含这三个模块!为了避免这种重复,有一个聪明的方法来包含多个文件:将index.js文件放在stories文件夹中,并让该文件包含所有的路线。这是一个很好的做法,因为如果你以后想添加更多的文件到routes文件夹或者改变现有文件的名称,你不需要从app.js请求一个新文件。你只需要修改routes/index.js代码。

例如,在app.js中,我们通过只传递文件夹名来导入index.js(如果我们传递文件夹名,require会寻找index.js):

app.get('/stories', require('./routes').stories.findStories);

或者,我们可以多次使用路线:

var routes = ('./routes');
app.get('/stories', routes.stories.findStories);
app.post('/stories', routes.stories. createStory);

这段代码将访问index.js,这将公开stories对象(和其他对象),该对象从routes文件夹中的stories.js文件导入:

module.exports.stories = require('./stories');
module.exports.users = require('./users);

前面的代码可以重写如下:

exports.stories = require('./stories');
exports.users = require('./users);

或者如下,这是我个人最喜欢的,因为它的口才:

module.exports = {
  stories: require('./stories'),
  users: require('./users)
};

最后,./routes/index.js代码读取的stories.js文件有

exports.findStories = function(req, res, next) {
// ...
};
exports.createStory = function(req, res, next) {
// ...
};
// ...

Image 提示为了更好地组织路线,你可以使用Router类,在第六章中有所介绍。

功能的每一部分都可以分割成一个单独的文件。例如,findStories方法进入ch10/routes-exports/find-stories.js,内容如下:

exports.findStories = function(ops){
  ops=ops || '';
  console.log ('findStories module logic ' + ops);
};

In the index.js we simply import the find-stories.js:

exports.stories = require('./find-stories.js');

举个工作例子,你可以从expressjsguide文件夹运行node -e "require('./routes-exports').stories.findStories();"来查看console.log从模块输出的字符串,如图图 10-1 所示。

9781484200384_Fig10-01.jpg

图 10-1 。通过文件夹和 index.js 文件导入模块

结合中间件和路由

为了说明代码重用的另一种方法,假设有一个应用具有路线/admin/api/stories:

app.get('/admin', function(req, res, next) {
  if (!req.query._token) return next(new Error('No token was provided.'));
  }, function(req, res, next) {
  res.render('admin');
});
*// Middleware that applied to all /api/* calls*
app.use('/api/*', function(req, res, next) {
  if (!req.query.api_key) return next(new Error('No API key was provided.'));
});
app.get('/api/stories', findStory, function(req, res){
  res.json(req.story):
});

在这两个示例中,我们使用以下代码行检查查询字符串参数:

  if (!req.query._token) return next(new Error('no token provided'));

  if (!req.query.api_key) return next(new Error('No API key was provided.'));

但是,参数是不同的,所以我们不能将两个语句抽象成一个函数。不用重复我们自己不是很聪明吗?

为了避免重复,我们可以实现一个返回函数的函数,如下所示:

var requiredParam = function (param) {
  *// Do something with the param, e.g.,*
  *// Create a private attribute paramName based on the value of param variable*
  var paramName = '';
  if (param === '_token')
    paramName = 'token';
  else if (param === 'api_key')
    paramName = 'API key'
  return function (req,res, next) {
    *// Use paramName, e.g.,*
    *// If query has no such parameter, proceed next() with error using paramName*
    if (!req.query[param]) return next(new Error('No ' + paramName +' was provided.'));
    next();
  });
}

app.get('/admin', requiredParam('_token'), function(req, res, next) {
  res.render('admin');
});
    *// Middleware that applied to all /api/* calls*
app.use('/api/*', requiredParam('api_key'));

从某种意义上说,这种“返回函数的函数”模式是一种开关,它根据传递的参数改变模式。

Image 提示前面提到的“返回函数的函数”模式类似于状态单子(http://en.wikipedia.org/wiki/Monad_(functional_programming)#State_monads)。这可以作为面试时讨论的一个很好的话题。

前面的例子非常简单,在大多数情况下,开发人员不需要担心正确的错误文本消息的映射。然而,您可以将这种模式用于许多目的,例如限制输出、管理权限以及在不同的块之间切换。

Image 注意__dirname全局变量提供了使用它的文件的绝对路径,而./返回当前工作目录,这可能会根据我们执行 Node.js 脚本的位置而有所不同(例如,$ node ~/code/app/index.js$ node index.js)。./规则的一个例外是当它被用在require()函数中时,比如conf = require('./config.json');,在这种情况下它充当__dirname

正如您所看到的,中间件/请求处理程序的使用是保持代码有组织的一个强大的概念。最佳实践是通过将所有逻辑移入相应的外部模块/文件来保持路由精简。这样,当您需要时,重要的服务器配置参数都会整齐地放在一个地方。

全局变量module.exportsexports在给它们分配新值时并不完全相同。在前面的例子中,module.exports = function(){...}运行良好,完全有道理,但是exports = function() {...}甚至exports = someObject;将悲惨地失败。原因是 JavaScript 基础知识:对象的属性可以被替换,而不会丢失对该对象的引用,但当我们替换整个对象(即exports = ...,)时,我们会丢失与暴露我们函数的外部世界的链接。

这种行为也被称为对象是可变的和原始的(字符串、数字和布尔值在 JavaScript 中是不可变的)。因此,exports只能通过创建属性并为其赋值来工作,比如exports.method = function() {...};。例如,我们可以在ch10文件夹中运行下面的代码:

$ node -e "require('./routes-module-exports').FindStories('databases');"

结果,你可以在图 10-2 中看到,我们的嵌套结构减少了一级。

9781484200384_Fig10-02.jpg

图 10-2 。使用 module.exports 的结果

如需更多示例,请参考 Karl sweep(http://openmymind.net/2012/2/3/Node-Require-and-Exports)的文章“Node.js,Require and Exports”。对于本书中的一个工作示例,请查看第二十二章中的 HackHall 示例。

摘要

养成良好的代码组织习惯和实践是极其重要的。没有他们,项目将很难维持。这在大型项目中更是如此。

在下一章中,我们将继续这个主题,但是将它应用于数据库。我们还将探索如何处理键(以及您不希望受到攻击的其他敏感信息),并在应用于 Express.js 应用时使用流。

十一、数据库、密钥和流提示

本章延续了上一章的主题,即在 Express.js 应用中更好地组织代码。本章提供了关于如何从另一个模块连接到数据库、访问应该保密的密钥和密码以及数据流的技巧。

在模块中使用数据库

本节讨论代码组织模式。这不是一个关于数据库和 Express.js 的详细教程。关于这方面的内容,请参考第二十章到第二十二章。

在我向您展示如何从另一个模块访问数据库连接之前,如果您将路由抽象到单独的app.js / server.js文件中,可能会用到这个模块,让我们先复习一些基础知识。

对于本机 Node.js MongoDB 驱动程序,Express.js 服务器需要等待连接被创建,然后才能使用数据库:

// ... Modules importing
var routes = require('routes');
var mongodb = require('mongodb');
var Db = mongodb.Db;
var db = new Db('test', new Server(dbHost, dbPort, {}));
// ... Instantiation
db.open(function(error, dbConnection){
  var app = express();
  app.get('/', routes.home);
  // ... Other routes
  app.listen(3000);
});

我们可以通过将路由和配置移到数据库回调之外来做得更好:

// ... Modules importing
var routes = require('routes');
var mongodb = require('mongodb');
var Db = mongodb.Db;
var db = new Db('test', new Server(dbHost, dbPort, {}));
// ... Instantiation
var app = express();
app.get('/', routes.home);
// ... Other routes

db.open(function(error, dbConnection){
app.listen(3000);
});

但是在数据库连接建立之后,仍然需要调用app.listen() 调用。

多亏了更高级的库,比如 Mongoskin、Monk 和 Mongoose,它们可以缓冲数据库请求,我们不需要将app.listen()调用放在db回调中。开发人员的任务可以像这样简单:

var express = require('express'),
  mongoskin = require('mongoskin'),
  bodyParser = require('body-parser');

var app = express();
app.use(bodyParser.json());
// ... Configurations and middleware

var db = mongoskin.db('localhost:27017/test', {safe:true});
app.get('/', function(req, res) {
  res.send('please select a collection, e.g., /collections/messages')
});
// ... Routes
app.listen(3000);

如果路由需要访问数据库对象,例如连接、模型等等,但是这些路由不在主服务器文件中,那么我们需要做的就是使用定制中间件将所需的对象附加到请求(即req)上:

app.use(function(req, res, next) {
  req.db = db;
  next();
});

这样在刚刚展示的自定义中间件之后声明的所有中间件和路由都会有req.db对象;req对象是Request对象的同一个实例。

或者,在导入或导出模块时,我们可以将需要的变量传递或接受到构造函数中。在 JavaScript/Node.js 中,对象是通过引用传递的,所以我们将在模块中处理原始对象。routes.js模块的例子如下:

module.exports = function(app){
  console.log(app.get('db')); *// app has everything we need!*
  // ... Initialize other objects as needed
  return {
    findUsers: function(req, res) {
    // ...
    },
    createUser: function(req, res) {
    // ...
    }
  } // for "return"
}

这是主文件:

var app = express();
// ... Configuration
app.set('db', db);
routes = require('./routes.js')(app);
// ... Middleware
app.get('/users', routes.findUser);
// ... Routes

或者,我们可以在变量中重构:

var app = express();
// ... Configuration
app.set('db', db);
Routes = require('./routes.js');
routes = Routes(app);
// ... Middleware
app.get('/users', routes.findUser);
// ... Routes

您可以尝试使用proexpressjs/ch11文件夹中的示例将数据传递给模块本身。为此,只需运行以下命令:

$ node -e "require('./routes-module-exports').FindStories('databases');"

proexpressjs/ch11文件夹运行它。该命令只是导入模块并从中调用一个方法。结果,您将看到在终端中打印出单词“databases”(或者您传递给FindStories的任何其他字符串)。

关于通过一个req对象将一个 Mongoose 数据库对象传递给 routes 的真实例子,请看一下第二十二章。

密钥和密码

对于数据库,典型的 web 服务可能需要通过用户名和密码连接到其他服务,对于第三方 API,则需要 API 密钥和秘密/令牌。正如您可能猜到的,将这些敏感数据存储在源代码中并不是一个好主意!解决这个问题的两种最普遍的方法是

  • JSON 文件
  • 环境变量

Image 注意这一节我们说的是 Node.js,不是浏览器 JavaScript。通常,您不希望在前端暴露您的密码和 API 密钥。

JSON 文件

JSON 文件方法 听起来很简单。我们只需要一个 JSON 文件。例如,假设我们在conf/keys.json中有一个本地数据库和两个外部服务,比如 HackHall 和 Twitter:

{
  "db": {
    "host": "http://localhost",
    "port": 27017,
    "username": "azat",
    "password": "CE0E08FE-E486-4AE0-8441-2193DF8D5ED0"
  },
  "hackhall": {
    "api_key": "C7C211A6-D8A7-4E41-99E6-DA0EB95CD864"
  },
  "twitter": {
    "consumer_key": "668C68E1-B947-492E-90C7-F69F5D32B42E",
    "consumer_secret": "4B5EE783-E6BB-4F4E-8B05-2A746056BEE1"
  }
}

Node.js 的最新版本允许开发者用require()函数导入 JSON 文件。为没有乱搞fs模块而欢呼!因此,主应用文件可能会使用这些语句:

var configurations = require('/conf/keys.json');
var twitterConsumerKey = configurations.twitter.consumer_key;

或者,我们可以用fs模块手动读取文件,并将流解析成 JavaScript 对象。自己试试这个。

至于对configurations的访问,如果我们可以使用app.set(name, value)全局共享这个配置对象就更好了:

app.set('configurations', configurations);

或者,使用中间件,传播到以下每个请求:

app.use(function(req, res, next) {
  req.configurations = configurations;
});

conf/keys.json添加到。gitignore防止跟踪和暴露文件。要添加它,只需创建一个新的系统文件.gitignore,并添加这一行:

conf/keys.json

如果您将您的密钥提交给 Git 一次,那么即使您删除了该文件,它们也会保留在历史记录中。从 Git 历史中删除敏感数据的解决方案很棘手。最好重新生成密钥以避免暴露。

将 JSON 配置文件传送到服务器时,问题仍然存在。这可以通过 SSH 和scp(安全复制)命令来完成:

$ scp [options] username1@source_host:directory1/filename1 username2@destination_host:directory2/filename2

比如$ scp ./keys.json azat@webapplog:/var/www/conf/keys.json

或者,您可以使用rsync,因为它只传输增量。例如:

$ rsync -avz ./keys.json azat@webapplog:var/www/conf

环境变量

第二种方法涉及到环境变量 (env vars)的使用。说明 env 变量最简单的方法是用前缀key=value开始脚本,例如$ NODE_ENV=test node app。这将填充process.env.NODE_ENV。试试这个脚本,它会将NODE_ENV打印出来:

$ NODE_ENV=test node -e 'console.log(process.env.NODE_ENV)'

为了将这些 var 交付/部署到远程服务器中,我们可以使用 Ubuntu 的/etc/init/nodeprogram.conf。凯文·范·松内维尔德的这篇简洁的教程提供了更多的细节:“在 Ubuntu 上运行 Node.js 作为服务”1

此外,还有一个 Nodejitsu 工具(http://www.nodejitsu.com)可以永远守护 Node 流程(http://npmjs.org/forever);GitHub: https://github.com/nodejitsu/forever

对于 Heroku 来说,将 env 变量与云同步的过程甚至更简单:在本地,我们将变量放入。env为工头 2 (自带 Heroku toolbelt)在项目文件夹中归档,然后用heroku-config ( https://github.com/ddollar/heroku-config推送到云端。更多信息在 Heroku 发展中心。 3

对于一个工作示例(显然没有敏感信息),看一看第二十二章。

Express.js 请求和响应对象分别是可读和可写的 Node.js 流。Streams 是在特定进程(读取、接收、写入、发送)实际结束之前处理大块数据的强大工具。这使得流在处理大量数据(如音频或视频)时非常有用。流的另一个例子是在执行大型数据库迁移时。

Image 提示关于如何使用 streams 的更多信息,substack 的 James Halliday ( http://substack.net/)提供了一些惊人的资源:stream-handbook ( https://github.com/substack/stream-handbook)和 stream-adventure ( https://npmjs.org/package/stream-adventure)。

下面是一个从proexpressjs/ch11/streams-http-res.js到普通响应的管道流的例子:

var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
    fs.createReadStream('users.csv').pipe(res);
});
server.listen(3000);

来自终端的带有 CURL 的 GET 请求如下所示:

$ curl http://localhost:3000

前面一行将导致服务器输出文件users.csv的内容;例如:

...
Stanton Botsford,Raina@clinton.name,619
Dolly Feeney,Aiden_Schaefer@carmel.tv,670
Oma Beahan,Mariano@paula.tv,250
Darrion Johnson,Miracle@liliana.com,255
Garth Huels V,Patience@leda.co.uk,835
Chet Hills II,Donna.Lesch@daniela.co.uk,951
Margarette Littel,Brenda.Prosacco@heber.biz,781
Alexandrine Schiller,Brown.Kling@jason.name,779
Concepcion Emmerich,Leda.Hudson@cara.biz,518
Mrs. Johnpaul Brown,Conrad.Cremin@tavares.tv,315
Aniyah Barrows,Alexane@daniela.tv,193
Okey Kohler PhD,Cordell@toy.biz,831
Bill Murray,Tamia_Reichert@zella.com,962
Allen O'Reilly,Jesus@joey.name,448
Ms. Bud Hoeger,Ila@freda.us,997
Kathryn Hettinger,Colleen@vincenza.name,566
...

结果也显示在图 11-1 中。

9781484200384_Fig11-01.jpg

图 11-1 。从 users.csv 文件运行流响应的结果

如果想创建自己的测试文件如users.csv,可以安装 faker . js(https://npmjs.org/package/Faker;GitHub: https://github.com/marak/Faker.js/)并重新运行seed-users.js文件:

$ npm install Faker@0.7.2
$ node seed-users.js

Express.js 实现在proexpressjs/ch11/stream-express-res.js中惊人地相似:

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

var app = express();

app.get('*', function (req, res) {
    fs.createReadStream('users.csv').pipe(res);
});

app.listen(3000);

请记住,请求是一个可读的流,而响应是一个可写的流,我们可以实现一个服务器,将 POST 请求保存到一个文件中。下面是proexpressjs/ch11/stream-http-req.js的内容:

var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
    if (req.method === 'POST') {
        req.pipe(fs.createWriteStream('ips.txt'));
    }
    res.end('\n');
});
server.listen(3000);

我们调用 Faker.js 来生成由名称、域、IP 地址、纬度和经度组成的测试数据。这一次,我们不会将数据保存到文件中,而是通过管道将它发送到 CURL。

下面是 Faker.js 脚本的一部分,它从proexpressjs/ch11/seed-ips.js向 stdout 输出 1000 条记录的 JSON 对象:

var Faker = require('Faker');
var body = [];

for (var i = 0; i < 1000; i++) {
  body.push({
    'name': Faker.Name.findName(),
    'domain': Faker.Internet.domainName(),
    'ip': Faker.Internet.ip(),
    'latitude': Faker.Address.latitude(),
    'longitude': Faker.Address.longitude()
  });
}
process.stdout.write(JSON.stringify(body));

为了测试我们的stream-http-req.js,让我们跑吧

$ node seed-ips.js | curl -d@- http://localhost:3000.

结果是一个 IP 数组,如图图 11-2 所示。

9781484200384_Fig11-02.jpg

图 11-2 。Node.js 服务器编写的文件的开头

让我们再一次将这个例子转换成一个 Express.js 应用:

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

app.post('*', function (req, res) {
   req.pipe(fs.createWriteStream('ips.txt'));
   res.end('\n');
});
app.listen(3000);

Image 提示在某些情况下,拥有不消耗太多资源的直通逻辑是很好的。为此,请通过(https://npmjs.org/package/through)查看模块;GitHub: https://github.com/dominictarr/through)。另一个有用的模块是 concat-stream(https://npmjs.org/package/concat-stream;GitHub: https://github.com/maxogden/node-concat-stream)。它允许流的连接。

摘要

到目前为止,我们已经介绍了从其他非app.js / server.js文件实现数据库连接的方法。我们还对我们的源库隐藏敏感信息,并流式传输数据。这些概念和方法在处理第十九章到第二十二章中的例子时会派上用场。

下一章将给出一些使用 Redis 和实现认证模式的 Express.js 技巧。


1

2

3

十二、Redis 和认证模式

本章讨论两个 Express.js 主题:Redis 和认证模式。Redis 是一个快速数据库,通常用于存储 Express.js 会话。

使用心得

Redis ( http://redis.io)经常在 Express.js 应用中用于会话持久性,因为将会话存储在物理存储中可以防止应用在系统重启或重新部署时丢失用户数据。它还支持使用多个 RESTful 服务器,因为它们可以连接到同一个 Redis 服务器。此外,Redis 可用于队列和调度任务(例如,电子邮件作业)。

Redis 本身是一项独立的服务。因此,要在 Express.js 中使用 Redis,我们需要两样东西:

  • Redis 服务器:可以监听特定端口并通过 Redis 控制台或应用访问的数据库服务器
  • Connect-redis :一个 NPM 模块(https://www.npmjs.org/package/connect-redis);GitHub: https://github.com/tj/connect-redis),使 Express.js 能够使用 redis 存储,并包含 Redis 模块(https://www.npmjs.org/package/redis;GitHub: https://github.com/mranney/node_redis)

要下载 Redis 2.6.7,请输入以下简单命令:

$ wget http://download.redis.io/releases/redis-2.6.7.tar.gz
$ tar xzf redis-2.6.7.tar.gz
$ cd redis-2.6.7
$ make

更多 Redis 说明,可以访问http://redis.io/download

要开始重定向,请按 enter 键

$ src/redis-server

要停止重复,只需按 Ctrl+C 即可。

要访问 Redis 命令行界面,请输入

$ src/redis-cli

以下是如何使用 Redis 管理 Express.js 会话的简单示例。

首先,要访问 Redis,使用connect-redis驱动程序。您可以使用ch12/package.json中熟悉的依赖项键/值对来实现这一点:

{
  "name": "redis-example",
  "dependencies": {
    "express": "4.8.1",
    "connect-redis": "2.1.0",
    "cookie-parser": "1.3.2",
    "express-session": "1.7.6"
  }
}

要使用 Redis 作为 Express.js 服务器中的会话存储(ch12/app.js),请输入以下内容:

var express = require('express');
var app = express();
var cookieParser = require('cookie-parser');
var session = require('express-session');
var RedisStore = require('connect-redis')(session);

app.use(cookieParser());
app.use(session({
  resave: true,
  saveUninitialized: true,
  store: new RedisStore({
    host: 'localhost',
    port: 6379
  }),
  secret: '0FFD9D8D-78F1-4A30-9A4E-0940ADE81645',
  cookie: { path: '/', maxAge: 3600000 }
}));

接下来,定义“/”路由,该路由将递增每个唯一会话的计数器。换句话说,如果我们关闭浏览器,停止 Express.js 服务器,等待一段时间,重新启动服务器,然后重新打开浏览器,只要带有会话 ID 的 cookie 没有过期或被删除,该值就会被保存并递增。此外,输出计数器和会话 ID:

app.get('/', function(request, response){
  console.log('Session ID: ', request.sessionID);
  if (request.session.counter) {
    request.session.counter = request.session.counter +1;
  } else {
    request.session.counter = 1;
  }
  response.send('Counter: ' + request.session.counter);
});

app.listen(3000);

现在,当你启动服务器时,它应该向你显示Counter: 1并且connect.sid cookie 应该有一个类似于下面的值(见图 12-1 ):

s%3AA3l_jSr25tbWWjRHot9sEUM5OApCn21R.qxxe7TLSaZBwuCGKmSfvI9jpVcnLyUrKMEkXxXMvAzM

9781484200384_Fig12-01.jpg

图 12-1 。Redis store Express.js 会话示例的输出

您的会话 ID 会有所不同,但格式和长度是相同的。让我们在浏览器或 Node.js 控制台中用decodeURIComponent()方法解码这个值(见图 12-2 ):

decodeURIComponent('s%3AA3l_jSr25tbWWjRHot9sEUM5OApCn21R.qxxe7TLSaZBwuCGKmSfvI9jpVcnLyUrKMEkXxXMvAzM')

9781484200384_Fig12-02.jpg

图 12-2 。解码 Express.js 会话 ID

s:.之间的值是会话 ID。将它与服务器日志中打印的值进行比较。它们应该匹配。

要再次检查计数器值是否确实存储在 Redis 中,而不是存储在其他地方,请复制会话 ID,并在新的终端窗口中打开 Redis 控制台

$ redis-cli

然后,键入以下命令来获取会话值:get sess:SESSION_ID,其中SESSION_ID是您的会话 ID;例如:

> get sess:A3l_jSr25tbWWjRHot9sEUM5OApCn21R

您应该看到带有会话值计数器的 JSON 对象的字符串,以及一些选项。例如,我的输出是

"{\"cookie\":{\"originalMaxAge\":3600000,\"expires\":\"2014-09-03T19:03:55.007Z\",\"httpOnly\":true,\"path\":\"/\"},\"counter\":1}"

您还可以使用> keys命令来获取存储的会话密钥列表(前缀为sess):

> keys sess*

正如本章开始时提到的,connect-redis 模块由 redis 模块提供支持。有了这个模块,Redis 可以作为一个平面的、独立的数据库使用。有趣的是,Redis 支持四种类型的数据:字符串、列表、集合和散列。

Image 提示要深入研究 Redis,可以在http://try.redis.io找到一个互动教程。

身份验证模式

最常见的认证类型是要求用户名和密码的组合。我们可以对照数据库检查匹配,然后在会话中存储一个authenticated=true标志。Express.js 为该代理发出的每个其他请求自动存储的会话数据:

app.use(function(req, res, next) {
  if (req.session && req.session.authenticated)
     return next();
  else {
    return res.redirect('/login');
  }
}

如果我们需要额外的用户信息,它也可以存储在会话中:

app.post('/login', function(req, res) {
  *// Check the database for the username and password combination*
  *// In a real-world scenario you would salt the password*
db.findOne({username: req.body.username,
    password: req.body.password},
    function(error, user) {
      if (error) return next();
      if (!user) return next(new Error('Bad username/password');
      req.session.user = user;
      res.redirect ('/protected_area');
    }
  );
});

对于 salt 密码的工具,看一下bcryptjs ( https://www.npmjs.org/package/bcryptjs),它与bcrypt兼容,但不需要编译,因为它完全在 JavaScript/Node.js 上运行。与其他库或您自己的哈希/salt 实现相比,bcryptjs(和 bcrypt)的另一个很酷的事情是,salt 是密码的一部分,所以您不需要在数据库中为每个用户存储额外的 salt 字段。例如:

var bcrypt = require('bcryptjs');
bcrypt.hash('pr0expressr0cks!, 8, function(err, hash) {
// ... Store the hash, which is a password and salt together
});

使用异步 bcryptjs 方法,因为它们会非常慢(越慢越好保护!).

与第三方的认证通常通过 OAuth 来完成。对于oauth模块(https://www.npmjs.org/package/oauth)的工作示例;GitHub:https://github.com/ciaranj/node-oauth)——作者提供了一些文档——查看第二十二章中的 HackHall 示例。

OAuth 1.0/2.0 要求回调路由,以便用户重定向回我们的站点。使用 Express.js,这是毫不费力的。此外,还有完全自动化的解决方案来处理一切(数据库、签名、路由等)。):Everyauth ( https://npmjs.org/package/everyauth)和 Passport ( https://npmjs.org/package/passport)。

Image 提示要想快速了解适用于 Node.js 的 OAuth,可以考虑阅读我的书,《用 Node.js 介绍 OAuth:Twitter API OAuth 1.0、OAuth2.0、OAuth Echo、Everyauth 和 OAuth 2.0 服务器示例 (webapplog.com,2014)。

摘要

在这一章中,我们安装并使用了 Redis,这样会话信息就可以持久,也就是说,如果服务器停机,它也不会丢失。Redis(或任何其他持久性存储)允许我们在多个服务器之间共享会话。然后我们讲述了用中间件实现认证。

在下一章中,我们将讨论集群多线程,继续让您的 Express.js 应用产品化的主题。

十三、集群和多线程

有很多批评者反对使用 Node.js,他们的许多观点都源于一个神话,即基于 Node.js 的系统是单线程的。事实远非如此——使用cluster模块,我们可以毫不费力地派生一个 Node.js 流程来创建多个流程。是的,每个进程仍将是单线程的,并且会被不合适的同步代码或一些费力的进程(如密码散列)阻塞。然而,现在由几个进程组成的整个系统不会被阻塞。

在 web 应用的情况下,进程可以监听同一个端口,从而确保如果第一个进程繁忙,请求将由第二个(或第三个或第四个)进程处理。通常情况下,我们会根据机器上的 CPU 数量生成尽可能多的进程,这样我们就可以利用机器上的所有 CPU。

多线程的例子

下面是一个在四个进程上运行的 Express.js 应用的工作示例。其中一个是所谓的师傅工序,另外三个是工人工序。master 负责管理和监控 worker,而 worker 本身就是一个独立的 Express.js app。master 和 worker 的代码包含在同一个文件中。

在文件的开头,我们导入依赖关系:

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
var express = require('express');

模块有一个属性告诉我们这个进程是主进程还是子进程。我们使用该属性生成四个工人(默认工人将使用相同的文件,但这可以用setupMaster()方法覆盖)。 1 除此之外,我们还可以附加事件监听器,接收来自工作器的消息(例如'kill')。

if (cluster.isMaster) {
  console.log('Fork %s worker(s) from master', numCPUs)
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  };
  cluster.on('online', function(worker) {
    console.log('Worker is running on %s pid', worker.process.pid)
  });
  cluster.on('exit', function(worker, code, signal) {
    console.log('Worker with %s is closed', worker.process.pid );
  });

worker 代码只是一个稍加改动的 Express.js 应用。我们正在获取进程 ID ,即 PID(下面的代码延续了前面的代码片段):

} else if (cluster.isWorker) {

现在我们编写一个端口为 3000 的 Express.js 服务器代码:

  var port = 3000;
  console.log('Worker (%s) is now listening to http://localhost:%s',
    cluster.worker.process.pid, port);
  var app = express();

服务器有一个包罗万象的路由,它将打印 PID:

  app.get('*', function(req, res) {
    res.send(200, 'cluster '
      + cluster.worker.process.pid
      + ' responded \n');
  })
  app.listen(port);
}

请在您的项目中随意使用ch13/cluster.js的完整源代码:

var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var express = require('express');

if (cluster.isMaster) {
  console.log('Fork %s worker(s) from master', numCPUs);
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('online', function(worker) {
    console.log('Worker is running on %s pid', worker.process.pid);
  });
  cluster.on('exit', function(worker, code, signal) {
    console.log('Worker with %s is closed', worker.process.pid );
  });
} else if (cluster.isWorker) {
  var port = 3000;
  console.log('Worker (%s) is now listening to http://localhost:%s',
    cluster.worker.process.pid, port);
  var app = express();
  app.get('*', function(req, res) {
    res.send(200, 'cluster ' + cluster.worker.process.pid + ' responded \n');
  });
  app.listen(port);
}

像往常一样,要启动一个应用,运行$ node cluster。应该有四个(或者两个,取决于你机器的架构)进程,日志可能看起来像这样(见图 13-1 ):

worker is running on 15279 pid
worker is running on 15277 pid
...

9781484200384_Fig13-01.jpg

图 13-1 。用集群启动四个进程

有不同的进程监听同一个端口并响应我们。例如,响应可能如下(见图 13-2 ):

cluster 15278 responded
cluster 15280 responded

9781484200384_Fig13-02.jpg

图 13-2 。服务器响应由不同的进程呈现

Image 提示如果你更喜欢现成的解决方案而不是低级库(比如cluster),那就去看看易贝创建并使用的真实世界的生产库:cluster2(https://www.npmjs.org/package/cluster2;GitHub: https://github.com/ql-io/cluster2)。

再次查看ch13/cluster.js,注意我们可以将实际的 Express.js 应用抽象到一个单独的文件中。这是一件好事,因为它保持了两个不同逻辑单元、集群和 Express.js 应用之间的分离。例如,前面列出的集群文件可以重构为:

var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var app = require('./app'); // <- THIS FILE!

if (cluster.isMaster) {
  console.log('Fork %s worker(s) from master', numCPUs);
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('online', function(worker) {
    console.log('Worker is running on %s pid', worker.process.pid);
  });
  cluster.on('exit', function(worker, code, signal) {
    console.log('Worker with %s is closed', worker.process.pid );
  });
} else if (cluster.isWorker) {
  var port = 3000;
  console.log('Worker (%s) is now listening to http://localhost:%s',
    cluster.worker.process.pid, port);
  app.listen(port);
}

摘要

在这一章中,我们探索了一种实现系统的方法,该系统有一个主 Node 和多个工作 Node,它们是在同一个端口上监听的 Express.js 应用。这是一个很有价值的技巧,你可以用它来减少你的服务器负载,让你用更少的机器提供更多的流量,这反过来又会节省你的钱。

在下一章,我们将介绍如何将 Stylus、Less 和 Sass CSS 库应用于 Express.js 服务器。


1

十四、应用 Stylus、Less 和 Sass

对于任何 web 项目来说,使用级联样式表(CCS)都是必须的,但是在复杂的项目中,CSS 样式表的编写和管理非常繁琐。这主要是因为 CSS 不是真正的编程语言。CSS 没有继承、变量或函数。对于普通的 CSS 资源来说,代码重用和可维护性是一个棘手的问题。对于大型项目或旨在在不同项目间共享的 CSS 库来说尤其如此。

解决使用普通 CSS 的痛苦的方法是使用另一种更好的语言,这种语言在构建时(用于生产)或在运行中(用于开发)被编译到 CSS 中。对于后者,Express.js 使用中间件,所以每次网页请求 CSS 资产时,框架都会将更好的 CSS 代码转换为普通 CSS。一些更好的 CSS 库包括 Stylus(我最喜欢的)、Less(Twitter Bootstrap 团队最喜欢的)和 Sass。

因此,Stylus、Less 和 Sass 带来了急需的可重用性(混合、扩展、变量等)。)转换为样式表,这样我们作为开发人员就可以更高效、更容易地重用 CSS 代码。让我们看看当我们使用 Express.js 时,如何利用这一令人惊叹的资源。

关于这些库及其特性的详细介绍超出了本章的范围,因为这些特性非常多,并且将来可能会发生变化(所以最好参考官方文档,本章末尾提供了相关链接)。本章教你如何将库插入 Express.js 应用。这些库本身(在很大程度上)是向后兼容普通 CSS 的;因此,他们有一个宽容、平坦的学习曲线。

Image 提示 Express Generator v4.2.0 支持 Less 和 Stylus 库,但不支持 Sass。

唱针

Stylus 是 Express.js 的兄弟,是最常用的 CSS 框架。 1 我最喜欢它的原因和我喜欢 Jade 的原因一样:它通过最小化工作所需的字符数来实现雄辩。我必须键入的字符越少,我出错的机会就越少,我就有更多的时间来解决实际问题。

要安装 Stylus,请键入并运行

$ npm install stylus@0.42.3 --save.

使用带有app.use和文件夹名称的 Stylus。然后,要应用static中间件,将它包含在您的服务器文件中:

//... Import dependencies and instantiate app
app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
//... Routes and app.listen()

*.styl文件放入我们公开的文件夹中(例如publicpublic/css,并使用*.css扩展名将它们包含在 Jade(或任何其他)模板中。对,没错!文件是*.styl,但是在 Jade 或 HTML 代码中,我们要求的是*.css文件。Express.js 会变魔术的!例如,我们在public/stylesheets/中有style.styl,所以我们在 template/HTML 中使用/stylesheets/style.css:

//...
  head
    link(rel='stylesheet', href='/stylesheets/style.css')
//...

或者,在您选择的任何其他模板或纯 HTML 文件中,输入以下内容:

<link rel="stylesheet" href="/stylesheets/style.css"/>

对于从头开始创建的项目,您可以使用生成器命令:

$ express -c stylus express-app-name command

您可以在ch14/stylus文件夹和 GitHub ( https://github.com/azat-co/proexpressjs)上找到支持 Stylus 的项目。

较少的

要在 Express.js 中少用,我们需要less-middleware,它是一个外部 NPM 模块( https://www.npmjs.org/package/less-middleware ) :

$ npm install less-middleware@1.0.0 --save

然后,我们需要在static和路由(ch14/less/app.js)前添加less-middleware :

//... Import dependencies and instantiate app
app.use(require('less-middleware')(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));
//... Routes and app.listen()

假设较少的文件在public/css中,我们可以链接*.css文件,剩下的会自动处理;例如,一个 Jade 模板可能会这样使用:

//...
  head
    link(rel='stylesheet', href='/stylesheets/style.css')
//...

或者,在您选择的任何其他模板中,或者在纯 HTML 文件中,使用以下内容:

<link rel="stylesheet" href="/stylesheets/style.css"/>

对于从头开始创建的项目,您可以使用$ express -c less express-app-name命令。

你可以在ch14/less文件夹和 GitHub ( https://github.com/azat-co/proexpressjs)上找到这个功能较少的项目。

厚颜无耻

要在 Express.js 中使用 Sass ,我们需要 node-sass,它是一个外部 NPM 模块(https://npmjs.org/package/node-sass;GitHub: https://github.com/sass/node-sass ):

$ npm install node-sass@0.6.7 --save

这是我们用于 Sass ( ch14/sass/app.js)的 Express.js 插件:

// ... Import dependencies and instantiate app
app.use(require('node-sass').middleware({
  src: __dirname + '/public',
  dest: __dirname + '/public',
  debug: true,
  outputStyle: 'compressed'
}));
app.use(express.static(path.join(__dirname, 'public')));
// ... Routes and app.listen()

Jade 模板还导入了*.css文件:

link(rel='stylesheet', href='/stylesheets/style.css')

支持 Sass 的项目在ch14/sass文件夹中,也在 GitHub ( https://github.com/azat-co/proexpressjs)上。

摘要

我们在本章中提到的 CSS 框架的大部分代码都与普通 CSS 兼容。换句话说,普通的 CSS 代码在 Stylus、Sass 或更少的软件中也能很好地工作。因此,在您的项目中包含这样一个 CSS 框架没有任何害处。

使用这种框架的好处很多,包括混合、变量、继承等的可用性。这些特性相当广泛,这也是它们没有被包括在本书中的原因。有关完整信息,请参考 Stylus(http://learnboost.github.io/stylus)、Sass ( http://sass-lang.com)和 Less ( http://lesscss.org)的官方在线文档。

下一章提供了一些关于 Express.js 和安全性的重要提示。


1

十五、安全提示

本章中的一组技巧涉及 Express.js 应用的安全性。安全性通常是一个被忽视的话题,直到发布前的最后一刻才被提及。显然,这种事后才考虑安全性的方法容易给攻击者留下漏洞。更好的方法是从头开始考虑和实现安全问题。

浏览器 JavaScript 因安全漏洞而声名狼藉,所以我们需要尽可能保证 Node.js 应用的安全!通过本章介绍的简单修改和中间件,您可以毫不费力地解决一些基本的安全问题。

本章涵盖以下主题:

  • 跨站点请求伪造(CSRF)
  • 流程权限
  • HTTP 安全标头
  • 输入验证

跨站请求伪造

CSRF 和csurf中间件在第四章中有简要介绍。有关 CSRF 的定义和解释,请参考该章。

csurf中间件完成了匹配来自请求的传入值的大部分工作。然而,我们仍然需要公开响应中的值,并在模板(或 JavaScript XHRs)中将它们传递回服务器。首先,我们像安装其他依赖项一样安装csurf模块

$ npm install csurf@1.6.0

然后,我们将csurfvar csrf = require('csurf'); app.use()一起应用,如第四章所述:

app.use(csrf());

csrf 必须在之前cookie-parserexpress-session,因为它依赖于这些中间件(即安装、导入和应用必要的模块)。

实现验证的方法之一是使用定制的中间件将 CSRF 令牌传递给所有使用response.local的模板。这个定制中间件必须在路由之前(大多数中间件语句都是这种情况):

app.use(function (request, response, next) {
  response.locals.csrftoken = request.csrfToken();
  next();
});

换句话说,我们手动促进了令牌在主体(如本例所示)、查询或头中的出现。(根据你的偏好或客户之间的合同,你可以使用查询或标题。)

因此,要将模板中的值呈现为隐藏的表单值,我们可以使用

input(type="hidden", name="_csrf", value="#{csrftoken}")

hidden输入字段将把令牌值添加到提交的表单数据中,便于将 CSRF 令牌与其他字段(如emailpassword)一起发送到/login路线。

以下是文件ch15/index.jade中完整的 Jade 语言内容:

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/css/style.css')
  body
    if errors
      each error in errors
        p.error= error.msg
    form(method="post", action="/login")
      input(type="hidden", name="_csrf", value="#{csrftoken}")
      input(type="text", name="email", placeholder="hi@webapplog.com")
      input(type="password", name="password", placeholder="Password")
      button(type="submit") Login
    p
      include lorem-ipsum

要在ch15/app.js中观看 CSRF 的演示,像通常使用$ node app一样启动服务器。然后导航到位于http://localhost:3000的主页。你应该在表单的hidden字段看到令牌,如图图 15-1 所示。请记住,您的令牌值会有所不同,但其格式是相同的。

9781484200384_Fig15-01.jpg

图 15-1 。将来自csurf模块的 CSRF 令牌插入表单,稍后发送到/登录路由

对于对主页(/)或页面刷新的每个请求,您将获得一个新令牌。但是,如果您增加令牌来模拟攻击(您可以在 Chrome 开发人员工具中直接进行),您将得到以下错误:

403 Error: invalid csrf token
  at verifytoken...

流程权限

显然,以 root 用户身份运行 web 服务通常不是一个好主意。运营开发人员可以利用 Ubuntu 的authbind 1 绑定到特权端口(例如,HTTP 的 80 和 HTTPS 的 443),而无需授予 root 访问权限。

或者,也可以在绑定到端口后取消特权。这里的想法是,我们将 GID(组 ID)和 UID(用户 ID)的值传递给 Node.js 应用,并使用解析后的值来设置流程的组标识和用户标识。这在 Windows 上不起作用,所以你可能想使用if / elseprocess.platformNODE_ENV来使你的代码跨平台。

下面是一个通过使用来自process.env.GIDprocess.evn.UID环境变量的属性设置 GID 和 UID2来删除特权的示例:

// ... Importing modules
var app = express();
// ... Configurations, middleware and routes
http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port "
    + app.get('port'));
    process.setgid(parseInt(process.env.GID, 10));
    process.setuid(parseInt(process.env.UID, 10));
});

HTTP 安全标头

叫做头盔 ( https://www.npmjs.org/package/helmet)的 Express.js 中间件;GitHub: https://github.com/helmetjs/helmet)是一个安全相关中间件的集合,它提供了 Recx 文章《免费提高 Web 应用安全性的七个 Web 服务器 HTTP Headers 中描述的大部分安全头。 3 撰写本文时,头盔的版本是 0.4.1,包括以下中间件:

  • crossdomain:用于/crossdomain.xml防止 Flash 加载某些不想要的内容(见http://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html)
  • csp:增加内容安全策略,允许载入内容白名单(见http://content-security-policy.comhttp://www.html5rocks.com/en/tutorials/security/content-security-policy)
  • hidePoweredBy:删除X-Powered-By以防止暴露您正在使用 Node.js 和 Express.js
  • hsts:增加 HTTP 严格传输安全,防止您的网站被 HTTP(而不是 HTTPS)查看
  • ienoopen:设置 IE 8+的X-Download-Options头,防止 IE 浏览器加载不可信的 HTML(见http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx)
  • nocache : Cache-ControlPragma头停止缓存(有助于清除用户浏览器的旧错误)
  • nosniff:设置适当的X-Content-Type-Options头以减少 MIME 类型嗅探(见http://msdn.microsoft.com/en-us/library/gg622941%28v=vs.85%29.aspx)
  • xframe:将X-Frame-Options标题设置为DENY,防止你的资源被放入框架进行点击劫持攻击(见https://en.wikipedia.org/wiki/Clickjacking)
  • xssFilter:设置 IE8+和 Chrome 的X-XSS-Protection头,防止 XSS 攻击(见http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx)

要安装helmet,只需运行

$ npm install helmet@0.4.1

像往常一样导入模块:

var helmet = require('helmet');

然后在路由之前应用中间件。默认用法如下(ch15/app.js):

app.use(helmet());

图 15-2 显示了helmet v0.4.1 HTTP 响应在使用默认选项时的样子:

9781484200384_Fig15-02.jpg

图 15-2 。头盔 v0.4.1 HTTP 响应与默认选项一起使用

输入验证

当您使用 body-parser 或 query 作为输入数据时,Express.js 不执行任何用户/客户端输入清理或验证。众所周知,我们永远不应该相信输入。恶意代码可以插入(XSS 或 SQL 注入)到您的系统中。例如,当您在页面上打印该字符串时,您视为良性字符串的浏览器 JavaScript 代码可能会变成攻击(特别是如果您的模板引擎没有自动转义特殊字符的话!).

第一道防线是在接受外部数据的路由上用正则表达式手动检查数据。额外的“防御”可以添加在对象关系映射层上,比如 Mongoose 模式(见第二十二章)。请记住,前端/浏览器验证的执行仅仅是为了可用性的目的(也就是说,它更加用户友好)——它并不能保护你的网站免受任何攻击。

例如,在ch15/app.js中,我们可以实现在email字段、if-else语句和test()方法上使用 RegExp 模式的验证,将错误消息附加到errors数组,如下所示:

app.post('/login-custom', function(request, response){
  var errors = [];
  var emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  if (!request.body.password) errors.push({msg: 'Password is required'});
  if (!request.body.email || !emailRegExp.test(request.body.email) ) errors.push({msg: 'A valid email is required'});
  if (errors)
    response.render('index', {errors: errors});
  else
    response.render('login', {email: request.email});
});

随着您添加更多要验证的路径和输入字段,您将会得到更多的 RegExp 模式和if / else语句。虽然这比没有验证要好,但是推荐的方法是编写自己的模块或者使用express-validator??。

要安装express-validator,请运行:

$ npm install express-validator@2.4.0

ch15/app.js中导入express-validator:

var validator = require('express-validator');

然后在 body-parser后应用express-validator :

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(validator());

现在,在请求处理程序中,我们可以访问request.assertrequest.validationErrors() :

app.post('/login', function(request, response){
  request.assert('password', 'Password is required').notEmpty();
  request.assert('email', 'A valid email is required').notEmpty().isEmail();
  var errors = request.validationErrors();
  if (errors)
    response.render('index', {errors: errors});
  else
    response.render('login', {email: request.email});
});

index.jade文件简单地打印来自数组的错误,如果有的话:

if errors
  each error in errors
    p.error= error.msg

并且login.jade模板打印电子邮件。仅当验证成功时,才会呈现该模板。

p= email

要进行演示,请转到主页并尝试输入一些数据。如果有错误,会显示错误首页,如图图 15-3 所示。双重“需要有效的电子邮件”消息来自于这样一个事实,即我们对email字段有两个断言(notEmptyisEmail)并且当email字段为空时都失败

9781484200384_Fig15-03.jpg

图 15-3 。使用 express-validator 断言表单值的错误消息

摘要

安全是最重要的,但经常被忽视。在发展的早期阶段尤其如此。典型的思考过程是这样的:让我们专注于提供更多的功能,然后当我们即将发布时,我们会考虑安全性。这个决定通常是善意的,但很少按计划进行。结果,系统的安全性受到损害。

有了中间件库,比如csurfhelmetexpress-validator,我们可以在不增加太多开发周期的情况下获得良好的基本安全性。

在下一章,我们将改变思路,介绍一些使用 Express.js 的方法。用于反应式(即实时更新)视图的 IO 库。


1

2

3

十六、SocketIO 和 Express.js

SocketIO ( http://socket.io)是一个库,它提供了在客户机和服务器之间实时建立双向通信的能力。这种双向通信是由 WebSocket 技术驱动的。

WebSocket 技术在大多数现代浏览器中都可以使用,最简单的概念化方法是想象浏览器和服务器之间的持续连接,而不是传统的零星 HTTP 请求。WebSocket 和传统 HTTP 请求的另一个区别是,前者是一个双向通道,这意味着服务器可以发起数据传输。这对于实时更新页面非常有用。

WebSocket 并不是实时类系统的唯一选择。您可以使用轮询实现接近实时的系统,这是指浏览器代码在很短的时间间隔内(例如,100 毫秒)发出大量 HTTP 请求。和 SocketIO 不是唯一可以用来实现 WebSocket 的库。在 Practical Node.js (Apress,2014)中,我展示了一个使用ws作为服务器库和一个本地(根本没有库)浏览器 API 的例子。但是,使用 Socket 的好处。IO 在于它具有广泛的、跨平台的、跨浏览器的支持;WebSocket 不可用时回退到轮询;并使用事件作为它的主要实现模式。

插座全覆盖。IO 图书馆值得拥有自己的书。然而,用 Express.js 开始使用是如此的酷和容易,以至于我包含了这一章来向你展示一个基本的例子。

使用插座。超正析象管(Image Orthicon)

该示例将实时反向回显(浏览器到服务器,然后返回)我们的输入。这意味着我们将构建一个 Express.js 服务器,其中包含一个包含表单输入字段的 web 页面。该网页还将有一些前端 JavaScript 代码,在我们键入时将输入字段字符实时传输到服务器。服务器将反转字符串并将其发送回浏览器,我们的前端 JavaScript 代码将最终字符串打印到浏览器控制台。我们将使用插座。所有这些功能的 IO 方法和事件监听器。

在最终产品中,如果您键入“!stekcoS,”应用会将其转换为“插座!”如图图 16-1 所示。并且将显示浏览器控制台输出

!
sending ! to server
received !
!
sending !
received !
!s
sending !s to server
received s!
...

9781484200384_Fig16-01.jpg

图 16-1 。的投入!stekcoS 出品插座!

可以从一个$ express socket创建的 fresh Express.js app 开始,也可以从ch16/socket文件夹下载源代码。然后,用$ cd socket && npm install安装依赖项。

Image 提示如果需要 Express.js 3.x 的例子,可以参考这个 app 为 Express.js 3.3.5 编写的版本。源代码在 GitHub repo: https://github.com/azat-co/proexpressjs/tree/master/ch16/socket-express3

要包含 Socket.io,我们可以使用:

$ npm install socket.io@1.1.0 --save

或者,我们可以使用package.json:

{
  "name": "socket-app",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "errorhandler": "1.1.1",
    "express": "4.8.1",
    "jade": "1.5.0",
    "morgan": "1.2.2",
    "serve-favicon": "2.0.1",
    "socket.io": "1.1.0"
  }
}

插座。从某种意义上来说,IO 可以被认为是另一个服务器。我们可以通过将 Express.js app对象传递给createServer()方法,然后调用套接字来重构自动生成的 Express.js 代码。木卫一listen()方法上server物体:

var server = http.createServer(app);
var io = require('socket.io').listen(server);

//...
server.listen(app.get('port'), function(){
  console.log('Express server listening on port '
  + app.get('port'));
});

io对象被实例化后,下面的代码可以用来建立一个连接:

io.sockets.on('connection', function (socket) {

一旦建立了连接——我们知道这是因为我们在回调中——我们附加了messageChange事件监听器,它将由浏览器上的用户操作触发:

socket.on('messageChange', function (data) {
  console.log(data);

一旦消息被更改(例如,用户输入了一些内容),我们可以反转字符串并以带有socket.emit():receive事件的形式将其发送回来

    socket.emit('receive',
    data.message.split('').reverse().join('') );
  })
});

下面是ch16/socket/app.js的全部内容:

var http = require('http'),
  express = require('express'),
  path = require('path'),
  logger = require('morgan'),
  favicon = require('serve-favicon'),
  errorhandler = require('errorhandler'),
  bodyParser = require('body-parser');

var app = express();

app.set('view engine', 'jade');
app.set('port', process.env.PORT || 3000);
app.use(logger('combined'));
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(express.static('public'));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

app.get('/', function(request, response){
  response.render('index');
});

app.use(errorhandler());

var server = http.createServer(app);
var io = require('socket.io').listen(server);

io.sockets.on('connection', function (socket) {
  socket.on('messageChange', function (data) {
    console.log(data);
    socket.emit('receive', data.message.split('').reverse().join('') );
  });
});

server.listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

最后,我们的应用在index.jade中需要一些前端程序,以输入框和 JavaScript 代码的形式发送和接收用户输入。输入框可以这样实现:

  input(type='text', class='message', placeholder='what is on your mind?', onkeyup='send(this)')

在每个按键事件中,它都调用send()函数,但是在我们编写这个函数之前,让我们包含这个套接字。IO 库:

  script(src="/socket.io/socket.io.js")

socket.io.js文件不是您需要下载的真实文件,例如 jQuery 文件。Node.js 套接字。IO 服务器将“自动”提供这个文件

现在,我们可以连接到 socket 服务器并附加receive事件监听器:

var socket = io.connect('http://localhost');
socket.on('receive', function (message) {
  console.log('received %s', message);
  document.querySelector('.received-message').innerText = message;
});

document.querySelector只是现代浏览器对 jQuery 选择器的模拟。它只是给了我们一个 HTML 元素,这样我们就不需要依赖 jQuery 了。我们使用innerText属性在页面上显示新的文本。

send函数发出将消息传递给服务器的事件messageChange。这种情况在每次击键时都会发生,因此变化会实时出现。

var send = function(input) {
  console.log(input.value)
  var value = input.value;
  console.log('sending %s to server', value);
  socket.emit('messageChange', {message: value});
    }

以下是ch16/socket/index.jade的完整源代码:

extends layout

block content
  h1= title
  p Welcome to
    span.received-message #{title}
  input(type='text', class='message', placeholder='what is on your mind?', onkeyup='send(this)')
  script(src="/socket.io/socket.io.js")
  script.
    var socket = io.connect('http://localhost');
    socket.on('receive', function (message) {
      console.log('received %s', message);
      document.querySelector('.received-message').innerText = message;
    });
    var send = function(input) {
      console.log(input.value)
      var value = input.value;
      console.log('sending %s to server', value);
      socket.emit('messageChange', {message: value});
    }

运行应用

现在一切都应该准备好了,你可以启动应用了($ node app)。转到主页(http://localhost:3000)输入“!stekcoS”,应用会将其转换为“插座!”如前图 16-1 所示。

当您查看浏览器输出时,您可能会认为代码只是在客户端进行了转换,而没有服务器的参与。事实并非如此,证据在服务器日志中,你会看到转换实际上发生在服务器上(见图 16-2 )。

9781484200384_Fig16-02.jpg

图 16-2 。Express.js 服务器实时捕捉和处理输入

更多 SocketIO 示例,转到http://socket.io/#how-to-use

摘要

同样,您已经看到了 Express.js 与另一个库的无缝集成。在这个双通道通信的简短示例中,我们使用了来自套接字的事件侦听器。IO 库。该库兼容 Node.js 和浏览器 JavaScript,这使得它更易于使用。SocketIO 为实时应用提供了良好的跨浏览器支持。它与 Express.js 堆栈无缝集成,与使用 WebSocket 的原生浏览器 API 相比具有许多优势。

在下一章,我们将讨论如何使用域来更好地处理异步错误。

十七、domain和 Express.js

domain模块是一个核心 Node.js 模块(http://nodejs.org/api/domain.html),它通过使用,帮助开发人员跟踪和隔离错误,这通常是一项艰巨的任务。把域想象成一个更聪明的版本try / catch语句 1

定义问题

为了说明异步代码可能给错误处理和调试带来的障碍,请看这段同步代码,它显示了“Custom Error: Fail!”正如我们所料:

try {
  throw new Error('Fail!');
} catch (e) {
  console.log('Custom Error: ' + e.message);
}

现在,让我们以setTimeout()函数的形式添加异步代码,这将从 10 到 100(来自文件ch17/async-errors.js)随机延迟执行毫秒数:

try {
setTimeout(function () {
    throw new Error('Fail!');
  }, Math.round(Math.random()*100));
} catch (e) {
  console.log('Custom Error: ' + e.message);
}

而不是“自定义错误:失败!”消息,我们看到一个标准的、未处理的错误(“错误:失败!”)可能是这样的:

/Users/azat/Documents/Code/proexpressjs/ch17/async-errors.js:4
      throw new Error("Fail!");
            ^
Error: Fail!
    at null._onTimeout (/Users/azat/Documents/Code/proexpressjs/ch17/async-errors.js:4:13)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因此,try / catch在异步错误时失败。一个好的经验法则是仅对同步 JavaScript/Node.js 代码使用try / catch

Image 提示 try / catch也可能会慢一些,尤其是在内部定义函数的时候,因为 JavaScript 引擎无法提前优化代码。关于try / catch含义的更多信息,请参见https://github.com/joyent/node/wiki/Best-practices-and-gotchas-with-v8http://jsperf.com/try-catch-performance-overhead

探索一个基本的领域示例

在我们深入研究domain和 Express.js 之前,让我们先来探索一下基本的域示例(ch17/basic.js)。在其中,我们实例化了域对象:

var domain = require('domain').create();

然后,我们附加一个事件监听器,它告诉我们如何处理这个错误:

domain.on('error', function(error){
  console.log(error);
});

大部分代码和容易出错的代码被回调到domain.run() :

domain.run(function(){
  throw new Error('Failed!');
});

现在,让我们通过用域(ch17/basic-timeout.js)替换try / catch来修改setTimeout()示例:

var domain = require('domain');
var d = domain.create();
d.on('error', function(e) {
   console.log('Custom Error: ' + e);
});
d.run(function() {
  setTimeout(function () {
    throw new Error('Failed!');
  }, Math.round(Math.random()*100));
});

当我们用$ node basic-timeout.js运行它时,输出是

Custom Error: Error: Failed!

编写领域应用

了解了领域的基础知识后,让我们来探索一下ch17/domain中的应用。它有两条路线,/ error-domain/ error-no-domain 。从路线的名称,你可以猜测他们都将崩溃。域路由(/error-domain)将发回一些 JSON,而非域路由(/error-no-domain)将使用来自errorhandler模块的标准错误处理中间件。

这是域驱动的容易出错的路线:

app.get('/error-domain', function (req, res, next) {
  var d = domain.create();
  d.on('error', function (error) {
    console.error(error.stack);
    d.exit()
    res.status(500).send({'Custom Error': error.message});
  });
  d.run(function () {
    // Error prone code goes here
    throw new Error('Database is down.');
  });
});

我们不必在每条路由中都使用域;例如,/error-no-domainerrorhandler模块的中间件使用标准的 Express.js 方法:

app.get('/error-no-domain', function (req, res, next) {
  // Error prone code goes here
  next(new Error('Database is down.'));
});

我们添加了一些定制逻辑,通过使用domain.active ( http://nodejs.org/api/domain.html#domain_domain_enter)来区分域和非域案例(路由):

app.use(function (error, req, res, next) {
  console.log(domain)
  if (domain.active) {
    console.info('Caught with domain', domain.active);
    domain.active.emit('error', error);
  } else {
    console.info('No domain');
    defaultHandler(error, req, res, next);
  }
});

当你用$ node app.js运行应用并转到http://localhost:3000/error-domainhttp://localhost:3000/error-no-domain时,你会分别看到 JSON 页面和 HTML 页面。在服务器日志上,你会看到/error-domain的“带域捕获”和/error-no-domain的“无域”。

请注意,在这两种情况下,Express.js 应用都优雅地处理了错误,并且没有导致崩溃。到目前为止,您可能想知道,如果标准的 Express.js 可以工作,为什么还要在 domain 上经历这么多麻烦。上例中的路由在“表面”级别抛出错误,即同步错误。然而,大多数事情都发生在异步代码中。还记得本章前面的setTimeout例子吗?它可以模拟数据库调用,因此让我们通过在两条路由中添加异步错误来使我们的示例更加真实:

app.get('/error-domain', function (req, res, next) {
  var d = domain.create();
  d.on('error', function (error) {
    console.error(error.stack);
    d.exit()
    res.status(500).send({'Custom Error': error.message});
  });
  d.run(function () {
    // Error-prone code goes here
    // throw new Error('Database is down.');
    setTimeout(function () {
      throw new Error('Database is down.');
    }, Math.round(Math.random()*100));
  });
});

app.get('/error-no-domain', function (req, res, next) {
  // Error-prone code goes here
  // throw new Error('Database is down.');
  setTimeout(function () {
    throw new Error('Database is down.');
  }, Math.round(Math.random()*100));
});

现在,在浏览器中检查路线。非域路由会惨失败停服务器!该消息可能如下所示:

/Users/azat/Documents/Code/proexpressjs/ch17/domain/app.js:41
    throw new Error('Database is down.');
          ^
Error: Database is down.
    at null._onTimeout (/Users/azat/Documents/Code/proexpressjs/ch17/domain/app.js:41:11)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

与非域路由不同,启用域的路由不会使服务器崩溃。相反,它发回了我们的自定义错误 JSON。因此,使用域名的好处!

工作(或者“崩溃”更准确)的例子在 GitHub 的ch17/domain文件夹中。 2

以下是ch17/domain/app.js的全部内容:

var http = require('http'),
  express = require('express'),
  path = require('path'),
  logger = require('morgan'),
  favicon = require('serve-favicon'),
  errorHandler = require('errorhandler');

var app = express();

app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(logger('combined'));
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(express.static(path.join(__dirname, 'public')));

var domain = require('domain');
var defaultHandler = errorHandler();

app.get('/error-domain', function (req, res, next) {
  var d = domain.create();
  d.on('error', function (error) {
    console.error(error.stack);
    d.exit()
    res.status(500).send({'Custom Error': error.message});
  });
  d.run(function () {
    // Error prone code goes here
    // throw new Error('Database is down.');
    setTimeout(function () {
      throw new Error('Database is down.');
    }, Math.round(Math.random()*100));
  });
});

app.get('/error-no-domain', function (req, res, next) {
  // Error prone code goes here
  // throw new Error('Database is down.');
  setTimeout(function () {
    throw new Error('Database is down.');
  }, Math.round(Math.random()*100));
});

app.use(function (error, req, res, next) {
  console.log(domain)
  if (domain.active) {
    console.info('Caught with domain', domain.active);
    domain.active.emit('error', error);
  } else {
    console.info('No domain');
    defaultHandler(error, req, res, next);
  }
});

var server = app.listen(app.get('port'), function() {
  console.log('Express server listening on port ' + server.address().port);
});

通过在中间件中实现域,您可以用最少的代码重复将域方法应用于每一个路由。确保将这个中间件放在路由之前。

和 Express.js 一样,有一个第三方模块将每条路由封装在域中:express-domain-middleware ( https://www.npmjs.org/package/express-domain-middleware)。

您可以通过以下方式安装express-domain-middleware v0.1.0:

$ npm install express-domain-middleware@0.1.0

然后,用require()将模块导入到您的项目中:

var expressDomain = require('express-domain-middleware');

将此中间件应用到您的 Express.js 应用,在其他中间件和路由之前使用app.use() :

app.use(expressDomain);

好好享受!

另一个与 domain 和 Express.js 一起用于错误处理的好模块是okay ( https://www.npmjs.org/package/ok)。这里的想法是okay是手动错误检查的更有力的替代,例如:

if (error) return next(error);

与前面的行不同,前面的行在每个嵌套的回调中重复出现(如果您写得正确,您应该在每个回调中处理错误),您需要做的只是用两个参数调用okay:一个错误回调(例如next)和一个无错误回调(例如常规闭包)。在某种程度上,okay是函数和常规回调之间的额外一层。

有了okay,代码变得更加清晰,正如您在这个 Express.js 路径中看到的:

var okay = require('okay');
app.get('/', function(req, res, next) {
   fs.readFile('file.txt', 'utf8', okay(next, function(contents) {
      res.send(contents);
   });
});

Image 注意在撰写本文时(2014 年),Domain 模块正处于不稳定的阶段。这意味着它的方法和行为可能会改变。因此,保持更新并使用package.json文件中的精确版本。

摘要

对于大多数程序员来说,使用域可能是一个难以理解的概念。如果你是其中之一,不要绝望!这是因为我们必须处理异步代码问题。我们需要改变我们的整个心态。出于这个原因,本章从一些非常基本的异步代码例子开始,来说明这些挑战。然后,我们讨论了 domain 和 Express.js 的使用。说到 Express.js,其受欢迎程度和 Node.js 的 NPM 的一个辉煌之处在于,它们是几乎任何东西的中间件。因此,如果理解域对您来说不是一个优先的 Node.js 主题,您可以只应用中间件,然后忘掉它。

使用 Domain 模块是使用框架的最后一个 Express.js 技巧和窍门。在下一章中,我们将返回到“Hello World”模式,以了解为什么开始学习 Express.js 的 Node.js 堆栈很重要,即使您计划使用不同的框架(剧透一下:这是因为许多 Node.js 框架借鉴了 Express.js 的一些概念或依赖于 express . js)。


1

2

十八、Sails.js、DerbyJS、LoopBack 和其他框架

如果您想要完成比标准 Express.js 应用能够完成的更全面的任务,那么您可能需要考虑探索其他真正的 MVC 框架。好消息是,一些最流行的替代 Node.js 框架使用 Express.js。因此,通过理解 Express.js,您可以更快、更好地理解其他框架的内部工作方式。

本章介绍以下更多依赖于 Express.js 的高级框架:

  • 帆. js
  • 德比耶斯
  • 回路

介绍这些其他框架的目的是为了证明,即使您使用的是 Express.js 以外的框架,您对 Express.js 的了解也会很方便。

帆. js

Sails.js 是一个约定超过配置类型的框架。这意味着它在理念上类似于 Ruby on Rails。Sails.js 是一个真正的 MVC 框架,不像 Express.js 依赖开发者添加像 Mongoose 这样的 ORM。Sails.js 使用水线形式(https://github.com/balderdashy/waterline)。

要开始使用 Sails.js:

$ npm -g install sails@0.10.5

这将为您提供sails命令,您可以如下使用该命令来查看可用选项列表:

$ sails -h

让我们使用new命令生成一个名为sails ( ch18/sails)的 app:

$ sails new sails

生成 app 后,用sails lift启动它:

$ cd sails
$ sails lift

现在,如果你去http://localhost:1337,你会看到一个 Sails.js 页面,上面有一些说明和链接,如图图 18-1 所示。

9781484200384_Fig18-01.jpg

图 18-1 。默认的 Sails.js 页面,带有一些说明和链接

Sails.js 有丰富的脚手架。要生成资源,您可以按照指定使用以下命令(用您自己的名称替换值)。

建立一个新的模型和控制器,api/models/NAME.jsapi/controllers/NAMEController.js:

$ sails generate api NAME

建立一个新模型api/models/NAME.js,带有属性(可选):

$ sails generate model NAME [attribute1:type1, attribute2:type2 ... ]

构建一个新的控制器api/controllers/NAMEController.js,带有动作(可选):

$ sails generate controller NAME [action1, action2, ...]:

构建新的适配器,api/adapters/NAME:

$ sails generate adapter NAME:

构建一个名为NAME的新生成器:

$ sails generate generator NAME

每个控制器都被构造成一个带有方法的模块。这些方法都是动作,比如/controller/action。每个动作都有一个请求和一个响应。它们的参数继承自它们的 Express.js 对应物。为了说明这一点,让我们创建一个控制器,并使用 Express.js 方法response.json()response.redirect()向其添加一些自定义代码。

首先,运行以下命令:

$ sails generate api user

打开新创建的文件ch18/sails/api/controllers/UserController.js,向其中添加两个动作json,它将输出当前时间,以及buy-oauth,它将使用重定向:

module.exports = {
  json: function (request, response) {
    response.json({time: new Date()})
  },
  'buy-oauth': function (request, response) {
    return res.redirect('https://gum.co/oauthnode');
  }
};

如果你去http://localhost:1337/user/json,你会看到

{ "time": "2014-09-09T14:59:28.377Z" }

如果你去http://localhost:1337/user/buy-oauth,你会被重定向到介绍 OAuth 的网页,带有 node . js(webapplog.com,2014)

这个简短的介绍已经表明,用 Sails.js 编写控制器对您来说很容易,因为您已经熟悉了 Express.js。控制器是视图和模型之间的中介,通常包含大部分代码和逻辑。有关 Sails.js 概念及其文档的更多信息,请访问http://sailsjs.org/#/documentation/conceptshttp://irlnathan.github.io/sailscasts

德比耶斯

DerbyJS 是一个全栈框架,这意味着它同时处理客户端和服务器端代码。它还提供了数据库连接和抽象层(模型):http://derbyjs.com。DerbyJS 在反应式全栈方法上与 Meteor ( https://www.meteor.com)类似,但是 DerbyJS 在使用其包(与 NPM 模块相比)时不那么固执己见,也不急于求成。

首先,用这些版本(ch18/derby/package.json)创建一个package.json文件:

{
  "name": "derby-app",
  "description": "",
  "version": "0.0.1",
  "main": "./server.js",
  "dependencies": {
    "derby": "0.6.0-alpha24",
    "derby-less": "0.1.0",
    "derby-starter": "0.2.3"
  }
}

要安装依赖项,请运行

$ npm install

显然,derby是框架的模块。不太明显的是derby-starter,它是一组获取derby应用(derby-app.js)并通过设置运行它的文件。设置包括连接到 Redis 和 MongoDB,并将 DerbyJS 应用安装到 Express.js 服务器。

要使用derby-starter启动 DerbyJS 应用,请使用server.js。你可以复制和定制derby-starter来满足你的需求,或者编写你自己的迷你模块。derby-starter/lib/server.js文件使用熟悉的 Express.js 语句:

//...
  expressApp
    // Creates an express middleware from the app's routes
    .use(app.router())
    .use(expressApp.router)
    .use(errorMiddleware)
//...

为了利用derby-starter,让我们创建一个server.js文件来启动服务器:

require('derby-starter').run(__dirname+'/derby-app.js');

主要的 DerbyJS 逻辑将在derby-app.js文件中,其内容从实例化开始:

var path = require('path'),
  derby = require('derby'),
  app = derby.createApp('derby-app', __filename);

derby-starter访问 DerbyJS 应用需要下一个任务:

module.exports = app;

仅仅为了多样化,我们在这里使用较少的 CSS 库,但是 Derby 支持 Stylus 和其他:

app.serverUse(module, 'derby-less');

以下是我们加载样式的方法:

app.loadStyles(path.join(__dirname, 'styles', 'derby-app'));

类似地,我们加载模板(ch18/derby/views/derby-app.html),它在语法上类似于把手:

app.loadViews (path.join(__dirname, 'views', 'derby-app'));

我们将定义的路由类似于 Express.js 路由。主要区别在于请求处理程序:pagemodelparamspage处理程序是用page.render()呈现的页面,model处理程序是传递给浏览器和从浏览器接收的数据库抽象层,params处理程序是具有 URL 参数(例如/:id)的对象:

app.get('/', function(page, model, params) {
  model.subscribe('d.message', function() {
    page.render();
  });
});

下一个方法是app.proto.create,当页面同时加载到服务器和客户端时使用。请注意,这是 Node.js 代码,而不是浏览器代码。因此,该框架具有全栈特性:

app.proto.create = function(model) {
  model.on('set', 'd.message', function(path, object) {
    console.log('message has been changed: ' + object);
  });
};

ch18/derby/views/derby-app.html的内容使用了一个特殊的 DerbyJS 标签,<Body:>:

<Body:>
  <input value="{{d.message}}"><h1>{{d.message}}</h1>

少文件derby-app.less有这种风格:

h1 {
  color: blue;
}

derby-starter还连接 Redis 和 MongoDB。因此,让我们在两个独立的终端窗口/选项卡中启动 Redis 和 MongoDB:

$ redis-server
$ mongod

然后使用以下两个命令之一启动服务器:

$ node server
$ node .

然后,去http://localhost:3000打个东西。文本将在页面的<h1>标签中自动更新(见图 18-2 ),并保存到 MongoDB 数据库中。一切都是实时的!这意味着,如果您重新启动服务器并刷新页面,您将再次看到相同的消息。

9781484200384_Fig18-02.jpg

图 18-2 。DerbyJS 应用中的实时通信

回路

LoopBack 是一个全面的框架,带有命令行脚手架和 web API explorer: http://strongloop.com/node-js/loopback

要安装命令行工具,请运行:

$ npm install -g strongloop@2.9.1

要创建样板应用,请运行此命令并回答后续问题:

$ slc loopback

最后,命令行工具将向您显示一些可用选项:

  • 将目录更改为您的应用:

    $ cd loopback
    
    
  • 在您的应用中创建模型:

    $ slc loopback:model
    
    
  • 可选项:启用 StrongOps 操作监控:

    $ slc strongops
    
    
  • 运行应用:

    $ slc run .
    
    

让我们用字符串类型的属性name、复数Books、内存存储和 REST API ( slc将要求输入)创建一个模型Book:

$ cd loopback
$ slc loopback:model

完成模型后,运行:

$ slc run

然后在你的浏览器中进入http://localhost:3000/explorer(见图 18-3 )并点击书籍探索 API。

9781484200384_Fig18-03.jpg

图 18-3 。LoopBack 的 explorer 是 API 的 web 界面

为了演示 LoopBack 是建立在 Express.js 之上的,编辑ch18/loopback/common/models/book.js如下:

module.exports = function(Book) {
  Book.buy = function(code, cb) {
    cb(null, 'Processing... ' + code);
  }

  Book.remoteMethod('buy', {accepts: [{http: function(ctx) {
    // HTTP request object as provided by Express.js
    var request = ctx.req;
    console.log(request.param('code'), request.ip, request.hostname)
    return request.param('code');
  }}],
      returns: {arg: 'response', type: 'string'}
    }
  );
};

request ( ctx.req)对象是 Express.js 请求。我们可以调用request.param()方法(在其他 Express.js 方法中,比如request.ip()request.hostname())来提取code参数。

客户端请求如下:

$ curl http://localhost:3000/api/Books/Buy -X POST -d "code=1"

客户的反应是:

{"response":"Processing... 1"}%

服务器日志包括:

1 127.0.0.1 localhost

Image 提示每次修改源文件时,不要忘记用$ slc run重启服务器。

另一个可以使用 Express.js 技能的地方是ch18/loopback/server/server.js;例如:

app.use(middleware());

其他框架

下面的列表介绍了一些你在掌握 Express.js 后可能想要使用的其他值得注意的框架(尽管它们不一定依赖于 Express.js):

  • 哈比神 ( http://hapijs.com):一个全面的、企业级的框架(实用 node . js【Apress,2014】有一个用这个框架构建的 REST API 示例)
  • Total.js ( http://www.totaljs.com):一个模块化的 web 应用框架
  • 一个简单的、结构化的、用于 Node 的 web 框架
  • 复合 ( http://compoundjs.com):用 Express + structure + extensions 公式构建的框架(该框架的创建者为本书写了前言)

要了解更多 Node.js 框架,请查看http://nodeframeworks.com——node . js 框架的精选注册表。

摘要

正如您从 Sails.js、DerbyJS 和 LoopBack 的这一系列介绍中看到的,了解 Express.js 是有帮助的!

本书的第三部分到此结束,其中您已经学习了如何解决 Express.js 中的常见问题,例如抽象代码、使用域实现异步错误处理、保护您的应用、轻松实现服务器和客户端之间的实时通信、在其他框架中应用 Express.js 知识,以及使用集群跨越多个进程。现在,您已经准备好了比我们用来演示某些特性的例子更全面的例子。

在第四部分中,我们将涵盖四个示例应用,从 Instagram Gallery 开始,它展示了一个与第三方服务提供商集成的简单服务器。

十九、Instagram 图库

如果你按时间顺序阅读这本书,你已经了解了 API 参考的重要但有些枯燥的细节,并且只接触了抽象的解决方案。现在你已经到了第四部分,激动人心的事情开始了,因为这一部分的五章都是关于编码和例子的!

本章中的教程演示了如何将 Express.js 与外部第三方服务(Storify API)一起使用。该应用的目的是从 Storify 获取 Instagram 照片,并将其显示在图库中。除了 Express.js,我们还将使用以下三个模块:

  • superagent ( https://www.npmjs.org/package/superagent)
  • consolidate ( https://www.npmjs.org/package/consolidate)
  • handlebars ( https://www.npmjs.org/package/handlebars)

我选择这些模块是因为它们在 Node.js 开发圈子里有些流行,所以你将来很有可能会遇到或使用它们。

Image 注意这个例子的完整源代码可以在https://github.com/azat-co/sfy-gallery找到。

Storify ( http://storify.com)运行在 Node.js ( http://nodejs.org)和 Express.js ( http://expressjs.com)上。因此,为什么不使用这些技术来编写一个应用,演示如何构建依赖于第三方 API 和 HTTP 请求的应用呢?

启动 Instagram 图库

Instagram Gallery 应用将获取一个故事对象,并显示其标题、描述和元素/图像的图库,如图 19-1 所示的示例。

9781484200384_Fig19-01.jpg

图 19-1 。Instagram 图库

Image 如果你想知道喀山是什么,它是鞑靼共和国(鞑靼斯坦)有 1000 年历史的首都。

该应用的文件结构如下所示:

- index.js
- package.json
- views/index.html
- css/bootstrap-responsive.min.css
- css/flatly-bootstrap.min.css

CSS 文件来自引导库(http://getbootstrap.com)和 Flatly theme ( http://bootswatch.com/flatly)。index.js文件是我们的主 Node.js 文件,其中包含了大部分逻辑,而index.html是 Handlebars 模板。这个应用使用了来自css文件夹中两个文件的普通 CSS。

我们的依赖包括

  • 用于 Express.js 框架的 4.8.1 版
  • 用于发出 HTTP(S)请求的 v0.18.2
  • consolidate v0.10.0 用于使用带有 Express.js 的把手
  • handlebarsv 2 . 0 . 0-beta 1 用于使用车把模板引擎

package.json文件的内容如下:

{
  "name": "sfy-gallery",
  "version": "0.2.0",
  "description": "Instagram Gallery: Storify API example written in Node.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "consolidate": "0.10.0",
    "express": "4.8.1",
    "handlebars": "2.0.0-beta.1",
    "superagent": "0.18.2"
  },
  "repository": "https://github.com/storify/sfy-gallery",
  "author": "Azat Mardan",
  "license": "BSD"
}

让我们继续安装模块

$ npm install

NPM 完成后,创建index.js。在文件的开头,我们需要以下依赖项:

var express = require('express');
var superagent = require('superagent');
var consolidate = require('consolidate');

var app = express();

然后,我们配置模板引擎:

app.engine('html', consolidate.handlebars);
app.set('view engine', 'html');
app.set('views', __dirname + '/views');

接下来,我们用中间件建立一个静态文件夹:

app.use(express.static(__dirname + '/public'));

如果你想提供任何其他的故事,请随意。所有你需要的是作者的用户名和故事鼻涕虫为我的鞑靼斯坦首都喀山画廊。留下以下内容:

var user = 'azat_co';
var story_slug = 'kazan';

如下所示粘贴您的值:Storify API key、username 和 _token(如果有)。在撰写本文时,Storify API 是公开的,这意味着不需要认证(即不需要密钥)。如果将来这种情况发生变化,请在http://dev.storify.com/request请求 API 密钥或在http://dev.storify.com遵循官方文档:

var api_key = "";
var username = "";
var _token = "";

让我们定义回家的路线(/):

app.get('/',function(req, res){

现在,我们将使用superagent get()方法从 Storify API 中获取路由回调的元素:

superagent.get("http://api.storify.com/v1/stories/"
  + user + "/" + story_slug)

Storify API 端点是" http://api.storify.com/v1/stories/ " + user + "/" + story_slug,在本例中是http://api.storify.com/v1/stories/azat/kazansuperagent的一个优点是我们可以编写链式方法。例如,query()在查询字符串中发送数据:

.query({api_key: api_key,
  username: username,
  _token: _token})

set()方法指定了请求头:

.set({Accept: 'application/json'})

并且end()在收到响应时执行回调:

.end(function(e, storifyResponse){
  if (e) return next(e);

要使用 HTTP 响应主体的 content 属性中的 story 对象来呈现模板,我们使用以下代码:

      return res.render('index', storifyResponse.body.content);
    })

})

app.listen(3001);

Storify API 将数据作为 JSON 返回。您可以通过进入https://api.storify.com/v1/stories/azat_co/kazan来查找格式(假设 API 仍然是公共的,在撰写本文时就是如此)。你可以在图 19-2 的中看到收缩的(即不显示每个嵌套的对象)JSON。

9781484200384_Fig19-02.jpg

图 19-2 。story 实体的约定 Storify API 输出示例

查看画廊

现在,我们已经有了从 Storify 获取数据并调用index模板来呈现数据的应用,让我们来看看位于views/index.html文件中的 Handlebars 模板:

*<!DOCTYPE html lang="en">*
<html>
  <head>
    <link type="text/css"
      href="css/flatly-bootstrap.min.css"
      rel="stylesheet" />
    <link type="text/css"
      href="css/bootstrap-responsive.min.css"
      rel="stylesheet"/>
  </head>

  <body class="container">
    <div class="row">

现在,我们使用{{title}}来显示 Storify 故事的标题,使用{{author.name}}来显示其作者姓名:

  <h1>{{title}}<small> by {{author.name}}</small></h1>
  <p>{{description}}</p>
</div>
<div class="row">
  <ul class="thumbnails">

下一行是一个内置的handlebars构造,它遍历数组项。并且,随着每一次迭代,我们打印一个新的<li>标签:

      {{#each elements}}
        <li class="span3">
          <a class="thumbnail" href="{{permalink}}"
          target="_blank">
            <img src="{{data.image.src}}"
              title="{{data.image.caption}}" />
          </a>
        </li>
      {{/each}}
      </ul>
    </div>
  </body>

</html>

当你用$ node .启动应用时,你会在http://localhost:3000看到图片。发生的情况是,当你进入页面时,本地服务器迅速向 Storify 发出请求,并从 Instagram 获取图片的链接。

摘要

Express.js 和superagent让开发者只需几行代码就可以检索和操作 Storify、Twitter 和脸书等第三方服务提供的数据。本章给出的例子相当简单,因为它不使用数据库。但是,在下一章,Todo 应用将向您展示如何利用 MongoDB。

Image 注意在大多数情况下,服务提供商(如谷歌、脸书和 Twitter)需要认证(在撰写本文时,Storify API 并非如此)。要使 OAuth 1.0、OAuth 2.0 和 OAuth Echo 请求,考虑 OAuth(https://www.npmjs.org/package/oauth;GitHub: https://github.com/ciaranj/node-oauth)、every auth(https://www.npmjs.org/package/everyauth);GitHub: https://github.com/bnoguchi/everyauth)和/或护照(网址:http://passportjs.org/;GitHub: https://github.com/jaredhanson/passport)。

二十、TODO 应用

Todo 应用被认为是很好的教学范例,因为它们有许多你将在典型的真实项目中看到的组件。此外,这种应用在展示浏览器 JavaScript 框架时很受欢迎。只要看看著名的 TodoMVC 项目(http://todomvc.com)就知道了,它有几十个 Todo 应用,用于各种前端 JavaScript 框架。

在我们的 Todo 应用中,我们将使用 MongoDB、Mongoskin、Jade、web 表单、Less 样式表和跨站点请求伪造(CSRF)保护。我们将有意地而不是使用 Backbone.js 或 AngularJS,因为我们的目标是演示如何使用表单、重定向和服务器端模板渲染来构建传统的网站。我们还将看看如何插入 CSRF 和更少。额外的好处是,会有一些对 RESTful API-ish 端点的 AJAX/XHR 调用,因为没有这样的调用,很难构建一个现代的用户界面/体验。您应该知道如何在这种混合 web 站点架构中使用它们(传统的服务器端 HTML 呈现和一些 AJAX/XHR 调用)。

Image 注意为了您的方便,此 Todo 应用的所有源代码都在https://github.com/azat-co/todo-express处。读者不断为该项目做出贡献,因此,当这本书到了您的手中时,GitHub 中的代码将与书中的代码不同,可能会有更多的功能和最新的库。

这个项目相当复杂,所以在你开始编码之前,这里有一个本章介绍如何实现最终产品的步骤的概述:

  • 概观
  • 设置
  • App.js
  • 路线
  • 翡翠
  • 较少的

概观

为了预览我们将在本章中实现的内容,让我们从 Todo 应用的一些截图开始,展示用户界面是如何工作的。图 20-1 显示了主页,它有一个标题、一个菜单和一些介绍性的文字。菜单由三个项目组成:

  • 首页:当前显示的页面
  • 待办事项列表:要做的任务列表
  • 已完成:已完成任务列表

9781484200384_Fig20-01.jpg

图 20-1 。Todo app 首页

在待办事项页面,有一个空列表,如图图 20-2 所示。还有一个新任务的输入表单和一个“添加”按钮。

9781484200384_Fig20-02.jpg

图 20-2 。清空待办事项页面

图 20-3 显示了添加四个项目到待办事项列表的结果。每个任务的左边有一个“完成”按钮,右边有一个“删除”按钮,它们的功能正如你所想。他们分别将任务标记为已完成(即,将其移动到已完成页面)和移除任务。

9781484200384_Fig20-03.jpg

图 20-3 。添加了项目的待办事项列表页面

图 20-4 显示了点击“购买牛奶”任务的“完成”按钮的结果。该项目已从待办事项列表中消失,列表已重新编号。

9781484200384_Fig20-04.jpg

图 20-4 。一个项目标记为完成的待办事项列表页

然而,已完成的“买牛奶”任务并没有从应用中完全消失。现在在已完成页面的已完成列表中,如图图 20-5 所示。

9781484200384_Fig20-05.jpg

图 20-5 。待办事宜 app 完成页面

点击“删除”按钮后,从待办事项列表页面删除一个项目是通过 AJAX/XHR 请求执行的操作。图 20-6 显示了删除任务时出现的紫色高亮通知消息(在本例中,是“Email LeanPub”任务)。其余的逻辑通过 get 和 POSTs(通过表单)实现。

9781484200384_Fig20-06.jpg

图 20-6 。任务已删除的待办事项列表页面

设置

我们通过创建一个新文件夹来开始 Todo 应用的设置:

$ mkdir todo-express
$ cd todo-express

像往常一样,我们从处理依赖关系开始。这个命令为我们提供了基本的package.json文件:

$ npm init

我们需要向package.json 添加以下额外的依赖项:

  • 4.8.1 版:用于 Express.js 框架
  • v1.6.6:用于处理有效载荷
  • v1.3.2:用于处理 cookies 和会话
  • 版本 1.7.6:用于会话支持
  • 1.5.0 版:针对 CSRF 安全
  • v1.1.1:用于基本的错误处理
  • jade v1.5.0:用于玉石模板
  • 1.0.4 版:支持更少
  • method-override v2.1.3:适用于不支持所有 HTTP 方法的客户端
  • v1.4.4:用于 MongoDB 连接
  • 1.2.3 版:用于记录请求
  • 2.1.1 版:支持网站图标

添加前面的依赖列表的方法之一是利用npm install--save ( -s)选项:

$ npm install less-middleware@1.0.4 --save
$ npm install mongoskin@1.4.4 --save
...

另一种方法是向package.json添加条目并运行$ npm install:

{
  "name": "todo-express",
  "version": "0.2.0",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "body-parser": "1.6.6",
    "cookie-parser": "1.3.2",
    "csurf": "1.5.0",
    "errorhandler": "1.1.1",
    "express": "4.8.1",
    "express-session": "1.7.6",
    "jade": "1.5.0",
    "less-middleware": "1.0.4",
    "method-override": "2.1.3",
    "mongoskin": "1.4.4",
    "morgan": "1.2.3",
    "serve-favicon": "2.1.1"
  }
}

现在,如果您还没有安装 MongoDB 数据库,请安装它。数据库与 NPM 模块mongodbmongoskin不同,它们是驱动程序。这些库允许我们与 MongoDB 数据库交互,但是我们仍然需要驱动程序和数据库。

在 OS X 上,可以使用brew安装 MongoDB(或者升级到 v2.6.3):

$ brew update
$ brew install mongodb
$ mongo --version

要了解更多的 MongoDB 安装版本,请查看官方文档 1 和/或实用 node . js(2014 年出版)。

应用的最终版本(0.20.0)具有以下文件夹和文件结构(http://github.com/azat-co/todo-express):

/todo-express
  /public
    /bootstrap
      *.less
    /images
    /javascripts
      main.js
      jquery.js
    /stylesheets
      style.css
      main.less
  favicon.ico
  /routes
    tasks.js
    index.js
  /views
    tasks_completed.jade
    layout.jade
    index.jade
    tasks.jade
  app.js
  readme.md
  package.json

bootstrap文件夹中的*.less表示有一堆引导程序(CSS 框架,http://getbootstrap.com/)源文件。它们可以在 GitHub 上获得。 2

App.js

本节展示了 Express.js 生成的app.js文件的分解,添加了路由、数据库、会话、Less 和app.param()中间件。

首先,我们用 Node.js 全局require()函数导入依赖关系:

var express = require('express');

同样,我们可以访问自己的模块,也就是应用的路线:

var routes = require('./routes');
var tasks = require('./routes/tasks');

我们还需要核心的httppath模块:

var http = require('http');
var path = require('path');

Mongoskin 是原生 MongoDB 驱动程序的更好替代,因为它提供了额外的特性和方法:

var mongoskin = require('mongoskin');

我们只需要一行代码就可以获得数据库连接对象。第一个参数遵循protocol://username:password@host:port/database的标准 URI 惯例:

var db = mongoskin.db('mongodb://localhost:27017/todo?auto_reconnect', {safe:true});

我们设置应用本身:

var app = express();

现在,我们从 NPM 模块导入中间件依赖关系:

var favicon = require('serve-favicon'),
  logger = require('morgan'),
  bodyParser = require('body-parser'),
  methodOverride = require('method-override'),
  cookieParser = require('cookie-parser'),
  session = require('express-session'),
  csrf = require('csurf'),
  errorHandler = require('errorhandler');

在这个中间件中,我们将数据库对象导出给所有的中间件功能。这样,我们将能够在 routes 模块中执行数据库操作:

app.use(function(req, res, next) {
  req.db = {};

我们简单地在每个请求中存储tasks集合:

  req.db.tasks = db.collection('tasks');
  next();
})

这一行允许我们从每个 Jade 模板中访问appname:

app.locals.appname = 'Express.js Todo App'

我们将服务器端口设置为环境变量,或者如果没有定义的话,设置为3000:

app.set('port', process.env.PORT || 3000);

这些语句告诉 Express.js 模板位于何处,以及在调用期间省略扩展名的情况下应该预先考虑什么文件扩展名:

app.set('views', __dirname + '/views');
app.set('view engine', 'jade');

下面显示了 Express.js favicon(浏览器的 URL 地址栏中的图形):

app.use(favicon(path.join('public','favicon.ico')));

现成的记录器将在终端窗口中打印请求:

app.use(logger('dev'));

需要bodyParser()中间件来轻松访问输入数据:

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

methodOverride()中间件是涉及头的 HTTP 方法的变通方法。这对本例来说并不重要,但我们将把它留在这里:

app.use(methodOverride());

要使用 CSRF,我们需要cookieParser()session()。下面这些看起来很奇怪的弦是秘密。你希望它们是随机的,来自环境变量(process.env),而不是硬编码的。

app.use(cookieParser('CEAF3FA4-F385-49AA-8FE4-54766A9874F1'));
app.use(session({
  secret: '59B93087-78BC-4EB9-993A-A61FC844F6C9',
  resave: true,
  saveUninitialized: true
}));

express- session 选项包含在第三章中,但是,如前面的代码所示,v1.7.6(我们在这里使用的)有一个resave选项,如果设置为true则保存未修改的会话,还有一个saveUninitialized选项,如果设置为true则保存新的但未修改的会话。两个选项的默认值都是true。推荐值为resavefalsesaveUninitializedtrue

如果您没有为这些选项指定值,那么您将会得到警告,因为这些选项的默认值将来可能会改变。所以,显式地设置选项是有好处的。或者,要取消这些警告,可以使用环境变量:

$ NO_DEPRECATION=express-session node app

接下来,我们应用csrf()中间件本身。顺序很重要:csrf()必须在cookieParser()session()之前。

app.use(csrf());

为了将较少的样式表处理成 CSS 样式表,我们以这种方式利用less-middleware:

app.use(require('less-middleware')(path.join(__dirname, 'public')));

其他静态文件也在public文件夹中:

app.use(express.static(path.join(__dirname, 'public')));

记得 CSRF 吗?这里的主要技巧是使用req.csrfToken(),它是由我们之前在app.js中应用的中间件创建的。这就是我们如何将 CSRF 令牌暴露给模板:

app.use(function(req, res, next) {
  res.locals._csrf = req.csrfToken();
  return next();
})

当有一个请求将route/RegExp:task_id匹配时,这个块被执行:

app.param('task_id', function(req, res, next, taskId) {

任务 ID 的值在taskId中,我们查询数据库以找到该对象:

req.db.tasks.findById(taskId, function(error, task){

检查错误和空结果非常重要:

if (error) return next(error);
if (!task) return next(new Error('Task is not found.'));

如果有数据,我们将它存储在请求中,并继续处理下一个中间件:

    req.task = task;
    return next();
  });
});

现在是时候定义我们的路线了。我们从主页开始:

app.get('/', routes.index);

接下来是待办事项页面:

app.get('/tasks', tasks.list);

如果用户单击“all done”按钮,下面的路径会将 Todo 列表中的所有任务标记为已完成。在 REST API 中,会放置 HTTP 方法,但是,因为我们正在构建带有表单的传统 web 应用,所以我们必须使用 POST :

app.post('/tasks', tasks.markAllCompleted)

用于添加新任务的相同 URL 用于标记所有已完成的任务,但是,在前面的方法(markAllCompleted())中,您将看到我们如何处理流控制:

app.post('/tasks', tasks.add);

为了标记一个任务完成,我们在 URL 模式中使用前面提到的:task_id字符串(在 REST API 中,这是一个 PUT 请求):

app.post('/tasks/:task_id', tasks.markCompleted);

与之前的 POST 路线不同,我们利用 Express.js param中间件和一个:task_id令牌:

app.del('/tasks/:task_id', tasks.del);

对于我们完成的页面,我们定义了这条路线:

app.get('/tasks/completed', tasks.completed);

在恶意攻击或错误输入 URL 的情况下,用*捕获所有请求是一种用户友好的活动。请记住,如果我们之前有一个匹配,Node.js 不会来执行这个块。

app.all('*', function(req, res){
  res.status(404).send();
})

可以根据环境配置不同的行为:

if ('development' == app.get('env')) {
    app.use(errorHandler());
}

最后,我们用传统的http方法加速我们的应用:

http.createServer(app).listen(app.get('port'),
  function(){
    console.log('Express server listening on port '
      + app.get('port'));
  }
);

app.js文件的完整内容如下(GitHub repo https://github.com/azat-co/todo-express中的代码是从社区贡献演化而来的,所以它将是这段代码的增强版本):

var express = require('express');
var routes = require('./routes');
var tasks = require('./routes/tasks');
var http = require('http');
var path = require('path');
var mongoskin = require('mongoskin');
var db = mongoskin.db('mongodb://localhost:27017/todo?auto_reconnect', {safe:true});
var app = express();

var favicon = require('serve-favicon'),
  logger = require('morgan'),
  bodyParser = require('body-parser'),
  methodOverride = require('method-override'),
  cookieParser = require('cookie-parser'),
  session = require('express-session'),
  csrf = require('csurf'),
  errorHandler = require('errorhandler');

app.use(function(req, res, next) {
  req.db = {};
  req.db.tasks = db.collection('tasks');
  next();
})
app.locals.appname = 'Express.js Todo App'

app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(favicon(path.join('public','favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(methodOverride());
app.use(cookieParser('CEAF3FA4-F385-49AA-8FE4-54766A9874F1'));
app.use(session({
  secret: '59B93087-78BC-4EB9-993A-A61FC844F6C9',
  resave: true,
  saveUninitialized: true
}));
app.use(csrf());

app.use(require('less-middleware')(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));
app.use(function(req, res, next) {
  res.locals._csrf = req.csrfToken();
  return next();
})

app.param('task_id', function(req, res, next, taskId) {
  req.db.tasks.findById(taskId, function(error, task){
    if (error) return next(error);
    if (!task) return next(new Error('Task is not found.'));
    req.task = task;
    return next();
  });
});

app.get('/', routes.index);
app.get('/tasks', tasks.list);
app.post('/tasks', tasks.markAllCompleted)
app.post('/tasks', tasks.add);
app.post('/tasks/:task_id', tasks.markCompleted);
app.delete('/tasks/:task_id', tasks.del);
app.get('/tasks/completed', tasks.completed);

app.all('*', function(req, res){
  res.status(404).send();
})
// development only
if ('development' == app.get('env')) {
  app.use(errorHandler());
}
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

路线

routes 文件夹里只有两个文件。其中一个是routes/index.js,服务于主页(例如http://localhost:3000/),非常简单:

exports.index = function(req, res){
  res.render('index', { title: 'Home' });
};

剩下的处理任务的逻辑已经放在了todo-express/routes/tasks.js中。让我们进一步分解这个文件。

我们首先导出一个list() 请求处理程序,它给出了一个未完成任务的列表:

exports.list = function(req, res, next){

为此,我们使用completed=false查询执行数据库搜索:

  req.db.tasks.find({
    completed: false
  }).toArray(function(error, tasks){

在回调中,我们需要检查任何错误:

if (error) return next(error);

因为我们使用toArray(),我们可以将数据直接发送到模板:

    res.render('tasks', {
      title: 'Todo List',
      tasks: tasks || []
    });
  });
};

添加新任务需要我们检查name参数:

exports.add = function(req, res, next){
  if (!req.body || !req.body.name)
    return next(new Error('No data provided.'));

感谢我们的中间件,我们已经在req对象中有了一个数据库集合,并且任务的默认值是不完整的(completed: false):

req.db.tasks.save({
  name: req.body.name,
  completed: false
}, function(error, task){

同样,使用 Express.js next()函数检查错误并传播它们是很重要的:

if (error) return next(error);
if (!task) return next(new Error('Failed to save.'));

日志记录是可选的。但是,它对学习和调试很有用:

console.info('Added %s with id=%s', task.name, task._id);

最后,当保存操作成功完成时,我们重定向回 Todo 列表页面:

    res.redirect('/tasks');
  })
};

此方法将所有未完成的任务标记为完成:

exports.markAllCompleted = function(req, res, next) {

因为我们必须重用 POST 路由,并且因为它是流控制的一个很好的例子,我们检查all_done参数来确定这个请求是来自“all done”按钮还是“add”按钮:

if (!req.body.all_done
  || req.body.all_done !== 'true')
  return next();

如果执行到这里,我们用multi: true选项执行数据库查询(更新许多文档)。这个查询将用$set指令将所有未完成的任务(completed: false)的completed属性分配给true

req.db.tasks.update({
  completed: false
}, {$set: {
  completed: true
}}, {multi: true}, function(error, count){

接下来,我们执行重大错误处理、日志记录,并重定向回 Todo 列表页面:

    if (error) return next(error);
    console.info('Marked %s task(s) completed.', count);
    res.redirect('/tasks');
  })
};

除了completed标志值,在本例中为true之外,完成的路线与待办事项列表路线相似:

exports.completed = function(req, res, next) {
  req.db.tasks.find({
    completed: true
  }).toArray(function(error, tasks) {
    res.render('tasks_completed', {
      title: 'Completed',
      tasks: tasks || []
    });
  });
};

这是负责将单个任务标记为已完成的路线。我们使用updateById,但是我们可以用 Mongoskin/MongoDB API 中的普通update()方法来完成同样的事情。

$set行,我们使用表达式completed: req.body.completed === 'true'代替req.body.completed值。之所以需要它,是因为req.body.completed的传入值是一个字符串而不是一个布尔值。

exports.markCompleted = function(req, res, next) {
  if (!req.body.completed)
    return next(new Error('Param is missing.'));
  req.db.tasks.updateById(req.task._id, {
    $set: {completed: req.body.completed === 'true'}},
    function(error, count) {

我们再次执行错误和结果检查:(update()updateById()不返回对象,而是返回受影响文档的数量):

      if (error) return next(error);
      if (count !==1)
        return next(new Error('Something went wrong.'));
      console.info('Marked task %s with id=%s completed.',
        req.task.name,
        req.task._id);
      res.redirect('/tasks');
    }
  )
}

Delete 是 AJAX 请求调用的单一路由。然而,它的实现并没有什么特别之处。唯一的区别是我们不重定向,而是发回状态200

或者,可以使用remove()方法代替removeById()

exports.del = function(req, res, next) {
  req.db.tasks.removeById(req.task._id, function(error, count) {
    if (error) return next(error);
    if (count !==1) return next(new Error('Something went wrong.'));
    console.info('Deleted task %s with id=%s completed.',
      req.task.name,
      req.task._id);
    res.status(204).send();
  });
}

为了方便起见,下面是todo-express/routes/tasks.js文件的完整内容:

exports.list = function(req, res, next){
  req.db.tasks.find({completed: false}).toArray(function(error, tasks){
    if (error) return next(error);
    res.render('tasks', {
      title: 'Todo List',
      tasks: tasks || []
    });
  });
};

exports.add = function(req, res, next){
  if (!req.body || !req.body.name) return next(new Error('No data provided.'));
  req.db.tasks.save({
    name: req.body.name,
    completed: false
  }, function(error, task){
    if (error) return next(error);
    if (!task) return next(new Error('Failed to save.'));
    console.info('Added %s with id=%s', task.name, task._id);
    res.redirect('/tasks');
  })
};

exports.markAllCompleted = function(req, res, next) {
  if (!req.body.all_done || req.body.all_done !== 'true') return next();
  req.db.tasks.update({
    completed: false
  }, {$set: {
    completed: true
  }}, {multi: true}, function(error, count){
    if (error) return next(error);
    console.info('Marked %s task(s) completed.', count);
    res.redirect('/tasks');
  })
};

exports.completed = function(req, res, next) {
  req.db.tasks.find({completed: true}).toArray(function(error, tasks) {
    res.render('tasks_completed', {
      title: 'Completed',
      tasks: tasks || []
    });
  });
};

exports.markCompleted = function(req, res, next) {
  if (!req.body.completed) return next(new Error('Param is missing.'));
  req.db.tasks.updateById(req.task._id, {$set: {completed: req.body.completed === 'true'}}, function(error, count) {
    if (error) return next(error);
    if (count !==1) return next(new Error('Something went wrong.'));
    console.info('Marked task %s with id=%s completed.', req.task.name, req.task._id);
    res.redirect('/tasks');
  })
};

exports.del = function(req, res, next) {
  req.db.tasks.removeById(req.task._id, function(error, count) {
    if (error) return next(error);
    if (count !==1) return next(new Error('Something went wrong.'));
    console.info('Deleted task %s with id=%s completed.', req.task.name, req.task._id);
    res.status(204).send();
  });
};

到目前为止,我们已经实现了主服务器文件app.js及其执行不同数据库操作的路径。现在,我们可以继续学习模板。

翡翠

在 Todo 应用中,我们使用四个模板:

  • 在所有页面上使用的 HTML 页面的框架
  • index.jade:首页
  • tasks.jade:全部列表页
  • tasks_completed.jade:已完成页面

让我们浏览一下每个文件,从layout.jade 开始。它以doctypehtmlhead类型开始:

doctype html
html
  head

我们应该设置appname变量:

title= title + ' | ' + appname

接下来,我们包括*.css文件,Express.js 将从更少的文件中提供它们的内容:

link(rel="stylesheet", href="/stylesheets/style.css")
link(rel="stylesheet", href="/bootstrap/bootstrap.css")
link(rel="stylesheet", href="/stylesheets/main.css")

具有引导结构的主体由.container.navbar类组成。要了解更多关于这些课程和其他课程的信息,请访问http://getbootstrap.com/css/

body
  .container
    .navbar.navbar-default
      .container
        .navbar-header
          a.navbar-brand(href='/')= appname
    .alert.alert-dismissable
    h1= title
    p Welcome to Express.js Todo app by&nbsp;
      a(href='http://twitter.com/azat_co') @azat_co
      |. Please enjoy.

这是其他 jade 模板(如tasks.jade)将被导入的地方:

block content

最后几行包括前端 JavaScript 文件:

script(src='/javascripts/jquery.js', type="text/javascript")
script(src='/javascripts/main.js', type="text/javascript")

以下是完整的layout.jade文件:

doctype html
html
  head
    title= title + ' | ' + appname
    link(rel="stylesheet", href="/stylesheets/style.css")
    link(rel="stylesheet", href="/bootstrap/bootstrap.css")
    link(rel="stylesheet", href="/stylesheets/main.css")

  body
    .container
      .navbar.navbar-default
        .container
          .navbar-header
            a.navbar-brand(href='/')= appname
      .alert.alert-dismissable
      h1= title
      p Welcome to Express.js Todo app by&nbsp;
        a(href='http://twitter.com/azat_co') @azat_co
        |. Please enjoy.
      block content
  script(src='/javascripts/jquery.js', type="text/javascript")
  script(src='/javascripts/main.js', type="text/javascript")

文件是我们的主页,非常普通。它最有趣的组件是nav-pills菜单,这是一个用于选项卡式导航的引导类。文件的其余部分只是静态超文本:

extends layout

block content
  .menu
    h2 Menu
    ul.nav.nav-pills
      li.active
        a(href="/tasks") Home
      li
        a(href="/tasks") Todo List
      li
        a(href="/tasks") Completed
  .home
    p This is an example of create, read, update, delete web application built with Express.js v4.8.1, and Mongoskin&MongoDB for&nbsp;
      a(href="http://proexpressjs.com") Pro Express.js
      |.
    p The full source code is available at&nbsp;
      a(href='http://github.com/azat-co/todo-express') github.com/azat-co/todo-express
      |.
    p For Express 3.x go to&nbsp;
      a(href="https://github.com/azat-co/todo-express/releases/tag/v0.1.0") release 0.1.0
      |.

接下来是tasks.jade ,用的是extends layout:

extends layout

block content

接下来是我们主页的具体内容:

.menu
  h2 Menu
  ul.nav.nav-pills
    li
      a(href='/') Home
    li.active
      a(href='/tasks') Todo List
    li
      a(href="/tasks/completed") Completed
h1= title

带有list类的div将保存待办事项列表:

  .list
    .item.add-task

将所有项目标记为完成的表单在隐藏字段中有一个 CSRF 标记(locals._csrf),并使用指向/tasks的 POST 方法:

div.action
  form(action='/tasks', method='post')
    input(type='hidden', value='true', name='all_done')
    input(type='hidden', value=locals._csrf, name='_csrf')
    input(type='submit', class='btn btn-success btn-xs', value='all done')

一个类似的启用 CSRF 的表单用于新任务的创建:

form(action='/tasks', method='post')
  input(type='hidden', value=locals._csrf, name='_csrf')
  div.name
    input(type='text', name='name', placeholder='Add a new task')
  div.delete
   input.btn.btn-primary.btn-sm(type='submit', value='add')

当我们第一次启动应用(或清理数据库)时,没有任务:

if (tasks.length === 0)
      | No tasks.

Jade 支持使用each命令进行迭代:

each task, index in tasks
 .item
   div.action

此表单将数据提交到其单独的任务路线:

form(action='/tasks/#{task._id}', method='post')
  input(type='hidden', value=task._id.toString(), name='id')
  input(type='hidden', value='true', name='completed')
  input(type='hidden', value=locals._csrf, name='_csrf')
  input(type='submit', class='btn btn-success btn-xs task-done', value='done')

index变量用于显示任务列表中的顺序:

div.num
  span=index+1
    |.&nbsp;
div.name
  span.name=task.name
  //- no support for DELETE method in forms
  //- http://amundsen.com/examples/put-delete-forms/
  //- so do XHR request instead from public/javascripts/main.js

“delete”按钮没有附加任何花哨的东西,因为事件是从main.js前端 JavaScript 文件附加到这些按钮上的:

        div.delete
          a(class='btn btn-danger btn-xs task-delete', data-task-id=task._id.toString(), data-csrf=locals._csrf) delete

这里提供了tasks.jade的完整源代码:

extends layout

block content

  .menu
    h2 Menu
    ul.nav.nav-pills
      li
        a(href='/') Home
      li.active
        a(href='/tasks') Todo List
      li
        a(href="/tasks/completed") Completed
  h1= title

  .list
    .item.add-task
      div.action
        form(action='/tasks', method='post')
          input(type='hidden', value='true', name='all_done')
          input(type='hidden', value=locals._csrf, name='_csrf')
          input(type='submit', class='btn btn-success btn-xs', value='all done')
      form(action='/tasks', method='post')
        input(type='hidden', value=locals._csrf, name='_csrf')
        div.name
          input(type='text', name='name', placeholder='Add a new task')
        div.delete
          input.btn.btn-primary.btn-sm(type='submit', value='add')
    if (tasks.length === 0)
      | No tasks.
    each task, index in tasks
      .item
        div.action
          form(action='/tasks/#{task._id}', method='post')
            input(type='hidden', value=task._id.toString(), name='id')
            input(type='hidden', value='true', name='completed')
            input(type='hidden', value=locals._csrf, name='_csrf')
            input(type='submit', class='btn btn-success btn-xs task-done', value='done')
        div.num
          span=index+1
            |.&nbsp;
        div.name
          span.name=task.name
          *//- no support for DELETE method in forms*
          *//-* *`http://amundsen.com/examples/put-delete-forms/`*
          *//- so do XHR request instead from public/javascripts/main.js*
        div.delete
          a(class='btn btn-danger btn-xs task-delete', data-task-id=task._id.toString(), data-csrf=locals._csrf) delete

最后但同样重要的是,tasks_completed.jade ,它只是tasks.jade文件的精简版:

extends layout

block content

  .menu
    h2 Menu
    ul.nav.nav-pills
      li
        a(href='/') Home
      li
        a(href='/tasks') Todo List
      li.active
        a(href="/tasks/completed") Completed

  h1= title

  .list
    if (tasks.length === 0)
      | No tasks.
    each task, index in tasks
      .item
        div.num
          span=index+1
            |.&nbsp;
        div.name.completed-task
          span.name=task.name

最后,我们可以用更少的资源自定义应用的外观。

较少的

如前所述,在app.js文件中应用适当的中间件后,我们可以将*. less文件放在public文件夹下的任何地方。Express.js 的工作原理是接受对某个.css文件的请求,然后尝试通过名称匹配相应的文件。因此,我们在 jade 模板中包含了*.css文件。

下面是todo-express/public/stylesheets/main.less文件的内容:

* {
  font-size:20px;
}
.item {
  height: 44px;
  width: 100%;
  clear: both;
  .name {
    width: 300px;
  }
  .action {
    width: 100px;
  }
  .delete {
    width: 100px
  }
  div {
    float:left;
  }
}
.home {
  margin-top: 40px;
}
.name.completed-task {
  text-decoration: line-through;
}

要运行这个应用,用$ mongo启动 MongoDB,在一个新的终端窗口中,执行$ node app并转到http://localhost:3000/——你应该会看到类似于前面图 20-1 中所示的页面。在您的终端窗口中,您应该会看到如下内容:

Express server listening on port 3000
GET / 200 30.448 ms - 1408
GET /stylesheets/style.css 304 7.196 ms - -
GET /javascripts/jquery.js 304 17.677 ms - -
GET /javascripts/main.js 304 27.151 ms - -
GET /stylesheets/main.css 200 453.584 ms - 226
GET /bootstrap/bootstrap.css 200 458.293 ms - 98336

摘要

您已经学习了如何使用 MongoDB、Jade 等等。这个 Todo 应用被认为是传统的 ??,因为它不依赖任何前端框架,并且在服务器上呈现 HTML。这样做是为了展示使用 Express.js 完成这样的任务是多么容易。在当今的开发中,人们经常利用某种 REST API 服务器架构,用 Backbone.js、AngularJS、Ember.js 或类似的东西构建前端客户端(见http://todomvc.com)。

在第二十二章的例子中,我们深入探讨了如何编写这样的服务器的细节。第二十二章应用 HackHall 使用 MEBN (MongoDB、Express.js、Backbone.js 和 Node.js)栈。但是,在我们讨论 HackHall 之前,我们将在第二十一章的中花更多时间讨论 REST API 和测试,其中有 REST API 的例子。


1

2

二十一、REST API

在本教程中,我们将构建一个 RESTful API 。除了 Express.js,我们还将通过 Mongoskin 库使用 MongoDB。我们还将使用 Mocha 和 SuperAgent 来编写功能测试。

本教程将指导您使用 Mocha 和 SuperAgent 库编写测试,然后向您展示如何以测试驱动的开发方式使用测试,利用 Express.js 框架和 MongoDB 的 Mongoskin 库构建 Node.js free-JSON REST API 服务器。

Image 注意为了方便起见,测试和应用文件的完整源代码都在https://github.com/azat-co/rest-api-express中。如果您想跳过教程,只运行代码,可以使用:

$ git clone https://github.com/azat-co/rest-api-express.git

$ cd rest-api-express

$ npm install

$ node express.jsIn a new terminal window, enter:$ ./node_modules/mocha/bin/mocha express.test.jsThe source code might be an enhanced version of the code in this chapter because of the ongoing contributions from readers. I encourage you to submit your own pull request!

在这个 REST API 服务器中,我们将执行创建、更新、移除和删除(CRUD)操作,并用app.param()app.use()方法利用 Express.js 中间件 1 概念。本章分为以下几个主题:

  • RESTful API 基础知识:RESTful API 初级读本
  • 测试覆盖范围:我们将使用测试驱动开发(TDD)方法,首先编写测试
  • 服务器依赖关系:我们将安装所需的模块
  • 服务器实现:我们将为 Express.js 应用编写代码

Image 注意在本章中,我们的 REST API 和测试示例使用了无分号的风格。JavaScript 中的分号绝对是可选的,除了两种情况:1)在 for 循环中,2)在以括号开头的表达式/语句之前(例如,立即调用的函数表达式或 IIFE)。使用这种风格给你一个不同的视角。键入更少的分号可以提高速度,而且看起来更好,更一致,因为开发人员往往会时不时地错过分号(完美运行的代码允许这样的草率)。此外,一些程序员发现不带分号的代码可读性更好。

RESTful API 基础

RESTful APIs 之所以流行,是因为分布式系统中的每个事务都需要包含足够的客户端状态信息。从某种意义上说,这个标准是无状态的,因为服务器上没有存储任何关于客户机状态的信息,这使得不同的系统为每个请求提供服务成为可能。

RESTful API 的独特特征(即,如果一个 API 是 RESTful 的,它通常遵循这些原则)如下:

  • 它具有更好的可伸缩性支持,因为不同的组件可以独立部署到不同的服务器上。
  • 它取代了简单对象访问协议(SOAP ),因为 REST 中的动词和名词结构更简单。
  • 它使用 HTTP 方法,比如 GET、POST、DELETE、PUT、OPTIONS 等等。
  • 它支持 JSON 以外的格式(尽管 JSON 是最流行的)。与 SOAP(一种协议)不同,REST 方法在选择格式方面非常灵活。例如,替代格式可能是可扩展标记语言(XML)或逗号分隔值(CSV)格式。

表 21-1 概述了一个用于消息收集的简单 CRUD REST API 的例子。

表 21-1 。CRUD REST API 结构示例

|

方法

|

统一资源定位器

|

意义

|
| --- | --- | --- |
| 得到 | /messages.json | 以 JSON 格式返回消息列表。 |
| 放 | /messages.json | 更新/替换所有消息,并在 JSON 中返回状态/错误。 |
| 邮政 | /messages.json | 创建新消息并以 JSON 格式返回其 ID。 |
| 得到 | /messages/{id}.json | 以 JSON 格式返回 ID 为{id}的消息。 |
| 放 | /messages/{id}.json | 更新/替换 ID 等于{id}值的消息;如果{id}消息不存在,则创建它。 |
| 删除 | /messages/{id}.json | 删除 ID 为{id}的消息,并以 JSON 格式返回状态/错误。 |

休息不是一个协议;它是一种架构,从某种意义上说,它比协议(如 SOAP)更灵活。因此,如果我们想支持这些格式,REST API URLs 可能看起来像/messages/list.html/messages/list.xml

PUT 和 DELETE 是幂等方法,这意味着,如果服务器收到两个或更多类似的请求,最终结果是相同的。POST 不是等幂的,可能会影响状态并导致副作用(例如,创建多个重复记录)。GET 是无效,这意味着多次调用它是安全的,因为结果不会改变。

Image 你可以在维基百科(http://en.wikipedia.org/wiki/Representational_state_transfer)和 Stefan Tilkov 的 InfoQ 文章《REST 简介》(www.infoq.com/articles/rest-introduction)中找到更多关于 REST 的信息。

正如“简介”一章中提到的,在我们的 REST API 服务器中,我们将执行 CRUD 操作,并通过app.param()app.use()方法利用 Express.js 中间件概念。因此,我们的应用应该能够使用 JSON 格式处理以下命令(collectionName是集合的名称,通常是复数名词,例如,消息、评论、用户等)。):

  • POST /collections/{collectionName}:请求创建一个对象;应用使用新创建的对象 ID 进行响应。
  • GET /collections/{collectionName}/{id}:用 URL 中的 ID 值请求;应用检索具有该 ID 的对象。
  • GET /collections/{collectionName}/:请求从集合中检索任意项目(items);在我们的例子中,我们有以下查询选项:最多 10 个条目,按 ID 排序。
  • PUT /collections/{collectionName}/{id}:用 ID 请求更新一个对象。
  • 删除/collections/{collectionName}/{id}:ID 为的请求删除一个对象。

因此,这个服务器可以处理任何数量的集合,而不仅仅是单个集合,只需要六个端点(例如messages,如表 21-1 所示)。

测试覆盖率

在我们做任何其他事情之前,让我们编写向我们即将创建的 REST API 服务器发出 HTTP 请求的功能测试。如果您知道如何使用 Mocha,或者只是想直接跳到 Express.js 应用实现,请随意。您也可以使用 CURL 终端命令进行测试。

假设我们已经安装了 Node.js、NPM 和 MongoDB,让我们创建一个新的文件夹(或者如果您编写了测试,使用那个文件夹):

$ mkdir rest-api-express
$ cd rest-api-express

我们将使用 Mocha、Expect.js ( https://github.com/Automattic/expect.js)和 SuperAgent ( http://visionmedia.github.io/superagent/)库。要安装它们,从项目文件夹运行这些命令:

$ npm install -g mocha@1.18.2 --save-dev
$ npm install expect.js@0.3.1 --save-dev
$ npm install superagent@0.17.0 --save-dev

Image 提示您可以在全球范围内安装 Mocha,因为它是一个命令行工具,但是在本地安装 Mocha 将使您能够同时使用不同版本的 Mocha——一个项目一个版本。要用本地摩卡运行测试,只需指向./node_modules/mocha/bin/mocha。你可以把它复制到 Makefile 中,如第二十二章中的所述,或者复制到package.json"scripts": {"test": "..."}中。持续集成(CI)的配置也需要本地 Mocha。

现在让我们创建一个express.test.js文件,它在同一个文件夹中有六个测试套件:

  • 创建新对象
  • 按 ID 检索对象
  • 检索整个收藏
  • 按 ID 更新对象
  • 通过 ID 检查更新的对象
  • 按 ID 删除对象

通过 SuperAgent 的链式函数,HTTP 请求变得轻而易举,我们将把这些函数放在每个测试套件中。

所以,我们从依赖关系开始:

var superagent = require('superagent')
var expect = require('expect.js')

接下来,我们编写包装在测试用例中的第一个测试用例(describe及其回调)。这个想法很简单。我们向服务器的本地实例发出一个 HTTP 请求。当我们发送请求时,我们传递一些数据,当然,还有 URL 路径,它随着测试用例的不同而变化。主要操作发生在请求(由 SuperAgent 发出)回调中。在那里,我们放置了多个断言,这是 TDD 的主要部分。严格来说,这个测试套件使用了行为驱动开发(BDD)语言,但是这个差异对于我们的项目来说并不重要。

describe('express rest api server', function(){
  var id

  it('posts an object', function(done){
    superagent.post('http://localhost:3000/collections/test')
      .send({ name: 'John'
        , email: 'john@rpjs.co'
      })
      .end(function(e,res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.eql(1)
        expect(res.body[0]._id.length).to.eql(24)
        id = res.body[0]._id
        done()
      })
  })

您可能已经注意到,我们正在检查以下内容:

  • 错误对象应该为空(eql(null))。
  • 响应体数组应该有一项(to.eql(1))。
  • 第一个响应主体项应该具有_id属性,该属性的长度为 24 个字符(即标准 MongoDB ObjectId类型的十六进制字符串表示)。

最后,我们将新创建的对象 ID 保存在id全局变量中,这样我们可以在以后使用它进行检索、更新和删除。说到对象检索,我们将在下一个测试用例中测试它们。注意,superagent方法已经变成了get(),URL 路径包含了对象 ID。您可以“取消注释”console.log来检查完整的 HTTP 响应体:

it('retrieves an object', function(done){
  superagent.get('http://localhost:3000/collections/test/'+id)
    .end(function(e, res){
      // console.log(res.body)
      expect(e).to.eql(null)
      expect(typeof res.body).to.eql('object')
      expect(res.body._id.length).to.eql(24)
      expect(res.body._id).to.eql(id)
      done()
    })
})

done() 回调允许我们测试异步代码。如果没有它,Mocha 测试用例会突然结束,远远早于缓慢的服务器有时间响应。

下一个测试用例的断言更有趣一些,因为我们对响应结果使用了map()函数来返回一个 id 数组。在这个数组中,我们用contain()方法找到我们的 ID(保存在id变量中),这是一个比原生indexOf()更优雅的替代方法。它之所以有效,是因为结果(限于 10 条记录)是按 id 排序的,还因为我们的对象是刚刚创建的。

it('retrieves a collection', function(done){
  superagent.get('http://localhost:3000/collections/test')
    .end(function(e, res){
      // console.log(res.body)
      expect(e).to.eql(null)
      expect(res.body.length).to.be.above(0)
      expect(res.body.map(function (item){return item._id})).to.contain(id)
      done()
    })
})

当需要更新对象时,我们实际上需要发送一些数据。我们通过将对象传递给 SuperAgent 的函数来实现这一点。然后,我们断言该操作在(msg=success)完成:

it('updates an object', function(done){
    superagent.put('http://localhost:3000/collections/test/'+id)
      .send({name: 'Peter'
        , email: 'peter@yahoo.com'})
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')
        done()
      })
  })

最后两个测试用例断言检索更新的对象及其删除,使用的方法与我们以前使用的方法类似。下面是rest-api-express/express.test.js文件的完整源代码:

var superagent = require('superagent')
var expect = require('expect.js')

describe('express rest api server', function(){
  var id

  it('posts an object', function(done){
    superagent.post('http://localhost:3000/collections/test')
      .send({ name: 'John'
        , email: 'john@rpjs.co'
      })
      .end(function(e,res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.eql(1)
        expect(res.body[0]._id.length).to.eql(24)
        id = res.body[0]._id
        done()
      })
  })

  it('retrieves an object', function(done){
    superagent.get('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body._id.length).to.eql(24)
        expect(res.body._id).to.eql(id)
        done()
      })
  })

  it('retrieves a collection', function(done){
    superagent.get('http://localhost:3000/collections/test')
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.be.above(0)
        expect(res.body.map(function (item){return item._id})).to.contain(id)
        done()
      })
  })

  it('updates an object', function(done){
    superagent.put('http://localhost:3000/collections/test/'+id)
      .send({name: 'Peter'
        , email: 'peter@yahoo.com'})
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')
        done()
      })
  })

  it('checks an updated object', function(done){
    superagent.get('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body._id.length).to.eql(24)
        expect(res.body._id).to.eql(id)
        expect(res.body.name).to.eql('Peter')
        done()
      })
  })

  it('removes an object', function(done){
    superagent.del('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')
        done()
      })
  })
})

为了运行测试,我们可以使用$ mocha express.test.js命令。现在,测试应该会失败,因为我们还没有实现服务器!

对于那些需要多个版本的 Mocha 的人来说,另一个更好的选择是使用本地 Mocha 二进制文件运行测试:

$ ./node_modules/mocha/bin/mocha express.test.js

当然,这是假设您已经在本地将 Mocha 安装到了node_modules中。

Image 注意默认情况下,Mocha 不使用任何记者,结果输出乏善可陈。要接收更多的解释性日志,请提供-R <name>选项(例如$ mocha test -R spec$ mocha test -R list)。

属国

和上一篇教程一样(第二十章),我们将使用 Mongoskin ,一个 MongoDB 库,它是 Node.js. 欲了解更多信息,请查看https://github.com/kissjs/node-mongoskin#comparation

Express.js 是核心 Node.js HTTP 模块对象(http://nodejs.org/api/http.html)的包装器。Express.js 框架构建在 Connect 中间件(https://github.com/senchalabs/connect)之上,提供了极大的便利。有些人把这个框架比作 Ruby 的 Sinatra,因为它是非自以为是和可配置的。

如果您在上一节中创建了一个rest-api-express文件夹,只需运行这些命令来为应用安装模块:

$ npm install express@4.8.1 --save
$ npm install mongoskin@1.4.1 --save

最终的package.json文件可能如下所示:

{
  "name": "rest-api-express",
  "version": "0.0.4",
  "description": "",
  "main": "express.js",
  "scripts": {
    "start": "node express.js",
    "test": "mocha express.test.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/azat-co/rest-api-express.git"
  },
  "author": "Azat Mardan",
  "license": "BSD-2-Clause",
  "bugs": {
    "url": "https://github.com/azat-co/rest-api-express/issues"
  },
  "dependencies": {
    "body-parser": "1.9.2",
    "express": "4.10.1",
    "mongoskin": "1.4.4",
    "morgan": "1.5.0" },
  "devDependencies": {
    "expect.js": "0.3.1",
    "mocha": "2.0.1",
    "superagent": "0.20.0" }
}

服务器实现

要实现服务器,我们首先需要定义我们的依赖关系:

var express = require('express'),
  mongoskin = require('mongoskin'),
  bodyParser = require('body-parser')
  logger = require('morgan')

在 3.x 版之后,Express.js 简化了其应用实例的实例化,因此这一行为我们提供了一个服务器对象:

var app = express()

为了从请求体中提取参数,我们将使用body-parser中间件。(如何使用中间件在第四章的中讨论过。)以下是 JSON 和 URL 编码函数的语句:

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true}))

morgan ( logger)中间件允许我们查看传入的请求:

app.use(logger('dev'))

中间件(在此为 3 等形式 4 )是 Express.js 和 Connect 中一种强大便捷的模式,用于组织和重用代码。

与节省我们编写额外代码(用于解析 HTTP 请求的主体对象)的bodyParser()方法一样, Mongoskin 使连接到 MongoDB 数据库成为可能,与原生 MongoDB 驱动程序代码相比,只需一行代码:

var db = mongoskin.db('@localhost:27017/test', {safe:true});

Image 注意如果您希望连接到一个远程数据库,比如 MongoHQ ( https://www.mongohq.com/home),用您的用户名、密码、主机和端口值替换该字符串。以下是 URI 字符串的格式:

mongodb://[username:password@] host1[:port1][,host2[:port2],... [,hostN[:portN]]] [/[database][?options]]

方法是另一个 Express.js 中间件。它基本上是说“每当请求处理程序的 URL 模式中有这个值时,就做一些事情。”在我们的例子中,当一个请求模式包含一个以冒号为前缀的字符串collectionName(您将在后面的 routes 中看到它):时,我们选择一个特定的集合

app.param('collectionName', function(req, res, next, collectionName){
  req.collection = db.collection(collectionName)
  return next()
})

为了方便用户,让我们在根路由中包含一条消息:

app.get('/', function(req, res, next) {
  res.send('please select a collection, e.g., /collections/messages')
})

现在真正的工作开始了。下面是我们检索一个条目列表的方法,这个列表按照_id (sort: {'_id':-1})排序,并且限制为十个(limit: 10):

app.get('/collections/:collectionName', function(req, res, next) {
  req.collection.find({},{
    limit: 10, sort: {'_id': -1}
  }).toArray(function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

您是否注意到 URL 模式参数中有一个:collectionName字符串?这个中间件和之前的app.param()中间件为我们提供了指向数据库中指定集合的req.collection对象。

创建对象的端点稍微容易理解,因为我们只是将整个有效负载传递给 MongoDB 方法(也称为 free-JSON REST API):

app.post('/collections/:collectionName', function(req, res, next) {
  req.collection.insert(req.body, {}, function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

单一对象检索函数(例如,findById())比find()更快,但是它们使用不同的接口。它们直接返回一个对象,而不是一个光标——请注意!ID 来自 URL 路径的:id部分,带有req.params.id Express.js magic:

app.get('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.findById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send(result)
  })
})

PUT 请求处理程序变得更加有趣,因为updateById() (as update())不返回增强的对象;相反,它返回受影响对象的计数。

另外,{$set: req.body}是一个特殊的 MongoDB 操作符(操作符往往以美元符号开始),它设置值。在这种情况下,我们更新发送给我们的任何机体数据。这被称为 free-JSON API 方法。这对于原型开发来说很棒,但是在大多数系统中,你需要执行验证(你可以使用express-validator中间件,在第十五章中有所介绍)。

第二个{safe: true, multi: false}参数是一个带有选项的对象,告诉 MongoDB 在运行回调函数之前等待执行,并且只处理一个(第一个)项目:

app.put('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.updateById(req.params.id,
    {$set: req.body},
    {safe: true, multi: false},
    function(e, result){
        if (e) return next(e)
        res.send((result === 1) ? {msg: 'success'} : {msg: 'error'})
    }
  )
})

最后,下面是删除方法 ,它利用了 Mongoskin 的removeById()方法,在成功的情况下输出一个自定义的 JSON 消息({msg: success}):

app.delete('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.removeById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send((result === 1)?{msg: 'success'} : {msg: 'error'})
  })
})

Image 注意app.delete()方法是现已废弃的(但仍在旧项目中使用)app.del()的别名。

在本例中,在端口 3000 上实际启动服务器的最后一行是:

app.listen(3000, function(){
  console.log('Express server listening on port 3000')
})

以防万一,这里有rest-api-express/express.js文件的完整代码:

var express = require('express'),
  mongoskin = require('mongoskin'),
  bodyParser = require('body-parser'),
  logger = require('morgan')

var app = express()
app.use(bodyParser())
app.use(logger('dev'))

var db = mongoskin.db('mongodb://@localhost:27017/test', {safe:true})

app.param('collectionName', function(req, res, next, collectionName){
  req.collection = db.collection(collectionName)
  return next()
})

app.get('/', function(req, res, next) {
  res.send('please select a collection, e.g., /collections/messages')
})

app.get('/collections/:collectionName', function(req, res, next) {
  req.collection.find({} ,{limit: 10, sort: {'_id': -1}}).toArray(function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

app.post('/collections/:collectionName', function(req, res, next) {
  req.collection.insert(req.body, {}, function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

app.get('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.findById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send(result)
  })
})

app.put('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.updateById(req.params.id, {$set: req.body}, {safe: true, multi: false}, function(e, result){
    if (e) return next(e)
    res.send((result === 1) ? {msg:'success'} : {msg: 'error'})
  })
})

app.delete('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.removeById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send((result === 1)?{msg: 'success'} : {msg: 'error'})
  })
})

app.listen(3000, function(){
  console.log('Express server listening on port 3000')
})

退出编辑器,在终端中运行以下命令:

$ node express.js

在另一个窗口中(不关闭第一个窗口,让服务器运行),输入:

$ mocha express.test.js

或者

$ ./node_modules/mocha/bin/mocha express.test.js

或者

$ npm test

Mocha 的终端输出应该如下所示:

......
  6 passing (57ms)

在服务器终端窗口中,您应该会看到如下内容:

Express server listening on port 3000
POST /collections/test 200 35.242 ms - 73
GET /collections/test/54724135101acb1334635994 200 4.254 ms - 71
GET /collections/test 200 5.181 ms - 108
PUT /collections/test/54724135101acb1334635994 200 4.037 ms - 17
GET /collections/test/54724135101acb1334635994 200 1.638 ms - 75
DELETE /collections/test/54724135101acb1334635994 200 1.382 ms - 17

如果你真的不喜欢摩卡和/或 BDD,你可以一直用 CURL。例如,下面是如何发出帖子请求:

$ curl -d "name=peter&email=peter337@rpjs.co" http://localhost:3000/collections/proexpressjs-readers
$ curl http://localhost:3000/collections/proexpressjs-readers

在这种情况下,输出是:

[{"name":"peter","email":"peter337@rpjs.co","_id":"541714c23f5b557785700d4c"}]%
...
[{"_id":"541714c23f5b557785700d4c","name":"peter","email":"peter337@rpjs.co"}]%

GET 请求也适用于浏览器。例如,您可以前往http://localhost:3000/collections/proexpressjs-readers获取收藏中的项目列表。

在本教程中,我们的测试比应用代码本身还要长,所以放弃测试驱动开发可能很有诱惑力,但是请相信我,在任何严肃的开发项目中,当您正在开发的应用非常复杂时,TDD 的好习惯将会节省您的工作时间

摘要

当您需要用几行代码构建一个简单的 REST API 服务器时,Express.js 和 Mongoskin 库是很好的资源。稍后,如果您需要扩展这些库,它们还提供了一种配置和组织代码的方法。像 MongoDB 这样的 NoSQL 数据库擅长处理 free-REST API,这意味着你不必定义模式,你可以向它抛出任何数据,它就会被保存。

在下一章中,我们将把 REST API 方法与前端框架 Backbone.js 结合起来,它将从服务器获取数据,编译数据,并在浏览器中呈现 HTML(不像第二十章中的 Todo 应用,它在服务器上处理模板)。


1

2

3

4

二十二、HackHall

HackHall app 是一款真正的 MVC 应用。它有 REST API 服务器,前端客户端用 Backbone.js 和下划线编写。出于本章的目的,我们将通过 mongose ORM/ODM(对象-关系映射/对象-文档映射)为后端 REST API 服务器演示如何使用 Express.js 和 MongoDB。此外,该项目直接使用 OAuth,并通过 Passport、sessions 和 Mocha 进行 TDD。它托管在 Heroku 上,正在积极开发中(见附近的注释)。

Image 本章使用的 HackHall 源代码可以在 3.1.0 版本下的公共 GitHub 资源库(https://github.com/azat-co/hackhall)中获得(https://github.com/azat-co/hackhall/releases/tag/v3.1.0https://github.com/azat-co/hackhall/tree/v3.1.0https://github.com/azat-co/hackhall/archive/v3.1.0.zip)。未来的版本可能与本章的例子不同,可能会有更多的特性。

本章结构如下:

  • 什么是 HackHall?
  • 跑步俱乐部
  • 结构
  • Package.json
  • Express.js app
  • 路线
  • 猫鼬模型
  • 摩卡测试

什么是 HackHall?

HackHall 是一个面向在线社区的开源项目。它在http://hackhall.com的实现是一个为黑客、潮人、设计师、企业家和盗版者(开玩笑)策划的社交网络/会员社区和协作工具。HackHall 社区类似于 Reddit、Hacker News 和脸书团体与监管的结合。可以在http://hackhall.com申请成为会员。

HackHall 项目正处于早期阶段,大致处于测试阶段。我们计划在未来扩展代码库,并引入更多的人来分享技能、智慧和编程热情。您可以在http://youtu.be/N1UILNqeW4k观看 HackHall.com 的快速演示视频。

在这一章中,我们将介绍 3.1.0 版本,它有以下特性:

  • 带有oauth模块(https://www.npmjs.org/package/oauth)和 AngelList API ( https://angel.co/api)的 OAuth 1.0
  • 电子邮件和密码验证
  • 密码哈希
  • 猫鼬模型和模式
  • 模块中带有路线的 Express.js 结构
  • JSON REST API
  • Express.js 错误处理
  • 前端客户端 Backbone.js app(关于 Backbone.js 的更多信息,请下载或在线阅读我的快速原型制作 JS 教程,在http://rapidprototypingwithjs.com/ )
  • 工头的.env环境变量
  • 摩卡的 TDD 测试
  • 基本 Makefile 设置
  • SendGrid 电子邮件通知
  • GitHub 登录

跑步大厅

要获得 HackHall 的源代码,您可以导航到hackhall文件夹或从 GitHub 克隆它:

$ git clone https://github.com/azat-co/hackhall.git
$ git checkout v3.1.0
$ npm install

如果你计划测试一个 AngelList,或者 GitHub 集成(可选),那么你应该作为开发者注册他们的 API 密匙。这样做之后,您需要通过环境变量将值传递给应用。HackHall 对这些敏感的 API 键使用 Heroku 和 Foreman ( http://ddollar.github.io/foreman)设置方法(.env文件)。Foreman gem 是一个命令行工具,用于管理基于 Procfile 的应用。Heroku toolbelt 包含它。要在环境变量中存储键,只需像这样添加一个.env文件(用您自己的值替换=后面的值):

ANGELLIST_CLIENT_ID=254C0335-5F9A-4607-87C0
ANGELLIST_CLIENT_SECRET=99F5C1AC-C5F7-44E6-81A1-8DF4FC42B8D9
GITHUB_CLIENT_ID=9F5C1AC-C5F7-44E6
GITHUB_CLIENT_SECRET=9F5C1AC-C5F7-44E69F5C1AC-C5F7-44E6
GITHUB_CLIENT_ID_LOCAL=9F5C1AC-C5F7-44E1
GITHUB_CLIENT_SECRET_LOCAL=9F5C1AC-C5F7-44E69F5C1AC-C5F7-44E6
...

注意等号(=)前后没有空格。

有了.env文件和值之后,使用foremannodemon:

$ foreman run nodemon server

如果您对foreman感到困惑或者不想安装它,那么您可以用您的环境变量创建一个 shell 文件,并用它来启动服务器。

在您创建了一个 AngelList 应用并注册它之后,您可以在https://angel.co/api获得 AngelList API 密钥。同样,对于 GitHub,你需要注册成为一名开发者,才能创建一个应用并获得 API 密钥。SendGrid 通过 Heroku 插件工作,因此您可以从 Heroku web 界面获得用户名和密码。

下面是我的.env寻找 v3.1.0 的样子(键被占位符代替),其中我有两组 GitHub 键,一组用于本地 app,一组用于生产(hackhall.com ) app,因为每一组的回调 URL 都不一样。当你注册应用时,在 GitHub 上设置回调 URL。

ANGELLIST_CLIENT_ID=AAAAAAAAAAAAAA
ANGELLIST_CLIENT_SECRET=AAAAAAAAAAAAAA
GITHUB_CLIENT_ID=AAAAAAAAAAAAAA
GITHUB_CLIENT_SECRET=AAAAAAAAAAAAAA
GITHUB_CLIENT_ID_LOCAL=AAAAAAAAAAAAAA
GITHUB_CLIENT_SECRET_LOCAL=AAAAAAAAAAAAAA
SENDGRID_USERNAME=AAAAAAAAAAAAAA@heroku.com
SENDGRID_PASSWORD=AAAAAAAAAAAAAA
COOKIE_SECRET=AAAAAAAAAAAAAA
SESSION_SECRET=AAAAAAAAAAAAAA
ANGELLIST_CLIENT_ID_LOCAL=AAAAAAAAAAAAAA
ANGELLIST_CLIENT_SECRET_LOCAL= AAAAAAAAAAAAAA
EMAIL=AAAAAAAAAAAAAA

cookie 和会话密码用于加密 Cookie(浏览器)和会话(存储)数据。

将敏感信息放入环境变量允许我将整个 HackHall 源代码公之于众。我还在 Heroku web 界面中为这个应用设置了一个变量(您可以使用 Heroku config [ https://devcenter.heroku.com/articles/config-vars ]将.env同步到云或从云同步,或者使用 web 界面)。这个变量就是NODE_ENV=production。当我需要确定要使用的 GitHub 应用时,我会使用它(本地应用与主要的实时应用)。

如果您还没有 MongoDB,请下载并安装它。数据库和第三方库超出了本书的范围。然而,你可以在网上找到足够的资料(例如,见http://webapplog.com)和之前提到的用 JS 快速成型。在启动应用之前,我建议运行seed-script.js文件或seed.js文件,用信息填充数据库,如下所述。

要通过运行seed-script.js MongoDB 脚本用默认管理员用户播种数据库hackhall,请输入

$ mongo localhost:27017/hackhall seed-script.js

随意修改seed-script.js 到你喜欢的程度(注意这样做会删除所有以前的数据!).例如,使用您的bcryptjs散列密码(跳到种子数据自动散列的seed.js指令)。稍后您将看到一个散列的例子。

首先,我们清理数据库:

db.dropDatabase();

然后,我们用用户信息定义一个对象:

var seedUser ={
  firstName: 'Azat',
  lastName: 'Mardan',
  displayName: 'Azat Mardan',
  password: 'hashed password',
  email: '1@1.com',
  role: 'admin',
  approved: true,
  admin: true
};

最后,使用 MongoDB shell 方法 将对象保存到数据库:

db.users.save(seedUser);

鉴于seed-script.js是一个 MongoDB shell 脚本,seed.js是一个迷你 Node.js 应用,用于播种数据库。您可以使用以下命令运行 Node.js 数据库播种程序:

$ node seed.js

seed.js程序更全面(它有密码哈希!)比 MongoDB shell 脚本seed-script.js。我们从导入模块开始:

var bcrypt = require('bcryptjs');
var async = require('async');
var mongo = require ('mongodb');
var objectId = mongo.ObjectID;

seed-script.js中类似,我们定义用户对象,只是这次密码是明文/未加密的:

seedUsers = [{...},{...}];

数组对象可能看起来像这样(添加你自己的用户对象!):

{
  firstName: "test",
  lastName: "Account",
  displayName: "test Account",
  password: "hashend password",
  email: "1@1.com",
  role: "user",
  admin: false,
  _id: objectId("503cf4730e9f580200000003"),
  photoUrl: "https://s3.amazonaws.com/photos.angel.co/users/68026-medium_jpg?1344297998",
  headline: "Test user 1",
  approved: true
}

这是将散列我们的普通密码的异步函数:

var hashPassword = function (user, callback) {

bcryptjs模块将 salt 存储在哈希密码中,所以不需要单独存储 salt;10是哈希复杂度(越高越好):

  bcrypt.hash(user.password, 10, function(error, hash) {
    if (error) throw error;
    user.password = hash;
    callback(null, user);
  });
};

在这里,我们定义了稍后会用到的变量:

var db;
var invites;
var users;
var posts;

我们用本地驱动程序连接到 MongoDB:

var dbUrl = process.env.MONGOHQ_URL || 'mongodb://@127.0.0.1:27017/hackhall';
mongo.Db.connect(dbUrl, function(error, client){
  if (error) throw error;
    else {
        db=client;

接下来,我们将集合分配给对象并清理所有用户,以防万一:

invites = new mongo.Collection(db, "invites");
users = new mongo.Collection(db, "users");
posts = new mongo.Collection(db, "posts");
invites.remove(function(){});
users.remove(function(){});

如果希望该脚本也删除帖子,可以取消对该行的注释:

// posts.remove();
 invites.insert({code:'smrules'}, function(){});

插入一个虚拟帖子(在此随意发挥创意):

posts.insert({
          title:'test',
          text:'testbody',
          author: {
           name:seedUsers[0].displayName,
           id:seedUsers[0]._id
           }
}, function(){});

我们使用异步函数,因为散列可能会很慢(这是一件好事,因为较慢的散列更难用暴力破解):

    async.map(seedUsers, hashPassword, function(error, result){
      console.log(result);
      seedUsers = result;
      users.insert(seedUsers, function(){});
      db.close();
    });
  }
});

要启动 MongoDB 服务器,打开一个新的终端窗口并运行:

$ mongod

当 MongoDB 在默认端口为 27017 的 localhost 上运行后,返回到项目文件夹并运行foreman(该命令从 Procfile 中读取):

$ foreman start

或者,可以用nodemon(http://nodemon.io;GitHub: https://github.com/remy/nodemon)带有更明确的foreman命令:

$ foreman run nodemon server

如果你打开浏览器到http://localhost:3000,你应该看到一个类似于图 22-1(3 . 1 . 0 版)所示的登录屏幕。

9781484200384_Fig22-01.jpg

图 22-1 。本地运行的 HackHall v3.1.0 登录页面

输入您的用户名和密码(来自您的seed.jsseed-script.js文件)以获得访问权限。使用无哈希(即普通)版本的密码。

认证成功后,用户被重定向到帖子页面,如图图 22-2 (你的数据,比如帖子名称,会有所不同;“测试”帖子是运行 Mocha 测试的副产品)。

9781484200384_Fig22-02.jpg

图 22-2 。HackHall 帖子页面

如果你点击一个帖子的“喜欢”按钮,就会出现“你现在喜欢这个帖子了!”应显示消息,该岗位上的类似计数器应增加,如图图 22-3 所示。手表按钮也是如此。作者可以编辑和删除他们自己的帖子。管理员可以编辑和删除任何帖子。有人员和个人资料页面,您将在本章后面看到。

9781484200384_Fig22-03.jpg

图 22-3 。HackHall 发布了一个有赞帖子的页面

现在,您已经看到了 HackHall v3.1.0 在本地机器上开箱后的样子。下面几节将带您了解实现该应用时使用的一些概念和模式。这一章没有前几章详细,因为我假设你已经熟悉了那些章节的主题;重复所有的细节会占用太多的空间,可能会让你感到厌烦。

结构

以下是 HackHall 的结构以及每个文件夹和文件所包含内容的简要描述:

  • /api : App 共享路线
  • /models:猫鼬模型
  • /public:主干 app,静态文件,如前端 JavaScript、CSS、HTML
  • /routes:休息 API 路线
  • /tests:摩卡测试
  • 内部(内部)图书馆
  • .gitignore:应该被git忽略的文件列表
  • Makefile:运行测试的生成文件
  • Heroku 部署所需的 Cedar 堆栈文件
  • package.json : NPM 依赖和 HackHall 元数据
  • readme.md:项目描述
  • server.js:主黑客大厅服务器文件
  • 不想与他人分享或泄露的秘密价值

我的项目文件夹内容如图图 22-4 所示。前端应用是用 Backbone.js 编写的,带有下划线模板引擎(HTML 在客户端呈现),它非常广泛,其覆盖范围超出了本书的范围,因为 Backbone.js 有许多替代方案(Angular.js 是最受欢迎的选择之一)。你可以随时从public文件夹:https://github.com/azat-co/hackhall/tree/v3.1.0/public中查找浏览器 app 的源代码。

9781484200384_Fig22-04.jpg

图 22-4 。HackHall 基本文件夹的内容

Package.json

和往常一样,让我们从package.json文件和依赖项开始。我们在本书之前没有使用的“新”库是passport (OAuth 集成)sendgrid(电子邮件通知)mongoose (MondoDB ORM/ODM)和bcryptjs(密码散列)。其他的都应该是你熟悉的。我们将使用 Express.js 中间件模块和实用程序(asyncmocha)。

这就是package.json的样子(自行决定使用新版本):

{
  "name": "hackhall",
  "version": "3.1.0",
  "private": true,
  "main": "server",
  "scripts": {
    "start": "node server",
    "test": "make test"
  },
  "dependencies": {
    "async": "0.9.0",
    "bcryptjs": "2.0.2",
    "body-parser": "1.6.6",
    "cookie-parser": "1.3.2",
    "csurf": "1.5.0",
    "errorhandler": "1.1.1",
    "express": "4.8.1",
    "express-session": "1.7.6",
    "method-override": "2.1.3",
    "mongodb": "1.4.9",
    "mongoose": "3.8.15",
    "mongoose-findorcreate": "0.1.2",
    "mongoskin": "1.4.4",
    "morgan": "1.2.3",
    "oauth": "0.9.12",
    "passport": "0.2.0",
    "passport-github": "0.1.5",
    "sendgrid": "1.2.0",
    "serve-favicon": "2.1.1"
  },

devDependencies类别是生产中不需要的模块:

  "devDependencies": {
    "mocha": "1.21.4",
    "superagent": "0.18.2"
  },
  "engines": {
    "node": "0.10.x"
  }
}

Express.js App

让我们直接跳到server.js文件,看看它是如何实现的。首先,我们声明依赖关系:

var express = require('express'),
  routes = require('./routes'),
  http = require('http'),
  util = require('util'),
  path = require('path'),
  oauth = require('oauth'),
  querystring = require('querystring');

接下来,我们对 Express.js 中间件模块做同样的事情(不需要单独的var,除了显示模块目的的不同):

var favicon = require('serve-favicon'),
  logger = require('morgan'),
  bodyParser = require('body-parser'),
  methodOverride = require('method-override'),
  cookieParser = require('cookie-parser'),
  session = require('express-session'),
  csrf = require('csurf');

接下来,我们有一个内部电子邮件库,它通过一个 Heroku 附件使用 SendGrid:

var hs = require(path.join(__dirname, 'lib', 'hackhall-sendgrid'));

具有不同字体颜色的日志消息很好,但当然是可选的。我们用lib/colors.js中的转义序列来完成这种着色:

var c = require(path.join(__dirname, 'lib', 'colors'));
require(path.join(__dirname, 'lib', 'env-vars'));

护照(http://passportjs.orghttps://www.npmjs.org/package/passporthttps://github.com/jaredhanson/passport)是给 GitHub OAuth 的。使用passport是比使用oauth:更高级的实现 OAuth 的方法

var GitHubStrategy = require('passport-github').Strategy,
  passport = require('passport');

然后,我们初始化应用并配置中间件。环境变量process.env.PORT由 Heroku 填充,并且在本地设置的情况下,依赖于3000。其余的配置你应该从《??》第四章中熟悉了。

app.set('port', process.env.PORT || 3000 );
app.use(favicon(path.join(__dirname,'public','favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(methodOverride());

认证需要传递给cookieParser和会话中间件的值。显然,这些秘密应该是私人的:

app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  secret: process.env.SESSION_SECRET,
  key: 'sid',
  cookie: {
    secret: true,
    expires: false
  },
  resave: true,
  saveUninitialized: true
}));

这就是我们如何提供前端客户端 Backbone.js 应用和其他静态文件,如 CSS:

app.use(express.static(__dirname + '/public'));

错误处理分为三个函数,其中clientErrorHandler()专用于来自 Backbone.js 应用的 AJAX/XHR 请求(用 JSON 响应)。现在,我们只声明函数。稍后我们将使用app.use()来应用它们。

第一个方法logErrors(),检查err是否是一个字符串,如果是,创建一个Error对象。然后,执行到下一个错误处理程序。

function logErrors(err, req, res, next) {
  if (typeof err === 'string')
    err = new Error (err);
  console.error('logErrors', err.toString());
  next(err);
}

如前所述,clientErrorHandler通过检查req.xhr专用于来自 Backbone.js 应用的 AJAX/XHR 请求(用 JSON 响应),它将发送一个 JSON 消息返回或转到下一个处理程序:

function clientErrorHandler(err, req, res, next) {
  if (req.xhr) {
    console.error('clientErrors response');
    res.status(500).json({ error: err.toString()});
  } else {
    next(err);
  }
}

最后一个错误处理程序errorHandler(),将假设请求不是 AJAX/XHR(否则clientErrorHandler()将会捕获它,但是这个顺序将在后面用app.use()定义),并将发回一个字符串:

function errorHandler(err, req, res, next) {
  console.error('lastErrors response');
  res.status(500).send(err.toString());
}

回想一下,我们使用||确定process.env.PORT并依靠本地设置值3000。我们用 MongoDB 连接字符串做类似的事情。我们从环境变量中提取 Heroku 附件 URI 字符串,或者回退到本地设置:

var dbUrl = process.env.MONGOHQ_URL
  || 'mongodb://@127.0.0.1:27017/hackhall';
var mongoose = require('mongoose');

现在,我们创建一个连接:

var connection = mongoose.createConnection(dbUrl);
connection.on('error', console.error.bind(console,
  'connection error:'));

有时记录连接open事件是个好主意:

connection.once('open', function () {
  console.info('Connected to database')
});

猫鼬模型存放在models文件夹中:

var models = require('./models');

这个中间件将提供对我们路由方法中的两个集合的访问:

function db (req, res, next) {
  req.db = {
    User: connection.model('User', models.User, 'users'),
    Post: connection.model('Post', models.Post, 'posts')
  };
  return next();
}

下面几行只是导入的routes/main.js文件授权函数的新名称:

var checkUser = routes.main.checkUser;
var checkAdmin = routes.main.checkAdmin;
var checkApplicant = routes.main.checkApplicant;

然后,我们转到 AngelList OAuth 路由进行 AngelList 登录。这是一个标准的三足 OAuth 1.0 策略,我们启动 auth ( /auth/angellist),将用户重定向到服务提供商(AngelList),然后等待用户从服务提供商(/auth/angellist):

app.get('/auth/angellist', routes.auth.angelList);
app.get('/auth/angellist/callback',
  routes.auth.angelListCallback,
  routes.auth.angelListLogin,
  db,
  routes.users.findOrAddUser);

Image 提示关于 OAuth 和 Node.js OAuth 例子的更多信息,请看我的书用 Node.js 介绍 OAuth(2014),可在https://gumroad.com/l/oauthnode获得。

接下来的几行代码处理 Passport 和 GitHub 登录逻辑。使用 Passport 实现 OAuth 比使用 OAuth 模块需要更少的人工工作。

让我们从app.get('/api/profile')开始,跳到主要的应用途径。Backbone.js 应用使用api/profile,如果用户登录,它将返回一个用户会话。请求通过checkUserdb传输,前者授权,后者填充数据库信息。

*// MAIN*
app.get('/api/profile', checkUser, db, routes.main.profile);
app.delete('/api/profile', checkUser, db, routes.main.delProfile);
app.post('/api/login', db, routes.main.login);
app.post('/api/logout', routes.main.logout);

PostsUsers收藏路线用于操作帖子和用户:

*// POSTS*
app.get('/api/posts', checkUser, db, routes.posts.getPosts);
app.post('/api/posts', checkUser, db, routes.posts.add);
app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
app.delete('/api/posts/:id', checkUser, db, routes.posts.del);

*// USERS*
app.get('/api/users', checkUser, db, routes.users.getUsers);
app.get('/api/users/:id', checkUser, db,routes.users.getUser);
app.post('/api/users', checkAdmin, db, routes.users.add);
app.put('/api/users/:id', checkAdmin, db, routes.users.update);
app.delete('/api/users/:id', checkAdmin, db, routes.users.del);

这些路线适用于尚未获得批准的新成员(即,他们已提交申请):

//APPLICATION
app.post('/api/application', checkAdmin, db, routes.application.add);
app.put('/api/application', checkApplicant, db, routes.application.update);
app.get('/api/application', checkApplicant, db, routes.application.get);

以下是无所不包的路线:

app.get('*', function(req, res){
  res.status(404).send();
});

我们按照我们希望它们被调用的顺序来应用错误处理程序:

app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

require.main === module是一个聪明的技巧,用来确定这个文件是作为独立的还是作为导入的模块执行的:

http.createServer(app);
if (require.main === module) {
  app.listen(app.get('port'), function(){

我们显示蓝色日志消息:

    console.info(c.blue + 'Express server listening on port '
      + app.get('port') + c.reset);
  });
}
else {
  console.info(c.blue + 'Running app as a module' + c.reset)
  exports.app = app;
}

为了节省篇幅,我就不列出hackhall/server.js的完整源代码了,不过大家可以在https://github.com/azat-co/hackhall/blob/v3.1.0/server.js查看。

路线

HackHall routes 位于hackhall/routes文件夹中,分为几个模块:

  • hackhall/routes/index.js:文件夹中server.js与其他路径之间的桥梁
  • hackhall/routes/auth.js:处理 OAuth 与 AngelList API“共舞”的路由
  • hackhall/routes/main.js:登录、注销和其他路径
  • hackhall/routes/users.js:与用户休息 API 相关的路线
  • hackhall/routes/application.js:处理成为用户的申请提交的路线
  • hackhall/routes/posts.js:与岗位休息 API 相关的路线

index.js

让我们看看hackhall/routes/index.js,这里我们已经包含了其他模块:

exports.posts = require('./posts');
exports.main = require('./main');
exports.users = require('./users');
exports.application = require('./application');
exports.auth = require('./auth');

auth . js

在这个模块中,我们用 AngelList API 处理 OAuth 舞蹈。为此,我们依赖于https库:

var https = require('https');

AngelList API 客户端 ID 和客户端秘密在https://angel.co/api获得,并存储在环境变量中。我添加了两个应用:一个用于本地开发,另一个用于生产,如图图 22-5 所示。应用会根据环境选择其中之一:

if (process.env.NODE_ENV === 'production') {
  var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
  var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;
} else {
  var angelListClientId = process.env.ANGELLIST_CLIENT_ID_LOCAL;
  var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET_LOCAL;
}

9781484200384_Fig22-05.jpg

图 22-5 。我的 AngelList 应用

exports.angelList()方法将用户重定向到https://angel.co/api网站进行身份验证。当我们导航到/auth/angellist时,这个方法被调用。在https://angel.co/api/oauth/faq的文档中描述了请求的结构。

exports.angelList = function(req, res) {
  res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClientId + '&scope=email&response_type=code');
}

在用户允许我们的应用访问他们的信息后,AngelList 将他们发送回此路由,以允许我们发出新的(HTTPS)请求来检索令牌:

exports.angelListCallback = function(req, res, next) {
  var token;
  var buf = '';
  var data;
  var angelReq = https.request({

hostpath的值是特定于您的服务提供商的,因此在实现 OAuth 时,您需要查阅提供商的文档。这些是 AngelList API 的值:

    host: 'angel.co',
    path: '/api/oauth/token?client_id=' + angelListClientId +
      '&client_secret=' + angelListClientSecret + '&code=' + req.query.code +
      '&grant_type=authorization_code',
    port: 443,
    method: 'POST',
    headers: {
      'content-length': 0
    }

此时,回调应该有带有令牌的响应(或者错误),所以我们解析响应并检查access_token。如果存在,我们在会话中保存令牌,并继续处理/auth/angellist/callback中的下一个中间件,即angelListLogin。首先,让我们在buf中附加一个保存响应的事件监听器:

  }, function(angelRes) {
    angelRes.on('data', function(buffer) {
      buf += buffer;
    });

然后,我们为end事件附加另一个事件监听器:

    angelRes.on('end', function() {

此时的buf对象应该有一个Buffer类型的完整响应体,所以我们需要将其转换为字符串类型并解析。数据应该只有两个属性,access_tokentoken_type ( 'bearer'):

      try {
        data = JSON.parse(buf.toString('utf-8'));
      } catch (e) {
        if (e) return next(e);
      }

让我们检查一下access_token是否 100%确定:

      if (!data || !data.access_token) return next(new Error('No data from AngelList'));
      token = data.access_token;

现在,我们可以在会话中保存token,并调用下一个中间件:

      req.session.angelListAccessToken = token;
      if (token) {
        next();
      }
      else {
        next(new Error('No token from AngelList'));
      }
    });
  });

请求代码的其余部分完成请求并处理一个error事件:

  angelReq.end();
  angelReq.on('error', function(e) {
    console.error(e);
    next(e);
  });
}

因此,用户被授权访问我们的 AngelList 应用,我们拥有令牌(angelListCallback)。现在,我们可以用之前中间件的令牌(angelListLogin)直接调用 AngelList API 来获取用户概要信息。中间件功能的顺序由路由/auth/angellist/callback决定,所以我们从 HTTPS 请求angelListLogin开始:

exports.angelListLogin = function(req, res, next) {
  var token = req.session.angelListAccessToken;
  httpsRequest = https.request({
    host: 'api.angel.co',

同样,每个服务的确切 URL 也是不同的:

    path: '/1/me?access_token=' + token,
    port: 443,
    method: 'GET'
  },
  function(httpsResponse) {
    var userBuffer = '';
    httpsResponse.on('data', function(buffer) {
      userBuffer += buffer;
    });

下一个事件侦听器将缓冲区类型的对象解析为普通的 JavaScript/Node.js 对象:

    httpsResponse.on('end', function(){
      try {
        data = JSON.parse(userBuffer.toString('utf-8'));
      } catch (e) {
        if (e) return next(e);
      }

在执行的这一点上,系统应该有填充了用户信息的数据字段(/1/me?access_token=...端点)。你可以在图 22-6 中看到这种响应数据的例子。

9781484200384_Fig22-06.jpg

图 22-6 。AngelList 用户信息响应示例

我们仍然需要检查对象是否为空,如果不为空,我们将用户数据保存在请求对象上:

      if (data) {
        req.angelProfile = data;
        next();
      } else
        next(new Error('No data from AngelList'));
      });
    }
  );
  httpsRequest.end();
  httpsRequest.on('error', function(e) {
    console.error(e);
  });
};

在撰写本文时,hackhall/routes/auth.js文件的完整源代码在https://github.com/azat-co/hackhall/blob/v3.1.0/routes/auth.js(随着 HackHall 版本的发展会有所变化)。

main.js

hackhall/routes/main.js文件也很有趣,因为它有这些方法:

  • checkAdmin()
  • checkUser()
  • checkApplicant()
  • login()
  • logout()
  • profile()
  • delProfile()

checkAdmin()函数执行管理员权限的认证。如果会话对象没有携带正确的标志,我们调用带有错误对象的 Express.js next()函数:

exports.checkAdmin = function(request, response, next) {
  if (request.session
    && request.session.auth
    && request.session.userId
    && request.session.admin) {
    console.info('Access ADMIN: ' + request.session.userId);
    return next();
  } else {
    next('User is not an administrator.');
  }
};

同样,我们可以只检查批准的用户,而不检查管理员权限:

exports.checkUser = function(req, res, next) {
  if (req.session && req.session.auth && req.session.userId
    && (req.session.user.approved || req.session.admin)) {
    console.info('Access USER: ' + req.session.userId);
    return next();
  } else {
    next('User is not logged in.');
  }
};

如果应用只是一个未批准的用户对象,我们还可以检查:

exports.checkApplicant = function(req, res, next) {
  if (req.session && req.session.auth && req.session.userId
    && (!req.session.user.approved || req.session.admin)) {
    console.info('Access USER: ' + req.session.userId);
    return next();
  } else {
    next('User is not logged in.');
  }
};

在登录功能中,我们搜索电子邮件。因为我们不在数据库中存储普通密码——我们只存储它的加密散列——我们需要使用bcryptjs来比较密码散列。匹配成功后,我们将用户对象存储在会话中,将auth标志设置为true ( req.session.auth = true),然后继续。否则,请求会失败:

var bcrypt = require('bcryptjs');
exports.login = function(req, res, next) {
  console.log('Logging in USER with email:', req.body.email)
  req.db.User.findOne({
      email: req.body.email
    },null, {
      safe: true
    }, function(err, user) {
      if (err) return next(err);
      if (user) {

我们使用异步的bcryptjs方法compare(),如果普通密码与保存的散列密码匹配,它将返回true:

        bcrypt.compare(req.body.password, user.password, function(err, match) {
          if (match) {

所以,一切都很好:系统分配会话标志并在会话中保存用户信息。这些值将用于所有需要授权(受保护)的路由,以识别用户:

            req.session.auth = true;
            req.session.userId = user._id.toHexString();
            req.session.user = user;

管理员有一个单独的布尔值:

            if (user.admin) {
              req.session.admin = true;
            }
            console.info('Login USER: ' + req.session.userId);

JSON {msg: 'Authorized'}对象是一个您可以定制的任意约定,但是您必须在服务器和客户机上保持它相同(以检查服务器响应):

            res.status(200).json({
              msg: 'Authorized'
            });
          } else {
            next(new Error('Wrong password'));
          }
        });
      } else {
        next(new Error('User is not found.'));
      }
    });
};

注销过程会删除所有会话信息:

exports.logout = function(req, res) {
  console.info('Logout USER: ' + req.session.userId);
  req.session.destroy(function(error) {
    if (!error) {
      res.send({
        msg: 'Logged out'
      });
    }
  });
};

该路径用于配置文件页面,也由 Backbone.js 用于用户验证:

exports.profile = function(req, res, next) {

我们不想公开所有的用户字段,所以我们只将我们想要的字段列入白名单:

  var fields = 'firstName lastName displayName' +
    ' headline photoUrl admin approved banned' +
    ' role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl';

这是一个通过 Mongoose 功能创建的自定义方法,因为它具有相当广泛的逻辑,并且被多次调用:

  req.db.User.findProfileById(req.session.userId, fields, function(err, obj) {
    if (err) next(err);
    res.status(200).json(obj);
  });
};

允许用户删除他们的个人资料很重要。我们利用findByIdAndRemove()方法并删除带有destroy()的会话:

exports.delProfile = function(req, res, next) {
  console.log('del profile');
  console.log(req.session.userId);
  req.db.User.findByIdAndRemove(req.session.user._id, {},
    function(err, obj) {
      if (err) next(err);
      req.session.destroy(function(error) {
        if (err) {
          next(err)
        }
      });
      res.status(200).json(obj);
    }
  );
};

https://github.com/azat-co/hackhall/blob/v3.1.0/routes/main.js可以获得hackhall/routes/main.js文件的完整源代码。

users.js

routes/users.js文件负责与用户集合相关的 RESTful 活动。我们有这些方法:

  • getUsers()
  • getUser()
  • add()
  • update()
  • del()
  • findOrAddUser()

首先,我们定义一些变量:

var path = require('path'),
  hs = require(path.join(__dirname, '..', 'lib', 'hackhall-sendgrid'));

var objectId = require('mongodb').ObjectID;

var safeFields = 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl';

然后,我们定义方法getUsers() ,该方法检索用户列表,其中每一项都只有来自safeFields字符串的属性:

exports.getUsers = function(req, res, next) {
  if (req.session.auth && req.session.userId) {
    req.db.User.find({}, safeFields, function(err, list) {
      if (err) return next(err);
      res.status(200).json(list);
    });
  } else {
    return next('User is not recognized.')
  }
}

getUser()方法用于用户资料页面。对于管理员(当前用户,不是我们获取的用户),我们添加一个额外的字段email,并调用定制的静态方法findProfileById():

exports.getUser = function(req, res, next) {
  var fields = safeFields;
  if (req.session.admin) {
    fields = fields + ' email';
  }
  req.db.User.findProfileById(req.params.id, fields, function(err, data){
    if (err) return next(err);
    res.status(200).json(data);
  })
}

要查看getUser()方法的运行情况,您可以导航到一个用户资料页面,如图 22-7 中的所示。管理员可以在个人资料页面上管理用户的帐户。因此,如果您是管理员,您会看到一个额外的角色下拉列表,为该用户设置角色。

9781484200384_Fig22-07.jpg

图 22-7 。以管理员身份登录时的 HackHall 个人资料页面

add()方法很简单:

exports.add = function(req, res, next) {
  var user = new req.db.User(req.body);
  user.save(function(err) {
    if (err) next(err);
    res.json(user);
  });
};

update()方法也用于批准新用户(approvedNow == true)。如果成功,我们使用内部方法notifyApproved()lib/hackhall-sendgrid.js文件发送一封电子邮件:

exports.update = function(req, res, next) {
var obj = req.body;
  obj.updated = new Date();
  delete obj._id;
  var approvedNow = obj.approved && obj.approvedNow;

approvedNow字段不是 Mongoose 模式中的字段,我们不想存储它。该字段的唯一目的是让系统知道它是常规更新呼叫还是批准:

  delete obj.approvedNow;
  req.db.User.findByIdAndUpdate(req.params.id, {
    $set: obj
  }, {

该选项将为我们提供新的对象,而不是原始对象(默认为true):

    new: true
  }, function(err, user) {
    if (err) return next(err);
    if (approvedNow && user.approved) {
      console.log('Approved... sending notification!');

因此,批准成功,我们可以发送电子邮件:

      hs.notifyApproved(user, function(error, user){
        if (error) return next(error);
        console.log('Notification was sent.');
        res.status(200).json(user);
      })
    } else {

如果是定期更新,而不是批准,那么我们只需发回用户对象:

      res.status(200).json(user);
    }
  });
};

图 22-8 显示了当你以管理员权限登录时,用户界面中的批准是什么样子。管理员可以使用下拉菜单来批准、删除或禁止申请人。

9781484200384_Fig22-08.jpg

图 22-8 。具有管理员权限的 HackHall 人员页面(以管理员身份登录)

删除一个用户,我们调用findByIdAndRemove() :

exports.del = function(req, res, next) {
  req.db.User.findByIdAndRemove(req.params.id, function(err, obj) {
    if (err) next(err);
    res.status(200).json(obj);
  });
};

最后,当用户使用 AngelList 登录时,使用findOrAddUser()方法。您可以使用插件提供的findOrCreate(这就是 GitHub OAuth 流中使用的),但是为了便于学习,最好知道如何自己实现相同的功能。当您将findOrCreate与这个函数进行比较时,它还会强化您的异步思维方式和您对如何重构代码的理解:

exports.findOrAddUser = function(req, res, next) {
  var data = req.angelProfile;
  req.db.User.findOne({
    angelListId: data.id
  }, function(err, obj) {
    console.log('angelList Login findOrAddUser');
    if (err) return next(err);

好了,我们在数据库中查询了用户,但是让我们检查用户是否在那里,如果不在,就创建用户:

if (!obj) {
          console.warn('Creating a user', obj, data);
          req.db.User.create({

我们将 AngelList 响应中需要的所有字段映射/规范化到用户对象。

angelListId: data.id,

我们可以使用这个令牌代表用户发出后续的 API 请求,而无需每次都请求授权和许可:

angelToken: req.session.angelListAccessToken,

为了以防万一,我们也将整个 AngelList 对象存储在angelListProfile : 中

angelListProfile: data,
email: data.email,

data.name是全名,所以我们按空格将它分成一个数组,并分别得到第一个和第二个元素:

firstName: data.name.split(' ')[0],
lastName: data.name.split(' ')[1],
displayName: data.name,
headline: data.bio,

图像只是文件的 URL,而不是二进制字段:

        photoUrl: data.image,
        angelUrl: data.angellist_url,
        twitterUrl: data.twitter_url,
        facebookUrl: data.facebook_url,
        linkedinUrl: data.linkedin_url,
        githubUrl: data.github_url
}, function(err, obj) {
        if (err) return next(err);
  console.log('User was created', obj);

好了,用户文档已经成功创建了。但是系统必须马上让用户登录,所以我们将session标志设置为true:

req.session.auth = true;

我们需要在会话中保存新创建的用户 ID,以便我们可以在来自该客户端的其他请求中使用它:

req.session.userId = obj._id;
req.session.user = obj;

该管理员需要由另一个管理员提升。新用户的默认数据库值由 Mongoose 模式负责(默认值为false)。但是这里需要设置会话值,所以我们默认认证为普通用户:

      req.session.admin = false;
      res.redirect('/#application');
      }
    );
} else {

当用户文档在数据库中时,我们只需登录用户并重定向到帖子或他们的会员申请:

        req.session.auth = true;
        req.session.userId = obj._id;
        req.session.user = obj;
        req.session.admin = obj.admin;
        if (obj.approved) {
          res.redirect('/#posts');
        } else {
          res.redirect('/#application');
        }
      }
    })
}

https://github.com/azat-co/hackhall/blob/v3.1.0/routes/users.js可以获得hackhall/routes/users.js文件的完整源代码。

users.js为人员页面的 REST API routes 提供功能,允许用户访问其他用户的个人资料,如图图 22-9 所示。在这个截图中,第一个 Azat 的配置文件来自于播种数据库。第二个 Azat 的简介是我用 GitHub 登录的。

9781484200384_Fig22-09.jpg

图 22-9 。HackHall 人员页面

application.js

hackhall/routes/application.js文件(“应用”是应用的意思,不是 app 里的!)处理申请加入 HackHall 社区的新用户。他们需要得到批准,以确保只有真正和认真的成员加入 HackHall.com。在您的本地版本中,您可能希望禁止有关提交和批准应用的电子邮件通知。

仅仅为了向数据库(电子邮件成员资格应用)添加一个用户对象(默认情况下使用approved=false),我们使用以下方法:

exports.add = function(req, res, next) {
  req.db.User.create({
    firstName: req.body.firstName,
    lastName: req.body.lastName,
    displayName: req.body.displayName,
    headline: req.body.headline,
    photoUrl: req.body.photoUrl,
    password: req.body.password,
    email: req.body.email,
    angelList: {
      blah: 'blah'
    },
    angelUrl: req.body.angelUrl,
    twitterUrl: req.body.twitterUrl,
    facebookUrl: req.body.facebookUrl,
    linkedinUrl: req.body.linkedinUrl,
    githubUrl: req.body.githubUrl
  }, function(err, obj) {
    if (err) return next(err);
    if (!obj) return next('Cannot create.')
    res.status(200).json(obj);
  })
};

我们让用户用这种方法更新他们应用中的信息:

exports.update = function(req, res, next) {
  var data = req.body;

首先需要删除_id,因为我们不想改变它:

  delete data._id;

findByIdAndUpdate()方法中,我们使用来自会话的用户 ID,而不是来自主体的用户 ID,因为它不可信:

  req.db.User.findByIdAndUpdate(req.session.user._id, {
    $set: data
  }, function(err, obj) {
    if (err) return next(err);
    if (!obj) return next('Cannot save.')

大概可以把整个发回去(obj),因为反正是这个用户的信息:

    res.status(200).json(obj);
  });
};

使用get()功能选择特定对象:

exports.get = function(req, res, next) {
  req.db.User.findById(req.session.user._id,

限制我们返回的字段:

    'firstName lastName photoUrl headline displayName'
      + 'angelUrl facebookUrl twitterUrl linkedinUrl'
      + 'githubUrl', {}, function(err, obj) {
      if (err) return next(err);
      if (!obj) return next('cannot find');
      res.status(200).json(obj);
    })
};

以下是hackhall/routes/applications.js文件的完整源代码:

exports.add = function(req, res, next) {
  req.db.User.create({
    firstName: req.body.firstName,
    lastName: req.body.lastName,
    displayName: req.body.displayName,
    headline: req.body.headline,
    photoUrl: req.body.photoUrl,
    password: req.body.password,
    email: req.body.email,
    angelList: {
      blah: 'blah'
    },
    angelUrl: req.body.angelUrl,
    twitterUrl: req.body.twitterUrl,
    facebookUrl: req.body.facebookUrl,
    linkedinUrl: req.body.linkedinUrl,
    githubUrl: req.body.githubUrl
  }, function(err, obj) {
    if (err) return next(err);
    if (!obj) return next('Cannot create.')
    res.status(200).json(obj);
  })
};

exports.update = function(req, res, next) {
  var data = req.body;
  delete data._id;
  req.db.User.findByIdAndUpdate(req.session.user._id, {
    $set: data
  }, function(err, obj) {
    if (err) return next(err);
    if (!obj) return next('Cannot save.')
    res.status(200).json(obj);
  });
};

exports.get = function(req, res, next) {
  req.db.User.findById(req.session.user._id,
    'firstName lastName photoUrl headline displayName angelUrl facebookUrl twitterUrl linkedinUrl githubUrl', {}, function(err, obj) {
      if (err) return next(err);
      if (!obj) return next('cannot find');
      res.status(200).json(obj);
    })
};

图 22-10 显示了会员申请页面此时的样子。

9781484200384_Fig22-10.jpg

图 22-10 。HackHall 会员申请页面

posts.js

我们需要剖析的最后一个 routes 模块是hackhall/routes/posts.js。它负责添加、编辑和删除帖子,以及评论、观看和喜欢。

我们使用对象 ID 将十六进制字符串转换为正确的对象:

objectId = require('mongodb').ObjectID;

帖子分页的默认值如下:

var LIMIT = 10;
var SKIP = 0;

add()函数处理新帖子的创建:

exports.add = function(req, res, next) {
  if (req.body) {

这里的req.db.Post是可用的,因为定制的db中间件用在大多数路线上:

    req.db.Post.create({
      title: req.body.title,
      text: req.body.text || null,
      url: req.body.url || null,

我们从用户的会话信息中设置帖子的作者:

      author: {
        id: req.session.user._id,
        name: req.session.user.displayName
      }
    }, function(err, docs) {
      if (err) {
        console.error(err);
        next(err);
      } else {
        res.status(200).json(docs);
      }

    });
  } else {
    next(new Error('No data'));
  }
};

为了使用来自请求查询的值limitskip或者缺省值来检索文章列表,我们使用下面的代码:

exports.getPosts = function(req, res, next) {
  var limit = req.query.limit || LIMIT;
  var skip = req.query.skip || SKIP;
  req.db.Post.find({}, null, {
    limit: limit,
    skip: skip,

我们按 ID 对结果进行排序,ID 通常按时间顺序列出结果(为了获得更精确的结果,我们可以在这里使用created字段):

    sort: {
      '_id': -1
    }
  }, function(err, obj) {

此时,我们检查在obj中是否有任何帖子,然后,我们执行一个循环来添加一些助手标志,如adminownlikewatch:

 if (!obj) return next('There are not posts.');
var posts = [];
docs.forEach(function(doc, i, list) {

doc对象是一个 Mongoose 文档对象,它有很多魔力,所以最好将数据转换成一个普通的对象:

var item = doc.toObject();

现在,我们可以检查用户是否有管理员权限,如果用户有,那么我们将item.admin设置为true,但是是在新对象item的属性上。这是多余的,因为客户端应用在其他地方有admin标志,但出于表示的目的,在每个帖子上有这些信息很方便,因为管理员可以编辑和删除任何帖子:

if (req.session.user.admin) {
        item.admin = true;
      } else {
        item.admin = false;
}

下一行检查用户是否是这篇文章的作者:

if (doc.author.id == req.session.userId) {
        item.own = true;
} else {
        item.own = false;
}

这一行检查这个用户是否喜欢这个帖子:

if (doc.likes && doc.likes.indexOf(req.session.user._id) > -1) {
        item.like = true;
       } else {
        item.like = false;
       }

这一行检查这个用户是否观看这个帖子:

        if (doc.watches && doc.watches.indexOf(req.session.user._id) > -1) {
          item.watch = true;
          } else {
            item.watch = false;
          }
          posts.push(item);
});

这里是我们形成响应体的地方:

var body = {};
      body.limit = limit;
body.skip = skip;
      body.posts = posts;

为了包含文档(文章)的总数以便分页,我们需要这个快速查询:

    req.db.Post.count({}, function(err, total) {
      if (err) return next(err);
      body.total = total;
      res.status(200).json(body);
    });
  });
};

对于个人帖子页面,我们需要getPost()方法。我们可以传递我们想要的属性,不是像在users.js中那样作为字符串,而是作为对象:

exports.getPost = function(req, res, next) {
  if (req.params.id) {
    req.db.Post.findById(req.params.id, {

这是限制我们希望从数据库返回的字段的另一种方法:

      title: true,
      text: true,
      url: true,
      author: true,
      comments: true,
      watches: true,
      likes: true
    }, function(err, obj) {
      if (err) return next(err);
      if (!obj) {
        next('Nothing is found.');
      } else {
        res.status(200).json(obj);
      }
    });
  } else {
    next('No post id');
  }
};

功能从数据库中删除特定的帖子。这个代码片段使用了 Mongoose 中的findById()remove()方法。然而,同样的事情只用remove()就可以完成。

exports.del = function(req, res, next) {
  req.db.Post.findById(req.params.id, function(err, obj) {
    if (err) return next(err);

以下只是一个完整性检查,以确认客户端是管理员还是我们将要删除的帖子的作者:

    if (req.session.admin || req.session.userId === obj.author.id) {
      obj.remove();
      res.status(200).json(obj);
    } else {
      next('User is not authorized to delete post.');
    }
  })
};

为了喜欢这篇文章,我们通过在post.likes数组前添加用户 ID 来更新文章条目:

function likePost(req, res, next) {
  req.db.Post.findByIdAndUpdate(req.body._id, {

这是一个简单的 MongoDB 操作数,用于向数组中添加值:

    $push: {
      likes: req.session.userId
    }
  }, {}, function(err, obj) {
    if (err) {
      next(err);
    } else {
      res.status(200).json(obj);
    }
  });
};

同样,当用户执行观察动作时,系统会向post.watches数组添加一个新的 ID:

function watchPost(req, res, next) {
  req.db.Post.findByIdAndUpdate(req.body._id, {
    $push: {
      watches: req.session.userId
    }
  }, {}, function(err, obj) {
    if (err) next(err);
    else {
      res.status(200).json(obj);
    }
  });
};

updatePost()方法是调用 like 或 watch 函数,基于随请求发送的动作标志(req.body.action):

exports.updatePost = function(req, res, next) {
  var anyAction = false;
  if (req.body._id && req.params.id) {

此逻辑用于添加 like:

    if (req.body && req.body.action == 'like') {
      anyAction = true;
      likePost(req, res);

下一个条件是添加观察器:

    } else if (req.body && req.body.action == 'watch') {
      anyAction = true;
      watchPost(req, res);

这一个是给帖子添加评论:

    } else if (req.body && req.body.action == 'comment'
      && req.body.comment && req.params.id) {
      anyAction = true;
      req.db.Post.findByIdAndUpdate(req.params.id, {
        $push: {
          comments: {
            author: {
              id: req.session.userId,
              name: req.session.user.displayName
            },
            text: req.body.comment
          }
        }
      }, {
        safe: true,
        new: true
      }, function(err, obj) {
        if (err) throw err;
        res.status(200).json(obj);
      });

最后,当前面的操作条件都不满足时,updatePost()处理帖子本身的更改(标题、文本等。)由作者或管理员制作(req.body.author.id == req.session.user._id || req.session.user.admin):

    } else if (req.session.auth && req.session.userId && req.body
      && req.body.action != 'comment' &&
      req.body.action != 'watch' && req.body != 'like' &&
      req.params.id && (req.body.author.id == req.session.user._id
      || req.session.user.admin)) {
      req.db.Post.findById(req.params.id, function(err, doc) {

在这个上下文中,doc对象是一个 mongose 文档对象,因此我们为它的属性分配新值并调用save(),这将触发模型中定义的预保存钩子(在下一节“mongose 模型”中讨论):

        if (err) next(err);
        doc.title = req.body.title;
        doc.text = req.body.text || null;
        doc.url = req.body.url || null;
        doc.save(function(e, d) {
          if (e) return next(e);

发送回更新的对象是一个规则:

          res.status(200).json(d);
        });
      })
    } else {
      if (!anyAction) next('Something went wrong.');
    }

  } else {
    next('No post ID.');
  }
};

https://github.com/azat-co/hackhall/blob/v3.1.0/routes/posts.js可以获得hackhall/routes/posts.js文件的完整源代码。

这就完成了新帖子页面的路径编码(见图 22-11 ),用户可以在这里创建一个帖子(例如,一个问题)。

9781484200384_Fig22-11.jpg

图 22-11 。HackHall 新帖子页面

我们完成了路线文件!你还记得 HackHall 是一个真正的 MVC 应用吗?接下来,我们将覆盖模型。

猫鼬模型

理想情况下,在一个大的应用中,我们应该将每个模型分解到一个单独的文件中。现在,在 HackHall 应用中,我们在hackhall/models/index.js中拥有它们。

和往常一样,我们的依赖项在顶部看起来更好:

var mongoose = require('mongoose');

该引用将用于 Mongoose 数据类型:

var Schema = mongoose.Schema;

此数组将用作枚举类型:

var roles = 'user staff mentor investor founder'.split(' ');

帖子模型表示一个帖子及其赞、评论和关注。架构中的每个属性都为该属性设置了特定的行为。例如,required表示该属性是必需的,type是猫鼬/BSON 数据类型。

Image 提示想了解更多关于猫鼬的信息,请查阅其官方文档(https://gumroad.com/l/mongoose)、*实用 Node.js * (Apress,2014),以及新的在线课程。

我们用操作数定义Schema:

var Post = new Schema ({

然后,我们有一个必需的title字段(Stringtype,它会自动删除开头和结尾的空白:

  title: {
    required: true,
    type: String,
    trim: true,

RegExp 表示“一个单词、一个空格或任何字符,.!?”,长度在 1 到 100 个字符之间:

    match: /^([\w ,.!?]{1,100})$/
  },

然后,我们用最多 1000 个字符定义 url(对于长 URL 应该足够了吧?)并打开修剪:

  url: {
    type: String,
    trim: true,
    max: 1000
  },

我们为text 定义类似的字段属性:

  text: {
    type: String,
    trim: true,
    max: 2000
  },

comments 是这篇文章的评论数组。每个评论对象都有一个textauthor。作者id是对User模式的引用:

  comments: [{
    text: {
      type: String,
      trim: true,
      max:2000
    },
    author: {
      id: {
        type: Schema.Types.ObjectId,
        ref: 'User'
      },
      name: String
    }
  }],

帖子可以被用户观看或喜欢。这些特性是通过使用带有用户 id 的数组watcheslikes来实现的:

  watches: [{
    type: Schema.Types.ObjectId,
    ref: 'User'
  }],
  likes: [{
    type: Schema.Types.ObjectId,
    ref: 'User'
  }],

接下来,我们输入作者信息并使嵌套对象中的每个字段成为必填字段:

  author: {
    id: {
      type: Schema.Types.ObjectId,
      ref: 'User',
      required: true
    },
    name: {
      type: String,
      required: true
    }
  },

最后,我们添加了时间和日期字段。最好有事件的时间戳,比如这篇文章是什么时候创建的,最后一次更新是什么时候。为此,我们使用Date.now作为默认字段。updated属性将由预保存钩子设置,也可以在每个save()上手动设置。(预保存钩子代码在这个模式代码之后提供。):

  created: {
    type: Date,
    default: Date.now,
    required: true
  },
  updated: {
    type: Date,
    default: Date.now,
    required: true
  }
});

回到updated字段,为了确保我们不必在每次更新(save())帖子时手动设置时间戳,我们使用了一个预保存挂钩来检查字段是否被修改(是否有新值)。如果它没有被修改,那么我们用一个新的日期和时间来设置它。这个钩子只有在你呼叫save()的时候才起作用;当你使用update()或类似的方法时就不会了。回调有一个异步next()函数,你可能会在 Express.js 中间件中看到:

Post.pre('save', function (next) {
  if (!this.isModified('updated')) this.updated = new Date;
  next();
})

User模型也可以作为应用对象(当approved=false时)。让我们将模式定义如下:

var User = new Schema({
  angelListId: String,

Mixed类型允许我们存储任何东西:

  angelListProfile: Schema.Types.Mixed,
  angelToken: String,
    firstName: {
    type: String,
    required: true,
    trim: true
  },
  lastName: {
    type: String,
    required: true,
    trim: true
  },
  displayName: {
    type: String,
    required: true,
    trim: true
  },
  password: String,
  email: {
    type: String,
    required: true,
    trim: true
  },

角色是enum,因为该值只能是来自roles数组 ( [user, staff, mentor, investor, founder])的值之一:

  role: {
    type: String,
    enum: roles,
    required: true,
    default: roles[0]
  },

以下是一些必需的布尔标志:

  approved: {
    type: Boolean,
    default: false
  },
  banned: {
    type: Boolean,
    default: false
  },
  admin: {
    type: Boolean,
    default: false
  },

现在是简短的简历陈述:

  headline: String,

我们不会存储照片二进制文件,只存储它的 URL:

  photoUrl: String,

angelList是一个松散的类型,将具有 AngelList 配置文件:

  angelList: Schema.Types.Mixed,

最好用日志来跟踪文档的创建时间和最后一次更新时间(我们在users.jsupdate()方法中手动设置时间):

  created: {
    type: Date,
    default: Date.now
  },
  updated: {
    type: Date,
    default: Date.now
  },

我们需要一些社交媒体网址:

  angelUrl: String,
  twitterUrl: String,
  facebookUrl: String,
  linkedinUrl: String,
  githubUrl: String,

我们将该用户创作、喜欢、观看和评论的帖子的 id 引用为对象数组(它们将是ObjectID s):

  posts: {
    own: [Schema.Types.Mixed],
    likes: [Schema.Types.Mixed],
    watches: [Schema.Types.Mixed],
    comments: [Schema.Types.Mixed]
  }
});

为了方便起见,我们应用了findOrCreate插件(https://www.npmjs.org/package/mongoose-findorcreate):

User.plugin(findOrCreate);

Mongoose 插件的行为类似于迷你模块。这允许您向模型添加额外的功能。添加额外功能的另一种方式是编写自己的自定义方法。这种方法可以是静态的(附加到实体的整个类别)或实例(附加到特定的模型)。

在《routes》中,你已经看过两次findProfileById():一次在main.js,一次在users.js。为了避免重复,代码被抽象为User模式的一个 Mongoose 静态方法。它检索信息,如评论、喜欢等。这就是为什么我们有多个嵌套的猫鼬叫声。

findProfileById()方法最初看起来可能有点复杂,但是这里没有什么困难——只需要几个嵌套的数据库调用,这样我们就可以获得完整的用户信息。这些信息不仅包括用户名、电子邮件地址等等,还包括用户发表的所有帖子、喜欢、关注和评论。这些信息用于个人资料页面上的游戏化目的,将评论、喜欢和观看的数量转换为点数。但是让我们从第一个基本查询开始,限制我们请求的字段(以避免泄露密码和电子邮件地址):

User.statics.findProfileById = function(id, fields, callback) {
  var User = this;
  var Post = User.model('Post');

  return User.findById(id, fields, function(err, obj) {
    if (err) return callback(err);
    if (!obj) return callback(new Error('User is not found'));

找到用户后,我们通过使用_iddisplayName找到用户的帖子。字段选项设置为null,这样我们可以传递其他参数,结果按照创建日期排序。在回调中,我们检查错误,如果有错误就退出(callback(err))。

    Post.find({
      author: {
        id: obj._id,
        name: obj.displayName
      }
    }, null, {
      sort: {
        'created': -1
      }
    }, function(err, list) {

处理每个嵌套回调的错误是至关重要的:

      if (err) return callback(err);
      obj.posts.own = list || [];

现在我们已经将该用户的帖子列表保存到了obj.posts.own中,下一个查询将查找该用户喜欢的所有帖子:

      Post.find({
        likes: obj._id
      }, null, {

时间顺序由created保证:

        sort: {
          'created': -1
        }
      }, function(err, list) {
        if (err) return callback(err);

万一这个用户不喜欢任何帖子,我们用一个空数组来解释:

        obj.posts.likes = list || [];

此查询获取该用户观看的帖子:

        Post.find({
          watches: obj._id
        }, null, {
          sort: {
            'created': -1
          }
        }, function(err, list) {

先前上下文中的 errlist对象被这个闭包的errlist所掩盖,但我们并不在乎。这种风格允许变量名重用:

          if (err) return callback(err);
          obj.posts.watches = list || [];

最后一个查询查找该用户留下评论的帖子:

          Post.find({
            'comments.author.id': obj._id
          }, null, {
            sort: {
              'created': -1
            }
          }, function(err, list) {
            if (err) return callback(err);
            obj.posts.comments = [];

在我们获得该用户留下评论的帖子列表后,可能会有一些帖子中该用户留下了不止一条评论。出于这个原因,我们需要仔细检查帖子列表和每个评论,并将作者 ID 与用户 ID 进行比较。如果它们匹配,那么我们将该注释包含到列表中:

            list.forEach(function(post, key, arr) {
              post.comments.forEach(function(comment, key, arr) {
                if (comment.author.id.toString() == obj._id.toString())
                  obj.posts.comments.push(comment);
              });
            });

最后,我们用正确的数据和空错误调用回调:

            callback(null, obj);
          });
        });
      });
    });
  });
}

最后,我们导出模式对象,以便可以将它们编译成另一个文件中的模型:

exports.Post = Post;
exports.User = User;

hackhall/models/index.js的完整源代码可在https://github.com/azat-co/hackhall/blob/v3.1.0/models/index.js获得。

摩卡测试

使用 REST API 服务器架构的一个好处是,每条路线以及整个应用都变得非常容易测试。通过测试的保证是开发过程中的一个很好的补充——所谓的测试驱动开发方法在第二十一章中介绍。

HackHall 测试位于tests文件夹中,包括:

  • hackhall/tests/application.js:未批准用户信息的功能测试
  • hackhall/tests/posts.js:岗位功能测试
  • hackhall/tests/users.js:用户功能测试

为了运行测试,我们利用一个 Makefile。我喜欢在 Makefile 中有不同的目标,因为这给了我更多的灵活性。以下是本例中的任务:

  • test:运行tests文件夹中的所有测试
  • test-w:每次有文件更改时重新运行测试
  • users:对用户相关的路线进行tests/users.js测试
  • posts:运行tests/posts.js岗位相关路线测试
  • application:运行tests/application.js测试应用相关的路由

Makefile 可能是这样的,从 Mocha 的选项开始:

REPORTER = list
MOCHA_OPTS = --ui tdd

然后我们定义一个任务test:

test:
        clear
        echo Seeding **********************
        node seed.js
        echo Starting test **********************
        foreman run ./node_modules/mocha/bin/mocha \
        --reporter $(REPORTER) \
        $(MOCHA_OPTS) \
        tests/*.js
        echo Ending test

同样,我们还定义了其他目标:

test-w:
        ./node_modules/mocha/bin/mocha \
        --reporter $(REPORTER) \
        --growl \
        --watch \
        $(MOCHA_OPTS) \
        tests/*.js

users:
        clear
        echo Starting test **********************
        foreman run ./node_modules/mocha/bin/mocha \
        --reporter $(REPORTER) \
        $(MOCHA_OPTS) \
        tests/users.js
        echo Ending test

posts:
        clear
        echo Starting test **********************
        foreman run ./node_modules/mocha/bin/mocha \
        --reporter $(REPORTER) \
        $(MOCHA_OPTS) \
        tests/posts.js
        echo Ending test

application:
        clear
        echo Starting test **********************
        foreman run ./node_modules/mocha/bin/mocha \
        --reporter $(REPORTER) \
        $(MOCHA_OPTS) \
        tests/application.js
        echo Ending test

.PHONY: test test-w users posts application

因此,我们可以用$ make$ make test命令开始测试(要运行示例中的 Makefile,您必须有foreman工具和.env变量)。

所有 36 个测试都应该通过(在 HackHall v3.1.0 中撰写本文时),如图 22-12 所示。

9781484200384_Fig22-12.jpg

图 22-12 。运行所有摩卡测试的结果

测试使用一个名为superagent ( https://npmjs.org/package/superagent)的库;GitHub: https://github.com/visionmedia/superagent。这些测试在概念上类似于第二十一章中针对 REST API 的测试。我们登录,然后发出一些请求,同时检查它们的正确响应。

例如,这是hackhall/tests/application.js的开始,其中我们有一个带有散列密码的虚拟用户对象(bcrypt.hashSync()):

var bcrypt = require('bcryptjs');

var user3 = {
  firstName: 'Dummy',
  lastName: 'Application',
  displayName: 'Dummy Application',
  password: bcrypt.hashSync('3', 10),
  email: '3@3.com',
  headline: 'Dummy Application',
  photoUrl: '/img/user.png',
  angelList: {blah:'blah'},
  angelUrl: 'http://angel.co.com/someuser',
  twitterUrl: 'http://twitter.com/someuser',
  facebookUrl: 'http://facebook.com/someuser',
  linkedinUrl: 'http://linkedin.com/someuser',
  githubUrl: 'http://github.com/someuser'
}

var app = require ('../server').app,
  assert = require('assert'),
  request = require('superagent');

我们启动服务器:

app.listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

下一行将存储客户机对象,以便我们可以作为该用户登录并发出授权请求:

var user1 = request.agent();
var port = 'http://localhost:'+app.get('port');
var userId;

我们使用由seed.js创建的管理员用户:

var adminUser = {
  email: 'admin-test@test.com',
  password: 'admin-test'
};

接下来,我们创建一个测试套件:

suite('APPLICATION API', function (){

这是一个测试套件准备(目前为空):

  suiteSetup(function(done){
    done();
  });

下面是对/api/login的 POST 调用的第一个测试用例定义:

  test('log in as admin', function(done){
    user1.post(port+'/api/login').send(adminUser).end(function(res){
        assert.equal(res.status,200);
      done();
    });
  });

让我们检查一下我们是否可以获得受保护的资源/api/profile:

  test('get profile for admin',function(done){
    user1.get(port+'/api/profile').end(function(res){
        assert.equal(res.status,200);
      done();
    });
  });
  test('submit application for user 3@3.com', function(done){

在这里,我们使用user3数据和散列密码创建一个新的成员资格应用:

    user1.post(port+'/api/application').send(user3).end(function(res){
      assert.equal(res.status,200);
      userId = res.body._id;
      done();
    });
  });

然后,我们注销user1并检查我们是否已经注销:

  test('logout admin',function(done){
    user1.post(port+'/api/logout').end(function(res){
        assert.equal(res.status,200);
      done();
    });
  });
  test('get profile again after logging out',function(done){
    user1.get(port+'/api/profile').end(function(res){
        assert.equal(res.status,500);
      done();
    });
  });

现在,我们尝试使用普通密码作为user3登录,就像在网页上输入一样(系统将对其进行哈希处理,以便与哈希密码进行比较):

  test('log in as user3 - unapproved', function(done){
    user1.post(port+'/api/login').send({email:'3@3.com', password:'3'}).end(function(res){
        assert.equal(res.status, 200);
      done();
    });
  });

...

假设您已经从这个测试用例中获得了一般的想法,那么就没有必要列出所有平凡的测试用例。当然,你可以在https://github.com/azat-co/hackhall/tree/v3.1.0/tests获得hackhall/tests/application.jshackhall/tests/posts.jshackhall/tests/users.js的全部内容。

Image 注意不要在数据库中存储普通密码/密钥。任何严肃的制作应用至少应该在存储密码 1 之前加盐。用bcryptjs代替!

到目前为止,您应该能够在本地运行应用和测试(通过从书中复制或者下载代码)。如果你得到了 API 密匙,你应该可以用 AngelList 和 GitHub 登录,也可以用 SendGrid 收发邮件。至少,您应该能够使用您在数据库播种脚本中指定的电子邮件和密码在本地登录。

摘要

现在你知道了构建 HackHall 所用到的所有技巧和窍门,包括重要的、真实的生产应用组件,比如 REST API 架构、OAuth、Mongoose 及其模型、Express.js 应用的 MVC 结构、环境变量的访问等等。

如本章所述,HackHall 仍在积极开发中,因此代码将继续发展。确保您遵循 GitHub 上的资源库。您可以访问 live HackHall.com 应用,并通过申请会员资格加入社区!当然,您可以通过提交拉取请求来做出贡献。

本章总结了我们对 Express.js 和相关 web 开发主题的研究。涵盖一个不断发展的框架是一项困难的任务,类似于向一个移动的目标射击,所以我在这一章的目标是让你获得最新的信息,最重要的是,向你展示一些更基本的方面,比如代码组织。我还花了很多精力解释和重复中间件模式的例子。如果您面临截止日期的压力,或者只是喜欢即时学习(在需要时学习,而不是为未来学习),那么您会发现大量代码可以复制并粘贴到您自己的项目中。我知道构建自己的项目比从教程中借用另一个抽象应用更有趣。

我希望你喜欢这些例子和这本书!我想通过推特(@azat_co)和电子邮件(hi@azat.co)收到你的来信。下面的附录将作为参考。别忘了领取你的两页纸的 Express.js 4 备忘单(下载链接在附录 C )。


1

二十三、附录 A:相关阅读和资源

这个简短的附录提供了几个最有用的 Node.js 资源,供进一步学习使用:

  • 其他 Node.js 框架
  • Node.js 书籍
  • JavaScript 经典

其他 Node.js 框架

毫无疑问,Express.js 框架是 Node.js web 服务中最成熟、最流行、最健壮、经过测试和使用最多的项目。截至本文撰写之时,Express.js 也是来自 NPM 社区的明星数量最多的 NPM 知识库,如图图 A-1 所示。依赖 Express.js 2.x 和 3.x 的真实制作 app 有很多,包括 Storify 1 (被 LiveFyre 2 收购)、DocuSign、 3 新 MySpace、 4 LearnBoost、 5 Geeklist、6 10

9781484200384_FigAppA-01.jpg

图 A-1 。Express.js 是最受欢迎的 NPM 知识库

尽管如此,Express.js 还有很多可供选择的 Node.js 框架。为了帮助开发人员浏览众多选项,我创建了一个类似于 TodoMVC 集合(http://todomvc.com):Node 框架(http://nodeframework.com),其 MVC 框架页面如图图 A-2 所示。顺便说一下,一些更全面的框架依赖于 Express.js(例如,SailsJS 11 ),所以您开始了解 Express.js 是件好事!

9781484200384_FigAppA-02.jpg

图 A-2 。Node Frameworks 提供了 Node.js 框架及其统计信息的列表

Node.js 书籍

有关 Node.js 堆栈的其他组件(如数据库和 WebSockets)的更多核心概述和/或信息,请参考以下资源:

  • Azat Mardan (Apress,2014 年)的《实用 Node.js:构建真实世界的可扩展 web 应用》:这是一个循序渐进的指南,帮助您学习如何构建可扩展的真实世界的 web 应用,从安装 Express.js 到编写全栈 Web 应用。
  • JS 快速原型:敏捷 JavaScript 开发12(Azat Mardan,2013):一本关于 Node.js、MongoDB 和 Backbone.js 的初级到中级书籍
  • JavaScript 和 Node 基础知识:基本知识集锦13(Azat Mardan,2014):关于浏览器 JS 和 Node 的简单但重要的概念的简短阅读。
  • 用 Node.js 介绍 OAuth:Twitter API OAuth 1.0,OAuth2.0,OAuth Echo,Everyauth 和 OAuth 2.0 服务器示例14(Azat Mardan,2014):一本关于不同 OAuth 场景的迷你书。
  • Colin J. Ihrig (Apress,2013 年)为开发人员编写的 Pro Node.js):这是一本关于 Node.js 的综合性低级书籍,没有任何非核心模块。
  • Node.js in Action ,作者 Mike Cantelon 等人(Manning Publications,2013):一本由多位作者撰写的关于 Express.js 和其他主题的书。
  • 学习 Node,作者 Shelley Powers (O'Reilly Media,2012):涵盖了 Express、MongoDB、Mongoose 和 Socket.IO。
  • Node 食谱,作者 David Mark Clements(Packt Publishing,2012):涵盖了数据库和 WebSockets。
  • Node:启动并运行,作者汤姆·休斯-克劳奇和迈克·沃森(O'Reilly Media,2012):Node . js 的简要概述
  • Smashing node . js:JavaScript Everywhere,作者 Guillermo Rauch (Wiley,2012):涵盖了 Express.js、Jade 和 Stylus,作者是 Mongoose ORM for MongoDB 的创建者。

JavaScript 经典

要想更深入地了解 JavaScript 这种最容易被误解也是最流行的编程语言,请务必阅读以下经典著作:

  • 雄辩的 JavaScript,第二版,作者 Marijn Haverbeke(无淀粉出版社,2014):JavaScript 编码中的编程基础。
  • 道格拉斯·克洛克福特(O'Reilly Media,2008 年)的《JavaScript:好的部分》(The Good Parts )讲述了 JavaScript 语言中棘手的部分。

课程

如果你喜欢这本书,那么可以看看 Azat 的 Node.js 课程:Node Program ( http://nodeprogram.com)和 Mongoose 课程(http://mongoosecourse.com)。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

二十四、附录 B:将 Express.js 3.x 迁移到 4.x:中间件、路由和其他变化

Express.js 4 ( http://expressjs.com)是针对 web 应用、服务和 API 的最流行、最成熟、最健壮的 Node.js 框架的最新(截至 2014 年 5 月)主要版本。在从 3.x 到 4.x 的过渡中有一些突破性的变化,因此本附录作为一个简短的迁移指南,包括以下内容:

  • 在 Express.js 4 中引入非捆绑中间件
  • 从 Express.js 4 应用中删除不推荐使用的方法
  • Express.js 4 的其他更改
  • 探索新的 Express.js 4 路由实例以及如何链接它
  • 更多 Express.js 4 迁移阅读链接

即使您不打算使用版本 3,本指南也可能对您有用,因为它阐明了从 Express 3 到 4 进行更改的基本原理,以及这些更改背后的哲学。

在 Express.js 4 中引入非捆绑中间件

让我们从最大的改变开始,这个改变将打破你的大多数 Express.js 3.x 项目。这也是讨论最多的(也是期待已久的?)网上的 Express.js 新闻。是的,它是非捆绑的中间件。

就我个人而言,我不确定这是好消息还是坏消息,因为我喜欢不必声明额外的依赖项。然而,我也看到了支持非绑定的理由,包括好处,比如保持 Express.js 模块小,独立升级中间件包,等等。

那么,什么是非捆绑中间件呢?还记得我们能够简单地通过键入app.use(express.middlwarename())来使用的神奇中间件吗?嗯,它们来自于Connect库,但现在它们不再是 Express.js 的一部分。例如,习惯上用 Express.js 3.x 编写app.use(express.cookieParser())。这些模块对于几乎任何 web 应用都是必不可少的。它们是Connect库的一部分,但是 Express.js 4.x 没有Connect作为依赖项。这意味着,如果我们想使用中间件(我们确实这么做了!),我们需要明确地包含中间件,就像这样:

$ npm install body-parse@1.0.2 --save

然后,在 Express.js 主配置文件(如app.js)中,我们这样使用包含的模块:

var bodyParser = require('body-parse')
// ... other dependencies
app.use(bodyParser())
// ... some other Express.js app configuration

表 B-1 描述了开发人员将不得不替换的非捆绑中间件:除了静态之外的所有中间件。没错,静电被遗漏了(为了方便?).下表列出了 Express.js 3.x 中间件名称及其在 Express.js 4.x 中的 NPM 模块对应项。

表 B-1 。中间件比较

|

Express.js 3.x

|

Express.js 4.x

|

github link(github 链接)

|
| --- | --- | --- |
| express.bodyParser | body-parser | https://github.com/expressjs/body-parser |
| express.compress | compression | https://github.com/expressjs/compression |
| express.timeout | connect-timeout | https://github.com/expressjs/timeout |
| express.cookieParser | cookie-parser | https://github.com/expressjs/cookie-parser |
| express.cookieSession | cookie-session | https://github.com/expressjs/cookie-session |
| express.csrf | csurf | https://github.com/expressjs/csurf |
| express.error-handler | errorhandler | https://github.com/expressjs/errorhandler |
| express.session | express-session | https://github.com/expressjs/session |
| express.method-override | method-override | https://github.com/expressjs/method-override |
| express.logger | morgan | https://github.com/expressjs/morgan |
| express.response-time | response-time | https://github.com/expressjs/response-time |
| express.favicon | serve-favicon | https://github.com/expressjs/serve-favicon |
| express.directory | serve-index | https://github.com/expressjs/serve-index |
| express.static | serve-static | https://github.com/expressjs/serve-static |
| express.vhost | vhost | https://github.com/expressjs/vhost |

让事情变得更复杂的是,Express.js/Connect 团队放弃了对表 B-2 中所示模块的支持,并建议您使用替代方案:

表 B-2 。丢弃模块及其替代品

|

丢弃的模块

|

可供选择的事物

|
| --- | --- |
| cookieParser | cookieskeygrip |
| limit | 生体 |
| multipart | connect-multipartyconnect-busboy |
| query | qs |
| staticCache | stconnect-static |

这些未捆绑的 Express.js/Connect 模块很多都需要维护者1;这是您在 Node.js 领域取得进展的机会!

从 Express.js 4 应用中删除不推荐使用的方法

本节讨论那些不推荐使用的方法以及如何替换它们。

app.configure()

大多数人可能从来没有使用过app.configure(),这是一个很好但不重要的方法,主要用于设置环境。如果你用过,就把app.configure('name', function(){...})换成if (process.env.NODE_ENV === 'name') {...}。例如,这个旧的 Express.js 3 生产配置

app.configure('production', function() {
  app.set('port', 80)
})

在 Express.js 4.x 中变成以下内容:

if (process.env.NODE_ENV === 'production') {
  app.set('port', 80)
}

app .路由

Express.js 4 的一个好的变化是它不再需要编写app.router!所以现在,基本上,中间件和路由的顺序是唯一重要的事情,而在 Express.js 3x 中,开发人员可以通过将app.router放在中间的某个位置来增加执行的顺序。

如果您有任何应该在路由之后订购的中间件,那么将它移动到路由之后的,按照您想要的顺序。

例如,在 Express.js 3.x 配置中的路由之后执行的错误处理中间件:

app.use(express.cookieParser())
app.use(app.router)
app.use(express.errorHandler())
app.get('/', routes.index)
app.post('/signup', routes.signup)

在 Express.js 4.x 中迁移到以下代码:

var cookieParse = require('cookie-parser')
var errorHandler = require('errorhandler')
...
app.use(cookieParser())
app.get('/', routes.index)
app.post('/signup', routes.signup)
app.use(errorHandler())

换句话说,app.use()和带动词的路线,如app.get()app.post()app.put()app.del()成了对等物。

res.on(“标题”)

从 Connect 3 中移除了res.on('header')事件。

res.charset

在 Express.js 4.x 中,用res.type()res.set('content-type')代替 Express.js 3.x 中的res.charset

利斯特 headersent

在 Express.js 4.x 中,用res.headersSent代替res.headerSent

req.accepted()

在 Express.js 4.x 中,用req.accepts()代替req.accepted()

Express.js 4.x 中的req.accepts由模块accepts ( https://github.com/expressjs/accepts)提供支持,GitHub 文档表明该模块是从 Koa ( http://koajs.com)中提取的,用于一般用途。

Express.js 4 的其他更改

本节介绍了 Express.js 4 的其他一些变化。

app.use()

令人惊讶的是,app.use()现在接受 URL 参数。这是使app.use()和动词 route 方法相等并减少混淆的又一步。参数在req.params对象中。

例如,如果我们需要从 URL 获得一个 ID,在 Express.js 4.x 中间件中我们可以写

app.use('/posts/:slug', function(req, res, next) {
  req.db.findPostBySlug(req.params.slug, function(post){
  ...
  })
})

资源位置()

在 Express.js 4 中,res.location()不再解析相对 URL。

app.route()

参见下一节“探索新的 Express.js 4 Route 实例以及如何链接它”,了解关于 Express.js 4 中app.route()角色的详细信息。

json 空间

在 Express.js 4 中,json spaces在开发中默认是关闭的。

请求参数

req.params是对象,不是数组。

没有.本地

res.locals现在是一个物体。

req.is

Express.js 4.x 中的req.is已经被模块type-is ( https://github.com/expressjs/type-is)所取代,根据 GitHub 文档,该模块也是从 Koa.js ( http://koajs.com)中提取出来用于一般用途。

Express.js 命令行生成器

对于命令行生成器,请使用

$ sudo npm install -g express-generator

而不是普通的老式$ sudo npm install -g express

探索新的 Express.js 4 路由实例以及如何链接它

app.route()方法为我们提供了新的 Express.js 4 route 实例。在探究之前,我们先来看看路由本身。

在 Express.js 4.x 中,Router类得到了增强。在 Express.js 3.x 中,app 实例使用 router,但现在我们可以创建许多 route 实例,并通过附加特定的中间件和其他逻辑将它们用于特定的路径。这可以用来重组代码。

这里有一个开发者如何在 Express.js 4.x 中使用Router的基本例子,假设我们有两个类别的评论:书籍和游戏。审查的逻辑类似于路由,并被打包成路由:

var express = require('express')
var app = express()
var router = express.Router()

router.use(function(req, res, next) {
  //process each request
});

router.get('/', function(req, res, next) {
  // get the home page for that entity
  next();
});

router.get('/reviews', function(req, res, next) {
  // Get the reviews for that entity
  next();
});

app.use('/books', router);
app.use('/games', router);

app.listen(3000);

app.route()router.route()返回新的 Express.js 4.x route 实例,我们可以这样链:

router.route('/post/:slug')
  .all(function(req, res, next) {
    // Runs each time
    // We can fetch the post by id from the database
  })
  .get(function(req, res, next) {
    //Render post
  })
  .put(function(req, res, next) {
    //Update post
  })
  .post(function(req, res, next) {
    //Create new comment
  })
  .del(function(req, res, next) {
    //Remove post
  })

在 Express.js 3.x 中,如果没有路由链,我们将不得不反复输入相同的路径(增加了输入错误的风险):

router.all('/post/:slug', function(req, res, next) {
  // runs each time
  // we can fetch the post by ID from the database
})
router.get('/post/:slug', function(req, res, next) {
  //render post
})
router.put('/post/:slug', function(req, res, next) {
  //update post
})
router.post('/post/:slug', function(req, res, next) {
  //create new comment
})
router.delete('/post/:slug', function(req, res, next) {
  //remove post
})

同一个路由实例也可以有自己的中间件、param 和 HTTP 动词方法(如上所示)。

更多 Express.js 4 迁移阅读链接

所以,总的来说,Express.js 4.x 的变化不是很大,而且迁移可能是一个相对轻松的过程。但是,在您点击$ git checkout -b express4为从 3.x 到 4.x 的迁移创建一个新的 Git 分支express4之前,请仔细考虑您是否真的需要迁移!我知道许多成功的生产应用没有更新它们的主框架版本。在 Storify,我们曾经在 3.x 可用的时候运行 Express.js 2.x,也不是什么大事。作为另一个例子,从 Ruby 世界来看,我知道许多应用和开发人员仍然在使用 Ruby on Rails 2.x,尽管 Ruby on Rails 4.x 已经推出。

如果您决定使用 Express.js 4,不要仅仅依赖这个简单的概述。看看这些额外的资源,帮助您更轻松地从 Express.js 3.x 过渡到 4.x:

  • 官方迁移指南 2
  • Express.js 中的新功能 4 . x3
  • Express.js 4.x 文档 4

Express.js 4、Node.js 和 MongoDB REST API 教程 5


1

2

3

4

5

二十五、附录 C:Express.js 4 备忘单

Pro Express.js 是一本内容丰富的书,有很多例子教你关于中间件、模式和配置的知识。因此,当您开始从事自己的项目时,您会发现拥有最重要的函数和命令的快速参考—备忘单是非常有用的。我已经创建了自己的 Express.js 4 备忘单,如图图 C-1 所示,我将它作为礼物送给 Pro Express.js 的读者。你可以在https://gum.co/NQiQ/AC30238A-5C5C免费下载这份备忘单的精美 PDF 格式,正常价格是 4.99 美元。此链接仅供 Pro Express.js 的读者使用,请勿分享给任何人。

9781484200384_FigAppC-01.jpg

图 C-1 。Express.js 4 备忘单

备忘单中的信息可在本附录中找到,也可在线访问https://github.com/azat-co/cheatsheets/blob/master/express4/index.md。备忘单包含以下几个部分:

  • 装置
  • 发电机
  • 基础
  • HTTP 动词和路由
  • 请求
  • 请求标题快捷方式
  • 反应
  • 经手人签名
  • 铁笔和玉石
  • 身体
  • 静态
  • 连接中间件
  • 其他流行的中间件

在撰写本文时,备忘单适用于 Express 4.10.4。

装置

  • 本地安装最新的 Express.js】

  • 本地安装 Express.js v4.2.0,保存到package.json

    $ sudo npm install express@4.2.0 --save
    
    
  • 安装 Express.js 命令行生成器 v4.0.0

    $ sudo npm install -g express-generator@4.0.0
    
    

发电机

用法:$ express [options] [dir]

选项:

  • -h:打印使用信息
  • -V:打印快速生成器版本号
  • -e:增加 ejs 引擎支持,省略默认为 jade
  • -H:增加 hogan.js 引擎支持
  • -c <library>:添加 CSS 支持(less|stylus|compass),如果省略,默认为普通 CSS
  • -f:生成到非空目录

基础

  • 包含模块:

    var express = require('express')
    
    
  • 创建实例:

    var app = express()
    
    
  • 启动 Express.js 服务器:

    app.listen(portNumber, callback)
    
    
  • 启动 Express.js 服务器:

    http.createServer(app).listen(portNumber, callback)
    
    
  • 通过

    app.set(key, value)
    
    

    键设置属性值

  • 通过关键字

    app.get(key)
    
    

    获取属性值

HTTP 动词和路由

  • app.get(urlPattern, requestHandler[, requestHandler2, ...])
  • app.post(urlPattern, requestHandler[, requestHandler2, ...])
  • app.put(urlPattern, requestHandler[, requestHandler2, ...])
  • app.delete(urlPattern, requestHandler[, requestHandler2, ...])
  • app.all(urlPattern, requestHandler[, requestHandler2, ...])
  • app.param([name,] callback):
  • app.use([urlPattern,] requestHandler[, requestHandler2, ...])

请求

  • request.params:参数中间件
  • request.param:提取一个参数
  • request.query:提取查询字符串参数
  • request.route:返回路线字符串
  • request.cookies:cookie,需要cookie-parser
  • request.signedCookies:已签名的 cookies,需要cookie-parser
  • request.body:有效载荷,需要body-parser

请求标题快捷方式

  • request.get(headerKey):表头键值
  • request.accepts(type):检查类型是否被接受
  • request.acceptsLanguage(language):检查语言
  • request.acceptsCharset(charset):检查字符集
  • request.is(type):检查类型
  • request.ip : IP 地址
  • request.ips : IP 地址(打开信任代理)
  • request.pathURL 路径
  • request.host:没有端口号的主机
  • request.fresh:检查新鲜度
  • request.stale:检查陈旧性
  • request.xhr:对于 AJAX-y 请求为真
  • request.protocol:返回 HTTP 协议
  • request.secure:检查协议是否为https
  • request.subdomains:子域数组
  • request.originalUrl原始 URL

反应

  • response.redirect(status, url):重定向请求
  • response.send(status, data):发送响应
  • response.json(status, data):发送 JSON 并强制正确的报头
  • response.sendfile(path, options, callback):发送文件
  • response.render(templateName, locals, callback):渲染模板
  • response.locals:将数据传递给模板

经手人签名

  • function(request, response, next) {}:请求处理者签名
  • function(error, request, response, next) {}:错误处理程序签名

铁笔和玉石

app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
app.use(require('stylus').middleware(path.join(__dirname, 'public')))

身体

var bodyParser = require('body-parser')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded())

静态

app.use(express.static(path.join(__dirname, 'public')))

连接中间件

$ sudo npm install <package_name> --save

  • body-parser:请求有效载荷
  • compression : Gzip
  • connect-timeout:请求超时,单位为毫秒,默认为 5000
  • cookie-parser:饼干
  • cookie-session:通过 Cookies 存储的会话
  • csurf : CSRF
  • errorhandler:错误处理程序
  • express-session:通过内存或其他存储进行会话
  • method-override : HTTP 方法覆盖
  • morgan:服务器日志
  • response-time:响应时间中间件
  • serve-favicon:收藏夹图标
  • serve-index:提供包含目录列表的页面
  • serve-static:静态内容
  • vhost:虚拟主机

其他流行的中间件

  • cookieskeygrip:类似于cookieParser
  • raw-body:生体
  • connect-multipartyconnect-busboy:连接多方中间件,连接勤杂工中间件
  • qs:类似于query
  • stconnect-static类似于staticCache
  • express-validator:验证
  • less:少 CSS
  • passport:认证库
  • helmet:安全标题
  • connect-cors : 心形
  • connect-redis:连接中继

二十六、附录 D:ExpressWorks

ExpressWorks 是一个自动化的研讨会,它引导您构建 Express.js 服务器,处理 GET、POST 和 PUT 请求,以及提取查询字符串、有效负载和 URL 参数。ExpressWorks 为您提供了任务和提示。你写下这些任务的解决方案。然后,在您将解决方案编写为 Express.js 应用后,ExpressWorks 会验证您对问题的解决方案。

ExpressWorks 以 workshop1为原型,以@substack 3 和@maxogden 为灵感来源于 stream-adventure24 本附录包括以下高速公路简介:

  • 装置
  • 使用
  • 重复定位
  • 任务

装置

ExpressWorks v0.0.23 的建议全局安装如下:

$ npm install -g expressworks@0.0.23
$ expressworks

如果遇到错误,请尝试

$ sudo npm install -g expressworks@0.0.23

另一种方法(对于高级开发人员)是使用本地安装。为此,请在本地运行并安装以下程序:

$ mkdir node_modules
$ npm install expressworks@0.0.23
$ cd node_modules/expressworks$ node expressworks

使用

完成安装后,ExpressWorks 理解这些命令:

  • $ expressworks:显示菜单,交互选择车间。
  • 显示所有车间的换行符列表。
  • $ expressworks select NAME:选择一个车间。
  • $ expressworks current:显示当前选择的车间。
  • $ expressworks run program.js:针对所选输入运行程序。
  • 根据预期的输出来验证你的程序。

重复定位

如果要重置已完成任务列表,如图 D-1 所示,清空~/.config/expressworks/completed.json文件。

9781484200384_FigAppD-01.jpg

图 D-1 。已完成的任务

任务

本节描述了您将遇到的一些任务。

你好世界

创建一个运行在 l ocalhost:3000上的 Express.js app,输出“Hello World!”有人去 root '/home'的时候。

ExpressWorks 给你提供的process.argv[2],就是端口号。

翡翠

创建一个 Express.js 应用,其主页(/home)由 Jade 模板引擎呈现,显示当前日期(toDateString)。

古老的好形式

编写一个处理 HTML 表单输入(<form><input name="str"/></form>)并反向打印str值的路径('/form')。

静态

将静态中间件应用到没有任何路由的服务器index.html文件。通过路径的process.argv[3]值提供并使用index.html文件。但是,您可以将自己的文件用于此内容:

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="/main.css"/>
  </head>
  <body>
    <p>I am red!</p>
  </body>
</html>

时尚的 CSS

用一些 Stylus 中间件来设计你的 HTML。process.argv[3]中提供了main.styl文件的路径,或者您可以从这些文件/文件夹中创建自己的文件/文件夹:

p
color red

index.html文件如下:

<html>
  <head>
    <title>expressworks</title>
    <link rel="stylesheet" type="text/css" href="/main.css"/>
  </head>
  <body>
    <p>I am red!</p>
  </body>
</html>

帕梅!帕梅!帕梅!帕梅!帕梅!帕梅

创建一个 Express.js 服务器来处理 PUT /message/:id请求(例如,PUT /message/526aa677a8ceb64569c9d4fb)。

该请求的响应返回用日期散列的 id SHA1:

require('crypto')
  .createHash('sha1')
  .update(new Date().toDateString().toString() + id)
  .digest('hex')

查询中有什么

编写一个路由,从 GET /search URL 路由(例如?results=recent&include_tabs=true)中的查询字符串中提取数据,然后转换并以 JSON 格式输出回给用户。

加入我吧

写一个服务器,读取一个文件(文件名传入process.argv[3]),然后解析成 JSON,用res.json(object)把内容输出给用户。

摘要

ExpressWorks 是一个命令行工具,它将帮助您熟悉 Express.js 的一些基础知识。如果您喜欢这种学习方法,请在http://nodeschool.io查看免费提供给您的类似研讨会/工具。


1

2

3

4

第一部分:入门指南

第二部分:深度 API 参考

第三部分:解决常见和抽象的问题

第四部分:教程和示例

posted @ 2024-08-13 14:28  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报