Skip to content

Latest commit

 

History

History
638 lines (469 loc) · 33.2 KB

File metadata and controls

638 lines (469 loc) · 33.2 KB

十二、Express 框架

在《T4》第 10 章中,你学习了如何使用net模块创建低级 TCP 应用。然后,在第 11 章中,使用http模块抽象出 TCP 的底层细节。向更高抽象层次的转移允许我们做更多的事情,同时编写更少的代码。第 11 章还通过连接库介绍了中间件的概念。中间件促进代码重用,并使您能够以流水线的方式请求处理。然而,使用httpconnect模块创建复杂的应用仍然有点乏味。

TJ Holowaychuk 创建的 Express 框架,在httpconnect之上提供了另一个抽象层次。Express 基于 Ruby 的 Sinatra 框架,并标榜自己是“一个最小且灵活的 Node.js web 应用框架,为构建单页面、多页面和混合 web 应用提供了一组强大的功能。”Express 为许多常见的任务提供了方便的方法和语法糖,否则这些任务将是乏味的或多余的。本章详细分析了 Express 框架。而且记住,因为 Express 是建立在httpconnect之上的,所以你在第 11 章中学到的一切都是适用的。

快速路线

在看 Express 提供了什么之前,让我们先确定一下httpconnect的一些缺点。清单 12-1 包括了一个支持三个唯一的GETURL 的例子,并为其他的所有内容返回一个404。注意,每个新支持的动词/URL 组合在if语句中都需要一个额外的分支。还有相当数量的重复代码。通过更好地优化代码,可以消除一些重复,但这需要牺牲代码的可读性和一致性。

清单 12-1 。使用http模块支持多种资源

var http = require("http");

http.createServer(function(request, response) {
  if (request.url === "/" && request.method === "GET") {
    response.writeHead(200, {
      "Content-Type": "text/html"
    });
    response.end("Hello <strong>home page</strong>");
  } else if (request.url === "/foo" && request.method === "GET") {
    response.writeHead(200, {
      "Content-Type": "text/html"
    });
    response.end("Hello <strong>foo</strong>");
  } else if (request.url === "/bar" && request.method === "GET") {
    response.writeHead(200, {
      "Content-Type": "text/html"
    });
    response.end("Hello <strong>bar</strong>");
  } else {
    response.writeHead(404, {
      "Content-Type": "text/html"
    });
    response.end("404 Not Found");
  }
}).listen(8000);

HTTP 动词和 URL 的组合被称为路由,Express 拥有处理它们的高效语法。清单 12-2 显示了来自清单 12-1 的路线是如何使用 Express 的语法编写的。首先,express模块必须安装(npm install express)并导入到应用中。http模块也必须导入。在清单 12-2 的第三行,通过调用express()函数创建了一个 Express app。这个应用的行为类似于一个连接应用,并被传递给清单 12-2 最后一行的http.createServer()方法。

清单 12-2 。使用 Express 重写清单 12-1 中的服务器

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

app.get("/", function(req, res, next) {
  res.send("Hello <strong>home page</strong>");
});

app.get("/foo", function(req, res, next) {
  res.send("Hello <strong>foo</strong>");
});

app.get("/bar", function(req, res, next) {
  res.send("Hello <strong>bar</strong>");
});

http.createServer(app).listen(8000);

对应用的get()方法的三次调用用于定义路线。get()方法定义了处理GET请求的路径。Express 还为其他 HTTP 动词定义了类似的方法(put()post()delete()等等)。所有这些方法都将 URL 路径和一系列中间件作为参数。路径是表示路由响应的 URL 的字符串或正则表达式。请注意,查询字符串不被视为路径 URL 的一部分。还要注意,我们还没有定义一个404路由,因为这是当一个请求与任何已定义的路由都不匹配时 Express 的默认行为。

image 注意 Express 中间件遵循与 Connect 相同的request - response - next签名。Express 还用其他方法增加了请求和响应对象。这方面的一个例子是response.send()方法,如清单 12-2 所示,因为res.send(). send()用于将响应状态代码和/或主体发送回客户端。如果send()的第一个参数是一个数字,那么它将被视为状态代码。如果没有提供状态代码,Express 将发回一个200。响应体可以在第一个或第二个参数中指定,可以是字符串、Buffer、数组或对象。send()也设置Content-Type标题,除非你明确这样做。如果响应体是一个Buffer,那么Content-Type头也被设置为application/octet-stream。如果主体是字符串,Express 会将Content-Type头设置为text/html。如果主体是数组或对象,那么 Express 会发回 JSON。最后,如果没有提供主体,则使用状态代码的原因短语。

Route Parameters

假设您正在创建一个销售数百或数千种不同产品的电子商务网站,每种产品都有自己唯一的产品 ID。您肯定不希望手动指定数百条唯一的路线。一种方法是创建一条路线,并将产品 ID 指定为查询字符串参数。尽管这是一个非常有效的选择,但它会导致不吸引人的 URL。如果毛衣的网址看起来像/products/sweater而不是/products?productId=sweater不是更好吗?

事实证明,可以定义为正则表达式的 Express routes 非常适合支持这种场景。清单 12-3 展示了如何使用正则表达式来参数化一条路线。在本例中,产品 ID 可以是除正斜杠以外的任何字符。在路由的中间件内部,任何匹配的参数都可以通过req.params对象访问。

清单 12-3 。使用正则表达式参数化快速路径

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

app.get(/\/products\/([^\/]+)\/?$/, function(req, res, next) {
  res.send("Requested " + req.params[0]);
});

http.createServer(app).listen(8000);

为了更加方便,即使 URL 是用字符串描述的,路由也可以参数化。清单 12-4 展示了这是如何完成的。在本例中,使用冒号(:)字符创建了一个命名参数productId。在路由的中间件内部,使用req.params对象按名称访问这个参数。

清单 12-4 。带有命名参数的路线

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

app.get("/products/:productId", function(req, res, next) {
  res.send("Requested " + req.params.productId);
});

http.createServer(app).listen(8000);

您甚至可以从字符串中为参数定义一个正则表达式。假设productId参数现在只能由数字组成,清单 12-5 展示了正则表达式是如何定义的。请注意\d字符类上的附加反斜杠。因为正则表达式是在字符串常量中定义的,所以需要一个额外的反斜杠作为转义字符。

清单 12-5 。在路由字符串中定义正则表达式

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

app.get("/products/:productId(\\d+)", function(req, res, next) {
  res.send("Requested " + req.params.productId);
});

http.createServer(app).listen(8000);

image 注意可选的命名参数后面都是问号。例如,在前面的例子中,如果productId是可选的,它将被写成:productId?

创建快速应用

Express 包含一个名为express(1)的可执行脚本,用于生成 skeleton Express 应用。运行express(1)的首选方式是使用清单 12-6 中的命令全局安装express模块。要复习全局安装模块的含义,请参见第 2 章

清单 12-6 。全局安装模块express

npm install -g express

在全局安装 Express 之后,你可以通过发出清单 12-7 所示的命令在你机器的任何地方创建一个框架应用。这个清单还包括命令的输出,其中详细列出了创建的文件以及配置和运行应用的指令。注意,在这个例子中你实际输入的唯一东西是express testapp

清单 12-7 。使用express(1)创建应用框架

$ express testapp

   create : testapp
   create : testapp/package.json
   create : testapp/app.js
   create : testapp/public
   create : testapp/public/stylesheets
   create : testapp/public/stylesheets/style.css
   create : testapp/routes
   create : testapp/routes/index.js
   create : testapp/routes/user.js
   create : testapp/public/javascripts
   create : testapp/views
   create : testapp/views/layout.jade
   create : testapp/views/index.jade
   create : testapp/public/images

   install dependencies:
     $ cd testapp && npm install

   run the app:
     $ node app

将在新文件夹中创建 skeleton Express 应用。在这种情况下,文件夹将被命名为testapp。接下来,使用清单 12-8 中的命令安装应用的依赖项。

清单 12-8 。安装框架应用的依赖项

$ cd testapp && npm install

npm安装完依赖项之后,我们就可以运行框架程序了。快速应用的入口点位于文件app.js中。因此,要运行testapp,从项目的根目录发出命令node app。你可以通过连接到localhost的 3000 端口进入测试程序。框架应用定义了两条路线——//users——它们都响应GET请求。图 12-1 显示了使用 Chrome 连接到/路线的结果。

9781430258605_Fig12-01.jpg

图 12-1 。骷髅 app 返回的索引页

检查骨架应用

app.js是快递 app 的心脏。在清单 12-7 中生成的app.js文件的内容如清单 12-9 所示。该文件首先导入expresshttppath模块,以及两个项目文件/routes/index.js/routes/user.js。从routes目录导入的两个文件包含框架应用的路由所使用的中间件。在require()语句之后,使用express()函数创建一个快速应用。

清单 12-9app.js的生成内容

/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

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

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

image 注意如果传递给require()的模块路径解析为一个目录,Node 将在该目录中寻找一个index文件。这就是为什么表达require("./routes")解析为/routes/index.js

接下来,您将看到对应用的set()方法的三次调用,该方法用于定义应用设置。第一个调用定义了一个名为port的设置,它定义了服务器将绑定到的端口号。端口号默认为 3000,但是这个值可以通过定义一个名为PORT的环境变量来覆盖。接下来的两个设置,viewsview engine,由快速模板系统使用。模板系统将在本章后面被重新讨论。现在,只需要知道这些设置使用 Jade 模板语言来呈现存储在views目录中的视图。

在设置定义之后是对use()的几个调用,这些调用定义了用于处理所有请求的中间件。表 12-1 包含了对 skeleton 应用中包含的各种中间件的简短描述。这些功能中有许多只是使用了同名的 Connect 中间件。

表 12-1 。app.js 中使用的中间件 T3

|

中间件

|

描述

| | --- | --- | | favicon | 如果您一直在使用浏览器测试您的 web 服务器,那么您可能已经注意到了对文件favicon.ico的请求。这个中间件通过为您的favicon.ico文件提供服务来处理这样的请求,或者如果您没有提供文件,则使用连接默认值。 | | logger | 这个中间件记录它收到的每个请求的信息。在框架应用中使用的dev模式中,logger显示请求动词和 URL,以及响应代码、处理请求所用的时间和返回数据的大小。 | | bodyParser | 这个中间件在第 11 章中有解释。它将请求体字符串解析成一个对象,并将其作为request.body附加到请求对象上。 | | methodOverride | 有些浏览器只允许 HTML 表单发出GETPOST请求。要发出其他类型的请求(PUTDELETE等等),表单可以包含一个名为X-HTTP-Method-Override的输入,其值是所需的请求类型。这个中间件检测到这种情况,并相应地设置request.method属性。 | | app.router | 这是用于将传入请求映射到定义的路由的快速路由。如果没有明确使用它,Express 将在第一次遇到路由时装载它。然而,手动安装路由将确保它在中间件序列中的位置。 | | static | 这个中间件接受一个目录路径作为输入。该目录被视为静态文件服务器的根目录。这对于提供图像、样式表和其他静态资源等内容非常有用。在骷髅 app 中,静态目录是public。 | | errorHandler | 顾名思义,errorHandler是处理错误的中间件。与其他中间件不同,errorHandler接受四个参数— errorrequestresponsenext。在 skeleton app 中,这个中间件只在开发模式下使用(见development only注释)。 |

set()use()呼叫之后,使用get()方法定义两条GET路线。如前所述,这些路线的网址是//users/users路由使用存储在user.list变量中的一个中间件。回头看看require()语句,user变量来自文件/routes/user,其内容如清单 12-10 所示。正如您所看到的,这个路由只是返回字符串"respond with a resource"

清单 12-10/routes/user.js生成的内容

/*
 * GET users listing.
 */

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

/路线比较有意思。在/routes/index.js 中定义,如清单 12-11 所示。这里显示的代码看起来不像能创建如图 12-1 所示的页面。关键是render()方法,它与 Express 模板系统联系在一起。这可能是一个探索模板化,以及如何在 Express 中处理它的好时机。

清单 12-11/routes/index.js生成的内容

/*
 * GET home page.
 */

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

模板

创建动态 web 内容通常涉及构建长的 HTML 字符串。手动完成这项工作既繁琐又容易出错。例如,很容易忘记对长字符串中的字符进行适当的转义。模板引擎是一种替代方法,它通过提供一个框架文档(模板)来大大简化这个过程,您可以在这个框架文档中嵌入动态数据。现在有许多兼容 JavaScript 的模板引擎,其中一些比较流行的选项是 Mustache、Handlebars、嵌入式 JavaScript (EJS)和 Jade。Express 支持所有这些模板引擎,但是默认情况下,Jade 是和 Express 一起打包的。这一节解释了如何使用玉。其他模板引擎可以很容易地安装和配置,以便与 Express 一起工作,但这里不讨论。

配置 Jade 就像在app.js文件中定义两个设置一样简单。这些设置是viewsview engineviews设置指定了 Express 可以定位模板的目录,也称为视图。如果没有提供,那么view engine指定要使用的视图文件扩展名。清单 12-12 显示了如何应用这些设置。在本例中,模板位于名为views的子目录中。这个目录应该包括一些 Jade 模板文件,文件扩展名是.jade

清单 12-12 。用于在 Express 中配置 Jade 的设置

app.set("views", __dirname + "/views");
app.set("view engine", "jade");

一旦 Express 被配置为使用您最喜欢的模板引擎,您就可以开始呈现视图了。这是通过response对象的render()方法完成的。render()的第一个参数是您的views目录中视图的名称。如果您的views目录包含子目录,该名称可以包含正斜杠。render()的下一个参数是传递数据的可选参数。这用于在静态模板中嵌入动态数据。render()的最后一个参数是一个可选的回调函数,一旦模板完成渲染,这个函数就会被调用。如果省略回调,Express 将自动用呈现的页面响应客户端。如果包含回调,Express 将不会自动响应,调用函数时会出现一个可能的错误,并以呈现的字符串作为参数。

假设您正在为一个用户的帐户页面创建一个视图。用户登录后,您需要称呼他们的名字。清单 12-13 显示了一个使用render()处理这种情况的例子。这个例子假设模板文件被命名为home.jade,并且位于views文件夹中的一个名为account的目录中。假设用户的名字是 Bob。在实际的应用中,这些信息可能来自某种类型的数据存储。这里还包含了可选的回调函数。在回调中,我们检查错误。如果出现错误,将返回一个500内部服务器错误。否则,返回呈现的 HTML。

清单 12-13render()的使用示例

res.render("account/home", {
  name: "Bob"
}, function(error, html) {
  if (error) {
    return res.send(500);
  }

  res.send(200, html);
});

当然,为了呈现视图,我们需要实际创建视图。因此,在您的views目录中,创建一个名为account/home.jade 的文件,包含如清单 12-14 所示的代码。这是一个 Jade 模板,虽然对 Jade 语法的解释超出了本书的范围,但我们将介绍绝对的基础知识。第一行用于指定 HTML5 文档类型。第二行创建了开始的<html>标记。请注意,Jade 不包含任何尖括号或结束标记。相反,Jade 根据代码缩进来推断这些事情。

清单 12-14 。一个翡翠模板的例子

doctype 5
html
  head
    title Account Home
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    h1 Welcome back #{name}

接下来是文档的<head>标签。标题包括页面标题和一个样式表链接。link旁边的括号用于指定标签属性。样式表链接到一个静态文件,Express 可以使用static中间件找到该文件。

清单 12-14 的最后两行定义了文档的<body>。在这种情况下,主体由欢迎用户的单个<h1>标记组成。#{name}的值取自传递给render()的 JSON 对象。在花括号内,可以使用 JavaScript 的标准点和下标符号访问嵌套的对象和数组。

产生的 HTML 字符串显示在清单 12-15 中。请注意,为了可读性,该字符串已被格式化。实际上,Express 呈现的模板没有额外的缩进和换行符。有关 Jade 语法的更多信息,请参见 Jade 主页上的http``:``//``www``.``jade``-``lang``.``com

清单 12-15 。从清单 12-14 中的模板呈现的 HTML 示例

<!DOCTYPE html>
<html>
  <head>
    <title>Account Home</title>
    <link rel="stylesheet" href="/stylesheets/style.css">
  </head>
  <body>
    <h1>Welcome back Bob</h1>
  </body>
</html>

express-validator

express-validator是一个有用的第三方模块,用于确保用户输入以预期的格式提供。express-validator创建中间件,将数据检查方法附加到request对象上。清单 12-16 中的显示了一个使用express-validator验证产品 ID 的例子。在示例的第二行导入了express-validator模块,然后用use()将其添加为中间件。中间件将assert()validationErrors()方法附加到req上,在路由中使用。

assert()方法将参数名和错误消息作为参数。该参数可以是命名的 URL 参数、查询字符串参数或请求正文参数。由assert()返回的对象用于验证参数的数据类型和/或值。清单 12-16 展示了三种验证方法,notEmpty()isAlpha()len()。这些方法验证了productId参数存在,并且长度在 2 到 10 个字母之间。为了方便起见,这些方法可以链接在一起,如第二个assert()所示。当然,如果您完全省略了productId参数,路由将不会被匹配,验证器将永远不会运行。notEmpty()在验证查询字符串参数和表单体数据时更有用。

清单 12-16express-validator的一个例子

var express = require("express");
var validator = require("express-validator");
var http = require("http");
var app = express();

app.use(express.bodyParser());
app.use(validator());

app.get("/products/:productId", function(req, res, next) {
  var errors;

  req.assert("productId", "Missing product ID").notEmpty();
  req.assert("productId", "Invalid product ID").isAlpha().len(2, 10);
  errors = req.validationErrors();

  if (errors) {
    return res.send(errors);
  }

  res.send("Requested " + req.params.productId);
});

http.createServer(app).listen(8000);

在做出所有断言后,使用validationErrors()方法来检索任何错误。如果没有错误,将返回null。但是,如果检测到错误,将返回一组验证错误。在这个例子中,错误数组只是作为响应被发送回来。

还有许多其他有用的验证方法没有在清单 12-16 中显示。其中一些是isInt()isEmail()isNull()is()contains()。前三种方法验证输入是整数、电子邮件地址还是nullis()方法接受一个正则表达式参数,并验证该参数是否与之匹配。contains()也接受一个参数,并检查参数是否包含它。

express-validator还为req附加了一个sanitize()方法,用于清理输入。清单 12-17 显示了sanitize()的几个例子。前两个示例分别将参数值转换为布尔值和整数。第三个示例删除了参数开头和结尾多余的空白。最后一个例子用相应的字符(<>)替换字符实体(比如&lt;&gt;)。

清单 12-17express-validator sanitize()方法的例子

req.sanitize("parameter").toBoolean()
req.sanitize("parameter").toInt()
req.sanitize("parameter").trim()
req.sanitize("parameter").entityDecode()

REST

代表性状态转移 或 REST,是一种越来越常见的创建 API 的软件架构。由 Roy Fielding 在 2000 年提出的 REST 本身并不是一项技术,而是一套用于创建服务的原则。RESTful APIs 几乎总是使用 HTTP 实现,但这不是严格的要求。下面的列表列举了 RESTful 设计背后的一些原则。

  • RESTful 设计应该有一个单一的基本 URL,和一个类似目录的 URL 结构。例如,一个博客 API 可以有一个基本 URL/blog。某一天的个人博客条目可以使用类似于/blog/posts/2013/03/17/的 URL 结构进行访问。
  • 作为应用状态引擎的超媒体(HATEOAS) 。客户端应该能够只使用服务器提供的超链接来导航整个 API 。例如,在访问一个 API 的入口点之后,服务器应该提供链接,客户端可以使用这些链接来导航 API。
  • 服务器不应该维护任何客户端状态,例如会话。相反,每个客户端请求都应该包含定义状态所需的所有信息。这一原则通过简化服务器来提高可伸缩性。
  • 服务器响应应该声明它们是否可以被缓存。这种声明可以是显式的,也可以是隐式的。如果可能,响应应该是可缓存的,因为它可以提高性能和可伸缩性。
  • RESTful 设计应该尽可能地利用底层协议的词汇。例如,CRUD(创建、读取、更新和删除)操作分别使用 HTTP 的POSTGETPUTDELETE动词来实现。此外,服务器应该尽可能使用适当的状态代码进行响应。

RESTful API 示例

Express 使得 RESTful 应用的实现变得非常简单。在接下来的几个例子中,我们将创建一个 RESTful API 来操作服务器上的文件。API 更常用于操作数据库条目,但是我们还没有涉及数据库。我们的示例应用也被分成许多文件。这使得示例更具可读性,同时也使得应用更加模块化。

首先,我们从app.js 开始,如清单 12-18 所示。这其中的大部分应该看起来很熟悉。然而,增加了一个额外的中间件来定义req.store。这是包含应用将使用的文件的目录。路线声明也被删除了,取而代之的是对文件routes.js中定义的自定义函数routes.mount(). mount()的调用,该函数将 Express app 作为其唯一的参数。

清单 12-18app.js的内容

var express = require("express");
var routes = require("./routes");
var http = require("http");
var path = require("path");
var app = express();
var port = process.env.PORT || 8000;

app.use(express.favicon());
app.use(express.logger("dev"));
app.use(express.bodyParser());
app.use(express.methodOverride());

// define the storage area
app.use(function(req, res, next) {
  req.store = __dirname + "/store";
  next();
});

app.use(app.router);

// development only
if ("development" === app.get("env")) {
  app.use(express.errorHandler());
}

routes.mount(app);

http.createServer(app).listen(port, function() {
  console.log("Express server listening on port " + port);
});

routes.js 的内容如清单 12-19 所示。测试应用接受四个路径,每个 CRUD 操作一个路径。每个路由的中间件都在自己的文件中定义(create.jsread.jsupdate.jsdelete.js)。需要指出的一点是,delete既是 HTTP 动词又是 JavaScript 保留字,所以在某些地方将delete操作简称为del

清单 12-19routes.js的内容

var create = require("./create");
var read = require("./read");
var update = require("./update");
var del = require("./delete");

module.exports.mount = function(app) {
  app.post("/:fileName", create);
  app.get("/:fileName", read);
  app.put("/:fileName", update);
  app.delete("/:fileName", del);
};

POST进路处理的create操作在create.js中找到,如清单 12-20 所示。因为我们正在执行文件系统操作,所以我们从导入fs模块开始。在路由中间件内部,计算文件路径及其内容。该路径由req.store值和fileName参数组成。要写入文件的数据来自名为dataPOST主体参数。然后使用fs.writeFile()方法创建新文件。文件是使用wx标志创建的,如果文件已经存在,这会导致操作失败。在writeFile()回调中,我们返回一个400状态码来表明请求不能被满足,或者返回一个201来表明一个新文件被创建。

清单 12-20create.js的内容

var fs = require("fs");

module.exports = function(req, res, next) {
  var path = req.store + "/" + req.params.fileName;
  var data = req.body.data || "";

  fs.writeFile(path, data, {
    flag: "wx"
  }, function(error) {
    if (error) {
      return res.send(400);
    }

    res.send(201);
  });
};

下一个 CRUD 操作是读取,由GET路径处理。read.js的内容如清单 12-21 所示。这一次,fs.readFile()方法用于检索在fileName参数中指定的文件内容。如果读取因任何原因失败,将返回一个404状态代码。否则,将返回一个200状态代码,以及包含文件数据的 JSON 主体。值得指出的是,在设置响应代码时,可以更彻底地检查error参数。例如,如果error.code等于"ENOENT",那么文件确实不存在,状态代码应该是404。所有其他错误都可以简单地返回一个400

清单 12-21read .js的内容

var fs = require("fs");

module.exports = function(req, res, next) {
  var path = req.store + "/" + req.params.fileName;

  fs.readFile(path, {
    encoding: "utf8"
  }, function(error, data) {
    if (error) {
      return res.send(404);
    }

    res.send(200, {
      data: data
    });
  });
};

接下来是PUT路线,它实现了update操作,如清单 12-22 所示。这非常类似于create操作,有两个小的不同。首先,在成功更新时返回一个200状态代码,而不是一个201。第二,用r+标志而不是wx打开文件。如果文件不存在,这会导致update操作失败。

清单 12-22update.js的内容

var fs = require("fs");

module.exports = function(req, res, next) {
  var path = req.store + "/" + req.params.fileName;
  var data = req.body.data || "";

  fs.writeFile(path, data, {
    flag: "r+"
  }, function(error) {
    if (error) {
      return res.send(400);
    }

    res.send(200);
  });
};

最终的 CRUD 操作是delete ,如清单 12-23 中的所示。方法删除由参数fileName指定的文件。这条路由失败时返回一个400,成功时返回一个200

清单 12-23delete.js的内容

var fs = require("fs");

module.exports = function(req, res, next) {
  var path = req.store + "/" + req.params.fileName;

  fs.unlink(path, function(error) {
    if (error) {
      return res.send(400);
    }

    res.send(200);
  });
};

测试 API

我们可以创建一个简单的测试脚本,如清单 12-24 所示,用于测试 API。该脚本使用request模块至少访问一次所有的 API 路径。async模块也用于避免回调地狱。通过查看对async.waterfall()的调用,您可以看到脚本是从创建一个文件并读回内容开始的。然后,文件被更新并再次被读取。最后,我们删除文件并尝试再次读取它。所有的请求都处理同一个文件,foo。每个请求完成后,将显示操作名称和响应代码。对于成功的GET请求,也会显示文件内容。

清单 12-24 。RESTful API 的测试脚本

var async = require("async");
var request = require("request");
var base = "http://localhost:8000";
var file = "foo";

function create(callback) {
  request({
    uri: base + "/" + file,
    method: "POST",
    form: {
      data: "This is a test file!"
    }
  }, function(error, response, body) {
    console.log("create:  " + response.statusCode);
    callback(error);
  });
}

function read(callback) {
  request({
    uri: base + "/" + file,
    json: true  // get the response as a JSON object
  }, function(error, response, body) {
    console.log("read:  " + response.statusCode);

    if (response.statusCode === 200) {
      console.log(response.body.data);
    }

    callback(error);
  });
}

function update(callback) {
  request({
    uri: base + "/" + file,
    method: "PUT",
    form: {
      data: "This file has been updated!"
    }
  }, function(error, response, body) {
    console.log("update:  " + response.statusCode);
    callback(error);
  });
}

function del(callback) {
  request({
    uri: base + "/" + file,
    method: "DELETE"
  }, function(error, response, body) {
    console.log("delete:  " + response.statusCode);
    callback(error);
  });
}

async.waterfall([
  create,
  read,
  update,
  read,
  del,
  read
]);

测试脚本的输出显示在清单 12-25 中。在运行脚本之前,请确保创建了store目录。创建操作返回一个201,表示在服务器上成功创建了foo。当文件被读取时,返回一个200,并显示文件的正确内容。接下来,文件被成功更新并再次读取。然后,文件被成功删除。随后的read操作返回一个404,因为文件不再存在。

清单 12-25 。清单 12-24 中测试脚本的输出

$ node rest-test.js
create:  201
read:  200
This is a test file!
update:  200
read:  200
This file has been updated!
delete:  200
read:  404

摘要

本章介绍了 Express 框架的基础知识。Express 在 Connect 和 HTTP 之上提供了一个层,这大大简化了 web 应用的设计。在撰写本文时,Express 是npm注册表中第五大依赖模块,已经被用于构建超过 26,000 个 web 应用。这使得 Express 对于全面发展的 Node 开发人员来说极其重要。尽管 Express 可能是一整本书的主题,但本章已经触及了框架和相关技术的最重要的方面。为了更好地理解这个框架,我们鼓励你浏览位于http://www.expressjs.com的 Express 文档,以及位于https://github.com/visionmedia/express的源代码。