美文网首页程序员之言Java 杂谈
Vue.js+Node+Mongodb开发一个完整博客流程

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

作者: java面试笔试 | 来源:发表于2018-08-28 10:20 被阅读33次

作者: 咚子

https://segmentfault.com/a/1190000013025450

前言

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

服务端用的是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 文件讲解

说明:

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 文件

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

src - 代码区域:

components - 组件

filters - 过滤器

font - 字体/字体图标

images - 图片

router - 路由

store - vuex状态管理

styles - 样式表

utils - 请求封装

views - 页面模块

App.vue - app组件

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

main.js - 入口JS

index.html - webpack 模板文件

2.client - web端界面源码

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

3.server - 服务端源码

说明:

controller: 所有接口逻辑代码

middleware: 所有的中间件

models: 数据库model

router: 路由/接口

app.js: 入口

config.js: 配置文件

index.js: babel编译

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:

   //server端

   import jwt from 'jsonwebtoken'

   let data = { //用户信息

       username,

       roles,

       ...

   }

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

       id: data.userId, //用户ID

       username: data.username, // 用户名

       roles: data.roles // 用户权限

   },

   secret = 'admin_token'

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

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

   // 存入cookie发送给前台

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

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

权限

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

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

 import store from '../store'

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

 import { router } from './index'

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

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

 const whiteList = ['/login'];

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

     NProgress.start()

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

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

             next('/')

         }else{

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

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

                     let roles = res.data.roles

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

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

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

                     })

                 })

             }else{

                 next()

             }

         }

     }else{

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

             next()

         }else{

             next('/login')

         }

     }    

 })

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

     document.title = to.name

     NProgress.done()

 })

 export default router

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

 getUserInfo ({state, commit}) {

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

         axios.get('user/info',{

             token: state.token

         }).then( res => {

             commit('SET_USERINFO', res.data)

             resolve(res)

         }).catch( err => {

             reject(err)

         })

     })

 }

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

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

 const hasPermission = (roles, route) => {

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

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

     } else {

         return true

     }

 }

 const filterAsyncRouter = (asyncRouterMap, roles) => {

     const accessedRouters = asyncRouterMap.filter(route => {

         if (hasPermission(roles, route)) {

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

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

             }

             return true

         }

         return false

     })

     return accessedRouters

 }

 const permission = {

     state: {

         routes: constantRouterMap.concat(asyncRouterMap),

         addRouters: []

     },

     mutations: {

         SETROUTES(state, routers) {

             state.addRouters = routers;

             state.routes = constantRouterMap.concat(routers);

         }

     },

     actions: {

         setRoutes({ commit }, info) {

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

                 let {roles} = info;

                 let accessedRouters = [];

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

                     accessedRouters = asyncRouterMap;

                 }else{

                     accessedRouters = filterAsyncRouter(asyncRouterMap, roles)

                 }

                 commit('SETROUTES', accessedRouters)

                 resolve()

             })

         }

     }

 }

 export default permission

axios 请求封装,统一对请求进行管理

 import axios from 'axios'

 import qs from 'qs'

 import { Message } from 'element-ui'

 axios.defaults.withCredentials = true

 // 发送时

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

     // 开始(LLoading动画..)

     return config

 }, err => {

     return Promise.reject(err)

 })

 // 响应时

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

 // 检查状态码

 function checkStatus(res) {

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

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

         return res.data

     }

     return {

         code: 0,

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

         data: res.statusText

     }

     return res

 }

 // 检查CODE值

 function checkCode(res) {

     if (res.code === 0) {

         Message({

           message: res.msg,

           type: 'error',

           duration: 2 * 1000

         })

         throw new Error(res.msg)

     }

     return res

 }

 const prefix = '/admin_demo_api/'

 export default {

     get(url, params) {

         if (!url) return

         return axios({

             method: 'get',

             url: prefix + url,

             params,

             timeout: 30000

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

     },

     post(url, data) {

         if (!url) return

         return axios({

             method: 'post',

             url: prefix + url,

             data: qs.stringify(data),

             timeout: 30000

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

     },

     postFile(url, data) {

         if (!url) return

         return axios({

             method: 'post',

             url: prefix + url,

             data

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

     }

 }

面包屑 / 标签路径

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

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

 // 面包屑

 getBreadcrumb() {

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

     let first = matched[0],

         second = matched[1];

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

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

     }

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

         this.levelList = [second];

     }else{

         this.levelList = matched;

     }

 }

 // 检测路由变化

 watch: {

     $route() {

         this.getBreadcrumb();

     }

 }

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

前台

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

server端

权限

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

 import jwt from 'jsonwebtoken'

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

 export default () => {

     return async (ctx, next) => {

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

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

             try {

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

             }catch (e) {

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

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

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

                 }

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

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

             }

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

         }

         await next();

     }

 }

日志日志是采用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))。

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

 let appenders = {}

 appenders.all = {

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

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

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

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

 }

 let logConfig = {

     appenders,

     /**

      * 指定日志的默认配置项

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

      */

     categories: {

         default: {

             appenders: Object.keys(appenders),

             level: logLevel

         }

     }

 }

 log4js.configure(logConfig)

定制书写规范(API)

设计思路

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

   //other.js

   const path = require('path');

   module.exports = {

       async markdown_upload_img (ctx, next) {

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

           let opts = {

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

           }

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

           ctx.send(result)

       },

       async del_markdown_upload_img (ctx, next) {

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

           let id = ctx.request.query.id

           try {

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

               ctx.send()

           }catch(e){

               ctx.sendError(e)

           }

           // console.log(id)

       }

   }

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

app.controller.admin.other.markdown_upload_img

便能读取到markdown_upload_img方法。

   async markdown_upload_img (ctx, next) {

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

       let opts = {

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

       }

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

       ctx.send(result)

   }

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

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

通过 mongoose 链接 mongodb

 import mongoose from 'mongoose'

 import conf from './config'

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

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

 mongoose.Promise = global.Promise

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

     if (err) {

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

     }else{

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

     }

 })

 export default mongoose

封装返回的send函数

 export default () => {

     let render = ctx => {

         return (json, msg) => {

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

             ctx.body = JSON.stringify({

                 code: 1,

                 data: json || {},

                 msg: msg || 'success'

             });

         }

     }

     let renderError = ctx => {

         return msg => {

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

             ctx.body = JSON.stringify({

                 code: 0,

                 data: {},

                 msg: msg.toString()

             });

         }

     }

     return async (ctx, next) => {

         ctx.send = render(ctx);

         ctx.sendError = renderError(ctx);

         await next()    

     }

 }

通过 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 集合注册一个账号进行登录

 db.users.insert({

     "name" : "cd",

     "pwd" : "e10adc3949ba59abbe56e057f20f883e",

     "username" : "admin",

     "roles" : [

         "admin"

     ]

 })

 // 账号: 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

公众号:javafirst

相关文章

网友评论

    本文标题:Vue.js+Node+Mongodb开发一个完整博客流程

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