koa2+vue+mysql 全栈开发记录

栏目: Node.js · 发布时间: 7年前

内容简介:基于想要自己制作一个个人项目为由,于是有了这么一个开发记录(梳理开发过程也是一个知识巩固的过程)前端这边其实没什么好写的,主要是在

基于想要自己制作一个个人项目为由,于是有了这么一个开发记录(梳理开发过程也是一个知识巩固的过程)

koa2+vue2+mysql 个人的一个通用DEMO(本篇文章的范例)

koa2+vue2+mysql GITHUB地址

前端工具

  • vue
  • vue-router
  • vuex
  • axios
  • element ui 页面UI组件
  • echartsjs 百度强大的图表展示
  • vue-admin-template 花裤衩大佬的一个实用管理后台模版 配套教程
  • vue-i18n 国际化
  • scss

后端工具

解析 PUT / POST 请求中的 body
jwt鉴权
处理跨域
加密
时间处理

前端篇

前端这边其实没什么好写的,主要是在 vue-admin-template 基础上做了一些修改

src/utils/request.js的修改

  • request拦截器
// request拦截器
  service.interceptors.request.use(
    config => {
      if (store.getters.token) {
        // config.headers['X-Token'] = getToken()
        // 因为是jwt方式鉴权 所以在header头中改为如下写法
        config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
      }
      return config
    },
    error => {
      // Do something with request error
      console.log(error) // for debug
      Promise.reject(error)
    }
  )
复制代码
  • response 拦截器
// response 拦截器
  service.interceptors.response.use(
    response => {
      /**
      * code为非0是抛错 可结合自己业务进行修改
      */
      const res = response.data
      if (res.code !== 0) { // 因为后台返回值为0则是成功,所以将原来的20000改为了0
        Message({
          message: res.message,
          type: 'error',
          duration: 5 * 1000
        })

        // 70002:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
        if (res.code === 70002 || res.code === 50012 || res.code === 50014) {
          MessageBox.confirm(
            '你已被登出,可以取消继续留在该页面,或者重新登录',
            '确定登出',
            {
              confirmButtonText: '重新登录',
              cancelButtonText: '取消',
              type: 'warning'
            }
          ).then(() => {
            store.dispatch('FedLogOut').then(() => {
              location.reload() // 为了重新实例化vue-router对象 避免bug
            })
          })
        }
        return Promise.reject('error')
      } else {
        return response.data
      }
    },
    error => {
      console.log('err' + error) // for debug
      Message({
        message: error.message,
        type: 'error',
        duration: 5 * 1000
      })
      return Promise.reject(error)
    }
  )
复制代码

封装了一个Echart组件

后端篇

构建项目目录

npm install -g koa-generator
koa2 /server && cd /server
npm install
  • 安装组件
  1. npm i jsonwebtoken koa-jwt koa-mysql-session koa-session-minimal koa2-cors md5 moment mysql save --save
  • 配置app.js
const Koa = require('koa')
  const jwt = require('koa-jwt')
  const app = new Koa()
  const views = require('koa-views')
  const json = require('koa-json')
  const onerror = require('koa-onerror')
  const bodyparser = require('koa-bodyparser')
  const logger = require('koa-logger')
  const convert = require('koa-convert');

  var session = require('koa-session-minimal')
  var MysqlStore = require('koa-mysql-session')
  var config = require('./config/default.js')
  var cors = require('koa2-cors')

  const users = require('./routes/users')
  const account = require('./routes/account')

  // error handler
  onerror(app)

  // 配置jwt错误返回
  app.use(function(ctx, next) {
    return next().catch(err => {
      if (401 == err.status) {
        ctx.status = 401
        ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.INVALID_TOKEN)
        // ctx.body = {
        //   // error: err.originalError ? err.originalError.message : err.message
        // }
      } else {
        throw err
      }
    })
  })

  // Unprotected middleware
  app.use(function(ctx, next) {
    if (ctx.url.match(/^\/public/)) {
      ctx.body = 'unprotected\n'
    } else {
      return next()
    }
  })

  // Middleware below this line is only reached if JWT token is valid

  app.use(
    jwt({ secret: config.secret, passthrough: true }).unless({
      path: [/\/register/, /\/user\/login/]
    })
  )

  // middlewares
  app.use(convert(bodyparser({
    enableTypes:['json', 'form', 'text']
  })))
  app.use(convert(json()))
  app.use(convert(logger()))
  app.use(require('koa-static')(__dirname + '/public'))

  app.use(views(__dirname + '/views', {
    extension: 'pug'
  }))

  // logger
  app.use(async (ctx, next) => {
    const start = new Date()
    await next()
    const ms = new Date() - start
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
  })

  // cors
  app.use(cors())

  // routes
  app.use(users.routes(), users.allowedMethods())
  app.use(account.routes(), account.allowedMethods())

  // error-handling
  app.on('error', (err, ctx) => {
    console.error('server error', err, ctx)
  });

  module.exports = app

复制代码
  • 新建config文件夹用于存放数据库连接等操作
default.js
// 数据库配置
  const config = {
    port: 3000,
    database: {
      DATABASE: 'xxx', //数据库
      USERNAME: 'root', //用户
      PASSWORD: 'xxx', //密码
      PORT: '3306', //端口
      HOST: '127.0.0.1' //服务ip地址
    },
    secret: 'jwt_secret'
  }

  module.exports = config

复制代码

数据库相关(mysql)

createTables.js 一个简单的用户角色权限表 用户、角色、权限表的关系(mysql)

// 数据库表格创建
const createTable = {
  users: `CREATE TABLE IF NOT EXISTS user_info (
      id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)',
      user_id VARCHAR ( 100 ) NOT NULL COMMENT '账号',
      user_name VARCHAR ( 100 ) NOT NULL COMMENT '用户名',
      user_pwd VARCHAR ( 100 ) NOT NULL COMMENT '密码',
      user_head VARCHAR ( 225 ) COMMENT '头像',
      user_mobile VARCHAR ( 20 ) COMMENT '手机',
      user_email VARCHAR ( 64 ) COMMENT '邮箱',
      user_creatdata TIMESTAMP NOT NULL DEFAULT NOW( ) COMMENT '注册日期',
      user_login_time TIMESTAMP DEFAULT NOW( ) COMMENT '登录时间',
      user_count INT COMMENT '登录次数'
    ) ENGINE = INNODB charset = utf8;`,
    role: `CREATE TABLE IF NOT EXISTS role_info (
      id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)',
      role_name VARCHAR ( 20 ) NOT NULL COMMENT '角色名',
      role_description VARCHAR ( 255 ) DEFAULT NULL COMMENT '描述'
    ) ENGINE = INNODB charset = utf8;`,
    permission: `CREATE TABLE IF NOT EXISTS permission_info (
      id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)',
      permission_name VARCHAR ( 20 ) NOT NULL COMMENT '权限名',
      permission_description VARCHAR ( 255 ) DEFAULT NULL COMMENT '描述'
    ) ENGINE = INNODB charset = utf8;`,
    userRole: `CREATE TABLE IF NOT EXISTS user_role (
      id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)',
      user_id INT NOT NULL COMMENT '关联用户',
      role_id INT NOT NULL COMMENT '关联角色',
      KEY fk_user_role_role_info_1 ( role_id ),
      KEY fk_user_role_user_info_1 ( user_id ),
      CONSTRAINT fk_user_role_role_info_1 FOREIGN KEY ( role_id ) REFERENCES role_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE,
      CONSTRAINT fk_user_role_user_info_1 FOREIGN KEY ( user_id ) REFERENCES user_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE = INNODB charset = utf8;`,
    rolePermission: `CREATE TABLE IF NOT EXISTS role_permission (
      id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)',
      role_id INT NOT NULL COMMENT '关联角色',
      permission_id INT NOT NULL COMMENT '关联权限',
      KEY fk_role_permission_role_info_1 ( role_id ),
      KEY fk_role_permission_permission_info_1 ( permission_id ),
      CONSTRAINT fk_role_permission_role_info_1 FOREIGN KEY ( role_id ) REFERENCES role_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE,
      CONSTRAINT fk_role_permission_permission_info_1 FOREIGN KEY ( permission_id ) REFERENCES permission_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE = INNODB charset = utf8;`
}

module.exports = createTable
复制代码
  • 创建lib文件夹 用于存储数据库查询语句

mysql.js

const mysql = require('mysql')
const config = require('../config/default')
const createTables = require('../config/createTables.js')

var pool = mysql.createPool({
  host: config.database.HOST,
  user: config.database.USERNAME,
  password: config.database.PASSWORD,
  database: config.database.DATABASE
})

let query = function(sql, values) {
  return new Promise((resolve, reject) => {
    pool.getConnection(function(err, connection) {
      if (err) {
        resolve(err)
      } else {
        connection.query(sql, values, (err, rows) => {
          if (err) {
            reject(err)
          } else {
            resolve(rows)
          }
          connection.release()
        })
      }
    })
  })
}

let createTable = function(sql) {
  return query(sql, [])
}

// 建表
// createTable(createTables.users)
// createTable(createTables.role)
// createTable(createTables.permission)
// createTable(createTables.userRole)
// createTable(createTables.rolePermission)

// 查询用户是否存在
let findUser = async function(id) {
  let _sql = `
        SELECT * FROM user_info where user_id="${id}" limit 1;
    `
  let result = await query(_sql)

  if (Array.isArray(result) && result.length > 0) {
    result = result[0]
  } else {
    result = null
  }
  return result
}
// 查询用户以及用户角色
let findUserAndRole = async function(id) {
  let _sql = `
      SELECT u.*,r.role_name FROM user_info u,user_role ur,role_info r where u.id=(SELECT id FROM user_info where user_id="${id}" limit 1) and ur.user_id=u.id and r.id=ur.user_id limit 1;
    `
  let result = await query(_sql)

  if (Array.isArray(result) && result.length > 0) {
    result = result[0]
  } else {
    result = null
  }
  return result
}

// 更新用户登录次数和登录时间
let UpdataUserInfo = async function(value) {
  let _sql =
    'UPDATE user_info SET user_count = ?, user_login_time = ? WHERE id = ?;'
  return query(_sql, value)
}

module.exports = {
  //暴露方法
  createTable,
  findUser,
  findUserAndRole,
  UpdataUserInfo,
  getShopAndAccount
}

复制代码

koa 路由配置

  • 接口报错信息统一方法 创建error文件夹
ApiErrorNames.js
/**
 * API错误名称
 */
var ApiErrorNames = {};

ApiErrorNames.UNKNOW_ERROR = "UNKNOW_ERROR";
ApiErrorNames.SUCCESS = "SUCCESS";

/* 参数错误:10001-19999 */
ApiErrorNames.PARAM_IS_INVALID = 'PARAM_IS_INVALID';
ApiErrorNames.PARAM_IS_BLANK = 'PARAM_IS_BLANK';
ApiErrorNames.PARAM_TYPE_BIND_ERROR = 'PARAM_TYPE_BIND_ERROR';
ApiErrorNames.PARAM_NOT_COMPLETE = 'PARAM_NOT_COMPLETE';

/* 用户错误:20001-29999*/
ApiErrorNames.USER_NOT_LOGGED_IN = 'USER_NOT_LOGGED_IN';
ApiErrorNames.USER_LOGIN_ERROR = 'USER_LOGIN_ERROR';
ApiErrorNames.USER_ACCOUNT_FORBIDDEN = 'USER_ACCOUNT_FORBIDDEN';
ApiErrorNames.USER_NOT_EXIST = 'USER_NOT_EXIST';
ApiErrorNames.USER_HAS_EXISTED = 'USER_HAS_EXISTED';

/* 业务错误:30001-39999 */
ApiErrorNames.SPECIFIED_QUESTIONED_USER_NOT_EXIST = 'SPECIFIED_QUESTIONED_USER_NOT_EXIST';

/* 系统错误:40001-49999 */
ApiErrorNames.SYSTEM_INNER_ERROR = 'SYSTEM_INNER_ERROR';

/* 数据错误:50001-599999 */
ApiErrorNames.RESULE_DATA_NONE = 'RESULE_DATA_NONE';
ApiErrorNames.DATA_IS_WRONG = 'DATA_IS_WRONG';
ApiErrorNames.DATA_ALREADY_EXISTED = 'DATA_ALREADY_EXISTED';

/* 接口错误:60001-69999 */
ApiErrorNames.INTERFACE_INNER_INVOKE_ERROR = 'INTERFACE_INNER_INVOKE_ERROR';
ApiErrorNames.INTERFACE_OUTTER_INVOKE_ERROR = 'INTERFACE_OUTTER_INVOKE_ERROR';
ApiErrorNames.INTERFACE_FORBID_VISIT = 'INTERFACE_FORBID_VISIT';
ApiErrorNames.INTERFACE_ADDRESS_INVALID = 'INTERFACE_ADDRESS_INVALID';
ApiErrorNames.INTERFACE_REQUEST_TIMEOUT = 'INTERFACE_REQUEST_TIMEOUT';
ApiErrorNames.INTERFACE_EXCEED_LOAD = 'INTERFACE_EXCEED_LOAD';

/* 权限错误:70001-79999 */
ApiErrorNames.PERMISSION_NO_ACCESS = 'PERMISSION_NO_ACCESS';
ApiErrorNames.INVALID_TOKEN = 'INVALID_TOKEN';

/**
 * API错误名称对应的错误信息
 */
const error_map = new Map();

error_map.set(ApiErrorNames.SUCCESS, { code: 0, message: '成功' });
error_map.set(ApiErrorNames.UNKNOW_ERROR, { code: -1, message: '未知错误' });
/* 参数错误:10001-19999 */
error_map.set(ApiErrorNames.PARAM_IS_INVALID, { code: 10001, message: '参数无效' });
error_map.set(ApiErrorNames.PARAM_IS_BLANK, { code: 10002, message: '参数为空' });
error_map.set(ApiErrorNames.PARAM_TYPE_BIND_ERROR, { code: 10003, message: '参数类型错误' });
error_map.set(ApiErrorNames.PARAM_NOT_COMPLETE, { code: 10004, message: '参数缺失' });
/* 用户错误:20001-29999*/
error_map.set(ApiErrorNames.USER_NOT_LOGGED_IN, { code: 20001, message: '用户未登录' });
error_map.set(ApiErrorNames.USER_LOGIN_ERROR, { code: 20002, message: '账号不存在或密码错误' });
error_map.set(ApiErrorNames.USER_ACCOUNT_FORBIDDEN, { code: 20003, message: '账号已被禁用' });
error_map.set(ApiErrorNames.USER_NOT_EXIST, { code: 20004, message: '用户不存在' });
error_map.set(ApiErrorNames.USER_HAS_EXISTED, { code: 20005, message: '用户已存在' });
/* 业务错误:30001-39999 */
error_map.set(ApiErrorNames.SPECIFIED_QUESTIONED_USER_NOT_EXIST, { code: 30001, message: '某业务出现问题' });
/* 系统错误:40001-49999 */
error_map.set(ApiErrorNames.SYSTEM_INNER_ERROR, { code: 40001, message: '系统繁忙,请稍后重试' });
/* 数据错误:50001-599999 */
error_map.set(ApiErrorNames.RESULE_DATA_NONE, { code: 50001, message: '数据未找到' });
error_map.set(ApiErrorNames.DATA_IS_WRONG, { code: 50002, message: '数据有误' });
error_map.set(ApiErrorNames.DATA_ALREADY_EXISTED, { code: 50003, message: '数据已存在' });
/* 接口错误:60001-69999 */
error_map.set(ApiErrorNames.INTERFACE_INNER_INVOKE_ERROR, { code: 60001, message: '内部系统接口调用异常' });
error_map.set(ApiErrorNames.INTERFACE_OUTTER_INVOKE_ERROR, { code: 60002, message: '外部系统接口调用异常' });
error_map.set(ApiErrorNames.INTERFACE_FORBID_VISIT, { code: 60003, message: '该接口禁止访问' });
error_map.set(ApiErrorNames.INTERFACE_ADDRESS_INVALID, { code: 60004, message: '接口地址无效' });
error_map.set(ApiErrorNames.INTERFACE_REQUEST_TIMEOUT, { code: 60005, message: '接口请求超时' });
error_map.set(ApiErrorNames.INTERFACE_EXCEED_LOAD, { code: 60006, message: '接口负载过高' });
/* 权限错误:70001-79999 */
error_map.set(ApiErrorNames.PERMISSION_NO_ACCESS, { code: 70001, message: '无访问权限' });
error_map.set(ApiErrorNames.INVALID_TOKEN, { code: 70002, message: '无效token' });

//根据错误名称获取错误信息
ApiErrorNames.getErrorInfo = (error_name) => {

    var error_info;

    if (error_name) {
        error_info = error_map.get(error_name);
    }

    //如果没有对应的错误信息,默认'未知错误'
    if (!error_info) {
        error_name = UNKNOW_ERROR;
        error_info = error_map.get(error_name);
    }

    return error_info;
}

//返回正确信息
ApiErrorNames.getSuccessInfo = (data) => {

    var success_info;
    let name = 'SUCCESS';
    success_info = error_map.get(name);
    if (data) {
        success_info.data = data
    }

    return success_info;
}

module.exports = ApiErrorNames;
复制代码
  • 创建controller文件夹

对路由users进行逻辑编写

const mysqlModel = require('../lib/mysql') //引入数据库方法
const jwt = require('jsonwebtoken')
const config = require('../config/default.js')
const ApiErrorNames = require('../error/ApiErrorNames.js')
const moment = require('moment')

/**
 * 普通登录
 */
exports.login = async (ctx, next) => {
  const { body } = ctx.request
  try {
    const user = await mysqlModel.findUser(body.username)
    if (!user) {
      // ctx.status = 401
      ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.USER_NOT_EXIST)
      return
    }
    let bodys = await JSON.parse(JSON.stringify(user))
    // 匹配密码是否相等
    if ((await user.user_pwd) === body.password) {
      let data = {
        user: user.user_id,
        // 生成 token 返回给客户端
        token: jwt.sign(
          {
            data: user.user_id,
            // 设置 token 过期时间
            exp: Math.floor(Date.now() / 1000) + 60 * 60 // 60 seconds * 60 minutes = 1 hour
          },
          config.secret
        )
      }
      ctx.body = ApiErrorNames.getSuccessInfo(data)
    } else {
      ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.USER_LOGIN_ERROR)
    }
  } catch (error) {
    ctx.throw(500)
  }
}

/**
 * 获取用户信息
 */
exports.info = async (ctx, next) => {
  const { body } = ctx.request
  // console.log(body)
  try {
    const token = ctx.header.authorization
    let payload
    if (token) {
      payload = await jwt.verify(token.split(' ')[1], config.secret) // 解密,获取payload
      const user = await mysqlModel.findUserAndRole(payload.data)
      if (!user) {
        ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.USER_NOT_EXIST)
      } else {
        let cont = user.user_count + 1
        let updateInfo = [
          cont,
          moment().format('YYYY-MM-DD HH:mm:ss'),
          user.id
        ]
        await mysqlModel
        .UpdataUserInfo(updateInfo)
        .then(res => {
          let data = {
            avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
            name: user.user_id,
              // roles: [user.user_admin === 0 ? 'admin' : '']
            roles: [user.role_name]
          }
          ctx.body = ApiErrorNames.getSuccessInfo(data)
        })
        .catch(err => {
          ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.DATA_IS_WRONG)
        })
      }
    } else {
      ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.INVALID_TOKEN)
    }
  } catch (error) {
    ctx.throw(500)
  }
}

/**
 * 退出登录
 */
exports.logout = async (ctx, next) => {
  try {
    // ctx.status = 200
    ctx.body = ApiErrorNames.getSuccessInfo()
  } catch (error) {
    ctx.throw(500)
  }
}

复制代码

routes中的users.js

const router = require('koa-router')() //引入路由函数
const userControl = require('../controller/users') //引入逻辑
// const config = require('../config/default.js')

router.get('/', async (ctx, next) => {
  'use strict'
  ctx.redirect('/user/login')
})
// 路由中间间,页面路由到/,就是端口号的时候,(网址),页面指引到//user/login

router.get('/user/info', userControl.info)
router.post('/user/logout', userControl.logout)
router.post('/user/login', userControl.login)

module.exports = router
//将页面暴露出去
复制代码

备注

ctx.request 获取post请求中的body ctx.query.xx 获取get请求中的参数 router.prefix('/account') 给router实例添加前缀


以上所述就是小编给大家介绍的《koa2+vue+mysql 全栈开发记录》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mastering Regular Expressions, Second Edition

Mastering Regular Expressions, Second Edition

Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95

Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试