Vue2 + Koa2 實現後臺管理系統

看了些 koa2 與 Vue2 的資料,模仿着作了一個基本的後臺管理系統,包括增、刪、改、查與圖片上傳。css

工程目錄:html

因爲 koa2 用到了 async await 語法,因此 node 的版本須要至少 v7.6.0,我目前用的是 v7.9.0前端

 

1. 根據 package.json 安裝好依賴:vue

{
  "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,我用的是後者。
node

數據庫用可視化工具 HeidiSQL 來操做。mysql

建一個數據庫 my_test_db,字符集選擇 utf8_unicode_ci(以前沒注意的時候,直接用了服務器默認的 latin1_swedish_ci,致使數據庫不能保存中文)webpack

my_test_db 中新建兩張表 user(用戶) 與 goods(商品)ios

user 表相關字段:git

goods 表相關字段:github

能夠先手動輸入一些數據。

 

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:\project\vue-demo\vue2.x-koa2.x\uploads\album\fc37b8a61133.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開發完整的先後端項目

相關文章
相關標籤/搜索