zoukankan      html  css  js  c++  java
  • 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开发完整的前后端项目

  • 相关阅读:
    Win10 安装 Oracle32bit客户端 提示:引用数据不可用于验证此操作系统分发的先决条件
    ORACLE 拆分逗号分隔字符串函数
    PLSQL 中文乱码
    不要把分层当做解耦!
    MySQL 迁移到 PG 怎么做
    在 MySQL 创造类似 PipelineDB 的流视图(continuous view)
    TeamViewer 的替代品 ZeroTier + NoMachine
    所有 HTML attribute
    使用PG的部分索引
    基于 500 份标注数据用深度学习破解验证码
  • 原文地址:https://www.cnblogs.com/caihg/p/6900986.html
Copyright © 2011-2022 走看看