代码改变世界

一个完整的基于Node.js web应用详解

2013-01-23 23:23 Owen Chen 阅读(...) 评论(...) 编辑 收藏

本博客停止更新,请访问新个人博客:owenchen.duapp.com

前言

这篇文章所使用的例子是基于《node.js开发指南》这本书中的例子和源代码来做的。express从2.x到3.x引入了非常大的变化,很多模块都被独立出去,并且调用的接口也发生了很大的变化,所以原有的代码在express3.x上是不能运行的。在尝试的过程中,做了很多迁移的工作,同时将一些模块进行了一定的分离和整合,使得整个项目更加具有结构性和可扩展性。

这里首先要推荐一下《node.js开发指南》这本书,想学习和使用Node.js的同学,这确实是一本非常全面的介绍Node.js的书籍,看完了之后,结合书中的例子代码,基本可以使用Node进行web应用的开发。

我这里主要结合这个例子,以自己的理解和对例子的修改讲述一下使用Node.js进行web开发的整个过程。 我就直接从代码开始讲,对于环境的搭建,npm包安装,模块引入等大家可以另外找一些文章,或者从《node.js开发指南》这本书的相应章节去了解。

目录结构:

首先贴出目录结构:

从每个文件和文件夹的名字上,相信大家能看出他们各自的功能。

  • main.js是整个应用的启动文件
  • settings.js中存放着系统的配置信息
  • server.js是系统服务配置和创建的地方
  • db.js是与数据库相关的内容
  • models模块中中存放着模型类如User,Post等,类似于Java中的Entity
  • routes中是系统页面跳转和请求分发处理的模块
  • views是系统展现给用户的页面
  • daos中分装了所有对数据库的操作,熟悉J2EE的同学应该都理解这一层做的事情
  • web中是一些静态元素,如html,js,css,images
  • package.json中定义了系统需要的其他的第三方模块,如express,ejs等
  • node_modules中则是存放通过npm安装的第三方包的地方

接下来会对这些部分分别做详细的介绍和分析。

基于express和ejs的MVC

在后台的各个模块中其实有一个基于express和ejs的MVC模式。对应的三个模块为:models(M)-views(V)-routes(C),具体会在后面讲解他们三者是如何工作的。

express

关于express,我在这里不做详细的介绍,大家可以上它的官网做比较详细的了解。简单的讲,就是一套在Node.js上创建web应用的框架。提供了包括服务创建、启动、会话、路由等接口和实现。一个最基本的基于express的服务代码如下:

var express = require('express');
var app = express(); //创建服务

app.get('/', function(req, res){ //路由所有的到根目录的请求
  res.send('hello world');
});

app.listen(3000); //启动服务,监听3000端口

ejs

ejs: ejs是一种基于js的模版技术,即通过在html片段中插入js代码。在发送到客户端之前现在服务器端进行解析处理,动态设置一些字段或者添加一些节点。JSP就是一种基于java的模版语言。

在最新的ejs中可以支持以html文件作为模版文件,并且引入了include机制,可以使用include语句来引入其他的页面内容。这点和jsp很像。

一个基于ejs的html文件可以写成如下:

<% include header.html %>
 <% if (!locals.user) { %>
  <div class="hero-unit">
    <h1>欢迎来到 Microblog</h1>
    <p>Microblog 是一个基于 Node.js 的微博系统。</p>
    <p>
      <a class="btn btn-primary btn-large" href="/login">登录</a>
      <a class="btn btn-large" href="/reg">立即注册</a>
    </p>
  </div>
<% } else { %>
 <% include say.html %>
<% } %>
<% include footer.html %>

在使用ejs的时候,可以在js代码中调用res.render('index',{user:"Owen"})方法。这个方法有两个参数,第一个为模版文件的名字,第二个为需要传入到模版中使用的参数。

exports.index = function(req, res) {
     res.render('index', {
        title: '首页',
        posts: posts
    });
};

在最新的ejs中,加入了作用域的概念,在模版文件中不能直接引用变量名来访问变量,而需要使用locals.xxx来访问相应的变量。这样做是为了避免全局变量的污染和冲突。

MVC

在这个MVC模式中主要用到了express的route功能和ejs的模版机制。

在models模块中定义一些模型模块如User,Post等,这些类似与java中的Pojo或者Entity类。定义了模型的一些属性和方法。这些属性与数据库的字段相对应。比如一个简单的User model的模块可以定义如下:

function User(user) {
    this.name = user.name;
    this.password = user.password;
};
module.exports = User;

routes中定义了请求分发处理的过程。比如到所有到根目录(/)的请求都经过一定的处理然后转发到index view中,到/login的请求应该返回login.html页面。这个与j2ee项目中的web.xml或者使用struts时的struts.xml类似。在node中,遵循代码即配置的原则,下面先看一下/routes/index.js模块中的code。

var crypto = require('crypto');
var User = require('../models/User');
var Post = require('../models/Post');
var user = require('./user');

var that = exports;

exports.index = function(req, res) {
    Post.get(null, function(err, posts) {
        if (err) {
            posts = [];
        }
        res.render('index', {
            title: '首页',
            posts: posts
        });
    });
};

exports.login = function(req, res) {
    res.render('login', {
        title: '用戶登入',
    });
};

module.exports = function(app) {
    app.get('/', that.index);

    app.get('/login', checkNotLogin);
    app.get('/login', that.login);

    app.post('/login', checkNotLogin);
    app.post('/login', that.doLogin);

    app.get('/reg', user.reg);
    ....
};

上面的代码片段定义了index函数用来处理所有的到根目录的请求,login函数处理到/login的get请求。 而到注册模块的(/reg)的请求,则被转发到user.reg方法中。在route/user模块中,我们可以定义跟user相关的一些处理函数,如:

var crypto = require('crypto');
var User = require('../models/User');
var UserDao = require('../daos/UserDao');
var PostDao = require('../dao/PostDao');

exports.view = function(req, res) {
    UserDao.get(req.params.user, function(err, user) {
        if (!user) {
            req.flash('error', '用户不存在');
            return res.redirect('/');
        }
        PostDao.get(user.name, function(err, posts) {
            if (err) {
                req.flash('error', err);
                return res.redirect('/');
            }
            res.render('user', {
                title: user.name,
                posts: posts,
            });
        });
    });
};

exports.reg = function(req, res) {
    res.render('reg', {
        title: '用户注册',
    });
};

通过将处理函数的拆分和封装,可以很好的让这些route模块起到类似java中的servlet或者service的作用。

Views中则是将要返回给客户端展示的内容,route中通过对model的处理,将处理结果或者model的内容通过ejs的方式植入到html页面中返回给客户端。

 routes->models->daos

routes作为请求接收分发的模块,在接收到请求之后调用models中的接口处理数据,在models中通过daos中的数据库操作接口完成对数据库数据的查询活更改。这样三个层次各自的职责以及之间的依赖关系就非常明确。拿注册过程举例,典型的调用如下:

routes/index.js
app.post('/login', checkNotLogin);
app.post('/login', user.doLogin);

routes/user.js
exports.doLogin = function(req, res) {
    //生成口令的散列值
    var md5 = crypto.createHash('md5');
    var password = md5.update(req.body.password).digest('base64');

    User.get(req.body.username, function(err, user) {
        if (!user) {
            req.flash('error', '用户不存在');
            return res.redirect('/login');
        }
        if (user.password != password) {
            req.flash('error', '密码错误');
            return res.redirect('/login');
        }
        req.session.user = user;
        req.flash('success', '登录成功');
        res.redirect('/');
    });
};

models/User.js
var UserDao = require('../daos/UserDao');

User.get = function get(username, callback) {
    UserDao.get(username, callback);
};

daos/UserDao.js
exports.get = function get(username, callback) {
  mongodb.open(function(err, db) {
    if (err) {
      return callback(err);
    }
    db.collection('users', function(err, collection) {
      if (err) {
        mongodb.close();
        return callback(err);
      }
      collection.findOne({name: username}, function(err, doc) {
        mongodb.close();
        if (doc) {
          var user = new User(doc);
          callback(err, user);
        } else {
          callback(err, null);
        }
      });
    });
  });
};

服务配置和启动

main.js是系统启动的文件,在终端使用:node main.js将启动整个web应用。 

var server = require('./server');
server.start();

我的这个文件中代码很简单,我将服务的配置和创建过程移到了server.js中:

var express = require('express');
var ejs = require('ejs');
var flash = require('connect-flash');
var MongoStore = require('connect-mongo')(express);
var settings = require('./settings');
var routes = require('./routes');

var app = express();
app.configure(function() {
    console.log(__dirname);
    app.set('views', __dirname + '/views');
    // app.set('view engine', 'ejs');
    app.engine('.html', ejs.__express);
    app.set('view engine', 'html');
    app.use(express.bodyParser());
    app.use(flash());
    app.use(express.methodOverride());
    app.use(express.cookieParser());
    app.use(express.session({
        secret: settings.cookieSecret,
        store: new MongoStore({
            db: settings.db
        })
    }));
    app.use(function(req, res, next) {
        res.locals.error = req.flash('error').toString();
        res.locals.success = req.flash('success').toString();
        res.locals.user = req.session ? req.session.user : null;
        next();
    });
    app.use(app.router);
    routes(app);
    app.use(express.static(__dirname + '/web'));
});

app.configure('development', function() {
    app.use(express.errorHandler({
        dumpExceptions: true,
        showStack: true
    }));
});

app.configure('production', function() {
    app.use(express.errorHandler());
});

exports.start = function() {
    app.listen(settings.port);
    console.log("Express server listening on port %d in %s mode", settings.port, app.settings.env);
}

这里重点介绍几个关键的地方:

app.engine('.html', ejs.__express);
app.set('view engine', 'html');

这两句设置让ejs将.html文件作为模版文件。

app.use(flash());

express3.0以后移除了req.flash()方法,所以只有通过使用'connect-flash'中间件模块才能继续使用req.flash()方法。

app.use(function(req, res, next) {
  res.locals.error = req.flash('error').toString();
  res.locals.success = req.flash('success').toString();
  res.locals.user = req.session ? req.session.user : null;
  next();
});

由于express3.0移除了app.dynamicHelper()接口,所以要继续使用类似的功能,可以使用如上的代码,将flash或者session中的内容动态绑定到locals局部对象上。这样在模版文件中可以直接使用locals.xxx的方式来访问到这些变量。

 

到这里项目中比较关键的代码部分都介绍完了,项目代码我放到了github上:https://github.com/owenXin/microblogByOwen

我本地win7环境运行正常(记得起mongodb哦 )。这是我第一次在Node上做项目,只是初步设想的结构,欢迎大家一起交流,提供宝贵的意见和建议。

 

本博客停止更新,请访问新个人博客:owenchen.duapp.com