Vue2 + Koa2 实现后台管理系统

看了些 koa2 与 Vue2 的资料,模仿着做了一个基本的后台管理系统,包括增、删、改、查与图片上传。

工程目录:

由于 koa2 用到了 async await 语法,所以 node 的版本需要至少 v7.6.0,我目前用的是 v7.9.0

1. 根据 package.json 安装好依赖:

{
  "name": "vue2.x-koa2.x",
  "version": "1.0.0",
  "description": "A Vue.js and Koa project",
  "author": "caihuaguang@aixuedai.com <caihuaguang@aixuedai.com>",
  "private": true,
  "scripts": {
    "server": "node app.js",
    "dev": "node build/dev-server.js",
    "build": "node build/build.js"
  },
  "dependencies": {
    "axios": "^0.15.3",
    "bcryptjs": "^2.4.0",
    "busboy": "^0.2.14",
    "element-ui": "^1.2.7",
    "koa": "^2.2.0",
    "koa-bodyparser": "^4.2.0",
    "koa-history-api-fallback": "^0.1.3",
    "koa-jwt": "^1.3.1",
    "koa-logger": "^2.0.1",
    "koa-router": "^5.4.0",
    "koa-static": "^3.0.0",
    "mysql": "^2.12.0",
    "sequelize": "^3.30.4",
    "stylus": "^0.54.5",
    "stylus-loader": "^2.4.0",
    "vue": "^2.2.6",
    "vue-router": "^2.3.0",
    "vuex": "^2.2.1"
  },
  "devDependencies": {
    "autoprefixer": "^6.4.0",
    "babel-core": "^6.24.0",
    "babel-loader": "^6.4.1",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.0",
    "babel-preset-stage-0": "^6.22.0",
    "babel-register": "^6.24.0",
    "chalk": "^1.1.3",
    "connect-history-api-fallback": "^1.1.0",
    "css-loader": "^0.25.0",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.13.3",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.9.0",
    "friendly-errors-webpack-plugin": "^1.1.2",
    "function-bind": "^1.0.2",
    "html-webpack-plugin": "^2.8.1",
    "http-proxy-middleware": "^0.17.2",
    "json-loader": "^0.5.4",
    "opn": "^4.0.2",
    "ora": "^0.3.0",
    "semver": "^5.3.0",
    "shelljs": "^0.7.4",
    "url-loader": "^0.5.7",
    "vue-loader": "^10.0.0",
    "vue-style-loader": "^1.0.0",
    "vue-template-compiler": "^2.1.0",
    "webpack": "^1.9.11",
    "webpack-dev-middleware": "^1.8.3",
    "webpack-hot-middleware": "^2.12.2",
    "webpack-merge": "^0.14.1"
  },
  "engines": {
    "node": ">= 4.0.0",
    "npm": ">= 3.0.0"
  }
}
View Code

2. mysql 数据库:可以单独安装,也可以用集成工具 WampServer,我用的是后者。

数据库用可视化工具 HeidiSQL 来操作。

建一个数据库 my_test_db,字符集选择 utf8_unicode_ci(之前没注意的时候,直接用了服务器默认的 latin1_swedish_ci,导致数据库不能保存中文)

my_test_db 中新建两张表 user(用户) 与 goods(商品)

user 表相关字段:

goods 表相关字段:

可以先手动输入一些数据。

3. 将数据库的表结构导出到 schema 文件夹

sequelize-auto -o "./schema" -d my_test_db -h 127.0.0.1 -u root -p 3306 -x 123456 -e mysql

其中,root是数据库用户名,123456是密码。sequelize-auto 具体用法参考这里

成功后会生成两个文件 user.js 与 goods.js

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('user', {
    id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    user_name: {
      type: DataTypes.CHAR(50),
      allowNull: false
    },
    password: {
      type: DataTypes.CHAR(128),
      allowNull: false
    }
  }, {
    tableName: 'user'
  });
};
user 表结构
module.exports = function(sequelize, DataTypes) {
  return sequelize.define('goods', {
    id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    name: {
      type: DataTypes.CHAR(50),
      allowNull: false
    },
    description: {
      type: DataTypes.CHAR(200),
      allowNull: true
    },
    img_url: {
      type: DataTypes.STRING,
      allowNull: true
    }
  }, {
    tableName: 'goods'
  });
};
goods 表结构

4. 连接数据库

在 server/config 目录添加 db.js,内容如下:

const Sequelize = require('sequelize');

// 使用 url 形式连接数据库
const theDb = new Sequelize('mysql://root:123456@localhost/my_test_db', {
  define: {
    timestamps: false // 取消Sequelzie自动给数据表添加的 createdAt 和 updatedAt 两个时间戳字段
  }
})

module.exports = {
  theDb
}

sequelize 具体用法查看这里

(一)用户

5. 操作数据库

在 server/models 目录添加 user.js

const theDatabase = require('../config/db.js').theDb;
const userSchema = theDatabase.import('../schema/user.js');

// 通过用户名查找
const getUserByName = async function(name) {
  const userInfo = await userSchema.findOne({
    where: {
      user_name: name
    }
  })

  return userInfo
}

// 通过用户 id 查找
const getUserById = async function(id) {
  const userInfo = await userSchema.findOne({
    where: {
      id: id
    }
  });

  return userInfo
}

const getUserList = async function() {
  return await userSchema.findAndCount(); // findAndCount() 用 get 路由访问,会得到 204 状态:无数据返回。改用 post 就行
}

module.exports = {
  getUserByName,
  getUserById,
  getUserList
}
查找 user 表

findOne 与 findAndCount 都可以查询数据库,其本质是对 select 语句的封装

6. 服务端具体业务代码

在 server/controllers 目录添加 user.js

 1 const userModel = require('../models/user.js');
 2 const jwt = require('koa-jwt');
 3 const bcrypt = require('bcryptjs');
 4 
 5 const postUserAuth = async function() {
 6   const data = this.request.body; // 用 post 传过来的数据存放于 request.body
 7   const userInfo = await userModel.getUserByName(data.name);
 8 
 9   if (userInfo != null) { // 如果查无此用户会返回 null
10     if (userInfo.password != data.password) {
11       if (!bcrypt.compareSync(data.password, userInfo.password)) {
12         this.body = { // 返回给前端的数据
13           success: false,
14           info: '密码错误!'
15         }
16       }
17     } else { // 密码正确
18       const userToken = {
19         id: userInfo.id,
20         name: userInfo.user_name,
21         originExp: Date.now() + 60 * 60 * 1000, // 设置过期时间(毫秒)为 1 小时
22       }
23       const secret = 'vue-koa-demo'; // 指定密钥,这是之后用来判断 token 合法性的标志
24       const token = jwt.sign(userToken, secret); // 签发 token
25       this.body = {
26         success: true,
27         token: token
28       }
29     }
30   } else {
31     this.body = {
32       success: false,
33       info: '用户不存在!'
34     }
35   }
36 }
37 
38 const getUserInfo = async function() {
39   const id = this.params.id; // 获取 url 里传过来的参数里的 id
40   const result = await userModel.getUserById(id);
41   this.body = result
42 }
43 
44 const getUserList = async function() {
45   const result = await userModel.getUserList();
46 
47   this.body = {
48     success: true,
49     total: result.count,
50     list: result.rows,
51     msg: '获取用户列表成功!'
52   }
53 }
54 
55 module.exports = {
56   postUserAuth,
57   getUserInfo,
58   getUserList
59 }
user 控制器

7. 定义接口,用于前端发送 ajax 的 url

 在 server/routes 目录添加 user.js

const userController = require('../controllers/user.js');
const router = require('koa-router')();

router.post('/user', userController.postUserAuth);
router.get('/user/:id', userController.getUserInfo); // 定义 url 的参数 id
router.post('/user/list', userController.getUserList);

module.exports = router;

这是登录的后端部分。

8. 在根目录添加 app.js

const path = require('path'),
  koa = new (require('koa'))(),
  koaRouter = require('koa-router')(),
  logger = require('koa-logger'),
  koaStatic = require('koa-static'),
  historyApiFallback = require('koa-history-api-fallback'),
  image = require('./server/routes/image.js'),
  user = require('./server/routes/user.js'),
  goods = require('./server/routes/goods.js');

koa.use(require('koa-bodyparser')());
koa.use(logger());
koa.use(historyApiFallback());

koa.use(async (ctx, next) => {
  let start = new Date();
  await next();
  let ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

koa.on('error', function(err, ctx) {
  console.log('server error: ', err);
});

// 静态文件 koaStatic 在 koa-router 的其他规则之上
koa.use(koaStatic(path.resolve('dist'))); // 将 webpack 打包好的项目目录作为 Koa 静态文件服务的目录

// 挂载到 koa-router 上,同时会让所有的 user 的请求路径前面加上 '/auth' 。
koaRouter.use('/auth', user.routes());

koaRouter.use(goods.routes());

koaRouter.use(image.routes());

koa.use(koaRouter.routes()); // 将路由规则挂载到Koa上。

koa.listen(8889, () => {
  console.log('Koa is listening on port 8889');
});

module.exports = koa;

在命令行中执行 npm run server,正常情况下应该会成功(前提是 mysql 已经正常启动)

9. 前端部分

src/main.js

 1 import Vue from 'vue'
 2 import ElementUI from 'element-ui'
 3 import 'element-ui/lib/theme-default/index.css'
 4 import VueRouter from 'vue-router'
 5 import Axios from 'axios'
 6 
 7 Vue.use(ElementUI);
 8 Vue.use(VueRouter);
 9 Vue.prototype.$http = Axios
10 
11 import routes from './routes'
12 const router =  new VueRouter({
13   mode: 'history',
14   base: __dirname,
15   routes: routes
16 })
17 
18 import jwt from 'jsonwebtoken'
19 
20 router.beforeEach((to, from, next) => {
21   let token = localStorage.getItem('demo-token');
22 
23   const decoded = token && jwt.verify(token, 'vue-koa-demo');
24   if (decoded) {
25     if (decoded.originExp - Date.now() < 0) { // 已过期
26       localStorage.removeItem('demo-token');
27     } else {
28       decoded.originExp = Date.now() + 60 * 60 * 1000;
29       token = jwt.sign(decoded, 'vue-koa-demo');
30       localStorage.setItem('demo-token', token);
31     }
32   }
33 
34   if (to.path == '/') {
35     if (token) {
36       next('/page/userHome')
37     }
38     next();
39   } else {
40     if (token) {
41       Vue.prototype.$http.defaults.headers.common['Authorization'] = 'Bearer ' + token; // 全局设定 header 的 token 验证
42       next()
43     } else {
44       next('/')
45     }
46   }
47 })
48 
49 import Main from './components/main.vue'
50 const app = new Vue({
51   router,
52   render: h => h(Main)
53 }).$mount('#app')
View Code

其它部分,比如路由、组件,可查看之前的文章,或下载该示例在 github 上的代码

启动前端的本地环境:npm run dev

根据我在 user 表中创建的帐户与密码,登录成功

(二):商品

服务端代码与登录功能的相似,老三样:数据模型、业务控制器、api 接口:

server/models/goods.js

 1 const theDatabase = require('../config/db.js').theDb;
 2 const goodsSchema = theDatabase.import('../schema/goods.js');
 3 
 4 const getGoodsList = async (searchVal) => {
 5   return await goodsSchema.findAndCount(
 6     {
 7       where: {
 8         name: {
 9           $like: '%' + searchVal + '%'  // searchVal:要搜索的商品名称
10         }
11       }
12     }
13   );
14 }
15 
16 // 根据商品 id 查找数据
17 const getGoodsDetails = async (id) => {
18   return await goodsSchema.findById(id);
19 }
20 
21 // 添加商品
22 const addGoods = async (name, description, img_url) => {
23   await goodsSchema.create({
24     name,
25     description,
26     img_url
27   });
28 
29   return true;
30 }
31 
32 // 根据商品 id 修改
33 const updateGoods = async (id, name, description, img_url) => {
34   await goodsSchema.update(
35     {
36       name,
37       description,
38       img_url
39     },
40     {
41       where: {
42         id
43       }
44     }
45   );
46 
47   return true;
48 }
49 
50 // 根据商品 id 删除数据
51 const removeGoods = async (id) => {
52   await goodsSchema.destroy({
53     where: {
54       id
55     }
56   });
57 
58   return true;
59 }
60 
61 module.exports = {
62   getGoodsList,
63   getGoodsDetails,
64   addGoods,
65   updateGoods,
66   removeGoods
67 }
View Code

server/controllers/goods.js

 1 const goodsModel = require('../models/goods.js');
 2 
 3 const getGoodsList = async function() {
 4   const data = this.request.body; // post 请求,参数在 request.body 里
 5   const currentPage = Number(data.currentPage);
 6   const pageSize = Number(data.pageSize);
 7   const searchVal = data.searchVal;
 8   const result = await goodsModel.getGoodsList(searchVal);
 9 
10   let list = result.rows;
11 
12   // 根据分页输出数据
13   let start = pageSize * (currentPage - 1);
14   list = list.slice(start, start + pageSize);
15 
16   this.body = {
17     success: true,
18     list,
19     total: result.count,
20     msg: '获取商品列表成功!'
21   }
22 }
23 
24 const getGoodsDetails = async function() {
25   const id = this.params.id;
26   const list = await goodsModel.getGoodsDetails(id);
27 
28   this.body = {
29     success: true,
30     list: Array.isArray(list) ? list : [list],
31     msg: '获取商品详情成功!'
32   };
33 }
34 
35 const manageGoods = async function() {
36   const data = this.request.body;
37   const id = data.id;
38   const name = data.name;
39   const description = data.description;
40   const imgUrl = data.imgUrl;
41 
42   let success = false;
43   let msg = '';
44 
45   if (id) {
46     if (name) {
47       await goodsModel.updateGoods(id, name, description, imgUrl);
48       success = true;
49       msg = '修改成功!';
50     }
51   } else if (name) {
52     await goodsModel.addGoods(name, description, imgUrl);
53     success = true;
54     msg = '添加成功!';
55   }
56 
57   this.body = {
58     success,
59     msg
60   }
61 }
62 
63 const removeGoods = async function() {
64   const id = this.params.id;
65 
66   await goodsModel.removeGoods(id);
67 
68   this.body = {
69     success: true,
70     msg: '删除成功!'
71   }
72 }
73 
74 module.exports = {
75   getGoodsList,
76   getGoodsDetails,
77   removeGoods,
78   manageGoods
79 }
View Code

server/routes/goods.js

const goodsController = require('../controllers/goods.js');
const router = require('koa-router')();

router.post('/goods/list', goodsController.getGoodsList);
router.get('/goods/:id', goodsController.getGoodsDetails);
router.delete('/goods/:id/', goodsController.removeGoods);
router.post('/goods/management', goodsController.manageGoods);

module.exports = router;

商品列表界面:

点击“编辑”时,将 id 附在 url 上,通过 vue-router 跳转到商品详情页,根据 id 发送 ajax 获取详情数据:

 (商品里有上传图片,但是无法显示,那是因为图片上传到了本地,路径也是本地的 F:projectvue-demovue2.x-koa2.xuploadsalbumfc37b8a61133.jpg)

(三)图片

在 server/controllers/common 目录添加 file.js,作为所有文件上传的公共文件:

  1 const inspect = require('util').inspect
  2 const path = require('path')
  3 const fs = require('fs')
  4 const Busboy = require('busboy')
  5 
  6 /**
  7  * 同步创建文件目录
  8  * @param  {string} dirname 目录绝对地址
  9  * @return {boolean}        创建目录结果
 10  */
 11 function mkdirsSync( dirname ) {
 12   if (fs.existsSync( dirname )) {
 13     return true
 14   } else {
 15     if (mkdirsSync( path.dirname(dirname)) ) {
 16       fs.mkdirSync( dirname )
 17       return true
 18     }
 19   }
 20 }
 21 
 22 /**
 23  * 获取上传文件的后缀名
 24  * @param  {string} fileName 获取上传文件的后缀名
 25  * @return {string}          文件后缀名
 26  */
 27 function getSuffixName( fileName ) {
 28   let nameList = fileName.split('.')
 29   return nameList[nameList.length - 1]
 30 }
 31 
 32 /**
 33  * 上传文件
 34  * @param  {object} ctx       koa上下文
 35  * @param  {object} options   文件上传参数
 36  *                  dir       文件目录
 37  *                  path      文件存放路径
 38  * @return {promise}
 39  */
 40 function uploadFile( ctx, options) {
 41   let req = ctx.req
 42   // let res = ctx.res
 43   let busboy = new Busboy({headers: req.headers})
 44 
 45   // 获取类型
 46   let dir = options.dir || 'common'
 47   let filePath = path.join( options.path,  dir)
 48   let mkdirResult = mkdirsSync( filePath )
 49 
 50   return new Promise((resolve, reject) => {
 51     console.log('文件上传中...')
 52     let result = {
 53       success: false,
 54       filePath: '',
 55       formData: {},
 56     }
 57 
 58     // 解析请求文件事件
 59     busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
 60       let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
 61       let _uploadFilePath = path.join( filePath, fileName )
 62       let saveTo = path.join(_uploadFilePath)
 63 
 64       // 文件保存到指定路径
 65       file.pipe(fs.createWriteStream(saveTo))
 66 
 67       // 文件写入事件结束
 68       file.on('end', function() {
 69         result.success = true
 70         result.filePath = saveTo
 71         result.message = '文件上传成功'
 72         console.log('文件上传成功!')
 73       })
 74     })
 75 
 76     // 解析表单中其他字段信息
 77     busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
 78       console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
 79       result.formData[fieldname] = inspect(val);
 80     });
 81 
 82     // 解析结束事件
 83     busboy.on('finish', function() {
 84       console.log('文件上传结束')
 85       resolve(result)
 86     })
 87 
 88     // 解析错误事件
 89     busboy.on('error', function(err) {
 90       console.log('文件上传出错')
 91       reject(result)
 92     })
 93 
 94     req.pipe(busboy)
 95   })
 96 }
 97 
 98 module.exports =  {
 99   uploadFile
100 }
View Code

在 server/controllers 目录添加 image.js,用来处理商品的图片上传业务:

const path = require('path');
const uploadFile = require('./common/file.js').uploadFile;

const uploadImg = async function() {
  const serverFilePath = path.join( __dirname, '../../uploads' )

  result = await uploadFile(this, {
    dir: 'album',
    path: serverFilePath
  });

  this.body = result;
}

module.exports = {
  uploadImg
}

在 server/routes 目录添加 image.js,定义文件上传的接口:

const imgController = require('../controllers/image.js');
const router = require('koa-router')();

router.post('/uploads/img', imgController.uploadImg)

module.exports = router;

其它更多细节请见项目

参考:全栈开发实战:用Vue2+Koa1开发完整的前后端项目

原文地址:https://www.cnblogs.com/caihg/p/6900986.html