一個完整的基於Node.js web應用詳解


本博客停止更新,請訪問新個人博客: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM