专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  快!快!快!DeepSeek 满血版真是快 ·  昨天  
码农翻身  ·  漫画 | 为什么大家都愿意进入外企? ·  昨天  
程序猿  ·  “我真的受够了Ubuntu!” ·  4 天前  
程序猿  ·  “未来 3 年内,Python 在 AI ... ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

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

SegmentFault思否  · 公众号  · 程序员  · 2018-02-03 08:00

正文

前言

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

服务端用的是 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 - 代码区域:

  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.  }







请到「今天看啥」查看全文