koa+mysql+vue+socket.io全棧開發之web api篇

JeffZhong發表於2019-04-11

原文地址:koa+mysql+vue+socket.io全棧開發之web api篇

目標是建立一個 web QQ的專案,使用的技術棧如下:

  1. 後端是基於koa2 的 web api 服務層,提供curd操作的http介面,登入驗證使用的是 json web token,跨域方案使用的是 cors
  2. 資料庫使用的是 mysql
  3. 為了實時通訊,使用的是基於websocket協議的 socket.io 框架;
  4. 前端則使用的是 vue + vuex

本篇則講敘服務端的搭建,之所以使用 koa,而不使用其他封裝過的框架,比如 Egg.jsThinkjs。因為在我看來,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 構建 資料庫訪問層。

相關文章