前端项目中使用 pdf.js 开发指南

在前端想要处理和 PDF 相关的需求,那 pdf.js 基本可以算是唯一的选择,但是 pdf.js 的使用方式和普通的 npm 包又有不小的区别,在摸索上手阶段需要耗费不少的功夫,这篇文章就来详细介绍一下如何正确的在前端项目中集成 pdf.js。

开发思路

首先我们要根据业务上的需求来整理一下开发思路。咱们从简单到复杂来讲:

最简单的是只在 pc 端把 pdf 展示一下。这种的话直接 iframe 打开 pdf 链接(调用浏览器内置的 pdf 浏览器),或者使用 <embed src="example.pdf" type="application/pdf" /> 来直接展示 pdf。

但是这种操作对于移动端的兼容性不好,很多移动端的 webview 都不支持 pdf 预览,而且不同浏览器内置的 pdf 预览在样式上也会有区别。对于统一交互体验上并不好。

所以如果你需要在移动端上展示,或者需要通过代码对 pdf 进行一些操作的话(比如搜索、跳转页码),那就需要用到 pdf.js 了。一般来说有两种方案:

而具体使用哪种方案要看你的需求,比如:

1、我只是要通过代码控制 pdf 的一些功能,希望有一个完整的 pdf 工具,拿来就可以用。

这种情况的话那推荐第一种。因为第一种是 mozilla 已经开发好的一个静态网站(称之为 pdfjs-viewer),你直接访问就可以用。具体样式如下,你也可以通过这个 在线 DEMO 查看。注意他已经包含了一个顶部的工具条,里边内置了很多功能,比如切换分页、懒加载、编辑、导出、快捷键等等。

2、我的需求很简单,只希望用户能看到 pdf 内容即可,其他什么功能都没有,也不能复制。或者我有一个技术团队,需要从头开发完整的 pdf 功能。

这样的话就推荐使用第二种方案,现在网上有很多相关的文章,核心思路就是从 pdfjs-dist 中引入一些核心类,然后自己实例化出来。比如这些:

但是由于 pdfjs 并没有一个完整的文档,导致这种方案开发起来会比较痛苦,而且很多懒加载、搜索、页码跳转之类的功能都需要自己来实现了。所以用这种方案前要慎重考虑工期和成本。


另外你也可以看一下 PDF.js Express,这是基于 pdf.js 二次开发的一个 pdf 工具,但是完整的内容需要购买商业许可,要不要用大家自行斟酌。

核心问题

其实 pdf.js 在实际开发中存在的最大问题就是:没有成熟的 API 文档 。对于内部的核心类来说,开发组提供了 一个稍显简陋的文档。而对于完整的产品 pdfjs viewer 的说明文档,我在整理之后只找到下面这些有用的:

太少了,对于开发者来说,项目里接入 pdfjs 可能有一半的需求都是通过代码控制 pdf 预览器的行为,从而完成一些功能。但是 pdfjs viewer 都提供了哪些 api?有什么 event?怎么调用?什么时候调用?现代前端项目里如何接入 pdfjs?都没有对应的说明,这就导致了大家需要耗费很多精力来摸索这些东西。

本文的内容就是将 pdfjs 最常用的一些功能整理并分享出来,避免大家再走弯路。为了让不同前端框架的同学都可以使用,下面我只会以 vite 搭建的纯原生 js 工程来讲解,最终会实现一个如下效果的 DEMO,链接放在文末了,需要自取:

OK 废话少说,咱们现在开始。

正文

现代前端项目引入 pdfjs

这里我比较推荐的操作是直接把打包好的 pdfjs 静态资源放在项目的 public 目录下。其实 pdfjs 也支持 npm 安装 pdfjs-dist 通过 gulp 自动附加到项目里,但是现在的项目基本都已经抛弃 gulp 了,所以这里就直接忽略。

首先访问 下载页面。选第一个 Prebuilt (modern browsers):

在我所开发产品的实际使用下来,只要你不要求 IE,那第一个包的兼容性就足以应付市面上大多数 pc 和 h5 浏览器了。并且注意其中的版本,随着这个版本的迭代,本文的内容可能会出现过期的情况,请注意辨别。

下载好后解压到 public 里的目录下就可以了:

然后直接 <iframe class="pdf-container" src="/pdfjs/web/viewer.html"></iframe> 就可以直接访问了,默认情况下它会打开一个内置的测试 pdf:

我们后续的核心思路就是通过访问 iframe 里的 pdfjs 实例来对 pdf 进行控制。

打开指定 pdf / 访问跨域 pdf

想访问指定 pdf 很简单,pdfjs-viewer 可以通过指定 file 参数的形式直接打开在线 pdf:

js 复制代码
const actionSwitchFile = (src) => {
  const pdfIframe = document.querySelector('.pdf-container');
  pdfIframe.src = `/pdfjs/web/viewer.html?file=${encodeURIComponent(src)}`
}

actionSwitchFile('https://arxiv.org/pdf/2001.09977.pdf')

但是有个问题,如果你访问的资源和 pdfjs-viewer 不是同源的话,pdfjs-viewer 就会主动禁止访问,原因在于 pdf 内部会包含一些敏感的可执行内容,为了防止被不安全的 pdf 文件攻击,所以这个是默认禁用的。

想要处理这个问题可以点开报错右上角的定位:

然后把这两行注释掉或者做你自己的安全校验:

获取 pdfjs-viewer 实例

pdfjs-viewer 非常贴心的把自己的实例注入到了 window 上,也就是说我们可以直接通过 iframe 的 window 拿到这个实例。

js 复制代码
const getViewerInstance = () => {
  const pdfIframe = document.querySelector('.pdf-container');
  if (pdfIframe.contentWindow.document.readyState !== 'complete') {
    throw new Error('页面尚未加载完成');
  }
  const { PDFViewerApplication, PDFViewerApplicationOptions } = pdfIframe.contentWindow;
  return { PDFViewerApplication, PDFViewerApplicationOptions };
}

注意看我们解构出了两个对象:

  • PDFViewerApplication重点,这个就是 pdfjs 封装好的、完整的 pdf 实例,我们后续所有的操作基本都是在和它打交道。
  • PDFViewerApplicationOptions:当前 pdfjs 实例正在使用的一些配置项,可以了解一下,一般来说用不到。

我们可以打印一下 PDFViewerApplication 看看:

里边的内容还是很多的啊,其中最核心的就是 eventBus 对象。pdfjs-viewer 的核心功能都是基于事件模型实现的,可以通过 eventBus.on 监听某些事件,或者通过 eventBus.dispatch 来触发某些功能。具体的用法我们下文会详细讨论。

监听 pdf 是否就绪

pdfjs-viewer 的准备阶段有四个:

  • iframe 页面是否加载完成 :通过 iframe.onload 事件监听
  • pdf 实例是否准备就绪 :通过 PDFViewerApplication.initializedPromise 来监听。这个主要是实例会准备一些功能,在这个 promise 完成之前可能没法正常使用某些功能,所以如果你要 dispatch 的话,最好先 await 一下这个。当然,如果你 on 监听事件的话就不用管这个。
  • 文档是否加载完成:在实例创建完成后会去加载 pdf 文档,就是顶部有个小蓝条一直在走进度的时候。
  • 页面是否渲染完成:在文档下载完成后会开始渲染指定的页面,由于 pdfjs-viewer 内部做了页面懒加载,所以只会加载一定范围内的页面。所以如果你需要处理 dom 的话,需要等待这个渲染完成。

以上这些可以通过如下方案进行监听:

js 复制代码
/** 监听加载状态 */
const actionListenInit = (app) => {
  app.initializedPromise.then(() => {
    console.log('实例加载完成');
  })

  app.eventBus.on('documentloaded', (e) => {
    console.log('pdf 内容加载完成', e)
  })

  app.eventBus.on('pagerendered', (e) => {
    console.log(`第 ${e.pageNumber} 渲染完成`, e)
  })
}

/** 创建 pdf viewer */
const createPdfViewer = () => {
  sidebarOpened = false;
  if (pdfIframe) {
    pdfIframe.parentNode.removeChild(pdfIframe);
  }

  const iframe = document.createElement("iframe");
  iframe.src = `/pdfjs/web/viewer.html#locale=${getCurrentLocale()}`;

  iframe.onload = () => {
    console.log('网页加载完成')
    const { PDFViewerApplication } = getViewerInstance();
    actionListenInit(PDFViewerApplication)
  };

  const container = document.querySelector('.pdf-container');
  container.appendChild(iframe);
}

切换到指定页码

发射一个 pagenumberchanged 事件即可,注意,页码是从 1 开始的:

js 复制代码
const actionChangePage = (pageNumber) => {
  const { PDFViewerApplication } = getViewerInstance();
  PDFViewerApplication?.eventBus?.dispatch('pagenumberchanged', {
    value: pageNumber,
  });
}

搜索指定内容

发射一个 find 事件即可:

js 复制代码
const actionSearch = (query) => {
  const { PDFViewerApplication } = getViewerInstance();
  PDFViewerApplication?.eventBus?.dispatch('find', {
    type: '',
    query,
    highlightAll: true,
  });
}

侧边栏切换

是否打开侧边栏这个比较特殊,我们可以直接调对应的 api:

js 复制代码
/** 展开 / 收起 pdf 侧边栏 */
const actionToggleSidebar = (visible) => {
  const { PDFViewerApplication } = getViewerInstance();
  if (visible) return PDFViewerApplication?.pdfSidebar?.open();
  else return PDFViewerApplication?.pdfSidebar?.close();
}

切换国际化

pdfjs-viewer 切换国际化比较特殊,需要先把 debug 模式打开:全局搜索 pdfBugEnabled,把对应的值改为 true。

然后就可以通过 hash 参数 locale 的形式指定要使用的多语言类型:

js 复制代码
pdfIframe.src = `/pdfjs/web/viewer.html#locale=zh-CN`

我也不知道为什么指定国际化需要放在 debug 参数里。如果你觉得这样不安全的话,也可以仿造 file 参数,添加一个 params 参数来控制 locale 类型。

顺带提一句,你不传的话,pdfjs-viewer 也是会按照浏览器的语音来设置自己的国际化的。

批注模式

批注模式是 pdfjs-viewer 里一个比较大的功能,首先简单介绍一下用法。

批注模式一共有三种:文字批注、绘制批注、图片批注,可以通过右上角的按钮切换:

点一下进入批注模式,再点一下退出,进入批注模式后可以随意编辑之前添加的批注。比如你创建了个文字批注,切换成图片批注模式时依旧可以控制刚才创建的文字批注。

这个功能的事件主要有两个:

  • switchannotationeditormode:用来进入、退出批注模式
  • switchannotationeditorparams:设置批注模式的参数,例如绘制批注时可以设置笔画的粗细和颜色。三种批注模式的参数都是通过这个事件设置的。

用法也很简单:

js 复制代码
const AnnotationEditorType = {
  DISABLE: -1,
  NONE: 0,
  FREETEXT: 3,
  HIGHLIGHT: 9,
  STAMP: 13,
  INK: 15
};

/** 切换为批注模式 */
const actionChangeAnnotation = (annotationType) => {
  const { PDFViewerApplication } = getViewerInstance();
  PDFViewerApplication?.eventBus?.dispatch('switchannotationeditormode', {
    mode: annotationType,
  });
}

const AnnotationEditorParamsType = {
  RESIZE: 1,
  CREATE: 2,
  FREETEXT_SIZE: 11,
  FREETEXT_COLOR: 12,
  FREETEXT_OPACITY: 13,
  INK_COLOR: 21,
  INK_THICKNESS: 22,
  INK_OPACITY: 23,
  HIGHLIGHT_COLOR: 31,
  HIGHLIGHT_DEFAULT_COLOR: 32,
  HIGHLIGHT_THICKNESS: 33,
  HIGHLIGHT_FREE: 34,
  HIGHLIGHT_SHOW_ALL: 35
};

/** 设置批注模式参数 */
const actionChangeAnnotationParam = (paramType, paramValue) => {
  const { PDFViewerApplication } = getViewerInstance();
  PDFViewerApplication?.eventBus?.dispatch('switchannotationeditorparams', {
    type: paramType,
    value: paramValue,
  });
}

这里面有两个枚举,都是从 pdfjs-viewer 里复制过来的,代表了批注模式的类型和批注参数的类型。比如发射一个 类型为 AnnotationEditorParamsType.CREATEswitchannotationeditorparams 事件,会触发图片批注的图片上传功能。

总结

其实除了这些之外,pdfjs-viewer 还有很多事件和 api 没有被发掘出来。你可以查看最下面的 demo 示例,或者直接搜索 _on 来查看全部的事件以及用法:

你像其中的 download、print、nextpage、lastpage 一眼就能看到是干什么的,这里就不赘述了。DEMO 地址在这里:

HoPGoldy/hoho-pdfjs-playground: 如何在现代前端项目中使用 pdfjs 的演示 demo

如果你在使用 pdfjs-viewer 时发现什么新的功能,欢迎评论区留言或者直接向这个 git 仓库提交 pr。

相关推荐
前端大卫3 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘4 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare4 小时前
浅浅看一下设计模式
前端
Lee川4 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
一个处女座的程序猿4 小时前
AI之Agent之VibeCoding:《Vibe Coding Kills Open Source》翻译与解读
人工智能·开源·vibecoding·氛围编程
Ticnix4 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人4 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl4 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人4 小时前
vue3使用jsx语法详解
前端·vue.js