項目地址:koa-typescript-cmsjavascript
koa
自己是沒有路由的,需藉助第三方庫koa-router
實現路由功能,可是路由的拆分,致使app.ts
裏須要引入許多路由文件,爲了方便,咱們能夠作一個簡單的路由自動加載功能來簡化咱們的代碼量;全局異常處理是每一個cms框架中比不可少的部分,咱們能夠經過koa
的中間件機制來實現此功能。java
├── 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
api
文件夾下的全部文件.ts
,若是是,使用CommonJS
規範加載文件Router
類型,若是是,則加載路由因爲咱們須要不少功能到要在服務執行後就加載,因此建立一個專門加載功能的類InitManager
。
再InitManager
類中建立類方法initLoadRouters
,此方法專門做爲加載路由的功能模塊。
先建立一個輔助函數getFiles
,此函數利用node
的fs
文件功能模塊,來獲取某文件夾下後的全部文件名,並返回一個字符串數組: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');
複製代碼