突袭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。

app.get('/', function(req, res){
    res.render('index.jade', { title: 'Express' });
});

    新版本中新增的view engine设置可以指定默认模板引擎,如果我们想使用jade可以这样设置:

app.set('view engine', 'jade');

    这样设置以后,渲染模板的时候就不用指定文件扩展名了,也就是说res.render('index')等价于res.render('index.jade')。

程序结构

    使用express开发比较简单,基本过程就是启动服务器,配置运行环境,配置中间件。就比如下面这个常见的server.js:

var express = require('express'),
    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.cookieParser());
app.use(express.session({ secret: "secret" }));
    默认session中间件使用connect绑定的内存存储,但也可以使用其他的实现方式。比如connect-redis就提供了一个Redis的session存储方案: 
var RedisStore = require('connect-redis');
app.use(express.cookieParser());
app.use(express.session({ secret: "secret", store: new RedisStore }));
    这样配置以后,req.session和req.sessionStore属性就可以被所有路由及下级中间件所访问,req.session的属性会伴随着每一次响应发送给客户端,下面是一个购物车的例子:
var RedisStore = require('connect-redis');
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以后的版本,这个信息都可以通过“握手/授权”机制获得,这个机制放到下面说。使用握手数据的例子如下:

var express = require('express'),
    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(nulltrue);
            }
        });
    }
    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.configure(function (){
  io.set('authorization', function (handshakeData, callback) {
    callback(nulltrue); // error first callback style 
  });
});

在上面的授权函数中,共有2个参数:
1.handshakeData:是我们在握手过程中产生的。
2.callback:它需要两个参数,第一个参数error可以用undefined或者String,来代表错误;第二个参数authorized是Boolean类型的,用于指示客户端是否被授权了。
    发送一个error或者设置authorized为false都会导致客户端连不上服务器。

    因为handshakeData是在授权后存储的,所以我们可以在授权方法中增加/删除一些数据。需要注意的是Global授权与Namespace授权是共享handshakeData的,所以在这两个授权方法任何一个中都可以修改了这个数据。

  客户端如何响应全局授权
    客户端监听两个事件:error和connect就可以了。

var sio = io.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的内容)

var io = require('socket.io').listen(80);

io.of('/private').authorization(function (handshakeData, callback) {
  console.dir(handshakeData);
  handshakeData.foo = 'baz';
  callback(nulltrue);
}).on('connection', function (socket) {
  console.dir(socket.handshake.foo);
});

  客户端如何响应Namespace授权
    客户端需要监听connect_failed和connect事件就可以了。 

var sio = io.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

模块学习手册:https://github.com/LearnBoost

posted @ 2012-02-03 11:10  沙场秋点兵  阅读(1703)  评论(0编辑  收藏  举报