通过实现部分nginx功能,掌握bun的高级用法
- Setp0: 确保bun已经正确安装
shell
$ bun -v
1.3.4
如果还未安装,请到B站看下我的安装视频:bun win11 安装 bun linux 安装 。
- Setp1: 建立bun脚手架项目bun-nginx
shell
$ bun init bun-nginx
? Select a project template - Press return to submit.
❯ Blank
React
Library
✓ Select a project template: Blank
+ .gitignore
+ CLAUDE.md
+ .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc -> CLAUDE.md
+ index.ts
+ tsconfig.json (for editor autocomplete)
+ README.md
- Setp2: 改成js项目(本文使用js示例)
- 将index.ts重命名为nginx-proxy.js
- package.json 的module 改为 nginx-proxy.js
js
//类似nginx 配置upstream代理
const upstream = {
"vip": {//模拟VIP业务
"servers": [
{ "host": "192.168.1.10:3000", "weight": 1 },//算法 1/(1+2)
{ "host": "192.168.1.10:4000", "weight": 2 },
],
"ip_hash": false//开启ip_hash,weight不生效
},
"normal": {//模拟普通业务
"servers": [
{ "host": "192.168.1.2:8080", "weight": 1 },//算法 1/(1+2)
{ "host": "192.168.1.3:8080", "weight": 2 },
],
"ip_hash": false//关闭ip_hash,weight生效
}
}
// 按权重随机选择(带概率)
function randomSelectByProbability(list) {
if (!Array.isArray(list) || list.length === 0) {
throw new Error('Input must be a non-empty array');
}
// 可选:校验 probability 是否存在且有效
const totalProb = list.reduce((sum, item) => sum + (item.probability || 0), 0);
if (totalProb <= 0) {
throw new Error('Total probability must be greater than 0');
}
// 计算随机数 rand,范围 [0, totalProb)
const rand = Math.random() * totalProb; // [0, totalProb)
let cumulative = 0;
for (const item of list) {
cumulative += item.probability;
if (rand < cumulative) {
return item;
}
}
// 理论上不会走到这里(如果 totalProb > 0)
return list[list.length - 1];
}
// 处理 CORS 预检请求
function handleCORS(request) {
const origin = request.headers.get('Origin') || '*';
// 允许的 CORS 头(所有响应都带)
const corsHeaders = {
'Access-Control-Allow-Origin': '*', // 注意:若需 credentials,不能用 '*',见下方说明
'Access-Control-Allow-Credentials': 'true',
};
// 如果是 OPTIONS 预检请求
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers':
'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range',
'Access-Control-Max-Age': '1728000',
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '0',
},
});
}
// 非 OPTIONS 请求:不返回 Response,由主逻辑处理
return null;
}
// 代理请求
async function fetchProxy(url, method, msg, hdrs) {
console.log(`Fetching=> ${url}`);
const response = await fetch(url, {
method: method,
body: msg,
headers: hdrs,
});
return response;
}
const server = Bun.serve({
port: 80,
idleTimeout: 10,
reusePort: false,
async fetch(req) {
const corsResponse = handleCORS(req);
if (corsResponse) {
return corsResponse;
}
const urlObj = new URL(req.url);
const path = urlObj.pathname;
const queryParams = urlObj.searchParams;
const fullPath = urlObj.pathname + urlObj.search;
const method = req.method;
const msg = req.body ? await req.text() : null;
const hdrs = req.headers;
// respond with text/html
if (path === "/") return new Response("Welcome to Bun!");
const upstream_name = fullPath.split("/")[1].split("?")[0];
const upstream_config = upstream[upstream_name];
if (!upstream_config) {
return new Response("Upstream not found", { status: 404 });
} else if (upstream_config.ip_hash) {
// 使用ip_hash算法
const client_ip = req.headers.get("x-forwarded-for") || req.headers.get("remote-addr");
let index = 0;
if (client_ip) {
index = Bun.hash(client_ip) % upstream_config.servers.length;
} else {
index = Math.floor(Math.random() * upstream_config.servers.length);
}
const server = upstream_config.servers[index];
console.log('fullPath', fullPath);
const url = `http://${server.host}${fullPath}`;
return await fetchProxy(url, method, msg, hdrs);
} else {
const serverConfig = upstream_config.servers;
//没有配置weight的,默认weight设置为1
serverConfig.forEach((server, index) => {
if (!server.weight) {
server.weight = 1;
}
});
const total_weight = serverConfig.reduce((sum, server) => sum + server.weight, 0);
serverConfig.forEach((server, index) => {
server.probability = server.weight / total_weight;
});
const server = randomSelectByProbability(serverConfig);
const url = `http://${server.host}${fullPath}`;
console.log(`Proxying to ${url}`);
return await fetchProxy(url, method, msg, hdrs);
}
},
});
- Setp3: 编写模拟后台http服务程序,这个程序也可以用python,java等语言模拟
- nginx-proxy-backend-server1.js
js
import minimist from 'minimist';
function getPort() {
// 🔥 关键:跳过 bun run 部分,只取脚本后的参数
const args = minimist(process.argv.slice(2)); // ["-p", "3000"] 或 ["--port", "3000"]
// 获取端口(支持 -p 和 --port)
const portArg = args.p || args.port;
if(!portArg){
throw new Error('Port argument is required. Use -p or --port to specify the port.');
process.exit(1);
}
const PORT = parseInt(portArg, 10); // 默认端口
if (isNaN(PORT)) {
console.error(`❌ Invalid port value: "${portArg}" is not a number`);
process.exit(1); // 主动退出并报错
}
// 验证端口有效性
if (PORT < 1 || PORT > 65535) {
throw new Error('PORT less then 1 or greater than 65535');
process.exit(1);
}
return PORT;
}
const PORT = getPort();
console.log(`Starting server on port ${PORT}...`);
async function handle(req, server) {
const urlObj = new URL(req.url);
const path = urlObj.pathname;
const queryParams = urlObj.searchParams;
const fullPath = urlObj.pathname + urlObj.search;
console.log(`Handling request for ${fullPath}`);
const body = req.body ? await req.text() : null;
//用内置的response对象,自己json,可以给response加header包括Content-Type
return new Response(body);
}
const server = Bun.serve({
port: PORT,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === '/vip') {
return handle(req, server);
}
// 匹配 /vip/123
if (url.pathname.startsWith('/vip/')) {
const id = url.pathname.split('/')[2]; // 获取 ID
return handle(req, id);
}
return new Response("Not found", { status: 404 });
}
});
console.log(`Listening on ${server.url}`);
- Setp4: 启动反向代理服务
shell
$ bun run nginx-proxy.js
- Setp5: 启动后台服务
shell
$ bun run nginx-proxy-backend-server1.js -p 3000
$ bun run nginx-proxy-backend-server1.js -p 4000
- Setp5: 测试
- 后台服务的ip和端口要和nginx-proxy.js的upstream的配置对应。
- 浏览器输入 http://localhost:vip 会随机返回3000或4000。
您要是觉得我的文章有帮助,请点赞,收藏。感谢!