zoukankan      html  css  js  c++  java
  • 突袭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

  • 相关阅读:
    Java WEB 之页面间传递特殊字符
    c++ using Handle Class Pattern to accomplish implementation hiding
    c++ simple class template example: Stack
    c++ why can't class template hide its implementation in cpp file?
    c++ what happens when a constructor throws an exception and leaves the object in an inconsistent state?
    c++ 用namespace实现java的package的功能
    c++ virtual 和 pure virtual的区别
    c++ istream(ostream)是如何转换为bool的
    c++ 使用boost regex库 总结
    c++ 如何使用第三方的library
  • 原文地址:https://www.cnblogs.com/dxy1982/p/2328025.html
Copyright © 2011-2022 走看看