@iss-ai/browser-pool
v0.0.6
Published
`BrowserPool` 是一个用于管理和调度无头浏览器(Headless Browser)资源的高效任务池,基于 [Playwright](https://playwright.dev/) 实现。它专门为高并发爬虫、自动化测试、AI 会话调度等场景设计,支持多工作节点(Worker)复用、超时控制、状态隔离、持久化用户状态以及动态适配不同的浏览器内核(如 Chromium, Camoufox 等)。
Maintainers
Readme
BrowserPool 浏览器任务池
BrowserPool 是一个用于管理和调度无头浏览器(Headless Browser)资源的高效任务池,基于 Playwright 实现。它专门为高并发爬虫、自动化测试、AI 会话调度等场景设计,支持多工作节点(Worker)复用、超时控制、状态隔离、持久化用户状态以及动态适配不同的浏览器内核(如 Chromium, Camoufox 等)。
主要特性
- 高并发与复用:预启动和复用多个浏览器 Worker,避免频繁开启和关闭浏览器进程带来的资源消耗。
- 多内核支持:原生支持 Playwright 提供的 Chromium、Firefox、Webkit,并支持通过配置动态加载专门为反爬优化的
camoufox引擎。 - 状态隔离与持久化:
- 极致隔离(默认):每个任务执行前会自动重置隐身上下文,确保任务之间数据(Cookies/LocalStorage)互不污染。
- 状态注入:支持通过
storageState为所有隔离的任务注入相同的账号 Cookies。 - 持久化模式:支持通过
userDataDir绑定本地文件夹,自动保存和维持扫码登录等持久化状态。
- 异常容灾:自动监测 Worker 内页面的崩溃或超时。当发生异常时,会自动销毁该节点并在后台无缝替换为健康的新 Worker。
接口说明 (API Reference)
1. BrowserPoolOptions 配置接口
在创建 BrowserPool 时传入的配置参数:
export interface BrowserPoolOptions {
/** 最大并发工作节点 (页面) 数量,默认为 2 */
maxWorkers?: number;
/** 最小保持待命的闲置节点数量,默认为 0 */
minWorkers?: number;
/** 浏览器引擎类型: 'chromium', 'firefox', 'webkit' 或 'camoufox',默认为 'chromium' */
browserType?: 'chromium' | 'firefox' | 'webkit' | 'camoufox';
/**
* 传递给底层引擎的启动参数 (等价于 Playwright 的 LaunchOptions)。常用参数包括:
* - `headless`: boolean (是否为无头模式,默认 true)
* - `args`: string[] (传递给浏览器进程的附加命令行参数,例如扩展插件配置)
* - `proxy`: { server, username, password } (全局代理配置)
* - `executablePath`: string (自定义浏览器可执行文件的绝对路径)
* - `timeout`: number (等待浏览器启动的最大超时时间)
*/
launchOptions?: LaunchOptions;
/** 单个任务执行的最大超时时间(毫秒),超时将强制中断任务并重建 Worker,默认 30000ms */
taskTimeoutMs?: number;
/** 等待获取空闲 Worker 的最大排队超时时间(毫秒),默认 60000ms */
acquireTimeoutMs?: number;
/** Worker 空闲超过此时间(毫秒)且数量大于 minWorkers 时将被自动销毁,默认 60000ms */
idleTimeoutMs?: number;
/** 可选:持久化用户数据目录。开启后浏览器将记住所有的 Cookies/缓存,多 Worker 之间将共享状态,不再保持强隔离。 */
userDataDir?: string;
/** 可选:状态注入文件路径或 JSON 对象。在无痕隔离模式下,用于注入预先保存的登录态 Cookie。 */
storageState?: string | { cookies: any[]; origins: any[] };
/** 动态上下文配置解析器。用于在创建每个 Worker 时,动态提供代理、User-Agent、分辨率等,实现指纹隔离。 */
getContextOptions?: () => BrowserContextOptions | Promise<BrowserContextOptions>;
/** 自动拦截并丢弃指定的资源类型,例如 ['image', 'media'] 以提升加载速度并节省带宽。 */
blockAssets?: ('image' | 'stylesheet' | 'media' | 'font' | 'script' | 'document')[];
/** 是否为 Chromium 引擎启用 Stealth 反反爬插件伪装(默认: false)。 */
stealth?: boolean;
/** 是否开启静态资源(JS/CSS/Image)的内存级 LRU 缓存加速(默认: false)。 */
cacheAssets?: boolean;
/** 全局任务执行间隔(毫秒),用于严格限流,防止瞬时高并发被封 IP(默认: 0)。 */
delayBetweenTasksMs?: number;
/** 对空闲 Worker 进行僵尸进程心跳检测的时间间隔(毫秒),默认 30000ms。 */
healthCheckIntervalMs?: number;
/** 自定义任务队列提供者,默认使用内部单机 `MemoryQueueProvider`。 */
queueProvider?: IQueueProvider<any>;
/** 智能预热缓冲池大小。当空闲 Worker 低于该值时会自动在后台预启动浏览器,实现 0ms 等待(默认: 0)。 */
preWarmBuffer?: number;
/** 分布式队列提供者(如 Redis)。开启后系统将转入 Pub/Sub 分布式模式,接受跨机器的任务调度。 */
distributedQueueProvider?: IDistributedQueueProvider;
/**
* 内置 Redis 队列提供者的连接 URL。配置后自动开启分布式 MQ 模式。
* 支持带有密码的 URL,例如:'redis://:[email protected]:6379'
*/
redisUrl?: string;
/** 内置 Redis 队列提供者的额外配置项(对应 ioredis 的 RedisOptions)。 */
redisOptions?: RedisOptions;
/**
* 是否开启 Redis 队列的可靠模式 (At-Least-Once Delivery)。
* 如果设为 true,引擎将使用双队列和 ACK 机制确保哪怕断电宕机也不丢任务。
*/
redisReliableMode?: boolean;
/**
* 是否在每次任务前彻底销毁并重建独立的 BrowserContext (默认: true)。
* 如果设为 false,则进入极速软隔离模式:复用上下文,仅清理 Cookies 和新建标签页。
*/
strictIsolation?: boolean;
/**
* 自动注册进程退出信号 (SIGINT/SIGTERM) 并执行优雅停机。
* 可以传入 true (默认等待 10000ms),也可以传入具体的超时毫秒数。
*/
autoGracefulShutdown?: boolean | number;
}2. BrowserPool 类
constructor(options?: BrowserPoolOptions)
构造函数,实例化浏览器任务池。此时不会立即启动浏览器进程。
async init(): Promise<void>
初始化连接池。拉起浏览器引擎,并创建指定数量(maxWorkers)的空闲 Worker。一般无需手动调用,在第一次执行 execute 时会自动触发。
async execute<T>(task: (worker: Worker) => Promise<T>, options?: ExecuteOptions): Promise<T>
将任务派发给任务池,排队等待空闲的 Worker 并执行。
- 参数:
task为回调函数,接收一个当前分配给该任务的健康Worker对象。options(可选) 为重试和优先级配置,例如{ retries: 3, retryDelayMs: 2000, priority: 10 }。
- 返回值: 返回该任务回调中最终执行完成并返回的结果。
- 超时机制: 受
acquireTimeoutMs(排队超时) 和taskTimeoutMs(执行超时) 双重控制。
registerAction<P, R>(action: string, handler: ActionHandler<P, R>)
预先注册一个命名动作(Action)的爬取逻辑。专为微服务或分布式架构设计。
- 参数:
action: 任务名称(字符串)。handler: 具体的爬虫处理回调(worker, payload) => Promise<R>。
async dispatch<P, R>(action: string, payload: P, options?: ExecuteOptions): Promise<R | string>
派发一个已被 registerAction 注册的任务。
- 如果未配置
distributedQueueProvider:该任务将直接放入本地内存队列,其行为与execute完全一致,并最终返回 Promise 的执行结果。 - 如果配置了
distributedQueueProvider:任务参数将被序列化并压入外部 MQ。该调用将立即返回一个由于推送而产生的标志字符(如 Job ID),具体的执行由后台的所有跨机消费者节点接管。
async destroy(options?: { gracefulTimeoutMs?: number }): Promise<void>
销毁并关闭连接池,彻底终止底层浏览器进程,清空排队队列。
- 参数:
options.gracefulTimeoutMs(可选) 如果提供该参数,框架将拒绝新任务,并等待最多指定时间让正在运行的 Worker 结束它们的当前任务,实现优雅停机。
async stats(): Promise<{ totalWorkers: number; idleWorkers: number; busyWorkers: number; localQueueLength: number; }>
返回当前浏览器池的实时运行监控数据,非常适合用于对接 Grafana 或 Prometheus 等监控大盘。
使用示例
示例 1: 基础无痕并发(强隔离,不带 Cookie)
适用于需要完全干净的运行环境、无需账号登录的场景。
import { BrowserPool } from './src/pool/BrowserPool';
async function main() {
const pool = new BrowserPool({
maxWorkers: 3,
browserType: 'chromium', // 使用默认内核
launchOptions: { headless: true },
});
// 并发派发 3 个独立任务
const results = await Promise.all([
pool.execute(async worker => {
await worker.page!.goto('https://example.com/1');
return worker.page!.title();
}),
pool.execute(async worker => {
await worker.page!.goto('https://example.com/2');
return worker.page!.title();
}),
pool.execute(async worker => {
await worker.page!.goto('https://example.com/3');
return worker.page!.title();
}),
]);
console.log('Results:', results);
await pool.destroy();
}示例 2: 使用 Camoufox 与状态注入(带 Cookie 但互相隔离)
适用于需要使用高级反爬引擎,并且多个并发任务都需要携带预先登录好的 Cookie。
import { BrowserPool } from './src/pool/BrowserPool';
async function main() {
const pool = new BrowserPool({
maxWorkers: 2,
browserType: 'camoufox', // 动态加载 camoufox-js,强化反反爬能力
storageState: './cookies.json', // 注入提前保存的登录状态文件
launchOptions: { headless: false },
});
await pool.execute(async worker => {
// 此时的 worker.page 已经携带了 cookies.json 中的登录态,但不会污染其他并发任务
await worker.page!.goto('https://google.com');
// 执行操作...
});
await pool.destroy();
}示例 3: 持久化目录模式(共享状态,自动记录登录)
最简单的人工辅助模式:像日常浏览器一样,第一次打开时可人工干预扫码登录,后续所有的重启和执行都会持久化保留这个登录状态。所有任务共享相同的登录账号。
import { BrowserPool } from './src/pool/BrowserPool';
async function main() {
const pool = new BrowserPool({
maxWorkers: 1, // 这种模式下通常配置为 1 做队列处理即可
userDataDir: './user_data_profile', // 指定一个本地文件夹存放持久化数据
launchOptions: { headless: false }, // 必须显示浏览器,以便人类进行扫码/点击确认
});
await pool.execute(async worker => {
await worker.page!.goto('https://google.com');
// 如果没有登录,你可以在这里通过 await worker.page.waitForTimeout(60000) 给自己扫码时间
// 一旦扫码成功,所有的缓存、Session 都会永久保留在 './user_data_profile' 中
});
}示例 4: 配置代理 (Proxy)
无论是 chromium, firefox 还是 camoufox 引擎,代理的配置方式都是一致的,直接在 launchOptions 中传入 proxy 即可。
const pool = new BrowserPool({
maxWorkers: 1,
browserType: 'chromium',
launchOptions: {
proxy: {
server: 'http://myproxy.com:3128',
username: 'my_username',
password: 'my_password',
},
},
});示例 5: 加载扩展插件 (Extensions)
Chromium 引擎 (推荐)
支持通过 args 参数直接加载本地未打包的扩展。注意:必须配置持久化目录(userDataDir) 并且通常需要关闭无头模式。
import path from 'path';
const pathToExtension = path.join(__dirname, 'my-chrome-extension');
const pool = new BrowserPool({
maxWorkers: 1,
userDataDir: './user_data_profile',
launchOptions: {
headless: false,
args: [`--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`],
},
});Firefox / Camoufox 引擎
由于 Playwright 对 Firefox 扩展原生支持有限,不支持动态命令行加载。如果必须在 Firefox 引擎中使用插件,推荐使用 预配置 Profile (用户目录) 方案:
- 手动打开正常的 Firefox/Camoufox。
- 建立一个固定的 Profile,并在其中手动安装好需要的
.xpi插件。 - 在代码的
userDataDir中指定这个已经安装好插件的 Profile 目录。
const pool = new BrowserPool({
browserType: 'camoufox',
userDataDir: './my_preconfigured_firefox_profile', // 指向你预先配置好的目录
launchOptions: {
headless: false,
},
});示例 6: 深层特征与 IP 指纹隔离 (Context Isolation)
如果需要防止 IP 和设备特征被封禁,你可以提供 getContextOptions 方法。非持久化模式下,每次创建新的并发 Worker 时,都会调用此方法动态注入一套全新的环境指纹(包括代理 IP、User-Agent、分辨率、语言等)。
const pool = new BrowserPool({
maxWorkers: 5,
// 动态返回一套全新的浏览器指纹,让并发的每个 Worker 都是完全独立的真实设备
getContextOptions: async () => {
return {
proxy: { server: await fetchMyProxyIpFromProvider() },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...', // 随机 UA
viewport: { width: 1920, height: 1080 }, // 随机分辨率
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
};
},
});注意:该功能在配置了 userDataDir(持久化模式)时由于强制环境共享,将不起作用。
示例 7: 任务自动重试与优先级队列 (Task Retry & Priority)
在执行不稳定网络环境的爬取任务时,可以使用重试和优先级控制。
// 提交一个高优先级、最多重试3次的任务
pool
.execute(
async worker => {
await worker.page!.goto('https://example.com/unstable-page');
// ... 其他操作
},
{
retries: 3, // 失败最多自动重试3次
retryDelayMs: 2000, // 每次重试前间隔2秒
priority: 10, // 优先级(数字越大越优先出队执行)
}
)
.then(res => {
console.log('执行成功', res);
})
.catch(err => {
console.error('重试3次依然失败', err);
});示例 8: 按需拉起与弹性缩容 (Lazy Init & Auto Scaling)
通过配置弹性参数,可以避免启动时占用大量空闲内存。
const pool = new BrowserPool({
minWorkers: 1, // 初始化时只拉起 1 个 Worker 待命
maxWorkers: 10, // 任务高峰期,最多自动扩容到 10 个并发 Worker
idleTimeoutMs: 60000, // 当 Worker 闲置超过 60 秒且当前 Worker 数大于 minWorkers 时,自动销毁释放资源
});示例 9: 自动资源拦截加速 (Asset Blocking)
如果你只关心页面数据而不需要渲染图片或样式,可以通过内置的 blockAssets 极大提升页面加载速度和节省带宽。
const pool = new BrowserPool({
maxWorkers: 3,
// 框架层自动注入 page.route,拦截并中止这几类请求
blockAssets: ['image', 'stylesheet', 'media', 'font'],
});示例 10: 智能预热池 (Warm-up Buffer)
如果你的业务对请求响应时间要求极高,开启预热池可以提前在后台启动空闲浏览器页签。当新任务到达时可以达到 0ms 启动延时。
const pool = new BrowserPool({
maxWorkers: 10,
preWarmBuffer: 2, // 只要系统内的空闲 Worker 数量少于 2,且未达到 maxWorkers 限制,就会在后台自动创建新的 Worker 待命
});示例 11: 分布式架构 Action (Pub/Sub Pattern)
BrowserPool 支持注册跨机器的分布式任务。你可以通过向 registerAction 注册处理器,并使用 dispatch 发送 JSON 格式的任务载荷,这完美兼容外挂式的 Redis 或 MQ。如果不外挂任何 MQ,默认会在单机内存中运行,与 execute 的行为一致。
// 1. 预先注册 Action 处理器
pool.registerAction('scrape_page', async (worker, payload: { url: string }) => {
await worker.page!.goto(payload.url);
return await worker.page!.title();
});
// 2. 派发任务 (单机环境下会返回执行结果,分布式环境下会立即压入 MQ)
const result = await pool.dispatch(
'scrape_page',
{ url: 'https://example.com' },
{
priority: 10, // 同样支持优先级等选项
}
);
console.log(result);如何外挂真实的 Redis/MQ 队列:
由于我们内置了强大的 RedisQueueProvider,你完全不需要自己写任何接口代码!只需要在初始化时传入 redisUrl,底层就会自动实例化一套基于 ioredis 的双工长轮询机制,接管整个集群的任务分发与消费。
const pool = new BrowserPool({
maxWorkers: 5,
/**
* 内置 Redis 队列提供者的连接 URL。配置后自动开启分布式 MQ 模式。
* 支持带有密码的 URL,例如:'redis://:[email protected]:6379'
*/
redisUrl: 'redis://127.0.0.1:6379',
/** 内置 Redis 队列提供者的额外配置项(对应 ioredis 的 RedisOptions)。 */
redisOptions: {
// ...
},
});如果你使用的是除 Redis 外的其他非标 MQ (如 RabbitMQ, Kafka),依然可以通过传入实现了 IDistributedQueueProvider 的 distributedQueueProvider 参数进行自定义外挂。
⚠️ 特别说明:任务路由机制
当你配置了 Redis 参数开启分布式模式后,任务的流向取决于你调用的 API:
- 走 Redis 全局分发 (
dispatch):只要你调用了pool.dispatch(action, payload),任务就会自动序列化并推入 Redis,由整个集群池共同竞争消费。 - 只走本地执行 (
execute):如果你调用了pool.execute(async worker => { ... }),因为传递的是原生 JS 函数(无法跨机器序列化),这些任务绝对不会进入 Redis,而是像以前一样只在你当前的 Node.js 进程本地内存中排队执行。
这种设计让你可以混用分布式架构与本地快捷脚本,两者互不干扰!
示例 12: 无头环境终极隐身伪装 (Stealth Plugin)
原生的 Chromium 容易被 Cloudflare 等盾牌识别。如果开启 stealth,框架会在底层自动集成 playwright-extra 和 puppeteer-extra-plugin-stealth 进行指纹擦除(需确保项目安装了这两个依赖)。
const pool = new BrowserPool({
browserType: 'chromium',
stealth: true, // 启动 Stealth 反检测机制
});示例 13: 全局生命周期与监控事件流 (EventEmitter)
BrowserPool 继承了原生的 EventEmitter,你可以轻松外挂各类监控探针或日志采集系统。
pool.on('worker:created', worker => console.log('新 Worker 上线'));
pool.on('worker:zombie', worker => console.warn('检测到无响应的僵尸进程!'));
pool.on('task:success', (task, result) => console.log('任务爬取成功', result));
pool.on('task:fail', (task, error) => console.error('任务彻底失败', error));
pool.on('pool:exhausted', () => console.log('队列所有任务已清空'));示例 14: 静态请求拦截与内存级加速 (Asset In-Memory Cache)
爬取不同页面时常常需要加载同样的公共库(如 jQuery / Vue 等)。配合资源拦截功能,你可以开启 cacheAssets。
const pool = new BrowserPool({
// 首个页面下载静态文件后,会存入 LRU 内存缓存。后续其他并发请求直接 0ms 拦截返回,不走网络!
cacheAssets: true,
blockAssets: ['image', 'media'], // 依然阻断图片和视频
});示例 15: 全局防封并发限流锁 (Global Rate Limiting)
为了避免池子因为空闲 Worker 太多而造成瞬时高并发被目标网站封 IP,可以设置严格的请求间隔。
const pool = new BrowserPool({
maxWorkers: 20,
// 无论有多少个 Worker 闲置待命,向网站派发任务的时间间隔至少为 2000 毫秒
delayBetweenTasksMs: 2000,
});示例 16: 僵尸进程健康度探针 (Zombie Health Check)
针对爬虫代码因未知的异步错误卡死、内存泄露或无限阻塞等“假死”情况,框架提供自带的心跳扫描。
const pool = new BrowserPool({
// 每 30 秒巡查一次,向空闲 Worker 发送 evaluate('1')。
// 若 3 秒未响应,主动判定为僵尸,将其猎杀并重建全新的 Worker 替补!
healthCheckIntervalMs: 30000,
});示例 17: 极速复用软隔离 (Fast Isolation)
当你的爬虫任务非常简单且并发量极大(例如:爬取纯公开数据、不需要登录、不需要频繁换 IP 和 LocalStorage),你可以关闭严格隔离。
关闭后,引擎不会频繁销毁并重建 BrowserContext,而是直接复用上下文,仅清空 Cookies 并新建标签页,能将任务切换的引擎损耗从 ~15ms 压缩到 ~1ms。
const pool = new BrowserPool({
maxWorkers: 10,
strictIsolation: false, // 开启极速软隔离模式
});示例 18: 优雅停机 (Graceful Shutdown)
在微服务或云原生环境下,当进程收到 SIGTERM 退出信号时,为了防止正在执行爬取任务的 Worker 被暴力斩断导致数据丢失,你可以开启自动优雅停机机制。
const pool = new BrowserPool({
maxWorkers: 5,
// 仅需一行代码开启。当按下 Ctrl+C 或收到 k8s SIGTERM 信号时,
// 引擎会自动拦截并给予运行中的爬虫最多 10000ms 的收尾时间。
autoGracefulShutdown: true,
// 或者传入具体的毫秒数: autoGracefulShutdown: 15000
});注:开启该选项后,引擎会在底层自动替你接管 process.on('SIGTERM') 和 SIGINT 事件,超时或完成后自动安全退出。
示例 19: 分布式可靠队列 (At-Least-Once Delivery)
在默认情况下,Redis 队列为了追求极致的高并发吞吐,采用的是 BRPOP (弹出即删除) 的火炮发射模式。如果遇到机器突然断电宕机,正在处理的任务可能会永久丢失。
如果你的业务是对数据一致性要求极高的核心数据爬取,可以开启可靠模式。开启后,底层将自动启用 BRPOPLPUSH 机制,将弹出的任务安全地备份在 processing 队列中,只有当代码完美执行结束,框架才会发送 ACK 信号予以彻底销毁。
const pool = new BrowserPool({
redisUrl: 'redis://127.0.0.1:6379',
redisReliableMode: true, // 开启双队列绝对可靠投递
});