ExpressJS-高级教程-四-

168 阅读25分钟

ExpressJS 高级教程(四)

原文:Pro Express.js

协议:CC BY-NC-SA 4.0

十七、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