原文地址:koa+mysql+vue+socket.io全棧開發之web api篇
目標是建立一個 web QQ的項目,使用的技術棧如下:
-
后端是基於koa2 的 web api 服務層,提供curd操作的http接口,登錄驗證使用的是 json web token,跨域方案使用的是 cors;
-
數據庫使用的是 mysql;
-
為了實時通信,使用的是基於websocket協議的 socket.io 框架;
-
前端則使用的是 vue + vuex。
本篇則講敘服務端的搭建,之所以使用 koa,而不使用其他封裝過的框架,比如 Egg.js, Thinkjs。因為在我看來,koa2 已經夠方便,插件也足夠多,完全可以根據自己的需求,像搭積木一樣構建出最適合業務需求的框架。這樣不但摒棄了很多用不到的插件,使整個框架更加精簡,也能對整個框架知根知底,減少了很多不可預知因素的影響。
當然我覺得最主要的是我比較懶😄,不想再去學其他框架特有的api,特有的配置。因為前端有太多框架太多api需要掌握了,對於非互聯網公認的技術標准,我覺得學習的優先級還是要靠后一點的。因為這些個框架,三天兩頭就冒出個熱門的,簡直多不勝數,學不過來啊,而koa基本都是這些框架的底層,明顯靠譜多了。
基本框架搭建
這幾個koa插件大部分項目八九不離十要用到:
- koa-body 解析http數據
- koa-compress gzip壓縮
- koa-router 路由
- koa-static 設置靜態目錄
- koa2-cors 跨域cors
- log4js 老牌的日志組件
- jsonwebtoken jwt 組件
基本的目錄結構
public #公共目錄
src #前端目錄
server #后端目錄
├── common #工具
├── config #配置文件
├── controller #控制器
├── daos #數據庫訪問層
├── logs #日志目錄
├── middleware #中間件目錄
├── socket #socketio目錄
├── app.js #入口文件
└── router.js #路由
入口文件app.js
主要就是幾個中間件配置需要注意一下,這里同時還加載了 socket.io 服務。socket.io 相關的基本知識點可以看我之前寫的文章關於socket.io的使用。
//app.js
//...
const path = require("path");
const baseDir = path.normalize(__dirname + "/..");
// gzip
app.use(
compress({
filter: function(content_type) {
return /text|javascript/i.test(content_type);
},
threshold: 2048,
flush: require("zlib").Z_SYNC_FLUSH
})
);
// 解析請求
app.use(
koaBody({
jsonLimit: 1024 * 1024 * 5,
formLimit: 1024 * 1024 * 5,
textLimit: 1024 * 1024 * 5,
multipart: true, // 解析FormData數據
formidable: { uploadDir: path.join(baseDir, "public/upload") }//上傳文件目錄
})
);
// 設置靜態目錄
app.use(static(path.join(baseDir, "public"), { index: false }));
app.use(favicon(path.join(baseDir, "public/favicon.ico")));
//cors
app.use(
cors({
origin: "http://localhost:" + config.clientPort,
credentials: true,
allowMethods: ["GET", "POST", "DELETE"],
exposeHeaders: ["Authorization"],
allowHeaders: ["Content-Type", "Authorization", "Accept"]
})
);
//json-web-token中間件
app.use(
jwt({
secret: config.secret,
exp: config.exp
})
);
// 登錄驗證中間件,exclude 表示不驗證的頁面,include 表示要驗證的頁面
app.use(
verify({
exclude: ["/login", "/register", "/search"]
})
);
// 錯誤處理中間件
app.use(errorHandler());
// 路由
addRouters(router);
app.use(router.routes()).use(router.allowedMethods());
// 處理404
app.use(async (ctx, next) => {
log.error(`404 ${ctx.message} : ${ctx.href}`);
ctx.status = 404;
ctx.body = { code: 404, message: "404! not found !" };
});
// 處理中間件和系統錯誤
app.on("error", (err, ctx) => {
log.error(err); //log all errors
ctx.status = 500;
ctx.statusText = "Internal Server Error";
if (ctx.app.env === "development") {
//throw the error to frontEnd when in the develop mode
ctx.res.end(err.stack); //finish the response
} else {
ctx.body = { code: -1, message: "Server Error" };
}
});
if (!module.parent) {
const { port, socketPort } = config;
/**
* koa app
*/
app.listen(port);
log.info(`=== app server running on port ${port}===`);
console.log("app server running at: http://localhost:%d", port);
/**
* socket.io
*/
addSocket(io);
server.listen(socketPort);
}
跨域cors 和 json web token
這里解釋一下 koa-cors 參數的設置,我項目使用的是 json web token,需要把認證字段Authorization添加到header,前端獲取該header字段,之后給后台發送http請求的時候,再帶上該Authorization。
- origin:如果要訪問header里面的字段或者設置cookie,要寫具體的域名地址,用 星號 * 是不行的;
- credentials:主要是給前端獲取cookie;
- allowMethods:允許訪問的方法;
- exposeHeaders:前端如果要獲取該header字段,必須寫明(json web token用);
- allowHeaders:添加到header的字段;
至於 json web token的原理,網上資料齊全,這里不再介紹了。
app.use(
cors({
origin: "http://localhost:" + config.clientPort, // 訪問header,要寫明具體域名才行
credentials: true, //將憑證暴露出來, 前端才能獲取cookie
allowMethods: ["GET", "POST", "DELETE"],
exposeHeaders: ["Authorization"], // 將header字段expose出去
allowHeaders: ["Content-Type", "Authorization", "Accept"] // 允許添加到header的字段
})
);
中間件middleware
koa 的中間件就是 web開發的利器,通過它可以非常方便的實現 強類型語言中的 aop 切面編程,而koa2 中間件 的編寫也足夠簡單 koajs。
項目在以下幾個地方都用中間件進行了封裝,很多重復的樣板代碼因此得以簡化。
- json web token(jwt)
- 登錄驗證(verify)
- 錯誤處理(errorHandler)
就以最簡單的錯誤處理中間件為例子,如果不使用錯誤處理中間件,我們需要每個控制器方法進行 try{…} catch{…} ,其他中間件編寫方式類似,就不再介紹。
/**
* error handler 中間件
*/
module.exports = () => {
return async (ctx, next) => {
try {
await next();//沒有錯誤則進入下一個中間件
} catch (err) {
log.error(err);
let obj = {
code: -1,
message: '服務器錯誤'
};
if (ctx.app.env === 'development') {
obj.err = err;
}
ctx.body = obj
}
};
};
// 控制器代碼使用error handler中間件后,每個方法都不需要 try catch處理錯誤,記錄錯誤日志,處理邏輯都集中在中間件里面了。
exports.getInfo = async function(ctx) {
// try {
const token = await ctx.verify();
const [users, friends] = await Promise.all([
userDao.getUser({ id: token.uid }),
getFriends([token.uid])
]);
const msgs = applys.map(formatTime);
ctx.body = {
code: 0,
message: "好友列表",
data: {
user: users[0],
friends: mergeReads(friends, reads),
groups,
msgs
}
};
// } catch (err) {
// log.error(err);
// let obj = {
// code: -1,
// message: "服務器錯誤"
// };
// if (ctx.app.env === "development") {
// obj.err = err;
// }
// ctx.body = obj;
// }
};
路由配置
路由配置只使用了get,post 方法,當然要使用 put,delete也只是改一下名字就行。
// router.js
const { uploadFile } = require('./controller/file')
const { login, register } = require('./controller/sign')
const { addGroup, delGroup, updateGroup } = require('./controller/group')
//...
module.exports = function (router) {
router
.post('/login', login)
.post('/register', register)
.post('/upload', uploadFile)
.post('/addgroup', addGroup)
.post('/delgroup', delGroup)
.post('/updategroup', updateGroup)
//...
};
控制器
以updateInfo方法為例,koa2 已經全面支持 async await,編寫方式和同步代碼沒多大區別。
exports.updateInfo = async function (ctx) {
const form = ctx.request.body;
const token = await ctx.verify();
const ret = await userDao.update([form, token.uid]);
if (!ret.affectedRows) {
return ctx.body = {
code: 2,
message: '更新失敗'
};
}
ctx.body = {
code: 0,
message: '更新成功'
};
}
后續
接着下一編就是基於 mysql 構建 數據庫訪問層。