在 Deno 和 Bun 中使用代理:开发者完整指南

Deno 和 Bun 的原生 fetch() 默认不读取代理配置。本指南涵盖 Deno.createHttpClient、Bun fetch proxy、住宅代理轮换、SOCKS5 及生产环境最佳实践。

Using Proxies in Deno and Bun: A Code-First Guide for Modern JavaScript

为什么 Deno 和 Bun 的 fetch() 默认忽略代理

如果你正在在 Deno 和 Bun 中使用代理,你可能会发现一个令人困惑的问题:设置了 HTTP_PROXY 环境变量后,fetch() 仍然直连目标服务器。这是因为 Deno 和 Bun 都实现了 WHATWG Fetch 标准,而该标准本身不包含代理配置的概念——浏览器代理由操作系统或浏览器扩展处理,JavaScript 代码无法直接控制。

Node.js 生态通过 https-proxy-agent 等第三方库绕过了这一限制。Deno 和 Bun 选择了不同的路径:它们在运行时层面提供了原生代理支持,但需要你显式配置。本文将带你从基础配置走到生产级轮换策略,涵盖住宅代理、SOCKS5、并发会话管理和重试机制。

技术背景:Deno 与 Bun 的代理支持差异

Deno 和 Bun 都基于现代 JavaScript 引擎(V8 和 JavaScriptCore),但它们的代理实现方式截然不同。理解这些差异是正确配置 Deno proxyBun fetch proxy 的前提。

特性DenoBun
代理配置方式Deno.createHttpClient({ proxy }) 传入 fetchfetch(url, { proxy }) 直接传入
环境变量支持支持 HTTP_PROXY / HTTPS_PROXY支持 HTTP_PROXY / HTTPS_PROXY
SOCKS5 支持通过 proxy URL scheme通过 proxy URL scheme
自定义 CA 证书caCerts 选项tls 选项
连接复用HttpClient 实例复用自动连接池

两者都支持通过 HTTP_PROXYHTTPS_PROXY 环境变量配置全局代理,但在需要细粒度控制(如按请求轮换 IP)时,应优先使用 per-client 配置。

Deno 中使用 Deno.createHttpClient 配置代理

Deno 的代理配置通过 Deno.createHttpClient 创建一个可复用的 HTTP 客户端,然后将该客户端作为 fetchclient 选项传入。这种方式的核心优势是连接复用——同一个 HttpClient 实例可以跨多个请求共享 TCP 连接,减少握手开销约 200ms 每次请求。

基础示例:Deno HTTP 代理

const client = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: {
      username: "user-country-US",
      password: "pass",
    },
  },
});

const res = await fetch("https://httpbin.org/ip", { client });
const data = await res.json();
console.log("出口 IP:", data.origin);

注意 basicAuth 是独立字段,不写在 URL 里。如果你只需要无认证的代理,可以省略 basicAuth

SOCKS5 代理(端口 1080)

const socksClient = Deno.createHttpClient({
  proxy: {
    url: "socks5://gate.proxyhat.com:1080",
    basicAuth: {
      username: "user-country-DE-session-abc123",
      password: "pass",
    },
  },
});

const res = await fetch("https://httpbin.org/ip", { client: socksClient });
console.log(await res.json());

SOCKS5 代理在需要更低延迟或绕过 HTTP 代理限制时非常有用。ProxyHat 的 SOCKS5 端口为 1080

Bun 中使用 fetch proxy 配置代理

Bun 的代理配置更加简洁——直接在 fetch 调用中传入 proxy 选项即可,无需创建额外的客户端对象。参考 Bun 官方文档。这种 proxy fetch JavaScript 方式语法更接近 curl--proxy 参数。

基础示例:Bun fetch proxy

const res = await fetch("https://httpbin.org/ip", {
  proxy: "http://user-country-US:pass@gate.proxyhat.com:8080",
});
const data = await res.json();
console.log("出口 IP:", data.origin);

Bun SOCKS5 代理

const res = await fetch("https://httpbin.org/ip", {
  proxy: "socks5://user-country-JP:pass@gate.proxyhat.com:1080",
});
console.log(await res.json());

地理定位与粘性会话:在用户名中编码

ProxyHat 通过用户名字段支持地理定位和会话管理。这种设计的好处是无需额外的 API 调用或 header——所有控制逻辑都编码在认证字符串中。

地理定位

// 指定国家
const usProxy = "http://user-country-US:pass@gate.proxyhat.com:8080";

// 指定国家和城市
const berlinProxy = "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080";

// Bun 示例
const res = await fetch("https://httpbin.org/ip", {
  proxy: berlinProxy,
});

粘性会话(Sticky Session)

粘性会话确保在指定时间窗口内所有请求使用同一个出口 IP。这对于需要登录状态保持的爬虫任务至关重要——如果每次请求都换 IP,目标网站会立即检测到异常。

// 生成随机会话 ID
const sessionId = crypto.randomUUID().slice(0, 8);

// Deno 示例:同一会话复用同一 IP
const stickyClient = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: {
      username: `user-country-US-session-${sessionId}`,
      password: "pass",
    },
  },
});

// 多个请求共享同一出口 IP
const results = await Promise.all([
  fetch("https://httpbin.org/ip", { client: stickyClient }),
  fetch("https://httpbin.org/headers", { client: stickyClient }),
]);

for (const r of results) {
  console.log(await r.json());
}

HTTP_PROXY 环境变量:何时使用,何时避免

两个运行时都支持通过环境变量配置全局代理:

# 设置环境变量
export HTTP_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"
export HTTPS_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"

# Deno 和 Bun 的 fetch() 会自动读取这些变量
deno run --allow-net script.ts
bun run script.ts

环境变量方式适合开发环境或单一代理场景。但在以下情况中,应优先使用 per-client 配置:

  • 需要按请求轮换不同国家或城市的 IP
  • 需要同时管理多个粘性会话
  • 需要混合使用 HTTP 和 SOCKS5 代理
  • 需要为不同请求设置不同的超时和重试策略

住宅代理轮换:并发请求实战

对于 SERP 爬取、电商价格监控等高封禁风险场景,数据中心代理往往在几十个请求后就被目标网站封禁。住宅代理使用真实 ISP 分配的 IP 地址,封禁难度显著更高。下面是一个完整的并发轮换示例,使用 Promise.allAbortController 实现超时控制。

// Deno 示例:住宅代理池并发轮换
const PROXY_GATEWAY = "gate.proxyhat.com:8080";
const PROXY_USER = "your_user";
const PROXY_PASS = "your_pass";
const TARGETS = [
  "https://httpbin.org/ip",
  "https://httpbin.org/headers",
  "https://httpbin.org/user-agent",
  "https://httpbin.org/ip",
  "https://httpbin.org/ip",
];

// 为每个请求生成独立的粘性会话
function createProxyClient(sessionId: string, country: string) {
  return Deno.createHttpClient({
    proxy: {
      url: `http://${PROXY_GATEWAY}`,
      basicAuth: {
        username: `${PROXY_USER}-country-${country}-session-${sessionId}`,
        password: PROXY_PASS,
      },
    },
  });
}

async function fetchWithTimeout(
  url: string,
  client: Deno.HttpClient,
  timeoutMs = 10000,
) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, { client, signal: controller.signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

// 并发执行,每个请求使用不同的会话 ID
const countries = ["US", "DE", "JP", "GB", "FR"];
const tasks = TARGETS.map((url, i) => {
  const sessionId = crypto.randomUUID().slice(0, 8);
  const country = countries[i % countries.length];
  const client = createProxyClient(sessionId, country);
  return fetchWithTimeout(url, client, 10000)
    .then((data) => ({ url, success: true, data }))
    .catch((err) => ({ url, success: false, error: err.message }));
});

const results = await Promise.all(tasks);

let successCount = 0;
for (const r of results) {
  if (r.success) {
    successCount++;
    console.log(`✓ ${r.url} →`, JSON.stringify(r.data).slice(0, 80));
  } else {
    console.error(`✗ ${r.url} → ${r.error}`);
  }
}
console.log(
  `成功率: ${successCount}/${results.length} ` +
  `(${(successCount / results.length * 100).toFixed(1)}%)`
);

这个示例展示了几个关键模式:每个请求使用独立的 sessionId 以获得不同的出口 IP;AbortController 确保单个请求不会无限挂起(超时设为 10000ms);Promise.all 实现最大并发。在实际生产中,你可能需要限制并发数以避免内存压力——可以使用信号量模式或分批处理,建议控制在 50-100 个并发会话以内。

生产环境最佳实践

指数退避重试

网络请求会因各种原因失败——代理服务器重启、目标网站临时 503、DNS 解析超时等。一个健壮的重试策略应该包含指数退避和抖动(jitter),避免所有客户端同时重试造成惊群效应。

// Deno / Bun 通用:指数退避重试封装
async function fetchWithRetry(
  url: string,
  options: RequestInit & { proxy?: string } = {},
  maxRetries = 3,
  baseDelayMs = 1000,
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url, options);
      if (res.ok || attempt === maxRetries) return res;
      // 仅 5xx 错误才重试
      if (res.status < 500) return res;
    } catch (err) {
      if (attempt === maxRetries) throw err;
    }
    // 指数退避 + 随机抖动
    const delay = baseDelayMs * Math.pow(2, attempt) +
      Math.random() * 500;
    await new Promise((r) => setTimeout(r, delay));
  }
  throw new Error("max retries exceeded");
}

// Bun 使用示例
const res = await fetchWithRetry("https://httpbin.org/ip", {
  proxy: "http://user-country-US:pass@gate.proxyhat.com:8080",
}, 3, 1000);
console.log(await res.json());

自定义 CA 证书(Deno)

在需要通过 MITM 代理或使用自签名证书的企业环境中,Deno 允许你通过 caCerts 选项注入自定义 CA 证书:

const caCert = await Deno.readTextFile("./custom-ca.pem");
const client = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: { username: "user", password: "pass" },
  },
  caCerts: [caCert],
});
const res = await fetch("https://internal-api.local/data", { client });

连接复用与资源清理

Deno 中复用同一个 HttpClient 实例可以显著降低延迟——避免每次请求都重新建立 TCP 连接和 TLS 握手。对于需要同一出口 IP 的批量请求,应创建一个 client 并在所有请求中共享。对于需要轮换 IP 的场景,则为每个会话创建一个 client,使用后调用 client.close() 释放资源,避免文件描述符泄漏。

ProxyHat Node SDK:在 Deno 和 Bun 中通用

如果你已经在使用 Node.js 生态的库,ProxyHat 的 Node SDK 也可以在 Deno 和 Bun 中运行。下面是原生 fetch 与 SDK 的对比:

特性原生 fetch + proxyProxyHat Node SDK
代理配置手动编码用户名参数SDK 自动处理轮换和会话
重试逻辑需自行实现内置重试与退避
IP 轮换手动管理会话 ID 池SDK 自动轮换
运行时兼容性Deno / Bun 原生需 npm 兼容层
控制粒度完全可控封装层抽象
// ProxyHat Node SDK 在 Bun 中的使用示例
import { ProxyHat } from "proxyhat-sdk";

const proxy = new ProxyHat({
  username: "your_user",
  password: "your_pass",
  gateway: "gate.proxyhat.com",
  port: 8080,
  type: "residential",
});

// SDK 自动管理会话和轮换
const result = await proxy.fetch("https://httpbin.org/ip", {
  country: "US",
  session: "my-session-001",
});
console.log(await result.json());

// 对比:Bun 原生方式
const nativeRes = await fetch("https://httpbin.org/ip", {
  proxy: "http://user-country-US-session-my-session-001:pass@gate.proxyhat.com:8080",
});
console.log(await nativeRes.json());

选择 SDK 还是原生方式取决于你的需求:如果需要最大控制和最小依赖,原生方式更合适;如果希望减少样板代码并利用内置重试逻辑,SDK 是更好的选择。查看 ProxyHat 文档了解更多 SDK 细节。

伦理与合规:爬取前必读

代理技术是一把双刃剑。在使用代理进行数据采集时,必须遵守以下原则:

  • 优先使用官方 API:如果目标网站提供公开 API,应优先使用 API 而非爬取。API 通常有更稳定的接口、更清晰的使用条款和更低的被封禁风险。
  • 仅采集公开数据:不绕过登录墙、不访问需要认证才能查看的内容。在美国,《计算机欺诈和滥用法》(CFAA)对未授权访问计算机系统有严格规定。
  • 遵守 robots.txt:robots.txt 是网站对爬虫的明确指令,应予以尊重。
  • 遵守 GDPR:在欧盟,采集涉及个人身份信息的数据需遵守《通用数据保护条例》。确保你有合法依据处理个人数据。
  • 控制请求频率:即使使用住宅代理,也应设置合理的请求间隔(如 200ms-1000ms),避免对目标服务器造成压力。

更多关于合规爬取的实践,请参考我们的 Web 爬取用例指南SERP 追踪用例

关键要点

  • Deno 通过 Deno.createHttpClient({ proxy }) 配置代理,Bun 通过 fetch(url, { proxy }) 直接配置——两者都支持 HTTP 和 SOCKS5。
  • ProxyHat 的地理定位和会话管理编码在用户名字段中:user-country-US-session-abc123,无需额外 API 调用。
  • HTTP 代理端口 8080,SOCKS5 端口 1080,网关地址始终为 gate.proxyhat.com
  • 住宅代理在高封禁场景下成功率可达 99% 以上,远优于数据中心代理。
  • 生产环境必须实现重试机制、超时控制和连接复用——指数退避 + 抖动是最佳实践。
  • 伦理爬取:优先使用官方 API,仅采集公开数据,遵守 CFAA 和 GDPR。

准备好开始了吗?查看 ProxyHat 定价方案全球代理位置,选择适合你项目的代理类型。

准备开始了吗?

通过AI过滤访问148多个国家的5000多万个住宅IP。

查看价格住宅代理
← 返回博客