You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Puppeteer的page.evaluate()中获取fetch()的流式数据?(Node.js)

解决Puppeteer流式传输多请求失效问题

问题根源

你的代码核心问题在于全局复用单个page实例,且首次请求时通过exposeFunction绑定的是第一个请求的res对象。后续请求进来时,不仅复用了已绑定旧res的page,若多个请求并发,exposeFunction还会被覆盖,导致所有请求的流都指向同一个res,或后续请求的res完全未被关联。

解决方案

复用全局browser实例(浏览器启动开销极高,复用可大幅提升性能),但每个请求创建独立的page实例,并为每个page单独绑定当前请求的res处理函数,确保每个请求的流完全独立。

修改后的代码

const puppeteer = require('puppeteer');
const express = require('express');
const app = express();

// 全局复用browser,避免重复启动浏览器
let browser = null;

app.post("/test", async(req, res) => {
    try {
        // 首次请求启动浏览器
        if (!browser) {
            browser = await puppeteer.launch({
                headless: "new"
            });
        }

        // 每个请求创建独立的page
        const page = await browser.newPage();
        const desiredUrl = "url here";
        await page.goto(desiredUrl);

        // 给当前page绑定当前请求的res处理函数
        await page.exposeFunction('writeData', (data) => {
            console.log("Writing data for request");
            res.write(data);
        });

        await page.exposeFunction('endStream', () => {
            console.log("End stream for request");
            res.end();
            // 请求结束后关闭当前page,释放资源
            page.close().catch(err => console.error("Close page error:", err));
        });

        await page.evaluate(async ()=>{
            const output = await fetch("/endpoint_here", {
                headers: {
                    /* 你的headers配置 */
                },
            });

            const reader = output.body.getReader();
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    window.endStream();
                    return;
                }
                
                const decoder = new TextDecoder();
                const decodedData = decoder.decode(value);
                window.writeData(decodedData);
            }
        });

    } catch (err) {
        console.error("Request error:", err);
        if (!res.headersSent) {
            res.status(500).send("Server error");
        }
    }
});

// 进程退出时关闭browser,避免资源泄漏
process.on('exit', async () => {
    if (browser) {
        await browser.close();
    }
});

app.listen(3000, () => console.log("Server running on port 3000"));

关键说明

  1. 全局仅复用browser:浏览器启动耗时久,复用能降低资源消耗;page是轻量实例,每个请求新建不会有明显性能问题。
  2. 每个page绑定独立res:exposeFunction是绑定到当前page实例的,每个请求的page对应自己的res,不会出现串流或失效问题。
  3. 请求结束后关闭page:避免page实例堆积占用内存,高频请求场景可改用page池优化。
  4. 错误处理:通过try/catch捕获异常,确保请求出错时能正确响应客户端,避免请求挂起。

额外优化建议

  • 高频请求场景可引入page池(如generic-pool库),复用page实例减少创建销毁开销,但需注意复用前重置page状态(清除绑定函数、跳转到空白页等)。
  • 流式传输时可设置响应头res.setHeader('Transfer-Encoding', 'chunked'),明确告知客户端采用分块传输。
  • 为reader.read()添加异常捕获,避免上游服务出错导致page挂起。

内容的提问来源于stack exchange,提问作者Stacky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 13:26:01