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>
This commit is contained in:
2025-12-06 12:30:47 +08:00
parent ef0faf84b1
commit c692061b23
6 changed files with 99 additions and 228 deletions

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package", "$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router", "name": "@kevisual/router",
"version": "0.0.34", "version": "0.0.36",
"description": "", "description": "",
"type": "module", "type": "module",
"main": "./dist/router.js", "main": "./dist/router.js",

View File

@@ -16,16 +16,18 @@ type AppOptions<T = {}> = {
io?: boolean; io?: boolean;
ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string }; ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string };
}; };
export type AppReqRes = HandleCtx;
export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T> };
/** /**
* 封装了 Router 和 Server 的 App 模块处理http的请求和响应内置了 Cookie 和 Token 和 res 的处理 * 封装了 Router 和 Server 的 App 模块处理http的请求和响应内置了 Cookie 和 Token 和 res 的处理
* U - Route Context的扩展类型
*/ */
export class App<T = {}, U = AppReqRes> { export class App<U = {}> {
router: QueryRouter; router: QueryRouter;
server: Server; server: Server;
io: WsServer; io: WsServer;
constructor(opts?: AppOptions<T>) { constructor(opts?: AppOptions<U>) {
const router = opts?.router || new QueryRouter(); const router = opts?.router || new QueryRouter();
const server = opts?.server || new Server(opts?.serverOptions || {}); const server = opts?.server || new Server(opts?.serverOptions || {});
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext)); server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
@@ -62,10 +64,10 @@ export class App<T = {}, U = AppReqRes> {
add = this.addRoute; add = this.addRoute;
Route = Route; Route = Route;
route(opts: RouteOpts): Route<U>; route(opts: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
route(path: string, key?: string): Route<U>; route(path: string, key?: string): Route<AppRouteContext<U>>;
route(path: string, opts?: RouteOpts): Route<U>; route(path: string, opts?: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
route(path: string, key?: string, opts?: RouteOpts): Route<U>; route(path: string, key?: string, opts?: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
route(...args: any[]) { route(...args: any[]) {
const [path, key, opts] = args; const [path, key, opts] = args;
if (typeof path === 'object') { if (typeof path === 'object') {
@@ -82,8 +84,8 @@ export class App<T = {}, U = AppReqRes> {
} }
return new Route(path, key, opts); return new Route(path, key, opts);
} }
prompt(description: string): Route<Required<RouteContext>>; prompt(description: string): Route<AppRouteContext<U>>
prompt(description: Function): Route<Required<RouteContext>>; prompt(description: Function): Route<AppRouteContext<U>>
prompt(...args: any[]) { prompt(...args: any[]) {
const [desc] = args; const [desc] = args;
let description = '' let description = ''
@@ -95,13 +97,19 @@ export class App<T = {}, U = AppReqRes> {
return new Route('', '', { description }); return new Route('', '', { description });
} }
async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: AppRouteContext<U> & { [key: string]: any }) {
const router = this.router; const router = this.router;
return await router.call(message, ctx); return await router.call(message, ctx);
} }
async queryRoute(path: string, key?: string, payload?: any, ctx?: RouteContext & { [key: string]: any }) { /**
* @deprecated
*/
async queryRoute(path: string, key?: string, payload?: any, ctx?: AppRouteContext<U> & { [key: string]: any }) {
return await this.router.queryRoute({ path, key, payload }, ctx); return await this.router.queryRoute({ path, key, payload }, ctx);
} }
async run(path: string, key?: string, payload?: any, ctx?: AppRouteContext<U> & { [key: string]: any }) {
return await this.router.run({ path, key, payload }, ctx);
}
exportRoutes() { exportRoutes() {
return this.router.exportRoutes(); return this.router.exportRoutes();
} }

View File

@@ -1,8 +1,6 @@
import { nanoid, random } from 'nanoid'; import { nanoid } from 'nanoid';
import { CustomError } from './result/error.ts'; import { CustomError } from './result/error.ts';
import { Schema, Rule, createSchema } from './validator/index.ts';
import { pick } from './utils/pick.ts'; import { pick } from './utils/pick.ts';
import { get } from 'lodash-es';
import { listenProcess } from './utils/listen-process.ts'; import { listenProcess } from './utils/listen-process.ts';
export type RouterContextT = { code?: number;[key: string]: any }; export type RouterContextT = { code?: number;[key: string]: any };
@@ -12,6 +10,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
// response body // response body
/** return body */ /** return body */
body?: number | string | Object; body?: number | string | Object;
forward?: (response: { code: number, data?: any, message?: any }) => void;
/** return code */ /** return code */
code?: number; code?: number;
/** return msg */ /** return msg */
@@ -39,20 +38,15 @@ export type RouteContext<T = { code?: number }, S = any> = {
nextQuery?: { [key: string]: any }; nextQuery?: { [key: string]: any };
// end // end
end?: boolean; end?: boolean;
// 处理router manager app?: QueryRouter;
// TODO:
/**
* 请求 route的返回结果包函ctx
*/
queryRouter?: QueryRouter;
error?: any; error?: any;
/** 请求 route的返回结果包函ctx */ /** 请求 route的返回结果不解析body为data */
call?: ( call?: (
message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any }, message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any },
ctx?: RouteContext & { [key: string]: any }, ctx?: RouteContext & { [key: string]: any },
) => Promise<any>; ) => Promise<any>;
/** 请求 route的返回结果不包函ctx */ /** 请求 route的返回结果解析了body为data就类同于 query.post获取的数据*/
queryRoute?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>; run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
index?: number; index?: number;
throw?: (code?: number | string, message?: string, tips?: string) => void; throw?: (code?: number | string, message?: string, tips?: string) => void;
/** 是否需要序列化, 使用JSON.stringify和JSON.parse */ /** 是否需要序列化, 使用JSON.stringify和JSON.parse */
@@ -69,29 +63,16 @@ export type RouteMiddleware =
id?: string; id?: string;
} }
| string; | string;
export type RouteOpts = { export type RouteOpts<T = {}> = {
path?: string; path?: string;
key?: string; key?: string;
id?: string; id?: string;
run?: Run; run?: Run<T>;
nextRoute?: NextRoute; // route to run after this route nextRoute?: NextRoute; // route to run after this route
description?: string; description?: string;
metadata?: { [key: string]: any }; metadata?: { [key: string]: any };
middleware?: RouteMiddleware[]; // middleware middleware?: RouteMiddleware[]; // middleware
type?: 'route' | 'middleware'; type?: 'route' | 'middleware';
/**
* validator: {
* packageName: {
* type: 'string',
* required: true,
* },
* }
*/
validator?: { [key: string]: Rule };
schema?: { [key: string]: any };
isVerify?: boolean;
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
/** /**
* $#$ will be used to split path and key * $#$ will be used to split path and key
*/ */
@@ -102,8 +83,8 @@ export type RouteOpts = {
delimiter?: string; delimiter?: string;
isDebug?: boolean; isDebug?: boolean;
}; };
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>; export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware', 'metadata'] as const; const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const;
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>; export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
export class Route<U = { [key: string]: any }> { export class Route<U = { [key: string]: any }> {
/** /**
@@ -121,13 +102,7 @@ export class Route<U = { [key: string]: any }> {
metadata?: { [key: string]: any }; metadata?: { [key: string]: any };
middleware?: RouteMiddleware[]; // middleware middleware?: RouteMiddleware[]; // middleware
type? = 'route'; type? = 'route';
private _validator?: { [key: string]: Rule };
schema?: { [key: string]: any };
data?: any; data?: any;
/**
* 是否需要验证
*/
isVerify?: boolean;
/** /**
* 是否开启debug开启后会打印错误信息 * 是否开启debug开启后会打印错误信息
*/ */
@@ -151,118 +126,16 @@ export class Route<U = { [key: string]: any }> {
this.description = opts.description; this.description = opts.description;
this.metadata = opts.metadata; this.metadata = opts.metadata;
this.type = opts.type || 'route'; this.type = opts.type || 'route';
this.validator = opts.validator;
this.middleware = opts.middleware || []; this.middleware = opts.middleware || [];
this.key = opts.key || key; this.key = opts.key || key;
this.path = opts.path || path; this.path = opts.path || path;
this.isVerify = opts.isVerify ?? true;
this.createSchema();
} else { } else {
this.isVerify = true;
this.middleware = []; this.middleware = [];
this.id = nanoid(); this.id = nanoid();
} }
this.isDebug = opts?.isDebug ?? false; this.isDebug = opts?.isDebug ?? false;
} }
private createSchema() {
try {
const validator = this.validator;
const keys = Object.keys(validator || {});
const schemaList = keys.map((key) => {
return { [key]: createSchema(validator[key]) };
});
const schema = schemaList.reduce((prev, current) => {
return { ...prev, ...current };
}, {});
this.schema = schema;
} catch (e) {
console.error('createSchema error:', e);
}
}
/**
* set validator and create schema
* @param validator
*/
set validator(validator: { [key: string]: Rule }) {
this._validator = validator;
this.createSchema();
}
get validator() {
return this._validator || {};
}
/**
* has code, body, message in ctx, return ctx if has error
* @param ctx
* @param dev
* @returns
*/
verify(ctx: RouteContext, dev = false) {
const query = ctx.query || {};
const schema = this.schema || {};
const validator = this.validator;
const check = () => {
const queryKeys = Object.keys(validator);
for (let i = 0; i < queryKeys.length; i++) {
const key = queryKeys[i];
const value = query[key];
if (schema[key]) {
const result = schema[key].safeParse(value);
if (!result.success) {
const path = result.error.errors[0]?.path?.join?.('.properties.');
let message = 'Invalid params';
if (path) {
const keyS = `${key}.properties.${path}.message`;
message = get(validator, keyS, 'Invalid params') as any;
}
throw new CustomError(500, message);
}
}
}
};
check();
}
/**
* Need to manully call return ctx fn and configure body, code, message
* @param key
* @param ctx
* @param dev
* @returns
*/
verifyKey(key: string, ctx: RouteContext, dev = false) {
const query = ctx.query || {};
const schema = this.schema || {};
const validator = this.validator;
const check = () => {
const value = query[key];
if (schema[key]) {
try {
schema[key].parse(value);
} catch (e) {
if (dev) {
return {
message: validator[key].message || 'Invalid params',
path: this.path,
key: this.key,
error: e.message.toString(),
};
}
return {
message: validator[key].message || 'Invalid params',
path: this.path,
key: this.key,
};
}
}
};
const checkRes = check();
return checkRes;
}
setValidator(validator: { [key: string]: Rule }) {
this.validator = validator;
return this;
}
prompt(description: string): this; prompt(description: string): this;
prompt(description: Function): this; prompt(description: Function): this;
prompt(...args: any[]) { prompt(...args: any[]) {
@@ -283,15 +156,11 @@ export class Route<U = { [key: string]: any }> {
// 全覆盖所以opts需要准确不能由idUsePath 需要check的变量 // 全覆盖所以opts需要准确不能由idUsePath 需要check的变量
const setOpts = (opts: DefineRouteOpts) => { const setOpts = (opts: DefineRouteOpts) => {
const keys = Object.keys(opts); const keys = Object.keys(opts);
const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug']; const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
for (let item of keys) { for (let item of keys) {
if (!checkList.includes(item)) { if (!checkList.includes(item)) {
continue; continue;
} }
if (item === 'validator') {
this.validator = opts[item];
continue;
}
if (item === 'middleware') { if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]); this.middleware = this.middleware.concat(opts[item]);
continue; continue;
@@ -320,16 +189,12 @@ export class Route<U = { [key: string]: any }> {
update(opts: DefineRouteOpts, checkList?: string[]): this { update(opts: DefineRouteOpts, checkList?: string[]): this {
const keys = Object.keys(opts); const keys = Object.keys(opts);
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug']; const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
checkList = checkList || defaultCheckList; checkList = checkList || defaultCheckList;
for (let item of keys) { for (let item of keys) {
if (!checkList.includes(item)) { if (!checkList.includes(item)) {
continue; continue;
} }
if (item === 'validator') {
this.validator = opts[item];
continue;
}
if (item === 'middleware') { if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]); this.middleware = this.middleware.concat(opts[item]);
continue; continue;
@@ -360,10 +225,10 @@ export class QueryRouter {
} }
add(route: Route) { add(route: Route) {
const has = this.routes.find((r) => r.path === route.path && r.key === route.key); const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key);
if (has) { if (has !== -1) {
// remove the old route // remove the old route
this.routes = this.routes.filter((r) => r.id !== has.id); this.routes.splice(has, 1);
} }
this.routes.push(route); this.routes.push(route);
} }
@@ -460,19 +325,6 @@ export class QueryRouter {
for (let i = 0; i < routeMiddleware.length; i++) { for (let i = 0; i < routeMiddleware.length; i++) {
const middleware = routeMiddleware[i]; const middleware = routeMiddleware[i];
if (middleware) { if (middleware) {
if (middleware?.isVerify) {
try {
middleware.verify(ctx);
} catch (e) {
if (middleware?.isDebug) {
console.error('=====debug====:', 'middleware verify error:', e.message);
}
ctx.message = e.message;
ctx.code = 500;
ctx.body = null;
return ctx;
}
}
try { try {
await middleware.run(ctx); await middleware.run(ctx);
} catch (e) { } catch (e) {
@@ -503,19 +355,6 @@ export class QueryRouter {
// run route // run route
if (route) { if (route) {
if (route.run) { if (route.run) {
if (route?.isVerify) {
try {
route.verify(ctx);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:', 'verify error:', e.message);
}
ctx.message = e.message;
ctx.code = 500;
ctx.body = null;
return ctx;
}
}
try { try {
await route.run(ctx); await route.run(ctx);
} catch (e) { } catch (e) {
@@ -588,9 +427,20 @@ export class QueryRouter {
ctx.throw = this.throw; ctx.throw = this.throw;
ctx.app = this; ctx.app = this;
ctx.call = this.call.bind(this); ctx.call = this.call.bind(this);
ctx.queryRoute = this.queryRoute.bind(this); ctx.run = this.run.bind(this);
ctx.index = 0; ctx.index = 0;
ctx.progress = ctx.progress || []; 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 res = await this.runRoute(path, key, ctx);
const serialize = ctx.needSerialize ?? true; // 是否需要序列化 const serialize = ctx.needSerialize ?? true; // 是否需要序列化
if (serialize) { if (serialize) {
@@ -628,6 +478,7 @@ export class QueryRouter {
* 请求 result 的数据 * 请求 result 的数据
* @param message * @param message
* @param ctx * @param ctx
* @deprecated use run or call instead
* @returns * @returns
*/ */
async queryRoute(message: { id?: string; path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { async queryRoute(message: { id?: string; path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
@@ -638,6 +489,20 @@ export class QueryRouter {
message: res.message, 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函数中使用 * @description 这里的上下文是为了在handle函数中使用
@@ -658,17 +523,7 @@ export class QueryRouter {
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => { return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
try { try {
const context = { ...ctx, ...handleContext }; const context = { ...ctx, ...handleContext };
if (msg.id) { const res = await router.call(msg, context);
const route = router.routes.find((r) => r.id === msg.id);
if (route) {
msg.path = route.path;
msg.key = route.key;
} else {
return { code: 404, message: 'Not found route' };
}
}
// @ts-ignore
const res = await router.parse(msg, context);
if (wrapperFn) { if (wrapperFn) {
res.data = res.body; res.data = res.body;
return wrapperFn(res, context); return wrapperFn(res, context);
@@ -796,34 +651,19 @@ export class QueryRouterServer extends QueryRouter {
} }
/** /**
* 等于queryRoute但是调用了handle * 调用了handle
* @param param0 * @param param0
* @returns * @returns
*/ */
async run({ path, key, payload }: { path: string; key?: string; payload?: any }) { async run({ path, key, payload }: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
const handle = this.handle; const handle = this.handle;
const resultError = (error: string, code = 500) => { if (handle) {
const r = { const result = await this.call({ path, key, payload }, ctx);
code: code, return handle(result);
message: error,
};
return r;
};
try {
const end = handle({ path, key, ...payload });
return end;
} catch (e) {
if (e.code && typeof e.code === 'number') {
return {
code: e.code,
message: e.message,
};
} else {
return resultError('Router Server error');
}
} }
return super.run({ path, key, payload }, ctx);
} }
} }
export const Mini = QueryRouterServer export class Mini extends QueryRouterServer { }

View File

@@ -95,7 +95,7 @@ class QueryChain {
* @param queryData * @param queryData
* @returns * @returns
*/ */
getKey(queryData?: SimpleObject): Pick<RouteOpts, 'path' | 'key' | 'metadata' | 'description' | 'validator'> { getKey(queryData?: SimpleObject): Pick<RouteOpts, 'path' | 'key' | 'metadata' | 'description'> {
const obj = this.omit(this.obj, this.omitKeys); const obj = this.omit(this.obj, this.omitKeys);
return { return {
...obj, ...obj,

View File

@@ -222,7 +222,17 @@ export class Server {
} else { } else {
this._server.on('request', listener); this._server.on('request', listener);
} }
this._server.on('request', this._callback || this.createCallback()); const callbackListener = this._callback || this.createCallback();
this._server.on('request', callbackListener);
return () => {
if (Array.isArray(listener)) {
listener.forEach((l) => this._server.removeListener('request', l as Listener));
} else {
this._server.removeListener('request', listener as Listener);
}
this.hasOn = false;
this._server.removeListener('request', callbackListener);
}
} }
get callback() { get callback() {
return this._callback || this.createCallback(); return this._callback || this.createCallback();

13
src/test/app-type.ts Normal file
View File

@@ -0,0 +1,13 @@
import { App } from '../app.ts'
const app = new App<{ f: string }>();
app.route({
path: 't',
run: async (ctx) => {
// ctx.r
ctx.app;
}
}).define(async (ctx) => {
ctx.f = 'hello';
}).addTo(app);