經過nodeclub項目源碼來說解如何作一個nodejs + express + mongodb項目

1. About

  • 1.1 what:
    nodeclub是cnodejs.com的源碼,cnode算是一個基本的博客系統,包含文章發佈, 關注,評論等功能。這些功能能夠說是任何一個網站的基礎。從nodeclub裏能夠學到什麼?javascript

    • 1.基本的架構
    • 2.開發測試過程
    • 3.MVC的設計
    • 4.middleware 的正確用法
    • 5.如何設計mongodb schema
    • 6.如何正確的使用mongoose
    • 7.如何實現一個標籤系統
    • 8.plugins? services ?
    • 9.如何正確的使用ejs helper
    • 10.到底該怎樣寫路由, restful?
    • 11.如何作基本的控制驗證
    • 12.如何發郵件
    • 13.session
    • 14.github 用戶登陸
    • 15.圖片上傳
    • 16.消息發送

    除了nodeclub源碼的學習筆記之外, 還會有一點最近搗鼓這一塊的經驗分享php

    • 1.一個完整的消息訂閱設計
    • 2.消息推送, socket + express如何合做?
    • 3.包裝action
    • 4.蛋疼的異步回調如何處理

    nodeclub源碼html

  • 1.2 why:
    對於想用nodejs + express + mongodb 來作網站技術基礎的項目, nodeclub能夠說是很好的源碼級指南,固然也是個人指南,這篇文章權當作我的學習nodeclub的學習筆記。前端

  • 1.3 whojava

who = 一名本應該在寫前端的但不知怎的一直在寫後端的馬膿 -> 
    @echo 'github: https://github.com/6174'
    @echo 'weibo: http://weibo.com/u/2254313183'
    @echo 'email: 57017125@qq.com'
    @echo 'ps: 一直在求後端partner中,有意者聯繫我' 
    @send()

2. nodeclude 中用到了哪些開源技術

  • 2.1 nodejs項目一大優勢就是有一個package.json, 裏邊的dependencies & devDependencies能夠看到這個項目全部的依賴。 對於有經驗的開發者來講, 看完package.json基本就能知道項目的架構是怎樣。node

  • 2.2 dependenciesnginx

    • express: 基礎框架:
    • mongodb: 數據存儲
    • mongoose: orm
    • connect-mongo: session (對於redis, 可使用connect-redis)
    • nodemailer:郵件
    • validator:驗證
    • passportpassport-github: passport,
    • loader: ejs-view-helper, 靜態資源加載處理
    • 其餘: event-proxy, node-markdown, ndir
  • 2.3 devDependenciesgit

    • 測試框架:mocha, should
    • 運行: forever
    • 請求模擬: supertest
  • 2.4 nodeclub以express + mongodb + mongoose做爲基本框架, 典型的MVC應用github

    • Model: 對應mongoose orm, models目錄
    • view: ejs模板, views目錄
    • controler:express middleware , contollers目錄
  • 2.5 目錄結構:redis

    - common/
    - controllers/
    - libs/
    # express中間件, 基本的auth, session 驗證
    - middlewares/
    - models/
    #消息, 郵件服務
    - services/
    - plugins/
    #能夠看作是對model處理的加工庫
    - proxy/
    - test/
    - views/
    - app.js
    - route.js
    - config.js

3. 應用入口app.js

神聖的入口文件,幾乎每一個項目都會有一個entry,對於瞭解一個應用熟悉入口邏輯很重要。 下面將分步來看看,nodeclub的app.js作了什麼:

3.1 require(./config)

  • 3.1.1 應用相關的配置的設置, 主要分爲

    • 1.應用全局數據配置
    • 2.數據庫鏈接配置
    • 3.session,auth相關配置
    • 4.rss配置
    • 5.mail配置
    • 6.第三方鏈接相關配置, github, weibo

    配置文件也是瞭解應用的一個好地方, 在config.default.js中能夠看到如下信息, 這些極可能是咱們平時作應用開發的時候沒有留意到的地方

//--應用數據統計
  google_tracker_id: 'UA-41753901-5',

  //--靜態文件極可能使用cdn來作
  site_static_host: '', // 靜態文件存儲域名

  //--求解釋
  site_enable_search_preview: false, // 開啓google search preview
  site_google_search_domain:  'cnodejs.org',  // google search preview中要搜索的域名

  //--運營數據
  list_topic_count: 20,
  post_interval: 10000,
  admins: { admin: true },
  side_ads:[]
  allow_sign_up: true,

  //--插件模式
  plugins: []
  • 3.1.2 固然這裏的配置文件是default的,配置文件能夠放在一個config的文件夾下面,多個文件的方式來整理。好比運營數據配置和其餘數據配置分開,由於頗有可能須要作一個小的工具來讓非技術人員配置相關參數。這時候能夠用一個index.js做爲facade,至關於一個大的node module。

3.2 require('./models')

  • 3.2.1 以前已經講了models目錄對應MVC的M部分。
  • 3.2.2 models目錄下面有index.js, require('./models')至關於require('./models/index')
    index至關於一個模型的facade, index.js作得事情分別是

    • 1.connect mongodb
    • 2.require各個model模塊
    • 3.exports 全部的model
      簡單而言就是初始化了應用model層。
  • 3.2.3 模型使用orm框架mogoose來寫,瞭解mogoose事後, models部分的代碼也就是秒懂了
    , 我說的只是代碼,literaly, 一個項目的核心就是model的設計,之前作過的任何項目都是同樣, 數據庫table的設計好壞直接影響應用的開發以及性能。 下面來看看各個model的schema設計(幾乎直接ctr+c, ctr+v加上了一點點註釋) :

  • 3.2.4 user

var UserSchema = new Schema({
          //--基本用戶信息, index表示在mongodb中會創建索引
          //--unique: true 惟一性設置
          name: { type: String, index: true },
          loginname: { type: String, unique: true },
          pass: { type: String },
          email: { type: String, unique: true },
          url: { type: String },
          profile_image_url: {type: String},
          location: { type: String },
          signature: { type: String },
          profile: { type: String },
          weibo: { type: String },
          avatar: { type: String },
          githubId: { type: String, index: true },
          githubUsername: {type: String},
          is_block: {type: Boolean, default: false},

          //--用戶產生數據meta
          score: { type: Number, default: 0 },
          topic_count: { type: Number, default: 0 },
          reply_count: { type: Number, default: 0 },
          follower_count: { type: Number, default: 0 },
          following_count: { type: Number, default: 0 },
          collect_tag_count: { type: Number, default: 0 },
          collect_topic_count: { type: Number, default: 0 },
          create_at: { type: Date, default: Date.now },
          update_at: { type: Date, default: Date.now },
          is_star: { type: Boolean },
          level: { type: String },
          active: { type: Boolean, default: true },

          //-mail
          receive_reply_mail: {type: Boolean, default: false },
          receive_at_mail: { type: Boolean, default: false },
          from_wp: { type: Boolean },
          retrieve_time : {type: Number},
          retrieve_key : {type: String}
        });
  • 3.2.5 topic 話題

//1 <- 多 //tag <- topic <- collect var TopicSchema = new Schema({ title: { type: String }, content: { type: String }, author_id: { type: ObjectId }, top: { type: Boolean, default: false }, reply_count: { type: Number, default: 0 }, visit_count: { type: Number, default: 0 }, collect_count: { type: Number, default: 0 }, create_at: { type: Date, default: Date.now }, update_at: { type: Date, default: Date.now }, //--這裏reply的設計方式不知道是否合適, 由於mongdb不一樣於關係型數據庫,這裏每次讀取文章都須要重reply集合裏邊查找遍歷一邊,文章是讀繁忙的。 //-- 一個document的大小爲5Mb, 一本牛津詞典的內容, 我以爲將reply放在這裏應該不會有太大問題。 即使不存放reply 內容, 存放一個id數組也會好不少。 //-- 客官們怎麼看? last_reply: { type: ObjectId }, last_reply_at: { type: Date, default: Date.now }, content_is_html: { type: Boolean } }); var ReplySchema = new Schema({ content: { type: String }, topic_id: { type: ObjectId, index: true }, author_id: { type: ObjectId }, reply_id : { type: ObjectId }, create_at: { type: Date, default: Date.now }, update_at: { type: Date, default: Date.now }, content_is_html: { type: Boolean } }); //--話題集合 var TopicCollectSchema = new Schema({ user_id: { type: ObjectId }, topic_id: { type: ObjectId }, create_at: { type: Date, default: Date.now } }); //--話題標籤 var TopicTagSchema = new Schema({ topic_id: { type: ObjectId }, tag_id: { type: ObjectId }, create_at: { type: Date, default: Date.now } });
  • 3.2.6 tag
    標籤系統
//tag <- collect
        var TagSchema = new Schema({
          name: { type: String },
          order: { type: Number, default: 1 },
          description: { type: String },
          background: { type: String },
          topic_count: { type: Number, default: 0 },
          collect_count: { type: Number, default: 0 },
          create_at: { type: Date, default: Date.now }
        });

        var TagCollectSchema = new Schema({
          user_id: { type: ObjectId, index: true },
          tag_id: { type: ObjectId },
          create_at: { type: Date, default: Date.now }
        });
  • 3.2.7 關係
var RelationSchema = new Schema({
          user_id: { type: ObjectId },
          follow_id: { type: ObjectId },
          create_at: { type: Date, default: Date.now }
        });
  • 3.2.8 消息
    消息model設計, 對於一個blog來講, 基本的只有回覆消息, 這裏加了關注和@消息。
/*
     * type:
     * reply: xx 回覆了你的話題
     * reply2: xx 在話題中回覆了你
     * follow: xx 關注了你
     * at: xx @了你
     */
    var MessageSchema = new Schema({
      type: { type: String },
      master_id: { type: ObjectId, index: true },
      author_id: { type: ObjectId },
      topic_id: { type: ObjectId },
      reply_id: { type: ObjectId },
      has_read: { type: Boolean, default: false },
      create_at: { type: Date, default: Date.now }
    });

3.3 require middlewares

  • 3.3.1 express的基礎是middleware,或者說express的基礎是connect,connect的基礎是middleware。middleware模式在professional nodejs中有一個專門的章節來說解。何爲middleware呢? middleware模式 至關於一個加工流水線(你們叫middleware stack),每個middleware至關於一個加工步驟,當出現一個http請求的時候,http請求會挨着每一個middleware執行下去。
    express裏處理一個請求的過程基本上就是請求經過middleware stack的過程: * -> middlewares -> 路由 -> controllers -> errorhandlering。

  • 3.3.2 middleware 怎樣作到的, 異步的方法呢? middleware使用promise的方式來處理異步,全部每一個middleware都有三個參數req, res, next, 對於異步的狀況, 必需要調用next()方法。否則後續的middleware就沒法執行。 ps: debug 的時候沒調用next()還不會報錯,必定注意

  • 3.3.3 auth.js
    auth.js exports出來的函數所有都是中間件,從變量名就徹底清楚的知道到底在作什麼了


//-- 須要admin權限 exports.adminRequired = function (req, res, next) {} //-- 須要有用戶 exports.userRequired = function (req, res, next) {} //-- 須要有用戶並登陸 exports.signinRequired = function (req, res, next) { if (!req.session.user) { res.render('notify/notify', {error: '未登入用戶不能發佈話題。'}); return; } next(); } //-- 屏蔽用戶 -_- exports.blockUser = function (req, res, next) {}

這裏其實就能夠看到中間件的做用了,咱們之前寫php的時候每次都須要判斷用戶是否登陸, 沒登錄redirect到index.php ,只不過這裏的方式是經過中間件來處理。
明白這裏什麼意思,其餘的中間件模塊也就秒懂了。

3.4 require('./routes')

  • 3.4.1 express 的世界裏另一個很重要的就是route, nodejs啓動的是服務, 監聽了某一端口, 接受http or https or sockt請求, 那url中像"/index.php?blabla"這一串的存在怎麼處理呢, express的route功能就能夠幫咱們解析。

  • 3.4.2 MVC中如何將一個請求和controller聯繫起來呢, route就是這樣的紐帶

//--get, post 請求
  app.get('/signin', sign.showLogin);
  app.post('/signin', sign.login);
  //--使用中間件
  app.get('/signup', configMiddleware.github, passport.authenticate('github'));
  app.post('/:topic_id/reply', auth.userRequired, limit.postInterval, reply.add);
  • 3.4.3 route是瞭解一個應用最佳的地方,一個請求如何處理, 到相應的controller去看就知道了。 相比起在PHP環境下配置更加靈活。固然你說你經過nginx來配置也很靈活,好吧,咱們說的不是一回事。

3.5 initialization

  • 3.5.1 experess initialize: app.js 中其餘大多部分就是express的初始化了, 初始化流程以下:

    • 1.配置上傳 upload_dir
    • 2.模板引擎設置
    • 3.express通用中間件設置
    • 4.pasport中間件
    • 5.自定義中間件

      • 1.auth_user
      • 2.block_user
      • 3.staticfile: upload
      • 4.staticfile: user_data
    • 6.csrf
    • 7.errorhandler
    • 8.set view cache

    @Note:配置的順序很重要, 中間件的執行順序是按照定義順序來執行的, 若是一箇中間件依賴另外的中間件, 而本身先執行了, 這種狀況就會錯誤。 常見的問題就是session配置, 必定要記得配置session中間件的時候, 要先配置cookieParser。

  • 3.5.2 session設置
    這個步驟在initialize裏邊已經有了, 不過再單獨講一下, nodeclub使用的是connect-mongo來做爲session的存儲
//--cookieParser必定要在前面, 由於session的設置依賴cookie
    app.use(express.cookieParser());
    app.use(express.session({
      secret: config.session_secret,
      store: new MongoStore({
        db: config.db_name,
      }),
    }));
  • 3.5.3 view helpers
    使用過ejs的確定知道, ejs裏邊view helper設置很簡單, 就像賦值變量同樣。 當對於一些通用的helper能夠這樣設置:
app.helpers({
          config: config,
          Loader: Loader,
          assets: assets
        });
        app.dynamicHelpers(require('./common/render_helpers'));
  • 3.5.4 github pasport initialize
// github oauth
        passport.serializeUser(function (user, done) {
          done(null, user);
        });
        passport.deserializeUser(function (user, done) {
          done(null, user);
        });
        passport.use(new GitHubStrategy(config.GITHUB_OAUTH, 
  githubStrategyMiddleware));
  • 3.5.5 start app

4. 用戶註冊

  • 4.1 user 是每一個應用都會處理的基本, 註冊登陸登出, 看看nodeclub作了哪些事情:
  • 4.2 路由:
//--設置可否直接註冊, 不能的話經過github註冊
  if (config.allow_sign_up) {
    app.get('/signup', sign.showSignup);
    app.post('/signup', sign.signup);
  } else {
    app.get('/signup', configMiddleware.github, passport.authenticate('github'));
  }
  app.post('/signout', sign.signout);
  app.get('/signin', sign.showLogin);
  app.post('/signin', sign.login);
  • 4.3 controller & model:sign.signup
sanitize = validator.sanitize;
    check = validator.check;
    exports.signup = function (req, res, next) {
      //--xss 消毒
      var name = sanitize(req.body.name).trim();
      name = sanitize(name).xss();
      ...
      //--validations
      try {
        check(name, '用戶名只能使用0-9,a-z,A-Z。').isAlphanumeric();
      } catch (e) {
        res.render('sign/signup', {error: e.message, name: name, email: email});
        return;
      }
      ...
      //--用用戶名登陸或者email登陸
      query = {'$or': [{'loginname': loginname}, {'email': email}]}
      User.getUserByQuery(query, {}, function(){
        ...
        pass = md5(pass);
        ...
        User.newAndSave(name, loginname, pass, email, avatar_url, false, function (err) {
          ...
          // 發送激活郵件
          mail.sendActiveMail(email, md5(email + config.session_secret), name);
          res.render('sign/signup', {
            success: '歡迎加入 ' + config.name + '!咱們已給您的註冊郵箱發送了一封郵件,請點擊裏面的連接來激活您的賬號。'
          });
        })
      })
    }

5. mongoose 的使用

  • 5.1 使用User.newAndSave,
  • 5.2 異步 callback pyramid
    一個應用一般會遇到這樣的情景, 一個頁面須要的數據包括, 文章列表, 評論列表,用戶數據,廣告數據, other stuff... 問題是每一個都是異步的, 怎麼辦。 user數據獲取事後的callback調用文章列表獲取, 文章列表獲取的callback調用評論列表的獲取... 這樣就太蛋疼了。 nodeclub使用了eventproxy模塊優雅的解決這樣的問題:
render = function(){}
    var proxy = EventProxy.create('tags', 'topics', 'hot_topics', 'stars', 'tops', 'no_reply_topics', 'pages', render);
    proxy.fail(next);
    Tag.getAllTags(proxy.done('tags'));
    Topic.getTopicsByQuery(query, options, proxy.done('topics'));
    User.getUsersByQuery({ is_star: true }, { limit: 5 }, proxy.done('stars'));

看完代碼不言而喻。。。
固然異步處理的方法有不少:
- 1.基於事件的:eventProxy
- 2.基於promise的:Async.js Q.js, when.js
- 3.基於編譯的:continuation, wind
- 4.基於語言語法的:yield, livescript
文章最後會講一下我個人異步選擇方案

6. 消息

  • 6.1 原先覺得有動態的消息推送, 有隊列處理, 錯了, 木有
  • 6.2 在sublime text裏邊全局搜索sendReply2Message會發現是在controller/reply.js裏邊調用的, 也就是說,消息是直接觸發的。
  • 6.3 好吧, 這部分大概你們都能秒懂。。

7. 開發

7.1 測試

  • 7.1.1 一個項目一定離不開測試, nodeclub基於mocha BDD測試框架, 一切的前提假設至少能看懂jasmine或者mocha或者任何一個BDD風格的測試代碼。
  • 7.1.2 打開即看到app.js
var app = require('../app');
  describe('app.js', function () {
    //--before, 執行it的前面會執行
    before(function (done) {
      //--done, 異步方法
      app.listen(3001, done);
    });
    after(function () {
      app.close();
    });
    it('should / status 200', function (done) {
      //--使用 app.request()就能夠模擬請求了? 這個api哪裏來的, 求解釋?
      app.request().get('/').end(function (res) {
        res.should.status(200);
        done();
      });
    });
  });
  //--按理說應該是能夠正常運行了可是我一直出現這個錯誤:
  //--connect ADDRNOTAVAIL 知道的求解釋
  //--我嘗試用supertest直接測試, 可是也是一直timeout, mocha
  //--裏邊加大timeout時間, 結果就是一直沒反應。 

  //--分析緣由, express版本問題, nodeclub中express的版本仍是2.x, 因此纔會有
  //--app.request(), app.close()這些api
  //--第二個緣由, 到supertest官網, 發現人家都已經轉戰到superagent項目了, 因而我寫了下面這個測試腳本, 能夠經過了
  var express = require('express');
  var should = require('should');
  var path = require('path');
  var superagent = require('superagent');
  var app = express()
  app.get('/user', function(req, res, next) {
      res.send(200, {
          name: 'tobi'
      })
  })
  describe('myapp.js', function() {
      this.timeout(5000)
      before(function(done) {
          app.listen(21, done);
      })
      after(function() {
          // app.close()
      })
      it('should /status 200', function(done) {
          agent = superagent.agent()
          agent.get('http://localhost:21/user').end(function(err, res) {
            console.log(err, res)
            res.should.have.status(200);
            res.text.should.include('tobi');
            return done();
          });
      })
  })

7.2 運行

  • nodejs是單線程應用, 若是咱們用node命令來運行咱們的應用, 當出現一個小錯誤, 它就掛了。 而後沒有而後了。 避免這種問題的方法有以下工具:

    • 1.forever
    • 2.nodemon
    • 3.supervisor
      nodeclub 使用forever來運行項目, 使用這類工具的好處就是, 當有代碼改動事後, 會自動的重啓應用。 沒必要每次本身去運行node *.js

8. 說說本身的經驗

待續...

8.1 消息訂閱設計

8.2 express + socket

8.3 異步

8.4 Action

相關文章
相關標籤/搜索