摘要:三更新內容在原來項目的基礎上,做了如下更新數據庫重新設計,改成以用戶分組的數據庫結構應數據庫改動,所有接口重新設計,并統一采用和網易立馬理財一致的接口風格刪除原來游客模式,增加登錄注冊功能,支持彈窗登錄。
這個項目最初其實是fork別人的項目。當初想接觸下mongodb數據庫,找個例子學習下,后來改著改著就面目全非了。后臺和數據庫重構,前端增加了登錄注冊功能,僅保留了博客設置頁面,但是也優化了。
一、功能特點一個基本的博客內容管理器功能,如發布并管理文章等
每個用戶可以通過注冊擁有自己的博客
支持markdown語法編輯
支持代碼高亮
可以管理博客頁面的鏈接
博客頁面對移動端適配優化
賬戶管理(修改密碼)
頁面足夠大氣、酷炫嘿
二、用到的技術和實現思路: 2.1 前端:Vue全家桶Vue.js
Vue-Cli
Vue-Resource
Vue-Validator
Vue-Router
Vuex
Vue-loader
2.2 后端Node.js
mongoDB (mongoose)
Express
2.3 工具和語言Webpack
ES6
SASS
Jade
2.4 整體思路:Node服務端除了主頁和首頁外,不做模板渲染,渲染交給瀏覽器完成
Node服務端不做任何路由切換的內容,這部分交給Vue-Router完成
Node服務端只用來接收請求,查詢數據庫并用來返回值
所以這樣做前后端幾乎完全解耦,只要約定好restful風格的數據接口,和數據存取格式就OK啦。
后端我用了mongoDB做數據庫,并在Express中通過mongoose操作mongoDB,省去了復雜的命令行,通過Javascript操作無疑方便了很多。
三、更新內容在原來項目的基礎上,做了如下更新:
數據庫重新設計,改成以用戶分組的subDocs數據庫結構
應數據庫改動,所有接口重新設計,并統一采用和網易立馬理財一致的接口風格
刪除原來游客模式,增加登錄注冊功能,支持彈窗登錄。
增加首頁,展示最新發布文章和注冊用戶
增加修改密碼,登出,注銷等功能。
優化pop彈窗組件,更加智能,更多配置項,接近網易$.dialog組件。并且一套代碼僅修改了下css,實現相同接口下pc端彈窗和wap端toast功能。
增加移動端適配
優化原來代碼,修復部分bug。
更多的更新內容請移步項目CMS-of-Blog_Production和CMS-of-Blog。
四、核心代碼分析原作者也寫過分析的文章。這里,主要分析一下我更新的部分。
4.1. 數據庫對原數據庫進行重新設計,改成以用戶分組的subDocs數據庫結構。這樣以用戶為一個整體的數據庫結構更加清晰,同時也更方便操作和讀取。代碼如下:
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可以創建的實體,比如新注冊一個用戶就會創建一個實體。
數據庫創建了之后需要去讀取和操作,可以看下注冊時發送郵箱驗證碼的這段代碼感受下。
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 ?"", // sender address to: email, // list of receivers subject: "親愛的用戶" + email, // Subject line text: "Hello world ?", // plaintext body html: [ " 您好!恭喜您注冊成為CMS-of-Blog博客用戶。
", "這是一封發送驗證碼的注冊認證郵件,請復制一下驗證碼填寫到注冊頁面以完成注冊。
", "本次驗證碼為:" + emailCode + "
", "上述驗證碼30分鐘內有效。如果驗證碼失效,請您登錄網站感謝您注冊成為CMS-of-Blog博客用戶!
", "CMS-of-Blog開發團隊
", ""+ (new Date()).toLocaleString() + "
" ].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()將這條數據刪除。更多具體用法請移步官方文檔。
4.2. 后臺將所有請求分為三種:
ajax異步請求,統一路徑:/web/
公共頁面部分,如博客首頁、登錄、注冊等,統一路徑:/
與博客用戶id相關的博客部分,統一路徑:/: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: "該用戶尚未開通博客。去注冊", }); } }) }) // 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文件。
4.3. pop/toast組件在原項目基礎上,優化了pop彈窗組件,更加智能,更多配置項,接近網易$.dialog組件。使并且一套代碼僅修改了下css,實現相同接口下pc端彈窗和wap端toast功能。因為有部分格式化參數代碼在vuex的action里,有時間,可以將這個進一步整理成一個vue組件,方便大家使用。
4.3.1 pop/toast組件配置參數說明pop: 彈窗的顯示與否, 根據content參數,有內容則為true
css: 自定義彈窗的class, 默認為空
showClose: 為false則不顯示關閉按鈕, 默認顯示
closeFn: 彈窗點擊關閉按鈕之后的回調
title: 彈窗的標題,默認"溫馨提示", 如果不想顯示title, 直接傳空
content(required): 彈窗的內容,支持傳html
btn1: "按鈕1文案|按鈕1樣式class", 格式化后為btn1Text和btn1Css
cb1: 按鈕1點擊之后的回調,如果cb1沒有明確返回true,則默認按鈕點擊后關閉彈窗
btn2: "按鈕2文案|按鈕2樣式class", 格式化后為btn2Text和btn2Css
cb2: 按鈕2點擊之后的回調,如果cb2沒有明確返回true,則默認按鈕點擊后關閉彈窗。按鈕參數不傳,文案默認"我知道了",點擊關閉彈窗
init: 彈窗建立后的初始化函數,可以用來處理復雜交互(注意彈窗一定要是從pop為false變成true才會執行)
destroy: 彈窗消失之后的回調函數
wapGoDialog: 在移動端時,要不要走彈窗,默認false,走toast
4.3.2 pop/toast組件代碼模板
+{{getPopPara.title}}{{{getPopPara.content}}}{{getPopPara.btn1Text}}
{{getPopPara.btn2Text}}
腳本
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() } } } }4.3.3 pop/toast組件參數格式化代碼
為了使用方便,我們在使用的時候進行了簡寫。為了讓組件能識別,需要在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為了讓移動端兼容pop彈窗組件,我們采用mediaQuery對移動端樣式進行了更改。增加參數wapGoDialog,表明我們在移動端時,要不要走彈窗,默認false,走toast。這樣可以一套代碼就可以兼容pc和wap。
后記這里主要分析了下后臺和數據庫,而且比較簡單,大家可以去看源碼。總之,這是一個不錯的前端入手后臺和數據庫的例子。功能比較豐富,而且可以學習下vue.js。
歡迎大家star學習交流:github地址 | 我的博客
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107525.html
摘要:項目來源以前曾用過搭建自己的博客網站,但感覺很是臃腫。所以一直想自己寫一個博客內容管理器。正好近日看完了各個插件的文檔,就用著嘗試寫了這個簡約的博客內容管理器。關于后端后端是用作為服務器的,使用了框架。 項目來源 以前曾用過WordPress搭建自己的博客網站,但感覺WordPress很是臃腫。所以一直想自己寫一個博客內容管理器。 正好近日看完了Vue各個插件的文檔,就用著Vue嘗試寫...
摘要:此項目我會長期更新,希望能和大家一起學習,共同進步更新本項目的版本基于開發,后端用進行了重寫。 感悟 歷時兩個多月,終于利用工作之余完成了這個項目的1.0版本,為什么要寫這個項目?其實基于vuejs+nodejs構建的開源博客系統有很多,但是大多數不支持服務端渲染,也不支持動態標題,只是做到了前后端分離,對于博客類系統seo肯定很重要,索性就自己動手寫了這個項目,其中也遇到了不少問題,...
摘要:此項目我會長期更新,希望能和大家一起學習,共同進步更新本項目的版本基于開發,后端用進行了重寫。 感悟 歷時兩個多月,終于利用工作之余完成了這個項目的1.0版本,為什么要寫這個項目?其實基于vuejs+nodejs構建的開源博客系統有很多,但是大多數不支持服務端渲染,也不支持動態標題,只是做到了前后端分離,對于博客類系統seo肯定很重要,索性就自己動手寫了這個項目,其中也遇到了不少問題,...
閱讀 3017·2023-04-26 00:32
閱讀 498·2019-08-30 15:52
閱讀 2105·2019-08-30 15:52
閱讀 3347·2019-08-30 15:44
閱讀 3280·2019-08-30 14:09
閱讀 1416·2019-08-29 15:15
閱讀 3390·2019-08-28 18:12
閱讀 1074·2019-08-26 13:55