Erweitere die Flummpress-Klasse um Response- und Middleware-Typen, verbessere die Fehlerbehandlung und optimiere die Anfrageverarbeitung
This commit is contained in:
parent
2f6f833549
commit
f2cf3821f2
261
src/index.ts
261
src/index.ts
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user