前端项目中使用 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。

相关推荐
Fan_web6 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常8 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java4 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele4 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范