单页应用的服务器则完全不同。大多数的业务逻辑(以及所有的HTML模板和展示逻辑)都移到了客户端。服务器仍旧很重要,但它的服务变得更精简和更专注了,比如持久化数据存储、数据验证、用户认证和数据同步。
在整个开发过程中,从前端到后端我们都使用JSON和JavaScript。这能消除数据转换的开销,并能显着减少需要掌握的语言和开发环境的数量。从而开发、交付和维护的成本将大大降低,产品的质量也变得更好。
希望使用Node.js来搭建完整的产品级应用,推荐阅读《Node.js Action》
7.1 服务器的作用
单页应用Web服务器最常见的职责是认证与授权、数据验证和数据的存储与同步。
认证和授权:认证是确定某人是他所说的身份的过程。越来越多的开发人员开始转向第三方认证服务,比如由Facebook和Yahoo提供的服务。对用户的好处是他们可以重复使用已经烂熟于心的用户名和密码。对开发人员的好处是可以把大部分单调乏味的实现细节“外包”出去,并能访问第三方的用户群体。
授权是确保只有能访问数据的人和系统才能接收数据。可以给用户绑定权限来完成这个功能,这样当用户登入的时候,就会有一份什么是他们允许看到的记录。授权的附带好处是,因为它只发送授权用户看到的数据,这会使得发送给客户端的数据量减至最小,事务处理可能更加快速。
验证:质量控制过程,确保只有正确和合理的数据才能被保存。有助于防止保存错误数据,防止错误数据传播给其他用户或者系统。比如,当用户挑选航班日期购买机票的时候,航空公司会进行验证,用户选择的是在未来有座位的日期。没有这一验证的话,航空公司的航班预定就会超额、预订了不存在的航班、或者是预订了已经起飞的航班。
客户端和服务端都进行验证是很重要的:客户端验证是为了快速响应,服务端验证是因为永远不能相信来自客户端的代码是有效的。各种各样的问题会导致服务器接收无效的数据。
- 编程错误会破坏或者遗漏单页应用的客户端验证。
- 个别客户端可能会缺少验证,有Web服务器的应用经常有多个客户端访问同台服务器。
- 曾经有效的选项在提交数据的时候可能会失效(比方说,用户刚刚点击提交按钮时,座位被其他人预订了)。
- 恶意的黑客又会露面并尝试劫持或者破坏网站,用脏数据填满数据存储。
不正确的服务端验证的经典案例是SQL注入攻击,这曾使很多著名的组织机构陷入窘境,实际上它们早该明白的。我们不想和它们一样,是吧?
数据的保存和同步:尽管单页应用可以把数据保存在客户端,但数据是临时的,可以很容易地修改或者删除,不受单页应用的控制。在大多数情况下,客户端应该只用于临时存储,服务器负责持久存储。
数据也需要在多个客户端之间进行同步,比如用户的在线状态需要在查看他们主页的每个人之间进行共享。完成这个功能的最简单方法是让客户端把状态发给服务器,让服务器保存这个状态,然后把这个状态广播给所有已认证的客户端。同步的数据也可以是临时的,比如,当我们使用聊天服务器把消息分发给已认证的客户端时:尽管服务器不会保存数据,但是它肩负着把消息发送给正确的已认证客户端的关键任务。
7.2 Node.js
HTTP服务器和应用一起编写,能够很容易地完成一些在HTTP服务器和应用组件分离的平台上面难以完成的任务。比如,如果想把日志写入内存数据库,不需要担心HTTP服务器停止和应用服务器启动的情形,就可以直接写入。
为什么选择Node.js:它有能力表明它是主流单页应用的不错选择。
- 服务器就是应用。结果是无须担心搭建单独的应用服务器并与之交互。所有的东西都在一个地方由一个进程进行控制。
- 服务器应用的语言是JavaScript,意味可以消除使用一种语言编写服务器应用、使用另外一种语言编写单页应用的认知负荷。也意味着可以在客户端和服务端之间共享代码,这有很多好处。比如,可以在单页应用和服务端上使用相同的数据验证库。
- Node.js是非阻塞和事件驱动的。简单地说,这意味着在一般硬件上的单个Node.js实例,可以开启数万或数十万的并发连接,比如用于实时消息传输的连接,这经常是主流单页应用非常希望具备的功能。
- Node.js很快,得到了很好的支持,功能模块和开发人员的数据都在迅速壮大。
Node.js处理网络请求的方式不同于其他大多数服务器平台。大多数HTTP服务器需要维护进程或者线程池,准备为到达的请求提供服务。相比之下,Node.js只有一个事件队列,会处理每个到达的请求,甚至在主事件队列中,会把请求的部分处理切分成单独的事件。在实际应用中这意味着,Node.js不用等长时间的事件完成之后才去处理其他事件。如果一个特别的数据库查询要花费很长的时间,Node.js会直接去处理其他事件。当完成数据库查询的时候,会在队列中放入一个事件,这样控制程序就可以使用该结果了。
使用Node.js创建“Hello world”应用:使用Node.js社区开发并测试过的框架。第一个要考虑的框架是Connect。
webapp/app.js
/*jslint browser : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : true,
white : true
*/
/*global */
var http, server;
http = require( 'http' );
server = http.createServer( function ( request, response ) {
var response_text = request.url === '/test' ? 'you have hit the test page' : 'Hello World';
console.log( request );
response.writeHead( 200, { 'Content-Type' : 'text/plain' } );
response.end(response_text);
}).listen( 3000 );
console.log( 'Listening on port %d', server.address().port );
node app.js
安装并使用Connect:一个可扩展的中间件框架,它向Node.js Web服务器添加基础功能,像是基本认证、会话管理、静态文件服务和表单处理。
npm install connect
我们定义了第一个中间件函数connectHello,然后告诉Connect对象app,使用该方法作为它的唯一中间件函数。它调用了response.end方法,所以它会结束服务器响应。
/*jslint browser : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : true,
white : true
*/
/*global */
var http = require( 'http' );
var connect = require( 'connect' );
var connectHello = function ( request, response, next ) {
response.setHeader( 'content-length', bodyText.length );
response.end( bodyText );
};
var bodyText = 'Hello Connect';
var app = connect();
app.use( connectHello);
server = http.createServer( app );
server.listen( 3000 );
console.log( 'Listening on port %d', server.address().port );
添加Connect中间件:记录日志的中间件函数connect.logger()
尽管Connect的抽象层次比Node.js更高,但是我们想要更多的功能。该升级到Express了。
安装并使用Express:Express是一个轻量级的Web框架。在单页应用中,不需要充分利用Express提供的每个功能,但它提供了比Connect更加丰富的功能集,事实上,它是构建在Connect之上的。
package.json
{
"name": "SPA",
"version": "0.0.3",
"private": true,
"dependencies": {
"express": "3.2.x"
}
}
npm install
app.js
'use strict';
var http = require( 'http' );
var express = require('express');
var app = express();
var server = http.createServer( app );
app.get( '/', function ( request, response ) {
response.send( 'Hello Express' );
});
server.listen( 3000 );
console.log( 'Express server listening on port %d in %s mode',
server.address().port,
app.settings.env
);
添加Express中间件:app.use( express.logger() );
或者使用app.configure( )
app.configure( function () {
app.use( express.logger() );
app.use( express.bodyParser() );
app.use( express.methodOverride() );
});
Express的使用环境:环境设置有development、testing、staging和production。Express会读取NODE_ENV环境变量,确定正在使用的是哪个环境,然后会相应地设置它的配置。
$ NODE_ENV=production node app.js
app.configure( function () {
app.use( express.bodyParser() );
app.use( express.methodOverride() );
});
//development环境
app.configure( 'development', function () {
app.use( express.logger() );
app.use( express.errorHandler({
dumpExceptions : true,
showStack : true
}) );
});
//对于production环境
app.configure( 'production', function () {
app.use( express.errorHandler() );
});
Express的静态文件服务:将第6章的内容复制到public下app.js node_modules package.json package-lock.json public/spa.html
app.configure( function () {
app.use( express.bodyParser() );
app.use( express.methodOverride() );
app.use( express.static( __dirname + '/public' )); //添加路由中间件
app.use( app.router );
});
app.get( '/', function ( request, response ) {
response.redirect( '/spa.html' ); //重定向
});
7.3 高级路由
用户对象的CRUD路由:CRUD操作(Create, Read, Update, Delete),是持久存储数据经常需要的主要操作。实现CRUD的常见设计模式称为REST。REST使用严格和定义明确的语义来定义动词GET、POST、PUT、PATCH和DELETE做什么事情。如果你知道并喜欢REST,就务必要实现它,它是在分布式系统之间交换数据的完美有效的方法,Node.js甚至有很多模块想使用REST来解决问题。
1、添加获取用户列表的路由:GET /user/list
app.get( '/usr/list', function ( request, response ) {
response.contentType( 'json' );
response.send({
title : 'user list'
});
});
2、添加创建用户对象的路由:POST /user/create
app.post( '/user/create', function ( request, response ) {
response.contentType( 'json' );
response.send({
title: 'user created'
});
});
curl http://localhost:3000/user/create -d {}
或 wget http://localhost:3000/user/create --post-data='{}' -O -
3、添加读取用户对象的路由:GET /user/read/:id
app.get( 'user/read/:id([0-9]+)', function ( request, response ) {
response.contenType( 'json' );
response.send( {
title: 'user with id ' + request.params.id + ' found'
});
});
curl http://localhost:3000/user/read/12
4、添加更新或者删除用户的路由:
app.get( 'user/update/:id([0-9]+)', function ( request, response ) {
response.contenType( 'json' );
response.send( {
title: 'user with id ' + request.params.id + ' updated'
});
});
app.get( 'user/delete/:id([0-9]+)', function ( request, response ) {
response.contenType( 'json' );
response.send( {
title: 'user with id ' + request.params.id + ' deleted'
});
});
app.all('/user/*?', function () {} ); //接收所有数据
通用CRUD路由:使用obj_type
app.get( '/', function ( request, response ) {
response.redirect( '/spa.html' );
});
app.get( '/:obj_type/*?', function ( request, response ) {
response.contentType( 'json' );
next();
});
app.get( '/:obj_type/list', function ( request, response ) {
response.contentType( 'json' );
response.send({
title : request.params.obj_type +' list'
});
});
app.post( '/:obj_type/create', function ( request, response ) {
response.contentType( 'json' );
response.send({
title: request.params.obj_type +'created'
});
});
app.get( '/:obj_type/read/:id([0-9]+)', function ( request, response ) {
response.contenType( 'json' );
response.send( {
title: request.params.obj_type +' with id ' + request.params.id + ' found'
});
});
app.get( '/:obj_type/update/:id([0-9]+)', function ( request, response ) {
response.contenType( 'json' );
response.send( {
title: request.params.obj_type + ' with id ' + request.params.id + ' updated'
});
});
app.get( '/:obj_type/delete/:id([0-9]+)', function ( request, response ) {
response.contenType( 'json' );
response.send( {
title: request.params.obj_type + ' with id ' + request.params.id + ' deleted'
});
});
把所有的CRUD请求放到像/api/1.0.0/这样的根名字下,这样动态内容和静态内容就被很好地分开了。
把路由放到单独的Node.js模块里面:routes.js
7.4 添加认证和授权
基本认证:HTTP/1.0和1.1标准中,定义当客户端发送请求的时候该如何提供用户名和密码,它通常被称为basic auth。在添加路由中间件之前添加其他中间件。
app.use( express.basicAuth( 'user', 'sap' ));
app.use( express.static( __dirname + '/public' ) );
在产品级应用中,不推荐使用基本认证。每个请求它都会发送纯文本的认证信息,安全专家称之为广泛攻击。即使使用SSL(HTTPS)对传输进行加密,在客户端和服务器之间也只有一层安全保证。
现在使用自己的认证机制显得过时了,很多创业公司乃至更加知名的公司都在使用来自像Facebook或者Google的第三方认证。有很多在线指南演示了如何集成这些服务,可以先从Node.js的Passport中间件入手。
7.5 Web socket和Socket.IO
Web socket是一项令人激动的技术,得到了浏览器的普遍支持。Web socket允许客户端和服务器保持持久、轻量和双向的通信信道,而不是单一的TCP连接。这让客户端或者服务器能够实时地推送消息,没有HTTP“请求-响应”周期的开销和延时。在Web socket技术出现之前,开发人员采取替代的(但效率较低)技术来提供类似的功能。这些技术包括使用Flash socket;长轮询(long-pulling),浏览器向服务器发送请求,然后当响应返回或者请求超时的时候,又重新发起请求;以及以很小的时间间隔(比如,每隔一秒)轮询服务器。
Web socket的问题是规范还没有最终定下来,旧浏览器也永远不会支持这个功能。Socket.IO是一个Node.js模块,它优雅地解决了。
简单的Socket.IO应用程序: npm install socket.io@0.9.x
socket.js
'use strict';
var countUp,
http = require( 'http' ),
express = require( 'express' ),
socketIo = require( 'socket.io' ),
app = express(),
server = http.createServer( app ),
io = socketIo.listen( server ),
countIdx = 0
;
countUp = function () {
countIdx++;
console.log( countIdx );
io.sockets.send( countIdx );
};
app.configure( function () {
app.use( express.static( __dirname + '/' ) );
});
app.get( '/', function (request, response ) {
response.redirect( '/socket.html' );
});
server.listen( 3000 );
console.log(
'Express server listening on port %d in %s mode',
server.address().port, app.settings.env
);
//每隔1000ms调用一次countUp
setInterval( countUp, 1000 );
socket.html
<!doctype html>
<!-- socket.html - simple socket example -->
<html>
<head>
<script src="public/js/jq/jquery-1.9.1.js" ></script>
<script src="/socket.io/socket.io.js"></script>
<script>
io.connect().on('message', function (count) {
$('body').html( count );
});
</script>
</head>
<body>
Loading...
</body>
</html>
node socket.js
Socket.IO 和消息服务器:Socket.IO在客户端和服务器之间创建一个消息服务器。而Apache2,是比较弱的消息服务器,因为它们会为每个连接创建和分配一个进程,会消耗掉服务器资源。它是为内容服务器而编写的,它的理念是在响应请求时,尽可能快地把数据推送出去,然后尽可能快地关闭连接。对于这些用途类型,Apache2是非常棒的选择,只要问问YouTube就知道了。
相比之下,Node.js是一个非常出色的消息服务器。它是事件模型,不会为每个连接创建一个进程。因此在一般的硬件上,它能够处理几万甚至几十万的并发连接。
在数据量很大的环境中,负载均衡会在提供消息通信的Node.js服务器集群之间、提供动态Web内容的Node.js服务器集群之间和提供静态内容的Apache2服务器集群之间“路由”流量。
使用Socket.IO更新JavaScript:
socket.js
/*
* app.js - Express server static files
*/
/*jslint node : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : false,
white : true
*/
/*global */
'use strict';
var countUp,
http = require( 'http' ),
express = require( 'express' ),
socketIo = require( 'socket.io' ),
fsHandle = require( 'fs' ),
app = express(),
server = http.createServer( app ),
io = socketIo.listen( server ),
watchMap = {}
;
var setWatch = function ( url_path, file_type ) {
console.log( 'setWatch called on ' + url_path );
if (!watchMap[url_path]) {
console.log('setting watch on ' + url_path );
//监听文件的变化
fsHandle.watchFile(
url_path.slice(1), //
function ( current, previous ) {
//console.log( 'file accessed' );
if (current.mtime !== previous.mtime ) {
console.log( 'file changed' );
io.sockets.emit( file_type, url_path ); //
}
}
);
watchMap[ url_path ] = true;
}
};
app.configure( function () {
app.use( function ( request, response, next ) {
if (request.url.indexOf( '/js/' ) >= 0 ) {
setWatch( request.url, 'script' );
} else if ( request.url.indexOf( '/css/' ) >= 0 ) {
setWatch( request.url, 'stylesheet' );
}
next();
});
app.use( express.static( __dirname + '/' ) );
});
app.get( '/', function (request, response ) {
response.redirect( '/socket.html' );
});
server.listen( 3000 );
console.log(
'Express server listening on port %d in %s mode',
server.address().port, app.settings.env
);
socket.html
<!doctype html>
<!-- socket.html - simple socket example -->
<html>
<head>
<script src="public/js/jq/jquery-1.9.1.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script id="script_a" src="/js/data.js"></script>
<script>
$(function () {
$('body').html(b);
});
io.connect('http://localhost').on('script', function ( path ) {
$( '#script_a' ).remove();
$( 'head' ).append(
'<script id="script_a" src="'
+ path +
'"></scr' + 'ipt>'
);
$('body').html( b );
});
</script>
</head>
<body>
Loading...
</body>
</html>
js/data.js
var b = "hello";