博客內容管理系統

来源:https://www.cnblogs.com/tywei90/archive/2018/02/21/8456547.html
-Advertisement-
Play Games

這個項目最初其實是fork別人的項目。當初想接觸下mongodb資料庫,找個例子學習下,後來改著改著就面目全非了。後臺和資料庫重構,前端增加了登錄註冊功能,僅保留了博客設置頁面,但是也優化了。 "線上地址" 一、更新內容 0. 資料庫重新設計,改成以用戶分組的subDocs資料庫結構 0. 應資料庫 ...


這個項目最初其實是fork別人的項目。當初想接觸下mongodb資料庫,找個例子學習下,後來改著改著就面目全非了。後臺和資料庫重構,前端增加了登錄註冊功能,僅保留了博客設置頁面,但是也優化了。

線上地址

一、更新內容

  1. 資料庫重新設計,改成以用戶分組的subDocs資料庫結構
  2. 應資料庫改動,所有介面重新設計,並統一採用和立馬理財一致的介面風格
  3. 刪除原來游客模式,增加登錄註冊功能,支持彈窗登錄。
  4. 增加首頁,展示最新發佈文章和註冊用戶
  5. 增加修改密碼,登出,註銷等功能。
  6. 優化pop彈窗組件,更加智能,更多配置項,接近網易$.dialog組件。並且一套代碼僅修改了下css,實現相同介面下pc端彈窗和wap端toast功能。
  7. 增加移動端適配
  8. 優化原來代碼,修複部分bug。

更多的更新內容請移步項目CMS-of-Blog_ProductionCMS-of-Blog

二、核心代碼分析

原作者也寫過分析的文章。這裡,主要分析一下我更新的部分。

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 ?" <[email protected]>', // 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()將這條數據刪除。更多具體用法請移步官方文檔

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: '該用戶尚未開通博客。<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文件。

3. pop/toast組件

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

3.2 pop/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>

<script>
    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()
                }
            }
        }
    }
</script>
<style lang="sass">
    @import "../style/components/Pop.scss";
</style>

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<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。

歡迎大家star學習交流:github地址 | 我的博客


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第一章 一些基本概念 HTML(超文本標記語言),構建網頁的靜態結構,由一系列的DOM組成; CSS(層疊樣式表),給網頁各部分結構添加樣式; JavaScript,通過獲取DOM給靜態結構加上動作,使用戶能夠與靜態網頁進行交互; DOM,一種API(應用程式介面),通過這個介面動態的訪問和修改結構 ...
  • 一、使用jQuery為標簽添加屬性或者樣式 1、$("#id名").css("css屬性名","屬性值");比如:$("tbody tr:even").css("background-color","yellow"); 2、使用addClass("class名"),然後在引入的css文件中寫樣式:. ...
  • 本文推薦兩款簡單的富文本編輯器【KindEditor,NicEdit】用於獲得所見即所得的編輯效果,本文僅供學習分享使用,如有不足之處,還請指正。 ...
  • pagemaker是一個前端頁面製作工具,方便產品,運營和視覺的同學迅速開發簡單的前端頁面,從而可以解放前端同學的工作量。此項目創意來自網易樂得內部項目 "nfop" 中的pagemaker項目。原來項目的前端是採用jquery和模板ejs做的,每次組件的更新都會重繪整個dom,性能不是很好。因為當 ...
  • arr sort " " " " " " " " " " 根據一個或者多個屬性對數組進行排序,支持嵌套的屬性。而且可以在每個條件中指定排序的方向,並支持傳入比較函數。 安裝 採用 "npm" 安裝: 採用 "yarn" 安裝: 用法 通過給定的對象屬性進行排序: 逆向排序 參數 : { Object ...
  • 研究了淘寶,天貓和網易彩票163的wap主頁樣式佈局,總結移動端佈局方案 註意:代碼運行是file協議,在chrome里不支持引用本地文件,會提示跨域錯誤,可以用firefox或者Safari打開 當時做的ppt下載: "2015年12月移動端佈局方案探究" 一、基本概念 1. 物理像素(physi ...
  • 其實在早之前,就做過 "立馬理財" 的銷售額統計,只不過是用前端js寫的,需要在首頁的console調試面板里粘貼一段代碼執行, "點擊這裡" 。主要是通過定時爬取 " " 非同步介面來獲取數據。然後通過一定的排重演算法來獲取最終的數據。但是這樣做有以下缺點: 0. 代碼只能在瀏覽器視窗下運行,關閉瀏覽 ...
  • 這個項目做得比較早,當時是基於ionic1和angular1做的。做了四個tabs的app,首頁模仿攜程首頁,第二頁主要是phonegap調用手機核心功能,第三頁模仿微信和qq聊天頁,第四頁模仿一般手機的表單設置頁。同時還模仿知乎做了一個側邊欄頁(賬號:wty,密碼:123456)。 沒有後臺,純前 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...