一个完整的基于Node.js web应用详解
2013-01-23 23:23 Owen Chen 阅读(19224) 评论(4) 编辑 收藏 举报本博客停止更新,请访问新个人博客: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上做项目,只是初步设想的结构,欢迎大家一起交流,提供宝贵的意见和建议。