简易cf workerjs web框架,参考golang gin

参考极客兔兔的7daysgolang中的gee,简易版的gin框架
极客兔兔的链接:https://geektutu.com/post/gee.html

可以简化workerjs开发,只需修改handleRequest 中的内容就可以,以下为示例代码

/**
 * Welcome to Cloudflare Workers! This is your first worker.
 *
 * - Run `npm run dev` in your terminal to start a development server
 * - Open a browser tab at http://localhost:8787/ to see your worker in action
 * - Run `npm run deploy` to publish your worker
 *
 * Learn more at https://developers.cloudflare.com/workers/
 */
class Engine {
	constructor() {
		this.name = 'gin';
		this.router = new Router();
		this._group = new RouterGroup(this);
		this.groups = [];
		this.groups.push(this._group);
	}
	addRoute(method, pattern, handler) {
		this.router.addRoute(method, pattern, handler);
	}
	group(prefix) {
		return this._group.group(prefix);
	}
	use(...middlewares) {
		this._group.use(...middlewares);
	}
	get(pattern, handler) {
		this.router.addRoute('GET', pattern, handler);
	}
	post(pattern, handler) {
		this.router.addRoute('POST', pattern, handler);
	}
	delete(pattern, handler) {
		this.router.addRoute('DELETE', pattern, handler);
	}
	put(pattern, handler) {
		this.router.addRoute('PUT', pattern, handler);
	}
	patch(pattern, handler) {
		this.router.addRoute('PATCH', pattern, handler);
	}
	options(pattern, handler) {
		this.router.addRoute('OPTIONS', pattern, handler);
	}
	async serve(request) {
		const ctx = new Context(request);
		const middlewares = [];
		for (let group of this.groups) {
			if (ctx.path.startsWith(group.prefix)) {
				middlewares.push(...group.middlewares);
			}
		}
		ctx.handlers = middlewares;
		return this.router.handle(ctx);
	}
}

class Context {
	constructor(request) {
		this.request = request;
		this.headers = {};
		const url = new URL(request.url);
		this._url = url;
		const params = new URLSearchParams(url.search);
		this.path = url.pathname;
		this._query = Object.fromEntries(params.entries());
		this.status = 200;
		this.method = request.method;
		this.params = {};
		this.handlers = [];
		this.index = -1;
	}
	next() {
		this.index++;
		let len = this.handlers.length;
		for (; this.index < len; this.index++) {
			return this.handlers[this.index](this);
		}
	}
	query(key) {
		return this._query[key];
	}
	param(key) {
		return this.params[key];
	}
	setHeader(key, value) {
		this.headers[key] = value;
	}
	setStatus(code) {
		this.status = code;
	}
	string(code, data) {
		this.setHeader('Content-Type', 'text/plain');
		this.setStatus(code);
		return new Response(data, {
			headers: this.headers,
			status: this.status,
		});
	}
	json(code, data) {
		this.setHeader('Content-Type', 'application/json');
		this.setStatus(code);
		return new Response(JSON.stringify(data), {
			headers: this.headers,
			status: this.status,
		});
	}
	data(code, data) {
		this.setStatus(code);
		return new Response(data, {
			headers: this.headers,
			status: this.status,
		});
	}
	html(code, data) {
		this.setHeader('Content-Type', 'text/html');
		this.setStatus(code);
		return new Response(data, {
			headers: this.headers,
			status: this.status,
		});
	}
}

class Router {
	constructor() {
		this.roots = new Map(); // [(get, post, put, delete, patch, options), treeNode]
		this.handlers = new Map();
	}
	addRoute(method, pattern, handler) {
		const parts = parsePattern(pattern);
		const key = method + '-' + pattern;
		if (!this.roots.has(method)) {
			this.roots.set(method, new treeNode('', '', false));
		}
		this.roots.get(method).insert(pattern, parts, 0);
		this.handlers.set(key, handler);
	}
	getRoute(method, path) {
		const searchParts = parsePattern(path);
		const params = {};
		const root = this.roots.get(method);
		if (!root) {
			return {
				node: null,
				params,
			};
		}
		const node = root.search(searchParts, 0);
		if (node) {
			const parts = parsePattern(node.pattern);
			for (let i = 0; i < parts.length; i++) {
				let part = parts[i];
				if (part[0] === ':') {
					params[part.slice(1)] = searchParts[i];
					continue;
				}
				if (part[0] === '*' && parts[i].length > 1) {
					params[part.slice(1)] = searchParts.slice(i).join('/');
					break;
				}
			}
			return {
				node,
				params,
			};
		}
		return {
			node: null,
			params,
		};
	}
	async handle(ctx) {
		const { node, params } = this.getRoute(ctx.method, ctx.path);
		if (node) {
			ctx.params = params;
			const key = ctx.method + '-' + node.pattern;
			const handler = this.handlers.get(key);
			ctx.handlers.push(handler);
			// if (handler) {
			// 	return handler(ctx);
			// }
		} else {
			ctx.handlers.push(async (ctx) => {
				return ctx.string(404, '404 Not Found');
			});
		}
		// return ctx.string(404, '404 Not Found');
		return ctx.next();
	}
}

class RouterGroup {
	constructor(engine) {
		this.prefix = '';
		this.middlewares = [];
		this.parent = null;
		this.engine = engine;
	}
	group(prefix) {
		const newGroup = new RouterGroup(this.engine);
		newGroup.prefix = this.prefix + prefix;
		// newGroup.middlewares = middlewares;
		newGroup.parent = this;
		this.engine.groups.push(newGroup);
		return newGroup;
	}
	use(...middlewares) {
		this.middlewares.push(...middlewares);
	}
	addRoute(method, pattern, handler) {
		this.engine.addRoute(method, this.prefix + pattern, handler);
	}
	get(pattern, handler) {
		this.engine.get(this.prefix + pattern, handler);
	}
	post(pattern, handler) {
		this.engine.post(this.prefix + pattern, handler);
	}
	delete(pattern, handler) {
		this.engine.delete(this.prefix + pattern, handler);
	}
	put(pattern, handler) {
		this.engine.put(this.prefix + pattern, handler);
	}
	patch(pattern, handler) {
		this.engine.patch(this.prefix + pattern, handler);
	}
	options(pattern, handler) {
		this.engine.options(this.prefix + pattern, handler);
	}
}

function parsePattern(pattern) {
	const parts = pattern.split('/');
	const nodes = [];
	for (let part of parts) {
		if (part !== '') {
			nodes.push(part);
			if (part[0] === '*') {
				break;
			}
		}
	}
	return nodes;
}

class treeNode {
	constructor(pattern, part, isWild) {
		this.pattern = pattern ?? ''; // 待匹配路由,例如 /p/:lang
		this.part = part; // 路由中的一部分,例如 :lang
		this.isWild = isWild; // 是否精确匹配,part 含有 : 或 * 时为true
		this.children = [];
	}
	matchChild(path) {
		for (let child of this.children) {
			if (child.part === path || child.isWild) {
				return child;
			}
		}
	}
	matchChildren(path) {
		let nodes = [];
		for (let child of this.children) {
			if (child.part === path || child.isWild) {
				nodes.push(child);
			}
		}
		nodes.sort((a, b) => {
			return a.part.length - b.part.length;
		});
		return nodes;
	}
	insert(pattern, parts, height) {
		if (parts.length === height) {
			this.pattern = pattern;
			return;
		}
		const part = parts[height];
		let child = this.matchChild(part);
		if (!child) {
			child = new treeNode('', part, part[0] === ':' || part[0] === '*');
			this.children.push(child);
		}
		child.insert(pattern, parts, height + 1);
	}
	search(parts, height) {
		if (parts.length === height || this.part.startsWith('*')) {
			if (this.pattern === '') {
				return;
			}
			return this;
		}
		const part = parts[height];
		const children = this.matchChildren(part);
		for (let child of children) {
			let node = child.search(parts, height + 1);
			if (node) {
				return node;
			}
		}
		return null;
	}
}

const logMiddleware = (ctx) => {
	const start = new Date();
	const response = ctx.next();
	const duration = Date.now() - start;
	// console.log('log middleware', ctx.path, duration, 'ms');
	return response; // 返回响应
};

const authMiddleware = (ctx) => {
	const token = ctx.query('token');
	if (!token) {
		return ctx.string(401, '401 Unauthorized');
	}
	return ctx.next();
};
const testIPMiddleware = async (ctx) => {
	const response = await fetch('http://ip-api.com/json');
	const data = await response.json();
	ctx.setHeader('X-IP', data.query);
	const resp = ctx.next();
	return resp;
};

const corsMiddleware = async (ctx) => {
	const origin = ctx.request.headers.get('Origin');
	// 允许cookie
	ctx.setHeader('Access-Control-Allow-Origin', origin);
	ctx.setHeader('Access-Control-Allow-Credentials', 'true');
	if (ctx.method === 'OPTIONS') {
		ctx.setHeader('Access-Control-Allow-Methods', ctx.request.headers.get('Access-Control-Request-Method'));
		ctx.setHeader('Access-Control-Allow-Headers', ctx.request.headers.get('Access-Control-Request-Headers'));
		ctx.setHeader('Access-Control-Max-Age', '7200');
		ctx.setStatus(204);
		return ctx.data(204, null);
	}
	return ctx.next();
};

const newEngine = () => {
	return new Engine();
};

const handleRequest = () => {
	const router = newEngine();
	router.use(logMiddleware);
	router.use(corsMiddleware);
	router.get('/', async (ctx) => {
		return ctx.string(200, 'Hello GIN');
	});
	router.get('/self/ip', async (ctx) => {
		const response = await fetch('http://ip-api.com/json');
		return response;
	});
	router.get('/self/ip2', async (ctx) => {
		const response = await fetch('http://ip-api.com/json');
		const data = await response.json();
		return ctx.json(200, data);
	});
	router.get('/about', async (ctx) => {
		const name = ctx.query('name');
		return ctx.json(200, {
			name,
		});
	});
	router.post('/v1/chat/completions', async (ctx) => {
		const response = await fetch('https://api.openai.com/v1/chat/completions', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				Authorization: 'Bearer sk-1234567890xxx',
			},
			body: ctx.request.body,
		});
		const newHeaders = new Headers(response.headers);
		for (const [key, value] of newHeaders.entries()) {
			ctx.setHeader(key, value);
		}
		return ctx.data(200, response.body);
	});
	router.get('/about/:name', async (ctx) => {
		const name = ctx.param('name');
		return ctx.json(200, {
			name,
		});
	});
	const api = router.group('/api');
	api.use(authMiddleware);
	api.use(testIPMiddleware);
	api.get('/ping', async (ctx) => {
		return ctx.string(200, 'pong');
	});
	api.get('/upload/*filepath', async (ctx) => {
		const filepath = ctx.param('filepath');
		return ctx.json(200, {
			filepath,
		});
	});
	const userApi = api.group('/user');
	userApi.get('/:id', async (ctx) => {
		const id = ctx.param('id');
		return ctx.json(200, {
			id,
			name: 'sky',
			age: 18,
		});
	});
	return async (request) => router.serve(request);
};

const listener = handleRequest();

addEventListener('fetch', (event) => {
	// event.respondWith(router.serve(event.request));
	event.respondWith(listener(event.request));
});

2 个赞

感谢大佬 !