Vue.js實踐:一個Node.js+mongoDB+Vue.js的博客內容管理系統

項目來源
以前曾用過WordPress搭建自己的博客網站,但感覺WordPress很是臃腫。所以一直想自己寫一個博客內容管理器。

正好近日看完了Vue各個插件的文檔,就用着Vue嘗試寫了這個簡約的博客內容管理器(CMS)。

完成的功能

  • 一個基本的博客內容管理器功能,如後臺登陸,發佈並管理文章等 支持markdown語法編輯
  • 支持代碼高亮
  • 可以管理博客頁面的鏈接
  • 博客頁面對移動端適配優化
  • 賬戶管理(修改密碼)
  • 頁面足夠大氣、酷炫~

於是,爲了彰顯一貫的酷炫,我給後臺寫了一套星空主題。。。

登陸頁面

這裏寫圖片描述

後臺管理頁面

這裏寫圖片描述
但博客頁面最後沒用後臺的星空主題,主要是覺得黑色不太好搭配。

於是我想,既然前端都是用Vue.js寫的,那就參chao考xi一下Vue.js尤雨溪的博客樣式吧!

在這基礎上博客頁面又加了自己寫的canvas動畫,應該是足夠優雅了~
這裏寫圖片描述

Demo

登陸後臺按鈕在頁面最下方“站長登陸”,可以以遊客身份登入後臺系統。

源碼

用到的技術和實現思路
前端:Vue全家桶

Vue.js
Vue-Cli
Vue-Resource
Vue-Validator
Vue-Router
Vuex
Vue-loader

後端

Node.js
mongoDB (mongoose)
Express

工具和語言

Webpack
ES6
SASS
Jade

整體思路

Node服務端除了主頁外,不做模板渲染,渲染交給瀏覽器完成
Node服務端不做任何路由切換的內容,這部分交給Vue-Router完成
Node服務端只用來接收請求,查詢數據庫並用來返回值

所以這樣做前後端幾乎完全解耦,只要約定好restful風格的數據接口,和數據存取格式就OK啦。

後端我用了mongoDB做數據庫,並在Express中通過mongoose操作mongoDB,省去了複雜的命令行,通過Javascript操作無疑方便了很多。

簡單的說一下Vue的各個插件:

Vue-Cli:官方的腳手架,用來初始化項目
Vue-Resource:可以看作一個Ajax庫,通過在跟組件引入,可以方便的注入子組件。子組件以this.$http調用
Vue-Validator:用來驗證表單
Vue-Router:官方的路由工具,用來切換子組件,是用來做SPA應用的關鍵
Vuex:控制組件中數據的流動,使得數據流動更加清晰,有跡可循。通過官方的vue-devtools可以無縫對接
Vue-loader:webpack中對Vue文件的加載器

文件目錄
這裏寫圖片描述
我將前端的文件統一放到了src目錄下,其中的mian.js是webpack的入口。

所有頁面分割成一個單一的vue組件,放在componentss中,通過入口文件mian.js,由webpack打包生成,生成的文件放在public文件夾下。

後端文件放在server文件夾內,這就是基於Express的node服務器,在server文件夾內執行

node www

就可以啓動Node服務器,默認偵聽3000端口。
關於Vue-Cli
我只是使用了simple的template,相對於默認的template,simple的配置簡單得多,但已經有了瀏覽器自動刷新,生產環境代碼壓縮等功能。

vue init simple CMS-of-Blog

以下是Vue-Cli生成的webpack的配置文件,我只做了一點小改動。

話說React.js裏就沒用像Vue-Cli這樣好用的腳手架,還是Vue.js簡單優雅。。

Webpack.config.js

var path = require('path')
var webpack = require('webpack')

module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, './public'),
        publicPath: '/public/',
        filename: 'build.js',
    },
    resolveLoader: {
        root: path.join(__dirname, 'node_modules'),
    },
    module: {
        loaders: [
            {
                test: /\.vue$/,
                loader: 'vue'
            },
            {
                test: /\.js$/,
                loader: 'babel',
                exclude: /node_modules/
            },
            {
                test: /\.json$/,
                loader: 'json'
            },
            {
                test: /\.html$/,
                loader: 'vue-html'
            },
            {
                test: /\.(png|jpg|gif|svg)$/,
                loader: 'url',
                query: {
                    limit: 10000,
                    name: '[name].[ext]?[hash]'
                }
            }
            , {
                test: /\.(woff|svg|eot|ttf)\??.*$/,
                loader: 'url-loader?limit=50000&name=[path][name].[ext]'
            }
        ]
    },
    babel: {
        presets: ['es2015'],
    },
    devServer: {
        historyApiFallback: true,
        noInfo: true
    },
    devtool: '#eval-source-map'
}

if (process.env.NODE_ENV === 'production') {
    module.exports.devtool = '#source-map'
    // http://vue-loader.vuejs.org/en/workflow/production.html
    module.exports.plugins = (module.exports.plugins || []).concat([
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: '"production"'
            }
        }),
        new webpack.optimize.UglifyJsPlugin({
            output: {
                comments: false,
            },
            compress: {
                warnings: false
            }
        }),
        new webpack.optimize.OccurenceOrderPlugin()
    ])
}

可以看出尤大大幫我們把瀏覽器自動刷新,熱加載和生產環境的代碼壓縮都寫好了,簡直超貼心。

然而實際項目中,我還是碰到一個麻煩的問題,下面是package.json中的script腳本

"scripts": {
    "dev": "webpack-dev-server --inline --hot",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
    "watch": "webpack --progress --color --watch",
    "server": "supervisor ./server/www"
  },

運行

npm run dev

後,瀏覽器在8080端口開了一個服務器,然而這個服務器是用來服務前端頁面的,也就是說,從這裏啓動服務器而不是開啓Node服務器會造成數據無法交互,畢竟這個服務器不能連接數據庫。但這個端口是可以在文件修改之後自動刷新瀏覽器的。

然後,通過分別執行

npm run watch
npm run server

來偵聽文件改動,並重啓Node服務器,此時瀏覽器是不能自動刷新的。找了一些方法但終歸沒解決。習慣了自動瀏覽器刷新,碰到這種情況蠻蛋疼的。

於是只好這樣:

在修改樣式的時候使用8080端口的服務器,在修改數據交互的時候手動刷新在3000端口服務器的瀏覽器。

雖然不是很方便,但至少通過supervisor不用自己重啓Node服務器了。。。

關於Vue-Router
因爲寫的是但也應用(SPA),服務器不負責路由,所以路由方面交給Vue-Router來控制。

下面是根組件,路由控制就在這裏,組件掛載在body元素下:

main.js

let router = new VueRouter()

router.map({
    '/': {
        component: Archive
    },
    'login': {
        component: Login
    },
    '/article': {
        component: Article
    },
    '/console': {
        component: Console,
        subRoutes: {
            '/': {
                component: ArticleList
            },
            '/editor': {
                component: Editor
            },
            '/articleList': {
                component: ArticleList
            },
            '/menu': {
                component: Links
            },
            'account': {
                component: Account
            },
        },
    },
})
let App = Vue.extend({
    data(){
        return {}
    },
    components: {Waiting,Pop,NightSky,MyCanvas},
    http: {
        root: '/'
    },
    computed: {
        waiting: ()=>store.state.waiting,
        pop:()=>store.state.popPara.pop,
        bg:()=>store.state.bg,
    },
    store
})

router.start(App, 'body')

對應的文檔首頁 index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <title>Blog-CMS</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <waiting v-if="waiting"></waiting>
    <pop v-show="pop"></pop>
    <component :is="bg"></component>
    <router-view></router-view>
    <script src="public/build.js"></script>
  </body>
</html>

可以看到路由控制在body元素下的router-view中。之前的waiting,pop和component元素分別是等待效果(就是轉圈圈)的彈出層,信息的彈出層,和背景樣式的切換。

其實這個index.html是有express通過jade生成的,實際項目中並沒有html文件,我是把生成好的html放在這裏方便展示。

關於Vue-loader
Vue-loader是Vue官方支持webpack的工具,用來將組件寫在一個文件裏。之前的目錄中,有很多分割好的vue文件,每一個文件是一個獨立的組件。

比如,這是一個彈出層的組件:

Pop.vue

<template>
    <div class="shade">
        <div class="content">
            <p>{{getPopPara.content}}</p>
            <div class="button">
                <button class="ok" @click="ok">確定</button>
                <button class="cancel" @click="cancel"
                        v-if="getPopPara.cb2">取消
                </button>
            </div>
        </div>
    </div>
</template>

<script>
    import {getPopPara} from '../vuex/getters'
    export default{
        vuex: {
            getters: {
                getPopPara,
            }
        },
        methods: {
            ok(){
                let fn = this.getPopPara.cb1
                typeof fn == 'function' && fn()
            },
            cancel(){
                let fn = this.getPopPara.cb2
                typeof fn == 'function' && fn()
            }
        }

    }
</script>
<style lang="sass">
    @import "../SCSS/Pop.scss";
</style>

每一個vue文件都有三個部分(其實是可選的),分別是template,script和style,這也很好理解,就是把html,JS和CSS合併在一起寫了嘛。

這個彈窗組件,通過vuex獲得其他組件傳遞過來的參數,參數是一個對象,包括彈出層的展示信息和點擊確定或取消時的回調函數。

因爲編輯器不支持在vue文件中用sass語法,所以我把sass文件放在外部,通過@import引入。

關於Vue-Resource
Vue-Resource可以看成一個與Vue集成的Ajax庫,用來創建xhr和獲取xhr的response。

因爲和Vue高度集成,所以在vue組件中使用很方便。

Article.vue

<template>
    <div class="wrap">
        <my-header></my-header>
        <section class="article">
            <article class="post-block">
                <div class="post-title">{{title}}</div>
                <div class="post-info">{{date}}</div>
                <div class="post-content"
                 v-html="content | marked">
                </div>
            </article>
        </section>
        <my-footer></my-footer>
    </div>
</template>
<script>
    import myHeader     from './MyHeader.vue'
    import myFooter     from './MyFooter.vue'
    import marked       from '../js/marked.min.js'
    import {bgToggle}   from '../vuex/actions'

    export default{
        data(){
            return {
                title: '',
                date: '',
                content: ''
            }
        },
        filters: {
            marked
        },
        created(){
            let id = this.$route.query.id
            this.$http.get('/article?id=' + id)
                    .then((response)=> {
                        let body = JSON.parse(response.body)
                        this.content = body.content
                        this.title = body.title
                        let d = new Date(body.date)
                        this.date = d.getFullYear() + '年' +
                                (d.getMonth() + 1) + '月' +
                                d.getDate() + '日'
                    }, (response)=> {
                        console.log(response)
                    })
        },
        components: {
            myHeader,
            myFooter
        },
        ready(){
            this.bgToggle('MyCanvas')
        },
        vuex:{
            actions:{
                bgToggle
            }
        }
    }
</script>
<style lang="sass">
    @import "../SCSS/Article.scss";
</style>

Article.vue組件種在created生命週期時創建併發送了一個xhr的get請求,在獲取成果後把response對象中的屬性在賦值給data中相應的屬性,vue會自動更新視圖。

博客所支持的markdown語法的關鍵所在也在這個組件裏。

<div class="post-content">
       {{{content | marked}}}
</div>

通過引入markdown的filter使得輸出的html直接被轉換成html結構,還是很方便的。

關於後端
後端是用node.js作爲服務器的,使用了最流行Express框架。

主體是由Express生成,本身十分精簡。在實踐中修改的地方主要是添加了各種前端發送的get和post請求。

router.get('/article', function (req, res, next) {
    var id = req.query.id
    db.Article.findOne({_id: id}, function (err, doc) {
        if (err) {
            return console.log(err)
        } else if (doc) {
            res.send(doc)
        }
    })
})

比如這個請求處理來自前端的get請求,通過mongoose來查詢數據庫並返回數據。

前端頁面通過promise控制異步操作,把得到的數據放入組件的data對象中,Vue偵測變化並更新視圖。

數據庫的初始化文件放在了init.js中,第一次運行的時候會新建名爲admin的用戶,初始密碼爲111,可以在控制檯的賬號管理中修改。

後記
其實還有很多很多沒有在這篇文章提及的地方。畢竟這個博客框架相對是比較大的東西。

寫過這個博客管理器後,感受還是蠻多的,對Vue.js中的數據綁定,組件化和數據流瞭解的更深入了一層,同時也對Node.js的後端有了一次優雅的實踐。

所以,學過東西之後,實踐是非常有必要的。前端很多時候就是不斷踩坑的過程。一路踩坑再爬坑,獲益匪淺。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章