Skip to content

Latest commit

 

History

History
1186 lines (861 loc) · 32.2 KB

File metadata and controls

1186 lines (861 loc) · 32.2 KB

二十、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 选项包含在第 3 章中,但是,如前面的代码所示,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 
      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 
        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 
      a(href="http://proexpressjs.com") Pro Express.js
      |.
    p The full source code is available at 
      a(href='http://github.com/azat-co/todo-express') github.com/azat-co/todo-express
      |.
    p For Express 3.x go to 
      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
    |. 
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
            |. 
        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
            |. 
        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 应用被认为是传统的 T2,因为它不依赖任何前端框架,并且在服务器上呈现 HTML。这样做是为了展示使用 Express.js 完成这样的任务是多么容易。在当今的开发中,人们经常利用某种 REST API 服务器架构,用 Backbone.js、AngularJS、Ember.js 或类似的东西构建前端客户端(见http://todomvc.com)。

在第 22 章的例子中,我们深入探讨了如何编写这样的服务器的细节。第 22 章应用 HackHall 使用 MEBN (MongoDB、Express.js、Backbone.js 和 Node.js)栈。但是,在我们讨论 HackHall 之前,我们将在第 21 章的中花更多时间讨论 REST API 和测试,其中有 REST API 的例子。


1T0】

2T0】