单页Web应用 7 Web服务器

   单页应用的服务器则完全不同。大多数的业务逻辑(以及所有的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";    


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值