美文网首页VueWeex开发技巧vue.js
一个Node.js+mongoDB+Vue.js的博客内容管理器

一个Node.js+mongoDB+Vue.js的博客内容管理器

作者: ciqulover | 来源:发表于2016-09-04 00:05 被阅读7173次

    在用过臃肿的WordPress后,一直想自己写一个轻便简约的博客内容管理器(CMS)。

    一拖再拖,在暑假开学前,终是完成了这么个玩意儿。

    嗯,我想完成的功能:

    • 一个基本的博客内容管理器功能,如后台登陆,发布并管理文章等
    • 支持markdown语法实时编辑
    • 支持代码高亮
    • 管理博客页面的链接
    • 博客页面对移动端适配优化
    • 账户管理(修改密码)

    Demo

    登陆后台按钮在页面最下方“站长登陆”,可以以游客身份登入后台系统。

    源码

    用到的技术和实现思路:

    前端:Vue全家桶

    • Vue.js
    • 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异步数据视图的部分,不过这一部能讲的太多,就不在这里展开了,可以看官方文档后,参考源代码的注释。

    相关文章

      网友评论

      • 3f6c24fe2a1c:先给个赞
      • 雨大桐小:能看一下楼主的源码吗?最近也在研究博客的搭建,谢谢啦!
      • 寒江水kevin:项目开源吗?也正准备搭一个😀😀
      • 65bf6cefddc0:我想问一下,为什么在login.vue中手动@import "./../../style/variables",这个variables不是已经在main.scss中引入到项目中去了吗?
      • 腿毛崩你一脸:你是学生?
        腿毛崩你一脸:@ycwalker 我看了你的GitHub 谢谢
        腿毛崩你一脸:@ycwalker 能看一下你的源码么 ?我现在就在用node + vue 可能还要用到mongoDB
        ciqulover:只有学生有时间瞎折腾啊
      • fendo:赞一个!
      • August_____:单页,写成但也了:hushed:
      • 9b8660ff2b23:给力!!! 正在学习这一块 收获很大
      • 201e89183035:不能自动刷新的问题。在express()之后,去use webpack-dev-middleware和webpack-hot-middleware, 然后webpack就用自动编译并watch和hot-load,缺点就是改了你的jade模板应该并不会加载。
        可以看这个:http://acgtofe.com/posts/2016/02/full-live-reload-for-express-with-webpack
        ciqulover:@微雨落心开 谢谢你提供的思路!有空慢慢研究,感谢~~
        201e89183035:@微雨落心开 或者尤大的vue-cli webpack模板,里面就是用这两个来实现热加载的。
        lz还没毕业就这么厉害了,好佩服。
      • 8c5aeae9da19:嗯,谢谢,我在摸索摸索
      • 8c5aeae9da19:为什么在本地正常上传到服务器会出现这个错啊
        2016-09-09T11:19:34.68+0800 [App/0] ERR module.js:341
        2016-09-09T11:19:34.68+0800 [App/0] ERR throw err;
        2016-09-09T11:19:34.68+0800 [App/0] ERR ^
        2016-09-09T11:19:34.68+0800 [App/0] ERR Error: Cannot find module 'express'
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Function.Module._resolveFilename (module.js:339:15)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Function.Module._load (module.js:290:25)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Module.require (module.js:367:17)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at require (internal/module.js:20:19)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Object.<anonymous> (/home/vcap/app/server/app.js:1:77)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Module._compile (module.js:413:34)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Object.Module._extensions..js (module.js:422:10)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Module.load (module.js:357:32)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Function.Module._load (module.js:314:12)
        2016-09-09T11:19:34.68+0800 [App/0] ERR at Module.require (module.js:367:17)
        2016-09-09T11:19:34.68+0800 [App/0] OUT
        ciqulover:@吴先生想要的还没有得到 express是Node.js服务器用的模块,并不是只是开发时的依赖,所以是需要的
        8c5aeae9da19: @ycwalker 上传到服务器也要安装吗
        ciqulover:@吴先生想要的还没有得到 Cannot find module 'express',没找到express模块,确保已经安装好依赖
      • e6057eb8d3f6:学习了。 :smile:
      • 03cb2024e94d:不明觉厉

      本文标题:一个Node.js+mongoDB+Vue.js的博客内容管理器

      本文链接:https://www.haomeiwen.com/subject/uuqlettx.html