初次提交
This commit is contained in:
12
src/error.ts
Normal file
12
src/error.ts
Normal 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
2
src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./error";
|
||||
export * from "./router";
|
211
src/router.ts
Normal file
211
src/router.ts
Normal 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!;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user