NodeJS 秘籍(四)
八、创建 WebSocket 服务器
这一章开始与书中前几章不同。以前,这些章节主要集中在 Node.js 核心及其功能上。这是为了更好地理解 Node.js 中包含的基础架构和平台可用性。然而,Node.js 之所以取得巨大成功,是因为第三方模块的生态系统以及它们提供的可扩展性。本章以及接下来的章节将会让你体验 Node.js 社区,以及它能为你的应用开发提供什么。本章从讨论 WebSockets 开始。
在 WebSockets 出现之前,客户端和服务器之间有许多类似 WebSocket 的通信方式。其中许多都使用了某种形式的从客户端到服务器的轮询,客户端连接到服务器,然后服务器要么直接用一个状态进行响应,要么长时间保持 HTTP 连接打开以等待事件。这会产生许多 HTTP 请求,并且不是客户端和服务器之间的完全双向通信。因此,HTML 5 规范起草了 WebSocket 协议,以允许这种具有持久连接的双向通信。
WebSockets 基于 WebSocket 协议,定义为与远程主机的双向通信,或 TCP 上的双向通信。WebSocket 通信是基于消息的,这使得它比 TCP 流等通信机制更容易处理。乍一看,WebSocket 实现可能看起来像一个 HTTP 实例,但接口的 HTTP 部分只是为了在客户端和服务器之间创建一个握手,并随后将连接升级到 WebSocket 协议。一旦握手成功,客户端和服务器都能够向对方发送消息。WebSocket 消息由帧组成,根据协议,帧是确定发送何种类型消息的信息部分。这些可以是内容的类型(二进制或文本),也可以是用于发出连接应该关闭的信号的控制帧。通过使用安全套接字层 (SSL) 连接的ws:// URI 方案和wss://来访问 WebSocket 端点。
WebSocket 之所以在 Node.js 中蓬勃发展,是因为 Node.js 的事件驱动特性以及手动或通过第三方工具快速高效地创建 web socket 服务器的能力。由于与 Node.js 的这种天然匹配,进入 WebSockets 世界的障碍使得用 Node.js 创建支持 WebSocket 的服务器变得很容易
您在第四章中简要地看到了如何创建一个升级的 WebSocket 连接,但是本章将展示如何利用不同的框架和技术来构建一个完整的 WebSocket 应用。您将涉及的一些主题包括:
- 使用第三方模块构建 WebSocket 服务器
- 监听客户端上的事件
- 用 WebSockets 构建 API
- 使用 WebSockets 从服务器传递事件
- 在浏览器中处理这些事件并创建双向通信
- 用 WebSockets 构建多用户应用
如果您不想创建自己的服务器,作为 Node.js 开发人员,您可以使用几种 WebSocket 实现。通过不创建自己的服务器,你会牺牲一些东西而获得另外一些东西。你牺牲了从概念到产品对服务的完全控制,但是如果你正在使用的模块得到很好的支持,你会得到围绕该模块的社区。本章将关注其中的两个模块:WebSocket-Node 和 Socket。IO 。两者都有强大的社区,开发人员可以向其寻求可靠的实现;然而,插座。IO 已经成为许多 WebSocket 开发者的首选。
8-1.用 WebSocket-Node 实现 WebSocket 服务器
问题
您想要开始使用 WebSocket-Node 模块 来创建 WebSocket 服务器。
解决办法
当您第一次转向 WebSocket-Node 来满足您的 WebSocket 需求时,您会发现您有机会利用一个关于如何格式化 WebSocket 的框架,因为它主要是 WebSocket 协议的 JavaScript 实现。
要开始使用这个 Node.js 模块,您首先需要从 npm 注册表安装,'npm install websocket]。'一旦你安装了这个,你就可以像清单 8-1 中所示的那样使用它,你可以看到你扩展了一个 web 服务器来利用升级后的 WebSockets 连接。
清单 8-1 。升级 Web 服务器以使用 WebSockets
/**
* using WebSocket-Node
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
wsserver.on('connect', function(connection) {
console.log('connected');
connection.send('yo');
});
wsserver.on('request', function(req) {
console.log('request');
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log(message.utf8Data);
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
});
它是如何工作的
当您使用 WebSocket-Node 创建 WebSocket 服务器时,您可以用一个简单易用的 API 完成很多事情。首先,您正在创建一个 HTTP 服务器。这是一个要求,因为 HTTP 连接必须升级,以便通过握手过程成功创建 WebSocket 连接。然后您需要在您的解决方案中创建一个 WebSocket 服务器配置对象serverConfig。
这个配置 将用于确定您的服务器将处理的 WebSocket 通信的类型。该配置上可供设置的选项如表 8-1 所示。这些默认值与您在 WebSocket 服务器中设置和使用的选项合并。
表 8-1 。WebSocket 服务器配置选项
| [计]选项 | 描述 |
|---|---|
| 集合碎片 | 这告诉服务器自动组装分段的消息,然后在“消息”事件中发出完整的消息。如果这不是真的,那么帧将在“帧”事件中发出,客户端需要自己将这些帧组装在一起。默认值:真 |
| 。自动接受连接 | 这告诉服务器是否接受任何 WebSocket 连接,而不管客户端指定的路径或协议。在大多数情况下应该避免这种情况,因为您最好检查请求以检查允许的来源和协议。默认值:false |
| 。关闭超时 | 这是发送关闭帧后等待的毫秒数,以查看在关闭套接字之前是否返回了确认。默认值:5000 |
| 。禁用算法 | 这将决定是否使用 Nagle 算法。该算法允许通过在传输前插入一小段延迟来将较小的数据包聚合在一起。默认值:真(无延迟) |
| 。dropConnectionOnKeepaliveTimeout | 这将告知 WebSocket 服务器断开与无法在. keepaliveGracePeriod 内响应 keepalive ping 的客户端的连接。默认值:true |
| 。分割阈值 | 如果传出帧大于这个数字,那么它将被分段。默认值:0x4000 (16KB) |
| 。fragmentOutgoingMessages | 此设置决定是否对超过 fragmentationThreshold 选项的邮件进行分段。默认值:真 |
| 。http server(http 服务器) | 这是您将要升级 WebSocket 协议连接的服务器。此选项是必需的。默认值:空 |
| 保持活力 | 此计时器将在每个指定的. keepaliveInterval 向所有客户端发送 ping 命令。默认值:true |
| 。keepaliveGracePeriod | 这是在发送 keepalive ping 后断开连接前等待的时间,以毫秒为单位。默认值:10000 |
| 。keepaliveInterval | 向连接的客户端发送 keepalive ping 的时间(毫秒)。默认值:20000 |
| 。maxReceivedFrameSize | 此选项用于设置 WebSocket 消息帧的最大帧大小阈值。默认值:0x10000(十六进制)= 64 千字节 |
| 。maxReceivedMessageSize | 这是为了设置邮件的最大大小。这仅适用于以下情况。assembleFragments 设置为 true。默认值:0x100000 (1 MB) |
| 。useNativeKeepalive | 这将告诉服务器使用 TCP keepalive,而不是 WebSocket ping 和 pong 数据包。不同的是,TCP keepalive 略小,减少了带宽。如果设置为 true,那么。keepaliveGracePeriod 和。dropConnectionOnKeepaliveTimeout 被忽略。默认值:false |
一旦使用 HTTP 服务器设置了配置,就可以通过调用new WebSocketServer([config])来实例化一个新的 WebSocket 服务器,这里的【config】表示您可以选择传入配置选项。在您的解决方案中,然后调用新 WebSocket 服务器的.mount()方法,这将合并选项并绑定到 HTTP 服务器的“upgrade”事件。
WebSocket 服务器可用的另一种方法是unmount(),它将取消从 HTTP 服务器升级到 WebSocket 协议的能力,但不会影响任何现有的连接。closeAllConnections() 是另一种方法,即优雅地关闭所有连接;shutdown()关闭所有连接并从服务器卸载。
有几个事件你也可以听。在您的示例中,您使用了“request”、“connect”和“close”事件。
当您没有将配置选项'autoAcceptConnections '设置为真时,将发出'request'事件。这将使您有机会检查传入的 WebSocket 请求,以保证您的目标是连接到所需的源和协议。然后,您可以选择accept()或reject()请求。你可以看到在这个例子中,accept()方法带参数。accept()方法可以接受三个参数:协议、来源和 cookies。该协议将只接受来自同一协议的 WebSocket 连接的数据。origin 允许您将 WebSocket 通信限制到指定的主机。参数中的 cookies 必须是名称/值对伴随请求的数组。
一旦请求被接受,服务器就会发出'connect'事件。然后,该事件将在已处理事件的回调中传递要处理的WebSocketConnection对象。
当与 WebSocket 服务器的连接因任何原因关闭时,会发出'close'事件。它不仅会传递WebSocketConnection对象,还会将关闭原因和描述传递给事件处理程序的回调。
您已经看到了如何使用 WebSocket-Node 创建到 WebSocket 服务器的连接,WebSocket-Node 是 web socket 实现的第三方模块。现在,您将研究两种与 WebSocket 服务器通信的方法,一种是使用 Node.js 客户机,另一种是从 web 应用上的客户机。
注意 WebSockets 并不完全适用于所有的浏览器。直到 Internet Explorer 版本 10,Internet Explorer 才实现该协议。Opera Mini(通过 7.0 版)和 Android 浏览器(通过 4.2 版)不支持该协议。除此之外,其他浏览器的一些旧版本不支持最新的实现。更多信息,请查看
http://caniuse.com/#feat=websockets。
8-2.在客户端监听 WebSocket 事件
问题
您希望能够作为客户机与 WebSocket 服务器通信。
解决办法
有几种方法可以连接到 WebSocket 连接,您将在本解决方案中看到其中的两种方法。实现这一点的一种方法是利用第三方框架 WebSocket-Node 来创建一个客户端应用,它将连接到 WebSocket 服务器并在两个端点之间进行通信。这在清单 8-2 中显示,并且不同于更典型的利用网页(你将在第 8-5 节中更详细地介绍)连接到 WebSocket 服务器并继续使用该协议进行通信的方法。
清单 8-2 。使用 WebSocket-Node 创建 WebSocket 客户端
/**
* A WebSocket Client
*/
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();
client.on('connectFailed', function(error) {
console.log('Connect Error: ' + error.toString());
});
client.on('connect', function(connection) {
console.log('woot: WebSocket client connected');
connection.on('error', function(error) {
console.log(error);
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
connection.on('message', function(message) {
switch (message.type) {
case 'utf8':
console.log('from server: ', message.utf8Data);
break;
default:
console.log(JSON.stringify(message));
break;
}
});
connection.send('heyo');
});
client.connect('ws://localhost:8080/', 'echo-protocol');
作为清单 8-2 中 WebSocket-Node 实现的替代方案,您可以创建一个 WebSocket 客户端,它将使用类似于清单 8-3 中所示的 HTML 页面进行连接。
清单 8-3 。WebSocket 客户端 HTML 页面
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok'}));
};
};
</script>
</body>
</html>
它是如何工作的
首先,您使用 WebSocket-Node 附带的 Node.js 应用中可用的WebSocketClient创建了一个客户机。当您调用new WebSocketClient();时,这个客户端为您创建到 WebSocket 服务器的升级连接。该构造函数将接受一个选项对象并用默认选项扩展该对象,如表 8-2 所示。
表 8-2 。WebSocketClient 选项
| [计]选项 | 描述 |
|---|---|
| 。集合碎片 | 这告诉客户端自动将碎片帧组装成一个完整的消息。默认值:真 |
| 。关闭超时 | 这是等待的时间,以毫秒为单位,直到连接在没有收到响应后关闭。默认值:5000 |
| 。禁用算法 | 这表明是否禁用 Nagle 算法,该算法将在发送消息之前设置一个小的延迟,以减少 HTTP 流量。默认值:真 |
| 。fragmentOutgoingMessages | 这将导致传出的消息大于集合。fragmentation 要分段的阈值。默认值:真 |
| 。分割阈值 | 这是将帧分割成片段的大小限制。默认值:16KB |
| 。webSocketVersion | 这是在此连接中使用的 WebSocket 协议的指定版本。默认值:13 |
| 。maxReceivedFrameSize | 这将设置通过 WebSocket 协议接收的帧的最大大小。默认值:1 MB |
| 。maxReceivedMessageSize | 这是通过协议接收的消息的最大大小。仅当。assembleFragments 选项设置为 true。默认值:8 MB |
| 。选项 | 该对象可以包含用于安全连接的传输层安全性(TLS)信息。 |
一旦创建了 WebSocket 客户端,就可以监听通过连接传输的事件和消息。在你的解决方案中,你监听一个'connect'事件。该事件将在回调中接收连接对象,然后您将使用该对象向服务器发送和接收数据。连接是通过调用。connect()web socket 客户端上的功能。这将接受您希望将端点绑定到的 URL 和协议。
为了向 WebSocket 服务器传输消息,您利用了connection.send()方法。该方法将接受两个参数:第一个是您希望发送的数据,第二个是回调函数(可选)。数据将被处理以检查数据是否是缓冲区。如果数据是缓冲区,它们将通过调用。sendBytes()连接的方法;否则,它将尝试使用连接的。sendUTF()方法如果数据可以用。toString()方法。那个。sendBytes()或。sendUTF()方法是传递回调的地方。您可以在清单 8-4 中看到 send 方法的 WebSocket-Node 实现的内部工作方式。
清单 8-4 。WebSocketClient 发送方法
WebSocketConnection.prototype.send = function(data, cb) {
if (Buffer.isBuffer(data)) {
this.sendBytes(data, cb);
}
else if (typeof(data['toString']) === 'function') {
this.sendUTF(data, cb);
}
else {
throw new Error("Data provided must either be a Node Buffer or implement toString()")
}
};
您还可以收听“消息”活动。这个事件是从 WebSocket 服务器发出的,在您的示例中,您检查了收到的消息类型。检查类型允许您适当地处理消息,无论它是 utf8 字符串还是其他格式。使用 WebSocket-Node 附带的WebSocketClient是为 Node.js 应用构建进程间通信的好方法。但是,您可能希望使用 HTML 页面来创建 WebSocket 客户端。
通过利用 web 浏览器中 WebSocket 对象中可用的WebSocketClient或本地 WebSockets,您可以创建一个到 WebSocket 服务器的有用的客户端连接。
8-3.构建 WebSocket API
问题
您希望构建一个利用 WebSockets 的应用,但是您需要创建一个非常适合 WebSocket 范例的 API。
解决办法
用 WebSockets 创建一个 API 似乎与另一个 API 方法不同,比如表述性状态转移(REST) 。这是因为,虽然你可以想象在你的应用中有多条路由,但是使用 WebSockets 你无法访问在 RESTful 设计中指示动作的 HTTP 动词。有几种方法仍然可以构建一个有组织的 API。在清单 8-5 中,你可以看到你构建了一个 WebSocket 服务器,与本章第一节中创建的服务器没有什么不同,它包含了一些额外的处理来自客户端的消息中发送的数据。
清单 8-5 。使用 WebSocket 服务器进行路由处理
/**
* using WebSocket-Node
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
wsserver.on('connect', function(connection) {
connection.send('yo');
});
wsserver.on('request', function(req) {
if (req.requestedProtocols[0] == 'echo-protocol') {
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
var rt = JSON.parse(message.utf8Data);
switch (rt.path) {
case 'route_a':
console.log('something cool on route a');
break;
case 'route_b':
console.log('something cool on route b', rt);
break;
default:
console.log('something awesome always can happen');
break;
}
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
} else {
console.log('protocol not acceptable');
}
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
});
一旦您在您的服务器上创建了这个路由处理,您就可以构建一个更符合逻辑的模型,通过 WebSocket 连接从客户端发送消息,如清单 8-6 中的所示。
清单 8-6 。手动路线
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok', path: 'route_a'}));
ws.send(JSON.stringify({ status: 'ok', path: 'route_b', action: 'update'}));
};
};
</script>
</body>
</html>
虽然这通常是使用 WebSockets 实现某种路由或 API 设计的成功策略,但是这种对象路由概念也有替代方案。一种替代方法是利用 WebSocket-Node WebSocketRouter对象。该对象允许您在基于 WebSocket 的 Node.js 应用中轻松地为不同的路径或协议指定单独的路由。这种类型的服务器如清单 8-7 所示。
清单 8-7 。一个 WebSocketRouter 服务器
/**
* WebSockets API
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server,
WebSocketRouter = require('websocket').router;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
var router = new WebSocketRouter();
router.attachServer(wsserver);
router.mount('*', 'echo-protocol', function(request) {
console.log('mounted to echo protocol');
var conn = request.accept(request.origin);
conn.on('message', function(message) {
console.log('routed message');
});
conn.send('hey');
});
router.mount('*', 'update-protocol', function(request) {
console.log('mounted to update protocol');
var conn = request.accept(request.origin);
conn.on('message', function(message) {
console.log('update all the things');
});
});
清单 8-8 展示了如何在 HTML 页面中构建一个 HTTP 客户端,它将展示如何从WebSocketRouter服务器指定路由。
清单 8-8 。WebSocketRouter HTTP 客户端
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok', path: 'route_a'}));
};
var wsupdate = new WebSocket('ws://localhost:8080', 'update-protocol');
wsupdate.onopen = function() {
wsupdate.send('update');
};
};
</script>
</body>
</html>
它是如何工作的
在研究了 WebSockets 的这两种 API 实现之后,您会立即注意到这些解决方案并没有什么过于复杂的地方。两者的基础都是在给定来自服务器的特定消息的情况下,指定要解决的动作。
在清单 8-4 中,您创建了一个服务器,处理从客户端传递到服务器的 JavaScript 对象符号(JSON) 对象。这要求您设计希望在 API 中提供的路线和动作。如果您愿意,您甚至可以通过相应地提供路线和动作来模仿 REST API。例如,如果您有一个希望通过 API 访问的用户配置文件,您可以构建一组如下所示的对象:
{ route: '/user/profile', action: 'GET' }
{ route: '/user/profile', action: 'POST' }
{ route: '/user/profile', action: 'PUT' }
{ route: '/user/profile', action: 'DELETE'}
这将由您的服务器通过解析传入的消息来处理,然后像您在解决方案的switch(rt.path) {...}中所做的那样处理路由。您可以看到,这个构建 WebSocket API 的解决方案非常适合许多需求,尤其是如果您只实现一个协议来处理 API 指令的话。当然,您可以隔离由不同 WebSocket 协议处理的路由。为此,WebSocket-Node 中有一个特性使得用一个WebSocketServer实例访问不同的协议变得更加容易。
清单 8-6 中的解决方案证明了这一点。在这里,您再次创建了您的服务器,但是您包括了。WebSocket-Node 模块中的路由对象。要利用这个特性,首先要创建 HTTP 服务器。然后,和以前一样,您必须告诉您的新 WebSocket 服务器,您希望使用这个 HTTP 服务器进行连接。然而,您现在可以传递WebSocketServer来绑定到一个WebSocketRouter实例,而不是之前看到的消息和连接处理。这个WebSocketRouter实例将允许您将路由的处理从您的客户端分离到特定的路径和/或协议。
在您的解决方案中,您构建了一个路由,它可以处理从客户端提供给它的任何路径(' * '),但是可以通过单独处理不同的协议来处理不同的路由。这意味着,如果您的应用中有一个逻辑分离,比如一个用于用户更新的 API 和一个用于产品更新的 API,您可以用一个单独的协议轻松地将它们分开。您只需在客户机上创建一个新的 WebSocket,它指向您的服务器并为每一项传递特定的协议。
var users = new WebSocket('ws://my.wsserver.co', 'users_protocol');
var products = new WebSocket('ws://my.wsserver.co', 'product_protocol');
从这里开始,您不再关心数据中路由的所有细节,尽管您仍然需要知道您希望通过 WebSocket 连接驱动的动作和特定事件,但是您知道如果您正在访问特定的 WebSocket 协议,您将被隔离到应用逻辑集。事实上,正如您在示例中看到的,整个路由在服务器上是隔离的。显然,分离对象类型是一种方法,但是您可以想象分离每种类型的更新/获取消息的可能性也是可能的。对于大多数情况来说,这可能太细了,但是在一个聊天室的例子中,您可能有一个'sendmessage_protocol'和一个'getmessage_protocol',并且完全独立地处理 get 和 send 操作。
在 Node.js 应用中,围绕 WebSocket 连接构建 API 的方式基本上是无限的,这允许您自由地创建自己认为合适的应用。
到目前为止,本章的大部分内容都基于 WebSocket-Node 模块及其实现。从这里开始,您将研究 Socket。IO,这是另一个非常流行的框架,用于构建基于 WebSocket 的 Node.js 应用。
8-4.使用插座。WebSocket 通信的 IO
问题
您希望通过利用套接字来构建基于 WebSocket 的 Node.js 应用。IO 模块。
解决办法
插座。IO 是一个完全可操作且非常受欢迎的框架,它将 WebSockets 与 Socket 的 Node.js.Implementations 结合使用。IO 可以采取多种形式,但最流行的是以类似于 Node.js 事件模型的方式在客户机和服务器之间传递消息。首先要安装 Socket。使用“$ npm install socket.io”命令通过 npm 进行 IO。来构建套接字。IO 服务器,您可以遵循清单 8-9 中所示的示例,该示例展示了在您实现 Node.js 套接字时可以使用的各种方法。IO 服务器。
清单 8-9 。实现套接字。IO 服务器
/**
* Socket.io Server
*/
var app = require('http').createServer(connectHandler),
io = require('socket.io').listen(app),
fs = require('fs');
app.listen(8080);
function connectHandler (req, res) {
fs.readFile(__dirname + '/8-4-1.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading 8-4-1.html');
}
res.writeHead(200);
res.end(data);
});
}
// General
io.sockets.on('connection', function (socket) {
socket.broadcast.emit('big_news'); // Emits to all others except this socket.
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
//namespaced
var users = io.of('/users').on('connection', function(socket) {
socket.emit('user message', {
that: 'only',
'/users': 'will get'
});
users.emit('users message', {
all: 'in',
'/users': 'will get'
});
});
插座的原因之一。IO 变得如此流行是因为它有一个嵌入的客户端模块,您可以在绑定到您的服务器的 HTML 页面中使用它。这允许毫不费力地连接到使用 Socket.IO 创建的 WebSocket 服务器。实现此连接需要添加一个 JavaScript 文件引用,然后使用套接字绑定到 WebSocket 服务器。IO 特定绑定,与 web 标准中的new WebSocket()实例化相反。
清单 8-10 。一个插座。IO 客户端
<!doctype html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
socket.on('big_news', function(data) {
console.log('holy cow!');
});
var users = io.connect('http://localhost/users');
users.on('connect', function() {
users.emit('users yo');
});
</script>
</head>
<body>
</body>
</html>
它是如何工作的
插座。在构建 WebSocket 服务器时,IO 可以为您做很多事情,但是它也提供了构建 Node.js 应用时所需的灵活性。就像 WebSocket-Node 一样,它抽象出握手来创建升级的 WebSocket 连接,不仅在服务器上,而且在包含套接字的客户端上。HTML 中的 IO JavaScript 文件。Socket.IO 也是独一无二的,因为它不仅利用了 WebSocket 协议的强大功能,而且还依赖于其他双向通信方法,比如 Flash sockets、long-polling 和 iframes。它将这样做,以便您可以构建您的应用,创建 WebSocket 通信结构 ,并且仍然能够依赖套接字。IO 通信,即使在旧的浏览器或不支持 WebSocket 协议的浏览器上。
在清单 8-8 中,你使用 Socket.IO 创建了一个 WebSocket 服务器。IO 包。然后,您使用本机 Node.js http 模块创建了一个简单的 HTTP 服务器;这是为了提供来自清单 8-9 的 HTML 页面,您计划用它来连接 WebSocket 服务器。
实例化套接字时。在您的代码中,您可以通过告诉新对象在 HTTP 服务器上进行监听来做到这一点,但是您也可以只传入一个监听端口。现在您可以访问插座了。IO API。在服务器上,有几个您用来与客户端通信的事件。
首先,您监听了“连接”事件 ,该事件在服务器接收到来自客户端的连接时发出。从该事件的回调中,您可以访问绑定到该特定连接的单个套接字。这个套接字是您的 WebSocket 通信可以发生的地方。
您执行的第一个通信是广播消息 。该消息通过调用socket.broadcast.emit('big_news');,来触发,调用socket.broadcast.emit('big_news');,会将消息'big_news''发送到连接到套接字的所有套接字。IO 服务器,发送广播的连接除外。接下来你通过使用socket.emit('news', { hello: 'world' });方法发出一个事件‘新闻’。可以在客户端监听该事件,然后可以在客户端处理与消息一起传输的数据。这类似于WebSocket.send()方法,您将在下一节中看到更详细的内容。您在“连接”事件回调中使用的最后一个方法是绑定到从客户端发出的任意事件消息。这与绑定到任何事件的方式相同。
然后,创建了一个绑定到名称空间的 WebSocket 连接。这将有助于创建类似于上一节中概述的示例的 API。您可以通过调用io.of('/path')绑定到名称空间。这将把该路径上的所有连接路由到指定的处理程序。您可以像在解决方案中一样命名这些名称空间var users = io.on('/users');.这很有用,因为您可以只在用户的名称空间上调用事件,比如当您通过调用users.emit('users message'...).向所有用户发出消息时
要在客户机上接收和传输消息,只需向 socket.io.js 文件添加一个 JavaScript 引用。这将为您提供对 I/O 对象的访问,然后您可以使用该对象连接到您的 Socket.IOserver。同样,就像服务器名称空间一样,您可以通过使用路径:var users = io.connect(' http://localhost/users ');连接到特定的路由。
通过这个实现,您可以看到如何利用套接字。IO 构建 Node.js WebSocket 服务器和客户端。IO 利用自己的 API 来发送和接收消息。但是,如果你选择使用 WebSocket 标准.send()而不是.emit()方法,Socket。木卫一将支持这一点。在接下来的几节中,您将进一步了解如何利用 WebSocket 对象跨连接发送和处理消息。
8-5.在浏览器中处理 WebSocket 事件
问题
您希望在浏览器中利用 WebSocket 事件。
解决办法
在本章的前面,您看到了如何使用 WebSocket-Node 模块构建 WebSocket 客户端。您还看到了可以在 web 浏览器中或通过使用 Socket 建立这些 WebSocket 连接的情况。浏览器中的 IO。清单 8-11 展示了如何在网络浏览器中直接使用 WebSocket API。在这种情况下,你应该运行一个类似于清单 8-1 所示的 WebSocket 服务器。
清单 8-11 。浏览器中的 WebSocket API
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok'}));
console.log(ws.binaryType);
console.log(ws.bufferedAmount);
console.log(ws.protocol);
console.log(ws.url);
console.log(ws.readyState);
};
ws.onerror = function() {
console.log('oh no! an error has occured');
}
ws.onclose = function() {
console.log('connection closed');
}
};
</script>
</body>
</html>
它是如何工作的
您可以通过简单地绑定到 WebSocket 服务器的端点并请求正确的协议来创建一个 HTML 格式的 WebSocket 客户端,在您的例子中,该协议被称为‘echo-protocol’。这是通过在网页的 JavaScript 中创建一个new WebSocket(<url>, <protocol>);对象来实现的。这个新的 WebSocket 对象可以访问几个事件和属性。可用的 WebSocket 方法有。close()和.send(),分别关闭连接或发送消息。您在解决方案中绑定到的事件是.onmessage和.onopen。那个。onopen一旦连接打开,就发出事件,这意味着连接准备好发送和接收数据。那个。onmessage事件是接收到来自 WebSocket 服务器的消息。其他可用的事件侦听器有。onerror,它将接收发生的任何错误。onclose事件,当状态变为关闭时发出。
浏览器中的 WebSocket 对象也可以访问几个属性。这些包括用于传输信息的 URL 和协议,以及服务器提供的状态和任何扩展。WebSocket 连接还可以查看通过该连接传输的数据类型。这是通过.binaryType属性访问的,它可以根据传输的数据报告“blob”或“arraybuffer”。浏览器上 WebSocket 的最后一个属性是。bufferedAmount房产。这告诉您通过使用。send()法。
8-6.通过 WebSockets 通信服务器事件
问题
您已经看到了如何实现 WebSocket 框架和模块。现在,您想使用 WebSockets 发送服务器信息。
解决办法
当您构建 WebSocket 服务器时,这种双向信息高速公路的一个非常吸引人的用例是能够以低延迟的方式发送服务器状态的更新或来自服务器的事件。这与标准 web 服务器相反,在标准 web 服务器中,您需要轮询信息;相反,您可以简单地按需发送信息,并在消息到达时绑定到消息。
您可以想象一个类似于您在前面章节中看到的情况,但是您已经创建了一个 WebSocket 服务器,它正在向客户端传输数据,包括连接和客户端驱动的消息传递。在这里,您将处理 WebSocket 连接,并定期向所有连接的客户端发送更新。
清单 8-12 。发送服务器事件
/**
* server events
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
var conns = [];
wsserver.on('connect', function(connection) {
console.log('connected');
conns.push(connection);
setInterval(pingClients, 5e3);
});
wsserver.on('request', function(req) {
console.log('request');
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log(message.utf8Data);
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
for (var i = 0; i < conns.length; i++) {
if (conns[i] === conn) {
conns.splice(i, 1);
}
}
});
function pingClients() {
for (var i =0; i < conns.length; i++) {
conns[i].send('ping');
}
}
它是如何工作的
在这个解决方案中,服务器本身的创建方式与您在 WebSocket-Node 中看到的许多其他方式相同。清单 8-11 突出显示了这种差异,显示了当建立连接时,它们被添加到一个到服务器的连接数组中。这样,您就不必为了向连接发送消息而留在连接内部或请求回调。在这个解决方案中,您创建了一个pingClients()方法,它将遍历数组中的所有连接,并每隔一段时间向它们发送一条消息;您可以想象这样一种情况,您有一个关键的服务器事件要传递,您能够以类似的方式将其分发到连接的套接字。
conns 数组包含对整个WebSocketConnection对象的引用。这意味着您能够从数组中挑选出一个连接并发送消息。您可以在pingClients函数中看到这一点,在这里您迭代数组并调用。send()法在每个单独的插座上。要在连接关闭后进行清理,只需在数组中找到关闭的连接,然后用splice()方法删除它。
8-7.与 WebSockets 的双向通信
问题
您需要能够利用 WebSockets 进行双向通信。
解决办法
这个解决方案将允许你用 WebSockets 创建一个简单的聊天室。您将使用套接字创建服务器。IO 并利用它在客户端与服务器和客户端之间传输数据。服务器如清单 8-13 所示,客户端网页如清单 8-14 所示。
清单 8-13 。插座。IO 聊天服务器
/**
* two-way communications
*/
var app = require('http').createServer(connectHandler),
io = require('socket.io').listen(app),
fs = require('fs');
app.listen(8080);
function connectHandler (req, res) {
fs.readFile(__dirname + '/8-7-1.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading 8-7-1.html');
}
res.writeHead(200);
res.end(data);
});
}
var members = [];
io.sockets.on('connection', function (socket) {
socket.on('joined', function(data) {
var mbr = data;
mbr.id = socket.id;
members.push(mbr);
socket.broadcast.emit('joined', data);
console.log(data.name, 'joined the room');
});
socket.on('message', function(data) {
// store chat now
socket.broadcast.emit('message', data);
});
socket.on('disconnect', function() {
for (var i = 0; i < members.length; i++) {
if (members[i].id === socket.id) {
socket.broadcast.emit('disconnected', { name: members[i].name });
}
}
});
});
清单 8-14 。聊天客户端
<!doctype html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="messages">
</div>
<form id="newChat">
<textarea id="text"></textarea>
<input type="submit" id="sendMessage" value="Send" />
</form>
<script>
var socket = io.connect('http://localhost');
var who;
socket.on('connect', function() {
var chatter = prompt('Please enter your name');
chatter = (chatter === "" || chatter === null) ? "anon" : chatter;
addChatter("you", "Joined");
who = chatter;
socket.emit('joined', { name: chatter});
});
function addChatter(name, message) {
var chat = document.getElementById("messages");
chat.innerHTML += "<div>" + name + " - " + message + "</div>";
}
socket.on('joined', function(data) {
console.log(data);
addChatter(data.name, ' joined');
});
socket.on('disconnected', function(data) {
addChatter(data.name, 'disconnected');
});
socket.on('message', function(data) {
addChatter(data.name, data.message);
});
var chat = document.getElementById("newChat");
chat.onsubmit = function() {
var msg = document.getElementById("text").value;
socket.emit("message", { name: who, message: msg });
document.getElementById("text").value = "";
addChatter(who, msg);
return false;
}
</script>
</body>
</html>
它是如何工作的
这个解决方案首先创建一个套接字。IO 服务器。该服务器将充当您连接的聊天客户端之间的中继,如果您要在生产环境中使用它,您可能希望添加一些持久层来将聊天存储在数据库中。
你的插座。IO server 为三个事件执行中继:加入聊天室、发送消息和断开套接字。
当你在客户端输入你的名字时,加入一个聊天室是被控制的。然后,客户端将通过socket.emit('joined', { name: <username> });发送一条消息,告诉服务器有一个加入事件,以及用户的名字。然后在服务器上接收,并立即向其他客户端发送广播事件。然后,这些客户端绑定到来自服务器的“joined”消息,该消息包含它们需要的数据,以便知道谁加入了房间。然后将其添加到网页的 HTML 中。
加入房间后,您可以向房间中的其他用户发送消息。这从客户端开始,您可以在文本区输入聊天消息,然后发送消息。这发生在socket.emit('message', {name: <user>, message: <text>});中,并且这再次被立即广播到文本所在的其他连接,并且用户被添加到 HTML 中。
最后,你想知道和你聊天的人是否已经离开了房间。为此,您绑定到套接字上的'disconnect'事件,并找到正在断开的套接字的用户名;这是通过将用户数据存储在服务器上的一个members[]数组中来实现的。然后,您向连接到服务器的其余客户端广播这一离开。
这是一个基本的聊天服务器,但它非常清楚地说明了如何使用 WebSockets 在客户端和服务器以及客户端之间进行低延迟的双向通信。在下一节中,您将看到如何使用类似的方法构建一个多用户白板,允许许多用户通过使用 WebSockets 以协作的方式共享绘制的坐标。
8-8.使用 WebSockets 构建多用户白板
问题
现在您已经理解了 WebSockets 的双向通信,您想要构建一个多用户白板应用,以便实时共享绘图。
解决办法
清单 8-15 展示了如何构建一个 WebSocket 服务器,作为 HTML 画布绘制客户端之间的媒介。这些客户端(HTML 如清单 8-16 所示)和(JavaScript 如清单 8-17 所示)将发送和接受 WebSocket 消息,该消息将提供跨客户端实例共享协作绘图程序的能力。
清单 8-15 。带有 WebSocket-Node 的绘图 WebSocket 服务器
var WebSocketServer = require('websocket').server,
http = require('http'),
sox = {},
idx = 0;
var server = http.createServer(function(request, response) {
response.writeHead(404);
response.end();
});
server.listen(8080, function() {
});
ws = new WebSocketServer({
httpServer: server,
autoAcceptConnections: false
});
function originIsAllowed(origin) {
//Check here to make sure we're on the right origin
return true;
}
var getNextId = (function() {
var idx = 0;
return function() { return ++idx; };
})();
ws.on('request', function(request) {
if (!originIsAllowed(request.origin)) {
request.reject();
console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
return;
}
var connection = request.accept('draw-protocol', request.origin);
connection.socketid = getNextId();
connection.sendUTF("socketid_" + connection.socketid);
console.log(connection.socketid);
sox[connection.socketid] = connection;
connection.on('message', function(message) {
if (message.type === 'utf8') {
sendToAll(JSON.parse(message.utf8Data), 'utf8');
}
else if (message.type === 'binary') {
connection.sendBytes(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
delete sox[connection.socketid];
});
});
function sendToAll(drawEvt, type) {
for (var socket in sox) {
if (type === 'utf8' &&drawEvt.socketid !== socket) {
sox[socket].sendUTF(JSON.stringify(drawEvt));
}
}
}
清单 8-16 。绘图画布和 HTML 标记
<!doctype html>
<html>
<head>
<title>whiteboard</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="jquery_1.10.2.js" type="text/javascript"></script>
<script src="drawings.js" type="text/javascript"></script>
</head>
<body>
<div id="wrapper">
<div class="menu">
<ul>
<li>
<a id="clear">Clear</a>
</li>
<li>
<li>
<a id="draw">Draw</a>
<ul id="colors">
<li style="background-color:white;">
<a>White</a>
</li>
<li style="background-color:red;">
<a>Red</a>
</li>
<li style="background-color:orange;">
<a>Orange</a>
</li>
<li style="background-color:yellow;">
<a>Yellow</a>
</li>
<li style="background-color:green;">
<a>Green</a>
</li>
<li style="background-color:blue;">
<a>Blue</a>
</li>
<li style="background-color:indigo;">
<a>Indigo</a>
</li>
<li style="background-color:violet;">
<a>Violet</a>
</li>
<li style="background-color:black;">
<a>Black</a>
</li>
</ul>
</li>
<label for="sizer">Line Size:</label>
<input name="sizer" id="sizer" type="number" min="5" max="100" step="5" />
</ul>
</div>
<canvas id="canvas" ></canvas>
<canvas id="remotecanvas"></canvas>
</div>
</body>
</html>
清单 8-17 。绘图应用:WebSockets 和 Canvas
$(document).ready(function() {
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
remotecanvas = document.getElementById("remotecanvas"),
remotectx = remotecanvas.getContext("2d"),
$cvs = $("#canvas"),
top = $cvs.offset().top,
left = $cvs.offset().left,
wsc = new WebSocket("ws://localhost:8080", "draw-protocol"),
mySocketId = -1;
var resizeCvs = function() {
ctx.canvas.width = remotectx.canvas.width = $(window).width();
ctx.canvas.height = remotectx.canvas.height = $(window).height();
};
var initializeCvs = function () {
ctx.lineCap = remotectx.lineCap = "round";
resizeCvs();
ctx.save();
remotectx.save();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
remotectx.clearRect(0,0, remotectx.canvas.width, remotectx.canvas.height);
ctx.restore();
remotectx.restore();
};
var draw = {
isDrawing: false,
mousedown: function(ctx, coordinates) {
ctx.beginPath();
ctx.moveTo(coordinates.x, coordinates.y);
this.isDrawing = true;
},
mousemove: function(ctx, coordinates) {
if (this.isDrawing) {
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
}
},
mouseup: function(ctx, coordinates) {
this.isDrawing = false;
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
ctx.closePath();
},
touchstart: function(ctx, coordinates){
ctx.beginPath();
ctx.moveTo(coordinates.x, coordinates.y);
this.isDrawing = true;
},
touchmove: function(ctx, coordinates){
if (this.isDrawing) {
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
}
},
touchend: function(ctx, coordinates){
if (this.isDrawing) {
this.touchmove(coordinates);
this.isDrawing = false;
}
}
};
// create a function to pass touch events and coordinates to drawer
function setupDraw(event, isRemote){
var coordinates = {};
var evt = {};
evt.type = event.type;
evt.socketid = mySocketId;
evt.lineWidth = ctx.lineWidth;
evt.strokeStyle = ctx.strokeStyle;
if (event.type.indexOf("touch") != -1 ){
evt.targetTouches = [{ pageX: 0, pageY: 0 }];
evt.targetTouches[0].pageX = event.targetTouches[0].pageX || 0;
evt.targetTouches[0].pageY = event.targetTouches[0].pageY || 0;
coordinates.x = event.targetTouches[0].pageX - left;
coordinates.y = event.targetTouches[0].pageY - top;
} else {
evt.pageX = event.pageX;
evt.pageY = event.pageY;
coordinates.x = event.pageX - left;
coordinates.y = event.pageY - top;
}
if (event.strokeStyle) {
remotectx.strokeStyle = event.strokeStyle;
remotectx.lineWidth = event.lineWidth;
}
if (!isRemote) {
wsc.send(JSON.stringify(evt));
drawevent.type;
} else {
drawevent.type;
}
}
window.addEventListener("mousedown", setupDraw, false);
window.addEventListener("mousemove", setupDraw, false);
window.addEventListener("mouseup", setupDraw, false);
canvas.addEventListener('touchstart',setupDraw, false);
canvas.addEventListener('touchmove',setupDraw, false);
canvas.addEventListener('touchend',setupDraw, false);
document.body.addEventListener('touchmove',function(event){
event.preventDefault();
},false);
$('#clear').click(function (e) {
initializeCvs(true);
$("#sizer").val("");
});
$("#draw").click(function (e) {
e.preventDefault();
$("label[for='sizer']").text("Line Size:");
});
$("#colors li").click(function (e) {
e.preventDefault();
$("label[for='sizer']").text("Line Size:");
ctx.strokeStyle = $(this).css("background-color");
});
$("#sizer").change(function (e) {
ctx.lineWidth = parseInt($(this).val(), 10);
});
initializeCvs();
window.onresize = function() {
resizeCvs();
};
wsc.onmessage = function(event) {
if (event.data.indexOf("socketid_") !== -1) {
mySocketId = event.data.split("_")[1];
} else {
var dt = JSON.parse(event.data);
setupDraw(dt, true);
}
};
});
它是如何工作的
这个解决方案再次从一个 WebSocket Node WebSocket 服务器的简单实现开始。该服务器将只接受“draw-protocol ”的连接。一旦建立了这些连接,就必须创建一个新的套接字标识符,以便稍后将消息传递给套接字。然后,您绑定到将从连接到达的消息事件。从这里开始,假设您将收到包含坐标的消息,这些坐标将从一个客户端向另一个客户端复制绘图。然后,通过遍历包含所有已连接套接字的对象,将这些消息发送给所有已连接的客户端。
function sendToAll(text, type) {
for (var socket in sox) {
if (type === 'utf8' && text.socketid !== socket) {
sox[socket].sendUTF(JSON.stringify(text));
}
}
}
在客户端,您创建了一个具有一些功能的画布绘制应用,但是您可以通过某种方式对其进行扩展,以便能够模拟从一个客户端到另一个客户端的整套鼠标或触摸运动。当然,您首先要绑定到 WebSocket 服务器的 URL,并利用该服务器所需的“draw-protocol”。然后在 JavaScript 中构建一个setupDraw函数。这将解析发生在画布上的鼠标或触摸事件,并将它们发送到画布上进行实际绘制。如果实例化绘图的事件在客户端开始,那么您将把坐标、样式和事件发送到 WebSocket 服务器进行调度。
if (!isRemote) {
wsc.send(JSON.stringify(evt));
drawevent.type;
} else {
drawevent.type;
}
然后在客户端上接收发送的绘画事件。这将再次调用setupDraw函数;只是这次您告诉绘图工具您的数据来自远程,这意味着您不需要将 stringified 事件发送回 WebSocket 服务器。
wsc.onmessage = function(event) {
if (event.data.indexOf("socketid_") !== -1) {
mySocketId = event.data.split("_")[1];
} else {
var dt = JSON.parse(event.data);
setupDraw(dt, true);
}
};
九、使用 Web 服务器框架
Node.js 非常适合构建 web 服务器。正因为如此,许多开发人员构建了 web 服务器应用。其中一些已经成为开源框架,可供任何希望开发 Node.js web 服务器的开发人员使用。在本章中,你将看到利用这些框架来构建 Node.js web 服务器的例子。
首先,您将研究最流行的 Node.js 框架之一 Express。您将了解如何利用 Express 构建 web 应用。您还将看到如何利用 Express 附带的工具快速构建应用。Express 还提供了一种路由请求的简单方法,允许您创建逻辑路由,并使用框架构建 API。
除了 Express 之外,本章还将介绍其他几个框架,它们允许您创建基于 Node.js 的 web 应用。您将研究的下一个框架在实现表达上略有不同,但是您将看到创建 Node.js 应用的各种方法。其中包括以下内容:
- 盖迪
- 雅虎!莫吉托(鸡尾酒的一种)
- 熨斗
9-1.快速入门
问题
您想要启动并运行 Express Node.js 应用框架。
解决办法
有几种方法可以帮助您开始使用 Express。在这个解决方案中,清单 9-1 ,您将使用 Express 构建一个 web 服务器来执行几个任务。
首先,您的服务器将使用 Express 中间件来记录向服务器发出的请求。您的服务器还将从脚本执行的目录中提供静态文件。这些静态文件将在提供时用 gzip 压缩。除了这些操作之外,您的 Express 服务器将能够执行简单的身份验证,并对静态页面无法提供服务的任何地址提供回退响应。
您还将看到如何获取和设置许多快速设置,以及如何启用和禁用它们。您还将创建一个方法,根据您是在开发模式还是在生产环境中提供内容,为您的应用设置不同的配置。要开始使用,您必须首先使用 npm 安装 Express framework。这可以通过运行命令$ npm install express 或全局执行$ npm install –g express 来完成;然后,您将能够开始使用 Express。
清单 9-1 。快速入门
/**
* Getting started with ExpressJS
*/
var express = require('express'),
app = express();
// use middleware
app.use(express.logger());
app.use(express.compress());
app.use(express.static(__dirname));
app.use(express.basicAuth(function(username, password) {
return username == 'shire' & password == 'baggins';
}));
// a simple route
app.get('/blah', function(req, res) {
res.send(app.get('default'));
});
// a default handler
app.use(function(req, res) {
res.send(app.get('default'));
});
// settings
console.log(app.get('env')); // development
console.log(app.get('trust proxy')); // undefined
app.disable('trust proxy');
console.log(app.get('trust proxy')); // false
console.log(app.get('jsonp callback name'));
console.log(app.get('json replacer'));
console.log(app.get('json spaces'));
console.log(app.get('case sensitive routing'));
console.log(app.get('strict routing'));
console.log(app.get('view cache'));
console.log(app.get('view engine'));
console.log(app.get('views'));
// configurations
app.configure('development', function() {
app.set('default', 'express development site');
});
app.configure('production', function() {
app.set('default', 'express production site');
});
// app.engine('jade', require('jade').__express);
app.listen(8080); // same as http.server.listen
它是如何工作的
Express 是一个为 Node.js 设计的应用框架。它是一个高度灵活的框架,允许您根据自己的需要构建 Node.js 应用。在清单 9-1 的解决方案中,你创建了一个执行几项任务的 web 服务器。为了实现这一点,首先必须安装 Express Node.js 模块。这可以用$ npm install express在你正在做的项目的本地完成,也可以用$ npm install –g express在全局完成。
一旦您安装了 Express,您就可以将它合并到您的项目中。您必须包含框架—require('express'),然后您告诉 express 通过实例化 Express 对象来创建一个应用。这将在您的解决方案中创建一个对 app 变量的引用,这将提供对 Express API 的访问。
这个解决方案首先让你看到的是对app.use() 的一系列调用(见清单 9-2 )。这个函数来自于 Express 的主要依赖项之一——Connect,它是 Sencha Labs 为 Node.js 构建的一个应用中间件框架。下面的片段来自于app.use()的快速实现。这向你展示了。use()调用是从 Connect 扩展而来的。
清单 9-2 。express/lib/application.js 中的 app.use
app.use = function(route, fn){
var app;
// default route to '/'
if ('string' != typeof route) fn = route, route = '/';
// express app
if (fn.handle && fn.set) app = fn;
// restore .app property on req and res
if (app) {
app.route = route;
fn = function(req, res, next) {
var orig = req.app;
app.handle(req, res, function(err){
req.__proto__ = orig.request;
res.__proto__ = orig.response;
next(err);
});
};
}
connect.proto.use.call(this, route, fn);
// mounted an app
if (app) {
app.parent = this;
app.emit('mount', this);
}
return this;
};
从这段代码中可以看出,app.use()方法是通过一些定制逻辑运行的,其中包括确保方法签名是预期的,请求和响应被正确传递,所有这些都是在调用 Connect 的.use()方法之前。
这意味着,当您在解决方案中调用app.use(<function>)时,您要么使用特殊的中间件功能提供一个通用路由,要么设置一个显式路由。在解决方案中,您首先使用它来调用express.logger()。
当添加到 Express 应用中时,express.logger()中间件用于记录服务器上的每个请求。在这个解决方案中,当您运行 Express 应用并导航到您的 Express 站点时,express.logger() 应用将记录类似如下的内容:
清单 9-3 。express.logger()在运行中
< 127.0.0.1 - - [Tue, 16 Jul 2013 00:26:22 GMT] "GET / HTTP/1.1" 401 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0"
< 127.0.0.1 - - [Tue, 16 Jul 2013 00:26:27 GMT] "GET / HTTP/1.1" 200 24 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0"
您使用的下一个中间件是express.compress() 。这个中间件将使用 gzip 压缩内容。例如,当添加了express.compress()中间件时,一个 1.3 KB(未压缩)的 JavaScript 文件大约需要 518 B。
在express.compress()之后,您将中间件express.static()添加到您的应用中。Express 使用express.static指向您希望用作静态文件处理程序的目录。例如,您可以使用它来提供静态 JavaScript、HTML 和 CSS 文件。关于express.static() 的案例对于发现app.use()的用法很有意思。如解决方案所示,您只需使用app.use()的功能处理程序,并将静态中间件放在那里。但是,您希望提供的静态内容可能位于应用中一个不知名的目录中,或者您只是希望将静态内容的路径重命名为其他名称。只需为静态处理程序命名一条路由,就可以轻松做到这一点。例如,如果您的静态内容驻留在子目录'/content/shared/static/'中,并且您希望将其作为'/static'来提供,那么您的app.use()将更改如下:
//Original
app.use(express.static(__dirname + '/content/shared/static'));
//Altered route
App.use('/static',express.static(__dirname + '/content/shared/static'));
您连接到 Express 的下一个中间件是express.basicAuth()中间件。这个中间件将允许您以基本的方式实现身份验证,以授予对 Express 应用的访问权限。在这个解决方案中,您提供了一个回调,然后直接检查这个回调中由basicAuth()中间件提供的凭证。
解决方案中的最后一个例子app.use()是为您的应用设置通用路由的默认响应。这是为回调提供请求和响应的地方。在解决方案部分,您还可以看到app.get()的使用。
app.get() 的表达方法有两个主要作用。第一个角色是路由角色。这是app.HTTP_VERB,意味着您将处理任何 HTTP GET 请求。您将在后面的章节中看到更多关于app.get()的用法。第二个角色,正如您在默认路由的回调中看到的,是通过使用app.set()方法检索您为 Express 应用设置的设置。
方法用于改变应用中的设置。签名是app.set(<name>, <value>)。在这个解决方案中,您设置了几个变量。一个是“default”变量,它设置您希望从 web 服务器中的默认路由提供的文本。
有一些设置用作 Express 应用的环境变量。有些,比如'jsonp callback name',默认情况下被设置为' callback ',但是可以设置为您希望的 JSON with padding (JSONP)方法的任何值。其他的,比如'trust proxy',显示设置的状态。在示例中,您可以看到默认的“trust proxy”设置是未定义的。然后利用app.disable('trust proxy')方法将该值设置为 false。这也可以使用app.enable()设置为真。还有一个env变量,它是 Node.js 进程运行的环境。然后,您可以使用来配置希望在开发和生产环境中保持不同的选项。
这是 Express 提供的基本 API。您可以将它用作 web 服务器框架,如本例所示,也可以利用 Express 的命令行功能来生成一个应用,您将在下面几节中看到。
9-2.使用 Express 生成应用
问题
您希望利用 Express 的命令行界面来快速生成应用支架。
解决办法
Express 不仅附带了您在上一节中看到的 API,还允许您创建能够处理 Node.js 服务器中所需的许多样板方法的 web 服务器,而且它还可以用作命令行应用生成器。清单 9-4 展示了快速应用生成的几种方法。
清单 9-4 。快递申请代
> npm install -g express
> mkdir myapp
> cd myapp
> express -h
Usage: express [options]
Options:
-h, --help output usage information
-V, --version output the version number
-s, --sessions add session support
-e, --ejs add ejs engine support (defaults to jade)
-J, --jshtml add jshtml engine support (defaults to jade)
-H, --hogan add hogan.js engine support
-c, --css <engine> add stylesheet <engine> support (less|stylus) (defaults to plain css)
-f, --force force on non-empty directory
> express
destination is not empty, continue? y
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./routes
create : ./routes/index.js
create : ./routes/user.js
create : ./public/images
create : ./public/javascripts
create : ./views
create : ./views/layout.jade
create : ./public/stylesheets
create : ./public/stylesheets/style.css
install dependencies:
$ cd . && npm install
run the app:
$ node app
> express --sessions --ejs --css less --force
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./routes
create : ./routes/index.js
create : ./routes/user.js
create : ./public/images
create : ./public/javascripts
create : ./views
create : ./views/index.ejs
create : ./public/stylesheets
create : ./public/stylesheets/style.less
install dependencies:
$ cd . && npm install
run the app:
$ node app
它是如何工作的
这个命令行实用程序首先使用npm install –g express将 Express 模块安装到您的机器上。从这里,您现在可以访问 express 命令行实用程序。当您键入“express”时,您运行的 JavaScript 文件可以在源文件的 bin 文件中找到。
首先,您想知道在 Express 命令行工具中可以访问什么。您可以通过键入express –h或express --help来完成此操作。这将打印可以伴随express命令的命令参数列表。前两个是帮助文本和您正在使用的 Express 版本的一般输出。
其他选项是通过解析命令行参数在应用中设置的,这些参数通过使用名为“commander”的模块传递并添加到名为“program”的对象中。
清单 9-5 。使用 commander 解析命令行参数
program
.version(version)
.option('-s, --sessions', 'add session support')
.option('-e, --ejs', 'add ejs engine support (defaults to jade)')
.option('-J, --jshtml', 'add jshtml engine support (defaults to jade)')
.option('-H, --hogan', 'add hogan.js engine support')
.option('-c, --css <engine>', 'add stylesheet <engine> support (less|stylus) (defaults to plain css)')
.option('-f, --force', 'force on non-empty directory')
.parse(process.argv);
接下来,您将创建一个 Express 应用,默认设置是不使用任何东西。这可以通过导航到您希望创建应用的目录并运行不带任何参数的express命令来完成。您还可以添加一个单独的参数来命名一个应用名express myapp,它将根据这个名称创建一个子目录,脚本将在这个目录中执行。
当 express 命令行应用执行时,除了如上所述解析参数之外,它还将立即调用一个函数,该函数将传入生成应用的路径。
清单 9-6 。生成应用
(function createApplication(path) {
emptyDirectory(path, function(empty){
if (empty || program.force) {
createApplicationAt(path);
} else {
program.confirm('destination is not empty, continue? ', function(ok){
if (ok) {
process.stdin.destroy();
createApplicationAt(path);
} else {
abort('aborting');
}
});
}
});
})(path);
这将检查您创建应用的目录是否为空。它通过利用 Node.js 和文件系统模块来实现这一点。
function emptyDirectory(path, fn) {
fs.readdir(path, function(err, files){
if (err && 'ENOENT' != err.code) throw err;
fn(!files || !files.length);
});
}
如果目录不为空,Express 将向您显示警告“目的地不为空,是否继续?”您只需输入“y”并继续。应用将根据您提供的参数生成应用结构并搭建您的应用。这是通过createApplicationAt()功能完成的。
该方法首先创建应用的根目录,然后创建应用所需的所有目录。您可以看到,这将利用您设置的标志来创建应用目录树。
mkdir(path, function(){
mkdir(path + '/public');
mkdir(path + '/public/javascripts');
mkdir(path + '/public/images');
mkdir(path + '/public/stylesheets', function(){
switch (program.css) {
case 'less':
write(path + '/public/stylesheets/style.less', less);
break;
case 'stylus':
write(path + '/public/stylesheets/style.styl', stylus);
break;
default:
write(path + '/public/stylesheets/style.css', css);
}
});
mkdir(path + '/routes', function(){
write(path + '/routes/index.js', index);
write(path + '/routes/user.js', users);
});
mkdir(path + '/views', function(){
switch (program.template) {
case 'ejs':
write(path + '/views/index.ejs', ejsIndex);
break;
case 'jade':
write(path + '/views/layout.jade', jadeLayout);
write(path + '/views/index.jade', jadeIndex);
break;
case 'jshtml':
write(path + '/views/layout.jshtml', jshtmlLayout);
write(path + '/views/index.jshtml', jshtmlIndex);
break;
case 'hjs':
write(path + '/views/index.hjs', hoganIndex);
break;
}
});
在设置好目录结构之后,将会生成根应用 JavaScript 文件以及正确配置的package.json文件。这可以通过简单地替换基于您在命令行上传递给 Express generator 的设置而设置的令牌来实现。
// CSS Engine support
switch (program.css) {
case 'less':
app = app.replace('{css}', eol + 'app.use(require(\'less-middleware\')({ src: __dirname + \'/public\' }));');
break;
case 'stylus':
app = app.replace('{css}', eol + 'app.use(require(\'stylus\').middleware(__dirname + \'/public\'));');
break;
default:
app = app.replace('{css}', ");
}
// Session support
app = app.replace('{sess}', program.sessions
? eol + 'app.use(express.cookieParser(\'your secret here\'));' + eol + 'app.use(express.session());'
: ");
// Template support
app = app.replace(':TEMPLATE', program.template);
// package.json
var pkg = {
name: 'application-name'
, version: '0.0.1'
, private: true
, scripts: { start: 'node app.js' }
, dependencies: {
express: version
}
}
if (program.template) pkg.dependencies[program.template] = '*';
// CSS Engine support
switch (program.css) {
case 'less':
pkg.dependencies['less-middleware'] = '*';
break;
default:
if (program.css) {
pkg.dependencies[program.css] = '*';
}
}
write(path + '/package.json', JSON.stringify(pkg, null, 2));
write(path + '/app.js', app);
现在,您已经有了一个通过应用的命令行界面工具构建的正常运行的 Express 应用。您可以看到,Express 附带的默认模板呈现利用了一个名为 Jade 的框架。在下一节中,您将看到如何利用这个工具来为您的 Express 应用创建最小且干净的 HTML 模板。
9-3.用 Jade 渲染 HTML
问题
您已经使用命令$ express生成了一个快速应用。默认情况下,这个应用将利用 Jade HTML 模板框架,因此您需要能够理解并在 Node.js 应用中利用这个框架。
解决办法
Jade 是专门为 Node.js 创建的模板语言,默认情况下它与 Express 一起使用,所以对它有所了解是很重要的。它是作为一个极简的模板引擎构建的;它可以用来从一个非常简洁的模板构建 HTML。
在此解决方案中,您将检查并构建使用 Express 生成的默认模板。这些位于/视图中。
清单 9-7 。/views/layout.jade
doctype 5
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
//if lt IE 8
script(src='/old_ie.js')
body
block content
清单 9-8 。/views/index.jade
extends layout
block content
h1= title
p.a_class Welcome to #{title}
#an_id this is a div which has an ID
label A range slider:
input(type='range')
#ckbx
label A checkbox:
input(type='checkbox', checked)
ul
li.odd: a(href='#', title='one') one
li.even: a(href='#', title='two') two
li.odd: a(href='#', title='three') three
case flag
when 0: #zero there is no flag
when 1: #one there is a single flag
default: #other other
- if (items.length)
ul
- items.forEach(function(item){
li= item
- })
清单 9-9 。Routes/index.js
/*
* GET home page.
*/
exports.index = function(req, res){
res.render('index', { title: 'Express', flag: 0, items: ['a', 'b', 'c'] });
};
它是如何工作的
Jade 的工作原理是解析自己的语法,然后将其转换成相应的 HTML 输出。看看你的 layout.jade 文件。这个文件声明了一个 doctype,特别是 HTML5 doctype。这可以通过键入“doctype 5”或“!!!5'.这些是文档类型的简写符号。虽然对于简洁的 HTML5 文档类型来说,这不是一个太极端的快捷方式,但是从清单 9-10 中可以看出,如果您在 Express 应用中使用一个过渡文档类型,这将变得非常有用。
清单 9-10 。Express.js 的可用文档类型
var doctypes = exports.doctypes = {
'5': '<!DOCTYPE html>',
'default': '<!DOCTYPE html>',
'xml': '<?xml version="1.0" encoding="utf-8" ?>',
'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
'1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
};
接下来,您会看到 HTML 元素是以速记的方式编写的,这允许高度可读的标记。创建元素,内部内容由两个空格或一个制表符嵌套,以指示它们相对于父元素。您可以看到,嵌套在 head 元素下面的 title 元素明显体现了这一点。元素中的属性(如 script 元素中的“src ”)在括号中设置。注释从单行 C 风格注释转换为 HTML 注释,因此您可以看到添加条件参数来测试旧版本的 Internet Explorer 并不复杂。一旦完成了这个 HTML 块的所有 Jade 呈现,直到元素,您可以看到这个相对简短的标记被翻译成下面的 HTML:
<!DOCTYPE html><html>
<head>
<title>Express</title>
<link rel="stylesheet" href="/stylesheets/style.css">
<!--[if lt IE 8]>
<script src=”/zomg_old_ie.js”></script>
<![endif]-->
</head>
<body>
</body>
</html>
在检查 index.jade 文件时,您会看到许多与 layout.jade 文件相同的内容。您可以看到,您已经用“扩展布局”命名了要扩展的 Jade 文件。您还有一个命名块“块内容”,它与布局文件上的相应块相匹配。
这个文件中还展示了 Jade 的模板呈现功能的一个示例。您有一个“h1= title”代码段。该代码片段将在您的代码中创建一个 H1 元素,但也会从 routes/index.js 文件中的索引呈现代码中获取对象,并将“标题”添加到 HTML 标记中。您创建的模板对象的其他部分是“flag”属性和“items”数组。这些项目也被解析成 HTML。如您所见,您在代码中的“case”语句中使用的标志。这允许条件标记。您还可以看到,您能够遍历项目数组,并为每个项目呈现一个列表项目。当进行这种迭代时,您必须在您用来迭代的代码前面加上一个连字符。然后你可以在这个连字符后面写 JavaScript 来生成你想要的布局。当您构建大型列表时,这可以极大地改进标记的生成,并在开发周期中节省时间。另一种遍历这个数组的方法是写“items 中的每一项”,然后写“li= item”(下面缩进)。
Jade 也有向 HTML 标签添加类和 id 的简写方法。您可以看到p.a_class将在哪里生成一个
标签,并将类“
a_class”添加到该标签中。类也可以链接在一起,允许任意数量的类名绑定到一个标签上。向标签添加 ID 也同样简单。只需在文本字符串前加上#就可以了,该文本字符串将成为标签的 ID。但是,您也可以创建一个带有 ID 的标记,而不命名该标记。只需添加'#an_id,您就可以在标记中生成一个<div id='an_id'>标记,而无需键入额外的三个字符来命名 div。
如果您使用 Express 和 Jade 创建 Node.js 应用,您就可以使用这个强大的模板引擎创建干净的标记。
9-4.使用 Express 路由
问题
您已经使用 Express 创建了一个应用。您的应用中的 URL 结构变得越来越复杂,因此您需要一个强大的路由机制来将这些 URL 连接到适当的处理程序。
解决办法
Express 提供了一种在 Node.js 应用中调用正确路由的简单方法。您可以在 Express 应用的路由处理程序中将路由作为外部模块或内联回调来处理。清单 9-11 给出了这两种路由行为的例子。这建立在第 9-2 节的服务器文件上。您可以看到已经添加的路由在这个列表中以粗体突出显示。
清单 9-11 。快速路由
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes')
, user = require('./routes/user')
, http = require('http')
, path = require('path');
var app = express();
// all environments
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(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
app.get('/', routes.index);
app.get('/user/:id', function(req, res) {
res.send('you supplied param ' + req.params.id);
});
app.get('/users', user.list);
app.post('/user/:id', function(req, res) {
console.log('update user' + req.params.id);
res.send('successfully updated');
});
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
它是如何工作的
Express 通过利用动词是 HTTP 动词的app.VERB范例来处理路由。这意味着你可以为特定类型的 HTTP 请求路由一个应用,当你在 9-6 节用 Express 构建一个 API 时,你会看到更多。
app.VERB方法的行为都类似于你在 9-1 节看到的中间件路由。它们各自接收一个路由,该路由指向请求的访问 URL,相对于应用根。它们还接收一个回调函数。如果您希望以特定的方式处理特定路由的所有请求,不管提供的 HTTP 动词是什么,您可以使用app.all()函数来实现。
在此解决方案中,有多种路线可供使用。首先,您会看到“/”的根路径有一个路由。这指向一个路由模块和驻留在那里的索引方法。这个索引方法有一个请求和响应对象,然后通过发送一个响应来处理它们,在本例中,响应是使用 Jade 呈现的,但不一定是 HTTP 响应可以发送的任何内容。
您还可以看到路由请求可以提供通用参数。这在路由“users/:id”中很明显。这里的':id '是您将传递给路由的参数。这不限于单一参数;事实上,您可以从清单 9-12 中的源代码片段中看到,任何数量的参数都可以添加到一个路由中。
清单 9-12 。找到一条匹配的路由,然后将剩余的路由解析为参数的一部分
Route.prototype.match = function(path){
var keys = this.keys
, params = this.params = []
, m = this.regexp.exec(path);
if (!m) return false;
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = 'string' == typeof m[i]
? decodeURIComponent(m[i])
: m[i];
if (key) {
params[key.name] = val;
} else {
params.push(val);
}
}
return true;
};
这个逻辑依赖于给定路径的正则表达式,正如您在 app.js 文件中定义的那样。对于' users/:id '路由,构建了这个正则表达式,它将成为下面显示的表达式。
清单 9-13 。“user/:id”路由的正则表达式
/^\\/user\\/(?:([^\\/]+?))\\/?$/
如果您提供了一个 ID,这将创建['/user/1 ',' 1']的正则表达式匹配。如果您要向这个路由添加一个额外的参数(例如' user/:role/:id '或类似的东西),那么正则表达式将会如下所示。
清单 9-14 。“/user/:role/:id”的正则表达式
/^\\/user\\/(?:([^\\/]+?))\\/(?:([^\\/]+?))\\/?$/
这个路由产生['/user/eng/1 ',' eng ',' 1']的正则表达式匹配。然后,这些匹配被添加到路由的 params 数组中。
您可以使用 Express 来建立布线和参数化布线。在下一节中,您将大致了解如何在 Node.js 应用中处理失败的请求。
9-5.处理应用中失败的请求
问题
您已经在 Node.js 中构建了一个 Express 应用,甚至还指定了路由和请求处理。但是,您需要能够在应用中正确处理失败的请求。
解决办法
Express 是一个健壮的框架,允许开发人员以自己的方式处理应用开发过程的许多方面。当您在构建快速路线或通过向 Node.js 应用添加中间件来扩充设置时,这一点很明显。处理失败的请求也是 Express 允许的事情,但它不会对此过于固执己见。
这个失败的请求处理显示在列表 9-15 中。这些处理程序必须遵循一定的模式,但是可以在需要的地方适合您的应用。
清单 9-15 。Express 中失败的请求处理
/**
* Getting started with ExpressJS
*/
var express = require('express'),
app = express();
// use middleware
app.use(express.logger());
app.use(express.compress());
app.use(express.static(__dirname));
app.use(express.basicAuth(function(username, password) {
return username == 'shire' & password == 'baggins';
}));
// a simple route
app.get('/blah', function(req, res, next) {
next(new Error('failing route'));
res.send(app.get('blah'));
});
// a default handler
app.use(function(req, res) {
res.send(app.get('default'));
});
app.use(function(err, req, res, next){
console.error(err.stack);
res.send(500, 'Oh no! Something failed');
});
// configurations
app.configure('development', function() {
app.set('default', 'express development site');
app.set('blah', 'blah blah blah');
});
app.configure('production', function() {
app.set('default', 'express production site');
});
// app.engine('jade', require('jade').__express);
app.listen(8080); // same as http.server.listen
它是如何工作的
你用与你在第 9-1 节中构建的初始例子相似的方式构建了这个解决方案。清单 9-15 中突出显示了两个重要部分。
首先,为了模拟失败的路由,您在路由'/blah '上构建一个错误。这会调用next()路由处理程序,并向其传递一个新的Error()对象。这允许失败的请求路由工作,因为如果没有这个处理程序,站点将会崩溃。
失败的请求路由是通过使用app.use()函数建立的。然后使用包含错误参数的默认回调。当调用next()函数出错时,Express 将利用这个错误处理程序,并将其作为失败的请求处理程序。通过这种方式,您可以优雅地处理 Node.js 服务器上的错误。
9-6.用 ExpressJS 设计 RESTful API
问题
在设计 API 时,很多时候你希望利用 HTTP 并为你的应用创建一个表述性状态转移(REST)架构。
解决办法
在前面的章节中,您已经了解了如何在 Express 中构建有用的路线。因为 Express API 允许路由以特定的 HTTP 动词为目标,所以它非常适合类似 REST 的 API。
在这个解决方案中,您将创建一个简单的 REST API 来与由产品组成的数据模型进行交互。在本例中,这些产品由一个简单的 JavaScript 对象表示,但是它们可以很容易地成为一个对象集并从数据存储中检索。该示例如清单 9-16 所示。
清单 9-16 。休息界面
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path');
var app = express();
// all environments
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(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
var products = [
{ id: 0, name: 'watch', description: 'Tell time with this amazing watch', price: 30.00 },
{ id: 1, name: 'sandals', description: 'Walk in comfort with these sandals', price: 10.00 },
{ id: 2, name: 'sunglasses', description: 'Protect your eyes in style', price: 25.00 }
];
app.get('/', routes.index);
// curl -X GET http://localhost:3000/products
app.get('/products', function(req, res) {
res.json(products);
});
// curl -X GET http://localhost:3000/products/2
app.get('/products/:id', function(req, res) {
if (req.params.id > (products.length - 1) || req.params.id < 0) {
res.statusCode = 404;
res.end('Not Found');
}
res.json(products[req.params.id]);
});
// curl -X POST -d "name=flops&description=sandals&price=12.00" http://localhost:3000/products
app.post('/products', function(req, res) {
if (typeof req.body.name === 'undefined') {
res.statusCode = 400;
res.end('a product name is required');
}
products.push(req.body);
res.send(req.body);
});
// curl -X PUT -d "name=flipflops&description=sandals&price=12.00" http://localhost:3000/products/3
app.put('/products/:id', function(req, res) {
if (req.params.id > (products.length -1) || req.params.id < 0) {
res.statusCode = 404;
res.end('No product found for that ID');
}
products[req.params.id] = req.body;
res.send(req.body);
});
// curl -X DELETE http://localhost:3000/products/2
app.delete('/products/:id', function(req, res) {
if (req.params.id > (products.length - 1) || req.params.id < 0) {
req.statusCode = 404;
res.end('No product found for that ID');
}
products.splice(req.params.id, 1);
res.json(products);
});
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
它是如何工作的
REST 是一种应用架构模式,可以依赖 HTTP 请求来执行操作。这些操作通常采用服务器上常见的创建、读取、更新和删除方法的形式。
典型的 REST 接口至少会使用 HTTP GET 和 POST 方法 在服务器上执行这些动作。然而,在这个解决方案中,您实际上使用了 GET、PUT、POST 和 DELETE HTTP 动词来创建您的 API。您用来简单地从资源中检索数据的 GET 方法。POST 方法用于创建数据,而 PUT 方法用于更新现有数据。DELETE 方法将从数据存储中移除数据。
您从创建产品的数据存储开始,这是代码中包含三个条目的对象数组。一旦你的服务器开始运行,你就可以通过发送 HTTP GET 到http://localhost:3000/products .来获取这些数据
$ CURL -X GET http://localhost:3000/products
[
{
"name": "watch",
"description": "Tell time with this amazing watch",
"price": 30
},
{
"name": "sandals",
"description": "Walk in comfort with these sandals",
"price": 10
},
{
"name": "sunglasses",
"description": "Protect your eyes in style",
"price": 25
}
]
现在您已经创建并获取了数据,您可以向产品数据存储中插入新记录了。在 REST API 中,您只需向产品路线发送一个 POST 请求。如果成功,并且它必须至少包括产品的名称,它将返回您刚刚添加的项目:
$ CURL -X POST -d "name=flops&description=sandal%20things&price=12.00"http://localhost:3000/products
{
"name": "flops",
"description": "sandal things",
"price": "12.00"
}
您现在已经创建了一个新记录,但是您意识到您给产品起的名字不正确。要更新产品的名称,您需要向服务器发送一个 PUT 请求,但是您还必须知道您希望将它发送到的:id。在您的例子中,您知道这是数组中的第四项,所以:id 变成了“3”为了更新名称,您现在可以发送请求,该请求将使用数据存储中的更新值进行响应:
$ curl -X PUT -d "name=flip%20flops&description=sandals&price=12.00" http://localhost:3000/products/3
{
"name": "flip flops",
"description": "sandals",
"price": "12.00"
}
在创建和更新这个新记录后,您意识到产品“凉鞋”在您的产品系列中不再是必需的。因此,您可以通过向该项发送 DELETE HTTP 请求 来删除它,这将把它从数据存储中完全删除。这也将返回删除此项目后现在可用的产品列表:
$ curl -X DELETE http://localhost:3000/products/1
[
{
"name": "watch",
"description": "Tell time with this amazing watch",
"price": 30
},
{
"name": "sunglasses",
"description": "Protect your eyes in style",
"price": 25
},
{
"name": "flip flops",
"description": "sandals",
"price": "12.00"
}
]
9-7.和盖迪一起行动
问题
您希望利用 Node.js 的 Geddy 应用框架来构建您的产品。
解决办法
要开始使用 Geddy,首先需要通过 npm 安装该模块。如果你全局安装它,如清单 9-17 所示,你将可以从你机器上的任何目录访问应用生成器。
清单 9-17 。安装 Geddy
$ npm install –g geddy
清单 9-18 。在指定的目录中生成应用
$ geddy gen app geddyapp
$ cd geddyapp
清单 9-19 。运行应用
$ geddy
清单 9-20 。为应用生成一个新模型并运行应用
$ geddy gen scaffold products name:default description price
$ geddy
您可以通过在浏览器中导航到http://localhost:4000来查看正在运行的应用。
它是如何工作的
Geddy 通过安装一个有用的命令行工具来工作,该工具将允许您快速有效地生成应用和构建新功能。一旦安装了这个命令行工具,您就可以通过键入命令$ geddy gen app <appname>来构建您的第一个应用。这将在appname目录中为您生成一个新的应用。目录结构将如下所示:
appname
-app
-controllers
-helpers
-models
-views
-config
-lib
-log
-node_modules
-public
-css
-img
-js
-test
-controllers
-models
您可以看到,这搭建出了一个工作应用,它在应用中有默认的模型、视图、控制器和助手,允许在默认情况下测试您的应用。为了运行应用,您使用了命令$ geddy,这将启动服务器运行。使用包含在 config 目录中的 production.js 或 development.js 文件配置服务器。
清单 9-21 。配置文件
var config = {
detailedErrors: true
, debug: true
, hostname: null
, port: 4000
, model: {
defaultAdapter: 'memory'
}
, sessions: {
store: 'memory'
, key: 'sid'
, expiry: 14 * 24 * 60 * 60
}
};
module.exports = config;
这为 HTTP 服务器的标准特性(如端口和主机名)创建了几个配置选项,但是它添加了一些特性,如数据模型适配器,您会看到这些特性默认为内存中的。Geddy 非常健壮,因为除了内存选项之外,它还提供了一组通用的适配器用于存储。这些选项包括 PostgreSQL、MongoDB 和 Riak。一旦应用开始运行,您将会在您的控制台中看到对服务器的任何请求。
[Sat, 20 Jul 2013 19:47:42 GMT] 127.0.0.1 - - [Sat Jul 20 2013 15:47:42 GMT-0400 (Eastern Daylight Time)] "GET / 1.1" 200 2645 "-" "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1568.2 Safari/537.36"
接下来,您在应用中为产品模型生成了一个支架。这是通过命令$ geddy gen scaffold products name:default description price完成的。这将在每个app/models、app/views/和app/controllers目录中生成相应的 JavaScript 或 HTML 视图文件。您会注意到,您用属性“default”设置了产品模型的“name”参数。这将它设置为应用中的必填字段,当您查看产品模型文件时,这一点变得很明显。
清单 9-22 。产品模型文件
var Product = function () {
this.defineProperties({
name: {type: 'string', required: true},
description: {type: 'string'},
price: {type: 'string'}
});
};
exports.Product = Product;
Geddy 搭建了一个完整的创建、读取、更新和删除(CRUD)控制器。通过这个简单的命令,您现在可以控制应用可能需要的所有 CRUD 操作。控制器将指导应用存储、删除或重定向您的视图到您试图到达的模型的任何部分。
清单 9-23 。产品控制员
var Products = function () {
this.respondsWith = ['html', 'json', 'xml', 'js', 'txt'];
this.index = function (req, resp, params) {
var self = this;
geddy.model.Product.all(function(err, products) {
self.respond({params: params, products: products});
});
};
this.add = function (req, resp, params) {
this.respond({params: params});
};
this.create = function (req, resp, params) {
var self = this
, product = geddy.model.Product.create(params);
if (!product.isValid()) {
this.flash.error(product.errors);
this.redirect({action: 'add'});
}
else {
product.save(function(err, data) {
if (err) {
self.flash.error(err);
self.redirect({action: 'add'});
}
else {
self.redirect({controller: self.name});
}
});
}
};
this.show = function (req, resp, params) {
var self = this;
geddy.model.Product.first(params.id, function(err, product) {
if (!product) {
var err = new Error();
err.statusCode = 404;
self.error(err);
}
else {
self.respond({params: params, product: product.toObj()});
}
});
};
this.edit = function (req, resp, params) {
var self = this;
geddy.model.Product.first(params.id, function(err, product) {
if (!product) {
var err = new Error();
err.statusCode = 400;
self.error(err);
}
else {
self.respond({params: params, product: product});
}
});
};
this.update = function (req, resp, params) {
var self = this;
geddy.model.Product.first(params.id, function(err, product) {
product.updateProperties(params);
if (!product.isValid()) {
this.flash.error(product.errors);
this.redirect({action: 'edit'});
}
else {
product.save(function(err, data) {
if (err) {
self.flash.error(err);
self.redirect({action: 'edit'});
}
else {
self.redirect({controller: self.name});
}
});
}
});
};
this.destroy = function (req, resp, params) {
var self = this;
geddy.model.Product.remove(params.id, function(err) {
if (err) {
self.flash.error(err);
self.redirect({action: 'edit'});
}
else {
self.redirect({controller: self.name});
}
});
};
};
exports.Products = Products;
控制器执行 CRUD 操作,在本例中是在内存中,但是有了可用的适配器,您可以轻松地存储到 PostgreSQL 或 MongoDB 中以实现持久性。有几个对 self.redirect 的调用。这个函数将你的应用重定向到控制器中描述的视图。默认情况下,这些视图是用 EmbeddedJS (EJS)模板创建的,但是在生成应用时,您可以通过将它作为命令行参数传递来利用 Jade。
EmbeddedJS 基本上就是听起来那样;您可以在视图模板中直接嵌入 JavaScript,这将控制页面的最终布局。嵌入的 JavaScript 简单地放在标记中的'< % % >'标记内,当视图呈现时,它将被剥离、解析和执行。如果您检查添加产品的模板,您会看到清单 9-24 中显示的内容,以及嵌入子模板的能力。
清单 9-24 。EJS 模板:add.html.ejs
<div class="hero-unit">
<form id="product-form" class="form-horizontal" action="/products" method="POST">
<fieldset>
<legend>Create a new Product</legend>
<% if(params.errors) { %>
<div class="control-group">
<ul>
<% for(var err in params.errors) { %>
<li><%= params.errors[err]; %></li>
<% } %>
</ul>
</div>
<% } %>
<%- partial('form', {product: {}}) %>
<div class="form-actions">
<%- contentTag('input', 'Add', {type: 'submit', class: 'btn btn-primary'}) %>
</div>
</fieldset>
</form>
</div>
清单 9-25 。分部子模板 .html.ejs
<div class="control-group">
<label for="name" class="control-label">name</label>
<div class="controls">
<%- contentTag('input', product.name, {type:'text', class:'span6', name:'name'}) %>
</div>
</div>
<div class="control-group">
<label for="description" class="control-label">description</label>
<div class="controls">
<%- contentTag('input', product.description, {type:'text', class:'span6', name:'description'}) %>
</div>
</div>
<div class="control-group">
<label for="price" class="control-label">price</label>
<div class="controls">
<%- contentTag('input', product.price, {type:'text', class:'span6', name:'price'}) %>
</div>
</div>
这里的 EJS 模板利用一个函数从产品模型中创建内容。例如,为了创建将成为产品名称的输入字段,EJS 看起来像<%- contentTag('input', product.name, {type:'text', class:'span6', name:'name'}) %>,它使用模型项的名称,并在表单被提交时将它分配回该模型。
除了在应用中自动生成这些 CRUD 方法的脚手架之外,您还可以使用$ geddy gen resource <resourcename>命令。resource命令不太固执己见,因为它不为模型上的每个操作创建特定的视图,控制器更一般化,如下所示:
var Products = function () {
this.respondsWith = ['html', 'json', 'xml', 'js', 'txt'];
this.index = function (req, resp, params) {
this.respond({params: params});
};
this.add = function (req, resp, params) {
this.respond({params: params});
};
this.create = function (req, resp, params) {
// Save the resource, then display index page
this.redirect({controller: this.name});
};
this.show = function (req, resp, params) {
this.respond({params: params});
};
this.edit = function (req, resp, params) {
this.respond({params: params});
};
this.update = function (req, resp, params) {
// Save the resource, then display the item page
this.redirect({controller: this.name, id: params.id});
};
this.destroy = function (req, resp, params) {
this.respond({params: params});
};
};
exports.Products = Products;
使用资源生成器可能更适合您的应用,因为它不那么固执己见,并且您可以轻松地添加您自己的特定于应用的逻辑,而无需使用scaffold命令中的 CRUD 样板文件。
最后,正如您在使用 Geddy 的测试中已经看到的,Geddy 提供了一个灵活的路由。该路由位于 config/router.js 文件中。它可以匹配路由,类似于 9-4 节中的快速路由。这意味着,如果您有一个特定的产品,并且您知道该 ID,并且想要在该路由上执行特定的操作,您可以将它添加到 router.js 文件中作为router.match('products/:id', 'GET').to(products.handleId);这将把特定的 ID 路由到products.handleId控制器方法。您不需要在router.match查询中指定 HTTP 动词,事实上,您可以将它写成router.get('products/:id').to(products.handleId);,它会以同样的方式执行。在此解决方案中,您利用了完全基于资源的路由,因此您的路由将显示如下:
var router = new geddy.RegExpRouter();
router.get('/').to('Main.index');
router.resource('users');
router.resource('products');
exports.router = router;
您可以使用 Geddy 快速启动并运行应用。这个框架对于启动一个简单的 CRUD 应用来说非常快,并且仍然允许您的 Node.js 应用的更有创造性的实现。
9-8.使用雅虎!莫吉托(鸡尾酒的一种)
问题
您希望通过使用 Yahoo!Mojito 框架。
解决办法
雅虎!Mojito 是另一个 Node.js 应用开发框架。由雅虎创建!,它允许您“使用 Node.js 构建在客户机和服务器上运行的高性能、独立于设备的 HTML5 应用。”
开始使用 Yahoo!Mojito 你首先必须安装命令行界面。这是通过国家预防机制完成的。从那时起,你可以利用命令行界面来构建你的应用,如清单 9-26 所示。
清单 9-26 。安装雅虎!Mojito 和创建应用
$ npm install –g mojito-cli
$ mojito create app mojitoapp
$cd mojitoapp
$ mojito create mojit testmojit
$ mojito test
$ mojito start
http://localhost:8666/@testmojit/index
它是如何工作的
当您通过 npm 安装 Mojito 时,您是在全局范围内这样做的,以获得对整个机器的命令行界面的访问。然后通过键入$ mojito create app mojitoapp创建一个应用。这将创建一个名为“mojitoapp”的目录,其结构如下。
mojitoapp
- artifacts
- assets
- mojits
- node_modules
application.json
package.json
routes.json
server.js
server.js 文件控制 Mojito 应用,并在您运行mojito start时启动。在这个文件中,您可以看到似乎是 Node.js HTTP 服务器的 Mojito 版本。
/*jslint anon:true, sloppy:true, nomen:true*/
process.chdir(__dirname);
/*
* Create the MojitoServer instance we'll interact with. Options can be passed
* using an object with the desired key/value pairs.
*/
var Mojito = require('mojito');
var app = Mojito.createServer();
// ---------------------------------------------------------------------------
// Different hosting environments require different approaches to starting the
// server. Adjust below to match the requirements of your hosting environment.
// ---------------------------------------------------------------------------
module.exports = app.listen();
从这一点上来说,你创造了一个“魔咒”“mojit”是一个 Mojito 术语,表示名称“模块”和“小部件”的混搭。这意味着当你创建一个 mojit 时,你可以把它看作是构建一个模块。默认情况下,Mojito 应用中的 mojit 可以通过路径http://localhost:8666/@mojitname/index访问。当您在应用中导航到这个位置时,您将看到默认的 mojit 页面,它是 Mojito 模型-视图-控制器(MVC)框架的一部分。
Mojito 中的 MVC 架构以所谓的“动作上下文”为中心,你会在代码中的大多数地方看到“ac”。当您查看 controller.server.js 文件的源代码时,每个 mojit 的操作上下文变得很明显。这个文件向应用注册 mojit,并控制模型视图的行为。
/*jslint anon:true, sloppy:true, nomen:true*/
YUI.add('testmojit', function(Y, NAME) {
/**
* The testmojit module.
*
* @module testmojit
*/
/**
* Constructor for the Controller class.
*
* @class Controller
* @constructor
*/
Y.namespace('mojito.controllers')[NAME] = {
/**
* Method corresponding to the 'index' action.
*
* @param ac {Object} The ActionContext that provides access
* to the Mojito API.
*/
index: function(ac) {
ac.models.get('testmojitModel').getData(function(err, data) {
if (err) {
ac.error(err);
return;
}
ac.assets.addCss('./index.css');
ac.done({
status: 'Mojito is working.',
data: data
});
});
}
};
}, '0.0.1', {requires: ['mojito', 'mojito-assets-addon', 'mojito-models-addon', 'testmojitModel']});
在这个 mojit 的索引处理程序中提供了Y.namespace回调中的动作上下文。它将使用合适的型号ac.models.get('testmojitModel')。。。然后添加资产并通过ac.done()处理器发送数据。
模型服务器位于每个 mojit 的模型目录中,您可以看到它遵循类似的 Yahoo!用户界面(YUI ) 模式生成模型并通过Y.namespace将其添加到应用中。这也是您不仅用一个配置初始化模型,而且然后添加诸如getData之类的方法的地方。
/*jslint anon:true, sloppy:true, nomen:true*/
YUI.add('testmojitModel', function(Y, NAME) {
/**
* The testmojitModel module.
*
* @module testmojit
*/
/**
* Constructor for the testmojitModel class.
*
* @class testmojitModel
* @constructor
*/
Y.namespace('mojito.models')[NAME] = {
init: function(config) {
this.config = config;
},
/**
* Method that will be invoked by the mojit controller to obtain data.
*
* @param callback {function(err,data)} The callback function to call when the
* data has been retrieved.
*/
getData: function(callback) {
callback(null, { some: 'data', even: 'more data' });
}
};
}, '0.0.1', {requires: []});
在您了解这些视图如何与 Mojito 一起工作之前,您应该首先理解您并不局限于将您的 URL 作为默认的.../@mojitname/index路径。这些 URL 在你的应用中是没问题的,但是如果用户需要记住这些 URL,那就不是很好的体验了。有一种方法可以让这些 URL 看起来更干净、更友好。
首先,您需要通过将名称添加到应用根目录中的 application.json 文件来命名您的新端点。在这个文件中,您将一个规范命名为“test”,并将其指向您之前创建的“testmojit”类型。然后,您需要在 routes.json 文件中命名这个新路由。这是通过命名“test index”并告诉“/”路径上的 HTTP GET 方法解析。testmojit mojit 的索引处理程序。这些文件显示在以下示例中。
清单 9-27 。应用. json
[
{
"settings": [ "master" ],
"appPort": "8666",
"specs": {
"test": {
"type": "testmojit"
}
}
},
{
"settings": [ "environment:development" ],
"staticHandling": {
"forceUpdate": true
}
}
]
清单 9-28 。Routes.json
[{
"settings": [ "master" ],
"test index": {
"verbs": ["get"],
"path": "/",
"call": "test.index"
}
//^^ convert http://localhost:8666/@testmojit/index to http://localhost:8666/
}]
$ mojito start
http://localhost:8666/
现在,您已经在您想要的 URL 上提供了您的 mojit,您可以修改这些模板了。Mojito 的模板默认使用手柄模板语言。Handlebars 允许您使用简单的表达式从模型中插入对象,就像您在 testmojit 模板中看到的那样。
清单 9-29 。用车把做模板
<div id=" {{mojit_view_id}} ">
<dl>
<dt>status</dt>
<dd id="dd_status"> {{status}} </dd>
<dt>data</dt>
<dd id="dd_data">
<b>some:</b> {{#data}}{{some}}{{/data}}
<span>event:</b> {{#data}}{{even}}{{/data}}
</dd>
</dl>
</div>
使用 Yahoo!构建 Node.js 应用 Mojito 应用框架可以支持多功能的 MVC 应用。
9-9.构建熨斗应用
问题
您希望通过利用 Flatiron 应用框架来构建 Node.js 服务器。
解决办法
要开始使用 Flatiron,就像你在本章中看到的许多其他框架一样,你需要安装这个框架,这样做可以全局地允许对命令行界面的通用访问。命令行界面,正如你在清单 9-30 中看到的,允许你快速搭建一个 Flatiron 应用。
清单 9-30 。安装熨斗和生成应用
$ npm install -g flatiron
$ flatiron create flatironapp
一旦创建了应用,就可以从生成的目录$ cd flatironapp中安装依赖项。然后,为了使用您的 Flatiron 应用,您只需启动 app.js 文件。
$ npm install
$ node app.js # starts your app on localhost:3000
它是如何工作的
当您第一次用 Flatiron 生成一个应用时,您利用了创建该应用的cli/create.js文件。这将提示您输入作者和您要创建的应用的描述。
$ flatiron create flatironapp
info: Creating application flatironapp
info: Using http scaffold.
prompt: author: cgack
prompt: description: test application
test application
prompt: homepage:
info: Creating directory config
info: Creating directory lib
info: Creating directory test
info: Writing package.json
info: Writing file app.js
info: Writing file config/config.json
info: Application flatiron is now ready
提供这些信息后,您现在已经生成了 app.js 文件、配置文件和 package.json 文件。package.json 文件包含运行应用所需的依赖项。这意味着您需要做的就是导航到您的应用目录并安装 npm 的依赖项。这将使您能够访问主 Flatiron 应用,默认情况下,它将使用 HTTP 插件 flatiron.plugins.http。
var flatiron = require('flatiron'),
path = require('path'),
app = flatiron.app;
app.config.file({ file: path.join(__dirname, 'config', 'config.json') });
app.use(flatiron.plugins.http);
app.router.get('/', function () {
this.res.json({ 'hello': 'world' })
});
app.start(3000);
这个应用内置了一个路由,它使用语法app.router.VERB('/path', callback);来提供路由和这些路由的处理程序。在 Flatiron 应用中,您的视图没有默认的模板语言,但是文档建议您可以使用语言“Plates–NPM install Plates–save-dev”来创建简单的、与模板语言无关的模板。这些模板只在唯一的 HTML ID 属性之间使用绑定,看起来与这里显示的类似。
var flatiron = require('flatiron'),
path = require('path'),
plates = require('plates'),
app = flatiron.app;
app.config.file({ file: path.join(__dirname, 'config', 'config.json') });
app.use(flatiron.plugins.http);
app.router.get('/', function () {
this.res.json({ 'hello': 'world' })
});
app.router.get('/test', function() {
var html = '<div id="bind"></div>';
var data = { "bind": "Bound data" };
var output = plates.bind(html, data);
this.res.end(output);
});
app.start(3000);
Flatiron 具有类似于 Express 框架的开放性和 API 语法。它可以很快上手并运行,您应该会发现它是灵活的和可扩展的。像你在本章中看到的许多框架一样,它提供了一种构建 Node.js web 服务器应用的方式,这种方式允许快速生成和灵活的可伸缩性。