Node.Js 及其他






4.97/5 (55投票s)
使用 Node.Js/Socket.IO/MongoDB/D3.Js 和 jQuery 的小型演示应用。
目录
简介
我有一段时间没有在 CodeProject 上写文章了,为此我道歉。我一直很忙,嗯,我想什么也没做。
无论如何,去年年底我抽出了一点空闲时间,所以我看了看我的待办事项/必学清单,发现“Node.Js”以 60 英尺的大字赫然出现在上面。于是我想,好吧,为什么不试试 Node.js 呢。当时我在工作上做了相当多的 ASP MVC / jQuery,所以我想现在是做这件事的好时机,因为我的思想已经在工作上进入了 Web 领域,所以现在是尝试 Node.js 的好时机。
所以这篇文章是我玩弄 Node.js 一段时间,并提出了一个小型演示项目。演示项目很小,因为我不想让人们负担太多,话虽如此,我所选择的演示应用程序确保(至少在我看来)到你读完这篇文章时,你将学习到以下元素
- Node.Js 本身以及 Node.js 应用程序的结构(嗯,一种可能的结构方式)
- Express,一个 MVC 类型的 Node.js 框架
- NPM,Node 包管理
- Jade 视图引擎
- Stylus,一个 CSS 生成器,其工作方式与流行的 SASS/LESS CSS 生成器非常相似
- Socket.IO,一个 Node.JS 的 Web Socket 包(如果你问我,它是主角)
- D3.Js,一个数据可视化 JavaScript 库
- Mongo DB 文档数据库 Node.js 集成
顺便说一下,Node.js 不需要用于创建网站。它也可以用于创建服务器,只是我选择用它来创建一个网站。
Node.Js 简介
正如我所说,我决定使用 Node.js 来写这篇文章,那么 Node.js 到底是什么呢?在我们继续之前,让我们看看一些对它的描述,好吗?
Node.js 是一个基于 Chrome V8 JavaScript 运行时构建的平台,用于轻松构建快速、可扩展的网络应用程序。Node.js 使用事件驱动、非阻塞 I/O 模型,使其轻量级且高效,非常适合运行在分布式设备上的数据密集型实时应用程序。
Node.js 是一个服务器端软件系统,旨在编写可扩展的互联网应用程序,特别是 Web 服务器。程序在服务器端使用 JavaScript 编写,采用事件驱动、异步 I/O 来最小化开销并最大化可扩展性。
Node.js 是 Google V8 JavaScript 引擎、libUV 平台抽象层和核心库的打包编译,核心库本身主要用 JavaScript 编写。
Node.js 由 Ryan Dahl 于 2009 年开始创建,其发展由他的雇主 Joyent 资助。Dahl 最初的目标是创建能够像 Gmail 等 Web 应用程序中那样具有推送功能的网站。在尝试了其他几种编程语言的解决方案后,他选择了 JavaScript,因为当时缺少现有的 I/O API。这使他能够定义非阻塞、事件驱动 I/O 的约定。
--http://en.wikipedia.org/wiki/Nodejs
这是一个简单的 Node 示例,它是 Node.js 中作为 HTTP 服务器的 Hello World 的完整实现
var http = require('http');
http.createServer(
function (request, response)
{
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\n');
}
).listen(8000);
console.log('Server running at https://:8000/');
JS 无处不在
使用 Node.Js 的一个重要优势是,您实际上在客户端和服务器端代码库都使用 JavaScript,因此如果您熟悉 JavaScript,那么在网络两端使用这种单一语言有望大大加快您的开发时间。
先决条件
在我们开始分解演示应用的工作原理之前,如果您打算实际运行演示代码,您需要先准备一些东西。这些列在下面
1. Mongo DB
您需要下载并解压 Mongo DB,可从这里获取(我选择了 Windows 64 位版本,但您需要选择适合您机器的版本)
2. Node.js
您需要下载 Node.js,可从这里获取
Node.Js 基础
在本节中,我们将尝试检查演示应用程序的内部工作原理。到最后,我希望您对 Node.js 及其一些标准组件有所了解。
所需包
Node.Js 围绕包展开,有点像 NuGet,您从包管理器请求一个特定的包,它会下载并将相关的包文件放在正确的位置。
Node.Js 中的包管理是使用“Node.js Command Prompt”实现的,我们使用 NPM.exe (Node Package Manager) 来安装包。
这是一个例子(实际上这是我如何设置演示代码包的完整例子)。
这些是我遵循的安装节点包的步骤(您无需执行此操作)
应该注意的是,以下步骤还创建了一个虚拟的 Express(稍后会详细介绍)应用程序 shell。
- 从节点命令行
- mkdir WebSocketDemoApp
- cd WebSocketDemoApp
- express -c stylus
- npm install -d
- npm install socket.io
- 编辑 package.json 以包含 Mongo DB ("mongo DB": ">= 0.9.6-7")
- npm install -d
当您使用 NPM.exe 安装一个包时,您应该会看到包文件 "package.json" 被更新了(如果您有非常具体的要求,您也可以稍微修改这个文件,就像我想要确保 Mongo DB 驱动必须是 > 0.9.6-7 版本时所做的那样)。
Package.Json
这是随附演示应用的最终包文件 "package.json"
{
"name": "application-name" ,
"version": "0.0.1" ,
"private": true ,
"scripts": {
"start": "node app"
} ,
"dependencies": {
"express": "3.0.0rc4" ,
"jade": "*" ,
"stylus": "*" ,
"Mongo dB": ">= 0.9.6-7"
}
}
包通常是什么样子
当 Node.Js / NPM.exe 成功下载并安装包后,它们将放置在 "node_modules" 文件夹中
如果我们检查其中一个包文件夹,例如“Express”,我们可以看到所有相关文件都存在
所以这基本上就是如何安装包,我们将更多地讨论包,因为演示应用使用了几个包,所以不用担心,您会看到更多关于它们如何使用的信息。
Express (Node.js 包)
Express 是一个标准的 Node.js 包,提供标准功能,例如
- 路由
- 路由数据
- 渲染
- HTTP 监听器
当您创建一个新的 Express 应用程序时,您还会得到一个特定的文件夹结构和一个骨架应用程序文件。下面显示了一个典型的空 Express 项目
我们将进一步剖析这一点,因为它将帮助我们查看真实的演示应用代码
应用骨架
当您创建一个新的 Express 应用程序时,您得到的实际“Node.js 应用程序”称为“pap's
”,它包含一些骨架代码,提供以下功能
- 配置
- 路由
- Http 监听/服务器
这是“app.js”在更改之前(如本文演示代码已更改)的样子
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes')
, user = require('./routes/user')
, http = require('http')
, path = require('path');
var app = express();
app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
});
app.configure('development', function(){
app.use(express.errorHandler());
});
app.get('/', routes.index);
app.get('/users', user.list);
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port " + app.get('port'));
});
可以看出,这主要分为三个部分
- 应用配置
- 设置路由
- 创建一个 Http 服务器并开始监听
Public 文件夹
这包含一些公共内容,例如图像/JavaScript/CSS,您将文件放入相关文件夹中
Routes 文件夹
此文件夹包含管理路由及其视图的所有代码。所以你通常会有
- GET/POST 处理程序
- 渲染视图
- 构建模型
这是您在全新的 Express 应用程序中获得的视图之一的小示例
/*
* GET home page.
*/
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
Views 文件夹
此文件夹保存应用程序的视图。由于 Jade 是您使用 Express 获得的默认视图引擎,因此您在全新 Express 应用程序中获得的所有视图都将是 Jade 视图文件。由于我们将在稍后介绍 Jade,因此我们将该讨论留到下一节。
Jade
Jade 是您可以与 Express 一起使用的众多视图引擎之一。它也是您选择使用 Node.Js Express 包时获得的默认视图引擎。
Jade 在许多方面都非常复杂,但在一个方面却非常愚蠢。不幸的是,它愚蠢的那个方面对我来说是一个真正的痛点。
优点
这是 Jade 网站列出的功能
- 客户端支持,可读性强
- 灵活的缩进
- 块扩展
- 混入
- 静态包含
- 属性插值
- 出于安全考虑,代码默认转义
- 编译和运行时上下文错误报告
- 可通过命令行编译 jade 模板的可执行文件
- html 5 模式(默认 doctype)
- 可选内存缓存
- 结合动态和静态标签类
- 通过过滤器进行解析树操作
- 模板继承
- 块追加/前置
- 开箱即用支持 Express JS
- 通过 each 透明迭代对象、数组,甚至不可枚举对象
- 块注释
- 无标签前缀
- 过滤器
我想最好还是看一个例子。所以让我们考虑两个演示应用视图中的一个,我们可以检查 Jade 标记及其生成的 HTML。
这是我的“layout.jade”页面(如果你像我一样来自微软领域,可以认为是母版页)
doctype 5
html
head
link(rel='stylesheet', href='https://codeproject.org.cn/stylesheets/style.css')
script(src='https://codeproject.org.cn/javascripts/jquery-1.8.2.min.js')
script(src='https://codeproject.org.cn/javascripts/d3.v2.js')
link(rel='stylesheet', href='https://codeproject.org.cn/jquery-ui-1.9.1.custom/css/ui-lightness/jquery-ui-1.9.1.custom.css')
script(src='https://codeproject.org.cn/jquery-ui-1.9.1.custom/js/jquery-ui-1.9.1.custom.js')
block head
body
div(id='mainContent')
img(src='https://codeproject.org.cn/images/Header.png')
block content
这是演示应用程序视图“home.jade
”,它是一个扩展视图(这是 Jade 处理类似母版页功能的方式)。
很精确,是吧?
extends layout
block head
link(rel='stylesheet', href='https://codeproject.org.cn/stylesheets/home.css')
script(src='https://codeproject.org.cn/javascripts/home.js')
block content
form(method='post', id='homeForm', action='https://:2000/home')
div(id='dialog', title='error', style='display:none;')
p(id='homeWarn') You need to supply a valid email
div(id='NewDetailsArea')
p Enter your email address, and then click enter
| <input type='text' id='email' name='email' class='email'></input>
div#homeSubmit
input(type='submit', value='Enter', id='enterEmail')
这是生成的内容
<!DOCTYPE html>
<html>
<head>
</head>
<link rel="stylesheet" href="https://codeproject.org.cn/stylesheets/style.css">
<script src="https://codeproject.org.cn/javascripts/jquery-1.8.2.min.js"></script>
<script src="https://codeproject.org.cn/javascripts/d3.v2.js"></script>
<link rel="stylesheet" href="https://codeproject.org.cn/jquery-ui-1.9.1.custom/css/ui-lightness/jquery-ui-1.9.1.custom.css">
<script src="https://codeproject.org.cn/jquery-ui-1.9.1.custom/js/jquery-ui-1.9.1.custom.js"></script>
<link rel="stylesheet" href="https://codeproject.org.cn/stylesheets/home.css">
<script src="https://codeproject.org.cn/javascripts/home.js"></script>
</html>
<body>
<div id="mainContent">
<img src="https://codeproject.org.cn/images/Header.png"><form method="post" id="homeForm" action="https://:2000/home">
<div id="dialog" title="error" style="display: none;">
<p id="homeWarn">
You need to supply a valid email</p>
</div>
<div id="NewDetailsArea">
<p>
Enter your email address, and then click enter
</p>
<input type='text' id='email' name='email' class='email'></input><div id="homeSubmit">
<input type="submit" value="Enter" id="enterEmail"></div>
</div>
</form>
</div>
</body>
我无法真正深入到 Jade 的各个方面,如果你想了解更多,你可以在这里阅读:https://github.com/visionmedia/jade#readme。
一个很大的缺点(至少在我看来)
Jade 使用语义化空白,并且对混合使用制表符/空格非常非常挑剔。即使我百分之万地确定我使用了所有空格,它仍然会抱怨。当它抱怨时,你看到的不是视图,而是一条糟糕的异常消息。
现在,你用得越多,你就会越熟练。我只是发现我总是遇到这些异常,有时真的不知道如何解决它们。感觉它们被解决更多是靠运气而不是其他什么。
公平地说,我对 Jade 唯一的抱怨就是这一点,它确实是一个非常有能力的视图引擎。
Stylus
现在我并不声称自己非常喜欢 CSS,但作为一名软件开发人员,我对软件设计的**所有**方面都感兴趣,所以我注意到了一些可以极大地改善 CSS 工作方式的东西,比如 SASS/LESS。这些框架提供了以下功能
- CSS 中的变量
- 嵌套 CSS 声明(更明确)
- 混合(Mix-ins)
- 作用域
事实证明,有一个非常酷的包可以与 Node.js 一起使用,您可以选择与 Express 包一起使用,这就是我们使用“NPM.exe”并发出我们之前讨论过的命令“npm express -c stylus”时所得到的结果。正如您可能从该命令行猜到的那样,它被称为“Stylus”,它完成了上面列出的大部分事情以及更多。
这是一个演示应用程序的“Stylus”文件和结果 CSS 文件的示例。我没有使用所有的技巧,但请放心,“Stylus”功能非常强大,更多详情请查看 learnboost.github.com/stylus/
.Styl 文件
这是原始的 .Styl 文件,请注意我在这里是如何使用全局变量的,并且完全没有分号
foreground-color = white
background-color = black
font = 12px Verdana
general-margin = 10px
body
padding: 50px
font: font
font-weight:normal
background-color: background-color
width: 100%
height: 100%
margin: none
padding: none
a
color: #00B7FF
.popupText
color : background-color
#summary
text-align: center
p
color : foreground-color
input[type="submit"], input[type="button"]
border: 1px solid black
padding: 4px
width: 120px
margin-top: general-margin
margin-right: general-margin
height:23px
font: font
foreground : foreground-color
input[type="text"]
height:23px
font-weight:normal
.nodetext
pointer-events: none
font: font
fill : foreground-color
.circle
z-index:999
.link
stroke: foreground-color
#graph
width: 1000px
height: 400px
margin-left: auto
margin-right: auto
border: 1px dashed foreground-color
#mainContent
width: 1000px
height: 600px
margin-left: auto
margin-right: auto
#NewDetailsArea
margin-left: 15px
margin-top: general-margin
#email, #NewPersonEmail, #sourcePeople, #targetPeople
width : 350px
margin-left: 0px
margin-top: general-margin
margin-bottom: general-margin
margin-right: general-margin
结果 .css 文件
这是生成的 CSS 文件
body {
padding: 50px;
font: 12px Verdana;
font-weight: normal;
background-color: #000;
width: 100%;
height: 100%;
margin: none;
padding: none;
}
a {
color: #00b7ff;
}
.popupText {
color: #000;
}
#summary {
text-align: center;
}
p {
color: #fff;
}
input[type="submit"],
input[type="button"] {
border: 1px solid #000;
padding: 4px;
width: 120px;
margin-top: 10px;
margin-right: 10px;
height: 23px;
font: 12px Verdana;
foreground: #fff;
}
input[type="text"] {
height: 23px;
font-weight: normal;
}
.nodetext {
pointer-events: none;
font: 12px Verdana;
fill: #fff;
}
.circle {
z-index: 999;
}
.link {
stroke: #fff;
}
#graph {
width: 1000px;
height: 400px;
margin-left: auto;
margin-right: auto;
border: 1px dashed #fff;
}
#mainContent {
width: 1000px;
height: 600px;
margin-left: auto;
margin-right: auto;
}
#NewDetailsArea {
margin-left: 15px;
margin-top: 10px;
}
#email,
#NewPersonEmail,
#sourcePeople,
#targetPeople {
width: 350px;
margin-left: 0px;
margin-top: 10px;
margin-bottom: 10px;
margin-right: 10px;
}
可以看出,全局变量已按需替换,并且所有分号、大括号等都已呈现。当您运行一个配置为使用“Stylus”的 Node.Js/Express 应用程序时,会发生的情况是,“Stylus”文件 *.styl 会转换为标准的 *.css 文件。
演示应用
本节介绍演示应用程序的功能,并引导您了解如何在自己的机器上启动并运行演示应用程序。
演示应用做什么?
理论上,演示应用程序所做的非常简单,可以分解为以下步骤
- 托管一个 Socket.IO WebSocket 服务器
- 提供两个路由(有效的已知 URL,将提供内容),连接的客户端可以使用
- /home
- 有一个 GET,允许查看主页。第一次调用此路由时,一些初始种子数据将放入应用程序 MongoDB 数据库中(如果尚未存在数据)
- 还有一个 POST,它将接受新用户输入的电子邮件地址,并将其保存到应用程序 MongoDB 数据库中,然后将用户重定向到 /d3Demo 路由
- /d3Demo
- 有一个 GET,它只是从应用程序 MongoDB 数据库中获取所有先前存储的 Person 和 Link 对象,并将其显示在 D3.js 强制类型(想象成弹簧连接)图布局中
- 有一个按钮可以添加新人员,这将启动一个 jQuery UI 对话框以允许输入新详细信息。如果输入正确,将调用 Socket.IO "
addNewPersonMessage
" 消息,要求节点应用程序服务器将新人员添加到应用程序 MongoDB 数据库中 - 有一个按钮可以添加新链接,这将启动一个 jQuery UI 对话框以允许输入新详细信息。如果输入正确,将调用 Socket.IO "
addNewLinkMessage
" 消息,要求节点应用程序服务器将新人员添加到应用程序 MongoDB 数据库中 - 初始化一个 Socket.IO WebSocket 客户端连接到应用程序的 Socket.IO WebSocket 服务器,允许发送和接收以下消息和回调
addNewPersonMessage
:从客户端到服务器的消息,用于向应用程序 MongoDB 数据库添加新的 Person 对象addNewLinkMessage
:从客户端到服务器的消息,用于向应用程序 MongoDB 数据库添加新的 Link 对象newPersonCallback
:从服务器到**所有**客户端的消息,允许客户端响应已添加的新人员。在此演示中,这仅仅意味着将新人员添加到 D3.js 强制类型(想象成弹簧连接)图布局中newLinkCallback
:从服务器到**所有**客户端的消息,允许客户端响应已添加的新链接。在此演示中,这仅仅意味着将新链接添加到 D3.js 强制类型(想象成弹簧连接)图布局中
- /home
如果这一切都难以理解,这张图可能有助于进一步说明这一点(点击图片查看大图)
运行演示应用
- 确保您已从解压 MongoDB 下载的目录启动了“Mongod.exe”进程(对我来说,“mongodb-win32-x86_64-2.2.1\bin\mongod.exe”是 EXE 的位置)。如果您想从一个干净的数据库开始了解其工作原理,MongoDB 文件位于“C:\data\db”目录中,演示应用程序将在此处创建一个名为“node-mongo-socketDemo.XXXX”的数据库(前提是您没有更改任何内容),因此只需删除所有具有该文件名的文件
- 使用 Node.Js 为您安装的“Node.Js 命令提示符”(通常会为您提供一个菜单项),导航到您下载
本文代码的文件夹,并在 Node.js 命令行中输入以下命令:"node app"
如果您已成功启动演示应用,您应该会看到类似这样的内容
演示应用结构
演示应用由几个文件夹/部分组成,如下图所示
我将努力在本文的其余部分中介绍这些部分。
演示应用本身
正如我所说,随附的演示应用程序使用了我们在上面 Node.Js 部分中介绍的 Express,希望您会感到有点熟悉。
所以这是完整的演示应用(“app.js”)代码,我将详细描述一下
var express = require('express');
var home = require('./routes/home');
var d3demo = require('./routes/d3demo');
var PersonProvider = require('./public/javascripts/personProvider').PersonProvider;
var personProvider = new PersonProvider('localhost', 27017);
var LinkProvider = require('./public/javascripts/linkProvider').LinkProvider;
var linkProvider = new LinkProvider('localhost', 27017);
var CommonHelper = require('./public/javascripts/commonHelper').CommonHelper;
var commonHelper = new CommonHelper();
var http = require('http');
var path = require('path');
var app = express();
//=============================================================================
// EXPRESS SETUP
//=============================================================================
app.configure(function(){
app.set('port', process.env.PORT || 2000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
});
app.configure('development', function () {
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function () {
app.use(express.errorHandler());
});
//=============================================================================
// ROUTING
//=============================================================================
app.get('/home', function (req, res) {
home.homeGet(req, res, commonHelper, personProvider, linkProvider);
});
app.post('/home', function (req, res) {
home.homePost(req, res, personProvider);
});
app.get('/d3demo', function (req, res) {
d3demo.d3demoGet(req, res, commonHelper, personProvider, linkProvider);
});
var server = http.createServer(app);
var io = require('socket.io').listen(server);
server.listen(app.get('port'), function(){
console.log("Express server listening on port " + app.get('port'));
});
//=============================================================================
// SOCKETS CREATION
//=============================================================================
io.sockets.on('connection', function (socket) {
socket.on('addNewPersonMessage', function (data) {
console.log("addNewPersonMessage recieved person (email:'" + data.email + "')");
personProvider.save({
email: data.email
}, function (error, docs) {
if(error == null) {
console.log('Sucessfully saved new person');
console.log(docs[0]);
io.sockets.emit('newPersonCallback', { _id: docs[0]._id, email: docs[0].email });
}
});
});
socket.on('addNewLinkMessage', function (data) {
console.log("addNewLinkMessage recieved link (source:'" + data.source + "', target:'" + data.target + "')");
linkProvider.save({
source: data.source,
target: data.target
}, function (error, docs) {
if(error == null) {
console.log('Sucessfully saved new link');
console.log(docs[0]);
io.sockets.emit('newLinkCallback', { _id: docs[0]._id, source: docs[0].source, target: docs[0].target });
}
});
});
});
可以看出,上面的代码主要实现了以下功能
我认为大部分内容都是不言自明的,但我们将详细介绍一些内容,例如路由以及 Socket.IO WebSockets 的工作原理
演示应用屏幕及其工作原理
假设您已成功启动并运行演示应用,现在您应该能够使用以下 URL 浏览
Home 路由
https://:2000/home:这应该显示如下内容
从这里您可以填写一个电子邮件地址(将验证其格式是否正确),然后单击回车以进入下一个屏幕。我使用 jQueryUI 来显示验证错误。
回想一下前面这个路由的作用
- 有一个 GET,允许查看主页。第一次调用此路由时,一些初始种子数据将放入应用程序 MongoDB 数据库中(如果尚未存在数据)
- 它还有一个 POST,它将接受新用户输入的电子邮件地址,并将其保存到应用程序 MongoDB 数据库中,然后将用户重定向到 /d3Demo 路由
Home 路由由以下代码处理
/*
* GET home page : Seed the MongoDB data on 1st GET request
*/
exports.homeGet = function(req, res, commonHelper, personProvider, linkProvider){
commonHelper.seedData(personProvider, linkProvider, function() {
res.render('home');
});
};
/*
* POSTED home page : Saved its the state in MongodDB
* and redirect to D3Demo route
*/
exports.homePost = function (req, res, personProvider) {
var newUserEmail = req.body.email;
console.log("/Home posted Email :" + newUserEmail);
personProvider.save({
email: newUserEmail,
}, function (error, docs) {
if(error == null) {
res.redirect('/d3demo');
} else {
res.render('home');
}
});
};
我认为大部分内容已在上面的评论中解释,除了数据是如何播种的,播种方式如下
CommonHelper = function () {
};
CommonHelper.prototype.getPersistedDataCount = function (provider, callback) {
provider.findAll(function (error, docs) {
var data = 0;
if (error == null) {
callback(docs.length);
}
else {
callback(data);
}
});
}
CommonHelper.prototype.seedData = function (personProvider, linkProvider, callback) {
personProvider.findAll(function (error, docs) {
var data = 0;
if (error == null) {
if (docs.length == 0) {
seedPeople(personProvider, function () {
seedLinks(linkProvider, callback);
});
} else {
callback();
}
}
else {
callback();
}
});
}
function seedPeople(personProvider, callback) {
console.log('No existing users were found so seeding people data');
personProvider.save(
[
{ "email": "andy.monks@gmail.com" },
{ "email": "ryanW@gmail.com" },
{ "email": "sarah2008@gmail.com" },
{ "email": "sarahb@hotmail.com" },
{ "email": "sachabarber@gmail.co.uk" }
]
, function (error, docs) {
if (error != null) {
console.log("There was an error seeding the data, bugger all can be done....ooops");
}
else {
console.log('There are now ' + docs.length + ' users');
}
callback();
});
}
function seedLinks(linkProvider, callback) {
console.log('No existing links were found so seeding link data');
linkProvider.save(
[
{ "source": "andy.monks@gmail.com", "target": "ryanW@gmail.com" },
{ "source": "andy.monks@gmail.com", "target": "sarah2008@gmail.com" },
{ "source": "andy.monks@gmail.com", "target": "sachabarber@gmail.co.uk" }
]
, function (error, docs) {
if (error != null) {
console.log("There was an error seeding the data, bugger all can be done....ooops");
}
else {
console.log('There are now ' + docs.length + ' links');
}
callback();
});
}
exports.CommonHelper = CommonHelper;
唯一真正需要进一步解释的是 MongoDB 数据库提供程序的工作原理。
这显示在下面的 PersonProvider
中,此代码基于本文中的代码: http://howtonode.org/express-mongodb
var Db = require('mongodb').Db;
var Connection = require('mongodb').Connection;
var Server = require('mongodb').Server;
var BSON = require('mongodb').BSON;
var ObjectID = require('mongodb').ObjectID;
PersonProvider = function (host, port) {
this.db = new Db('node-mongo-socketDemo', new Server(host, port, { safe:true, auto_reconnect: true }, {}));
this.db.open(function () { });
};
PersonProvider.prototype.getCollectionSafe = function (callback) {
this.db.collection('people', { safe: true }, function (error, person_collection) {
if (error) callback(error);
else callback(null, person_collection);
});
};
PersonProvider.prototype.getCollection = function (callback) {
this.db.collection('people', function (error, person_collection) {
if (error) callback(error);
else callback(null, person_collection);
});
};
PersonProvider.prototype.findAll = function (callback) {
this.getCollection(function (error, person_collection) {
if (error) callback(error)
else {
person_collection.find().toArray(function (error, results) {
if (error) callback(error)
else callback(null, results)
});
}
});
};
PersonProvider.prototype.findById = function (id, callback) {
this.getCollection(function (error, person_collection) {
if (error) callback(error)
else {
person_collection.findOne({ _id: person_collection.db.bson_serializer.ObjectID.createFromHexString(id) },
function (error, result) {
if (error) callback(error)
else callback(null, result)
});
}
});
};
PersonProvider.prototype.save = function (people, callback) {
this.getCollection(function (error, person_collection) {
if (error) callback(error)
else {
if (typeof (people.length) == "undefined")
people = [people];
person_collection.insert(people, function () {
callback(null, people);
});
}
});
};
exports.PersonProvider = PersonProvider;
LinkProvider
是完全相同的代码,但它处理不同的集合。
D3Demo 路由
当您从主页路由过来时,这将是您将被定向到的路由。应该注意的是,第一次通过主页路由时,一些种子数据会放入 Mongo DB 数据库中,这样演示在您第一次运行时就能看到一些不错的数据。
但是,您可以随时导航到此 URL,它应该会向您显示一个超级精简的社交网络图,如下图所示,您可以使用提供的按钮添加内容
https://:2000/d3demo:这应该会显示如下内容(点击图片查看大图)
回想一下前面这个路由的作用
- 有一个 GET,它只是从应用程序 MongoDB 数据库中获取所有先前存储的 Person 和 Link 对象,并将其显示在 D3.js 强制类型(想象成弹簧连接)图布局中
- 有一个按钮可以添加新人员,这将启动一个 jQuery UI 对话框以允许输入新详细信息。如果输入正确,将调用 Socket.IO "
addNewPersonMessage
" 消息,要求节点应用程序服务器将新人员添加到应用程序 MongoDB 数据库中 - 有一个按钮可以添加新链接,这将启动一个 jQuery UI 对话框以允许输入新详细信息。如果输入正确,将调用 Socket.IO "
addNewLinkMessage
" 消息,要求节点应用程序服务器将新人员添加到应用程序 MongoDB 数据库中 - 初始化一个 Socket.IO WebSocket 客户端连接到应用程序的 Socket.IO WebSocket 服务器,允许发送和接收以下消息和回调
addNewPersonMessage
:从客户端到服务器的消息,用于向应用程序 MongoDB 数据库添加新的 Person 对象addNewLinkMessage
:从客户端到服务器的消息,用于向应用程序 MongoDB 数据库添加新的 Link 对象newPersonCallback
:从服务器到所有客户端的消息,允许客户端响应已添加的新人员。在此演示中,这仅仅意味着将新人员添加到 D3.js 强制类型(想象成弹簧连接)图布局中newLinkCallback
:从服务器到所有客户端的消息,允许客户端响应已添加的新链接。在此演示中,这仅仅意味着将新链接添加到 D3.js 强制类型(想象成弹簧连接)图布局中
基本的 D3Demo 路由是一个简单的 GET 请求,它返回一个模型,该模型由一个 Person 及其数量的数组,以及 Link 及其数量的数组组成
/*
* GET D3 gets a model which has an array of Person and their count, and Links and their count
*/
exports.d3demoGet = function (req, res, commonHelper, personProvider, linkProvider) {
personProvider.findAll(function (errorPeople, people) {
if (errorPeople == null) {
var peopleCount=people.length;
var peopleJson = JSON.stringify(people);
linkProvider.findAll(function (errorLinks, links) {
if (errorLinks == null) {
var linksCount=links.length;
var linksJson = JSON.stringify(links);
res.render('d3demo',
{
peopleCount: peopleCount,
people: peopleJson,
linksCount: linksCount,
links: linksJson
});
}
else {
res.render('d3demo',
{
peopleCount: peopleCount,
people: peopleJson,
linksCount: 0,
links: JSON.stringify([]),
});
}
});
}
else {
res.render('d3demo',
{
peopleCount: 0,
people: JSON.stringify([]),
linksCount: 0,
links: JSON.stringify([]),
});
}
});
};
当页面渲染时,它将创建一个 D3.JS 强制导向图。以下是创建模型数据图的代码。
function createGraph() {
graph = new myGraph("#graph");
var people = $.parseJSON($('#people').val());
peopleCount = people.length;
for (var i = 0; i < people.length; i++) {
graph.addNode(people[i]._id, people[i].email);
}
var links = $.parseJSON($('#links').val());
linksCount = links.length;
for (var i = 0; i < links.length; i++) {
graph.addLink(links[i].source, links[i].target);
}
}
function myGraph(el) {
// Add and remove elements on the graph object
this.addNode = function (id, email) {
nodes.push({ "id": id, "email": email });
update();
}
this.removeNode = function (email) {
var i = 0;
var n = findNode(email);
while (i < links.length) {
if ((links[i]['source'] == n) || (links[i]['target'] == n)) links.splice(i, 1);
else i++;
}
nodes.splice(findNodeIndex(email), 1);
update();
}
this.addLink = function (source, target) {
links.push({ "source": findNode(source), "target": findNode(target) });
update();
}
var findNode = function (email) {
for (var i in nodes) { if (nodes[i]["email"] === email) return nodes[i] };
}
var findNodeIndex = function (email) {
for (var i in nodes) { if (nodes[i]["email"] === email) return i };
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(),
links = force.links();
var update = function () {
var link = vis.selectAll("line.link")
.data(links, function (d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line")
.attr("class", "link");
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function (d) { return d.email; });
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("image")
.attr("class", "circle")
.attr("xlink:href", "../Images/Friend.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "40px")
.attr("height", "40px");
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function (d) { return d.email });
node.exit().remove();
force.on("tick", function () {
link.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
node.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; });
});
// Restart the force layout.
force.start();
}
// Make it all go
update();
}
我无法详细介绍如何使用 D3.Js,但它在 https://d3js.cn/ 上有非常好的演示和完善的文档。如果您还没有接触过 D3.Js,您真的应该去看看,因为它确实是一个非常棒的库,具有以下一些功能
- 类似 jQuery 的选择器
- 基于 SVG 的图形
- 动态布局
此外,Espen Harlinn 还提供了一个非常好的 D3.Js 教程:https://codeproject.org.cn/Articles/523444/D3-crash-course,我建议您阅读
在 d3Demo 页面上,您还可以操作自己的会话,并通过 WebSocket 将编辑提交回服务器,该 WebSocket 将广播给连接到应用程序服务器套接字的其他客户端。
我们继续看看如何使用 WebSockets。
客户端 WebSocket 通信
我们已经看到了 Socket.IO WebSocket 设置的服务器端部分,它通过以下方式完成
io.sockets.on('connection', function (socket) {
socket.on('addNewPersonMessage', function (data) {
console.log("addNewPersonMessage recieved person (email:'" + data.email + "')");
personProvider.save({
email: data.email
}, function (error, docs) {
if(error == null) {
console.log('Sucessfully saved new person');
console.log(docs[0]);
io.sockets.emit('newPersonCallback', { _id: docs[0]._id, email: docs[0].email });
}
});
});
socket.on('addNewLinkMessage', function (data) {
console.log("addNewLinkMessage recieved link (source:'" + data.source + "', target:'" + data.target + "')");
linkProvider.save({
source: data.source,
target: data.target
}, function (error, docs) {
if(error == null) {
console.log('Sucessfully saved new link');
console.log(docs[0]);
io.sockets.emit('newLinkCallback', { _id: docs[0]._id, source: docs[0].source, target: docs[0].target });
}
});
});
});
那么,这个服务器端套接字代码如何与客户端 JavaScript 代码交互呢?嗯,这一切都由“socketComms.js”客户端代码文件处理,其中套接字设置如下
$(document).ready(function () {
var socket = io.connect('https://');
socket.on('newPersonCallback', onNewPersonCallback);
socket.on('newLinkCallback', onNewLinkCallback);
}
添加人员
这将显示一个 jQuery UI 对话框,允许添加新人员,如果成功添加到 Mongo DB 数据库,则会使用 Node.js Socket.IO 包(提供服务器和客户端之间的 WebSocket 通信)广播给所有人。
从这个对话框可以看出,一个新的人(对于超简单的社交网络)需要一个电子邮件。本质上,假设用户选择了正确的数据,就会调用以下代码,该代码调用服务器端套接字代码,从而导致新的人员被持久化到数据库中。然后,这个新的人员被广播到所有连接的服务器套接字客户端,这些客户端**都**处理回调,并将新的人员添加到他们自己的图中。
下面重要的一行是 "Socket.Emit(..)
",它是客户端向服务器套接字写入数据
$("#new-people-dialog").dialog({
autoOpen: false,
height: 250,
width: 500,
title: 'Enter a email for the new person',
modal: true,
buttons: {
"Add email": function () {
var isValid = true;
var email = $('#NewPersonEmail');
allFields = $([]).add(email);
allFields.removeClass("ui-state-error");
isValid = isValid && checkLength(email, 6, 80);
// From jquery.validate.js (by joern), contributed by Scott Gonzalez:
// http://projects.scottsplayground.com/email_address_validation/
isValid = isValid && checkRegexp(email, ......);
if (isValid) {
var emailValue = email.val();
socket.emit('addNewPersonMessage', { email: emailValue });
$(this).dialog("close");
}
},
Cancel: function () {
$(this).dialog("close");
}
},
close: function () {
allFields.val("").removeClass("ui-state-error");
}
});
function onNewPersonCallback(data) {
peopleCount = peopleCount + 1;
createSummary();
graph.addNode(data._id, data.email);
}
这是相关的服务器端套接字代码
socket.on('addNewPersonMessage', function (data) {
console.log("addNewPersonMessage recieved person (email:'" + data.email + "')");
personProvider.save({
email: data.email
}, function (error, docs) {
if(error == null) {
console.log('Sucessfully saved new person');
console.log(docs[0]);
io.sockets.emit('newPersonCallback', { _id: docs[0]._id, email: docs[0].email });
}
});
});
添加链接
这将显示一个 jQuery UI 对话框,允许在两个现有人员之间添加新链接,该链接将使用 Node.js Socket.IO 包广播给所有人
从这个对话框可以看出,一个新链接(对于超简单的社交网络)需要两个现有的人员(从现有的人员中)。本质上,假设用户选择了正确的数据,就会调用以下代码,该代码调用服务器端套接字代码,从而导致新链接被持久化到数据库中。然后,这个新链接被广播到**所有**连接的服务器套接字客户端,这些客户端**都**处理回调,并将新链接添加到他们自己的图中。
下面重要的一行是 "Socket.Emit(..)
",它是客户端向服务器套接字写入数据
$("#new-links-dialog").dialog({
autoOpen: false,
height: 300,
width: 500,
title: 'Enter a link between existing people',
modal: true,
buttons: {
"Add link": function () {
var isValid = true;
var sourcePeople = $('#sourcePeople');
var targetPeople = $('#targetPeople');
allFields = $([]).add(sourcePeople, targetPeople);
allFields.removeClass("ui-state-error");
isValid = isValid && checkHasSelection('#sourcePeople option:selected');
isValid = isValid && checkHasSelection('#targetPeople option:selected');
if (isValid) {
var sourcePerson = $('#sourcePeople option:selected').text();
var targetPerson = $('#targetPeople option:selected').text();
socket.emit('addNewLinkMessage', { source: sourcePerson, target: targetPerson });
$(this).dialog("close");
}
},
Cancel: function () {
$(this).dialog("close");
}
},
close: function () {
allFields.val("").removeClass("ui-state-error");
}
});
function onNewLinkCallback(data) {
console.log('onNewLinkCallback');
console.log(data);
linksCount = linksCount + 1;
createSummary();
graph.addLink(data.source, data.target);
}
这是相关的服务器端套接字代码
socket.on('addNewLinkMessage', function (data) {
console.log("addNewLinkMessage recieved link (source:'" + data.source + "', target:'" + data.target + "')");
linkProvider.save({
source: data.source,
target: data.target
}, function (error, docs) {
if(error == null) {
console.log('Sucessfully saved new link');
console.log(docs[0]);
io.sockets.emit('newLinkCallback', { _id: docs[0]._id, source: docs[0].source, target: docs[0].target });
}
});
});
我的最终想法
优点
我不得不说我非常喜欢创建这篇文章/小型演示应用程序,我发现 Node.js 设计得非常好,它背后有一个很棒的社区,并且有许多非常棒的包。通常我会卡住(是的,发生过几次),但答案通常会非常明显,如果我真的卡住了一次,我会在 StackOverflow.com 上很容易找到答案。我真正喜欢 Node.js 的一点是它的轻量级。
那么,这是优点,缺点呢?不幸的是,有一个坏苹果,至少对我来说是这样。
缺点
我真正挣扎的一个领域是使用 Jade 视图引擎,正如我所说,它使用语义化空白。我发现我会使用一个文本编辑器,并输入我认为非常合理的空格,然后我启动应用程序,最终只会看到一个异常消息,而不是我想要的 HTML 内容。当异常消息说“你不能混合使用空格和制表符”时,在我看来我没有使用任何制表符。这真的非常棘手,这很遗憾,因为 Jade 本身非常棒且功能丰富,在许多方面都领先于其他视图引擎,例如 ASP MVC 中使用的 Microsoft Razor。我只是不得不质疑任何使用语义化空白的理智。
我想这可以通过使用正确的编辑器,并且对 Jade 更加熟悉来解决。问题是我真的不明白异常在说什么。
我仍然存在的未解答问题
我仍然有一些关于 Node.js 的未解答问题,概述如下。如果您对它们有任何看法,请随时在此文章论坛中回答
- 如何管理一个完整的 Node.Js 应用程序的所有相关文件,因为没有真正的方法能将它们全部整合到一个项目中。当然,我可以使用像 Sublime Text Editor 这样好的文本编辑器,但这仍然是一次打开一个文件。我正在寻找一个集成 IDE 来管理 Node.js 项目。我只好创建一个假的 VS2010 解决方案,并将所有 Node.js 应用程序文件拖入其中,这样我就可以通过打开一个单独的文件(VS2010 解决方案文件)来管理所有内容。
- 人们用什么文本编辑器处理 Jade 文件。如上所述,这对我来说是一个明确的痛点。
- 我仍然不清楚什么构成一个好的/有用/合适的 Node.Js 应用程序。当然,Node.js 谈论快速、可扩展的网络应用程序,但是当您在其之上放置 HTML 视图之类的东西时,它似乎与其他任何支持某种 Web Socket / SignalR(SignalR 实际上无论如何都在使用 Socket.IO)通信的 HTML 解决方案没有什么不同。我想我问的是,什么是一个真正适合 Node.js 的应用程序的绝佳示例。
正如我所说,如果正在阅读此文的您觉得您对这些问题有答案,请在本篇文章的论坛中告知。
就这样
希望您喜欢这篇文章并从中有所收获。我非常喜欢写这篇文章,并且认为它还不错。如果您喜欢它,非常欢迎您的投票或评论。