用優雅的代碼武裝咱們的koa2項目

衆所周知,koa2是基於nodejs的一款很是輕量級的服務端框架,其簡單易上手的特性更是大大節省了前端人員開發服務端api的成本。儘管許多功能可以實現,可是做爲一個有素養的開發人員,代碼的層次性、後期可維護性都是須要考慮周到的。前端

實話說,按照koa官方文檔來照葫蘆畫瓢,咱們的代碼是寫不漂亮的。node

這裏須要咱們在編碼以前有一個很是清晰的認識:咱們的代碼如何組織?如何分層?如何複用?ios

在經歷一系列的思考斟酌以及一些項目的實踐以後,我總結了一些關於koa的開發技巧,可以大幅度的提升項目的代碼質量,不再用讓同伴笑話代碼寫的爛啦!程序員

1、路由的自動加載

以前咱們的路由老是手動註冊的?大概是這樣的:web

//app.js
const Koa = require('koa');
const app = new Koa(); 

const user = require('./app/api/user');
const store = require('./app/api/store');

app.use(user.routes());
app.use(classic.routes());
複製代碼

對於寫過koa項目的人來講,這段代碼是否是至關熟悉呢?其實如今只有兩個路由文件還好,但實際上這樣的文件數量龐大到必定的程度,再像這樣引入再use方式未免會顯得繁瑣拖沓。那有沒有辦法讓這些文件自動被引入、自動被use呢?數據庫

有的。如今讓咱們來安裝一個很是好用的包:npm

npm install require-directory --save
複製代碼

如今只須要這麼作:json

//...
const Router = require('koa-router'); 
const requireDirectory = require('require-directory');
//module爲固定參數,'./api'爲路由文件所在的路徑(支持嵌套目錄下的文件),第三個參數中的visit爲回調函數
const modules = requireDirectory(module, './app/api', {
    visit: whenLoadModule
});
function whenLoadModule(obj) {
    if(obj instanceof Router) {
        app.use(obj.routes());
    }
}
複製代碼

因而可知,好的代碼是能夠提高效率的,這樣的自動加載路由省去了不少註冊配置的功夫,是否是很是酷炫?axios

2、用管理器將入口文件內容抽離

相信不少人都這樣作:路由註冊代碼寫在了入口文件app.js中,之後進行相應中間件的導入也是寫在這個文件。可是對於入口文件來講,咱們是不但願讓它變得十分臃腫的,所以咱們能夠適當地將一些操做抽離出來。後端

在根目錄下建一個文件夾core,之後一些公共的代碼都存放在這裏。

//core/init.js
const requireDirectory = require('require-directory');
const Router = require('koa-router'); 

class InitManager {
    static initCore(app) {
        //把app.js中的koa實例傳進來
        InitManager.app = app;
        InitManager.initLoadRouters();
    }
    static initLoadRouters() {
        //注意這裏的路徑是依賴於當前文件所在位置的
        //最好寫成絕對路徑
        const apiDirectory = `${process.cwd()}/app/api`
        const modules = requireDirectory(module, apiDirectory, {
            visit: whenLoadModule
        });
        function whenLoadModule(obj) {
            if(obj instanceof Router) {
                InitManager.app.use(obj.routes())
            }
        }
    }
}

module.exports = InitManager;
複製代碼

如今在app.js中

const Koa = require('koa');
const app = new Koa();

const InitManager = require('./core/init');
InitManager.initCore(app);
複製代碼

能夠說已經精簡不少了,並且功能的實現照樣沒有問題。

3、開發環境和生產環境的區分

有時候,在兩種不一樣的環境下,咱們須要作不一樣的處理,這時候就須要咱們提早在全局中注入相應的參數。

首先在項目根目錄中,建立config文件夾:

//config/config.js
module.exports = {
  environment: 'dev'
}
複製代碼
//core/init.js的initManager類中增長以下內容
static loadConfig() {
    const configPath = process.cwd() + '/config/config.js';
    const config = require(configPath);
    global.config = config;
}
複製代碼

如今經過全局的global變量中就能夠取到當前的環境啦。

4、全局異常處理中間件

一、異步異常處理的坑

在服務端api編寫的過程當中,異常處理是很是重要的一環,由於不可能每一個函數返回的結果都是咱們想要的。不管是語法的錯誤,仍是業務邏輯上的錯誤,都須要讓異常拋出,讓問題以最直觀的方式暴露,而不是直接忽略。關於編碼風格,《代碼大全》裏面也強調過,在一個函數遇到異常時,最好的方式不是直接return false/null,而是讓異常直接拋出。

而在JS中,不少時候咱們都在寫異步代碼,例如定時器,Promise等等,這就會產生一個問題,若是用try/catch的話,這樣的異步代碼中的錯誤咱們是沒法捕捉的。例如:

function func1() {
  try {
    func2();
  } catch (error) {
    console.log('error');
  }
}

function func2() {
  setTimeout(() => {
    throw new Error('error')
  }, 1000)
}

func1();
複製代碼

執行這些代碼,你會發現過了一秒後程序直接報錯,console.log('error')並無執行,也就是func1並無捕捉到func2的異常。這就是異步的問題所在。

那怎麼解決這個坑呢?

最簡便的方式是採起async-await。

async function func1() {
  try {
    await func2();
  } catch (error) {
    console.log('error');
  }
}

function func2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject()
    }, 1000)
  })
}

func1();
複製代碼

在這裏的異步函數被Promise封裝,而後reject觸發func1中的catch,這就捕捉到了func2中的異常。慶幸的是,像func2這樣的異步代碼,如今經常使用的庫(如axios、sequelize)已經爲咱們封裝好了Promise對象,不用咱們本身去封裝了,直接去經過async-await的方式去try/catch就好了。

忠告: 經過這種方式,只要是異步代碼,執行以前必需要加await,不加會報Unhandled promise rejection的錯誤。血的教訓!

二、設計異常處理中間件

//middlewares/exception.js
//這裏的工做是捕獲異常生成返回的接口
const catchError = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    if(error.errorCode) {
      ctx.body = {
        msg: error.msg,
        error_code: error.errorCode,
        request: `${ctx.method} ${ctx.path}`
      };
    } else {
      //對於未知的異常,採用特別處理
      ctx.body = {
        msg: 'we made a mistake',
      };
    }
  }
}
module.exports = catchError;
複製代碼

到入口文件使用這個中間件。

//app.js
const catchError = require('./middlewares/exception');
app.use(catchError)
複製代碼

接着咱們來以HttpException爲例生成特定類型的異常。

//core/http-exception.js
class HttpException extends Error {
  //msg爲異常信息,errorCode爲錯誤碼(開發人員內部約定),code爲HTTP狀態碼
  constructor(msg='服務器異常', errorCode=10000, code=400) {
    super()
    this.errorCode = errorCode
    this.code = code
    this.msg = msg
  }
}

module.exports = {
  HttpException
}
複製代碼
//app/api/user.js
const Router = require('koa-router')
const router = new Router()
const { HttpException } = require('../../core/http-exception')

router.post('/user', (ctx, next) => {
    if(true){
        const error = new HttpException('網絡請求錯誤', 10001, 400)
        throw error
  }
})
module.exports = router;
複製代碼

返回的接口這樣:

這樣就拋出了一個特定類型的錯誤。可是在業務中錯誤的類型是很是複雜的,如今我就把我編寫的一些Exception類分享一下,供你們來參考:

//http-exception.js
class HttpException extends Error {
  constructor(msg = '服務器異常', errorCode=10000, code=400) {
    super()
    this.error_code = errorCode
    this.code = code
    this.msg = msg
  }
}

class ParameterException extends HttpException{
  constructor(msg, errorCode){
    super(400, msg='參數錯誤', errorCode=10000);
  }
}

class NotFound extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='資源未找到', errorCode=10001);
  }
}

class AuthFailed extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='受權失敗', errorCode=10002);
  }
}

class Forbidden extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='禁止訪問', errorCode=10003);
    this.msg = msg || '禁止訪問';
    this.errorCode = errorCode || 10003;
    this.code = 404;
  }
}

module.exports = {
  HttpException,
  ParameterException,
  Success,
  NotFound,
  AuthFailed,
  Forbidden
}
複製代碼

對於這種常常須要調用的錯誤處理的代碼,有必要將它放到全局,不用每次都導入。

如今的init.js中是這樣的:

const requireDirectory = require('require-directory');
const Router = require('koa-router');

class InitManager {
  static initCore(app) {
    //入口方法
    InitManager.app = app;
    InitManager.initLoadRouters();
    InitManager.loadConfig();
    InitManager.loadHttpException();//加入全局的Exception
  }
  static initLoadRouters() {
    // path config
    const apiDirectory = `${process.cwd()}/app/api/v1`;
    requireDirectory(module, apiDirectory, {
      visit: whenLoadModule
    });

    function whenLoadModule(obj) {
      if (obj instanceof Router) {
        InitManager.app.use(obj.routes());
      }
    }
  }
  static loadConfig(path = '') {
    const configPath = path || process.cwd() + '/config/config.js';
    const config = require(configPath);
    global.config = config;
  }
  static loadHttpException() {
    const errors = require('./http-exception');
    global.errs = errors;
  }
}

module.exports = InitManager;
複製代碼

5、使用JWT完成認證受權

JWT(即Json Web Token)目前最流行的跨域身份驗證解決方案之一。它的工做流程是這樣的:

1.前端向後端傳遞用戶名和密碼

2.用戶名和密碼在後端覈實成功後,返回前端一個token(或存在cookie中)

3.前端拿到token並進行保存

4.前端訪問後端接口時先進行token認證,認證經過才能訪問接口。

那麼在koa中咱們須要作哪些事情?

在生成token階段:首先是驗證帳戶,而後生成token令牌,傳給前端。

在認證token階段: 完成認證中間件的編寫,對前端的訪問作一層攔截,token認證事後才能訪問後面的接口。

1.生成token

先安裝兩個包:

npm install jsonwebtoken basic-auth --save
複製代碼
//config.js
module.exports = {
  environment: 'dev',
  database: {
    dbName: 'island',
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: 'fjfj'
  },
  security: {
    secretKey: 'lajsdflsdjfljsdljfls',//用來生成token的key值
    expiresIn: 60 * 60//過時時間
  }
}

//utils.js 
//生成token令牌函數,uid爲用戶id,scope爲權限等級(類型爲數字,內部約定)
const generateToken = function(uid, scope){
    const { secretKey, expiresIn } = global.config.security
    //第一個參數爲用戶信息的js對象,第二個爲用來生成token的key值,第三個爲配置項
    const token = jwt.sign({
        uid,
        scope
    },secretKey,{
        expiresIn
    })
    return token
}
複製代碼

2.Auth中間件實現攔截

//前端傳token方式
//在請求頭中加上Authorization:`Basic ${base64(token+":")}`便可
//其中base64爲第三方庫js-base64導出的一個方法

//middlewares/auth.js
const basicAuth = require('basic-auth');
const jwt = require('jsonwebtoken');

class Auth {
  constructor(level) {
    Auth.USER = 8;
    Auth.ADMIN = 16;
    this.level = level || 1;
  }
  //注意這裏的m是一個屬性
  get m() {
    return async (ctx, next) => {
      const userToken = basicAuth(ctx.req);
      let errMsg = 'token不合法';

      if(!userToken || !userToken.name) {
        throw new global.errs.Forbidden();
      }
      try {
        //將前端傳過來的token值進行認證,若是成功會返回一個decode對象,包含uid和scope
        var decode = jwt.verify(userToken.name, global.config.security.secretKey);
      } catch (error) {
        // token不合法
        // 或token過時
        // 拋異常
        errMsg = '//根據狀況定義'
        throw new global.errs.Forbidden(errMsg);
      }
      //將uid和scope掛載ctx中
      ctx.auth = {
        uid: decode.uid,
        scope: decode.scope
      };
      //如今走到這裏token認證經過
      await next();
    }
  }
}
module.exports = Auth;
複製代碼

在路由相應文件中編寫以下:

//中間件先行,若是中間件中認證未經過,則不會走到路由處理邏輯這裏來
router.post('/xxx', new Auth().m , async (ctx, next) => {
    //......
})
複製代碼

6、require路徑別名

在開發的過程,當項目的目錄愈來愈複雜的時候,包的引用路徑也變得愈來愈麻煩。曾經就出現過這樣的導入路徑:

const Favor = require('../../../models/favor');
複製代碼

甚至還有比這個更加冗長的導入方式,做爲一個有代碼潔癖的程序員,實在讓人看的很是不爽。其實經過絕對路徑process.cwd()的方式也是能夠解決這樣一個問題的,可是當目錄深到必定程度的時候,導入的代碼也很是繁冗。那有沒有更好的解決方式呢?

使用module-alias將路徑別名就能夠。

npm install module-alias --save
複製代碼
//package.json添加以下內容
  "_moduleAliases": {
    "@models": "app/models"
  },
複製代碼

而後在app.js引入這個庫:

//引入便可
require('module-alias/register');
複製代碼

如今引入代碼就變成這樣了:

const Favor = require('@models/favor');
複製代碼

簡潔清晰了許多,也更容易讓人維護。

7、利用sequelize的事務解決數據不一致問題

當一個業務要進行多項數據庫的操做時,拿點贊功能爲例,首先你得在點贊記錄的表中增長記錄,而後你要將對應對象的點贊數加1,這兩個操做是必需要一塊兒完成的,若是有一個操做成功,另外一個操做出現了問題,那就會致使數據不一致,這是一個很是嚴重的安全問題。

咱們但願若是出現了任何問題,直接回滾到未操做以前的狀態。這個時候建議用數據庫事務的操做。利用sequelize的transaction是能夠完成的,把業務部分的代碼貼一下:

async like(art_id, uid) {
    //查找是否有重複的
    const favor = await Favor.findOne({
      where: { art_id, uid }
      }
    );
    //有重複則拋異常
    if (favor) {
      throw new global.errs.LikeError('你已經點過讚了');
    }
    //db爲sequelize的實例
    //下面是事務的操做
    return db.transaction(async t => {
      //1.建立點贊記錄
      await Favor.create({ art_id, uid }, { transaction: t });
      //2.增長點贊數
      const art = await Art.getData(art_id, type);//拿到被點讚的對象
      await art.increment('fav_nums', { by: 1, transaction: t });//加1操做
    });
  }
複製代碼

sequelize中的transaction大概就是這樣作的,官方文檔是promise的方式,看起來實在太不美觀,改爲async/await方式會好不少,可是千萬不要忘了寫await。

關於koa2的代碼優化,就先分享到這裏,未完待續,後續會不斷補充。歡迎點贊、留言!

相關文章
相關標籤/搜索