Files
router/src/route.ts
abearxiong c692061b23 refactor router system and update version
- Simplified Route class by removing validator functionality
- Added AppRouteContext type for better type safety
- Added forward method to RouteContext for response handling
- Replaced queryRoute with run method for consistency
- Improved Server class with proper cleanup methods
- Updated Mini class to extend QueryRouterServer properly
- Removed lodash-es dependency and nanoid random import
- Added deprecation warnings for older methods
- Enhanced route handling and middleware execution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 12:30:47 +08:00

669 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { nanoid } from 'nanoid';
import { CustomError } from './result/error.ts';
import { pick } from './utils/pick.ts';
import { listenProcess } from './utils/listen-process.ts';
export type RouterContextT = { code?: number;[key: string]: any };
export type RouteContext<T = { code?: number }, S = any> = {
// run first
query?: { [key: string]: any };
// response body
/** return body */
body?: number | string | Object;
forward?: (response: { code: number, data?: any, message?: any }) => void;
/** return code */
code?: number;
/** return msg */
message?: string;
// 传递状态
state?: S;
// transfer data
/**
* 当前路径
*/
currentPath?: string;
/**
* 当前key
*/
currentKey?: string;
/**
* 当前route
*/
currentRoute?: Route;
/**
* 进度
*/
progress?: [string, string][];
// onlyForNextRoute will be clear after next route
nextQuery?: { [key: string]: any };
// end
end?: boolean;
app?: QueryRouter;
error?: any;
/** 请求 route的返回结果不解析body为data */
call?: (
message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any },
ctx?: RouteContext & { [key: string]: any },
) => Promise<any>;
/** 请求 route的返回结果解析了body为data就类同于 query.post获取的数据*/
run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
index?: number;
throw?: (code?: number | string, message?: string, tips?: string) => void;
/** 是否需要序列化, 使用JSON.stringify和JSON.parse */
needSerialize?: boolean;
} & T;
export type SimpleObject = Record<string, any>;
export type Run<T extends SimpleObject = {}> = (ctx: RouteContext<T>) => Promise<typeof ctx | null | void>;
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
export type RouteMiddleware =
| {
path: string;
key?: string;
id?: string;
}
| string;
export type RouteOpts<T = {}> = {
path?: string;
key?: string;
id?: string;
run?: Run<T>;
nextRoute?: NextRoute; // route to run after this route
description?: string;
metadata?: { [key: string]: any };
middleware?: RouteMiddleware[]; // middleware
type?: 'route' | 'middleware';
/**
* $#$ will be used to split path and key
*/
idUsePath?: boolean;
/**
* id 合并的分隔符,默认为 $#$
*/
delimiter?: string;
isDebug?: boolean;
};
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const;
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
export class Route<U = { [key: string]: any }> {
/**
* 一级路径
*/
path?: string;
/**
* 二级路径
*/
key?: string;
id?: string;
run?: Run;
nextRoute?: NextRoute; // route to run after this route
description?: string;
metadata?: { [key: string]: any };
middleware?: RouteMiddleware[]; // middleware
type? = 'route';
data?: any;
/**
* 是否开启debug开启后会打印错误信息
*/
isDebug?: boolean;
constructor(path: string = '', key: string = '', opts?: RouteOpts) {
if (!path) {
path = nanoid(8)
}
path = path.trim();
key = key.trim();
this.path = path;
this.key = key;
if (opts) {
this.id = opts.id || nanoid();
if (!opts.id && opts.idUsePath) {
const delimiter = opts.delimiter ?? '$#$';
this.id = path + delimiter + key;
}
this.run = opts.run;
this.nextRoute = opts.nextRoute;
this.description = opts.description;
this.metadata = opts.metadata;
this.type = opts.type || 'route';
this.middleware = opts.middleware || [];
this.key = opts.key || key;
this.path = opts.path || path;
} else {
this.middleware = [];
this.id = nanoid();
}
this.isDebug = opts?.isDebug ?? false;
}
prompt(description: string): this;
prompt(description: Function): this;
prompt(...args: any[]) {
const [description] = args;
if (typeof description === 'string') {
this.description = description;
} else if (typeof description === 'function') {
this.description = description() || ''; // 如果是Promise需要addTo App之前就要获取应有的函数了。
}
return this;
}
define<T extends { [key: string]: any } = RouterContextT>(opts: DefineRouteOpts): this;
define<T extends { [key: string]: any } = RouterContextT>(fn: Run<T & U>): this;
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T & U>): this;
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T & U>): this;
define(...args: any[]) {
const [path, key, opts] = args;
// 全覆盖所以opts需要准确不能由idUsePath 需要check的变量
const setOpts = (opts: DefineRouteOpts) => {
const keys = Object.keys(opts);
const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
for (let item of keys) {
if (!checkList.includes(item)) {
continue;
}
if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]);
continue;
}
this[item] = opts[item];
}
};
if (typeof path === 'object') {
setOpts(path);
return this;
}
if (typeof path === 'function') {
this.run = path;
return this;
}
if (typeof path === 'string' && typeof key === 'function') {
setOpts({ path, run: key });
return this;
}
if (typeof path === 'string' && typeof key === 'string' && typeof opts === 'function') {
setOpts({ path, key, run: opts });
return this;
}
return this;
}
update(opts: DefineRouteOpts, checkList?: string[]): this {
const keys = Object.keys(opts);
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
checkList = checkList || defaultCheckList;
for (let item of keys) {
if (!checkList.includes(item)) {
continue;
}
if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]);
continue;
}
this[item] = opts[item];
}
return this;
}
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }) {
router.add(this);
}
setData(data: any) {
this.data = data;
return this;
}
throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) {
throw new CustomError(...args);
}
}
export class QueryRouter {
routes: Route[];
maxNextRoute = 40;
context?: RouteContext = {}; // default context for call
constructor() {
this.routes = [];
}
add(route: Route) {
const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key);
if (has !== -1) {
// remove the old route
this.routes.splice(has, 1);
}
this.routes.push(route);
}
/**
* remove route by path and key
* @param route
*/
remove(route: Route | { path: string; key?: string }) {
this.routes = this.routes.filter((r) => r.path === route.path && r.key == route.key);
}
/**
* remove route by id
* @param uniqueId
*/
removeById(unique: string) {
this.routes = this.routes.filter((r) => r.id !== unique);
}
/**
* 执行route
* @param path
* @param key
* @param ctx
* @returns
*/
async runRoute(path: string, key: string, ctx?: RouteContext) {
const route = this.routes.find((r) => r.path === path && r.key === key);
const maxNextRoute = this.maxNextRoute;
ctx = (ctx || {}) as RouteContext;
ctx.currentPath = path;
ctx.currentKey = key;
ctx.currentRoute = route;
ctx.index = (ctx.index || 0) + 1;
const progress = [path, key] as [string, string];
if (ctx.progress) {
ctx.progress.push(progress);
} else {
ctx.progress = [progress];
}
if (ctx.index > maxNextRoute) {
ctx.code = 500;
ctx.message = 'Too many nextRoute';
ctx.body = null;
return;
}
// run middleware
if (route && route.middleware && route.middleware.length > 0) {
const errorMiddleware: { path?: string; key?: string; id?: string }[] = [];
const getMiddleware = (m: Route) => {
if (!m.middleware || m.middleware.length === 0) return [];
const routeMiddleware: Route[] = [];
for (let i = 0; i < m.middleware.length; i++) {
const item = m.middleware[i];
let route: Route | undefined;
const isString = typeof item === 'string';
if (isString) {
route = this.routes.find((r) => r.id === item);
} else {
route = this.routes.find((r) => {
if (item.id) {
return r.id === item.id;
} else {
// key 可以是空,所以可以不严格验证
return r.path === item.path && r.key == item.key;
}
});
}
if (!route) {
if (isString) {
errorMiddleware.push({
id: item as string,
});
} else
errorMiddleware.push({
path: m?.path,
key: m?.key,
});
}
const routeMiddlewarePrevious = getMiddleware(route);
if (routeMiddlewarePrevious.length > 0) {
routeMiddleware.push(...routeMiddlewarePrevious);
}
routeMiddleware.push(route);
}
return routeMiddleware;
};
const routeMiddleware = getMiddleware(route);
if (errorMiddleware.length > 0) {
console.error('middleware not found');
ctx.body = errorMiddleware;
ctx.message = 'middleware not found';
ctx.code = 404;
return ctx;
}
for (let i = 0; i < routeMiddleware.length; i++) {
const middleware = routeMiddleware[i];
if (middleware) {
try {
await middleware.run(ctx);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:middlerware error');
console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
console.error('=====debug====:', e.message);
}
if (e instanceof CustomError || e?.code) {
ctx.code = e.code;
ctx.message = e.message;
ctx.body = null;
} else {
console.error(`fn:${route.path}-${route.key}:${route.id}`);
console.error(`middleware:${middleware.path}-${middleware.key}:${middleware.id}`);
ctx.code = 500;
ctx.message = 'Internal Server Error';
ctx.body = null;
}
return ctx;
}
if (ctx.end) {
}
}
}
}
// run route
if (route) {
if (route.run) {
try {
await route.run(ctx);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:', 'router run error:', e.message);
}
if (e instanceof CustomError) {
ctx.code = e.code;
ctx.message = e.message;
} else {
console.error(`[error]fn:${route.path}-${route.key}:${route.id}`);
console.error('error', e.message);
ctx.code = 500;
ctx.message = 'Internal Server Error';
}
ctx.body = null;
return ctx;
}
if (ctx.end) {
// TODO: 提前结束, 以及错误情况
return;
}
if (route.nextRoute) {
let path: string, key: string;
if (route.nextRoute.path || route.nextRoute.key) {
path = route.nextRoute.path;
key = route.nextRoute.key;
} else if (route.nextRoute.id) {
const nextRoute = this.routes.find((r) => r.id === route.nextRoute.id);
if (nextRoute) {
path = nextRoute.path;
key = nextRoute.key;
}
}
if (!path || !key) {
ctx.message = 'nextRoute not found';
ctx.code = 404;
ctx.body = null;
return ctx;
}
ctx.query = { ...ctx.query, ...ctx.nextQuery };
ctx.nextQuery = {};
return await this.runRoute(path, key, ctx);
}
if (!ctx.code) ctx.code = 200;
return ctx;
} else {
// return Promise.resolve({ code: 404, body: 'Not found runing' });
// 可以不需要run的route因为不一定是错误
return ctx;
}
}
// 如果没有找到route返回404这是因为出现了错误
return Promise.resolve({ code: 404, body: 'Not found' });
}
/**
* 第一次执行
* @param message
* @param ctx
* @returns
*/
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
if (!message?.path) {
return Promise.resolve({ code: 404, body: null, message: 'Not found path' });
}
const { path, key = '', payload = {}, ...query } = message;
ctx = ctx || {};
ctx.query = { ...ctx.query, ...query, ...payload };
ctx.state = { ...ctx?.state };
ctx.throw = this.throw;
ctx.app = this;
ctx.call = this.call.bind(this);
ctx.run = this.run.bind(this);
ctx.index = 0;
ctx.progress = ctx.progress || [];
ctx.forward = (response: { code: number; data?: any; message?: any }) => {
if (response.code) {
ctx.code = response.code;
}
if (response.data !== undefined) {
ctx.body = response.data;
}
if (response.message !== undefined) {
ctx.message = response.message;
}
}
const res = await this.runRoute(path, key, ctx);
const serialize = ctx.needSerialize ?? true; // 是否需要序列化
if (serialize) {
res.body = JSON.parse(JSON.stringify(res.body || ''));
}
return res;
}
/**
* 返回的数据包含所有的context的请求返回的内容可做其他处理
* @param message
* @param ctx
* @returns
*/
async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
let path = message.path;
let key = message.key;
// 优先 path + key
if (path) {
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else if (message.id) {
const route = this.routes.find((r) => r.id === message.id);
if (route) {
path = route.path;
key = route.key;
} else {
return { code: 404, body: null, message: 'Not found route' };
}
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else {
return { code: 404, body: null, message: 'Not found path' };
}
}
/**
* 请求 result 的数据
* @param message
* @param ctx
* @deprecated use run or call instead
* @returns
*/
async queryRoute(message: { id?: string; path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
const res = await this.call(message, { ...this.context, ...ctx });
return {
code: res.code,
data: res.body,
message: res.message,
};
}
/**
* Router Run获取数据
* @param message
* @param ctx
* @returns
*/
async run(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
const res = await this.call(message, { ...this.context, ...ctx });
return {
code: res.code,
data: res.body,
message: res.message,
};
}
/**
* 设置上下文
* @description 这里的上下文是为了在handle函数中使用
* @param ctx
*/
setContext(ctx: RouteContext) {
this.context = ctx;
}
getList(): RouteInfo[] {
return this.routes.map((r) => {
return pick(r, pickValue as any);
});
}
/**
* 获取handle函数, 这里会去执行parse函数
*/
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
try {
const context = { ...ctx, ...handleContext };
const res = await router.call(msg, context);
if (wrapperFn) {
res.data = res.body;
return wrapperFn(res, context);
}
const { code, body, message } = res;
return { code, data: body, message };
} catch (e) {
return { code: 500, message: e.message };
}
};
}
exportRoutes() {
return this.routes.map((r) => {
return r;
});
}
importRoutes(routes: Route[]) {
for (let route of routes) {
this.add(route);
}
}
importRouter(router: QueryRouter) {
this.importRoutes(router.routes);
}
throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) {
throw new CustomError(...args);
}
hasRoute(path: string, key: string = '') {
return this.routes.find((r) => r.path === path && r.key === key);
}
createRouteList(force: boolean = false) {
const hasListRoute = this.hasRoute('route', 'list');
if (!hasListRoute || force) {
const listRoute = new Route('route', 'list', {
description: '列出当前应用下的所有的路由信息',
run: async (ctx: RouteContext) => {
const list = this.getList();
ctx.body = list;
},
});
this.add(listRoute);
}
}
/**
* 等待程序运行, 获取到message的数据,就执行
*
* emitter = process
* -- .exit
* -- .on
* -- .send
*/
wait(params?: { path?: string; key?: string; payload?: any }, opts?: { emitter?: any, timeout?: number, getList?: boolean }) {
const getList = opts?.getList ?? true;
if (getList) {
this.createRouteList();
}
return listenProcess({ app: this, params, ...opts });
}
}
type QueryRouterServerOpts = {
handleFn?: HandleFn;
context?: RouteContext;
};
interface HandleFn<T = any> {
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
(res: RouteContext<T>): any;
}
/**
* QueryRouterServer
* @description 移除server相关的功能只保留router相关的功能和http.createServer不相关独立
*/
export class QueryRouterServer extends QueryRouter {
handle: any;
constructor(opts?: QueryRouterServerOpts) {
super();
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
this.setContext({ needSerialize: false, ...opts?.context });
}
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
this.handle = this.getHandle(this, wrapperFn, ctx);
}
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
const route = new Route(path, '', opts);
route.run = fn;
this.add(route);
}
addRoute(route: Route) {
this.add(route);
}
Route = Route;
route(opts: RouteOpts): Route<Required<RouteContext>>;
route(path: string, key?: string): Route<Required<RouteContext>>;
route(path: string, opts?: RouteOpts): Route<Required<RouteContext>>;
route(path: string, key?: string, opts?: RouteOpts): Route<Required<RouteContext>>;
route(...args: any[]) {
const [path, key, opts] = args;
if (typeof path === 'object') {
return new Route(path.path, path.key, path);
}
if (typeof path === 'string') {
if (opts) {
return new Route(path, key, opts);
}
if (key && typeof key === 'object') {
return new Route(path, key?.key || '', key);
}
return new Route(path, key);
}
return new Route(path, key, opts);
}
prompt(description: string): Route<Required<RouteContext>>;
prompt(description: Function): Route<Required<RouteContext>>;
prompt(...args: any[]) {
const [desc] = args;
let description = ''
if (typeof desc === 'string') {
description = desc;
} else if (typeof desc === 'function') {
description = desc() || ''; // 如果是Promise需要addTo App之前就要获取应有的函数了。
}
return new Route('', '', { description });
}
/**
* 调用了handle
* @param param0
* @returns
*/
async run({ path, key, payload }: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
const handle = this.handle;
if (handle) {
const result = await this.call({ path, key, payload }, ctx);
return handle(result);
}
return super.run({ path, key, payload }, ctx);
}
}
export class Mini extends QueryRouterServer { }