Erweitere die Flummpress-Klasse um Response- und Middleware-Typen, verbessere die Fehlerbehandlung und optimiere die Anfrageverarbeitung

This commit is contained in:
Flummi 2025-03-15 16:41:11 +01:00
parent 2f6f833549
commit f2cf3821f2

View File

@ -6,9 +6,9 @@ import querystring from "node:querystring";
import Router from "./router.js"; import Router from "./router.js";
import Tpl from "./template.js"; import Tpl from "./template.js";
export { Router, Tpl, Request }; export { Router, Tpl, Request, Response, Middleware };
type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void; type Middleware = (req: Request, res: Response, next: () => void) => void;
interface Request extends IncomingMessage { interface Request extends IncomingMessage {
parsedUrl: { parsedUrl: {
@ -22,16 +22,27 @@ interface Request extends IncomingMessage {
post?: Record<string, string>; post?: Record<string, string>;
}; };
interface Response extends ServerResponse {
reply: (options: { code?: number; type?: string; body: string }) => void;
json: (body: string, code?: number) => void;
html: (body: string, code?: number) => void;
redirect: (target: string, code?: number) => void;
}
export default class Flummpress { export default class Flummpress {
private server?: http.Server; private server?: http.Server;
private errorHandler: Middleware;
router: Router; router: Router;
tpl: Tpl; tpl: Tpl;
middleware: Set<Middleware>; middleware: Middleware[];
constructor() { constructor() {
this.router = new Router(); this.router = new Router();
this.tpl = new Tpl(); this.tpl = new Tpl();
this.middleware = new Set(); this.middleware = [];
this.errorHandler = async (req: Request, res: Response) => {
res.writeHead(500).end("500 - Internal Server Error");
};
} }
/** /**
@ -39,15 +50,18 @@ export default class Flummpress {
* @param {any} obj - An instance of Router, Tpl, or a middleware function. * @param {any} obj - An instance of Router, Tpl, or a middleware function.
* @returns {this} - The current instance for chaining. * @returns {this} - The current instance for chaining.
*/ */
use(obj: Router | Tpl): this { use(obj: Router | Tpl | Middleware): this {
if(obj instanceof Router) if(obj instanceof Router)
this.router.use(obj); this.router.use(obj);
else if(obj instanceof Tpl) else if(obj instanceof Tpl)
this.tpl = obj; this.tpl = obj;
else { else
if(!this.middleware.has(obj)) this.middleware.push(obj);
this.middleware.add(obj); return this;
} }
setErrorHandler(handler: Middleware): this {
this.errorHandler = handler;
return this; return this;
} }
@ -57,169 +71,132 @@ export default class Flummpress {
* @returns {this} - The current instance for chaining. * @returns {this} - The current instance for chaining.
*/ */
listen(...args: any[]): this { listen(...args: any[]): this {
this.server = http.createServer(async (request: IncomingMessage, res: ServerResponse) => { this.server = http
const req = request as Request; .createServer(async (request: IncomingMessage, response: ServerResponse) => {
const t_start = process.hrtime(); const req = request as Request;
const res = this.createResponse(req, response);
const start = process.hrtime();
// URL and query parsing try {
const _url = new URL(req.url!.replace(/(?!^.)(\/+)?$/, ''), "http://localhost"); this.parseRequest(req);
req.parsedUrl = {
pathname: _url.pathname,
split: _url.pathname.split("/").slice(1),
searchParams: _url.searchParams,
qs: Object.fromEntries(Object.entries(querystring.parse(_url.search.substring(1))).filter(([_, v]) => typeof v === 'string')) as Record<string, string>,
};
// extract cookies for(const mw of this.middleware) {
req['cookies'] = {}; await mw(req, res, () => {});
if(req.headers.cookie) { }
req.headers.cookie.split("; ").forEach((c: string) => {
const parts = c.split('=');
req.cookies[parts.shift()?.trim() ?? ""] = decodeURI(parts.join('='));
});
}
// run middleware const route = this.router.getRoute(req.parsedUrl.pathname, req.method!);
await Promise.all([...this.middleware].map(m => m(req, res, () => {})));
// route and method handling if(route) {
const method = req.method === 'HEAD' ? 'get' : req.method?.toLowerCase(); const [pathPattern, methods] = route;
const route = this.router.getRoute(req.parsedUrl.pathname, req.method === 'HEAD' ? 'GET' : req.method!); const method = req.method?.toLowerCase();
const handler = methods[method!];
const middleware = methods[`${method}mw`];
if(route) { if(middleware) {
// route found for(const mw of middleware) {
const cb = route[1][method!]; await mw(req, res, () => {});
const middleware = route[1][`${method}mw`]; }
}
req['params'] = req.parsedUrl.pathname.match(new RegExp(route[0]))?.groups; if(handler) {
req['post'] = await this.readBody(req); req.params = req.parsedUrl.pathname.match(new RegExp(pathPattern))?.groups || {};
req.post = await this.readBody(req);
handler(req, res);
}
else {
res.writeHead(405).end("405 - Method Not Allowed");
}
}
else {
res.writeHead(404).end("404 - Not Found");
}
}
catch(err: any) {
this.errorHandler(req, res, () => {});
}
const result = await this.processMiddleware(middleware, req, this.createResponse(req, res)); console.log([
if(result) `[${new Date().toISOString()}]`,
cb(req, res); `${(process.hrtime(start)[1] / 1e6).toFixed(2)}ms`,
} `${req.method} ${res.statusCode}`,
else { req.parsedUrl.pathname,
// route not found ].join(" | "));
res.writeHead(404).end(`404 - ${req.method} ${req.parsedUrl.pathname}`); }).listen(...args);
}
console.log([
`[${new Date().toLocaleTimeString()}]`,
`${(process.hrtime(t_start)[1] / 1e6).toFixed(2)}ms`,
`${req.method} ${res.statusCode}`,
req.parsedUrl.pathname
].map(e => e.toString().padEnd(15)).join(""));
}).listen(...args);
return this; return this;
} }
private parseRequest(req: Request): void {
const url = new URL(req.url!.replace(/(?!^.)(\/+)?$/, ""), "http://localhost");
req.parsedUrl = {
pathname: url.pathname,
split: url.pathname.split("/").slice(1),
searchParams: url.searchParams,
qs: Object.fromEntries(url.searchParams.entries()),
};
req.cookies = {};
if(req.headers.cookie) {
req.headers.cookie.split("; ").forEach((cookie) => {
const [key, value] = cookie.split("=");
req.cookies[key] = decodeURIComponent(value);
});
}
}
/** /**
* Reads and parses the body of an incoming HTTP request. * Reads and parses the body of an incoming HTTP request.
* @param {IncomingMessage} req - The HTTP request object. * @param {IncomingMessage} req - The HTTP request object.
* @returns {Promise<any>} - A promise that resolves to the parsed body data. * @returns {Promise<any>} - A promise that resolves to the parsed body data.
*/ */
readBody(req: IncomingMessage): Promise<Record<string, string>> { private async readBody(req: IncomingMessage): Promise<Record<string, string>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let data = ""; let body = "";
req.on("data", (chunk: string) => data += chunk); req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => { req.on("end", () => {
if(req.headers["content-type"] === "application/json") { try {
try { resolve(
resolve(JSON.parse(data) as Record<string, string>); req.headers["content-type"] === "application/json"
} ? JSON.parse(body)
catch(err: any) { : querystring.parse(body)
reject(err);
}
}
else {
const parsedData = querystring.parse(data);
const decodedData = Object.fromEntries(
Object.entries(parsedData).map(([key, value]) => {
try {
return [key, decodeURIComponent(value as string)];
}
catch(err: any) {
return [key, value];
}
})
); );
resolve(decodedData as Record<string, string>); }
catch(err: any) {
reject(err);
} }
}); });
req.on("error", (err: Error) => reject(err)); req.on("error", reject);
}); });
} }
/** private createResponse(
* Enhances the HTTP response object with additional methods for convenience. req: Request,
* @param {Request} req - The HTTP request object. response: ServerResponse
* @param {ServerResponse} res - The HTTP response object. ): Response {
* @returns {ServerResponse & { const res = response as Response;
* reply: Function, json: Function, html: Function, redirect: Function
* }} - The enhanced response object.
*/
createResponse(req: Request, res: ServerResponse): ServerResponse & {
reply: (options: { code?: number; type?: string; body: string }) => ServerResponse;
json: (body: string, code?: number) => ServerResponse;
html: (body: string, code?: number) => ServerResponse;
redirect: (target: string, code?: number) => void;
} {
const enhancedRes = Object.assign(res, {
reply: ({ code = 200, type = "text/html", body }: { code?: number; type?: string; body: string }) => {
res.writeHead(code, {
"content-type": `${type}; charset=UTF-8`,
"content-length": Buffer.byteLength(body, "utf-8"),
});
if(req.method === "HEAD")
body = null as unknown as string;
res.end(body);
return res;
},
json: (body: string, code = 200) => {
if(typeof body === "object")
body = JSON.stringify(body);
return enhancedRes.reply({ code, body, type: "application/json" });
},
html: (body: string, code = 200) => enhancedRes.reply({ code, body, type: "text/html" }),
redirect: (target: string, code = 307) => {
res.writeHead(code, {
"Cache-Control": "no-cache, public",
"Location": target,
});
res.end();
}
});
return enhancedRes; res.reply = ({ code = 200, type = "text/html", body }) => {
} res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` });
res.end(body);
};
/** res.json = (body, code = 200) => {
* Processes middleware functions for an incoming request. res.reply({ code, type: "application/json", body: JSON.stringify(body) });
* @param {Middleware} middleware - The middleware function to process. };
* @param {IncomingMessage} req - The HTTP request object.
* @param {ServerResponse} res - The HTTP response object.
* @returns {Promise<boolean>} - Resolves to true if middleware processing is successful.
*/
private async processMiddleware(
middleware: Middleware[],
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
if(!middleware || middleware.length === 0)
return true;
for(const fn of middleware) { res.html = (body, code = 200) => {
const proceed = await new Promise<boolean>(resolve => { res.reply({ code, type: "text/html", body });
fn(req, res, () => resolve(true)); };
});
if(!proceed) res.redirect = (target, code = 302) => {
return false; res.writeHead(code, { Location: target });
} res.end();
return true; };
return res;
} }
} }