本博客停止更新,請訪問新個人博客: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上做項目,只是初步設想的結構,歡迎大家一起交流,提供寶貴的意見和建議。