突袭HTML5之WebSocket入门4 - 开发框架express
为什么需要express?
express是node.js的卓越MVC框架。它采用各种中间件,提供了相当易用的功能,比如路由,会话,配置,页面模板,响应,重定向,视图等。
安装
在命令行中执行:npm install express 即可安装到当前目录下。如果要在命令行中直接使用express,则执行:npm install -g express。
注意:在Windows环境下(XP, Win7都试过),目前的版本(2.5.5)不能直接用express生成简单的程序框架。而在linux系统中,直接使用express去创建程序框架吧。
框架结构
使用express创建的框架基本结构如下,在Windows下面自己写也不是太麻烦:
当前目录:
|--app.js 服务器程序|--router.json 路由配置:关联路径,视图和内容
|--views目录:存放视图模板
|--layout.jade 通用模板
|--index.jade 首页模板
|--404.jade 错误页面模板
|--public目录:存放静态资源
|--js目录:存放js文件
|--.js文件
|--css目录:存放css文件
|--.css文件
|--images目录:存放图片
|--.png等资源文件
这里比较特别的是jade文件,这个是生成页面的模板文件,需要靠渲染引擎渲染生成页面。express支持许多模板引擎,常用的有:
- Haml haml 的实现
- Jade haml.js 接替者,同时也是express的默认模板引擎
- JS 嵌入JavaScript模板
- CoffeeKup 基于CoffeeScript的模板引擎
- jQuery Templates 的NodeJS版本
视图(也就是模板文件)的文件名默认需遵循“<name>.<engine>”的形式,这里<engine>是要被加载的渲染模块的名字。例如下面使用Jade引擎来渲染index.html,默认情况下由于没有设置layout:false,index.jade渲染后的内容将被作为body本地变量传入layout.jade。
res.render('index.jade', { title: 'Express' });
});
新版本中新增的view engine设置可以指定默认模板引擎,如果我们想使用jade可以这样设置:
这样设置以后,渲染模板的时候就不用指定文件扩展名了,也就是说res.render('index')等价于res.render('index.jade')。
程序结构
使用express开发比较简单,基本过程就是启动服务器,配置运行环境,配置中间件。就比如下面这个常见的server.js:
app = express.createServer();
//配置中间件以及环境
app.configure(function(){
app.set('views', __dirname + '/views'); //设置模板路径,比如index.jade
app.set('view engine', 'jade'); //配置模板解析引擎
app.use(express.bodyParser()); //将client提交过来的post请求放入request.body中
app.use(express.methodOverride()); //伪装PUT,DELETE请求
app.use(express.static(__dirname + '/public')); //设置静态文件路径
app.use(app.router); //设置路由中间件,可有可无
});
//配置调试环境,需要显示异常
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
//配置生产环境
app.configure('production', function(){
app.use(express.errorHandler());
});
//路由
app.get('/', function(req, res){
res.render('index',{title: 'Test Page'});
});
app.listen(3000);
注意:
1. 配置路中间件以及环境那几句顺序是不能随便动的,比如app.use(express.bodyParser())一定要在app.use(express.methodOverride())前面,因为后者需要利用request.body,这个是前者设置完才有的。如果不是太清楚依赖关系,可以看看官方文档。
2. express支持多工作环境,比如生产环境和开发环境等。开发者可以使用configure()方法根据当前环境的需要进行设置,当configure()没有传入环境名称时,它会在各环境之前被调用。一般情况下,不同的工作环境主要是异常显示信息不一样,比如开发环境下,是需要显示异常信息的。
Session的支持
大多数的功能参考官方文档就可以了,这里重点说一下会话session,这个用的比较多。下面的描述出自官方文档:
可以在express中通过增加connect的session中间件来开启session支持,当然前提是需要在这之前使用cookieParser中间件,用于分析和处理req.cookies的cookie数据(session利用cookie进行通信)。
app.use(express.session({ secret: "secret" }));
app.use(express.cookieParser());
app.use(express.session({ secret: "secret", store: new RedisStore }));
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({ secret: "keyboard cat", store: new RedisStore }));
app.post('/add-to-cart', function(req, res){
// 利用bodyParser()中间件处理POST提交的表单数据
var items = req.body.items;
req.session.items = items;
res.redirect('back');
});
app.get('/add-to-cart', function(req, res){
// 当页面回到返回并通过GET请求/add-to-cart 时
// 我们可以检查req.session.items && req.session.items.length,然后将信息打印到页面
if (req.session.items && req.session.items.length) {
req.flash('info', 'You have %s items in your cart', req.session.items.length);
}
res.render('shopping-cart');
});
但是通常我们都是采用socket.io开发的,它并不是路由中间件的下级中间件,这种情况下,如何让socket知道session的存在呢?当socket连接上的时候socket.io并不知道当前连接的socket的sessionID是多少。那如何获得这个信息呢?毕竟对很多应用来说,session信息还是很重要的。从socket.io版本0.7以后的版本,这个信息都可以通过“握手/授权”机制获得,这个机制放到下面说。使用握手数据的例子如下:
sio = require('socket.io'),
parseCookie = require('./node_modules/express/node_modules/connect').utils.parseCookie,
MemoryStore = require('./node_modules/express/node_modules/connect/lib/middleware/session/memory'),
storeMemory = new MemoryStore(),
app = express.createServer();
app.configure(function(){
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({
secret: 'secret',
store:storeMemory
}));
app.use(express.methodOverride());
app.use(app.router);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.static(__dirname + '/public'));
});
var io = sio.listen(app);
io.set('authorization', function(handshakeData, callback){
// 通过客户端的cookie字符串来获取其session数据
handshakeData.cookie = parseCookie(handshakeData.headers.cookie)
var connect_sid = handshakeData.cookie['connect.sid'];
if (connect_sid) {
storeMemory.get(connect_sid, function(error, session){
if (error) {
// if we cannot grab a session, turn down the connection
callback(error.message, false);
}
else {
// save the session data and accept the connection
handshakeData.session = session;
callback(null, true);
}
});
}
else {
callback('nosession');
}
});
app.get('/',function(req,res){
//...
});
io.sockets.on('connection', function (socket){
//取得session数据
var session = socket.handshake.session;
var name = session.name;
//...
socket.on('disconnect', function(){
//...
});
});
app.listen(8080);
socket.io握手/授权机制
握手过程
当客户端想要与socket.io服务器建立一个实时的持久化的连接时,它需要开始一个握手流程。握手以一个XHR或者JSONP请求(跨域请求)开始。
当服务端收到这个连接请求时,它开始从这个请求收集后面可能用到数据,这样做有很多原因:比如当给客户端授权时,是需要这些信息中的headers和IP数据的;还有并不是每次建立实时连接的请求都会携带headers数据,所以我们在内部存储handshake数据,这样我们能确保当客户端连上以后,能重用这些数据。例如,你可能想从cookie中读出session id信息并为连接上的socket初始化一个express session。
生成的handshakeData对象含有下面的数据:
, headers: req.headers // <Object> the headers of the request
, time: (new Date) +'' // <String> date time of the connection
, address: socket.address() // <Object> remoteAddress and remotePort object
, xdomain: !!headers.origin // <Boolean> was it a cross domain request?
, secure: socket.secure // <Boolean> https connection
, issued: +date // <Number> EPOCH of when the handshake was created
, path: request.url // <String> the entrance path of the request
, query: data.query // <Object> the result of url.parse().query or a empty object
}
当我们获取到这些信息后,我们检查程序是不是配置了全局授权函数。如果已经配置了的话,我们把这个handshakeData以及一个callback回调函数传给这个授权函数。当这个回调函数callback执行的时候,我们在内部存储handshakeData数据,这样这些数据就可以在连接建立后,通过socket.handshake属性来访问。基于callback回调函数的结果,我们可以发送403,500或者200响应。
Global授权
通过设置socket.io的authorization方法来启用Global授权。
io.set('authorization', function (handshakeData, callback) {
callback(null, true); // error first callback style
});
});
在上面的授权函数中,共有2个参数:
1.handshakeData:是我们在握手过程中产生的。
2.callback:它需要两个参数,第一个参数error可以用undefined或者String,来代表错误;第二个参数authorized是Boolean类型的,用于指示客户端是否被授权了。
发送一个error或者设置authorized为false都会导致客户端连不上服务器。
因为handshakeData是在授权后存储的,所以我们可以在授权方法中增加/删除一些数据。需要注意的是Global授权与Namespace授权是共享handshakeData的,所以在这两个授权方法任何一个中都可以修改了这个数据。
客户端如何响应全局授权
客户端监听两个事件:error和connect就可以了。
sio.socket.on('error', function (reason){
console.error('Unable to connect Socket.IO', reason);
});
sio.on('connect', function (){
console.info('successfully established a working connection \o/');
});
Namespace授权
我们也可以根据每个namespace进行授权,这样更灵活。通常这么做的原因是我们想在一次连接中,建立多条独立通信的通道,而不是建立多次连接。比如我们可以提供public性质的namespace给未注册用户来聊天,同时也可以提供只对注册用户开发的聊天工具。
所有的namespace都附带ahthorization方法。这个可以串成链的方法可以用于为namespace注册一个授权方法。这个方法的参数与global授权是一样的。(这方面的例子也可以参看前面介绍socket.io中namespace的内容)
io.of('/private').authorization(function (handshakeData, callback) {
console.dir(handshakeData);
handshakeData.foo = 'baz';
callback(null, true);
}).on('connection', function (socket) {
console.dir(socket.handshake.foo);
});
客户端如何响应Namespace授权
客户端需要监听connect_failed和connect事件就可以了。
, socket = sio.socket;
socket.of('/example')
.on('connect_failed', function (reason) {
console.error('unable to connect to namespace', reason);
})
.on('connect', function () {
console.info('sucessfully established a connection with the namespace');
});
桃园结义
到这里,WebSocket的JavaScript开发核心成员就聚齐了:node.js提供高效的服务器,socket.io提供统一的通信模型,express提供卓越的开发框架。融合使用它们,并搭配上其他的合适扩展,必将大幅提升开发效率。
实用参考
espress官方文档:http://expressjs.com/guide.html
express文档中文版:http://www.csser.com/tools/express-js/express-guide-reference-zh-CN.html
express入门教程http://moodle.hammonsenterprisesllc.com/1326267259/
安装express以及配置介绍:http://js8.in/774.html