项目地址: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');
复制代码