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

項目來源

之前曾用過WordPress搭建本身的博客網站,但感受WordPress非常臃腫。因此一直想本身寫一個博客內容管理器。css

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

嗯,我想完成的功能:

  • 一個基本的博客內容管理器功能,如後臺登錄,發佈並管理文章等前端

  • 支持markdown語法實時編輯vue

  • 支持代碼高亮node

  • 管理博客頁面的連接webpack

  • 博客頁面對移動端適配優化git

  • 帳戶管理(修改密碼)github

Demo

登錄後臺按鈕在頁面最下方「站長登錄」,能夠以遊客身份登入後臺系統。web

源碼

用到的技術和實現思路:

前端:Vue全家桶

  • Vue.jsvue-router

  • Vue-Cli

  • Vue-Resource

  • Vue-Router

  • Vuex

後端:Node

  • Node.js

  • mongoDB (mongoose)

  • Express

工具和語言

  • Webpack

  • ES6

  • SASS

總體思路:

  • Node服務端不作路由切換,這部分交給Vue-Router完成

  • Node服務端只用來接收請求,查詢數據庫並用來返回值

因此這樣作先後端幾乎徹底解耦,只要約定好restful數據接口,和數據存取格式就OK啦。

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

Vue的各個插件:

  • vue-cli:官方的腳手架,用來初始化項目

  • vue-resource:能夠看做一個Ajax庫,經過在跟組件引入,能夠方便的注入子組件。子組件以this.$http調用

  • vue-router:官方的路由工具,用來切換子組件,是用來作SPA應用的關鍵

  • vuex:規範組件中數據流動,主要用於異步的http請求後數據的刷新。經過官方的vue-devtools能夠無縫對接

文件目錄

│  .babelrc           babel配置
│  .editorconfig
│  .eslintignore  
│  .eslintrc.js       eslintrc配置
│  .gitignore
│  index.html         入口頁面
│  package.json
│  README.md
│  setup.html         初始化帳戶頁面
│  webpack.config.js  webpack配置
│
├─dist                打包生成
│     
├─server              服務端
│      api.js         Restful接口
│      db.js          數據庫
│      index.js
│      init.json      初始數據
│
└─src
    │  main.js        項目入口
    │  setup.js       初始化帳戶
    │
    ├─assets          外部引用文件
    │  ├─css
    │  ├─fonts
    │  ├─img
    │  └─js         
    │
    ├─components      vue組件
    │  ├─back         博客控制檯組件
    │  ├─front        博客頁面組件
    │  └─share        公共組件
    │
    ├─router          路由
    │
    ├─store           vuex文件
    │
    └─style           全局樣式

前端的文件統一放到了src目錄下,有兩個入口文件,分別是main.jssetup.js,有過WordPress經驗應該知道,第一次進入博客是須要設置用戶名密碼和數據庫的,這裏的setup.js就是第一次登入時的頁面腳本,而main.js則是剩餘全部文件的入口

main.js

import Vue          from 'vue'
import VueResource  from 'vue-resource'
import {mapState}   from 'vuex'

//三個頂級組件,博客主頁和控制檯共享
import Spinner      from './components/share/Spinner.vue'
import Toast        from './components/share/Toast.vue'
import MyCanvas     from './components/share/MyCanvas.vue'

import store        from './store'
import router       from './router'

import './style/index.scss'

Vue.use(VueResource)

new Vue({
  router,
  store,
  components: {Spinner, Toast, MyCanvas},
  computed: mapState(['isLoading', 'isToasting'])
}).$mount('#CMS2')

然後全部頁面分割成一個單一的vue組件,放在components中,經過入口文件main.js,由webpack打包生成,生成的文件放在dist文件夾下。

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

node index

就能夠啓動Node服務器,默認偵聽3000端口。

 關於 Webpack

Webpack的配置文件主體是有vue-cli生成的,但爲了配合後端自動刷新、支持Sass和生成獨立的css文件,稍微修改了一下:

webpack.config.js

const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
//萃取css文件,在此命名
const extractCSSFromVue = new ExtractTextPlugin('styles.css')
const extractCSSFromSASS = new ExtractTextPlugin('index.css')

module.exports = {
  entry: {
    main: './src/main.js',
    setup: './src/setup.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: '[name].js'
  },
  resolveLoader: {
    moduleExtensions: ['-loader']
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue',
        //使用postcss處理加工後的scss文件
        options: {
          preserveWhitespace: false,
          postcss: [
            require('autoprefixer')({
              browsers: ['last 3 versions']
            })
          ],
          loaders: {
            sass: extractCSSFromVue.extract({
              loader: 'css!sass!',
              fallbackLoader: 'vue-style-loader'
            })
          }
        }
      },
      {
        test: /\.scss$/,
        loader: extractCSSFromSASS.extract(['css', 'sass'])
      },
      {
        test: /\.js$/,
        loader: 'babel',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file',
        options: {
          name: '[name].[ext]?[hash]'
        }
      },
      //字體文件
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000&mimetype=application/font-woff'
      },
      {
        test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'file-loader'
      }
    ]
  },
  plugins: [
      //取出css生成獨立文件
    extractCSSFromVue,
    extractCSSFromSASS,
    new CopyWebpackPlugin([
      {from: './src/assets/img', to: './'}
    ])
  ],
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue'
    }
  },
  //服務器代理,便於開發時全部http請求轉到node的3000端口,而不是前端的8080端口
  devServer: {
    historyApiFallback: true,
    noInfo: true,
    proxy: {
      '/': {
        target: 'http://localhost:3000/'
      }
    }
  },
  devtool: '#eval-source-map'
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

運行

npm start

後,node端開啓了3000端口,接着運行

npm run dev

打開webpack在8080端口服務器,具備動態加載的功能,而且全部的http請求會代理到3000端口

關於Vue-Router

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

router.js

import Vue      from 'vue'
import Router   from 'vue-router'
//博客頁面
import Archive  from '../components/front/Archive.vue'
import Article  from '../components/front/Article.vue'
//控制檯頁面
import Console  from '../components/back/Console.vue'
import Login    from '../components/back/Login.vue'
import Articles from '../components/back/Articles.vue'
import Editor   from '../components/back/Editor.vue'
import Links    from '../components/back/Links.vue'
import Account  from '../components/back/Account.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {path: '/archive', name: 'archive', component: Archive},
    {path: '/article', name: 'article', component: Article},
    {path: '/', component: Login},
    {
      path: '/console',
      component: Console,
      children: [
        {path: '', component: Articles},
        {path: 'articles', name: 'articles', component: Articles},
        {path: 'editor', name: 'editor', component: Editor},
        {path: 'links', name: 'links', component: Links},
        {path: 'account', name: 'account', component: Account}
      ]
    }
  ]
})

文檔首頁  

index.html  

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>cms2simple</title>
    <link rel="stylesheet" href="dist/index.css">
    <link rel="stylesheet" href="dist/styles.css">
  </head>
  <body>
    <div id="CMS2" style="height: 100%">
      <my-canvas></my-canvas>
      <spinner v-show="isLoading"></spinner>
      <Toast v-show="isToasting"></Toast>
      <router-view ></router-view>
    </div>
    <script src="/dist/main.js"></script>
  </body>
</html>

能夠看到路由控制在body元素下的router-view中。前面的spinnertoast元素分別是等待效果(轉圈圈)的彈出層和信息的彈出層,和背景樣式的切換。

關於後端

後端是用node.js做爲服務器的,使用了express框架。

其中代碼很是簡單:

index.js

const fs = require('fs')
const path = require('path')
const express = require('express')
const favicon = require('serve-favicon')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const db = require('./db')
const resolve = file => path.resolve(__dirname, file)
const api = require('./api')
const app = express()

// const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

app.set('port', (process.env.port || 3000))
app.use(favicon(resolve('../dist/favicon.ico')))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false}))
app.use(cookieParser())
app.use('/dist', express.static(resolve('../dist')))
app.use(api)

app.post('/api/setup', function (req, res) {
  new db.User(req.body)
    .save()
    .then(() => {
      res.status(200).end()
      db.initialized = true
    })
    .catch(() => res.status(500).end())
})

app.get('*', function (req, res) {
  const fileName = db.initialized ? 'index.html' : 'setup.html'
  const html = fs.readFileSync(resolve('../' + fileName), 'utf-8')
  res.send(html)
})

app.listen(app.get('port'), function () {
  console.log('Visit http://localhost:' + app.get('port'))
})

服務器作的事情很簡單,畢竟路由在前端。在接受請求的時候判斷一下數據庫是否初始化,若是初始化就轉向主頁,不然轉向setup.html,之因此沒有直接sendfile是由於考慮到以後添加服務端渲染(雖然主頁並無啥值得渲染的,由於很簡單)

express框架中使用了mongoose來鏈接mongoDB數據庫,在接收請求時作對應的curd操做,好比這就是在接收保存文章時對應的操做:

api.js

router.post('/api/saveArticle', (req, res) => {
  const id = req.body._id
  const article = {
    title: req.body.title,
    date: req.body.date,
    content: req.body.content
  }
  if (id) {
    db.Article.findByIdAndUpdate(id, article, fn)
  } else {
    new db.Article(article).save()
  }
  res.status(200).end()
})

後記

固然還有不少沒說起的地方,最先寫這個博客管理器的時候用的仍是vue 1.x,後來用2.0改寫後文檔一直沒改,因此最近更新了一下,避免誤解。

其實整個管理器最複雜的地方時vuex異步數據視圖的部分,不過這一部能講的太多,就不在這裏展開了,能夠看官方文檔後,參考源代碼的註釋。

相關文章
相關標籤/搜索