1.1 what:
nodeclub是cnodejs.com的源碼,cnode算是一個基本的博客系統,包含文章發佈, 關注,評論等功能。這些功能能夠說是任何一個網站的基礎。從nodeclub裏能夠學到什麼?javascript
除了nodeclub源碼的學習筆記之外, 還會有一點最近搗鼓這一塊的經驗分享php
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.1 nodejs項目一大優勢就是有一個package.json, 裏邊的dependencies & devDependencies能夠看到這個項目全部的依賴。 對於有經驗的開發者來講, 看完package.json基本就能知道項目的架構是怎樣。node
2.2 dependenciesnginx
express
: 基礎框架:mongodb
: 數據存儲 mongoose
: orm connect-mongo
: session (對於redis, 可使用connect-redis)nodemailer
:郵件 validator
:驗證 passport
,passport-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
2.5 目錄結構:redis
- common/ - controllers/ - libs/ # express中間件, 基本的auth, session 驗證 - middlewares/ - models/ #消息, 郵件服務 - services/ - plugins/ #能夠看作是對model處理的加工庫 - proxy/ - test/ - views/ - app.js - route.js - config.js
神聖的入口文件,幾乎每一個項目都會有一個entry,對於瞭解一個應用熟悉入口邏輯很重要。 下面將分步來看看,nodeclub的app.js作了什麼:
3.1.1 應用相關的配置的設置, 主要分爲
配置文件也是瞭解應用的一個好地方, 在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.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} });
//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 } });
//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 } });
var RelationSchema = new Schema({ user_id: { type: ObjectId }, follow_id: { type: ObjectId }, create_at: { type: Date, default: Date.now } });
/* * 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.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.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.5.1 experess initialize: app.js 中其餘大多部分就是express的初始化了, 初始化流程以下:
@Note
:配置的順序很重要, 中間件的執行順序是按照定義順序來執行的, 若是一箇中間件依賴另外的中間件, 而本身先執行了, 這種狀況就會錯誤。 常見的問題就是session配置, 必定要記得配置session中間件的時候, 要先配置cookieParser。
//--cookieParser必定要在前面, 由於session的設置依賴cookie app.use(express.cookieParser()); app.use(express.session({ secret: config.session_secret, store: new MongoStore({ db: config.db_name, }), }));
app.helpers({ config: config, Loader: Loader, assets: assets }); app.dynamicHelpers(require('./common/render_helpers'));
// 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));
//--設置可否直接註冊, 不能的話經過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);
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 + '!咱們已給您的註冊郵箱發送了一封郵件,請點擊裏面的連接來激活您的賬號。' }); }) }) }
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
文章最後會講一下我個人異步選擇方案
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(); }); }) })
待續...