为什么Node.js加代理不只是"填个参数"?
直接说结论:Node.js生态里至少有5种主流HTTP客户端库,每种库的代理接入方式都不一样,没有统一的"填一个字段就通用"的写法。
以最常见的axios为例,它本身不内置SOCKS5支持,需要额外引入agent库;got的代理配置和axios完全不同;node-fetch又是另一套;Puppeteer和Playwright作为无头浏览器,代理注入走的是启动参数而非请求参数。
更关键的是,"能连上"只是第一步。企业级采集任务中,代理配置还要解决以下问题:
| 问题 | 后果 |
|---|---|
| 鉴权方式未正确集成 | 全部请求返回407,IP资源完全浪费 |
| 未做IP轮换 | 单IP请求过多被限制,成功率骤降 |
| 连接池未复用 | 每个请求新建TCP连接,延迟翻倍 |
| 异常处理缺失 | 单个代理故障导致整个采集任务卡死 |
| SOCKS5未配agent | 请求未走代理通道,访问环境暴露 |
本文按"HTTP客户端库 → 无头浏览器 → 鉴权 → IP轮换 → 异常处理"的顺序,逐一给出可直接复用的代码方案。
主流HTTP客户端库怎么接入代理?
Node.js生态里用得最多的HTTP客户端是axios、got、node-fetch和undici。四种库的代理配置方式差异明显,下表先做速查,后面逐一给代码。
| 客户端 | 代理配置方式 | SOCKS5原生支持 | 适合场景 |
|---|---|---|---|
| axios | 通过httpAgent/httpsAgent注入 | 否,需socks-proxy-agent | API接口采集、结构化数据抓取 |
| got | 通过agent选项注入 | 否,需socks-proxy-agent | 流式下载、大文件采集 |
| node-fetch | 通过agent参数注入 | 否,需socks-proxy-agent | 轻量级请求、Serverless环境 |
| undici | 通过ProxyAgent内置支持 | 否 | 高性能并发、Node.js 18+原生场景 |
axios配置代理
axios是Node.js最流行的HTTP客户端,但它的代理配置有一个常见坑:proxy配置项只支持HTTP代理,且在某些版本中行为不一致。推荐用HttpsProxyAgent方式接入,兼容性更好。
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
// HTTP/HTTPS代理
const agent = new HttpsProxyAgent('http://user:pass@proxy-host:port');
const response = await axios.get('https://target-site.com/api/data', {
httpsAgent: agent,
httpAgent: agent,
timeout: 15000
});SOCKS5代理需要换用socks-proxy-agent:
const { SocksProxyAgent } = require('socks-proxy-agent');
const agent = new SocksProxyAgent('socks5://user:pass@proxy-host:1080');
const response = await axios.get('https://target-site.com/api/data', {
httpsAgent: agent,
httpAgent: agent,
timeout: 15000
});got配置代理
got从v12开始是纯ESM模块,代理配置通过agent选项注入。
import got from 'got';
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent('http://user:pass@proxy-host:port');
const response = await got('https://target-site.com/api/data', {
agent: { https: agent, http: agent },
timeout: { request: 15000 }
});undici配置代理
undici是Node.js 18+内置的HTTP客户端底层,性能显著优于axios和got。它内置了ProxyAgent,不需要额外安装代理库。
const { ProxyAgent, fetch } = require('undici');
const proxyAgent = new ProxyAgent('http://user:pass@proxy-host:port');
const response = await fetch('https://target-site.com/api/data', {
dispatcher: proxyAgent
});选型建议: 新项目且Node.js版本在18以上,优先用undici,原生支持、性能好、代理配置简单。存量项目用axios的,不需要迁移,按上面的agent方式接入即可。
无头浏览器怎么注入代理?
网站采集器场景中,部分目标站点需要渲染JavaScript才能获取完整数据,这时候需要用Puppeteer或Playwright。无头浏览器的代理配置和HTTP客户端完全不同:代理地址在浏览器启动时通过命令行参数传入,而非在请求层面设置。
Puppeteer代理配置
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
args: [
'--proxy-server=http://proxy-host:port'
]
});
const page = await browser.newPage();
// 如果代理需要账密认证
await page.authenticate({
username: 'proxy_user',
password: 'proxy_pass'
});
await page.goto('https://target-site.com', {
waitUntil: 'networkidle2',
timeout: 30000
});Playwright代理配置
Playwright的代理配置比Puppeteer更简洁,直接在launch选项里传入proxy对象。
const { chromium } = require('playwright');
const browser = await chromium.launch({
proxy: {
server: 'http://proxy-host:port',
username: 'proxy_user',
password: 'proxy_pass'
}
});
const page = await browser.newPage();
await page.goto('https://target-site.com');两者对比:
| 维度 | Puppeteer | Playwright |
|---|---|---|
| 代理配置方式 | 启动参数 + page.authenticate | launch选项内置proxy对象 |
| SOCKS5支持 | --proxy-server=socks5://host:port | server: 'socks5://host:port' |
| 按页面切换代理 | 需要创建新的Browser实例 | 支持BrowserContext级别的代理隔离 |
| 推荐场景 | 轻量级页面采集 | 需要多代理并行的复杂采集 |
关键细节: Puppeteer的--proxy-server参数对整个Browser实例生效,如果舆情监测任务需要不同页面走不同代理,每个代理需要启动一个独立的Browser实例。Playwright可以通过BrowserContext实现代理隔离,资源开销更小。
鉴权方式在Node.js里怎么实现?
三种主流鉴权方式在Node.js中的实现方式差异较大,选错方式会导致请求全量失败。
| 鉴权方式 | 适用库 | 实现方式 |
|---|---|---|
| IP白名单 | 所有库 | 无需代码改动,在代理平台控制台添加服务器出口IP |
| 账密认证 | axios/got/undici | 写在代理URL里:http://user:pass@host:port |
| 账密认证 | Puppeteer | page.authenticate({username, password}) |
| 账密认证 | Playwright | proxy: {server, username, password} |
| API Token | 所有库 | 先调API获取IP列表,再用返回的IP直连 |
API Token鉴权的Node.js实现模式:
async function getProxyList(apiUrl, token) {
const response = await fetch(`${apiUrl}?token=${token}&count=10`);
const data = await response.json();
// 返回格式通常是 [{ip: "1.2.3.4", port: 8080}, ...]
return data.list;
}
// 从IP列表中取一个代理使用
const proxyList = await getProxyList(API_URL, process.env.PROXY_TOKEN);
const proxy = proxyList[0];
const agent = new HttpsProxyAgent(`http://${proxy.ip}:${proxy.port}`);凭证安全要点:
- 代理账密通过环境变量传入,不硬编码在代码里
- API Token存入
.env文件,并加入.gitignore - CI/CD环境用密钥管理服务注入凭证
IP轮换逻辑怎么写?
拓客数据、舆情监测等场景下,单IP持续请求很快会触发目标站点的访问频率控制。IP轮换是保证长时间采集任务成功率的核心机制。
基础轮换器实现:
class ProxyRotator {
constructor(proxyList) {
this.proxyList = proxyList;
this.index = 0;
this.cooldownMap = new Map(); // 冷却池
}
getNext() {
let attempts = 0;
while (attempts < this.proxyList.length) {
const proxy = this.proxyList[this.index % this.proxyList.length];
this.index++;
// 跳过冷却中的IP
const cooldownUntil = this.cooldownMap.get(proxy);
if (cooldownUntil && Date.now() < cooldownUntil) {
attempts++;
continue;
}
return proxy;
}
return null; // 所有IP都在冷却中
}
// 将IP放入冷却池
cooldown(proxy, durationMs = 10 * 60 * 1000) {
this.cooldownMap.set(proxy, Date.now() + durationMs);
}
}与请求逻辑的集成:
const rotator = new ProxyRotator(proxyList);
async function fetchWithRotation(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const proxy = rotator.getNext();
if (!proxy) {
await sleep(5000); // 所有IP冷却中,等待5秒
continue;
}
try {
const agent = new HttpsProxyAgent(proxy);
const response = await axios.get(url, {
httpsAgent: agent,
timeout: 15000
});
if (response.status === 200) return response.data;
} catch (error) {
if (error.response?.status === 429 || error.response?.status === 403) {
rotator.cooldown(proxy); // 被限制的IP进冷却池
}
}
}
throw new Error(`Failed after ${maxRetries} retries: ${url}`);
}轮换策略选型:
| 策略 | 实现方式 | 适合场景 |
|---|---|---|
| 顺序轮换 | 按索引依次取用 | IP池较小、目标站点限制宽松 |
| 随机轮换 | 随机选取 | IP池较大、需要分散请求模式 |
| 加权轮换 | 按成功率分配权重 | 长期运行的采集任务,自动淘汰低质量IP |
| 域名隔离轮换 | 不同目标域名用不同IP子池 | 多站点并行采集 |
异常处理和重试机制怎么做?
代理IP场景下的异常比普通HTTP请求更复杂。除了目标站点返回的HTTP错误码,还有代理本身的连接超时、鉴权失败、代理服务端不可用等异常类型。
异常分类与处理策略:
| 异常类型 | 典型错误 | 处理策略 |
|---|---|---|
| 代理连接失败 | ECONNREFUSED、ETIMEDOUT | 切换IP重试,原IP标记故障 |
| 代理鉴权失败 | HTTP 407 | 检查凭证配置,不重试 |
| 目标站点限制 | HTTP 429、403 | 当前IP进冷却池,切换新IP重试 |
| 目标站点异常 | HTTP 500、502、503 | 保留当前IP,延迟后重试 |
| 读取超时 | ESOCKETTIMEDOUT | 延长超时或切换IP重试 |
| DNS解析失败 | ENOTFOUND | 检查代理地址是否正确,不重试 |
带分类处理的重试封装:
async function resilientFetch(url, rotator, options = {}) {
const { maxRetries = 3, retryDelay = 1000 } = options;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const proxy = rotator.getNext();
if (!proxy) throw new Error('No available proxies');
try {
const agent = new HttpsProxyAgent(proxy);
const response = await axios.get(url, {
httpsAgent: agent,
timeout: 15000
});
return response.data;
} catch (error) {
const status = error.response?.status;
const code = error.code;
// 不可重试错误:鉴权失败、DNS解析失败
if (status === 407 || code === 'ENOTFOUND') {
throw error;
}
// 需要切换IP的错误
if (status === 429 || status === 403 ||
code === 'ECONNREFUSED' || code === 'ETIMEDOUT') {
rotator.cooldown(proxy);
}
// 可保留IP重试的错误:5xx
if (status >= 500) {
await sleep(retryDelay * (attempt + 1)); // 递增延迟
continue;
}
}
}
throw new Error(`All retries exhausted for ${url}`);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}并发控制建议: Node.js的异步特性很容易写出高并发采集逻辑,但并发数不加控制会导致代理IP被快速消耗。推荐使用p-limit或自定义信号量控制并发:
const pLimit = require('p-limit');
const limit = pLimit(10); // 最大10并发
const urls = [/* 待采集URL列表 */];
const results = await Promise.all(
urls.map(url => limit(() => resilientFetch(url, rotator)))
);广告监测等需要稳定持续运行的场景,建议并发数控制在5-15之间,配合IP轮换周期和冷却时间综合调优。
FAQ
Q:axios的proxy配置项和httpsAgent方式有什么区别?
axios内置的proxy配置项底层走的是HTTP CONNECT隧道,在某些版本中对HTTPS站点支持不稳定。用httpsAgent传入HttpsProxyAgent实例的方式更通用,兼容性更好,也更容易切换SOCKS5代理。新项目建议统一用agent方式。
Q:Node.js 18以上是不是可以不装axios直接用fetch?
可以。Node.js 18引入了基于undici的全局fetch,配合undici的ProxyAgent就能实现代理请求,不需要额外安装axios或got。但undici的ProxyAgent目前不支持SOCKS5,如果业务需要SOCKS5协议,仍然需要引入socks-proxy-agent。
Q:Puppeteer怎么实现每个页面用不同的代理?
Puppeteer的代理参数在Browser级别生效,无法按Page切换。要实现不同页面走不同代理,需要为每个代理启动一个独立的Browser实例。如果这种需求频繁,建议改用Playwright,它支持BrowserContext级别的代理隔离,同一个Browser实例下可以创建多个Context各自走不同代理。
Q:IP轮换时怎么判断一个IP是该冷却还是该永久淘汰?
建议用计数器区分。单个IP在短时间内连续触发3次以上429或403,放入冷却池等待10-30分钟后复用。如果冷却后首次请求仍然失败,标记为永久淘汰,从IP池移除。代理平台通常有IP池自动刷新机制,被淘汰的IP会在下一轮刷新时被新IP替换。
Q:SOCKS5代理在Node.js里是不是比HTTP代理更难配?
配置复杂度略高一步:所有主流HTTP客户端库都不原生支持SOCKS5,需要额外安装socks-proxy-agent,然后用SocksProxyAgent替换HttpsProxyAgent。除了agent实例化的URL前缀从http://改成socks5://,其余代码完全一致。
Q:采集任务跑到一半代理突然全部失效怎么处理?
在IP轮换器里加一个"全池冷却"检测:当getNext()返回null时,说明所有IP都在冷却中。这时有两个策略——等待最近一个IP冷却结束后继续,或者调用代理平台的API重新拉取一批新IP。建议在调度层做告警,连续5分钟无可用IP时触发通知,避免任务静默失败。
Q:并发数设多少合适?
取决于代理IP池的大小和目标站点的访问频率控制策略。经验值:IP池50个以下,并发控制在5-10;IP池100-500个,并发可到10-30;IP池500个以上,并发可到30-50。核心原则是单IP的QPS不超过目标站点的限制阈值,并发数 × 单请求耗时 / IP池大小 ≈ 单IP的实际QPS。
