Serverless 环境中使用 Chrome + Puppeteer 指南

0 阅读1分钟

Serverless 环境中使用 Chrome + Puppeteer 指南

为什么需要这个方案?

在 Serverless 环境(Vercel、AWS Lambda 等)中,没有预装浏览器。但很多场景需要真实浏览器来渲染页面:

  • 抓取 SPA(单页应用)的动态内容
  • 网页截图 / 生成 PDF
  • 网页转 Markdown

本文介绍如何在 Serverless 中运行 Headless Chrome。

核心依赖

作用
puppeteer-corePuppeteer 的轻量版,不自带 Chromium
@sparticuz/chromium-min专为 Serverless 优化的 Chromium 二进制文件

puppeteer-corepuppeteer 的区别:puppeteer 会自动下载完整 Chromium(~170MB),不适合 Serverless。puppeteer-core 不带浏览器,需要手动指定可执行路径。

安装

pnpm add puppeteer-core @sparticuz/chromium-min

Next.js 配置

next.config.ts 中将 Chromium 标记为外部包,避免被 Next.js 打包:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  serverExternalPackages: ["@sparticuz/chromium-min"],
};

export default nextConfig;

核心代码:创建浏览器实例

关键点是区分本地开发和生产环境,使用不同的 Chromium 来源:

// Chromium 压缩包的远程地址(生产环境按需下载)
const CHROMIUM_PATH =
  "https://github.com/Sparticuz/chromium/releases/download/v143.0.0/chromium-v143.0.0-pack.x64.tar";

async function getBrowser() {
  const puppeteerCore = await import("puppeteer-core").then(
    (mod) => mod.default
  );

  if (process.env.VERCEL_ENV === "production") {
    // 生产环境:使用 @sparticuz/chromium-min
    const chromium = await import("@sparticuz/chromium-min").then(
      (mod) => mod.default
    );
    const executablePath = await chromium.executablePath(CHROMIUM_PATH);

    return await puppeteerCore.launch({
      args: chromium.args,
      executablePath,
      headless: true,
    });
  } else {
    // 本地开发:使用系统安装的 Chrome
    return await puppeteerCore.launch({
      executablePath:
        "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
      headless: true,
    });
  }
}

使用浏览器实例

export async function POST(request: NextRequest) {
  const { url } = await request.json();
  const browser = await getBrowser();
  const page = await browser.newPage();

  // 设置 User-Agent 避免被识别为爬虫
  await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
  );
  await page.setExtraHTTPHeaders({
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  });

  await page.goto(url, { waitUntil: "networkidle0" });
  const html = await page.content();

  // 用完必须关闭,释放资源
  await browser.close();

  return NextResponse.json({ html });
}