返回博客

pdfjs-dist 在 Next.js dev 下崩溃:eval-source-map 与内置 webpack 运行时的致命冲突

某后台管理系统需要在页面内预览 PDF 文件,接入了 react-pdf(底层依赖 pdfjs-dist)。功能本地跑起来之后,开发模式下每次渲染 PDF 组件,控制台就炸出一个让人摸不着头脑的报错——而切换到生产构建完全正常。一番排查,藏着一个 webpack 的深层机制问题。

现象

在 Next.js 开发服务器里渲染 <Document> / <Page>(或直接 import pdfjs-dist),页面抛出:

Runtime TypeError: Object.defineProperty called on non-object
  at Object.defineProperty (<anonymous>)
  at .../node_modules/pdfjs-dist/build/pdf.mjs (...:NN:1)
  at eval (webpack-internal:///.../PdfViewer.tsx:...)

错误栈里两个特征:webpack-internal:/// URL + eval(...) frame。生产 build 正常,仅 dev 崩溃。

根因

pdfjs-dist v4/v5 的 build/pdf.mjs 是一个已经 webpack 打包过的产物,只是用了 .mjs 扩展名挂上了 ES Module 外壳——它的头部是 var __webpack_require__ = {},尾部才是 export { ... }

Next.js 开发模式强制使用 devtool: 'eval-source-map',这会把每个模块都包进 eval()。当 pdf.mjs 自带的内层 webpack 运行时(__webpack_require__.d / .r,调用 Object.defineProperty(exports, ...)) 在 eval() 里执行时,exports 解析为非对象,Object.defineProperty 直接抛错。

生产模式不用 eval-* devtool,所以 prod 正常。

坑:官方 devtool 修复对 Next.js 完全无效

react-pdf 官方 wiki(升级 8.x→9.x Workarounds → Webpack 章节)给出的方案是:把 devtool 改成非 eval-* 的值:

// next.config.js
webpack: (config, { dev, isServer }) => {
  if (dev && !isServer) config.devtool = 'cheap-module-source-map'
  return config
}

但 Next.js dev 会主动把自定义 config.devtool 回滚回 eval-source-map——它会打印一条性能警告,然后忽略你的覆盖。报错栈里依然出现 webpack-internal:/// + eval(...) 就说明被回滚了。

同样无效的招式:

  • .mjstype: 'javascript/auto' rule —— pdf.mjs 有 export {},webpack 仍把它当 ESM 处理,eval 包裹依然存在
  • next/dynamic(..., { ssr: false }) —— 崩溃发生在客户端模块 eval,延迟 import 不能阻止 eval 包裹

正解

如果只需要展示 PDF(不做 canvas 绘制 / 文字选取 / 注解),根本不需要 pdfjs。直接用原生 <iframe> 指向 PDF URL——Chromium 和 Electron(pdfium)内置 PDF 查看器:

'use client'
 
export default function PdfViewer({ src }: { src: string }) {
  return (
    <iframe
      src={src}
      title="PDF 预览"
      className="h-[75vh] w-full rounded-md border"
    />
  )
}

这完全绕开了 pdfjs:没有 pdf.mjs,没有 worker 文件,没有 webpack 互操作,没有 eval 包裹崩溃。dev 和 prod 行为一致,Electron 桌面壳里同样正常。

iframe 内联渲染的前提条件

URL 必须响应 Content-Type: application/pdf,且不能设置 Content-Disposition: attachment(attachment 会触发下载而非内联预览)。对象存储的预签名 URL(S3 / MinIO)只在「明确下载」按钮场景才加 response-content-disposition,预览场景不要加。跨域完全没问题——浏览器允许跨域 PDF 在 iframe 里展示(这是显示,不是脚本访问)。

正解:纯展示场景用 iframe,彻底绕开 pdfjs webpack 互操作

替换后验证三步:

  1. 开发模式点预览——PDF 正常渲染,不再出现 Object.defineProperty 错误
  2. 确认组件不再 import pdfjs:rg "react-pdf|pdfjs" src/.../PdfViewer.tsx → 无匹配
  3. tsc --noEmit 通过

原来的 react-pdf / pdfjs-dist 依赖和 public/pdf.worker.min.mjs 已成死重,可以一并删除。

一句话外卖

当供应商组件在你的构建环境里持续水土不服,先看原生浏览器能不能直接完成这件事——iframe 预览 PDF 往往比与 webpack 互操作博弈更干净。