看了些 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" } }
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' }); };
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' }); };
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 }
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 }
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')
其它部分,好比路由、組件,可查看以前的文章,或下載該示例在 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 }
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 }
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 }
在 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;
其它更多細節請見項目。