Node 应用服务器与 CouchDB





5.00/5 (1投票)
Node 应用服务器与 CouchDB
最近关于“NoSQL”非关系型数据库的讨论很多。几周前,Lou 写了一篇介绍 CouchDB 的文章,CouchDB 是一个免费的 Apache 基金会文档数据存储。Lou 的应用程序完全由 CouchDB 托管。我最近用一种更传统的方法编写了一个 Web 应用程序,服务器使用 CouchDB 作为其后端。这在某种意义上是传统的,但在另一方面又是非传统的:首选的后端语言是 JavaScript。
没错,今天的文章将向您介绍 nodeJS,即服务器端 JavaScript 引擎。node 允许您在 JavaScript 中完成与 Java、C#、Ruby 或 Python 等语言在服务器端可以完成的任何事情!这有几个优点。首先,随着越来越多的 Web 应用程序开发在浏览器中通过 Backbone 等类似 MVC 的框架进行(稍后会详细介绍),前端开发人员现在可以轻松地无缝过渡到后端开发。从 JavaScript 切换到 Java 时无需进行心理上的切换,而且您可以在服务器端和客户端使用 JSON/JS 对象。JSON FTW!其次,就像我们都熟悉和喜爱的基于浏览器的 JavaScript 一样,node 本质上是异步的。服务器线程可以使用回调,而不是在等待数据库 I/O 完成时挂起一个线程。这在实际应用中意味着 node 速度很快。
应用程序
今天的文章将展示一个简单的 Web 应用程序后端,它是借助 Node 库 Express 编写的。这个特定应用程序的前端是用 Backbone 编写的,但那是后话了。这种应用程序架构,即由基于 MVC 的 JavaScript 框架驱动的浏览器中的单页应用程序、服务器上的 node 以及作为数据存储的 NoSQL 数据库,代表了现代 Web 应用程序开发的前沿。它也易于理解,并且是一个非常有趣的堆栈!
我将向您介绍的应用程序 Freelection 可以在线运行此处。源代码可以在 gitHub 上找到。这是一个简单的工具,我的小学老师妻子让我建造,让她的学校的孩子们在总统选举中投票。它允许用户创建选举、添加候选人、在此选举中投票,然后查看结果,包括总计结果和按投票站细分的结果(这只是按教室细分结果所需的额外维度)。
Node 基础知识
要开始使用 node,您必须首先在您的机器上安装 node。
注意:对于 Windows 用户来说,这可能有点麻烦。请按照此处的说明进行设置。
node 应用程序的库/依赖项可以在 package.json 文件中指定。捆绑的命令行实用程序 npm 用于安装此包中的依赖项。对于 Java 开发人员来说,这类似于 Maven pom,只是直观程度提高了 3 倍,美观程度提高了 12 倍。这是我的样子:
package.json
{
"name": "freelection",
"version": "0.0.1",
"private": true,
"engines": {
"node": "0.8.12",
"npm": "1.1.49"
},
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.0.0rc5",
"cradle":"0.6.4",
"underscore":"1.4.2",
"emailjs":"0.3.2",
"handlebars":"1.0.7"
}
}
这定义了应用程序“freelection
”,并指示了要使用的 node 和 npm 版本,以及要运行的启动脚本(命令行上的 node app 将把我们的 app.js 文件作为 node 应用程序运行)和依赖项。导航到包含 package.json 和 app.js 的根目录,并运行命令 npm install
将所需的依赖项安装到 node_modules 文件夹中。
应用程序核心
一旦 npm 安装了所需的库,我们就可以简单地通过运行 node app 来启动我们的应用程序。这将运行下面列出的 app.js 文件作为 node 应用程序。
app.js
var express = require('express'),
routes = require('./routes'),
election = require('./routes/election'),
candidates = require('./routes/candidates' ),
vote = require('./routes/vote' ),
http = require('http'),
path = require('path'),
cradle = require('cradle');
var app = express();
exports.app = app;
app.configure(function() {
app.set('port', process.env.PORT || 8080);
app.set('views', __dirname + '/views');
app.set('view engine', 'jshtml');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});
app.configure('development', function() {
app.use(express.errorHandler());
});
app.get('/', function( req, res ) {
res.sendfile( "public/index.html" );
});
app.post('/election', election.postElection);
app.get('/election/:electionId', election.getElectionInfo);
app.get('/candidates', candidates.getCandidates);
app.get('/candidate/:candidateId', candidates.getCandidateInfo);
app.put( '/candidate/:candidateId', candidates.updateCandidate );
app.delete( '/candidate/:candidateId', candidates.deleteCandidate );
app.post( '/candidate', candidates.addCandidate );
app.post( '/castVote', vote.castVote );
app.get( '/results/:id', vote.getResults );
http.createServer(app).listen(app.get('port'), function() {
console.log("Express server listening on port " + app.get('port'));
});
让我们分解一下这个看似简单的文件。在顶部,我们看到 require()
语句。这些语句相当于导入,用于加载其他 JavaScript 文件以在 app.js 中使用。例如,‘./routes/election’ 将文件 ./routes/election.js 加载为变量 election。同样,var express = require(‘express’)
将 package.json 中定义的 Express 模块加载。
使用 Exports 管理作用域
在 Node 中,函数和变量只能从同一文件内部访问。本质上,就像大多数合理的 OOP 语言一样,除非另有指定,否则您拥有私有访问权限。在 Node 中,这种“另有指定”以神奇的 exports 对象的形式出现。例如:
email.js
var emailJS = require("emailjs");
/* server is not accessible to other files */
var server = emailJS.server.connect({
user: process.env.SMTP_USER,
password: process.env.SMTP_PASS,
host: "smtp.gmail.com",
ssl: true
});
/* this makes the sendEmail function available outside of email.js */
exports.sendEmail = function( to, subject, body ) {
server.send( {
text: body,
from: "freelectionapp@gmail.com",
to: to,
subject: subject,
attachment: [ {data:"<html>" + body + "</html>", alternative:true} ]
}, function(err, message) { console.log(err || message); });
};
elsewhere.js
var email = require("./utility/email")
email.sendEmail( "congress@us.gov", "Budget", "Please balance the budget! Signed, everyone" );
够简单了吧?如果这令人困惑,您可以在这里阅读更多内容。
配置和运行 Express
好的,回到我们的主 app.js 文件。
var app = express();
exports.app = app;
app.configure(function() {
app.set('port', process.env.PORT || 8080);
app.set('views', __dirname + '/views');
app.set('view engine', 'jshtml');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});
在这里,我们调用 express()
来初始化我们的 express 应用程序。接下来,我们使用 exports
使应用程序可以从外部访问,并配置我们的 app
。这里还有几个值得注意的“神奇”变量—— __dirname
是您的 node 应用程序运行的目录路径,process.env.PORT
用于获取环境变量 %PORT%
。
现在跳到文件底部,我们启动刚刚创建的 app
:
http.createServer(app).listen(app.get('port'), function() {
console.log("Express server listening on port " + app.get('port'));
});
app.get(‘port’)
获取我们在上面配置中设置的 port
变量。HTTP 服务器创建后,我们提供的回调方法会被触发,将 express 应用程序正在运行的端口记录到控制台。要从命令行启动我们的应用程序,我们只需从命令行运行 node app,就会收到此消息:
Express server listening on port 8080
现在我们已经启动并运行了!让我们看看如何使用 express 设置控制器路径。
Express 控制器路径/服务终端
在 express 中,我们根据四个 HTTP 动词定义 HTTP 控制器路径:GET
、POST
、PUT
和 DELETE
。例如:
app.get('/', function( req, res ) {
res.sendfile( "public/index.html" );
});
在这种情况下,对 https://:8080/ 的 GET
请求将由上面指定的功能处理。请求被编组到 req
JavaScript 对象,并且响应可以作为 res
进行操作。在这种情况下,我们只是在 res
对象上调用 sendfile()
方法,指定我们的主 index.html 文件的路径。此 index.html 文件包含我们的单页 Web 应用程序,并且是此应用程序中唯一代表完整页面重新加载的控制器路径。其余指定的控制器路径/服务端点通过 AJAX 由客户端访问。
服务终结点可以与 exports
结合使用,以将服务处理推迟到另一个文件/模块:
app.js
election = require('./routes/election')
app.get('/election/:electionId', election.getElectionInfo);
election.js
exports.getElectionInfo = function( req, res ) {
var electionId = req.param('electionId');
console.log( "getElectionInfo: " + electionId );
db.get( electionId, function( err, doc ) {
if ( err) {
console.log( err );
res.send(500, "Unable to retrieve election data");
} else {
console.log( doc );
res.json( doc );
}
});
};
在这里,您可以看到我们的服务如何异步响应错误或 JSON 文档。这使我们进入第一个 CouchDB
查询。
CouchDB
对于这个项目,已经为本地开发设置了一个 CouchDB
实例,对于我在 Heroku 上的生产部署,设置了一个环境变量指向我的 Cloudant CouchDB
安装。(顺便说一句,Heroku 让 node Web 应用程序的部署变得轻而易举。非常简单的“gyros
”工作者模型,它基于 Amazon EC2。值得研究!)为了连接它,我使用了 cradle,一个用于 node 的 CouchDB
驱动程序。Cradle 使用起来非常简单,作为 node 和 CouchDB
的 RESTful 架构之间的简单接口。
我定义了一个名为 dao.js 的 node 文件,它实例化并导出我的 cradle 对象 db
的一个实例。
dao.js
var cradle = require('cradle');
if ( process.env.CLOUDANT_URL ) {
var db = new(cradle.Connection)(process.env.CLOUDANT_URL, 80).database('elections');
} else {
db = new(cradle.Connection)('http://127.0.0.1', 5984).database('elections');
}
db.exists(function (err, exists) {
if (err) {
console.log('error', err);
} else if (exists) {
console.log('db elections exists');
} else {
console.log('database elections does not exist. Creating...');
db.create();
console.log('database created');
}
db.save('_design/candidate', {
views: {
byId: {
map: function (doc) {
if (doc.type === 'candidate') {
emit(doc._id, doc);
}
}
},
byElectionId: {
map: function(doc) {
if ( doc.type === 'candidate' ) {
emit( doc.electionId, doc );
}
}
}
}
});
db.save('_design/vote', {
views: {
byElection: {
map: function( doc ) {
if ( doc.type === 'vote' ) {
emit( doc.electionId, doc );
}
}
}
}
});
});
exports.db = db;
在此文件中,我们检查我们的“elections
”数据库是否存在,如果不存在,则创建它。然后,我们保存了一些 CouchDB
视图,如果您还记得 Lou 的文章,这些视图允许我们根据不同的键和值查询我们的数据库。
我们使用单个数据库来存储数据。每个文档都以 _id
为键。为了区分存储的不同类型的文档(选举、候选人、投票等),按照惯例,我们在每个文档中包含一个 type
变量,指示文档的类型。例如,一个 election
文档看起来像这样:
{
"_id": "1a00a48331732c4436d51d770777f94f",
"_rev": "1-12146938649f35ee37b0d72b541897a2",
"type": "election",
"name": "Cambridge Elementary Presidential Election",
"email": "bjones@keyholesoftware.com",
"description": "This is an example election"
}
而一个 candidate
记录可能看起来像这样:
{
"_id": "f311b1dbca3624ef21959b2204fa4e40",
"_rev": "1-4d7bd4605957125729b82ed3cd7d86bd",
"type": "candidate",
"electionId": "1a00a48331732c4436d51d770777f94f",
"name": "Barack Obama",
"party": "Democrat",
"description": ""
}
请记住,CouchDB
中没有模式这样的东西,因此我们的应用程序本身负责保持数据类型的一致性!在这里,我们看到 candidates
通过 electionId
字段链接到 election
文档。这是在 RDBMS 中实现外键等效的一种方法。
更新/插入
使用 cradle 更新 CouchDB
相当简单。例如,这是创建新 vote
记录的方式:
db.save({
type: 'vote',
electionId: electionId,
candidateId: candidateId,
candidate: candidate,
station: station
}, function( err, doc ) {
console.log( err, doc );
if ( err ) {
res.send(500, err );
} else {
res.send( {success: true} );
}
});
请注意,如果 db.save()
传入了现有文档的 _id
,则该文档将被更新,而不是创建新记录。
CouchDB 视图和 Map/Reduce
让我们仔细看看我们数据库的设计文档及其视图。回想一下,在 CouchDB
中,视图被分组到不同的“设计文档”中,并且可以针对它们进行查询。
视图是根据 map
和(可选)reduce
函数定义的。map
函数会为数据存储中的每一个文档调用。当涉及大量数据时,这在多台机器上实际上是高效的,因为 CouchDB
将记录细分为子集,并并行处理。然后,map
的结果会传递给 reduce
函数,该函数可以将这些潜在的巨大结果集缩减为所需的子集。在我们的应用程序中,没有 reduce
函数,只有 map
。Map
/Reduce
是一个值得单独写一篇博客文章的主题,因此有关更多信息,我建议您在此处阅读更多内容。让我们看看我们 candidates
设计文档中的视图:
dao.js
db.save('_design/candidate', {
views: {
byId: {
map: function (doc) {
if (doc.type === 'candidate') {
emit(doc._id, doc);
}
}
},
byElectionId: {
map: function(doc) {
if ( doc.type === 'candidate' ) {
emit( doc.electionId, doc );
}
}
}
}
});
这是 cradle 定义一个名为 candidate
的设计文档的方式。Candidate 可以通过两种方式查询,直接通过 _id
,或者根据每个候选人的 electionId
进行聚合查询。
byId
视图很容易理解。它只是根据 doc.type
过滤文档。emit()
是 CouchDB
中的一个特殊方法,它将结果传递给 reduce 或返回给查询。它接受的第一个参数是可用于查询视图的 id
。第二个参数是返回的文档。在不指定 _id
的情况下查询 byId view
将返回所有选举中的所有候选人。
byElectionId
用于根据 candidate
文档的 electionId
获取它们列表。请注意,与 byId
不同,此视图的键是文档的 electionId
。此视图的查询方式如下:
candidates.js
exports.getCandidates = function(req, res) {
var electionId = req.param('id');
console.log( "get candidates for election: " + electionId);
db.view( 'candidate/byElectionId' , { key: electionId }, function(err, doc ) {
if ( err ) {
console.log(err);
res.send( 500, err );
} else {
var candidates = _.pluck(doc, "value");
console.log( "Candidates: ", candidates );
res.json( candidates );
}
});
};
请注意,cradle 查询的 doc 响应数组包含元数据以及每个返回的实际文档。_.pluck
用于从 doc
数组中的每个对象中获取 value
属性。value
是每个记录的实际文档对象。此服务调用的结果如下所示:
[
{
"_id": "1a00a48331732c4436d51d77077b4463",
"_rev": "1-669993a8d4339e32c309cfe129e22e86",
"type": "candidate",
"electionId": "1a00a48331732c4436d51d770777f94f",
"name": "Mitt Romney",
"party": "Republican",
"description": ""
},
{
"_id": "f311b1dbca3624ef21959b2204fa4e40",
"_rev": "1-4d7bd4605957125729b82ed3cd7d86bd",
"type": "candidate",
"electionId": "1a00a48331732c4436d51d770777f94f",
"name": "Barack Obama",
"party": "Democrat",
"description": ""
}
]
结论
CouchDB
和 node 非常适合快速开发简单、可伸缩的应用程序。CouchDB
是一个非关系型数据库,但是如果您对应用程序采用 NoSQL 路线的权衡没问题,那么 node
/CouchDB
搭配是相当不错的选择。客户端、服务器和数据存储三层都用 JavaScript 编写,具有某种美感。无需将 JSON 编组到严格的服务器端对象,也无需处理对象关系映射带来的范式不匹配。如果您希望在创纪录的时间内构建快速、可伸缩的应用程序(整个项目兼职工作不到一周),那么您应该认真考虑 node 方法。
– Brett Jones, asktheteam@keyholesoftware.com
参考文献
尽管本文并未涵盖所有这些库,但此应用程序的完整技术堆栈如下所示,值得一看。这些都是值得学习的尖端工具,也就是说,所有酷孩子都在用它们!
- Node – 服务器端 JavaScript
- Express – 用于 REST 服务调用的 node 框架
- CouchDB – NoSQL 分布式数据库
- cradle – 适用于 node 的 CouchDB 驱动程序
- RequireJS – 将 JS 文件作为单独模块异步加载 – AMD 规范的实现
- Backbone – 客户端 MVC* 框架**
- jQuery
- Bootstrap – 值得研究的惊人 CSS 框架和小部件工具包
- Handlebars – 模板引擎
- Backbone.validation – 用于表单/模型验证的 Backbone 插件
- Underscore – Backbone 构建的实用程序库
标签:CouchDB, Javascript, JSON, node.js, NoSQL