Skip to content

Latest commit

 

History

History
1184 lines (880 loc) · 34.5 KB

File metadata and controls

1184 lines (880 loc) · 34.5 KB

二、创建简单的网络应用

到本章结束时,您应该能够使用 Node.js、CouchDB 和 Flatiron 创建一个简单的 web 应用。

本章涵盖的主题有:

  • 设置节点和熨斗
  • 创建和处理用户表单

定义我们的网络应用的需求

在我们深入 Zombie.js 世界之前,我们需要为我们的测试创建一个目标,也就是一个提供待办事项列表的 web 应用。这是此类应用的一组顶级要求:

  • 用户可以注册服务,他应该提供一个电子邮件地址作为用户名和密码。通过提供用户名和密码,用户可以创建一个经过身份验证的会话,该会话将在以后的交互中识别他。
  • 用户可以创建待办事项。
  • 用户可以查看待办事项列表。
  • 用户可以删除待办事项。

为了实现这个应用,我们将使用 Node.js,这是一个用 JavaScript 构建网络应用的平台,Zombie.js 也使用这个平台。我们还将使用 Flatiron,这是一组组件,将帮助您在 Node.js 之上构建一个网络应用

为了保持简单,我们在 Node.js 中构建我们的应用。然而,Zombie.js 适用于测试使用动态 HTTP 服务器的任何框架构建的应用。

此外,请记住,构建这个 web 应用的目标不是向您展示如何构建 web 应用,而是在一个已知且简单的领域上提供一个工作应用,我们可以将其用作测试的主题。

在接下来的部分中,您将学习如何安装 Node.js 和 Flatiron,以及如何创建您的待办事项应用服务器。

设置 Node.js 和平铁

如果您没有安装最新版本的 Node.js,您将需要安装它。出于几个原因,您将需要 Node.js。我们的 web 应用将使用 Flatiron,它运行在 Node.js 之上。您还需要使用与 Node 捆绑在一起的节点包管理器(【NPM】)。最后,您将需要 Node.js 来安装和运行您的 Zombie.js 测试。

安装 Node.js

  1. To install Node.js head out to the nodejs.org website.

    Installing Node.js

  2. Then click on the Download button, which should open the following page:

    Installing Node.js

  3. 如果你运行的是视窗或麦金塔系统,点击相应的安装图标。应该下载并启动图形安装程序。

从源代码安装节点

如果您没有运行其中一个系统,并且您在一个类似 Unix 的系统上,您可以通过以下步骤从源代码安装 Node.js:

  1. Click on the source code icon, which will start downloading the source code tarball. Once downloaded, expand it using the terminal:

    $ tar xvfz node-v0.8.7.tar.gz

    导航到创建的目录:

    $ cd node-v0.8.7
  2. 配置:

    $ ./configure
  3. 建造它:

    $ make
  4. And finally install it:

    $ make install

    如果您没有足够的权限将节点二进制文件复制到最终目的地,您需要在您的命令前面加上sudo:

    $ sudo make install
  5. 现在你的系统上应该安装了 Node.js。试运行:

    $ node -v
    v0.8.7
  6. 现在,让我们尝试打开 Node 命令行并键入一些内容:

    $ node
    > console.log('Hello World!');
  7. 如果现在按进入,应该会得到如下输出:

    ...
    > Hello World!
  8. 通过安装 Node.js,您还安装了它忠实的伙伴 NPM,节点包管理器。可以尝试从终端调用:

    $ npm -v
    1.1.48

安装熨斗并启动应用

现在你需要安装 Flatiron 框架,这样你就可以开始构建你的应用了。

  1. Use NPM to download and install Flatiron as follows:

    $ npm install -g flatiron

    同样,如果您没有足够的权限安装 Flatiron,请运行最后一个以sudo为前缀的命令。

    这将在全球范围内安装 Flatiron,使flatiron命令行实用程序可用。

  2. 现在您应该进入一个包含应用代码的目录。然后,您可以通过执行以下命令为您的 web 应用创建基本支架:

    $ flatiron create todo
  3. After prompting you for the name of the author, the app description, and the homepage (which is optional), it will create a directory named todo containing the base for your application code. Step into that directory using the following command:

    $ cd todo

    在那里你会发现两个文件和三个文件夹:

    $ tree
    .
    ├── app.js
    ├── config
       └── config.json
    ├── lib
    ├── package.json
    └── test

    其中一个文件package.json包含应用清单,其中包含应用依赖的包。现在,您将从该文件中删除devDependencies字段。

    您还需要为名为plates的包添加一个依赖项,该依赖项将用于动态更改 HTML 模板。

    此外,您将提供一些不需要任何修改的静态文件。为此,您将使用名为node-static的包,您还需要将其添加到应用清单的依赖项列表中。

    现在你的package.json应该是这样的:

    {
      "description": "To-do App",
      "version": "0.0.0",
      "private": true,
      "dependencies": {
        "union": "0.3.0",
        "flatiron": "0.2.8",
        "plates": "0.4.x",
        "node-static": "0.6.0"
      },
      "scripts": {
        "test": "vows --spec",
        "start": "node app.js"
      },
      "name": "todo",
      "author": "Pedro",
      "homepage": ""
    }
  4. Next, install those dependencies by using the following:

    $ npm install

    这将在本地node_modules目录中安装所有依赖项,并应该输出如下内容:

    union@0.3.0 node_modules/union
    ├── qs@0.4.2
    └── pkginfo@0.2.3
    
    flatiron@0.2.8 node_modules/flatiron
    ├── pkginfo@0.2.3
    ├── director@1.1.0
    ├── optimist@0.3.4 (wordwrap@0.0.2)
    ├── broadway@0.2.5 (eventemitter2@0.4.9, cliff@0.1.8, utile@0.1.2, nconf@0.6.4, winston@0.6.2)
    └── prompt@0.2.6 (revalidator@0.1.2, read@1.0.4, utile@0.1.3, winston@0.6.2)
    
    plates@0.4.6 node_modules/plates
    
    node-static@0.6.0 node_modules/node-static

    您不必担心这一点,因为节点将能够自动获取这些依赖关系。

  5. Now you can try to start up your app:

    $ node app.js

    如果打开一个浏览器,指向http://localhost:3000,会得到如下回应:

    {"hello":"world"}

创建您的待办事项应用

现在您已经运行了一个 flat iron“hello world”示例,您需要扩展它,以便我们的待办事项应用成型。为此,您需要创建和更改一些文件。如果你迷路了,你可以随时参考章节的源代码。此外,作为参考,本章末尾有一个完整的项目文件列表。

建立数据库

如同在任何真实应用中一样,您将需要一种可靠的方法来持久化数据。在这里,我们将使用开源和面向文档的数据库 CouchDB。您可以选择在本地安装 CouchDB,也可以使用互联网上的服务,如 Iris Couch。

如果选择在本地开发机上安装 CouchDB ,可以前往访问http://couchdb.apache.org/,点击下载并按照说明进行操作。

如果您喜欢简单地通过互联网使用 CouchDB,您可以前往http://www.iriscouch.com/,点击立即注册按钮并填写登记表。几秒钟之内就应该有一个正在运行的 CouchDB 实例。

Setting up the database

截至本文撰写之时,Iris Couch 是一项面向低流量小型数据库的免费服务,这使得它非常适合像这样的应用原型。

从节点访问 CouchDB

要从节点访问库数据库,我们将使用一个名为nano的库,您可以将其添加到package.json文件的依赖项部分:

{
  "description": "To-do App",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "union": "0.3.0",
    "flatiron": "0.2.8",
    "plates": "0.4.6",
    "node-static": "0.6.0",
 "nano": "3.3.0"
  },
  "scripts": {
    "test": "vows --spec",
    "start": "node app.js"
  },
  "name": "todo",
  "author": "Pedro",
  "homepage": ""
}

现在,您可以通过在应用的根目录下运行以下命令来安装这个缺失的依赖项:

$ npm install
nano@3.3.0 node_modules/nano
├── errs@0.2.3
├── request@2.9.203.8.0 (request@2.2.9request@2.2.9)

这将在node_modules文件夹中安装nano,使其在构建该应用时可以获得帮助。

要真正连接到数据库,您需要定义 CouchDB 服务器 URL。如果在本地运行 CouchDB,URL 应该类似于ht tp://127.0.0.1:5984。如果您在 Iris Couch 或类似服务中运行 CouchDB,您的网址将类似于https://mytodoappcouchdb.iriscouch.com

在任何一种情况下,如果您需要使用用户名和密码进行访问,您应该将它们编码在 URL 中,http://username:password@mytodoappco uchdb.iriscouch.com

这个网址现在应该被输入到config/config.json下的配置文件中,在couchdb键下:

{
  "couchdb": "http://localhost:5984"
}

接下来,通过在lib/couchdb.js下提供一个简单的模块来封装对数据库的访问:

var nano = require('nano'),
    config = require('../config/config.json');

module.exports = nano(config.couchdb);

该模块将用于获取一个 CouchDB 服务器对象,而不是在整个代码中重复多次confignano舞蹈。

应用布局

像现在许多网站一样,我们将使用推特引导框架来帮助我们让网站看起来和感觉起来最少,但又很像样。为此,您将前往引导网站http://twitter.github.com/bootstrap/并点击下载引导按钮:

Application layout

您将得到一个 zip 文件,您应该将它展开到本地public文件夹中,以这些文件结束:

$ tree public/
public/
├── css
   ├── bootstrap-responsive.css
   ├── bootstrap-responsive.min.css
   ├── bootstrap.css
   └── bootstrap.min.css
├── img
   ├── glyphicons-halflings-white.png
   └── glyphicons-halflings.png
└── js
    ├── bootstrap.js
    └── bootstrap.min.js

您还需要将 jQuery 添加到组合中,因为引导依赖于它。从http://jquery.com下载 jQuery 并命名为public/js/jquery.min.js

开发前端

现在我们已经安装了 Bootstrap 和 jQuery,是时候创建我们应用的前端了。

首先我们将设置布局 HTML 模板,它定义了所有页面的外部结构。为了托管所有模板,我们将有一个名为templates的目录,在templates/layout.html下包含以下内容:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title id="title"></title>
    <link href="/css/bootstrap.min.css" rel="stylesheet" />
  </head>
  <body>

    <section role="main" class="container">

      <div id="messages"></div>

      <div id="main-body"></div>

    </section>

    <script src="/js/jquery.min.js"></script> 
    <script src="/js/bootstrap.min.js"></script>

  </body>
</html>

该模板加载 CSS 和脚本,并包含消息和主要部分的占位符。

我们还需要一个“T2”小模块来获取主要内容和一些其他选项,并将它们应用到这个模板中。我们将把它放在templates/layout.js里面:

var Plates = require('plates'),
    fs     = require('fs');

var templates = {
  layout : fs.readFileSync(__dirname + '/layout.html', 'utf8'),
  alert  : fs.readFileSync(__dirname + '/alert.html', 'utf8')
};

module.exports = function(main, title, options) {

  if (! options) {
    options = {};
  }

  var data = {
    "main-body": main,
    "title": title,
    'messages': ''
  };

  ['error', 'info'].forEach(function(messageType) {
    if (options[messageType]) {
      data.messages += Plates.bind(templates.alert,
        {message: options[messageType]});
    }
  });

  return Plates.bind(templates.layout, data);
};

在 Node.js 中,一个模块只是一个 JavaScript 文件,旨在供其他模块使用。模块内部的所有变量都是私有的;如果模块作者希望向外界公开一个值或函数,则在module.exports中修改或设置特殊变量。

在我们的例子中,这个模块导出一个函数,该函数获取主页面内容、页面标题和一些选项(如信息或错误消息)的标记,并将其应用于布局模板。

我们还需要在templates/alert.html下放置以下标记文件:

<div class="alert">
  <a class="close" data-dismiss="alert">×</a>
  <p class="message"></p>
</div>

现在我们准备开始实现一些需求。

用户注册

这款应用将为用户提供一份个人待办事项清单。在他们能够访问它之前,他们需要在系统中注册。为此,你需要定义一些网址,用户将用来获取我们的用户注册表单并提交它。

现在您将更改app.js文件。此文件包含一组初始化过程,包括此块:

app.router.get('/', function () {
  this.res.json({ 'hello': 'world' })
});

该块将所有具有/网址的 HTTP 请求路由到给定的函数,其中 HTTP 方法是GET。然后,这个函数将为具有这两个特征的每个请求调用,在这种情况下,您正在回复的是{"hello":"world"},用户将在浏览器上看到打印的内容。

现在我们需要删除这个路由,添加一些允许用户自己注册的路由。

为此,创建一个名为routes的文件夹,您将在其中放置所有路由模块。第一个是routes/users.js ,将包含以下代码:

var fs      = require('fs'),
    couchdb = require('../lib/couchdb'),
    dbName  = 'users',
    db      = couchdb.use(dbName),
    Plates  = require('plates'),
    layout  = require('../templates/layout');

var templates = {
  'new' : fs.readFileSync(__dirname +
    '/../templates/users/new.html', 'utf8'),
  'show': fs.readFileSync(__dirname +
    '/../templates/users/show.html', 'utf8')
};

function insert(doc, key, callback) {
  var tried = 0, lastError;

  (function doInsert() {
    tried ++;
    if (tried >= 2) {
      return callback(lastError);
    }

    db.insert(doc, key, function(err) {
      if (err) {
        lastError = err;
        if (err.status_code === 404) {
          couchdb.db.create(dbName, function(err) {
            if (err) {
              return callback(err);
            }
            doInsert();
          });
        } else {
          return callback(err);
        }
      }
      callback.apply({}, arguments);
    });
  }());
}

function render(user) {
  var map = Plates.Map();
  map.where('id').is('email').use('email').as('value');
  map.where('id').is('password').use('password').as('value');
  return Plates.bind(templates['new'], user || {}, map);
}

module.exports = function() {
  this.get('/new', function() {
    this.res.writeHead(200, {'Content-Type': 'text/html'});
    this.res.end(layout(render(), 'New User'));
  });

  this.post('/', function() {

    var res = this.res,
        user = this.req.body;

    if (! user.email || ! user.password) {
      return this.res.end(layout(templates['new'],
        'New User', {error: 'Incomplete User Data'}));
    }

    insert(user, this.req.body.email, function(err) {
      if (err) {
        if (err.status_code === 409) {
          return res.end(layout(render(user), 'New User', {
            error: 'We already have a user with that email address.'}));
        }
        console.error(err.trace);
        res.writeHead(500, {'Content-Type': 'text/html'});
        return res.end(err.message);
      }
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(layout(templates['show'], 'Registration Complete'));
    });
  });

};

这个新模块导出一个函数,该函数将绑定两条新路线GET /newPOST /。这些路由稍后将被附加到/users名称空间,这意味着当服务器接收到对/users/newGET请求和对/usersPOST请求时,它们将被激活。

GET /new路线上,我们将展示一个包含用户表单的模板。放在templates/users/new.html下面:

<h1>New User</h1>
<form action="/users" method="POST">
  <p>
    <label for="email">E-mail</label>
    <input type="email" name="email" value="" id="email" />
  </p>
  <p>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" value="" required/>
  </p>
  <input type="submit" value="Submit" />
</form>

我们还需要创建一个Thank you for registering模板,您需要将其放置在templates/users/show.html中:

<h1>Thank you!</h1>
<p>Thank you for registering. You can now <a href="/session/new">log in here</a></p>

POST /路由处理程序中,我们将进行一些简单的验证,并通过调用名为insert的函数将用户文档插入到 CouchDB 数据库中。这个函数试图插入用户文档,并利用一些巧妙的错误处理。如果错误为“404 未找到”,则表示users数据库尚未创建,我们趁机创建并自动重复用户文档插入。

您还捕捉到了 409 冲突 HTTP 状态代码,如果我们试图插入一个已经存在密钥的文档,CouchDB 将返回该代码。因为我们使用用户电子邮件作为文档密钥,所以我们通知用户这样的用户名已经存在。

在这里,除了其他简化,您将用户密码以纯文本形式存储在数据库中。这显然是不推荐的,但是由于本书的核心不是如何创建 web 应用,所以这个实现细节与您的目标无关。

现在,我们需要通过更新并在文件app.js中的app.start(3000)前添加一行,将这些新路由附加到/users/网址名称空间:

var flatiron = require('flatiron'),
    path = require('path'),
    nstatic = require('node-static'),
    app = flatiron.app;

app.config.file({ file: path.join(__dirname, 'config', 'config.json') });

var file = new nstatic.Server(__dirname + '/public/');

app.use(flatiron.plugins.http, {
  before: [
    function(req, res) {
      var found = app.router.dispatch(req, res);
      if (! found) {
        file.serve(req, res);
      }
    }
  ]
});

app.router.path('/users', require('./routes/users'));

app.start(3000);

现在,您可以通过在命令行中键入以下命令来启动应用:

$ node app

这将启动服务器。然后打开网页浏览器,点击http://localhost:3000/users/new。您将获得一个用户表单:

User registration

提交电子邮件和密码,您将看到一个确认屏幕:

User registration

该屏幕将向您呈现一个到/session/new网址的链接,该网址尚不存在。

现在,您已经准备好实现登录屏幕了。

登录和会话管理

为了能够保持会话,您的 HTTP 服务器需要能够做两件事:解析 cookies 和存储会话数据。为此,我们使用两个模块,即flatware-cookie-parserflatware-session,您应该将其添加到package.json清单中:

{
  "description": "To-do App",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "union": "0.3.0",
    "flatiron": "0.2.8",
    "plates": "0.4.x",
    "node-static": "0.6.0",
    "nano": "3.3.0",
 "flatware-cookie-parser": "0.1.x",
 "flatware-session": "0.1.x"
  },
  "scripts": {
    "test": "vows --spec",
    "start": "node app.js"
  },
  "name": "todo",
  "author": "Pedro",
  "homepage": ""
}

现在,安装缺少的依赖项:

$ npm install
flatware-cookie-parser@0.1.0 node_modules/flatware-cookie-parser

flatware-session@0.1.0 node_modules/flatware-session

接下来、在文件app.js中将这些中间件组件添加到您的服务器中:

var flatiron = require('flatiron'),
    path = require('path'),
    nstatic = require('node-static'),
    app = flatiron.app;

app.config.file({ file: path.join(__dirname, 'config', 'config.json') });

var file = new nstatic.Server(__dirname + '/public/');

app.use(flatiron.plugins.http, {
  before: [
 require('flatware-cookie-parser')(),
 require('flatware-session')(),
    function(req, res) {
      var found = app.router.dispatch(req, res);
      if (! found) {
        file.serve(req, res);
      }
    }
  ]
});

app.router.path('/users', require('./routes/users'));
app.router.path('/session', require('./routes/session'));

app.start(3000);

我们还需要创建一个routes/session.js模块来处理新的会话路由:

var plates  = require('plates'),
    fs      = require('fs'),
    couchdb = require('../lib/couchdb'),
    dbName  = 'users',
    db      = couchdb.use(dbName),
    Plates  = require('plates'),
    layout  = require('../templates/layout');

var templates = {
  'new' : fs.readFileSync(__dirname +
    '/../templates/session/new.html', 'utf8')
};

module.exports = function() {

  this.get('/new', function() {
    this.res.writeHead(200, {'Content-Type': 'text/html'});
    this.res.end(layout(templates['new'], 'Log In'));
  });

  this.post('/', function() {

    var res   = this.res,
        req   = this.req,
        login = this.req.body;

    if (! login.email || ! login.password) {
      return res.end(layout(templates['new'], 'Log In',
        {error: 'Incomplete Login Data'}));
    }

    db.get(login.email, function(err, user) {
      if (err) {
        if (err.status_code === 404) {
          // User was not found
          return res.end(layout(templates['new'], 'Log In',
            {error: 'No such user'}));
        }
        console.error(err.trace);
        res.writeHead(500, {'Content-Type': 'text/html'});
        return res.end(err.message);
      }

      if (user.password !== login.password) {
        res.writeHead(403, {'Content-Type': 'text/html'});
        return res.end(layout(templates['new'], 'Log In',
            {error: 'Invalid password'}));
      }

      // store session
      req.session.user = user;

      // redirect user to TODO list
      res.writeHead(302, {Location: '/todos'});
      res.end();
    });

  });  

};

接下来我们需要在templates/session/new.html下添加一个包含登录表单的视图模板:

<h1>Log in</h1>
<form action="/session" method="POST">
  <p>
    <label for="email">E-mail</label>
    <input type="email" name="email" value="" id="email"/>
  </p>
  <p>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" value="" required/>
  </p>
  <input type="submit" value="Log In" />
</form>

接下来,如果服务器仍在运行,请将其停止(通过按下 Ctrl + C )并再次启动:

$ node app.js

将浏览器指向http://localhost:3000/session/new并插入您已经注册的用户的电子邮件和密码:

Logging in and session management

如果登录成功,您将被重定向到/todos网址,服务器尚未响应。

接下来,我们要让待办事项清单发挥作用。

待办事项列表

对于显示待办事项列表,我们将使用表格。使用拖放对待办事项进行排序会很好。一个简单的方法是使用 jQuery 用户界面。单就这项功能而言,您不需要完整的 jQuery UI 库,您可以通过将浏览器指向http://jqueryui.com/download,取消选择交互元素中除可排序选项之外的所有选项,然后单击下载按钮来下载定制的 jQuery UI 库。解压缩生成的文件,并将jquery-ui-1.8.23.custom.min.js文件复制到public/js中。

The to-do list

我们需要在templates.htmllayout.html文件中引用该脚本:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title id="title"></title>
    <link href="/css/bootstrap.min.css" rel="stylesheet" />
  </head>
  <body>

    <section role="main" class="container">

      <div id="messages"></div>

      <div id="main-body"></div>

    </section>

    <script src="/js/jquery.min.js"></script> 
 <script src="/js/jquery-ui-1.8.23.custom.min.js"></script> 
    <script src="/js/bootstrap.min.js"></script>
 <script src="/js/todos.js"></script>
  </body>
</html>

您还应该在public/js/todos.js下添加一个包含一些前端交互代码的文件。

现在我们需要通过首先在app.js文件中包含新的路由来响应/todos网址:

var flatiron = require('flatiron'),
    path = require('path'),
    nstatic = require('node-static'),
    app = flatiron.app;

app.config.file({ file: path.join(__dirname, 'config', 'config.json') });

var file = new nstatic.Server(__dirname + '/public/');

app.use(flatiron.plugins.http, {
  before: [
    require('flatware-cookie-parser')(),
    require('flatware-session')(),
    function(req, res) {
      var found = app.router.dispatch(req, res);
      if (! found) {
        file.serve(req, res);
      }
    }
  ]
});

app.router.path('/users', require('./routes/users'));
app.router.path('/session', require('./routes/session'));
app.router.path('/todos', require('./routes/todos'));

app.start(3000);

然后我们需要在routes/todos.js下放置新的待办路线模块:

var fs      = require('fs'),
    couchdb = require('../lib/couchdb'),
    dbName  = 'todos',
    db      = couchdb.use(dbName),
    Plates  = require('plates'),
    layout  = require('../templates/layout'),
    loggedIn = require('../middleware/logged_in')();

var templates = {
  index : fs.readFileSync(__dirname +
    '/../templates/todos/index.html', 'utf8'),
  'new' : fs.readFileSync(__dirname +
    '/../templates/todos/new.html', 'utf8')
};

function insert(email, todo, callback) {
  var tries = 0,
      lastError;

  (function doInsert() {
    tries ++;
    if (tries >= 3) return callback(lastError);

    db.get(email, function(err, todos) {
      if (err && err.status_code !== 404) return callback(err);

      if (! todos) todos = {todos: []};
      todos.todos.unshift(todo);

      db.insert(todos, email, function(err) {
        if (err) {
          if (err.status_code === 404) {
            lastError = err;
            // database does not exist, need to create it
            couchdb.db.create(dbName, function(err) {
              if (err) {
                return callback(err);
              }
              doInsert();
            });
            return;
          }
          return callback(err);
        }
        return callback();
      });
    });
  })();

}

module.exports = function() {

  this.get('/', [loggedIn, function() {

    var res = this.res;

    db.get(this.req.session.user.email, function(err, todos) {

      if (err && err.status_code !== 404) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      if (! todos) todos = {todos: []};
      todos = todos.todos;

      todos.forEach(function(todo, idx) {
        if (todo) todo.pos = idx + 1;
      });

      var map = Plates.Map();
      map.className('todo').to('todo');
      map.className('pos').to('pos');
      map.className('what').to('what');
      map.where('name').is('pos').use('pos').as('value');

      var main = Plates.bind(templates.index, {todo: todos}, map);
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(layout(main, 'To-Dos'));

    });

  }]);

  this.get('/new', [loggedIn, function() {

    this.res.writeHead(200, {'Content-Type': 'text/html'});
    this.res.end(layout(templates['new'], 'New To-Do'));
  }]);

  this.post('/', [loggedIn, function() {

    var req  = this.req,
        res  = this.res,
        todo = this.req.body
    ;

    if (! todo.what) {
      res.writeHead(200, {'Content-Type': 'text/html'});
      return res.end(layout(templates['new'], 'New To-Do',
        {error: 'Please fill in the To-Do description'}));
    }

    todo.created_at = Date.now();

    insert(req.session.user.email, todo, function(err) {

      if (err) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      res.writeHead(303, {Location: '/todos'});
      res.end();
    });

  }]);

  this.post('/sort', [loggedIn, function() {

    var res = this.res,
        order = this.req.body.order && this.req.body.order.split(','),
        newOrder = []
        ;

    db.get(this.req.session.user.email, function(err, todosDoc) {
      if (err) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      var todos = todosDoc.todos;

      if (order.length !== todos.length) {
        res.writeHead(409);
        return res.end('Conflict');
      }

      order.forEach(function(order) {
        newOrder.push(todos[parseInt(order, 10) - 1]);
      });

      todosDoc.todos = newOrder;

      db.insert(todosDoc, function(err) {
        if (err) {
          res.writeHead(500);
          return res.end(err.stack);
        }
        res.writeHead(200);
        res.end();
      });

    });
  }]);

  this.post('/delete', [loggedIn, function() {

    var req = this.req,
        res = this.res,
        pos = parseInt(req.body.pos, 10)
        ;

    db.get(this.req.session.user.email, function(err, todosDoc) {
      if (err) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      var todos = todosDoc.todos;
      todosDoc.todos = todos.slice(0, pos - 1).concat(todos.slice(pos));

      db.insert(todosDoc, function(err) {
        if (err) {
          res.writeHead(500);
          return res.end(err.stack);
        }
        res.writeHead(303, {Location: '/todos'});
        res.end();
      });

    });

  }]);

};

该模块响应待办事项索引(GET /todos),为登录用户提取并呈现所有待办事项。将以下模板放置在templates/todos/index.html下方:

<h1>Your To-Dos</h1>

<a class="btn" href="/todos/new">New To-Do</a>

<table class="table">
  <thead>
    <tr>
      <th>#</th>
      <th>What</th>
      <th></th>
    </tr>
  </thead>
  <tbody id="todo-list">
    <tr class="todo">
      <td class="pos"></td>
      <td class="what"></td>
      <td class="remove">
        <form action="/todos/delete" method="POST">
          <input type="hidden" name="pos" value="" />
          <input type="submit" name="Delete" value="Delete" />
        </form>
      </td>
    </tr>
  </tbody>
</table>

另一个新的路线是GET /todos/new,向用户呈现一个用于创建新待办事项的表单。该路线使用放置在templates/todos/new.html中的新模板:

<h1>New To-Do</h1>
<form action="/todos" method="POST">
  <p>
    <label for="email">What</label>
    <textarea name="what" id="what" required></textarea>
  </p>
  <input type="submit" value="Create" />
</form>

POST /todos路线通过调用本地insert函数创建一个新的待办事项,该函数处理数据库不存在时的错误,根据需要创建并在稍后重试insert函数。

索引模板取决于位于public/js/todos.js下的客户端脚本的存在:

$(function() {
  $('#todo-list').sortable({
    update: function() {
      var order = [];
      $('.todo').each(function(idx, row) {
        order.push($(row).find('.pos').text());
      });

      $.post('/todos/sort', {order: order.join(',')}, function() {
        $('.todo').each(function(idx, row) {
          $(row).find('.pos').text(idx + 1);
        });
      });

    } 
  });
});

该文件激活并处理拖放项目,用待办事项的新顺序对/todos/sort网址进行 AJAX 调用。

每个项目上的删除按钮也在todos.js路由模块中通过加载用户待办事项、移除给定位置的项目并将项目存储回来来处理。

您可能已经注意到,我们将给定用户的所有待办事项存储在todos数据库的一个文档中。如果所有用户都将待办事项的数量保持在相对较低的水平,这种技术很简单,效果也很好。无论如何,这个细节对我们的目的来说并不重要。

要做到这一点,我们需要在middleware/logged_in.js下提供一个路由中间件。该中间件组件负责保护某些路由,并且在用户未登录时,将用户重定向到登录屏幕,而不是执行该路由:

function LoggedIn() {
  return function(next) {
    if (! this.req.session || ! this.req.session.user) {
      this.res.writeHead(303, {Location: '/session/new'});
      return this.res.end();
    }
    next();
  };
}

module.exports = LoggedIn;

最后,如果服务器还在运行,停止服务器(通过点击 Ctrl + C )并重新启动:

$ node app.js

将浏览器指向http://localhost:3000/session/new,输入已注册用户的电子邮件和密码。然后,您将被重定向到用户的待办事项列表,该列表将以空开始。

The to-do list

您现在可以点击新待办事项按钮,获得以下表格:

The to-do list

插入一些文本,点击创建按钮。待办事项将被插入数据库,并显示更新后的待办事项列表:

The to-do list

您可以插入任意多的待办事项。一旦你有足够的数据,你可以试着通过拖放表格行来重新排序。

The to-do list

您也可以点击删除按钮删除特定的待办事项。

文件摘要

以下是组成此应用的文件列表:

$ tree
.
├── app.js
├── config
   └── config.json
├── lib
   └── couchdb.js
├── middleware
   └── logged_in.js
├── package.json
├── public
   ├── css
      ├── bootstrap-responsive.css
      ├── bootstrap-responsive.min.css
      ├── bootstrap.css
      └── bootstrap.min.css
   ├── img
      ├── glyphicons-halflings-white.png
      └── glyphicons-halflings.png
   └── js
       ├── bootstrap.js
       ├── bootstrap.min.js
       ├── jquery-ui-1.8.23.custom.min.js
       ├── jquery.min.js
       └── todos.js
├── routes
   ├── session.js
   ├── todos.js
   └── users.js
├── templates
   ├── alert.html
   ├── layout.html
   ├── layout.js
   ├── session
      └── new.html
   ├── todos
      ├── index.html
      └── new.html
   └── users
       ├── new.html
       └── show.html
└── test

13 directories, 27 files

总结

在本章中,您学习了如何使用 Node.js、Flatiron.js 和一些其他组件创建一个简单的 web 应用。

这个应用将是我们未来章节中用户界面测试的目标。