NodeJS 秘籍(三)
原文:
zh.annas-archive.org/md5/B8CF3F6C144C7F09982676822001945F译者:飞龙
第六章:使用 Express 加速开发
在本章中,我们将涵盖:
-
生成 Express 脚手架
-
定义和应用环境
-
动态路由
-
Express 中的模板
-
Express 中的 CSS 引擎
-
初始化和使用会话
-
创建一个 Express Web 应用程序
介绍
尽管 Node 的 HTTP 模块非常出色,但 Express 重新打包和简化了其功能,为我们提供了一个流畅的接口,几乎没有摩擦的快速 Web 开发。
在本章中,我们将从生成一个普通的 Express 项目基础开始,到一个完整的 Express Web 应用程序基础,MongoDB 提供后端数据支持。
提示
Express 2 到 Express 3
在本章中,有一些有用的提示框,比如这个,演示了如何将代码从 Express 2 迁移到 Express 3。支持代码文件包含了 Express 2 和 3 的代码(3 被注释掉)。代码文件可以从www.packtpub.com/support下载。
生成 Express 脚手架
Express 既可以作为一个 Node 模块,也可以作为一个命令行可执行文件。当我们从命令行运行express时,它会为我们生成一个项目骨架,加快准备过程。
准备工作
我们需要使用-g标志(全局安装)来安装express,以便从任何目录运行express可执行文件。
sudo npm -g install express
我们使用sudo来确保我们获得全局安装的权限。这在 Windows 下不适用。
如何做...
首先,我们决定我们应用的名称。让我们称之为nca(Node Cookbook App),然后简单地执行:
express nca
这将在一个名为nca的新目录下生成所有项目文件。在我们运行应用之前,我们必须确保所有依赖项都已安装。我们可以在nca/package.json中找到应用的依赖项:
{
"name": "application-name"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "jade": ">= 0.0.1"
}
}
为了可移植性,重要的是在project文件夹中安装相关模块。为了实现这一点,我们只需在命令行中cd进入nca目录,然后输入:
npm install
这将在我们的project文件夹中创建一个新的node_modules目录,其中包含所有的依赖项。
它是如何工作的...
当我们运行express可执行文件时,它会创建一个适合 Express 开发的文件夹结构。在项目根目录中,我们有app.js和package.json文件。
package.json是由 CommonJS 组(一个 Javascript 标准社区)建立的约定,并已成为描述 Node 中模块和应用的已建立方法。
npm install命令从package.json中解析依赖项,在node_modules文件夹中本地安装它们。
这很重要,因为它确保了稳定性。Node 的require函数在搜索父目录之前会在当前工作目录中寻找node_modules文件夹。如果我们在父目录中升级任何模块,我们的项目将继续使用构建时的相同版本。本地安装模块允许我们将项目与其依赖项一起分发。
app.js文件是我们项目的样板。我们用以下命令运行我们的应用:
node app.js
express可执行文件将三个子目录添加到项目文件夹中:public, routes和views。
public是app.js传递给express.static方法的默认文件夹,所有静态文件都放在这里。它包含images, javascripts和stylesheets文件夹,每个文件夹都有自己明显的目的。
routes文件夹包含index.js,被app.js所需。为了定义我们的路由,我们将它们推送到 Node 的exports对象上(我们将在第九章中学到更多关于编写自己的 Node 模块)。使用routes/index.js有助于避免app.js中的混乱,并将服务器代码与路由代码分开。这样我们可以纯粹地专注于我们的服务器,或者纯粹地专注于我们的路由。
最后,views包含模板文件,这可以真正帮助加速开发。我们将在Express 中的模板中了解如何处理视图。
还有更多...
让我们花一些时间深入了解我们生成的项目。
解析 app.js
让我们来看一下我们生成的app.js文件:
var express = require('express')
, routes = require('./routes')
var app = module.exports = express.createServer();
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Routes
app.get('/', routes.index);
app.listen(3000, function(){
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});
app变量保存了express.createServer的结果,这实质上是一个增强的http.createServer。
configure方法被调用了三次:一次用于全局设置,一次用于生产环境,一次用于开发环境。我们将在下一个示例中更详细地查看生产环境和开发环境。
在全局configure回调中,设置了默认的视图目录(views)和引擎(jade),并告诉app使用“express.bodyParser, express.methodOverride, app.router”和express.static中间件。
bodyParser在第二章探索 HTTP 对象的第一个示例的*还有更多..*部分中简要出现,以connect.bodyParser的形式。
Express 包含所有标准的 Connect 中间件,并兼容附加的 Connect 中间件。因此,在 Express 项目中,使用“express.bodyParser”加载bodyParser。bodyParser 使我们能够访问从客户端发送的任何数据(例如在 POST 请求中)。
methodOverride允许我们使用名为_method的隐藏输入元素从浏览器表单中进行伪DELETE和PUT请求。例如:
<input type=hidden name='_method' value='DELETE'>
在超文本传输协议文档中定义了许多 HTTP 方法(参见www.w3.org/Protocols/rfc2616/rfc2616-sec9.html))。然而,浏览器通常只支持 GET 和 POST,其他方法留给特定客户端支持。Express 通过使用隐藏输入来模拟DELETE请求来解决浏览器支持不足的问题,同时还支持来自支持该方法的客户端的真实 DELETE 请求。
app.router包含了所有定义的路由(传递给app.get, app.post等)。路由本身就是中间件。如果没有将app.router传递给app.use,则路由将自动附加到中间件堆栈。但是,通过手动包含,我们可以在app.router中间件之后放置其他中间件。
中间件通常构造如下:
function (req, res, next) {
//do stuff
next();
}
next参数是一种回调机制,它加载任何随后的中间件。因此,当app.router位于express.static之上时,客户端访问的任何动态路由都不会不必要地触发静态服务器去寻找不存在的文件,除非这些路由调用next(或者可以将next作为“req: req.next()”的方法调用)。有关中间件的更多信息,请参阅www.expressjs.com/guide.html#middleware。
查看 routes/index.js
在app.js, routes/index.js中加载了一个require:
, routes = require('./routes')
请注意,index没有指定,但是如果将目录传递给require,Node 将自动查找index.js。让我们来看一下:
exports.index = function(req, res){
res.render('index', { title: 'Express' })
};
将index推送到exports对象中,使其在app.js中作为routes.index可用,并将其传递给app.get如下:
app.get('/', routes.index);
routes.index函数应该看起来很熟悉。它遵循了http.createServer回调的模式,但是特定于路由。请求(req)和响应(res)参数由 Express 增强。我们将在接下来的示例中详细了解这些内容。该函数本身只是调用res.render方法,该方法从views/index.jade加载模板,将title作为变量传递,然后将生成的内容输出到客户端。
另请参阅
-
在本章中讨论的定义和应用环境
-
在本章中讨论的动态路由
-
在本章中讨论的Express 中的模板
定义和应用环境
开发和生产代码有不同的要求。例如,在开发过程中,我们很可能希望向客户端输出详细的错误信息,以进行调试。在生产环境中,我们通过尽可能少地暴露内部信息来保护自己免受机会主义性的利用。
Express 通过app.configure来满足这些差异,它允许我们定义具有特定设置的环境。
准备工作
我们需要从上一个配方中获取我们的项目文件夹(nca)。
如何做...
让我们来看看预配置的环境:
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
生成的文件为每个环境定义了定制的错误报告级别。让我们为我们的生产服务器添加缓存,这在开发中可能会成为障碍。
我们使用express.staticCache来实现这一点。但是,它必须在express.static之前调用,所以我们将express.static从全局configure中移动到开发和生产环境中,并在生产环境中加入staticCache,如下所示:
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
});
app.configure('development', function(){
app.use(express.static(__dirname + '/public'));
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.staticCache());
app.use(express.static(__dirname + '/public'));
app.use(express.errorHandler({dumpExceptions: true}));
});
我们还为生产errorHandler设置了dumpExceptions为true。这将使我们能够快速识别一旦启动我们的应用程序可能出现的任何问题。
要使用一个环境,我们在执行node时在命令行上设置特殊的NODE_ENV变量:
NODE_ENV=production node app.js
或者在 Windows 上:
set NODE_ENV=production
node app.js
开发环境是默认的,因此无需使用NODE_ENV来设置它。
它是如何工作的...
Express 为我们提供了一个非常方便的方法来分离我们的工作流程。我们所要做的就是传入我们的环境名称和特定设置。
在内部,Express 将使用process.env来确定NODE_ENV变量,检查是否与任何定义的环境匹配。如果未设置NODE_ENV,Express 默认加载开发环境。
还有更多...
让我们来看看一些管理我们环境的方法。
设置其他环境
我们的工作流程中可能有其他阶段,可以从特定设置中受益。例如,我们可能有一个分阶段,在这个阶段,我们在开发机器上尽可能模拟生产环境,以进行测试。
例如,如果我们的生产服务器要求我们在特定端口上运行进程(比如端口 80),而我们在开发服务器上无法实现(例如如果我们没有 root 权限),我们可以添加一个分阶段环境,并在生产环境中设置一个只在生产环境中设置为80的port变量。
参见第十章,“上线”,了解如何安全地在端口 80 上运行 Node 的信息。
让我们按照以下方式在开发环境下添加分阶段环境:
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
//our extra staging environment
app.configure('staging', function(){
app.use(express.errorHandler({dumpExceptions: true}));
});
现在我们将添加端口逻辑,如下面的代码所示:
var port;
app.configure('production', function(){
port = 80;
app.use(express.errorHandler({dumpExceptions: true}));
});
// Routes
app.get('/', routes.index);
app.listen(port || 3000);
因此我们的port是根据环境设置的,如果port为空,我们默认为3000。
我们可以用以下方式初始化我们的生产服务器:
sudo NODE_ENV=production node app.js
或者对于 Windows:
set NODE_ENV=production
node app.js
当尝试使用NODE_ENV设置为production运行服务器时,如果收到TypeError: Cannot read property 'port' of null,很可能是端口 80 上已经运行了一个服务。我们需要停止这个服务以测试我们的代码。例如,如果 Apache 在我们的系统上运行,它可能是通过端口80进行托管。我们可以使用sudo apachectl -k stop(或者在 Windows 上使用net stop apache2.2)来停止 Apache。
永久更改 NODE_ENV
如果我们处于一个分阶段的过程中,我们可能不希望每次加载我们的应用程序时都要输入NODE_ENV=staging。同样的情况也适用于生产环境。虽然服务器启动的次数会少得多,但我们必须记得在重新启动时设置NODE_ENV。
我们可以在类 Unix 系统(Linux 或 Max OS X)上使用export shell 命令来简化操作,如下所示:
export NODE_ENV=staging
这只在我们的终端打开时设置NODE_ENV。要使其永久生效,我们将这行添加到我们的主目录的rc文件中。rc文件的名称取决于 shell。对于 bash,它位于~/.bashrc(其中~是主文件夹)。其他 shell,如sh和ksh,将是~/.shrc, ~/.kshrc等。
要永久设置NODE_ENV,我们可以使用:
echo -e "export NODE_ENV=staging\n" >> ~/.bashrc
其中,staging 是我们期望的环境,bash 是我们的 shell。
在 Windows 中,我们使用set和setx:
set NODE_ENV=staging
setx NODE_ENV=staging
set立即生效,但一旦命令提示符关闭就会丢失。setx永久应用,但直到我们打开一个新的命令提示符,所以我们两者都使用。
另请参阅
-
生成 Express 脚手架在本章中讨论
-
部署到服务器环境在第十章中讨论,上线
-
制作 Express Web 应用程序在本章中讨论
-
初始化和使用会话在本章中讨论
动态路由
在本烹饪书的第一个食谱中,设置路由,我们探讨了在 Node 中设置路由的各种方法。Express 提供了一个远远优越且非常强大的路由接口,我们将在本食谱中探讨。
准备工作
我们将使用我们的nca文件夹。
如何做...
假设我们想为一个名为 Mr Page 的虚构角色添加一个页面。我们将路由命名为page,因此在app.js的routes部分中,我们添加以下代码:
app.get('/page', function (req, res) {
res.send('Hello I am Mr Page');
});
我们还可以定义灵活的路由,并使用req.params来获取请求的路由,如下所示:
app.get('/:page', function (req, res) {
res.send('Welcome to the ' + req.params.page + ' page');
});
在开发过程中,直接将回调函数放入app.get是可以的,但为了使app.js更整洁,让我们将回调函数从routes/index.js中加载,如下所示:
exports.index = function(req, res){
res.render('index', { title: 'Express' })
};
exports.mrpage = function (req, res) {
res.send('Hello I am Mr Page');
};
exports.anypage = function (req, res) {
res.send('Welcome to the ' + req.params.page + ' page');
};
然后在我们的app.js文件中,我们的路由变成了:
// Routes
app.get('/', routes.index);
app.get('/page', routes.mrpage);
app.get('/:page', routes.anypage);
它是如何工作的...
我们使用app.get创建了/page路由。然后在app.get的回调中概述我们希望如何响应该路由。在我们的示例中,我们使用res.send(增强的res.write)来输出简单的文本。这是我们不灵活的动态路由。
Express 还提供了使用占位符的灵活路由功能。在主要的示例中,我们定义了一个:page占位符。当请求填充占位符时(例如,/anyPageYouLike),占位符的实现将根据其名称添加到req.params中。因此,在这种情况下,req.params.page将保存/anyPageYouLike。
当用户加载localhost:3000/page时,他们会看到Hello I am Mr Page,当他们访问localhost:3000/absolutelyAnythingElse时,他们会得到Welcome to the absolutelyAnythingElse page。
还有更多...
Express 路由还可以做哪些其他事情?
路由验证
我们可以使用正则表达式语法的部分来限制灵活路由到特定的字符范围,如下所示:
app.get('/:page([a-zA-Z]+)', routes.anypage);
我们传递一个字符匹配,[a-zA-Z]以及一个加号(+)。这将匹配一个或多个字符。因此,我们将我们的:page参数限制为仅包含字母。
因此,http://localhost:3000/moo将给出Welcome to the moo page,而http://localhost:3000/moo1将返回404错误。
可选路由
我们还可以使用问号(?)来定义可选路由:
app.get('/:page/:admin?', routes.anypageAdmin);
我们将把这个放在我们的app.js文件中,在我们定义的其他路由下面。
我们在routes/index.js中的anypageAdmin函数可能是这样的:
exports.anypageAdmin = function (req, res) {
var admin = req.params.admin
if (admin) {
if (['add','delete'].indexOf(admin) !== -1) {
res.send('So you want to ' + req.params.admin + ' ' + req.params.page + '?');
return;
}
res.send(404);
}
}
我们检查:admin占位符是否存在。如果路由满足它,我们验证它是否被允许(添加或删除),并发送一个定制的响应。如果路由不被允许,我们发送一个404错误。
虽然查询通配符(?)可能适用于许多类似的路由,但如果我们只有我们的add和delete路由,并且没有可能以后添加更多路由,我们可以以更简洁的方式实现这个功能。
在app.js中,我们可以放置:
app.get('/:page/:admin((add|delete))', routes.anypageAdmin);
在index/routes.js中:
exports.anypageAdmin = function (req, res) {
res.send('So you want to ' + req.params.admin + ' ' + req.params.page + '?');
}
星号通配符
我们可以使用星号(*)作为通配符来进行一般匹配。例如,让我们添加以下路由:
app.get('/:page/*', routes.anypage);
并将routes.anypage更改为以下内容:
exports.anypage = function (req, res) {
var subpage = req.params[0],
parentPage = subpage ? ' of the ' + req.params.page + ' page' : '';
res.send('Welcome to the ' +
(subpage || req.params.page) + ' page' + parentPage);
};
现在,如果我们访问localhost:3000/foo/bar,我们会看到欢迎来到 foo 页面的 bar 页面,但如果我们只访问localhost:3000/foo,我们会看到欢迎来到 foo 页面。
我们还可以稍微疯狂一点,将其应用到 Mr Page 的路由上,如下所示:
app.get('/*page*', routes.mrpage);
现在,任何包含单词page的路由都将收到Mr Page的消息。
另请参阅
-
在第一章中讨论的设置路由器,制作 Web 服务器
-
在本章中讨论的制作 Express Web 应用
-
在本章中讨论的 Express 中的模板
在 Express 中使用模板
Express 框架的一个基本部分是其使用视图。视图只是保存模板代码的文件。Express 帮助我们将代码分离为操作上不同的关注点。我们在app.js中有服务器代码,在routes/index.js中有特定于路由的功能,然后我们在views文件夹中有我们的输出生成逻辑。模板语言提供了定义动态逻辑驱动内容的基础,模板(或视图)引擎将我们的逻辑转换为最终提供给用户的 HTML。在这个示例中,我们将使用 Express 的默认视图引擎 Jade 来处理和呈现一些数据。
注意
在*还有更多..*部分,我们将了解如何更改视图引擎。
可以在www.github.com/visionmedia/express/wiki找到支持的模板引擎列表。可以在paularmstrong.github.com/node-templates/找到各种模板引擎的比较。
准备工作
对于我们的数据,我们将使用我们在第三章中创建的profiles.js对象。我们需要将其复制到nca文件夹的根目录中。
如何做...
让我们保持简单,并删除我们添加到app.js的任何路由。我们只想要我们的顶级路由。
由于 Jade 被设置为app.configure中的默认视图引擎,在这个示例中我们不需要在app.js中做其他事情。
在routes/index.js中,我们将删除除index之外的所有路由。
exports.index = function(req, res){
res.render('index', { title: 'Express'})
};
res.render方法加载views/index.jade中的 Jade 模板。我们将使用index.jade作为我们的profiles.js对象数据的视图,因此我们需要将其提供给我们的index视图。
我们通过将其传递给res.render的options对象来实现这一点:
var profiles = require('../profiles.js');
exports.index = function(req, res){
res.render('index', { title: 'Profiles', profiles: profiles})
};
请注意,我们还将title属性更改为'Profiles'。
现在我们只需编辑views/index.jade。生成的index.jade包含以下内容:
h1= title
p Welcome to #{title}
我们将在页面上添加一个表格,输出profiles.js对象中每个人的详细信息:
table#profiles
tr
th Name
th Irc
th Twitter
th Github
th Location
th Description
each profile, id in profiles
tr(id=id)
each val in profile
td #{val}
要测试,我们启动我们的应用程序:
node app.js
然后导航到http://localhost:3000,看到类似以下内容:
它是如何工作的...
res.render从views文件夹中提取index.jade,即使第一个参数只是index。Express 知道views目录中的 Jade 文件是有意的,因为app.js的app.configure包含以下代码:
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
第二个参数是一个对象,包含两个属性:title和profiles。这些对象属性在 Jade 视图中成为局部变量。我们通过使用等号(=)符号进行返回值缓冲来输出变量,或者通过使用 Jade 的插值,像这样包装它:#{title}。
Jade 是一种精简的模板语言。它使用去除括号的标记和基于缩进的语法,还有一个替代块扩展选项(我们使用冒号而不是缩进来表示嵌套)。它还有一组最小的语法集,用于使用井号(#)和点(.)分别定义id和class属性。
例如,以下 Jade:
table#profiles
th Name
将创建以下 HTML:
<table id=profiles><th>Name</th></table>
提示
要了解有关 Jade 语言的更多信息,请访问其 GitHub 页面:www.github.com/visionmedia/jade。
Jade 还处理迭代逻辑。我们使用两个each Jade 迭代器从我们的profiles对象中提取值,如下所示:
each profile, id in profiles
tr(id=id)
each val in profile
td #{val}
这段代码遍历profiles对象,将每个 ID(ryan, isaac, bert等)加载到一个新的id变量中,将包含配置文件信息的每个对象加载到一个profile对象变量中。
在我们的第一个each下面,我们缩进tr(id=id)。与 JavaScript 不同,Jade 中的缩进是逻辑的一部分,因此正确的缩进至关重要。
这告诉 Jade,对于每个配置文件,我们要输出一个<tr>标签,其id属性设置为profile的 ID。在这种情况下,我们不使用井号(#)缩写来设置id属性,因为我们需要 Jade 来评估我们的id变量。tr#id会为每个配置文件生成<tr id=id>,而tr(id=id)会生成<tr id=ryan>或isaac或bert等。
在tr下面再次缩进,表示接下来的内容应该嵌套在<tr>标签内。我们再次使用each来遍历每个子对象的值,并在td下缩进,其中包含每个配置文件的值。
还有更多...
让我们看看 Express 还提供了哪些其他模板功能和特性。
使用其他模板引擎
Express 支持各种替代模板引擎,不支持的引擎可以适应 Express 而不会带来过多的麻烦。
express可执行文件只会生成基于 Jade 或 EJS 的项目脚手架。要生成 EJS,我们只需将ejs传递给-t标志:
express -t ejs nca
我们可以将现有项目转换为 EJS 作为默认视图引擎的 Express 项目(我们将首先将其复制到nca_ejs)。
首先,我们需要编辑package.json中的依赖项:
{
"name": "application-name"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "ejs": "0.7.1"
}
}
我们只是删除了jade,并用ejs代替。现在我们这样做:
npm install
所以npm会将 EJS 模块放入node_modules文件夹中。
最后,在app.configure中更改我们的视图引擎如下:
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
});
这种技术适用于任何 Express 支持的模板引擎。无需require EJS 模块,Express 会在后台处理。
EJS 模板
由于我们已经设置了nca_ejs,我们可以继续在嵌入式 JavaScript 中重写我们的索引视图。
在nca_ejs/views中添加一个新文件index.ejs,并写入:
<h1> <%= title %></h1>
<p> Welcome to <%= title %></p>
<table>
<tr><th>Name</th><th>Irc</th><th>Twitter</th>
<th>Github</th><th>Location</th><th>Description</th></tr>
<% Object.keys(profiles).forEach(function (id) {%>
<tr>
<% Object.keys(profiles[id]).forEach(function (val) { %>
<td><%= profiles[id][val]; %></td>
<% }); %>
</tr>
<% }); %>
</table>
<%和%>表示嵌入式 JavaScript。如果 JavaScript 恰好包裹任何 HTML 代码,则 HTML 将被处理为 JavaScript 的一部分。例如,在我们的forEach回调中,我们有<tr>和<td>,这些都包含在每次循环的输出中。
当开放标签伴随等号(<%=)时,它会评估任何给定的 JavaScript 变量,并将其拉入生成的输出中。例如,在我们的第一个<h1>中,我们输出title变量。
Jade 中的字面 JavaScript
Jade 也可以处理纯 JavaScript。让我们利用这一点,以更简洁、干燥的方式输出我们的表头:
- var headers = ['Name', 'Irc', 'Twitter', 'Github', 'Location', 'Description'];
table#profiles
tr
each header in headers
th= header
each profile, id in profiles
tr(id=id)
each val in profile
td #{val}
行的开头有一个破折号(—),告诉 Jade 我们正在使用纯 JavaScript。在这里,我们简单地创建一个名为headers的新数组,然后使用 Jade 的each迭代器输出我们的标题,使用等号(=)来评估header变量。
我们可以在 Jade 中创建我们的数组如下:
headers = ['Name', 'Irc', 'Twitter', 'Github', 'Location', 'Description'];
Jade 然后将其编译为前面示例中的嵌入式 JavaScript,包括var声明。
Jade 部分
部分被描述为迷你视图或文档片段。它们主要用于自动模板化对数组(集合)的迭代,尽管它们也可以与对象一起使用。
例如,而不是说:
tr(id=id)
each val in profile
td #{val}
我们可以创建一个视图文件,我们将其称为row.jade,在其中写入:
td= row
回到index.jade,我们将我们的each迭代器替换为partial,如下所示:
each profile, id in profiles
tr(id=id)
!= partial('row', {collection: profile})
!=告诉 Jade 不仅要缓冲partial返回的内容,还要避免转义返回的 HTML。如果我们不包含感叹号,Jade 会用特殊实体代码替换 HTML 字符(例如,<变成<)。
我们将'row'传递给partial,告诉 Jade 使用row.jade视图作为部分。我们将一个具有collection属性的对象作为下一个参数传递。如果我们的资料是一个简单的数组,我们可以简单地传递数组,Jade 会为数组中的每个值生成一个td标签。但是,profile变量是对象,因此将其传递给collection会导致 Jade 遍历值,就好像它们是一个简单的数组一样。
我们的collection中的每个值(Ryan Dahl, ryah, Node.js 的创建者等)都由视图的名称引用。因此,在我们的row.jade视图中,我们使用row变量来获取每个值。我们可以通过使用as属性来自定义它,如下所示:
!= partial('row', {collection: profile, as: 'line'})
然后在row.jade中,我们将row更改为line:
td= line
提示
Express 2 到 Express 3
为了简化查看系统内部并使模板引擎更容易集成到 Express 中,版本 3 将不再支持部分。在 Express 3 中,我们可以说,而不是使用partial调用row.jade文件:
each profile, id in profiles
tr(id=id)
each row in profile
td= row
Express 部分
部分的一个很棒的地方是我们可以在 Express 路由上使用它们在响应(res)对象上。这是特别了不起的,因为它允许我们无缝地将 HTML 片段发送到 AJAX 或 WebSocket 请求,同时从相同的片段(在我们的视图中)生成整个页面请求的内容。
在index.jade(部分版本)的末尾,我们将插入一个小的概念验证脚本:
script(src='http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js')
script
setTimeout(function () {
$.get('/', function (row) {
$('#profiles tbody').append(row);
});
}, 1500);
这将等待一秒半,然后向我们的index路由发出一个 AJAX 请求。因此,让我们修改routes/index.js中的index路由:
var profiles = require('../profiles.js');
exports.index = function(req, res){
if (req.xhr) {
res.partial('row', {collection: profiles.ryan});
}
res.render('index', { title: 'Profiles', profiles: profiles});
};
如果请求是一个XmlHttpRequest(AJAX),我们会根据ryan的资料生成一个新的表格行。
现在当我们加载http://localhost:3000时,经过短暂的延迟,Ryan 的资料将出现在表格底部。
提示
Express 2 到 Express 3
Express 3 不支持部分(无论在模板逻辑还是在应用程序代码中),因此我们必须以不同的方式处理。例如,我们可以发送资料的 JSON 表示,并让浏览器循环遍历以填充表格。
在撰写本文时,还没有部分的替代中间件,但在不久的将来可能会有。
Jade 包括
包含帮助我们分离和重用模板代码的部分。让我们将我们的profiles表格放入自己的视图中。我们将其称为profiles.jade。
要从index.jade文件中包含profiles.jade,我们只需执行以下操作:
h1= title
p Welcome to #{title}
include profiles
layout.jade
提示
Express 2 到 Express 3
在 Express 3 中,布局也被取消,以支持块继承。因此,不再将任何渲染的视图隐式地包装到body变量中并在layout.jade中渲染,现在我们必须明确声明一个块,然后将该块插入到我们的 body 中。
生成的项目中还包括layout.jade视图。这是一个与 Express 逻辑交织在一起的特殊视图。任何渲染的视图都被打包到一个body变量中,然后传递到layout.jade中。因此,在我们的情况下,我们告诉res.render组装index.jade。Express 将index.jade转换为 HTML,然后在内部渲染layout.jade,将生成的 HTML 传递给body变量。layout.jade允许我们为视图添加头部和底部。要禁用整个应用程序的此功能,我们使用app.set('view options', {layout:false})。要防止它应用于特定的渲染,我们只需将layout:false传递给res.render的选项对象。
提示
Express 2 到 Express 3
因此,在layout.jade中,我们不再使用body!=body,而是使用以下内容:
body
block content
在index.jade的顶部,我们将使用extend从layout.jade继承,然后定义content块,该块将加载到layout.jade的 body 中:
extends layout
block content
h1= title
p Welcome to #{title}
//- the rest of our template...
提示
所有 Jade Express 代码示例都有一个名为views-Express3的额外文件夹,其中包含等效的模板,这些模板遵循显式块继承模式,而不是隐式布局包装。
另请参阅
-
在本章中讨论的 Express 的 CSS 引擎
-
在本章中讨论的创建 Express web 应用程序
-
在本章中讨论的生成 Express 脚手架
使用 Express 的 CSS 引擎
一旦我们有了我们的 HTML,我们就会想要为它设置样式。当然,我们可以使用原始的 CSS,但 Express 与一些选择的 CSS 引擎集成得很好。
Stylus 就是这样一个引擎。它是为 Express 编写的,并且作为一种语法,它遵循了 Jade 中发现的许多设计原则。
在这个教程中,我们将把 Stylus 放在聚光灯下,学习如何使用它来为我们之前教程中的profiles表应用样式。
准备工作
我们需要我们之前教程中留下的nca文件夹。
如何做...
首先,我们需要设置我们的应用程序来使用 Stylus。
如果我们要开始一个新项目,我们可以使用express可执行文件来生成一个基于 Stylus 的 Express 项目,如下所示:
express -c stylus ourNewAppName
这将生成一个项目,其中stylus是package.json中的一个依赖项,在app.configure中的app.js中有一行额外的代码:
app.use(require('stylus').middleware({ src: __dirname + '/public' }));
然而,既然我们已经有一个项目在热板上,让我们修改我们现有的应用程序来使用 Stylus。
在package.json中:
{
"name": "application-name"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "jade": ">= 0.0.1"
, "stylus": "0.27.x"
}
}
然后在命令行中运行以下命令:
npm install
最后在app.js中,在app.configure中插入以下代码:
app.use(require('stylus').middleware({
src: __dirname + '/views',
dest: __dirname + '/public'
}));
请注意,我们设置了不同的src并添加了dest属性到生成的代码中。
我们将把我们的 Stylus 文件放在views/stylesheets中。所以让我们创建这个目录,并在其中放置一个新文件,我们将其命名为style.styl。Express 将找到这个文件,将生成的 CSS 放在public目录的相应文件夹(stylesheets)中。
为了开始我们的 Stylus 文件,我们将从/stylesheets/style.css中复制当前的 CSS,如下所示:
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00b7ff;
}
Stylus 完全兼容纯 CSS,但为了学习目的,让我们将其转换为最小缩进格式:
body
padding 50px
font 14px "Lucida Grande", Helvetica, Arial, sans-serif;
a
color #00B7FF
现在我们将为之前教程中的#profiles表设置样式。
我们可以使用 Stylus 的@extend指令为我们的td和th标签以及我们的#profile表应用一致的填充:
.pad
padding 0.5em
#profiles
@extend .pad
th
@extend .pad
td
@extend .pad
当新的 CSS 属性被引入到浏览器中时,它们通常带有特定于供应商的前缀,直到实现被认为是成熟和稳定的。其中一个属性是border-radius,在 Mozilla 浏览器上是-moz-border-radius,在 WebKit 类型上被引用为-webkit-border-radius。
编写和维护这种 CSS 可能会相当复杂,所以让我们使用一个 Stylus mixin 来简化我们的生活:
borderIt(rad = 0, size = 1px, type = solid, col = #000)
border size type col
if rad
-webkit-border-radius rad
-moz-border-radius rad
border-radius rad
现在,我们将我们的 mixin 应用到#profiles表和所有的td元素上:
#profiles
borderIt 20px 2px
@extend .pad
th
@extend .pad
td
@extend .pad
borderIt(col: #000 + 80%)
因此,我们的#profiles表现在看起来如下截图所示:
它是如何工作的...
作为一个模块,stylus可以独立于 Express 运行。然而,它也有一个方便的middleware方法,可以传递到app.use中。
当express可执行文件生成一个使用 Stylus 的项目时,只设置了src属性,这意味着 Stylus 从一个地方加载.styl文件,并将它们转换为.css文件放在同一个文件夹中。当我们设置dest时,我们从一个地方加载我们的 Stylus 代码,并将它保存在另一个地方。
我们的src是views,dest是public,但即使我们把我们的styles.styl放在views的子目录中,Stylus 仍然可以找到它,并将它放在dest文件夹的相应子目录中。
layout.jade文件包括一个到/stylesheets/style.css的link标签。因此,当我们在views/stylesheets中创建style.styl文件时,生成的 CSS 将被写入public/stylesheets。由于我们的静态服务器目录设置为public,对/stylesheets/style.css的请求将从public/stylesheets/style.css中提供。
我们使用了几个 Stylus 功能来创建我们的样式表。
@extend指令基于继承的概念。我们创建一个类,然后使用@extend将该类的所有特性应用到另一个元素上。我们在这个教程中使用@extend创建了以下 CSS:
.pad,
#profiles,
#profiles th,
#profiles td { padding: 0.5em;}
我们的样式基础越大,@extend指令就越能简化维护和可读性。
通过使用 mixin,我们可以更容易地定义边框,如果需要的话,可以使用圆角。Stylus mixins 允许我们在设置参数时定义默认值。如果我们没有参数混合borderIt,它将根据其默认值生成一个 1 像素宽的直角实心黑色边框。
我们首先在#profiles表上使用borderIt,传入20px和2px。无需使用括号 - Stylus 会理解它是一个 mixin。我们 mixin 中的第一个参数(20px)被命名为rad。由于rad已经指定了borderIt,mixin 继续输出各种供应商前缀以及所需的半径。第二个参数覆盖了我们的border-width默认值。
当我们将borderIt应用于td元素时,我们需要括号,因为我们使用kwarg(关键字参数)来定义我们的选项。我们只需要设置颜色,所以我们不需要提供所有前面的参数,我们只需将所需的参数引用为属性。我们传递的颜色是#000 + 80%。这不是有效的 CSS,但 Stylus 理解。
还有更多...
让我们探索一些更多的 Stylus 功能,并找出如何使用替代的 CSS 引擎 LESS 作为 Express 中间件。
嵌套 mixin 和 rest 参数
让我们看看如何在其他 mixin 中重用 mixin,以及 Stylus 的 rest 参数语法(本质上是一个消耗任何后续参数的单个参数,将它们编译成一个数组)。
我们可以通过使角<td>元素的相关角更圆来进一步软化我们表格的边缘,使它们与外边框的圆角性质相匹配。
我们需要能够为单个角设置半径。供应商的实现在这方面有所不同。在基于 Mozilla 的浏览器中,角在半径之后定义,没有破折号,例如:
-moz-border-radius-topleft: 9px
而 WebKit 符合规范(除了前缀)的代码如下:
-webkit-border-top-left-radius
让我们创建另一个专门用于创建圆角 CSS 的 mixin,无论角是否相等。
rndCorner(rad, sides...)
if length(sides) is 2
-moz-border-radius-{sides[0]}{sides[1]} rad
-webkit-border-{sides[0]}-{sides[1]}-radius rad
border-{sides[0]}-{sides[1]}-radius rad
else
-webkit-border-radius rad
-moz-border-radius rad
border-radius rad
sides是一个 rest 参数。它吸收了所有剩余的参数。我们需要两个边来形成一个角,例如,左上角。因此,我们使用条件语句来检查剩余参数的长度是否为 2(而不是is,我们可以使用==)。
如果我们有我们的边,我们将它们整合到各种特定于浏览器的 CSS 中。请注意,当在属性中包含变量时,我们用大括号({})进行转义。如果未指定边,我们将边的半径设置为所有边,就像我们的配方一样。
现在我们可以从我们的borderIt mixin 中调用这个 mixin,如下所示:
borderIt(rad = 0, size = 1px, type = solid, col = #000)
border size type col
if rad { rndCorner(rad) }
我们不必用大括号包裹条件语句。这只是让我们可以将我们的if语句和 mixin 调用放在同一行上。这相当于以下代码:
borderIt(rad = 0, size = 1px, type = solid, col = #000)
border size type col
if rad
rndCorner(rad)
最后,我们将单个角应用于相关的td元素:
tdRad = 9px
#profiles
borderIt 20px 2px
@extend .cell
th
@extend .cell
td
@extend .cell
borderIt(col: #000 + 80%)
tr
&:nth-child(2)
td:first-child
rndCorner tdRad top left
td:last-child
rndCorner tdRad top right
&:last-child
td:first-child
rndCorner tdRad bottom left
td:last-child
rndCorner tdRad bottom right
我们的第一个borderIt现在通过推理调用rndCorner mixin,因为它设置了半径。第二个borderIt不会调用rndCorner,这很好,因为我们希望在特定元素上自己调用它。
我们使用特殊的&引用符来引用父tr元素。我们使用 CSS 的:nth-child(2)来选择表格的第二行。第一行由th元素组成。对于first-child和last-child也是一样,我们用它们来对我们的td元素应用适当的角。
虽然:nth-child和:last-child伪选择器在 Internet Explorer 8 及以下版本中不起作用,border-radius也不会起作用,因此这是我们可以在更现代的浏览器中使用它并且仍然具有跨浏览器兼容性的少数情况之一。
玩转颜色
Stylus 对颜色做了一些令人惊讶的事情。它有函数允许我们调整颜色的明暗度,(去)饱和度,色调调整,甚至混合颜色。
让我们给我们的表格上色:
#profiles
borderIt 20px, 2px
@extend .pad
background: #000;
color: #fff;
th
@extend .pad
td
@extend .pad
background blue + 35%
borderIt(col: @background)
color pink - green - brown + salmon - yellow + gray - salmon + pink
color desaturate(@color, 100)
&:hover
color @background + 180deg
background desaturate(@background, 40)
border-color @background
我们可以引用已为元素设置的任何属性的值,我们在这段代码中始终使用@background属性查找变量,但在许多情况下它保存了不同的值。
首先,我们反转我们的#profile表,将color设置为白色,background设置为黑色。接下来,我们对我们的td元素应用颜色,通过添加35%来获得浅蓝色。我们使用@background属性查找将我们的td边框与它们的background颜色匹配。
然后我们可以随意混合颜色,最终将我们的td文本颜色设置为与原始粉色相差不远的颜色。然后,我们通过+将@color传递给去饱和,同时使其变亮。接下来,我们通过向我们的@background颜色添加 180 度来设置悬停文本颜色,获得互补色。我们还对我们的background进行了去饱和,并匹配了border-color(现在@background与去饱和的背景匹配,而当我们设置悬停颜色时,它匹配了悬停前的背景颜色)。
现在我们的表格看起来如下截图所示:
使用 LESS 引擎
LESS 可能是一个更熟悉和冗长的 Stylus 替代方案。我们可以通过替换来使用 LESS 与 Express:
app.use(require('stylus').middleware({
src: __dirname + '/views',
dest: __dirname + '/public'
}));
使用:
app.use(express.compiler({
src: __dirname + '/views',
dest: __dirname + '/public',
enable: ['less']
}));
为了确保这个工作,我们还应该按照以下方式更改我们的package.json文件:
{
"name": "application-name"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "jade": ">= 0.0.1"
, "less": "1.3.x"
}
然后运行以下命令:
npm install
为了测试它,我们将用 LESS 重写我们的配方。
一些 Stylus 功能在 LESS 中没有等价物。我们将@extend用于继承我们的pad类,我们将其转换为 mixin。LESS 中也没有if条件,因此我们将两次声明.borderIt mixin,第二次使用when语句。
body { padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; }
a { color: #00B7FF; }
.pad() { padding: 0.5em; }
.borderIt (@rad:0, @size:1px, @type: solid, @col: #000) {
border: @size @type @col;
}
.borderIt (@rad:0) when (@rad > 0) {
-webkit-border-radius: @rad;
-moz-border-radius: @rad;
border-radius: @rad;
}
#profiles {
.borderIt(20px, 2px);
.pad();
th { .pad(); }
td { .pad();
.borderIt(0,1px,solid,lighten(#000, 80%));
}
}
我们将这保存到views/styles.less。Express 将其编译为public/styles.css,再次我们的#profiles表具有圆角。
另请参阅
-
在本章中讨论的 Express 模板
-
在本章中讨论的生成 Express 脚手架
初始化和使用会话
如果我们想在页面请求之间保持状态,我们使用会话。Express 提供了大部分管理会话复杂性的中间件。在这个配方中,我们将使用 Express 在浏览器和服务器之间建立一个会话,以便促进用户登录过程。
准备工作
让我们创建一个新的项目:
express login
这将创建一个名为login的新的 Express 骨架。
如何做...
在我们的app.js文件中,我们对app.configure进行以下更改:
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({secret: 'kooBkooCedoN'}));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
会话依赖于 cookies,因此我们需要cookieParser和session中间件。
提示
Express 2 到 Express 3
在 Express 3 中,我们通过cookieParser而不是session设置秘密字符串:
app.use(express.cookieParser('kooBkooCedoN'));
app.use(express.session());
我们将用以下路由处理完成app.js:
app.get('/', routes.index);
app.post('/', routes.login, routes.index);
app.del('/', routes.logout, routes.index);
GET请求将正常提供页面,POST请求将被解释为登录尝试。这些将首先传递到一个验证路由,检查有效的用户数据。DELETE请求将清除routes.logout的会话,然后传递到routes.index。
现在编辑routes/index.js文件:
var users = {'dave' : 'expressrocks'}; //fake user db:
exports.login = function (req, res, next) {
var user = req.body.user;
if (user) {
Object.keys(users).forEach(function (name) {
if (user.name === name && user.pwd === users[name]) {
req.session.user = {
name: user.name,
pwd: user.pwd
};
}
});
}
next();
};
exports.logout = function (req, res, next) {
delete req.session.user;
next();
}
exports.index = function (req, res) {
res.render('index', {title: 'Express', user: req.session.user});
};
最后,让我们在一个文件中组合一个登录表单。我们将称之为login.jade:
if user
form(method='post', action='/')
input(name="_method", type="hidden", value="DELETE")
p Hello #{user.name}!
a(href='javascript:', onClick='forms[0].submit()') [logout]
else
p Please log in
form(method='post', action='/')
fieldset
legend Login
p
label(for="user[name]") Username:
input(name="user[name]")
p
label(for="user[pwd]") Password:
input(type="password", name="user[pwd]")
input(type="submit")
提示
_method
注意我们的注销表单如何使用名为_method的隐藏输入。将此值设置为DELETE会覆盖表单设置的POST方法。这是由app.js文件中app.configure内的methodOverride中间件实现的。
我们将在index.jade中包含这个表单:
h1= title
p Welcome to #{title}
include login.jade
现在,如果我们运行我们的应用程序,并导航到http://localhost:3000,我们将看到一个登录表单。我们输入用户名dave,密码expressrocks,现在我们看到一个问候语,并有注销选项。
它是如何工作的...
为了使用会话,我们必须包含一些额外的中间件。我们在app.configure中进行这样做。express.parseCookie首先出现,因为express.session依赖于它。
express.session接受一个包含secret属性的强制对象(或者在 Express 3 中,通过向express.cookieParser传递字符串参数来设置密钥)。secret用于生成会话哈希,因此需要是唯一的,并且对外部人员是未知的。
当我们设置我们的路由时,我们假设对/路径的POST请求是登录尝试,因此首先将它们传递给login路由,对/的DELETE请求首先由logout路由处理。
我们的login路由检查已发布的登录详细信息(使用bodyParser中间件提供给我们的req.body)与我们的占位符users对象相匹配。在真实世界的情况下,login可能会对用户数据库进行验证。
如果一切正常,我们将一个user对象添加到会话中,并将name和密码(pwd)放入其中。
当将用户详细信息推送到会话时,我们可以采取捷径并说:
req.session.user = req.body.user;
然而,这样做可能会让攻击者填充req.session.user对象,填充任何他们想要的内容,可能会有大量内容。虽然任何输入会话的数据都是由受信任的用户(具有登录详细信息的用户)输入的,而且bodyParser对 POST 数据有内置的安全限制,但总是更倾向于保守而不是方便。
index路由保持不变,只是我们设置了一个user属性,我们将req.session.user传递给它。
这使login.jade能够检查user变量。如果设置了,login.jade会显示一个问候语,并包含一个小表单,其中包含一个链接,该链接发送带有DELETE覆盖的 POST 请求到服务器,从而通过app.del触发logout路由。
logout路由只是从req.session中删除user对象,将控制传递给index.route(使用next),然后通过res.render将不存在的req.session.user推送回 Jade。
当 Jade 发现没有user时,它会显示登录表单,当然也会输出到预登录请求。
还有更多...
我们可以改进与会话的交互方式。
用于全站点会话管理的自定义中间件
如果我们想要将我们的登录和注销请求传递到一个路由,那么这个配方就很好。然而,随着我们的路由和视图增加,管理会话的复杂性可能会变得繁重。我们可以通过为处理会话目的创建自定义中间件来在一定程度上减轻这种情况。
为了从不同的 URL 进行测试,我们将修改我们的路由如下:
app.get('/', routes.index);
app.post('/', routes.index);
app.del('/', routes.index);
app.get('/:page', routes.index);
我们不再使用路由来控制我们的会话逻辑,因此我们已经删除了中间路由,直接发送到routes.index。:page可能指向另一个路由,但出于简洁起见,我们将其保留为routes.index。
在routes/index.js中,我们现在可以简单地使用以下代码:
exports.index = function (req, res) {
res.render('index', {title: 'Express'});
};
现在让我们创建一个文件,命名为login.js,编写以下代码:
var users = {'dave' : 'expressrocks'};
module.exports = function (req, res, next) {
var method = req.method.toLowerCase(), //cache the method
user = req.body.user,
logout = (method === 'delete'),
login = (method === 'post' && user),
routes = req.app.routes.routes[method];
if (!routes) { next(); return; }
if (login || logout) {
routes.forEach(function (route) {
if (!(req.url.match(route.regexp))) {
console.log(req.url);
req.method = 'GET';
}
});
}
if (logout) {
delete req.session.user;
}
if (login) {
Object.keys(users).forEach(function (name) {
if (user.name === name && user.pwd === users[name]) {
req.session.user = {
name: user.name,
pwd: user.pwd
};
}
});
}
if (!req.session.user) { req.url = '/'; }
next();
};
由于我们不再使用路由,我们没有机会通过res.render传递req.session.user。但是,我们可以使用动态助手。动态助手可以访问req和res对象,在视图呈现之前调用。我们传递给动态助手对象的任何属性都会作为本地变量推送到 Jade 视图中。在app.js中,我们在我们的路由上方放置:
app.dynamicHelpers({
user: function (req, res) {
return req.session.user;
}
});
提示
Express 2 到 Express 3
在 Express 3 中,使用app.locals.use设置动态助手:
app.locals.use( function (req, res) {
res.locals.user = req.session.user;
});
而是通过发送包含所需本地变量的对象,通过将它们添加到res.locals对象中来显式设置本地变量。
现在我们只需在app.js的app.configure回调中将login.js包含为中间件:
app.configure(function()
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({secret: 'kooBkooCedoN'}));
app.use(require('./login'));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
最后,我们将修改login.jade,使其不再是:
form(method='post', action='/')
我们有:
form(method='post')
这使表单 POST 到从中提交的任何地址。
现在所有的肌肉工作都是由login.js执行的。导出函数的下半部分执行与我们的配方相同的操作。由于我们没有使用 Express 路由器,我们必须手动检查方法。
在上半部分,我们访问req.app。在app.js中,app变量是express.createServer的结果。Express 允许我们在中间件和路由中访问我们的服务器,使用对服务器实例的引用req.app。
在我们与req.app的交互中,我们使用req.app.routes.routes,将其存储在routes变量中。这个属性保存了我们使用app.get, app.post等定义的任何路由。路由按请求方法类型存储,例如,req.app.routes.routes.post保存了所有app.post路由的数组。如果路由方法没有被定义,我们简单地调用next和return。这样 Express 可以处理未定义方法的问题。数组中的每个项目都是一个对象,包含path, method, callbacks, keys和regexp属性。我们循环遍历request方法的所有路由,并使用regexp属性来确定请求的 URL 是否有匹配项。如果没有,我们将方法重置为GET。我们这样做是为了透明地确保POST和DELETE请求可以通过任何 URL 进行服务,并且如果没有为它们定义post或del路由,不会返回404 错误。
如果缺少这段代码,登录或注销机制仍会发生,但用户将收到未找到消息。例如,如果我们导航到http://localhost:3000/anypage,并尝试登录,我们的中间件将首先捕获请求。它将确定是否满足登录条件(在请求体中有user的 POST 请求),并相应地处理它。如果没有为/anypage定义 POST 路由,我们将方法重置为 GET。稍后中间件调用next,将控制权传递给app.router,后者永远不会看到 POST 方法,因此重新加载/:page GET 路由。
回到app.js,我们有 Express 的dynamicHelpers方法。dynamicHelpers方法注册了助手,但直到视图渲染之前才调用它(这意味着动态助手在所有路由回调之后执行)。这很方便,因为它允许我们的路由在需要时进一步与req.session.user交互。我们将一个包含user属性的对象传递给dynamicHelpers。user属性的最终值直接加载到我们的视图中作为变量。以同样的方式,我们可以通过路由中的res.render选项对象将变量传递给视图。user属性包含一个回调,由 Express 进行评估。它的工作方式类似于路由或中间件回调,只是期望有一个return值。我们return req.session.user,因此与主要配方一样,req.session.user现在在login.jade中作为user可用。如果没有会话,我们确保将 URL 重置为/,以便其他路由不能用来绕过我们的授权过程。
最后,我们调用next,将控制权传递给下一个中间件,在我们的例子中是app.router。
闪现消息
Express 提供了一个基于会话的闪现消息的简单接口。闪现消息保存在会话对象中,仅用于一个请求,然后消失。这是一种生成请求相关信息或错误消息的简单方法。
提示
Express 2 到 Express 3
Express 3 不支持开箱即用的会话闪现消息。但是,connect-flash 作为中间件提供了这个功能。
在我们的package.json文件的依赖项部分,我们将添加:
, "connect-flash": "0.1.0"
然后执行:
npm install
最后,我们需要引入connect-flash,并在app.configure回调中调用它,放在 cookie 和会话中间件之后:
var express = require('express')
, routes = require('./routes')
, flash = require('connect-flash')
var app = module.exports = express.createServer();
app.configure(function(){
//prior middleware
app.use(express.cookieParser('kooBkooCedoN'));
app.use(express.session());
app.use(flash());
//rest of app.configure callback
让我们修改上一个扩展中的login.js文件到我们的配方(网站范围会话管理的自定义中间件)。我们将修改它以便在登录详情无效时闪现错误消息。首先,我们需要修改导出函数底部的代码,位于if (login)条件内:
if (login) {
var valid = Object.keys(users).some(function (name) {
return (user.name === name && user.pwd === users[name]);
});
if (valid) {
req.session.user = {
name: req.body.user.name,
pwd: req.body.user.pwd
};
} else {
req.flash('error','Login details invalid!');
}
}
这样做是有效的,但我们可以做得更好。让我们通过将验证代码提取到一个单独的函数中来整理它:
function validate(user, cb) {
var valid = Object.keys(users).some(function (name) {
return (user.name === name && user.pwd === users[name]);
});
cb((!valid && {msg: 'Login details invalid!'} ));
}
尽管validate中发生的一切都是同步的,但我们以异步的方式编写它(即通过回调传递值而不是返回值)。这是因为实际上,我们不会使用对象来存储用户详细信息。我们将使用远程数据库,必须异步访问。在下一个示例中,我们将把用户详细信息存储在 MongoDB 数据库中,并异步读取以验证登录请求。validate函数的结构考虑到了这一点。
现在我们用以下代码替换我们的中间件用户验证代码:
if (login) {
validate(user, function (err) {
if (err) { req.flash('error', err.msg); return; }
req.session.user = {
name: user.name,
pwd: user.pwd
};
});
}
if (!req.session.user) { req.url = '/'; }
next();
}; //closing bracket of module.exports
我们的validate函数使用相同的布尔方法。但是,它被隐藏了。还要注意到对next的各种策略性调用——无论是在从错误中提前退出时,添加用户会话,还是在最后。在回调上下文中放置这些next调用,为异步操作未来证明了我们的验证函数,这对于数据库交互非常重要。
我们使用validate函数中的callback(err)样式来让我们的中间件知道登录是否成功。err只是一个包含错误消息(msg)的对象,只有在valid不为真时才传递。
如果存在err,我们调用req.flash,这是内置的 Express 方法,将一个名为flash的对象推送到req.session中。一旦请求被满足,该对象将被清空所有属性。
我们需要使这个对象在login.jade中可用,所以我们将在app.js中添加另一个动态帮助程序。
app.dynamicHelpers({
user: function (req, res) {
return req.session.user;
},
flash: function(req, res) {
return req.flash();
}
});
提示
从 Express 2 到 Express 3
要在 Express 3 中添加 flash 消息,我们只需将其添加到我们现有的app.locals.use回调中的res.locals对象中:
app.locals.use( function (req, res) {
res.locals.user = req.session.user;
res.locals.flash= req.flash();});
我们可以使用req.session.flash,但req.flash()也可以做同样的事情。
最后,在login.jade的顶部,我们写入:
if flash.error
hr
b= flash.error
hr
如果登录详细信息不正确,用户将在水平线之间收到粗体错误通知。
参见
-
在本章中讨论创建一个 Express web 应用程序
-
在本章中讨论 Express 中的模板
-
动态路由在本章中讨论
创建一个 Express web 应用程序
在这个示例中,我们将把许多以前的示例组合在一起,还加入了一些额外的 Express 功能(如应用程序挂载),以创建一个具有集成管理功能的基于 Express 的 Web 应用程序的基础。
准备工作
让我们从命令行开始:
express profiler
Profiler 是我们的新应用的名称,它将是 Node 社区成员的个人资料管理器。
我们需要编辑package.json,写入:
{
"name": "Profiler"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "jade": ">= 0.0.1"
, "stylus": "0.27.x"
, "mongoskin": "0.3.6"
}
}
我们将名称设置为Profiler,添加stylus和mongoskin,为mongoskin设置更严格的版本要求。Jade 和 Stylus 是为 Express 设计的,因此它们可能会与新版本保持兼容(尽管我们将 Stylus 限制为次要版本更新)。Mongoskin 有自己的开发流程。为了确保我们的项目不会因为可能的 API 更改而被未来版本破坏,我们将版本锁定为 0.3.6(尽管这并不意味着我们不能在以后升级)。
所以我们用以下代码获取我们的依赖项:
npm install
我们还需要确保 MongoDB 已安装并在我们的系统上运行,请参阅第四章的使用 MongoDB 存储和检索数据,了解详情。
简而言之,我们用以下命令启动 Mongo:
sudo mongod --dbpath [a folder for the database]
我们还将向 MongoDB 中推送一些数据以开始。让我们在profiler目录中创建一个新文件夹,并将其命名为tools。然后将我们的profiles.js模块从第一章中,创建 Web 服务器,移到其中,创建一个名为prepopulate.js的新文件。在其中,我们写入以下代码:
var mongo = require('mongoskin'),
db = mongo.db('localhost:27017/profiler'),
profiles = require('./profiles'),
users = [{name : 'dave', pwd: 'expressrocks'},
{name : 'MrPage', pwd: 'hellomynamesmrpage'}
];
//make sure collection is empty before populating
db.collection('users').remove({});
db.collection('profiles').remove({});
db.collection('users').insert(users);
Object.keys(profiles).forEach(function (key) {
db.collection('profiles').insert(profiles[key]);
});
db.close();
执行后,这将给我们一个名为profiler的数据库,其中包含profiles和users集合。
最后,我们将使用上一个示例中的整个登录应用程序。但是,我们希望它具有站点范围的会话管理和闪存消息(在代码示例中,此文件夹称为login_flash_messages)。因此,让我们将login文件夹复制到我们的新配置文件目录中,命名为profiler/login。
如何做...
创建数据库桥接
让我们从一些后端编码开始。我们将创建一个名为models的新文件夹,并在其中创建一个名为profiles.js的文件。这将用于管理我们与 MongoDB 配置文件集合的所有交互。在models/profiles.js中,我们放置:
var mongo = require('mongoskin'),
db = mongo.db('localhost:27017/profiler'),
profs = db.collection('profiles');
exports.pull = function pull(page, cb) {
var p = {},
//rowsPer = 10, //realistic rowsPer
rowsPer = 2,
skip, errs;
page = page || 1;
skip = (page - 1) * rowsPer;
profs.findEach({}, {limit: rowsPer, skip: skip}, function (err, doc) {
if (err) { errs = errs || []; errs.push(err); }
if (doc) {
p[doc._id] = doc;
delete p[doc._id]._id;
return;
}
cb(errs, p);
});
}
exports.del = function (profile, cb) {
profs.removeById(profile, cb);
}
exports.add = function (profile, cb) {
profs.insert(profile.profile, cb);
}
我们定义了三种方法:pull, del和add。每个方法都是异步操作数据库,并在数据返回或操作完成时执行用户回调。我们设置了一个较低的每页行数限制(rowsPer),以便我们可以使用我们拥有的少量记录来测试我们的分页工作(将内容分成页面)。
我们还必须修改login/login.js,这是我们在上一个示例中创建的,以将我们的登录应用程序连接到 MongoDB 用户集合。主模块可以保持不变。我们只需要改变验证用户的方式,module.exports之上的所有内容都会改变为:
var mongo = require('mongoskin'),
db = mongo.db('localhost:27017/profiler'),
users = db.collection('users');
function validate(user, cb) {
users.findOne({name: user.name, pwd: user.pwd}, function (err, user) {
if (err) { throw err; }
if (!user) {
cb({msg: 'Invalid login details!'});
return;
}
cb();
});
}
配置 app.js 文件
现在让我们修改app.js。
app.configure应该如下所示:
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(require('stylus').middleware({
src: __dirname + '/views',
dest: __dirname + '/public'
}));
app.use(express.favicon());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
app.use('/admin', require('./login/app'));
});
我们已经启动了 Stylus 引擎,并添加了一个网站图标服务器以确保一切正常。最后一行app.use实际上将我们的登录应用程序挂载到/admin路由(我们在准备就绪中将login复制到我们的配置文件目录中)。
接下来,让我们将我们唯一的路由添加到我们的主app.j中,如下所示:
app.get('/:pagenum([0-9]+)?', routes.index);
我们指定了一个可选的占位符叫做:pagenum,它必须由一个或多个数字组成。因此,/, /1, /12和/125452都是有效的路由,但/alphaCharsPage不是。
现在让我们在login应用程序的app.js中的login/app.js中设置一些额外的配置细节,如下所示:
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({secret: 'kooBkooCedoN'}));
app.use(require('stylus').middleware({
src: __dirname + '/views',
dest: __dirname + '/public'
}));
app.mounted(function (parent) {
this.helpers({masterviews: parent._locals.settings.views + '/'});
});
app.use(require('./login'));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
提示
Express 2 到 Express 3
请记住,在 Express 3 中,秘密放在express.cookieParser中作为字符串,而不是传递给express.session的对象内部。
login应用程序将从profiler应用程序中拉取我们的配置文件表。我们已经配置它使用 Stylus,因为我们将应用额外的特定于管理员的 Stylus 生成的 CSS。我们还添加了一个名为masterviews的辅助变量。稍后将用它来定位我们应用程序的主视图目录的绝对路径。login应用程序需要知道这一点,以便从其父profiler应用程序加载视图。
接下来,我们将修改login/app.js文件中的路由:
app.get('/:pagenum([0-9]+)?', routes.index);
app.post('/:pagenum([0-9]+)?', routes.index);
app.del('/:pagenum([0-9]+)?', routes.index);
app.get('/del', routes.delprof);
app.post('/add', routes.addprof, routes.index);
将可选的:pagenum添加到get方法路由中,使得可以像在主应用程序中一样导航配置文件表。将:pagenum添加到post方法路由中允许用户从他们之前导航到的页面登录(例如,如果用户的会话已过期,这允许从http://localhost:/admin/2提供登录表单)。同样,del方法路由将允许我们从任何有效页面注销。
我们还为处理管理员任务添加了/del和/add路由。
当一个 Express 应用程序被挂载到另一个 Express 应用程序中时,在子应用程序上调用listen方法会导致端口冲突。子应用程序不必监听,它们的父应用程序会为它们监听。
因此,我们修改login/app.js中的listen调用为:
if (!module.parent) {
app.listen(3000, function(){
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});
}
module是一个内置的 Node 全局变量。parent 属性告诉我们我们的应用程序是否被另一个应用程序加载。由于我们的登录应用程序是由配置文件应用程序加载的,app.listen不会触发。
登录应用程序是我们管理部分的门卫,我们将能够添加和删除配置文件。
现在我们的主应用程序和挂载的应用程序都已经准备就绪,我们可以继续编辑我们的视图、样式和路由。
修改配置文件
让我们从profiler应用程序的index.jade视图开始。
h1= title
p Welcome to #{title}
p: a(href='admin/') [ Admin Login ]
include profiles
由于我们包含了profiles.jade,让我们将其写成如下形式:
masterviews = typeof(masterviews) !== 'undefined' ? masterviews : ''
table#profiles
tfoot
page = (page) || 1
s = (page > 1) ? null : 'display:none'
td
a#bck(href="{(+page-1)}", style=s) «
a#fwd(href="{(+page+1)}") »
thead
tr
th Name
th Irc
th Twitter
th Github
th Location
th Description
if typeof user !== 'undefined'
th Action
tbody
each profile, id in profiles
tr(id=id)
!= partial(masterviews + 'row', {collection: profile})
mixin del(id)
mixin add
mixin adminScript
提示
Express 2 到 Express 3
我们在profiles.jade中使用了一个partial(参见Express 中的模板中讨论的Jade Partials)。Express 3 不再支持 partials,因此我们需要手动遍历行:
tbody
each profile, id in profiles
tr(id=id)
each row in profile
td= row
mixin del(id)
profiles.jade应该保存到profiler/views目录中,它是基于我们之前的 recipes 中的profiles表。但是,我们添加了一些代码来支持与登录应用程序的无缝集成,并为分页添加了一些额外的 HTML 结构。
在profiles.jade的顶部,我们包含了一个安全网,以确保我们的视图在masterviews未定义时不会中断。对于分页,我们添加了一个tfoot元素来保存前进和后退链接,以及一个补充的thead来包装th元素。
我们使用一个 partial 来加载每一行,这将从row.jade中加载如下:
td= row
我们还包括了一些 Jade mixin 调用。当我们来编辑login应用程序视图时,我们将定义这些 mixin。
让我们在views下创建一个新的stylesheets目录,并在其中放置一个名为style.styl的文件。
在views/stylesheets/style.styl中,我们编写以下代码:
body
padding 50px
font 14px "Lucida Grande", Helvetica, Arial, sans-serif;
a
color #00B7FF
rndCorner(rad, sides...)
if length(sides) is 2
-moz-border-radius-{sides[0]}{sides[1]} rad
-webkit-border-{sides[0]}-{sides[1]}-radius rad
border-{sides[0]}-{sides[1]}-radius rad
else
-webkit-border-radius rad
-moz-border-radius rad
border-radius rad
borderIt(rad = 0, size = 1px, type = solid, col = #000)
border size type col
if rad {rndCorner(rad)}
.pad
padding 0.5em
tdRad = 9px
#profiles
width 950px
borderIt 20px, 2px
@extend .pad
background: #000;
color: #fff;
th
@extend .pad
tbody
td
@extend .pad
background blue + 35%
borderIt(col: @background)
color pink - green - brown + salmon - yellow + gray - salmon + pink
color desaturate(@color, 100)
&:hover
color @background + 180deg
background desaturate(@background, 40)
border-color @background
tr
&:first-child
td:first-child
rndCorner tdRad top left
td:last-child
rndCorner tdRad top right
&:last-child
td:first-child
rndCorner tdRad bottom left
td:last-child
rndCorner tdRad bottom right
tfoot
font-size 1.5em
td
a
text-decoration none
color #fff - 10%
&:hover
color #fff
这是来自Express 中的 CSS 引擎中Playing with color部分下的*There's more..*的相同 Stylus 表,但进行了一些修改。
由于我们将th元素放在thead下,我们可以简单地通过:first-child选择我们的tbody tr元素,而不是:nth-child(2)。我们还为新的tfoot元素添加了一些样式。
最后,我们将编写routes/index.js文件。
var profiles = require('../models/profiles');
function patchMixins(req, mixins) {
if (!req.session || !req.session.user) {
var noop = function(){},
dummies = {};
mixins.forEach(function (mixin) {
dummies[mixin + '_mixin'] = noop;
});
req.app.helpers(dummies);
}
}
exports.index = function (req, res) {
profiles.pull(req.params.pagenum, function (err, profiles) {
if (err) { console.log(err); }
//output no-ops to avoid choking on non-existant admin mixins
patchMixins(req, ['add','del','adminScript']);
res.render('index', { title: 'Profiler', profiles: profiles,
page: req.params.pagenum });
});
};
我们的index路由通过我们的models/profiles.js模块向 MongoDB 发出调用,传递所需的页码,并检索一些要显示的 profiles。
它还调用我们的patchMixins函数,在我们的路由之前包含了一个在profiles.jade中找到的 mixin 名称数组。这些 mixin 目前还不存在。此外,只有当我们登录到http://localhost:8080/admin时,这些 mixin 才可用。这是有意的,这些 mixin 将提供管理控件,这些控件位于我们的profiles表的顶部,我们只希望它们在用户登录时出现。
但是,如果我们不在 admin mixin 的位置包含虚拟 mixin,Node 将抛出错误。
在内部,Jade mixins 在执行视图模板之前被编译为 JavaScript 函数。因此,我们创建了虚拟的no-op(无操作)函数来防止服务器错误。然后当我们登录时,它们将被替换为管理 mixin。
如果我们导航到localhost:3000,我们现在应该有一个正常运行的profiler应用程序。
修改已挂载的登录应用程序
在login/views中,我们目前有index.jade, login.jade和layout.jade。在login.jade中,我们想要添加两个includes,如下所示:
if flash.error
hr
b= flash.error
hr
if user
form(method='post')
input(name="_method", type="hidden", value="DELETE")
p Hello #{user.name}!
a(href='javascript:', onClick='forms[0].submit()') [logout]
include admin
include ../../views/profiles
else
p Please log in
// rest of the login.jade...
我们不再重复代码,而是使用相对路径从主应用程序重用我们的profiles.jade视图。这意味着我们对前端站点所做的任何更改也会应用到我们的管理部分!admin.jade将包含 Jade mixins(在概念上类似于 Stylus mixins)。这些 mixin 有条件地包含在profiles.jade中(请参见前面的修改 profiler 应用程序部分)。
因此在admin.jade中:
mixin del(id)
td
a.del(href='/admin/del?id=#{id}&p=#{page}')
⨂
mixin add
#ctrl
a#add(href='#') ⊕
mixin adminScript
include adminScript
include addfrm
在admin.jade中有两个包含,一个作为 mixin 的一部分,另一个作为直接包含。
addfrm.jade应该如下所示:
fields = ['Name', 'Irc', 'Twitter', 'Github', 'Location', 'Description'];
form#addfrm(method='post', action='/admin/add')
fieldset
legend Add
each field, i in fields
div
label= field
input(name="profile[#{field.toLowerCase()}]")
.btns
button.cancel(type='button') Cancel
input(type='submit', value='Add')
adminScript.jade应包含以下代码:
script(src='http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js')
script
document.getElementsByTagName('body')[0].id = 'js';
$('#add').click(function (e) {
e.preventDefault();
$('#profiles, #ctrl').fadeOut(function () {
$('#addfrm').fadeIn();
});
$('#addfrm .cancel').click(function () {
$('#addfrm').fadeOut(function () {
$('#profiles, #ctrl').fadeIn();
});
});
});
在login.jade中,admin位于profiles上方,因此#addfrm将位于#profiles表的上方。但是,我们的adminScript mixin 隐藏了表格,在单击添加按钮时显示它。
我们在login/views下创建一个stylesheets文件夹,在其中创建admin.styl并编写以下代码:
@import '../../../views/stylesheets/style.styl'
tbody
td
.del
text-decoration none
color blue + 35% + 180deg
float right
&:hover
color red
#ctrl
width 950px
text-align center
margin-top -2.5em
a
color white - 10%
font-size 1.8em
text-decoration none
&:hover
color @color + 111%
#js
#addfrm
display none
#addfrm
width 250px
label
display block
float left
width 100px
font-weight bold
.btns
width @width
text-align right
现在我们还在主应用程序中重用 Stylus 表。@import声明由服务器端的 Stylus 处理(除非扩展名为.css)。因此,我们的主应用程序的styles.styl表与admin.styl合并,并编译为login/public/stylesheets/admin.css中的一个 CSS 文件。
要加载我们的admin.css文件,我们必须修改登录应用程序的layout.jade视图,如下面的代码所示:
!!!
html
head
title= title
link(rel='stylesheet', href='/admin/stylesheets/admin.css')
body!= body
我们已将link href属性从/stylesheet/style.css更改为/admin/stylesheets/admin.css,确保 CSS 从我们子应用程序的静态服务器上加载。
最后,我们完成了我们的管理员路由,在login/routes/index.js中如下所示:
var profiles = require('../../models/profiles');
exports.index = function (req, res) {
profiles.pull(req.params.pagenum, function (err, profiles) {
if (err) { console.log(err); }
res.render('index', { title: 'Profiler Admin', profiles: profiles, page: req.params.pagenum });
});
};
exports.delprof = function (req, res) {
profiles.del(req.query.id, function (err) {
if (err) { console.log(err); }
profiles.pull(req.query.p, function (err, profiles) {
req.app.helpers({profiles: profiles});
res.redirect(req.header('Referrer') || '/');
});
});
}
exports.addprof = function (req, res) {
profiles.add(req.body, function (err) {
if (err) { console.log(err); }
res.redirect(req.header('Referrer') || '/');
});
}
现在我们应该能够登录到http://localhost:3000/admin,以Dave的身份删除和添加配置文件,密码为expressrocks,或者以Mr.Page的身份,密码为hellomynamesmrpage。
提示
登录安全
在下一章中,我们将学习如何对我们的密码进行哈希处理并通过 SSL 登录。
它是如何工作的...
我们的应用程序包含许多组件共同工作。所以让我们从不同的角度来看一下。
应用程序挂载
在这个示例中,我们有两个应用程序与同一个数据库一起工作,共享视图和 Stylus 样式表。我们将登录应用程序导入到我们的新的profiler文件夹中,并使用app.use设置/admin作为其路由。
这是因为 Express 应用程序是中间件的组合,所以当我们挂载登录应用程序时,它只是作为中间件插件与我们的应用程序集成。中间件在请求和响应对象上工作。通过将/admin路由传递给app.use,我们限制了登录应用程序仅在该路由下的请求中工作。
数据流
我们的应用程序由 MongoDB 数据库支持,我们使用prepopulate.js工具进行设置。数据如下所示流向和从数据库流向:
models/profiles.js从配置文件集合中提取和推送数据,为主应用程序和子应用程序中的routes/index.js文件提供接口。我们的路由集成在各自的app.js文件中,并与models/profiles.js交互,执行所需的任务。
login.js只是验证用户的凭据,使用用户提供的输入进行搜索。login.js作为一个中间件坐落在login/app.js中,等待响应包含用户名和密码的 POST 请求。
路由处理
在两个应用程序中,index路由提供了显示和导航配置文件表的基础。在两者中,我们调用profiles.pull,传入req.params.pagenum。pagenum参数加载到req.params上。它永远不会是除了数字之外的任何东西 - 这要归功于我们对它的限制,尽管它是可选的,因此可能不存在。
我们的profiles.pull接受两个参数:页码和回调。如果页码不存在,则将page设置为1。我们通过将我们的内部rowsPer变量乘以page-1(我们想要从第一页开始,因此对于第一页,我们跳过 0 行)来确定要提取的行。结果作为skip修饰符传递给 MongoDB,同时rowsPer作为limit属性传递。skip将在输出之前跳过预定数量的行,limit限制输出的数量;因此我们实现了分页。
profiles.pull回调要么出现错误,要么包含配置文件的对象。在我们的index路由中,我们执行最小的错误处理。Express 倾向于捕获错误并将其输出到浏览器以进行调试。profiles被传递给res.render,稍后在profiles.jade视图中使用。
在login/app.js中,定义了两个不可变的路由:/add和/del。/del路由是一个基本的 GET 请求,指向routes.delprof,它期望两个查询参数:id和p. id被传递给profiles.del,它调用 Mongoskin 的removeByID方法,有效地从集合中删除了一个配置文件。我们直接将cb参数传递给removeById回调,使profiles.del回调直接成为removeById的结果。
回到login/routes/index.js,只要没有发生错误,我们调用profiles.pull,使用p查询参数更新profiles对象到视图中使用app.helpers。这确保了对数据库的更改会反映给用户。最后,我们将用户重定向回他们来的地方。
/add路由的工作方式基本相同,只是作为 POST 请求。req.body作为一个对象返回,我们可以直接将这个对象插入到 MongoDB 中(因为它类似于 JSON)。
Views
我们在视图中使用了很多includes,有时在应用程序之间的关系看起来如下图所示:
在我们的主应用程序中,索引视图加载 profiles 视图,并且 profiles 在partial语句中使用 rows 视图。
在登录应用程序中,索引视图包括登录视图。登录视图加载 profiles 视图,并在适当的条件下,还包括 admin 视图(在 profiles 之前)以提供管理层。Admin 包括addfrm和adminScript视图。在 admin 中定义的 mixins 对 profiles 可用。
profiles.jade视图对整个 Web 应用程序非常重要,它输出我们的数据,提供可选的管理覆盖层,并提供导航功能。让我们来看看导航部分:
table#profiles
tfoot
page = (page) || 1
s = (page > 1) : null ? 'display:none'
td
a#bck(href="#{settings.basepath}/#{(+page-1)}", style=s) «
a#fwd(href="#{settings.basepath}/#{(+page+1)}") »
page变量是从index路由传递的,并且是从req.params.pagenum确定的。如果 page 是0(或 false),我们将其设置为1,这在用户心中是第一页。然后我们创建一个名为s的变量。在 Jade 中,我们不必使用var,Jade 会处理这些复杂性。如果我们在第一页,那么链接到前一页是不必要的,因此添加一个包含display:none的 style 属性(如果我们想更整洁,我们可以设置一个 CSS 类设置display并添加一个 class 属性)。通过传递null,如果page大于 1,我们告诉 Jade 我们根本不想设置 style 属性。
Mixins
我们只在login/views/admin.jade视图中使用 Jade mixins,但它们对于管理部分和顶层站点之间的协同作用至关重要。除非用户已登录并在/admin路由下,否则 mixins 不会出现在profiles.jade中。它们只适用于特权用户。
我们使用 mixins 来补充profiles表与管理层。admin.jade中唯一不是 mixin 的部分是最终的include addfrm.jade。由于 admin 在 profiles 之前被包含,#addfrm位于profiles表上方。
adminScript mixin 就像其名称所示,是一个script块,快速地将id设置为js添加到body标签上。我们在admin.styl中使用它来隐藏我们的#addfrm(生成的 CSS 将是#js #addfrm {display:none})。这比直接使用 JavaScript 隐藏元素更快,并且最小化了在页面加载时隐藏页面元素可能出现的不良内容闪烁效果。因此,#addfrm最初是不可见的。在下面的截图中,我们可以看到在管理部分的#profiles表中显示的可见 mixins:
点击Add按钮会导致#profiles表淡出,#addfrm淡入。del mixin 接受一个id参数,然后使用它为每个 profile 生成一个链接,例如/del?id=4f3336f369cca0310e000003&p=1。p变量是从res.render时间中传递给index路由的page属性确定的。
Helpers
我们在登录应用程序中同时使用静态和动态 helpers。动态 helpers 位于路由中间件的最后一部分和视图渲染之间。因此,它们对发送出去的内容有最后的控制权。我们应用程序中的动态 helpers 保持不变。
静态助手在应用程序启动时设置,并且可以随时被覆盖。这些助手存储在app._locals中,以及其他 Express 预设(例如我们在profiles视图中用于base变量的settings对象)。我们在我们的登录应用程序中使用app.mounted来访问父应用程序对象,以从parent._locals.settings.views中发现父应用程序的视图目录。然后我们将其作为masterviews助手传递给我们登录应用程序的视图。
在profiles.jade中,我们这样做:
masterviews = typeof(masterviews) !== 'undefined' ? masterviews : ''
如果在我们的子应用程序中,我们包含来自父应用程序的视图,并且该视图包含另一个父视图,或者加载一个包含父视图的部分,我们可以使用masterviews来确保部分是从父目录加载的。masterviews使profiles.jade能够在两个领域中运行。
样式
我们的 Stylus 文件也具有一定程度的互连性,如下图所示:
用户流程
所有这些都共同作用,为网站提供了一个管理部分。
用户可以浏览配置文件表,使用返回和前进链接,并且可以链接到表上的特定页面。
特权用户可以导航到/admin,输入他们的登录详细信息,并继续添加和删除记录。/add和/delete路由受中间件保护。除非用户已登录,否则他们只能访问登录应用程序的index路由,要求登录详细信息。
还有更多...
让我们看看如何监视和分析我们的 Web 应用程序。
基准测试
对 Node 网站进行基准测试可能非常令人满意,但总会有改进的空间。
Apache Bench(ab)与 Apache 服务器捆绑在一起,虽然 Apache 与 NodeJS 无关,但他们的 HTTP 基准测试工具是测试我们应用程序响应大量同时请求能力的绝佳工具。
我们可以使用它来测试对我们应用程序的任何更改的性能优势或劣势。让我们快速地向站点和管理部分分别抛出 1000 个请求,每次 50 个请求,如下所示:
ab -n 1000 -c 50 http://localhost:3000/
ab -n 1000 -c 50 http://localhost:3000/admin
里程将根据系统功能而异。然而,由于测试是在同一台机器上运行的,因此可以从测试之间的差异中得出结论。
在我们对两个部分的测试中,/每秒传送 120 个请求,而/admin每秒仅传送近 160 个请求。这是有道理的,因为/admin页面只会提供登录表单,而/路由会从 MongoDB 获取数据,对profiles对象执行迭代逻辑,并使用部分来显示行。
使用记录器
Express 带有 Connect 的记录器中间件,它可以输出我们应用程序中有用的自定义信息。在生产场景中,这些信息可能是网站维护的重要部分。
要使用记录器,我们在所有其他中间件之前包含它,如下所示:
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.logger());
app.use(express.bodyParser());
app.use(express.methodOverride());
//rest of configure
默认情况下,记录器会输出如下所示的内容:
127.0.0.1 - - [Thu, 09 Feb 2012 15:56:40 GMT] "GET / HTTP/1.1" 200 908 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.3 Safari/535.19"
127.0.0.1 - - [Thu, 09 Feb 2012 15:56:40 GMT] "GET /stylesheets/style.css HTTP/1.1" 200 1395 "http://localhost:3000/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.3 Safari/535.19"
它包含有关请求的有用信息,客户端的 IP 地址,用户代理字符串以及内容长度(在示例输出中分别为 908 和 1395)。我们可以使用这些信息来确定一系列事实(例如浏览器统计数据,地理位置统计数据等)。
在开发过程中,我们可能希望设置logger如下所示:
app.use(express.logger('dev'));
这将以颜色格式输出请求信息到控制台,包括请求方法(例如 GET),请求的路由,状态码(例如 200),以及请求完成的时间。
我们还可以传递令牌。例如:
app.use(express.logger(':date -- URL::url Referrer::referrer '));
我们甚至可以像下面的代码中所示定义自己的令牌:
express.logger.token('external-referrer',function (req, res) {
var ref = req.header('Referrer'),
host = req.headers.host;
if (ref && ref.replace(/http[s]?:\/\//, '').split('/')[0] !== host) {
return ref;
} else {
return '#local#';
}
});
app.use(express.logger(':date -- URL::url Referrer::external-referrer'));
如果引荐者与我们的主机相同,我们用#local#掩盖它。稍后,我们可以过滤掉所有包含#local#的行。
默认情况下,logger输出到控制台。但是,我们可以将其传递给一个流以进行输出。记录器可以让我们通过 TCP、HTTP 或简单地输出到文件来流式传输日志。让我们将我们的日志写入文件:
app.use(express.logger( //writing external sites to log file
{format: ':date -- URL::url Referrer::external-referrer',
stream: require('fs').createWriteStream('external-referrers.log')
}));
这次我们给logger一个对象而不是一个字符串。为了通过对象设置格式,我们设置format属性。为了重定向输出流,我们创建一个writeStream到我们想要的日志文件。
提示
有关更多日志记录器选项,请参阅www.senchalabs.org/connect/middleware-logger.html.
另请参阅
-
本章讨论的动态路由
-
本章讨论的 Express 中的模板
-
本章讨论的 Express 中的 CSS 引擎
-
本章讨论的初始化和使用会话