初次提交

This commit is contained in:
2024-10-11 16:38:28 +08:00
commit 01ad00135a
6 changed files with 345 additions and 0 deletions

12
src/error.ts Normal file
View File

@ -0,0 +1,12 @@
export class RouterError extends Error {
#code: string;
public constructor(code: string, message: string) {
super(message);
this.#code = code;
}
public get code(): string {
return this.#code;
}
}

2
src/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./error";
export * from "./router";

211
src/router.ts Normal file
View File

@ -0,0 +1,211 @@
import { Middleware } from "koa";
import Koa from "koa";
import { RouterError } from "./error";
export type KoaRouterMethods = "get" | "post" | "put" | "delete" | "patch";
interface IKoaRouterMatcher {
}
interface IKoaRouteConfig {
method: KoaRouterMethods
pathname: string
regexp: RegExp
parsers: Record<string, IKoaRouterTypeParser>
middleware: Middleware
}
interface IKoaRouterTypeParser {
(value: string): any
}
type KoaRouterDefaultTypes = "int" | "boolean";
export interface IKoaRouterOptions {
/** 前缀 */
prefix?: string;
/** 默认的参数类型如果传true则安装所有如果传数组则安装指定的类型 */
defaultParamTypes?: Array<KoaRouterDefaultTypes> | boolean;
/** 自定义类型 */
customParamtypes?: Record<string, { regexp: RegExp, parser?: IKoaRouterTypeParser }>
}
export class KoaRouter {
#option: IKoaRouterOptions
#types: Record<string, { regexpString: string, parser?: IKoaRouterTypeParser }> = {};
#routes: IKoaRouteConfig[] = [];
constructor(option?: IKoaRouterOptions) {
this.#option = {
...option,
prefix: option?.prefix?.replace(/\/+$/, "") ?? "/",
};
//参数类型安装
if (typeof option?.defaultParamTypes === "boolean") {
if (option.defaultParamTypes) this.installDefaultParamTypes();
}
else if (Array.isArray(option?.defaultParamTypes)) this.installDefaultParamTypes(option.defaultParamTypes);
//自定义类型安装
if (option?.customParamtypes) {
for (const key in option.customParamtypes) {
this.installParamType(key, option.customParamtypes[key].regexp, option.customParamtypes[key].parser);
}
}
}
/**
* 安装类型,类型安装后,可以在路由路径中使用类型参数,例如:
*
* "/users/{id:int}"
*
* "/users/{name:string}"
*
* @param typename 类型名称
* @param regexp 类型正则表达式(不能包含开始符号^和结束符号$
* @param parser 匹配结果转换函数
*/
public installParamType(typename: string, regexp: RegExp, parser?: IKoaRouterTypeParser) {
this.#types[typename] = { regexpString: regexp.toString().slice(1, -1), parser };
}
/**
* 安装默认类型
* @param types 要安装的类型,如果没有传递则安装所有类型
*/
public installDefaultParamTypes(types?: KoaRouterDefaultTypes[]) {
const typeMap: Record<KoaRouterDefaultTypes, { regexp: RegExp, parser?: IKoaRouterTypeParser }> = {
"int": { regexp: /\d+/, parser: parseInt },
"boolean": { regexp: /(true|false|yes|no|on|off)/, parser: (v: string) => v.toLowerCase() == "true" },
};
for (const type of types ?? Object.keys(typeMap)) {
if (typeMap[type as KoaRouterDefaultTypes]) this.installParamType(type, typeMap[type as KoaRouterDefaultTypes].regexp, typeMap[type as KoaRouterDefaultTypes].parser);
}
}
/**
* 注册路由
* @param method 路由方法
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public register(method: KoaRouterMethods[], pathname: string, middleware: Middleware): void {
//处理路径参数
const prefix = this.#option.prefix?.replace(/\/+$/, "") ?? "";
pathname = prefix + (pathname[0] == "/" ? pathname : `/${pathname}`);
//匹配参数
// 支持的参数格式有:
// {name}
// {name:type}
const res = pathname.matchAll(/\{([a-zA-Z][a-zA-Z0-9_]*)(:([a-zA-Z][a-zA-Z0-9_]*))?\}/g);
const parsers: IKoaRouteConfig["parsers"] = {};
//处理参数,并生成正则表达式
const regexpItems: string[] = [];
let offset = 0;
for (const match of res) {
const name = match[1];
const type = match[3];
//放入路由前部分
regexpItems.push(pathname.slice(offset, match.index));
//放入路由参数部分
if (type) {
if (!this.#types[type]) throw new RouterError("ERR_ROUTER_PARAM_TYPE_NOT_INSTALL", `路由参数类型"${type}"未安装`);
regexpItems.push(`(?<${name}>${this.#types[type].regexpString})`)
if (this.#types[type].parser) parsers[name] = this.#types[type].parser;
}
else regexpItems.push(`(?<${name}>[^/]+)`);
//剩余内容
offset = match.index + match[0].length;
}
//得到正则表达式
const regexpStr = "^" + regexpItems.join("") + pathname.slice(offset) + "$";
const regexp = new RegExp(regexpStr);
//注册路由
for (const m of method) this.#routes.push({ method: m, pathname, regexp, middleware, parsers });
}
/**
* 注册GET路由
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public get(pathname: string, middleware: Middleware) { this.register(["get"], pathname, middleware); }
/**
* 注册POST路由
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public post(pathname: string, middleware: Middleware) { this.register(["post"], pathname, middleware); }
/**
* 注册PUT路由
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public put(pathname: string, middleware: Middleware) { this.register(["put"], pathname, middleware); }
/**
* 注册DELETE路由
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public delete(pathname: string, middleware: Middleware) { this.register(["delete"], pathname, middleware); }
/**
* 注册PATCH路由
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public patch(pathname: string, middleware: Middleware) { this.register(["patch"], pathname, middleware); }
/**
* 注册所有路由方法
* @param pathname 路由路径
* @param middleware 路由中间件
*/
public all(pathname: string, middleware: Middleware) { this.register(["get", "post", "put", "delete", "patch"], pathname, middleware); }
/** 路由匹配中间件 */
public callback(): Middleware {
return (ctx: Koa.Context, next: Koa.Next) => {
//匹配路由
const method = ctx.method.toLocaleLowerCase() as KoaRouterMethods;
let match: RegExpMatchArray | null = null as any;
const route = this.#routes.find(route => {
if (route.method != method) return false;
match = ctx.URL.pathname.match(route.regexp);
return match != null;
})
if (!route) return next();
//处理参数
const params = match?.groups ?? {};
for (const key in route.parsers) {
params[key] = route.parsers[key](params[key]);
}
ctx.params = params;
//执行中间件
return route.middleware(ctx, next);
}
}
get #prefix() {
return this.#option.prefix!;
}
}