在前端想要处理和 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 了。一般来说有两种方案:
- 直接下载 pdf.js 打包好的静态资源(Getting Started (mozilla.github.io)),把他放在自己网站上并通过 iframe 访问。
- npm 安装 pdfjs-dist,然后自己引用对应的 Class 重新开发一套。
而具体使用哪种方案要看你的需求,比如:
1、我只是要通过代码控制 pdf 的一些功能,希望有一个完整的 pdf 工具,拿来就可以用。
这种情况的话那推荐第一种。因为第一种是 mozilla 已经开发好的一个静态网站(称之为 pdfjs-viewer),你直接访问就可以用。具体样式如下,你也可以通过这个 在线 DEMO 查看。注意他已经包含了一个顶部的工具条,里边内置了很多功能,比如切换分页、懒加载、编辑、导出、快捷键等等。
2、我的需求很简单,只希望用户能看到 pdf 内容即可,其他什么功能都没有,也不能复制。或者我有一个技术团队,需要从头开发完整的 pdf 功能。
这样的话就推荐使用第二种方案,现在网上有很多相关的文章,核心思路就是从 pdfjs-dist 中引入一些核心类,然后自己实例化出来。比如这些:
但是由于 pdfjs 并没有一个完整的文档,导致这种方案开发起来会比较痛苦,而且很多懒加载、搜索、页码跳转之类的功能都需要自己来实现了。所以用这种方案前要慎重考虑工期和成本。
另外你也可以看一下 PDF.js Express,这是基于 pdf.js 二次开发的一个 pdf 工具,但是完整的内容需要购买商业许可,要不要用大家自行斟酌。
核心问题
其实 pdf.js 在实际开发中存在的最大问题就是:没有成熟的 API 文档 。对于内部的核心类来说,开发组提供了 一个稍显简陋的文档。而对于完整的产品 pdfjs viewer 的说明文档,我在整理之后只找到下面这些有用的:
- pdf.js 常见问题:对于 pdfjs 操作上的说明。
- pdfjs viwer 路由参数:一些通过地址栏可以指定的参数,比较少,但是聊胜于无。
- 一些基于 pdfjs 开发的第三方项目:听起来可能有不少参考价值,但是实际上绝大多数项目都已经停止维护了。
- 第三方查看器使用:看起来最有用的,但是也只是讲了下如何监听 pdf 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.CREATE
的 switchannotationeditorparams
事件,会触发图片批注的图片上传功能。
总结
其实除了这些之外,pdfjs-viewer 还有很多事件和 api 没有被发掘出来。你可以查看最下面的 demo 示例,或者直接搜索 _on
来查看全部的事件以及用法:
你像其中的 download、print、nextpage、lastpage 一眼就能看到是干什么的,这里就不赘述了。DEMO 地址在这里:
HoPGoldy/hoho-pdfjs-playground: 如何在现代前端项目中使用 pdfjs 的演示 demo
如果你在使用 pdfjs-viewer 时发现什么新的功能,欢迎评论区留言或者直接向这个 git 仓库提交 pr。