摘要:全棧開發(fā)記錄基于想要自己制作一個(gè)個(gè)人項(xiàng)目為由,于是有了這么一個(gè)開發(fā)記錄梳理開發(fā)過程也是一個(gè)知識(shí)鞏固的過程個(gè)人的一個(gè)通用本篇文章的范例地址前端工具頁(yè)面組件百度強(qiáng)大的圖表展示花褲衩大佬的一個(gè)實(shí)用管理后臺(tái)模版配套教程國(guó)際化后端工具解析請(qǐng)
koa2+vue2+mysql 全棧開發(fā)記錄
基于想要自己制作一個(gè)個(gè)人項(xiàng)目為由,于是有了這么一個(gè)開發(fā)記錄(梳理開發(fā)過程也是一個(gè)知識(shí)鞏固的過程)koa2+vue2+mysql 個(gè)人的一個(gè)通用DEMO(本篇文章的范例)
koa2+vue2+mysql GITHUB地址
前端工具vue
vue-router
vuex
axios
element ui 頁(yè)面UI組件
echartsjs 百度強(qiáng)大的圖表展示
vue-admin-template 花褲衩大佬的一個(gè)實(shí)用管理后臺(tái)模版 配套教程
vue-i18n 國(guó)際化
scss
后端工具koa
koa-bodyparser 解析 PUT / POST 請(qǐng)求中的 body
koa-convert
koa-json
koa-jwt jwt鑒權(quán)
koa-logger
koa-mysql-session
koa-onerror
koa-router
koa-session-minimal
koa-static
koa-views
koa2-cors 處理跨域
md5 加密
moment 時(shí)間處理
mysql
前端篇前端這邊其實(shí)沒什么好寫的,主要是在vue-admin-template基礎(chǔ)上做了一些修改src/utils/request.js的修改
request攔截器
// request攔截器 service.interceptors.request.use( config => { if (store.getters.token) { // config.headers["X-Token"] = getToken() // 因?yàn)槭莏wt方式鑒權(quán) 所以在header頭中改為如下寫法 config.headers["Authorization"] = "Bearer " + getToken() // 讓每個(gè)請(qǐng)求攜帶自定義token 請(qǐng)根據(jù)實(shí)際情況自行修改 } 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是拋錯(cuò) 可結(jié)合自己業(yè)務(wù)進(jìn)行修改 */ const res = response.data if (res.code !== 0) { // 因?yàn)楹笈_(tái)返回值為0則是成功,所以將原來(lái)的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( "你已被登出,可以取消繼續(xù)留在該頁(yè)面,或者重新登錄", "確定登出", { confirmButtonText: "重新登錄", cancelButtonText: "取消", type: "warning" } ).then(() => { store.dispatch("FedLogOut").then(() => { location.reload() // 為了重新實(shí)例化vue-router對(duì)象 避免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) } )封裝了一個(gè)Echart組件
具體參考我的另外一個(gè)文章 vue中使用echarts 使用記錄
后端篇 構(gòu)建項(xiàng)目目錄通過項(xiàng)目生成器生成koa-generator
npm install -g koa-generator
koa2 /server && cd /server
npm install
安裝組件
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錯(cuò)誤返回 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 " } 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文件夾用于存放數(shù)據(jù)庫(kù)連接等操作
default.js
// 數(shù)據(jù)庫(kù)配置 const config = { port: 3000, database: { DATABASE: "xxx", //數(shù)據(jù)庫(kù) USERNAME: "root", //用戶 PASSWORD: "xxx", //密碼 PORT: "3306", //端口 HOST: "127.0.0.1" //服務(wù)ip地址 }, secret: "jwt_secret" } module.exports = config數(shù)據(jù)庫(kù)相關(guān)(mysql)
createTables.js 一個(gè)簡(jiǎn)單的用戶角色權(quán)限表 用戶、角色、權(quán)限表的關(guān)系(mysql)
// 數(shù)據(jù)庫(kù)表格創(chuàng)建 const createTable = { users: `CREATE TABLE IF NOT EXISTS user_info ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT "(自增長(zhǎng))", user_id VARCHAR ( 100 ) NOT NULL COMMENT "賬號(hào)", user_name VARCHAR ( 100 ) NOT NULL COMMENT "用戶名", user_pwd VARCHAR ( 100 ) NOT NULL COMMENT "密碼", user_head VARCHAR ( 225 ) COMMENT "頭像", user_mobile VARCHAR ( 20 ) COMMENT "手機(jī)", user_email VARCHAR ( 64 ) COMMENT "郵箱", user_creatdata TIMESTAMP NOT NULL DEFAULT NOW( ) COMMENT "注冊(cè)日期", user_login_time TIMESTAMP DEFAULT NOW( ) COMMENT "登錄時(shí)間", user_count INT COMMENT "登錄次數(shù)" ) ENGINE = INNODB charset = utf8;`, role: `CREATE TABLE IF NOT EXISTS role_info ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT "(自增長(zhǎng))", 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 "(自增長(zhǎng))", permission_name VARCHAR ( 20 ) NOT NULL COMMENT "權(quán)限名", 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 "(自增長(zhǎng))", user_id INT NOT NULL COMMENT "關(guān)聯(lián)用戶", role_id INT NOT NULL COMMENT "關(guān)聯(lián)角色", 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 "(自增長(zhǎng))", role_id INT NOT NULL COMMENT "關(guān)聯(lián)角色", permission_id INT NOT NULL COMMENT "關(guān)聯(lián)權(quán)限", 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
創(chuàng)建lib文件夾 用于存儲(chǔ)數(shù)據(jù)庫(kù)查詢語(yǔ)句
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 } // 更新用戶登錄次數(shù)和登錄時(shí)間 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 路由配置
接口報(bào)錯(cuò)信息統(tǒng)一方法 創(chuàng)建error文件夾
ApiErrorNames.js
/** * API錯(cuò)誤名稱 */ var ApiErrorNames = {}; ApiErrorNames.UNKNOW_ERROR = "UNKNOW_ERROR"; ApiErrorNames.SUCCESS = "SUCCESS"; /* 參數(shù)錯(cuò)誤: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"; /* 用戶錯(cuò)誤: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"; /* 業(yè)務(wù)錯(cuò)誤:30001-39999 */ ApiErrorNames.SPECIFIED_QUESTIONED_USER_NOT_EXIST = "SPECIFIED_QUESTIONED_USER_NOT_EXIST"; /* 系統(tǒng)錯(cuò)誤:40001-49999 */ ApiErrorNames.SYSTEM_INNER_ERROR = "SYSTEM_INNER_ERROR"; /* 數(shù)據(jù)錯(cuò)誤:50001-599999 */ ApiErrorNames.RESULE_DATA_NONE = "RESULE_DATA_NONE"; ApiErrorNames.DATA_IS_WRONG = "DATA_IS_WRONG"; ApiErrorNames.DATA_ALREADY_EXISTED = "DATA_ALREADY_EXISTED"; /* 接口錯(cuò)誤: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"; /* 權(quán)限錯(cuò)誤:70001-79999 */ ApiErrorNames.PERMISSION_NO_ACCESS = "PERMISSION_NO_ACCESS"; ApiErrorNames.INVALID_TOKEN = "INVALID_TOKEN"; /** * API錯(cuò)誤名稱對(duì)應(yīng)的錯(cuò)誤信息 */ const error_map = new Map(); error_map.set(ApiErrorNames.SUCCESS, { code: 0, message: "成功" }); error_map.set(ApiErrorNames.UNKNOW_ERROR, { code: -1, message: "未知錯(cuò)誤" }); /* 參數(shù)錯(cuò)誤:10001-19999 */ error_map.set(ApiErrorNames.PARAM_IS_INVALID, { code: 10001, message: "參數(shù)無(wú)效" }); error_map.set(ApiErrorNames.PARAM_IS_BLANK, { code: 10002, message: "參數(shù)為空" }); error_map.set(ApiErrorNames.PARAM_TYPE_BIND_ERROR, { code: 10003, message: "參數(shù)類型錯(cuò)誤" }); error_map.set(ApiErrorNames.PARAM_NOT_COMPLETE, { code: 10004, message: "參數(shù)缺失" }); /* 用戶錯(cuò)誤:20001-29999*/ error_map.set(ApiErrorNames.USER_NOT_LOGGED_IN, { code: 20001, message: "用戶未登錄" }); error_map.set(ApiErrorNames.USER_LOGIN_ERROR, { code: 20002, message: "賬號(hào)不存在或密碼錯(cuò)誤" }); error_map.set(ApiErrorNames.USER_ACCOUNT_FORBIDDEN, { code: 20003, message: "賬號(hào)已被禁用" }); error_map.set(ApiErrorNames.USER_NOT_EXIST, { code: 20004, message: "用戶不存在" }); error_map.set(ApiErrorNames.USER_HAS_EXISTED, { code: 20005, message: "用戶已存在" }); /* 業(yè)務(wù)錯(cuò)誤:30001-39999 */ error_map.set(ApiErrorNames.SPECIFIED_QUESTIONED_USER_NOT_EXIST, { code: 30001, message: "某業(yè)務(wù)出現(xiàn)問題" }); /* 系統(tǒng)錯(cuò)誤:40001-49999 */ error_map.set(ApiErrorNames.SYSTEM_INNER_ERROR, { code: 40001, message: "系統(tǒng)繁忙,請(qǐng)稍后重試" }); /* 數(shù)據(jù)錯(cuò)誤:50001-599999 */ error_map.set(ApiErrorNames.RESULE_DATA_NONE, { code: 50001, message: "數(shù)據(jù)未找到" }); error_map.set(ApiErrorNames.DATA_IS_WRONG, { code: 50002, message: "數(shù)據(jù)有誤" }); error_map.set(ApiErrorNames.DATA_ALREADY_EXISTED, { code: 50003, message: "數(shù)據(jù)已存在" }); /* 接口錯(cuò)誤:60001-69999 */ error_map.set(ApiErrorNames.INTERFACE_INNER_INVOKE_ERROR, { code: 60001, message: "內(nèi)部系統(tǒng)接口調(diào)用異常" }); error_map.set(ApiErrorNames.INTERFACE_OUTTER_INVOKE_ERROR, { code: 60002, message: "外部系統(tǒng)接口調(diào)用異常" }); error_map.set(ApiErrorNames.INTERFACE_FORBID_VISIT, { code: 60003, message: "該接口禁止訪問" }); error_map.set(ApiErrorNames.INTERFACE_ADDRESS_INVALID, { code: 60004, message: "接口地址無(wú)效" }); error_map.set(ApiErrorNames.INTERFACE_REQUEST_TIMEOUT, { code: 60005, message: "接口請(qǐng)求超時(shí)" }); error_map.set(ApiErrorNames.INTERFACE_EXCEED_LOAD, { code: 60006, message: "接口負(fù)載過高" }); /* 權(quán)限錯(cuò)誤:70001-79999 */ error_map.set(ApiErrorNames.PERMISSION_NO_ACCESS, { code: 70001, message: "無(wú)訪問權(quán)限" }); error_map.set(ApiErrorNames.INVALID_TOKEN, { code: 70002, message: "無(wú)效token" }); //根據(jù)錯(cuò)誤名稱獲取錯(cuò)誤信息 ApiErrorNames.getErrorInfo = (error_name) => { var error_info; if (error_name) { error_info = error_map.get(error_name); } //如果沒有對(duì)應(yīng)的錯(cuò)誤信息,默認(rèn)"未知錯(cuò)誤" 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;
創(chuàng)建controller文件夾
對(duì)路由users進(jìn)行邏輯編寫
const mysqlModel = require("../lib/mysql") //引入數(shù)據(jù)庫(kù)方法 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, // 設(shè)置 token 過期時(shí)間 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")() //引入路由函數(shù) const userControl = require("../controller/users") //引入邏輯 // const config = require("../config/default.js") router.get("/", async (ctx, next) => { "use strict" ctx.redirect("/user/login") }) // 路由中間間,頁(yè)面路由到/,就是端口號(hào)的時(shí)候,(網(wǎng)址),頁(yè)面指引到//user/login router.get("/user/info", userControl.info) router.post("/user/logout", userControl.logout) router.post("/user/login", userControl.login) module.exports = router //將頁(yè)面暴露出去備注
ctx.request 獲取post請(qǐng)求中的body
ctx.query.xx 獲取get請(qǐng)求中的參數(shù)
router.prefix("/account") 給router實(shí)例添加前綴
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/102622.html
摘要:要注意這里必須和創(chuàng)建的時(shí)候傳入的一致,因?yàn)榉?wù)端需要用創(chuàng)建時(shí)的來(lái)解密。是校驗(yàn)碼解析時(shí)需要一致才能取到信息過期時(shí)間設(shè)置為格式有。 koa2+mysql+vue+vant 構(gòu)建簡(jiǎn)單版移動(dòng)端博客 具體內(nèi)容展示 showImg(https://segmentfault.com/img/remote/1460000015962704?w=375&h=670); showImg(https://s...
摘要:要注意這里必須和創(chuàng)建的時(shí)候傳入的一致,因?yàn)榉?wù)端需要用創(chuàng)建時(shí)的來(lái)解密。是校驗(yàn)碼解析時(shí)需要一致才能取到信息過期時(shí)間設(shè)置為格式有。 koa2+mysql+vue+vant 構(gòu)建簡(jiǎn)單版移動(dòng)端博客 具體內(nèi)容展示 showImg(https://segmentfault.com/img/remote/1460000015962704?w=375&h=670); showImg(https://s...
閱讀 1687·2021-11-15 11:38
閱讀 4544·2021-09-22 15:33
閱讀 2348·2021-08-30 09:46
閱讀 2194·2019-08-30 15:43
閱讀 839·2019-08-30 14:16
閱讀 2086·2019-08-30 13:09
閱讀 1265·2019-08-30 11:25
閱讀 714·2019-08-29 16:42