init router
This commit is contained in:
84
src/app.ts
Normal file
84
src/app.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
||||
import { Server, Cors } from './server/server.ts';
|
||||
import { WsServer } from './server/ws-server.ts';
|
||||
type RouterHandle = (msg: { path: string; [key: string]: any }) => { code: string; data?: any; message?: string; [key: string]: any };
|
||||
type AppOptions<T = {}> = {
|
||||
router?: QueryRouter;
|
||||
server?: Server;
|
||||
/** handle msg 关联 */
|
||||
routerHandle?: RouterHandle;
|
||||
routerContext?: RouteContext<T>;
|
||||
serverOptions?: {
|
||||
path?: string;
|
||||
cors?: Cors;
|
||||
handle?: any;
|
||||
};
|
||||
io?: boolean;
|
||||
ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string };
|
||||
};
|
||||
export class App<T = {}> {
|
||||
router: QueryRouter;
|
||||
server: Server;
|
||||
io: WsServer;
|
||||
constructor(opts?: AppOptions<T>) {
|
||||
const router = opts?.router || new QueryRouter();
|
||||
const server = opts?.server || new Server(opts?.serverOptions || {});
|
||||
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
|
||||
|
||||
this.router = router;
|
||||
this.server = server;
|
||||
if (opts?.io) {
|
||||
this.io = new WsServer(server, opts?.ioOpts);
|
||||
}
|
||||
}
|
||||
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
||||
listen(port: number, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(port: number, listeningListener?: () => void): void;
|
||||
listen(path: string, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(path: string, listeningListener?: () => void): void;
|
||||
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(handle: any, listeningListener?: () => void): void;
|
||||
listen(...args: any[]) {
|
||||
// @ts-ignore
|
||||
this.server.listen(...args);
|
||||
if (this.io) {
|
||||
this.io.listen();
|
||||
}
|
||||
}
|
||||
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
|
||||
const route = new Route(path, '', opts);
|
||||
route.run = fn;
|
||||
this.router.add(route);
|
||||
}
|
||||
addRoute(route: Route) {
|
||||
this.router.add(route);
|
||||
}
|
||||
add = this.addRoute;
|
||||
|
||||
Route = Route;
|
||||
route(opts: RouteOpts): Route;
|
||||
route(path: string, key?: string): Route;
|
||||
route(path: string, opts?: RouteOpts): Route;
|
||||
route(path: string, key?: string, opts?: RouteOpts): Route;
|
||||
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);
|
||||
}
|
||||
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||
const router = this.router;
|
||||
return await router.parse(message, ctx);
|
||||
}
|
||||
}
|
||||
67
src/connect.ts
Normal file
67
src/connect.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { RouteContext } from './route.ts';
|
||||
|
||||
export class Connect {
|
||||
path: string;
|
||||
key?: string;
|
||||
_fn?: (ctx?: RouteContext) => Promise<RouteContext>;
|
||||
description?: string;
|
||||
connects: { path: string; key?: string }[];
|
||||
share = false;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
this.key = nanoid();
|
||||
}
|
||||
use(path: string) {
|
||||
this.connects.push({ path });
|
||||
}
|
||||
useList(paths: string[]) {
|
||||
paths.forEach((path) => {
|
||||
this.connects.push({ path });
|
||||
});
|
||||
}
|
||||
useConnect(connect: Connect) {
|
||||
this.connects.push({ path: connect.path, key: connect.key });
|
||||
}
|
||||
useConnectList(connects: Connect[]) {
|
||||
connects.forEach((connect) => {
|
||||
this.connects.push({ path: connect.path, key: connect.key });
|
||||
});
|
||||
}
|
||||
getPathList() {
|
||||
return this.connects.map((c) => c.path).filter(Boolean);
|
||||
}
|
||||
set fn(fn: (ctx?: RouteContext) => Promise<RouteContext>) {
|
||||
this._fn = fn;
|
||||
}
|
||||
get fn() {
|
||||
return this._fn;
|
||||
}
|
||||
}
|
||||
export class QueryConnect {
|
||||
connects: Connect[];
|
||||
constructor() {
|
||||
this.connects = [];
|
||||
}
|
||||
add(connect: Connect) {
|
||||
const has = this.connects.find((c) => c.path === connect.path && c.key === connect.key);
|
||||
if (has) {
|
||||
// remove the old connect
|
||||
console.log('[replace connect]:', connect.path, connect.key);
|
||||
this.connects = this.connects.filter((c) => c.path !== connect.path && c.key !== connect.key);
|
||||
}
|
||||
this.connects.push(connect);
|
||||
}
|
||||
remove(connect: Connect) {
|
||||
this.connects = this.connects.filter((c) => c.path !== connect.path && c.key !== connect.key);
|
||||
}
|
||||
getList() {
|
||||
return this.connects.map((c) => {
|
||||
return {
|
||||
path: c.path,
|
||||
key: c.key,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
21
src/index.ts
Normal file
21
src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export { Route, QueryRouter, QueryRouterServer } from './route.ts';
|
||||
export { Connect, QueryConnect } from './connect.ts';
|
||||
|
||||
export type { RouteContext, RouteOpts } from './route.ts';
|
||||
|
||||
export type { Run } from './route.ts';
|
||||
|
||||
export { Server, handleServer } from './server/index.ts';
|
||||
/**
|
||||
* 自定义错误
|
||||
*/
|
||||
export { CustomError } from './result/error.ts';
|
||||
|
||||
/**
|
||||
* 返回结果
|
||||
*/
|
||||
export { Result } from './result/index.ts';
|
||||
|
||||
export { Rule, Schema, createSchema } from './validator/index.ts';
|
||||
|
||||
export { App } from './app.ts';
|
||||
6
src/io.ts
Normal file
6
src/io.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// TODO: Implement IOApp
|
||||
export class IOApp {
|
||||
constructor() {
|
||||
console.log('IoApp');
|
||||
}
|
||||
}
|
||||
67
src/result/error.ts
Normal file
67
src/result/error.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/** 自定义错误 */
|
||||
export class CustomError extends Error {
|
||||
code?: number;
|
||||
data?: any;
|
||||
message: string;
|
||||
tips?: string;
|
||||
constructor(code?: number | string, message?: string, tips?: string) {
|
||||
super(message || String(code));
|
||||
this.name = 'CustomError';
|
||||
if (typeof code === 'number') {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
} else {
|
||||
this.code = 500;
|
||||
this.message = code;
|
||||
}
|
||||
this.tips = tips;
|
||||
// 这一步可不写,默认会保存堆栈追踪信息到自定义错误构造函数之前,
|
||||
// 而如果写成 `Error.captureStackTrace(this)` 则自定义错误的构造函数也会被保存到堆栈追踪信息
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
static fromCode(code?: number) {
|
||||
return new this(code);
|
||||
}
|
||||
static fromErrorData(code?: number, data?: any) {
|
||||
const error = new this(code);
|
||||
error.data = data;
|
||||
return error;
|
||||
}
|
||||
static parseError(e: CustomError) {
|
||||
return {
|
||||
code: e?.code,
|
||||
data: e?.data,
|
||||
message: e?.message,
|
||||
tips: e?.tips,
|
||||
};
|
||||
}
|
||||
parse(e?: CustomError) {
|
||||
if (e) {
|
||||
return CustomError.parseError(e);
|
||||
} else {
|
||||
return {
|
||||
code: e?.code,
|
||||
data: e?.data,
|
||||
message: e?.message,
|
||||
tips: e?.tips,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
try {
|
||||
//
|
||||
} catch(e) {
|
||||
if (e instanceof CustomError) {
|
||||
const errorInfo = e.parse();
|
||||
if (dev) {
|
||||
return {
|
||||
error: errorInfo,
|
||||
};
|
||||
} else {
|
||||
return errorInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
45
src/result/index.ts
Normal file
45
src/result/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const Code400 = [
|
||||
{
|
||||
code: 400,
|
||||
msg: 'Bad Request',
|
||||
zn: '表示其他错误,就是4xx都无法描述的前端发生的错误',
|
||||
},
|
||||
{ code: 401, msg: 'Authentication', zn: '表示认证类型的错误' }, // token 无效 (无token, token无效, token 过期)
|
||||
{
|
||||
code: 403,
|
||||
msg: 'Authorization',
|
||||
zn: '表示授权的错误(认证和授权的区别在于:认证表示“识别前来访问的是谁”,而授权则是“赋予特定用户执行特定操作的权限”)',
|
||||
},
|
||||
{ code: 404, msg: 'Not Found', zn: '表示访问的数据不存在' },
|
||||
{
|
||||
code: 405,
|
||||
msg: 'Method Not Allowd',
|
||||
zn: '表示可以访问接口,但是使用的HTTP方法不允许',
|
||||
},
|
||||
];
|
||||
|
||||
export const ResultCode = [{ code: 200, msg: 'OK', zn: '请求成功。' }].concat(Code400);
|
||||
type ResultProps = {
|
||||
code?: number;
|
||||
msg?: string;
|
||||
userTip?: string;
|
||||
};
|
||||
export const Result = ({ code, msg, userTip, ...other }: ResultProps) => {
|
||||
const Code = ResultCode.find((item) => item.code === code);
|
||||
let _result = {
|
||||
code: code || Code?.code,
|
||||
msg: msg || Code?.msg,
|
||||
userTip: undefined,
|
||||
...other,
|
||||
};
|
||||
if (userTip) {
|
||||
_result.userTip = userTip;
|
||||
}
|
||||
return _result;
|
||||
};
|
||||
Result.success = (data?: any) => {
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
};
|
||||
518
src/route.ts
Normal file
518
src/route.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { CustomError } from './result/error.ts';
|
||||
import { Schema, Rule, createSchema } from './validator/index.ts';
|
||||
import { pick } from './utils/pick.ts';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
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;
|
||||
/** return code */
|
||||
code?: number;
|
||||
/** return msg */
|
||||
message?: string;
|
||||
// 传递状态
|
||||
state?: S;
|
||||
// transfer data
|
||||
currentPath?: string;
|
||||
currentKey?: string;
|
||||
currentRoute?: Route;
|
||||
progress?: [[string, string]][];
|
||||
// onlyForNextRoute will be clear after next route
|
||||
nextQuery?: { [key: string]: any };
|
||||
// end
|
||||
end?: boolean;
|
||||
// 处理router manager
|
||||
// TODO:
|
||||
queryRouter?: QueryRouter;
|
||||
error?: any;
|
||||
call?: (message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
|
||||
index?: number;
|
||||
} & T;
|
||||
|
||||
export type Run<T = any> = (ctx?: RouteContext<T>) => Promise<typeof ctx | null | void>;
|
||||
|
||||
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
|
||||
export type RouteOpts = {
|
||||
path?: string;
|
||||
key?: string;
|
||||
id?: string;
|
||||
run?: Run;
|
||||
nextRoute?: NextRoute; // route to run after this route
|
||||
description?: string;
|
||||
middleware?: Route[] | string[]; // middleware
|
||||
type?: 'route' | 'middleware';
|
||||
/**
|
||||
* validator: {
|
||||
* packageName: {
|
||||
* type: 'string',
|
||||
* required: true,
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
validator?: { [key: string]: Rule };
|
||||
schema?: { [key: string]: Schema<any> };
|
||||
isVerify?: boolean;
|
||||
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
|
||||
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
|
||||
idUsePath?: boolean;
|
||||
isDebug?: boolean;
|
||||
};
|
||||
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>;
|
||||
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware'] as const;
|
||||
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
|
||||
export class Route {
|
||||
path?: string;
|
||||
key?: string;
|
||||
id?: string;
|
||||
share? = false;
|
||||
run?: Run;
|
||||
nextRoute?: NextRoute; // route to run after this route
|
||||
description?: string;
|
||||
middleware?: (Route | string)[]; // middleware
|
||||
type? = 'route';
|
||||
private _validator?: { [key: string]: Rule };
|
||||
schema?: { [key: string]: Schema<any> };
|
||||
data?: any;
|
||||
isVerify?: boolean;
|
||||
isDebug?: boolean;
|
||||
constructor(path: string, key: string = '', opts?: RouteOpts) {
|
||||
path = path.trim();
|
||||
key = key.trim();
|
||||
this.path = path;
|
||||
this.key = key;
|
||||
if (opts) {
|
||||
this.id = opts.id || nanoid();
|
||||
if (!opts.id && opts.idUsePath) {
|
||||
this.id = path + '$#$' + key;
|
||||
}
|
||||
this.run = opts.run;
|
||||
this.nextRoute = opts.nextRoute;
|
||||
this.description = opts.description;
|
||||
this.type = opts.type || 'route';
|
||||
this.validator = opts.validator;
|
||||
this.middleware = opts.middleware || [];
|
||||
this.key = opts.key || key;
|
||||
this.path = opts.path || path;
|
||||
this.isVerify = opts.isVerify ?? true;
|
||||
this.createSchema();
|
||||
} else {
|
||||
this.isVerify = true;
|
||||
this.middleware = [];
|
||||
this.id = nanoid();
|
||||
}
|
||||
this.isDebug = opts?.isDebug ?? false;
|
||||
}
|
||||
private createSchema() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
define<T extends { [key: string]: any } = RouterContextT>(opts: DefineRouteOpts): this;
|
||||
define<T extends { [key: string]: any } = RouterContextT>(fn: Run<T>): this;
|
||||
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T>): this;
|
||||
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T>): 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', 'middleware', 'type', 'validator', 'isVerify', 'isDebug'];
|
||||
for (let item of keys) {
|
||||
if (!checkList.includes(item)) {
|
||||
continue;
|
||||
}
|
||||
if (item === 'validator') {
|
||||
this.validator = opts[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;
|
||||
}
|
||||
addTo(router: QueryRouter | { add: (route: Route) => void; [key: string]: any }) {
|
||||
router.add(this);
|
||||
}
|
||||
setData(data: any) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryRouter {
|
||||
routes: Route[];
|
||||
maxNextRoute = 40;
|
||||
constructor() {
|
||||
this.routes = [];
|
||||
}
|
||||
|
||||
add(route: Route) {
|
||||
const has = this.routes.find((r) => r.path === route.path && r.key === route.key);
|
||||
if (has) {
|
||||
// remove the old route
|
||||
this.routes = this.routes.filter((r) => r.path === route.path && r.key === route.key);
|
||||
}
|
||||
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;
|
||||
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 }[] = [];
|
||||
// TODO: 向上递归执行动作, 暂时不考虑
|
||||
const routeMiddleware = route.middleware.map((m) => {
|
||||
let route: Route | undefined;
|
||||
const isString = typeof m === 'string';
|
||||
if (typeof m === 'string') {
|
||||
route = this.routes.find((r) => r.id === m);
|
||||
} else {
|
||||
route = this.routes.find((r) => r.path === m.path && r.key === m.key);
|
||||
}
|
||||
if (!route) {
|
||||
if (isString) {
|
||||
errorMiddleware.push({
|
||||
id: m as string,
|
||||
});
|
||||
} else
|
||||
errorMiddleware.push({
|
||||
path: m?.path,
|
||||
key: m?.key,
|
||||
});
|
||||
}
|
||||
return 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) {
|
||||
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 {
|
||||
await middleware.run(ctx);
|
||||
} catch (e) {
|
||||
if (route?.isDebug) {
|
||||
console.error('=====debug====:middlerware error');
|
||||
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
|
||||
console.error('=====debug====:', e.message);
|
||||
}
|
||||
if (e instanceof CustomError) {
|
||||
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) {
|
||||
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 {
|
||||
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.nextQuery;
|
||||
ctx.nextQuery = {};
|
||||
return await this.runRoute(path, key, ctx);
|
||||
}
|
||||
// clear body
|
||||
ctx.body = JSON.parse(JSON.stringify(ctx.body||''));
|
||||
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: 'Not found path' });
|
||||
}
|
||||
const { path, key, payload = {}, ...query } = message;
|
||||
ctx = ctx || {};
|
||||
ctx.query = { ...ctx.query, ...query, ...payload };
|
||||
ctx.state = {};
|
||||
// put queryRouter to ctx
|
||||
// TODO: 是否需要queryRouter,函数内部处理router路由执行,这应该是避免去内部去包含的功能过
|
||||
ctx.queryRouter = this;
|
||||
ctx.call = this.call.bind(this);
|
||||
ctx.index = 0;
|
||||
return await this.runRoute(path, key, ctx);
|
||||
}
|
||||
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||
return await this.parse(message, ctx);
|
||||
}
|
||||
getList(): RouteInfo[] {
|
||||
return this.routes.map((r) => {
|
||||
return pick(r, pickValue as any);
|
||||
});
|
||||
}
|
||||
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
|
||||
return async (msg: { path: string; key?: string; [key: string]: any }) => {
|
||||
const context = { ...ctx };
|
||||
const res = await router.parse(msg, context);
|
||||
if (wrapperFn) {
|
||||
res.data = res.body;
|
||||
return wrapperFn(res, context);
|
||||
}
|
||||
const { code, body, message } = res;
|
||||
return { code, data: body, message };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
||||
this.handle = this.getHandle(this, wrapperFn, ctx);
|
||||
}
|
||||
}
|
||||
46
src/server/handle-server.ts
Normal file
46
src/server/handle-server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import http, { IncomingMessage, Server, ServerResponse } from 'http';
|
||||
import { parseBody } from './parse-body.ts';
|
||||
import url from 'url';
|
||||
|
||||
/**
|
||||
* get params and body
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const handleServer = async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.url === '/favicon.ico') {
|
||||
return;
|
||||
}
|
||||
const can = ['get', 'post'];
|
||||
const method = req.method.toLocaleLowerCase();
|
||||
if (!can.includes(method)) {
|
||||
return;
|
||||
}
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
// 获取token
|
||||
let token = req.headers['authorization'] || '';
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
}
|
||||
// 获取查询参数
|
||||
const param = parsedUrl.query;
|
||||
let body: Record<any, any>;
|
||||
if (method === 'post') {
|
||||
body = await parseBody(req);
|
||||
}
|
||||
if (param?.payload && typeof param.payload === 'string') {
|
||||
try {
|
||||
const payload = JSON.parse(param.payload as string);
|
||||
param.payload = payload;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
const data = {
|
||||
token,
|
||||
...param,
|
||||
...body,
|
||||
};
|
||||
return data;
|
||||
};
|
||||
2
src/server/index.ts
Normal file
2
src/server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Server } from './server.ts';
|
||||
export { handleServer } from './handle-server.ts';
|
||||
18
src/server/parse-body.ts
Normal file
18
src/server/parse-body.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as http from 'http';
|
||||
|
||||
export const parseBody = async (req: http.IncomingMessage) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const arr: any[] = [];
|
||||
req.on('data', (chunk) => {
|
||||
arr.push(chunk);
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const body = Buffer.concat(arr).toString();
|
||||
resolve(JSON.parse(body));
|
||||
} catch (e) {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
148
src/server/server.ts
Normal file
148
src/server/server.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import http, { IncomingMessage, ServerResponse } from 'http';
|
||||
import { handleServer } from './handle-server.ts';
|
||||
|
||||
export type Listener = (...args: any[]) => void;
|
||||
|
||||
export type Cors = {
|
||||
/**
|
||||
* @default '*''
|
||||
*/
|
||||
origin?: string | undefined;
|
||||
};
|
||||
type ServerOpts = {
|
||||
/**path default `/api/router` */
|
||||
path?: string;
|
||||
/**handle Fn */
|
||||
handle?: (msg?: { path: string; key?: string; [key: string]: any }) => any;
|
||||
cors?: Cors;
|
||||
};
|
||||
export const resultError = (error: string, code = 500) => {
|
||||
const r = {
|
||||
code: code,
|
||||
message: error,
|
||||
};
|
||||
return JSON.stringify(r);
|
||||
};
|
||||
|
||||
export class Server {
|
||||
path = '/api/router';
|
||||
private _server: http.Server;
|
||||
public handle: ServerOpts['handle'];
|
||||
private _callback: any;
|
||||
private cors: Cors;
|
||||
private hasOn = false;
|
||||
constructor(opts?: ServerOpts) {
|
||||
this.path = opts?.path || '/api/router';
|
||||
this.handle = opts?.handle;
|
||||
this.cors = opts?.cors;
|
||||
}
|
||||
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
||||
listen(port: number, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(port: number, listeningListener?: () => void): void;
|
||||
listen(path: string, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(path: string, listeningListener?: () => void): void;
|
||||
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
||||
listen(handle: any, listeningListener?: () => void): void;
|
||||
listen(...args: any[]) {
|
||||
this._server = http.createServer();
|
||||
const callback = this.createCallback();
|
||||
this._server.on('request', callback);
|
||||
this._server.listen(...args);
|
||||
}
|
||||
setHandle(handle?: any) {
|
||||
this.handle = handle;
|
||||
}
|
||||
/**
|
||||
* get callback
|
||||
* @returns
|
||||
*/
|
||||
createCallback() {
|
||||
const path = this.path;
|
||||
const handle = this.handle;
|
||||
const cors = this.cors;
|
||||
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.url === '/favicon.ico') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.headersSent) {
|
||||
// 程序已经在其他地方响应了
|
||||
return;
|
||||
}
|
||||
if (this.hasOn && !req.url.startsWith(path)) {
|
||||
// 其他监听存在,不判断不是当前路径的请求,
|
||||
// 也就是不处理!url.startsWith(path)这个请求了
|
||||
// 交给其他监听处理
|
||||
return;
|
||||
}
|
||||
// res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
if (cors) {
|
||||
res.setHeader('Access-Control-Allow-Origin', cors?.origin || '*'); // 允许所有域名的请求访问,可以根据需要设置具体的域名
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.writeHead(200); // 设置响应头,给予其他api知道headersSent,它已经被响应了
|
||||
|
||||
const url = req.url;
|
||||
if (!url.startsWith(path)) {
|
||||
res.end(resultError(`not path:[${path}]`));
|
||||
return;
|
||||
}
|
||||
const messages = await handleServer(req, res);
|
||||
if (!handle) {
|
||||
res.end(resultError('no handle'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const end = await handle(messages as any);
|
||||
if (typeof end === 'string') {
|
||||
res.end(end);
|
||||
} else {
|
||||
res.end(JSON.stringify(end));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.code && typeof e.code === 'number') {
|
||||
res.end(resultError(e.message || `Router Server error`, e.code));
|
||||
} else {
|
||||
res.end(resultError('Router Server error'));
|
||||
}
|
||||
}
|
||||
};
|
||||
this._callback = _callback;
|
||||
return _callback;
|
||||
}
|
||||
get handleServer() {
|
||||
return this._callback;
|
||||
}
|
||||
set handleServer(fn: any) {
|
||||
this._callback = fn;
|
||||
}
|
||||
/**
|
||||
* 兜底监听,当除开 `/api/router` 之外的请求,框架只监听一个api,所以有其他的请求都执行其他的监听
|
||||
* @description 主要是为了兼容其他的监听
|
||||
* @param listener
|
||||
*/
|
||||
on(listener: Listener | Listener[]) {
|
||||
this._server = this._server || http.createServer();
|
||||
this._server.removeAllListeners('request');
|
||||
this.hasOn = true;
|
||||
if (Array.isArray(listener)) {
|
||||
listener.forEach((l) => this._server.on('request', l));
|
||||
} else {
|
||||
this._server.on('request', listener);
|
||||
}
|
||||
this._server.on('request', this._callback || this.createCallback());
|
||||
}
|
||||
get callback() {
|
||||
return this._callback || this.createCallback();
|
||||
}
|
||||
get server() {
|
||||
return this._server;
|
||||
}
|
||||
}
|
||||
156
src/server/ws-server.ts
Normal file
156
src/server/ws-server.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { Server } from './server.ts';
|
||||
import { parseIfJson } from '../utils/parse.ts';
|
||||
|
||||
export const createWsServer = (server: Server) => {
|
||||
// 将 WebSocket 服务器附加到 HTTP 服务器
|
||||
const wss = new WebSocketServer({ server: server.server });
|
||||
return wss;
|
||||
};
|
||||
type WsServerBaseOpts = {
|
||||
wss?: WebSocketServer;
|
||||
path?: string;
|
||||
};
|
||||
export type ListenerFn = (message: { data: Record<string, any>; ws: WebSocket; end: (data: any) => any }) => Promise<any>;
|
||||
export type Listener<T = 'router' | 'chat' | 'ai'> = {
|
||||
type: T;
|
||||
listener: ListenerFn;
|
||||
};
|
||||
|
||||
export class WsServerBase {
|
||||
wss: WebSocketServer;
|
||||
path: string;
|
||||
listeners: { type: string; listener: ListenerFn }[] = [];
|
||||
listening: boolean = false;
|
||||
constructor(opts: WsServerBaseOpts) {
|
||||
this.wss = opts.wss || new WebSocketServer();
|
||||
this.path = opts.path || '';
|
||||
}
|
||||
setPath(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
listen() {
|
||||
if (this.listening) {
|
||||
console.error('WsServer is listening');
|
||||
return;
|
||||
}
|
||||
this.listening = true;
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
ws.on('message', async (message: string) => {
|
||||
const data = parseIfJson(message);
|
||||
if (typeof data === 'string') {
|
||||
ws.emit('string', data);
|
||||
return;
|
||||
}
|
||||
const { type, data: typeData, ...rest } = data;
|
||||
if (!type) {
|
||||
ws.send(JSON.stringify({ code: 500, message: 'type is required' }));
|
||||
}
|
||||
const listeners = this.listeners.find((item) => item.type === type);
|
||||
const res = {
|
||||
type,
|
||||
data: {} as any,
|
||||
...rest,
|
||||
};
|
||||
const end = (data: any, all?: Record<string, any>) => {
|
||||
const result = {
|
||||
...res,
|
||||
data,
|
||||
...all,
|
||||
};
|
||||
ws.send(JSON.stringify(result));
|
||||
};
|
||||
|
||||
if (!listeners) {
|
||||
const data = { code: 500, message: `${type} server is error` };
|
||||
end(data);
|
||||
return;
|
||||
}
|
||||
listeners.listener({
|
||||
data: typeData,
|
||||
ws,
|
||||
end: end,
|
||||
});
|
||||
});
|
||||
ws.on('string', (message: string) => {
|
||||
if (message === 'close') {
|
||||
ws.close();
|
||||
}
|
||||
if (message === 'ping') {
|
||||
ws.send('pong');
|
||||
}
|
||||
});
|
||||
ws.send('connected');
|
||||
});
|
||||
}
|
||||
addListener(type: string, listener: ListenerFn) {
|
||||
if (!type || !listener) {
|
||||
throw new Error('type and listener is required');
|
||||
}
|
||||
const find = this.listeners.find((item) => item.type === type);
|
||||
if (find) {
|
||||
this.listeners = this.listeners.filter((item) => item.type !== type);
|
||||
}
|
||||
this.listeners.push({ type, listener });
|
||||
}
|
||||
removeListener(type: string) {
|
||||
this.listeners = this.listeners.filter((item) => item.type !== type);
|
||||
}
|
||||
}
|
||||
// TODO: ws handle and path and routerContext
|
||||
export class WsServer extends WsServerBase {
|
||||
server: Server;
|
||||
constructor(server: Server, opts?: any) {
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const path = server.path;
|
||||
super({ wss });
|
||||
this.server = server;
|
||||
this.setPath(opts?.path || path);
|
||||
this.initListener();
|
||||
}
|
||||
initListener() {
|
||||
const server = this.server;
|
||||
const listener: Listener = {
|
||||
type: 'router',
|
||||
listener: async ({ data, ws, end }) => {
|
||||
if (!server) {
|
||||
end({ code: 500, message: 'server handle is error' });
|
||||
return;
|
||||
}
|
||||
const handle = this.server.handle;
|
||||
try {
|
||||
const result = await handle(data as any);
|
||||
end(result);
|
||||
} catch (e) {
|
||||
if (e.code && typeof e.code === 'number') {
|
||||
end({
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
});
|
||||
} else {
|
||||
end({ code: 500, message: 'Router Server error' });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
this.addListener(listener.type, listener.listener);
|
||||
}
|
||||
listen() {
|
||||
super.listen();
|
||||
const server = this.server;
|
||||
const wss = this.wss;
|
||||
// HTTP 服务器的 upgrade 事件
|
||||
server.server.on('upgrade', (req, socket, head) => {
|
||||
if (req.url === this.path) {
|
||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
// 这里手动触发 connection 事件
|
||||
// @ts-ignore
|
||||
wss.emit('connection', ws, req);
|
||||
});
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
97
src/static.ts
Normal file
97
src/static.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const fetch = require('node-fetch'); // 如果使用 Node.js 18 以上版本,可以改用内置 fetch
|
||||
const url = require('url');
|
||||
|
||||
// 配置远端静态文件服务器和本地缓存目录
|
||||
const remoteServer = 'https://example.com/static'; // 远端服务器的 URL
|
||||
const cacheDir = path.join(__dirname, 'cache'); // 本地缓存目录
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// 确保本地缓存目录存在
|
||||
fs.mkdir(cacheDir, { recursive: true }).catch(console.error);
|
||||
|
||||
// 获取文件的 content-type
|
||||
function getContentType(filePath) {
|
||||
const extname = path.extname(filePath);
|
||||
const contentType = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'text/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.wav': 'audio/wav',
|
||||
'.mp4': 'video/mp4'
|
||||
};
|
||||
return contentType[extname] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
// 处理请求文件
|
||||
async function serveFile(filePath, remoteUrl, res) {
|
||||
try {
|
||||
// 检查文件是否存在于本地缓存中
|
||||
const fileContent = await fs.readFile(filePath);
|
||||
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
|
||||
res.end(fileContent, 'utf-8');
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
// 本地缓存中不存在,向远端服务器请求文件
|
||||
try {
|
||||
const response = await fetch(remoteUrl);
|
||||
|
||||
if (response.ok) {
|
||||
// 远端请求成功,获取文件内容
|
||||
const data = await response.buffer();
|
||||
|
||||
// 将文件缓存到本地
|
||||
await fs.writeFile(filePath, data);
|
||||
|
||||
// 返回文件内容
|
||||
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
|
||||
res.end(data, 'utf-8');
|
||||
} else {
|
||||
// 远端文件未找到或错误,返回 404
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Error 404: File not found at ${remoteUrl}`);
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
// 处理请求错误
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Server Error: Unable to fetch ${remoteUrl}`);
|
||||
}
|
||||
} else {
|
||||
// 其他文件系统错误
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Server Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 HTTP 服务器
|
||||
http.createServer(async (req, res) => {
|
||||
let reqPath = req.url;
|
||||
|
||||
// 如果路径是根路径 `/`,将其设置为 `index.html`
|
||||
if (reqPath === '/') reqPath = '/index.html';
|
||||
|
||||
// 构建本地缓存路径和远端 URL
|
||||
const localFilePath = path.join(cacheDir, reqPath); // 本地文件路径
|
||||
const remoteFileUrl = url.resolve(remoteServer, reqPath); // 远端文件 URL
|
||||
|
||||
// 根据请求路径处理文件或返回 index.html(单页面应用处理)
|
||||
await serveFile(localFilePath, remoteFileUrl, res);
|
||||
|
||||
// 单页面应用的路由处理
|
||||
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||
|
||||
// 如果未匹配到任何文件,返回 index.html
|
||||
const indexFilePath = path.join(cacheDir, 'index.html');
|
||||
const indexRemoteUrl = url.resolve(remoteServer, '/index.html');
|
||||
await serveFile(indexFilePath, indexRemoteUrl, res);
|
||||
}).listen(PORT, () => {
|
||||
console.log(`Server running at http://localhost:${PORT}`);
|
||||
});
|
||||
13
src/utils/parse.ts
Normal file
13
src/utils/parse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const parseIfJson = (input: string): { [key: string]: any } | string => {
|
||||
try {
|
||||
// 尝试解析 JSON
|
||||
const parsed = JSON.parse(input);
|
||||
// 检查解析结果是否为对象(数组或普通对象)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果解析失败,直接返回原始字符串
|
||||
}
|
||||
return input;
|
||||
};
|
||||
9
src/utils/pick.ts
Normal file
9
src/utils/pick.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||
const result = {} as Pick<T, K>;
|
||||
keys.forEach((key) => {
|
||||
if (key in obj) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
1
src/validator/index.ts
Normal file
1
src/validator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './rule.ts';
|
||||
92
src/validator/rule.ts
Normal file
92
src/validator/rule.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { z, ZodError, Schema } from 'zod';
|
||||
export { Schema };
|
||||
type BaseRule = {
|
||||
value?: any;
|
||||
required?: boolean;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type RuleString = {
|
||||
type: 'string';
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
regex?: string;
|
||||
} & BaseRule;
|
||||
|
||||
type RuleNumber = {
|
||||
type: 'number';
|
||||
min?: number;
|
||||
max?: number;
|
||||
} & BaseRule;
|
||||
|
||||
type RuleBoolean = {
|
||||
type: 'boolean';
|
||||
} & BaseRule;
|
||||
|
||||
type RuleArray = {
|
||||
type: 'array';
|
||||
items: Rule;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
} & BaseRule;
|
||||
|
||||
type RuleObject = {
|
||||
type: 'object';
|
||||
properties: { [key: string]: Rule };
|
||||
} & BaseRule;
|
||||
|
||||
type RuleAny = {
|
||||
type: 'any';
|
||||
} & BaseRule;
|
||||
|
||||
export type Rule = RuleString | RuleNumber | RuleBoolean | RuleArray | RuleObject | RuleAny;
|
||||
|
||||
export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
||||
switch (rule.type) {
|
||||
case 'string':
|
||||
let stringSchema = z.string();
|
||||
if (rule.minLength) stringSchema = stringSchema.min(rule.minLength, `String must be at least ${rule.minLength} characters long.`);
|
||||
if (rule.maxLength) stringSchema = stringSchema.max(rule.maxLength, `String must not exceed ${rule.maxLength} characters.`);
|
||||
if (rule.regex) stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
||||
return stringSchema;
|
||||
case 'number':
|
||||
let numberSchema = z.number();
|
||||
if (rule.min) numberSchema = numberSchema.min(rule.min, `Number must be at least ${rule.min}.`);
|
||||
if (rule.max) numberSchema = numberSchema.max(rule.max, `Number must not exceed ${rule.max}.`);
|
||||
return numberSchema;
|
||||
case 'boolean':
|
||||
return z.boolean();
|
||||
case 'array':
|
||||
return z.array(createSchema(rule.items));
|
||||
case 'object':
|
||||
return z.object(Object.fromEntries(Object.entries(rule.properties).map(([key, value]) => [key, createSchema(value)])));
|
||||
case 'any':
|
||||
return z.any();
|
||||
default:
|
||||
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
||||
}
|
||||
};
|
||||
export const createSchema = (rule: Rule): Schema => {
|
||||
try {
|
||||
rule.required = rule.required || false;
|
||||
if (!rule.required) {
|
||||
return schemaFormRule(rule).nullable();
|
||||
}
|
||||
return schemaFormRule(rule);
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
console.error(e.format());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const createSchemaList = (rules: Rule[]) => {
|
||||
try {
|
||||
return rules.map((rule) => createSchema(rule));
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
console.error(e.format());
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user