Vue + Node + Mongodb 开发一个完整博客流程

Posted SegmentFault

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue + Node + Mongodb 开发一个完整博客流程相关的知识,希望对你有一定的参考价值。

前言

前段时间刚把自己的个人网站写完, 于是这段时间因为事情不是太多,便整理了一下,写了个简易版的博客系统。

服务端用的是 koa2框架 进行开发。

技术栈

Vue + vuex + element-ui + webpack + nodeJs + koa2 + mongodb

目录结构讲解

说明:

  • build - webpack的配置文件

  • code - 放置代码文件

  • config - 项目参数配置的文件

  • logs - 日志打印文件

  • node_modules - 项目依赖模块

  • public - 项目静态文件的入口 例如: public下的 demo.html文件, 可通过 localhost:3000/demo.html 访问

  • static - 静态资源文件

  • .babelrc - babel编译

  • postcss.config.js - css后处理器配置

build 文件讲解

Vue + Node + Mongodb 开发一个完整博客流程

说明:

  • build.js - 执行webpack编译任务, 还有打包动画 等等

  • get-less-variables.js - 解析less文件, 赋值less全局变量

  • style-loader.js - 样式loader配置

  • vue-config.js - vue配置

  • webpack.base.conf.js - webpack 基本通用配置

  • webpack.dev.conf.js - webpack 开发环境配置

  • webpack.prod.conf.js - webpack 生产环境配置

code 文件

Vue + Node + Mongodb 开发一个完整博客流程

1.admin - 后台管理界面源码

Vue + Node + Mongodb 开发一个完整博客流程

src - 代码区域:

  1. components - 组件

  2. filters - 过滤器

  3. font - 字体/字体图标

  4. images - 图片

  5. router - 路由

  6. store - vuex状态管理

  7. styles - 样式表

  8. utils - 请求封装

  9. views - 页面模块

  10. App.vue - app组件

  11. custom-components.js - 自定义组件导出

  12. main.js - 入口JS

  13. index.html - webpack 模板文件

2.client - web端界面源码

跟后台管理界面的结构基本一样。

3.server - 服务端源码

说明:

  1. controller: 所有接口逻辑代码

  2. middleware: 所有的中间件

  3. models: 数据库model

  4. router: 路由/接口

  5. app.js: 入口

  6. config.js: 配置文件

  7. index.js: babel编译

  8. mongodb.js: mongodb配置

其他文件

  • config - 项目参数配置的文件

  • logs - 日志文件

  • public - 项目静态文件的入口

  • static - 静态资源文件

  • .babelrc - babel编译

  • postcss.config.js - css后处理器配置

后台管理

开发中用的一些依赖模块
  • vue/vue-router/vuex - Vue全家桶

  • axios - 一个现在主流并且很好用的请求库 支持Promise

  • qs - 用于解决axios POST请求参数的问题

  • element-ui - 饿了么出品的vue2.0 pc UI框架

  • babel-polyfill - 用于实现浏览器不支持原生功能的代码

  • highlight.js / marked- 两者搭配实现Markdown的常用语法

  • js-md5 - 用于登陆时加密

  • nprogress - 顶部加载条

components

这个文件夹一般放入常用的组件, 比如 Loading组件等等。

views

所有模块页面。

store

vuex 用来统一管理公用属性, 和统一管理接口。

登陆

登陆是采用 jsonwebtoken方案 来实现整个流程的。

1. jwt.sign(payload,secretOrPrivateKey,[options,callback]) 生成TOKEN

2. jwt.verify(token,secretOrPublicKey,[options,callback]) 验证TOKEN

3.获取用户的账号密码。

4.通过 jwt.sign 方法来生成token:

 
   
   
 
  1.    //server端

  2.    import jwt from 'jsonwebtoken'

  3.    let data = { //用户信息

  4.        username,

  5.        roles,

  6.        ...

  7.    }

  8.    let payload = { // 可以把常用信息存进去

  9.        id: data.userId, //用户ID

  10.        username: data.username, // 用户名

  11.        roles: data.roles // 用户权限

  12.    },

  13.    secret = 'admin_token'

  14.    // 通过调用 sign 方法, 把 **用户信息**、**密钥** 生成token,并设置过期时间

  15.    let token = jwt.sign(payload, secret, {expiresIn: '24h'})

  16.    // 存入cookie发送给前台

  17.    ctx.cookies.set('Token-Auth', token, {httpOnly: false })

5.每次请求数据的时候通过 jwt.verify 检测token的合法性 jwt.verify(token,secret)

权限

通过不同的权限来动态修改路由表。

通过 vue的 钩子函数 beforeEach 来控制并展示哪些路由, 以及判断是否需要登陆。

 
   
   
 
  1.  import store from '../store'

  2.  import { getToken } from 'src/utils/auth'

  3.  import { router } from './index'

  4.  import NProgress from 'nprogress' // Progress 进度条

  5.  import 'nprogress/nprogress.css' // Progress 进度条样式

  6.  const whiteList = ['/login'];

  7.  router.beforeEach((to, from, next) => {

  8.      NProgress.start()

  9.      if (getToken()) { //存在token

  10.          if (to.path === '/login') { //当前页是登录直接跳过进入主页

  11.              next('/')

  12.          }else{

  13.              if (!store.state.user.roles) { //拉取用户信息

  14.                  store.dispatch('getUserInfo').then( res => {

  15.                      let roles = res.data.roles

  16.                      store.dispatch('setRoutes', {roles}).then( () => { //根据权限动态添加路由

  17.                          router.addRoutes(store.state.permission.addRouters)

  18.                          next({ ...to }) //hash模式  确保路由加载完成

  19.                      })

  20.                  })

  21.              }else{

  22.                  next()

  23.              }

  24.          }

  25.      }else{

  26.          if (whiteList.indexOf(to.path) >= 0) { //是否在白名单内,不在的话直接跳转登录页

  27.              next()

  28.          }else{

  29.              next('/login')

  30.          }

  31.      }    

  32.  })

  33.  router.afterEach((to, from) => {

  34.      document.title = to.name

  35.      NProgress.done()

  36.  })

  37.  export default router

通过调用 getUserInfo方法传入 token 获取用户信息, 后台直接解析 token 获取里面的信息返回给前台。

 
   
   
 
  1.  getUserInfo ({state, commit}) {

  2.      return new Promise( (resolve, reject) => {

  3.          axios.get('user/info',{

  4.              token: state.token

  5.          }).then( res => {

  6.              commit('SET_USERINFO', res.data)

  7.              resolve(res)

  8.          }).catch( err => {

  9.              reject(err)

  10.          })

  11.      })

  12.  }

通过调用 setRoutes方法 动态生成路由。

 
   
   
 
  1.  import { constantRouterMap, asyncRouterMap } from 'src/router'

  2.  const hasPermission = (roles, route) => {

  3.      if (route.meta && route.meta.role) {

  4.          return roles.some(role => route.meta.role.indexOf(role) >= 0)

  5.      } else {

  6.          return true

  7.      }

  8.  }

  9.  const filterAsyncRouter = (asyncRouterMap, roles) => {

  10.      const accessedRouters = asyncRouterMap.filter(route => {

  11.          if (hasPermission(roles, route)) {

  12.              if (route.children && route.children.length) {

  13.                  route.children = filterAsyncRouter(route.children, roles)

  14.              }

  15.              return true

  16.          }

  17.          return false

  18.      })

  19.      return accessedRouters

  20.  }

  21.  const permission = {

  22.      state: {

  23.          routes: constantRouterMap.concat(asyncRouterMap),

  24.          addRouters: []

  25.      },

  26.      mutations: {

  27.          SETROUTES(state, routers) {

  28.              state.addRouters = routers;

  29.              state.routes = constantRouterMap.concat(routers);

  30.          }

  31.      },

  32.      actions: {

  33.          setRoutes({ commit }, info) {

  34.              return new Promise( (resolve, reject) => {

  35.                  let {roles} = info;

  36.                  let accessedRouters = [];

  37.                  if (roles.indexOf('admin') >= 0) {

  38.                      accessedRouters = asyncRouterMap;

  39.                  }else{

  40.                      accessedRouters = filterAsyncRouter(asyncRouterMap, roles)

  41.                  }

  42.                  commit('SETROUTES', accessedRouters)

  43.                  resolve()

  44.              })

  45.          }

  46.      }

  47.  }

  48.  export default permission

axios 请求封装,统一对请求进行管理
 
   
   
 
  1.  import axios from 'axios'

  2.  import qs from 'qs'

  3.  import { Message } from 'element-ui'

  4.  axios.defaults.withCredentials = true

  5.  // 发送时

  6.  axios.interceptors.request.use(config => {

  7.      // 开始(LLoading动画..)

  8.      return config

  9.  }, err => {

  10.      return Promise.reject(err)

  11.  })

  12.  // 响应时

  13.  axios.interceptors.response.use(response => response, err => Promise.resolve(err.response))

  14.  // 检查状态码

  15.  function checkStatus(res) {

  16.      // 结束(结束动画..)

  17.      if (res.status === 200 || res.status === 304) {

  18.          return res.data

  19.      }

  20.      return {

  21.          code: 0,

  22.          msg: res.data.msg || res.statusText,

  23.          data: res.statusText

  24.      }

  25.      return res

  26.  }

  27.  // 检查CODE值

  28.  function checkCode(res) {

  29.      if (res.code === 0) {

  30.          Message({

  31.            message: res.msg,

  32.            type: 'error',

  33.            duration: 2 * 1000

  34.          })

  35.          throw new Error(res.msg)

  36.      }

  37.      return res

  38.  }

  39.  const prefix = '/admin_demo_api/'

  40.  export default {

  41.      get(url, params) {

  42.          if (!url) return

  43.          return axios({

  44.              method: 'get',

  45.              url: prefix + url,

  46.              params,

  47.              timeout: 30000

  48.          }).then(checkStatus).then(checkCode)

  49.      },

  50.      post(url, data) {

  51.          if (!url) return

  52.          return axios({

  53.              method: 'post',

  54.              url: prefix + url,

  55.              data: qs.stringify(data),

  56.              timeout: 30000

  57.          }).then(checkStatus).then(checkCode)

  58.      },

  59.      postFile(url, data) {

  60.          if (!url) return

  61.          return axios({

  62.              method: 'post',

  63.              url: prefix + url,

  64.              data

  65.          }).then(checkStatus).then(checkCode)

  66.      }

  67.  }

面包屑 / 标签路径
  • 通过检测路由来把当前路径转换成面包屑。

  • 把访问过的路径储存在本地,记录下来,通过标签直接访问。

 
   
   
 
  1.  // 面包屑

  2.  getBreadcrumb() {

  3.      let matched = this.$route.matched.filter(item => item.name);

  4.      let first = matched[0],

  5.          second = matched[1];

  6.      if (first && first.name !== '首页' && first.name !== '') {

  7.          matched = [{name: '首页', path: '/'}].concat(matched);

  8.      }

  9.      if (second && second.name === '首页') {

  10.          this.levelList = [second];

  11.      }else{

  12.          this.levelList = matched;

  13.      }

  14.  }

  15.  // 检测路由变化

  16.  watch: {

  17.      $route() {

  18.          this.getBreadcrumb();

  19.      }

  20.  }

上面介绍了几个主要以及必备的后台管理功能,其余的功能模块 按照需求增加就好

前台

前台展示的页面跟后台管理界面差不多, 也是用vue+webpack搭建,基本的结构都差不多,具体代码实现的可以直接在github下载便行。

server端

权限

主要是通过 jsonwebtoken 的verify方法检测 cookie 里面的 token 验证它的合法性。

 
   
   
 
  1.  import jwt from 'jsonwebtoken'

  2.  import conf from '../../config'

  3.  export default () => {

  4.      return async (ctx, next) => {

  5.          if ( conf.auth.blackList.some(v => ctx.path.indexOf(v) >= 0) ) { // 检测是否在黑名单内

  6.              let token = ctx.cookies.get(conf.auth.tokenKey);

  7.              try {

  8.                  jwt.verify(token, conf.auth.admin_secret);

  9.              }catch (e) {

  10.                  if ('TokenExpiredError' === e.name) {

  11.                      ctx.sendError('token已过期, 请重新登录!');

  12.                      ctx.throw(401, 'token expired,请及时本地保存数据!');

  13.                  }

  14.                  ctx.sendError('token验证失败, 请重新登录!');

  15.                  ctx.throw(401, 'invalid token');

  16.              }

  17.              console.log("鉴权成功");

  18.          }

  19.          await next();

  20.      }

  21.  }

日志日志是采用 log4js 来进行管理的, log4js 算 nodeJs 常用的日志处理模块,用起来额也比较简单。

log4js 的日志分为九个等级,各个级别的名字和权重如下:

1.图。

2.设置 Logger 实例的类型 logger=log4js.getLogger('cheese')

3.通过 Appender 来控制文件的 名字、路径、类型 。

4.配置到 log4js.configure

5.便可通过 logger 上的打印方法 来输出日志了 logger.info(JSON.stringify(currTime:当前时间为${Date.now()}s ))

 
   
   
 
  1.  //指定要记录的日志分类

  2.  let appenders = {}

  3.  appenders.all = {

  4.      type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符

  5.      filename: `${dir}/all/`, //日志文件名,可以设置相对路径或绝对路径

  6.      pattern: 'task-yyyy-MM-dd.log', //占位符,紧跟在filename后面  

  7.      alwaysIncludePattern: true //是否总是有后缀名

  8.  }

  9.  let logConfig = {

  10.      appenders,

  11.      /**

  12.       * 指定日志的默认配置项

  13.       * 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项

  14.       */

  15.      categories: {

  16.          default: {

  17.              appenders: Object.keys(appenders),

  18.              level: logLevel

  19.          }

  20.      }

  21.  }

  22.  log4js.configure(logConfig)

定制书写规范(API)

设计思路

当应用程序启动时候,读取指定目录下的 js 文件,以文件名作为属性名,挂载在实例 app 上,然后把文件中的接口函数,扩展到文件对象上。

 
   
   
 
  1.    //other.js

  2.    const path = require('path');

  3.    module.exports = {

  4.        async markdown_upload_img (ctx, next) {

  5.            console.log('----------------添加图片 markdown_upload_img-----------------------');

  6.            let opts = {

  7.                path: path.resolve(__dirname, '../../../../public')

  8.            }

  9.            let result = await ctx.uploadFile(ctx, opts)

  10.            ctx.send(result)

  11.        },

  12.        async del_markdown_upload_img (ctx, next) {

  13.            console.log('----------------删除图片 del_markdown_upload_img-----------------------');

  14.            let id = ctx.request.query.id

  15.            try {

  16.                ctx.remove(musicModel, {_id: id})

  17.                ctx.send()

  18.            }catch(e){

  19.                ctx.sendError(e)

  20.            }

  21.            // console.log(id)

  22.        }

  23.    }

读取出来的便是以下形式:

app.controller.admin.other.markdown_upload_img

便能读取到 markdown_upload_img 方法。

 
   
   
 
  1.    async markdown_upload_img (ctx, next) {

  2.        console.log('----------------添加图片 markdown_upload_img-----------------------');

  3.        let opts = {

  4.            path: path.resolve(__dirname, '../../../../public')

  5.        }

  6.        let result = await ctx.uploadFile(ctx, opts)

  7.        ctx.send(result)

  8.    }

在把该形式的方法赋值过去就行:

router.post('/markdown_upload_img',app.controller.admin.other.markdown_upload_img)

通过 mongoose 链接 mongodb
 
   
   
 
  1.  import mongoose from 'mongoose'

  2.  import conf from './config'

  3.  // const DB_URL = `mongodb://${conf.mongodb.address}/${conf.mongodb.db}`

  4.  const DB_URL = `mongodb://${conf.mongodb.username}:${conf.mongodb.pwd}@${conf.mongodb.address}/${conf.mongodb.db}`; // 账号登陆

  5.  mongoose.Promise = global.Promise

  6.  mongoose.connect(DB_URL, { useMongoClient: true }, err => {

  7.      if (err) {

  8.          console.log("数据库连接失败!")

  9.      }else{

  10.          console.log("数据库连接成功!")

  11.      }

  12.  })

  13.  export default mongoose

封装返回的send函数
 
   
   
 
  1.  export default () => {

  2.      let render = ctx => {

  3.          return (json, msg) => {

  4.              ctx.set("Content-Type", "application/json");

  5.              ctx.body = JSON.stringify({

  6.                  code: 1,

  7.                  data: json || {},

  8.                  msg: msg || 'success'

  9.              });

  10.          }

  11.      }

  12.      let renderError = ctx => {

  13.          return msg => {

  14.              ctx.set("Content-Type", "application/json");

  15.              ctx.body = JSON.stringify({

  16.                  code: 0,

  17.                  data: {},

  18.                  msg: msg.toString()

  19.              });

  20.          }

  21.      }

  22.      return async (ctx, next) => {

  23.          ctx.send = render(ctx);

  24.          ctx.sendError = renderError(ctx);

  25.          await next()    

  26.      }

  27.  }

通过 koa-static 管理静态文件入口

注意事项:

1. cnpm run server 启动服务器

2.启动时,记得启动mongodb数据库,账号密码 可以在 server/config.js 文件下进行配置

3. db.createUser({user:"cd",pwd:"123456",roles:[{role:"readWrite",db:'test'}]})(mongodb 注册用户)

4. cnpm run dev:admin 启动后台管理界面

5.登录后台管理界面录制数据

6.登录后台管理时需要在数据库 创建 users 集合注册一个账号进行登录

 
   
   
 
  1.  db.users.insert({

  2.      "name" : "cd",

  3.      "pwd" : "e10adc3949ba59abbe56e057f20f883e",

  4.      "username" : "admin",

  5.      "roles" : [

  6.          "admin"

  7.      ]

  8.  })

  9.  // 账号: admin  密码: 123456

7. cnpm run dev:client 启动前台页面

参考文章

个人博客:http://dzblog.cn/article/5a69609c3c04164b0bd4b964

github:https://github.com/cd-dongzi/vue-node-blog

基于Koa2搭建Node.js实战项目教程:https://github.com/ikcamp/koa2-tutorial

手摸手,带你用vue撸后台:https://segmentfault.com/a/1190000010043013


相关文章推荐




以上是关于Vue + Node + Mongodb 开发一个完整博客流程的主要内容,如果未能解决你的问题,请参考以下文章

Part1用JS写一个Blog(node + vue + mongoDB)

前端 Vue+Node+MongoDB高级全栈开发(完结)

推荐!前端 Vue+Node+MongoDB高级全栈开发(完结)

Vue+Node+MongoDB高级全栈开发

资源更新Vue+Node+MongoDB高级全栈开发全套教程

node+mongodb+vue实现一个写寄语小程序