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 已經夠方便,外掛也足夠多,完全可以根據自己的需求,像搭積木一樣構建出最適合業務需求的框架。這樣不但摒棄了很多用不到的外掛,使整個框架更加精簡,也能對整個框架知根知底,減少了很多不可預知因素的影響。
當然我覺得最主要的是我比較懶:smile:,不想再去學其他框架特有的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 構建 資料庫訪問層。