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 调用)。
注意为了您的方便,此 Todo 应用的所有源代码都在https://github.com/azat-co/todo-express处。读者不断为该项目做出贡献,因此,当这本书到了您的手中时,GitHub 中的代码将与书中的代码不同,可能会有更多的功能和最新的库。
这个项目相当复杂,所以在你开始编码之前,这里有一个本章介绍如何实现最终产品的步骤的概述:
- 概观
- 设置
- App.js
- 路线
- 翡翠
- 较少的
概观
为了预览我们将在本章中实现的内容,让我们从 Todo 应用的一些截图开始,展示用户界面是如何工作的。图 20-1 显示了主页,它有一个标题、一个菜单和一些介绍性的文字。菜单由三个项目组成:
- 首页:当前显示的页面
- 待办事项列表:要做的任务列表
- 已完成:已完成任务列表
图 20-1 。Todo app 首页
在待办事项页面,有一个空列表,如图图 20-2 所示。还有一个新任务的输入表单和一个“添加”按钮。
图 20-2 。清空待办事项页面
图 20-3 显示了添加四个项目到待办事项列表的结果。每个任务的左边有一个“完成”按钮,右边有一个“删除”按钮,它们的功能正如你所想。他们分别将任务标记为已完成(即,将其移动到已完成页面)和移除任务。
图 20-3 。添加了项目的待办事项列表页面
图 20-4 显示了点击“购买牛奶”任务的“完成”按钮的结果。该项目已从待办事项列表中消失,列表已重新编号。
图 20-4 。一个项目标记为完成的待办事项列表页
然而,已完成的“买牛奶”任务并没有从应用中完全消失。现在在已完成页面的已完成列表中,如图图 20-5 所示。
图 20-5 。待办事宜 app 完成页面
点击“删除”按钮后,从待办事项列表页面删除一个项目是通过 AJAX/XHR 请求执行的操作。图 20-6 显示了删除任务时出现的紫色高亮通知消息(在本例中,是“Email LeanPub”任务)。其余的逻辑通过 get 和 POSTs(通过表单)实现。
图 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:用于基本的错误处理
jadev1.5.0:用于玉石模板- 1.0.4 版:支持更少
method-overridev2.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 模块mongodb和mongoskin不同,它们是驱动程序。这些库允许我们与 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.jsonbootstrap文件夹中的*.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');我们还需要核心的http和path模块:
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。推荐值为resave的false和saveUninitialized的true。
如果您没有为这些选项指定值,那么您将会得到警告,因为这些选项的默认值将来可能会改变。所以,显式地设置选项是有好处的。或者,要取消这些警告,可以使用环境变量:
$ 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 开始。它以doctype、html和head类型开始:
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】





