這個項目最初實際上是fork別人的項目。當初想接觸下mongodb數據庫,找個例子學習下,後來改着改着就面目全非了。後臺和數據庫重構,前端增長了登陸註冊功能,僅保留了博客設置頁面,可是也優化了。css
因此這樣作先後端幾乎徹底解耦,只要約定好restful風格的數據接口,和數據存取格式就OK啦。html
後端我用了mongoDB作數據庫,並在Express中經過mongoose操做mongoDB,省去了複雜的命令行,經過Javascript操做無疑方便了不少。前端
在原來項目的基礎上,作了以下更新:vue
更多的更新內容請移步項目CMS-of-Blog_Production和CMS-of-Blog。jquery
原做者也寫過度析的文章。這裏,主要分析一下我更新的部分。git
對原數據庫進行從新設計,改爲以用戶分組的subDocs數據庫結構。這樣以用戶爲一個總體的數據庫結構更加清晰,同時也更方便操做和讀取。代碼以下:github
var mongoose = require('mongoose'), Schema = mongoose.Schema articleSchema = new Schema({ title: String, date: Date, content: String, }), linkSchema = new Schema({ name: String, href: String, newPage: Boolean }), userSchema = new Schema({ name: String, password: String, email: String, emailCode: String, createdTime: Number, articles: [articleSchema], links: [linkSchema] }), User = mongoose.model('User', userSchema); mongoose.connect('mongodb://localhost/platform') mongoose.set('debug', true) var db = mongoose.connection db.on('error', function () { console.log('db error'.error) }) db.once('open', function () { console.log('db opened'.silly) }) module.exports = { User: User }
代碼一開始新定義了三個Schema:articleSchema、linkSchema和userSchema。而userSchema裏又嵌套了articleSchema和linkSchema,構成了以用戶分組的subDocs數據庫結構。Schema是一種以文件形式存儲的數據庫模型骨架,不具有數據庫的操做能力。而後將將該Schema發佈爲Model。Model由Schema發佈生成的模型,具備抽象屬性和行爲的數據庫操做對。由Model能夠建立的實體,好比新註冊一個用戶就會建立一個實體。web
數據庫建立了以後須要去讀取和操做,能夠看下注冊時發送郵箱驗證碼的這段代碼感覺下。ajax
router.post('/genEmailCode', function(req, res, next) { var email = req.body.email, resBody = { retcode: '', retdesc: '', data: {} } if(!email){ resBody = { retcode: 400, retdesc: '參數錯誤', } res.send(resBody) return } function genRandomCode(){ var arrNum = []; for(var i=0; i<6; i++){ var tmpCode = Math.floor(Math.random() * 9); arrNum.push(tmpCode); } return arrNum.join('') } db.User.findOne({ email: email }, function(err, doc) { if (err) { return console.log(err) } else if (doc && doc.name !== 'tmp') { resBody = { retcode: 400, retdesc: '該郵箱已註冊', } res.send(resBody) } else if(!doc){ // 第一次點擊獲取驗證碼 var emailCode = genRandomCode(); var createdTime = Date.now(); // setup e-mail data with unicode symbols var mailOptions = { from: '"CMS-of-Blog ?" <tywei90@163.com>', // sender address to: email, // list of receivers subject: '親愛的用戶' + email, // Subject line text: 'Hello world ?', // plaintext body html: [ '<p>您好!恭喜您註冊成爲CMS-of-Blog博客用戶。</p>', '<p>這是一封發送驗證碼的註冊認證郵件,請複製一下驗證碼填寫到註冊頁面以完成註冊。</p>', '<p>本次驗證碼爲:' + emailCode + '</p>', '<p>上述驗證碼30分鐘內有效。若是驗證碼失效,請您登陸網站<a href="https://cms.wty90.com/#!/register">CMS-of-Blog博客註冊</a>從新申請認證。</p>', '<p>感謝您註冊成爲CMS-of-Blog博客用戶!</p><br/>', '<p>CMS-of-Blog開發團隊</p>', '<p>'+ (new Date()).toLocaleString() + '</p>' ].join('') // html body }; // send mail with defined transport object transporter.sendMail(mailOptions, function(error, info){ if(error){ return console.log(error); } // console.log('Message sent: ' + info.response); new db.User({ name: 'tmp', password: '0000', email: email, emailCode: emailCode, createdTime: createdTime, articles: [], links: [] }).save(function(err) { if (err) return console.log(err) // 半小時內若是不註冊成功,則在數據庫中刪除這條數據,也就是說驗證碼會失效 setTimeout(function(){ db.User.findOne({ email: email }, function(err, doc) { if (err) { return console.log(err) } else if (doc && doc.createdTime === createdTime) { db.User.remove({ email: email }, function(err) { if (err) { return console.log(err) } }) } }) }, 30*60*1000); resBody = { retcode: 200, retdesc: '' } res.send(resBody) }) }); }else if(doc && doc.name === 'tmp'){ // 在郵箱驗證碼有效的時間內,再次點擊獲取驗證碼(相似省略) ... } }) })
後臺接受到發送郵箱驗證碼的請求後,會初始化一個tmp的用戶。經過new db.User()
會建立一個User的實例,而後執行save()
操做會將這條數據寫到數據庫裏。若是在半小時內沒有註冊成功,經過匹配郵箱,而後db.User.remove()
將這條數據刪除。更多具體用法請移步官方文檔。mongodb
將全部請求分爲三種:
/web/
/
/:id/
這樣每一個用戶均可以擁有本身的博客頁面,具體代碼以下:
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var routes = require('./index'); var db = require('./db') var app = express(); // view engine setup app.set('views', path.join(__dirname, '../')); app.set('view engine', 'jade'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use('/public',express.static(path.join(__dirname, '../public'))); // 公共ajax接口(index.js) app.use('/web', routes); // 公共html頁面,好比登陸頁,註冊頁 app.get('/', function(req, res, next) { res.render('common', { title: 'CMS-blog' }); }) // 跟用戶相關的博客頁面(路由的第一個參數只匹配與處理的相關的,不越權!) app.get(/^\/[a-z]{1}[a-z0-9_]{3,15}$/, function(req, res, next) { // format獲取請求的path參數 var pathPara = req._parsedUrl.pathname.slice(1).toLocaleLowerCase() // 查詢是否對應有相應的username db.User.count({name: pathPara}, function(err, num) { if (err) return console.log(err) if(num > 0){ res.render('main', { title: 'CMS-blog' }); }else{ // 自定義錯誤處理 res.status(403); res.render('error', { message: '該用戶還沒有開通博客。<a href="/#!/register">去註冊</a>', }); } }) }) // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handlers // development error handler // will print stacktrace if (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: err }); }); } module.exports = app;
具體的ajax接口代碼你們能夠看server文件夾下的index.js文件。
在原項目基礎上,優化了pop彈窗組件,更加智能,更多配置項,接近網易$.dialog組件。使而且一套代碼僅修改了下css,實現相同接口下pc端彈窗和wap端toast功能。由於有部分格式化參數代碼在vuex的action裏,有時間,能夠將這個進一步整理成一個vue組件,方便你們使用。
pop
: 彈窗的顯示與否, 根據content參數,有內容則爲truecss
: 自定義彈窗的class, 默認爲空showClose
: 爲false則不顯示關閉按鈕, 默認顯示closeFn
: 彈窗點擊關閉按鈕以後的回調title
: 彈窗的標題,默認'舒適提示', 若是不想顯示title, 直接傳空content
(required): 彈窗的內容,支持傳htmlbtn1
: '按鈕1文案|按鈕1樣式class', 格式化後爲btn1Text和btn1Csscb1
: 按鈕1點擊以後的回調,若是cb1沒有明確返回true,則默認按鈕點擊後關閉彈窗btn2
: '按鈕2文案|按鈕2樣式class', 格式化後爲btn2Text和btn2Csscb2
: 按鈕2點擊以後的回調,若是cb2沒有明確返回true,則默認按鈕點擊後關閉彈窗。按鈕參數不傳,文案默認'我知道了',點擊關閉彈窗init
: 彈窗創建後的初始化函數,能夠用來處理複雜交互(注意彈窗必定要是從pop爲false變成true纔會執行)destroy
: 彈窗消失以後的回調函數wapGoDialog
: 在移動端時,要不要走彈窗,默認false,走toast模板
<template> <div class="m-dialog" :class="getPopPara.css"> <div class="dialog-wrap"> <span class="close" @click="handleClose" v-if="getPopPara.showClose">+</span> <div class="title" v-if="getPopPara.title">{{getPopPara.title}}</div> <div class="content">{{{getPopPara.content}}}</div> <div class="button"> <p class="btn" :class="getPopPara.btn1Css" @click="fn1"> <span>{{getPopPara.btn1Text}}</span> </p> <p class="btn" :class="getPopPara.btn2Css" @click="fn2" v-if="getPopPara.btn2Text"> <span>{{getPopPara.btn2Text}}</span> </p> </div> </div> </div> </template>
腳本
import {pop} from '../vuex/actions' import {getPopPara} from '../vuex/getters' import $ from '../js/jquery.min' export default{ computed:{ showDialog(){ return this.getPopPara.pop } }, vuex: { getters: { getPopPara }, actions: { pop } }, methods: { fn1(){ let fn = this.getPopPara.cb1 let closePop = false // 若是cb1函數沒有明確返回true,則默認按鈕點擊後關閉彈窗 if(typeof fn == 'function'){ closePop = fn() } // 初始值爲false, 因此沒傳也默認關閉 if(!closePop){ this.pop() } // !fn && this.pop() }, fn2(){ let fn = this.getPopPara.cb2 let closePop = false // 若是cb1函數沒有明確返回true,則默認按鈕點擊後關閉彈窗 if(typeof fn == 'function'){ closePop = fn() } // 初始值爲false, 因此沒傳也默認關閉 if(!closePop){ this.pop() } // !fn && this.pop() }, handleClose(){ // this.pop()要放在最後,由於先執行全部參數就都變了 let fn = this.getPopPara.closeFn typeof fn == 'function' && fn() this.pop() } }, watch:{ 'showDialog': function(newVal, oldVal){ // 彈窗打開時 if(newVal){ // 增長彈窗支持鍵盤操做 $(document).bind('keydown', (event)=>{ // 回車鍵執行fn1,會出現反覆彈窗bug if(event.keyCode === 27){ this.pop() } }) var $dialog = $('.dialog-wrap'); // 移動端改爲相似toast,經過更改樣式,既不須要增長toast組件,也不須要更改代碼,統一pop方法 if(screen.width < 700 && !this.getPopPara.wapGoDialog){ $dialog.addClass('toast-wrap'); setTimeout(()=>{ this.pop(); $dialog.removeClass('toast-wrap'); }, 2000) } //調整彈窗居中 let width = $dialog.width(); let height = $dialog.height(); $dialog.css('marginTop', - height/2); $dialog.css('marginLeft', - width/2); // 彈窗創建的初始化函數 let fn = this.getPopPara.init; typeof fn == 'function' && fn(); }else{ // 彈窗關閉時 // 註銷彈窗打開時註冊的事件 $(document).unbind('keydown') // 彈窗消失回調 let fn = this.getPopPara.destroy typeof fn == 'function' && fn() } } } }
爲了使用方便,咱們在使用的時候進行了簡寫。爲了讓組件能識別,須要在vuex的action裏對傳入的參數格式化。
function pop({dispatch}, para) { // 若是沒有傳入任何參數,默認關閉彈窗 if(para === undefined){ para = {} } // 若是隻傳入字符串,格式化內容爲content的para對象 if(typeof para === 'string'){ para = { content: para } } // 設置默認值 para.pop = !para.content? false: true para.showClose = para.showClose === undefined? true: para.showClose para.title = para.title === undefined? '舒適提示': para.title para.wapGoDialog = !!para.wapGoDialog // 沒有傳參數 if(!para.btn1){ para.btn1 = '我知道了|normal' } // 沒有傳class if(para.btn1.indexOf('|') === -1){ para.btn1 = para.btn1 + '|primary' } let array1 = para.btn1.split('|') para.btn1Text = array1[0] // 可能會傳多個class for(let i=1,len=array1.length; i<len; i++){ if(i==1){ // class爲disabled屬性不加'btn-' para.btn1Css = array1[1]=='disabled'? 'disabled': 'btn-' + array1[1] }else{ para.btn1Css = array1[i]=='disabled'? ' disabled': para.btn1Css + ' btn-' + array1[i] } } if(para.btn2){ if(para.btn2.indexOf('|') === -1){ para.btn2 = para.btn2 + '|normal' } let array2 = para.btn2.split('|') para.btn2Text = array2[0] for(let i=1,len=array2.length; i<len; i++){ if(i==1){ para.btn2Css = array2[1]=='disabled'? 'disabled': 'btn-' + array2[1] }else{ para.btn2Css = array2[i]=='disabled'? ' disabled': para.btn2Css + ' btn-' + array2[i] } } } dispatch('POP', para) }
爲了讓移動端兼容pop彈窗組件,咱們採用mediaQuery對移動端樣式進行了更改。增長參數wapGoDialog
,代表咱們在移動端時,要不要走彈窗,默認false,走toast。這樣能夠一套代碼就能夠兼容pc和wap。
這裏主要分析了下後臺和數據庫,並且比較簡單,你們能夠去看源碼。總之,這是一個不錯的前端入手後臺和數據庫的例子。功能比較豐富,並且能夠學習下vue.js。