Koa+TypeScript從0到1實現簡易CMS框架(二):路由自動加載與全局異常處理

目錄

項目地址:koa-typescript-cmsjavascript

前言

koa自己是沒有路由的,需藉助第三方庫koa-router實現路由功能,可是路由的拆分,致使app.ts裏須要引入許多路由文件,爲了方便,咱們能夠作一個簡單的路由自動加載功能來簡化咱們的代碼量;全局異常處理是每一個cms框架中比不可少的部分,咱們能夠經過koa的中間件機制來實現此功能。java

主要工具庫

  • koa web框架
  • koa-bodyparser 處理koa post請求
  • koa-router koa路由
  • sequelize、sequelize-typescript、mysql2 ORM框架與Mysql
  • validator、class-validator 參數校驗
  • jsonwebtoken jwt
  • bcryptjs 加密工具
  • reflect-metadata 給裝飾器添加各類信息
  • nodemon 監聽文件改變自動重啓服務
  • lodash 很是好用的工具函數庫

項目目錄

├── dist                                        // ts編譯後的文件
├── src                                         // 源碼目錄
│   ├── components                              // 組件
│   │   ├── app                                 // 項目業務代碼
│   │   │   ├── api                             // api層
│   │   │   ├── service                         // service層
│   │   │   ├── model                           // model層
│   │   │   ├── validators                      // 參數校驗類
│   │   │   ├── lib                             // interface與enum
│   │   ├── core                                // 項目核心代碼
│   │   ├── middlewares                         // 中間件
│   │   ├── config                              // 全局配置文件
│   │   ├── app.ts                              // 項目入口文件
├── tests                                       // 單元測試
├── package.json                                // package.json                                
├── tsconfig.json                               // ts配置文件
複製代碼

路由自動加載

思路:(此功能借鑑lin-cms開源的lin-cms-koa-core)node

  1. 獲取api文件夾下的全部文件
  2. 判斷文件的後綴名是否爲.ts,若是是,使用CommonJS規範加載文件
  3. 判斷文件導出的內容是否爲Router類型,若是是,則加載路由

因爲咱們須要不少功能到要在服務執行後就加載,因此建立一個專門加載功能的類InitManager
InitManager類中建立類方法initLoadRouters,此方法專門做爲加載路由的功能模塊。
先建立一個輔助函數getFiles,此函數利用nodefs文件功能模塊,來獲取某文件夾下後的全部文件名,並返回一個字符串數組:mysql

/**
 * 獲取文件夾下全部文件名
 *
 * @export
 * @param {string} dir
 * @returns
 */
export function getFiles(dir: string): string[] {
  let res: string[] = [];
  const files = fs.readdirSync(dir);
  for (const file of files) {
    const name = dir + "/" + file;
    if (fs.statSync(name).isDirectory()) {
      const tmp = getFiles(name);
      res = res.concat(tmp);
    } else {
      res.push(name);
    }
  }
  return res;
}
複製代碼

接下來編寫路由自動加載功能:git

/** * 路由自動加載 * * @static * @memberof InitManager */
  static initLoadRouters() {
    const mainRouter = new Router();
    const path: string = `${process.cwd()}/src/app/api`;
    const files: string[] = getFiles(path);
    for (let file of files) {
      // 獲取文件後綴名
      const extention: string = file.substring(
        file.lastIndexOf("."),
        file.length
      );
      if (extention === ".ts") {
        // 加載api文件夾下全部文件
        // 並檢測文件是不是koa的路由
        // 若是是路由便將路由加載
        const mod: Router = require(file);
        if (mod instanceof Router) {
          // consola.info(`loading a router instance from file: ${file}`);
          get(mod, "stack", []).forEach((ly: Router.Layer) => {
            consola.info(`loading a route: ${get(ly, "path")}`);
          });
          mainRouter.use(mod.routes()).use(mod.allowedMethods());
        }
      }
    }
  }
複製代碼

InitManager中建立另外一個類方法initCore,此方法需傳入一個koa實例,統一加載InitManager類中的其餘功能模塊。github

/** * 入口方法 * * @static * @param {Koa} app * @memberof InitManager */
  static initCore(app: Koa) {
    InitManager.app = app;
    InitManager.initLoadRouters();
  }
複製代碼

須要注意的是,路由文件導出的時候不能再以ES的規範導出了,必須以CommonJS的規範進行導出。web

api/v1/book.ts文件源碼:sql

import Router from 'koa-router'

const router: Router = new Router();
router.prefix('/v1/book')
router.get('/', async (ctx) => {
    ctx.body = 'Hello Book';
});

// 注意這裏
module.exports = router 
複製代碼

最後在app.ts中加載,代碼:typescript

import InitManager from './core/init'

InitManager.initCore(app)
複製代碼

此爲還須要全局加載配置文件,與加載路由大同小異,代碼一併附上數據庫

app/core/init.ts所有代碼:

import Koa from "koa";
import Router from "koa-router";
import consola from "consola";
import { get } from "lodash";
[
import { getFiles } from "./utils";
import { config, configInterface } from "../config/config";
declare global {
  namespace NodeJS {
    interface Global {
      config?: configInterface;
    }
  }
}
class InitManager {
  static app: Koa<Koa.DefaultState, Koa.DefaultContext>;

  /** * 入口方法 * * @static * @param {Koa} app * @memberof InitManager */
  static initCore(app: Koa) {
    InitManager.app = app;
    InitManager.initLoadRouters();
    InitManager.loadConfig();
  }

  /** * 路由自動加載 * * @static * @memberof InitManager */
  static initLoadRouters() {
    const mainRouter = new Router();
    const path: string = `${process.cwd()}/src/app/api`;
    const files: string[] = getFiles(path);
    for (let file of files) {
      // 獲取文件後綴名
      const extention: string = file.substring(
        file.lastIndexOf("."),
        file.length
      );
      if (extention === ".ts") {
        // 加載api文件夾下全部文件
        // 並檢測文件是不是koa的路由
        // 若是是路由便將路由加載
        const mod: Router = require(file);
        if (mod instanceof Router) {
          // consola.info(`loading](https://note.youdao.com/) a router instance from file: ${file}`);
          get(mod, "stack", []).forEach((ly: Router.Layer) => {
            consola.info(`loading a route: ${get(ly, "path")}`);
          });
          mainRouter.use(mod.routes()).use(mod.allowedMethods());
        }
      }
    }
  }

  /** * 載入配置文件 * * @static * @memberof InitManager */
  static loadConfig() {
    global.config = config;
  }
}
export default InitManager;

複製代碼

全局異常處理

此功能需依賴koa的中間件機制進行開發
異常分爲已知異常與未知異常,需針對其進行不一樣處理

常見的已知異常:路由參數錯誤、從數據庫查詢查詢到空數據……
常見的未知錯誤:不正確的代碼致使的依賴庫報錯……

已知異常咱們須要向用戶拋出,以json的格式返回到客戶端。
而未知異常通常只有在開發環境纔會讓它拋出,而且只有開發人員能夠看到。

已知異常向用戶拋出時,需攜帶錯誤信息、錯誤代碼、請求路徑等信息。
咱們須要針對已知異常封裝一個類,用來標識錯誤爲已知異常。
app/core目錄下建立文件exception.ts,此文件裏有一個基類HttpException,此類繼承JavaScript的內置對象Error,以後全部的已知異常類都將繼承HttpException
代碼:

/** * HttpException 類構造函數的參數接口 */
export interface Exception {
  code?: number;
  msg?: any;
  errorCode?: number;
}
export class HttpException extends Error {
  /** * http 狀態碼 */
  public code: number = 500;

  /** * 返回的信息內容 */
  public msg: any = "服務器未知錯誤";

  /** * 特定的錯誤碼 */
  public errorCode: number = 999;

  public fields: string[] = ["msg", "errorCode"];

  /** * 構造函數 * @param ex 可選參數,經過{}的形式傳入 */
  constructor(ex?: Exception) {
    super();
    if (ex && ex.code) {
      assert(isInteger(ex.code));
      this.code = ex.code;
    }
    if (ex && ex.msg) {
      this.msg = ex.msg;
    }
    if (ex && ex.errorCode) {
      assert(isInteger(ex.errorCode));
      this.errorCode = ex.errorCode;
    }
  }
}
複製代碼

針對以上的狀況進行編碼app/middlewares/exception.ts所有代碼:

import { BaseContext, Next } from "koa";
import { HttpException, Exception } from "../core/exception";
interface CatchError extends Exception {
  request?: string;
}
const catchError = async (ctx: BaseContext, next: Next) => {
  try {
    await next();
  } catch (error) {
    const isHttpException = error instanceof HttpException
    const isDev = global.config?.environment === "dev"
    
    if (isDev && !isHttpException) {
      throw error;  
    }
    if (isHttpException) {
      const errorObj: CatchError = {
        msg: error.msg,
        errorCode: error.errorCode,
        request: `${ctx.method} ${ctx.path}`
      };
      ctx.body = errorObj;
      ctx.status = error.code;
    } else {
      const errorOjb: CatchError = {
        msg: "出現異常",
        errorCode: 999,
        request: `${ctx.method} ${ctx.path}`
      };
      ctx.body = errorOjb;
      ctx.status = 500;
    }
  }
};

export default catchError;
複製代碼

最後,app.ts裏使用中間件,app.ts代碼:

import Koa from 'koa';
import InitManager from './core/init'
import catchError from './middlewares/exception';

const app = new Koa()
app.use(catchError)
InitManager.initCore(app)

app.listen(3001);

console.log('Server running on port 3001');
複製代碼

下一篇:Koa+TypeScript從0到1實現簡易CMS框架(三):用戶模型、參數校驗與用戶註冊接口

相關文章
相關標籤/搜索