Koa+TypeScript从0到1实现简易CMS框架(二):路由自动加载与全局异常处理

Koa+TypeScript从0到1实现简易CMS框架(二):路由自动加载与全局异常处理

目录

项目地址:koa-typescript-cms

前言

koa本身是没有路由的,需借助第三方库koa-router实现路由功能,但是路由的拆分,导致app.ts里需要引入许多路由文件,为了方便,我们可以做一个简单的路由自动加载功能来简化我们的代码量;全局异常处理是每个cms框架中比不可少的部分,我们可以通过koa的中间件机制来实现此功能。

主要工具库

  • 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 非常好用的工具函数库

项目目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── 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)

  1. 获取api文件夹下的所有文件
  2. 判断文件的后缀名是否为.ts,如果是,使用CommonJS规范加载文件
  3. 判断文件导出的内容是否为Router类型,如果是,则加载路由

由于我们需要很多功能到要在服务执行后就加载,所以创建一个专门加载功能的类InitManager
InitManager类中创建类方法initLoadRouters,此方法专门作为加载路由的功能模块。
先创建一个辅助函数getFiles,此函数利用nodefs文件功能模块,来获取某文件夹下后的所有文件名,并返回一个字符串数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 获取文件夹下所有文件名
*
* @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;
}

接下来编写路由自动加载功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 路由自动加载
*
* @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类中的其他功能模块。

1
2
3
4
5
6
7
8
9
10
11
/**
* 入口方法
*
* @static
* @param {Koa} app
* @memberof InitManager
*/
static initCore(app: Koa) {
InitManager.app = app;
InitManager.initLoadRouters();
}

需要注意的是,路由文件导出的时候不能再以ES的规范导出了,必须以CommonJS的规范进行导出。

api/v1/book.ts文件源码:

1
2
3
4
5
6
7
8
9
10
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中加载,代码:

1
2
3
import InitManager from './core/init'

InitManager.initCore(app)

此为还需要全局加载配置文件,与加载路由大同小异,代码一并附上

app/core/init.ts全部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 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全部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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代码:

1
2
3
4
5
6
7
8
9
10
11
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框架(三):用户模型、参数校验与用户注册接口

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×