Code前端首页关于Code前端联系我们

第一步,选对PDF.js的引入方式

terry 40分钟前 阅读数 19 #Vue
文章标签 js引入方式

最近逛前端社区和论坛,好多刚转Vue3的小伙伴都在问这个——既不想用封装得太死、功能还不一定全的第三方Vue插件包,又想把PDF.js原生的搜索、翻页、缩放这些核心功能快速塞到自己的Vue3组件库或者单页里,其实没那么复杂,跟着步骤走,大概半小时就能搭出一个能用的基础版,再加点细节调整就能直接上线用了。 很多新手一开始会直接npm install pdfjs-dist,结果要么是找不到worker路径报错,要么是打包后体积太大,这里得先明确:PDF.js的核心是PDF.js(解析PDF二进制流)、pdf.worker.js(渲染PDF的核心,不能和主线程抢资源,必须单独加载),有时候还需要可选的viewer.css/viewer.js,但原生的viewer组件耦合度太高,一般我们只用核心库自己写UI。

那最优引入方式是什么?有两种,根据项目场景选就行: 第一种是CDN引入,适合快速测试demo或者单页用PDF阅读器功能不重的项目,优点是配置少,体积完全是按需(或者说CDN那边已经优化好的),缺点是网络波动可能影响加载速度,而且生产环境如果要自己维护CDN缓存的话得费点心思,引入的时候要注意,pdf.js和pdf.worker.js的版本必须一致,不能随便混搭。 第二种是npm本地引入,适合功能复杂、需要离线使用、或者要做深度定制的项目,优点是完全可控,worker路径可以在webpack/vite里配置,打包后可以按需拆分模块,缺点是一开始的配置稍微麻烦一点,但其实现在vite有默认的静态资源处理规则,改两行配置就行。

第二步:本地项目用vite的话怎么快速配置路径

既然现在Vue3主流用vite,就专门说这个,先npm install pdfjs-dist --save或者pnpm add pdfjs-dist都行,yarn的话也差不多。 然后在项目根目录下找到vite.config.ts(或者vite.config.js),在defineConfig里加一个resolve.alias和assetsInclude的配置,resolve.alias是为了让我们import的时候更方便,assetsInclude是告诉vite,pdf.worker.js这种文件不要打包进bundle里,要作为静态资源单独处理,直接复制到dist目录下。 接下来在组件里引入的时候,先设置pdfjs.GlobalWorkerOptions.workerSrc,告诉PDF.js去哪里找worker文件,这个路径要和vite打包后的静态资源路径对应上——一般如果我们配置alias指向node_modules/pdfjs-dist/build/pdf.worker.min.js的话,assetsInclude复制的时候会直接把它放在dist/assets或者dist的根目录?不对,等下,assetsInclude的默认是['*.{png,jpe?g,gif,svg,ico,webp,avif}'],所以我们要加上'**/pdf.worker.min.js',然后可以手动在src/utils里放一个PDFLoader.js文件专门配置workerSrc和导出PDF.js的API?其实也不用这么麻烦,直接在组件里写就行,不过如果多个组件要用的话,封装成util会更省代码。

第三步:从零开始写核心的PDF阅读器组件

核心组件要解决四个问题:加载PDF二进制流、渲染单页、实现搜索、实现基础的交互(翻页、缩放、下载)。 先讲加载PDF二进制流:PDF.js加载PDF的入口是pdfjs.getDocument(),这个函数可以接受很多参数,最常用的是url(PDF的网络地址)或者data(PDF的二进制流数组或者Uint8Array),如果是本地文件上传的话,就用FileReader把File对象转成Uint8Array就行,getDocument()返回的是一个Promise对象,处理成异步函数await就行,拿到的是PDFDocumentProxy对象,里面有pdf.numPages可以拿到总页数,pdf.getPage(pageNum)可以拿到PDFPageProxy对象,这是渲染单页的关键。 然后是渲染单页:PDFPageProxy对象里有一个render()函数,这个函数需要一个渲染上下文,一般用canvas(性能最好,适合移动端和复杂PDF),有时候也可以用SVG(适合文字复制,但复杂PDF会卡顿),这里选canvas,在Vue3组件里,先放一个ref绑定的canvas元素和一个container容器(用来缩放和居中的),拿到PDFPageProxy对象后,先获取它的视口尺寸,用pdfPage.getViewport({ scale: currentScale }),这里currentScale是当前的缩放比例,默认可以设为1或者容器宽度除以PDF原宽度,然后设置canvas的width和height为视口的宽高乘以devicePixelRatio(解决高清屏模糊的问题,这个细节很多新手会漏),再设置canvas.style.width和style.height为视口的宽高,接着创建一个canvas的2D上下文,设置context.scale(devicePixelRatio, devicePixelRatio),最后调用pdfPage.render({ canvasContext: context, viewport: viewport }).promise,等待渲染完成就行。 接下来是翻页和缩放:翻页就是改变currentPage这个响应式变量,然后重新调用渲染单页的函数就行,注意要加边界判断,currentPage不能小于1也不能大于pdf.numPages,缩放就是改变currentScale,可以加个预设的缩放比例数组,0.5, 0.75, 1, 1.25, 1.5, 2, 'fit', 'fit-width'],'fit'就是自适应容器高度和宽度,取较小的那个比例,'fit-width'就是自适应容器宽度,'fit-height'同理,这两个需要每次容器大小改变的时候重新计算,所以要加一个window.resize的监听器,在组件mounted的时候加上,unmounted的时候移除,避免内存泄漏。 然后是搜索功能:这个是很多第三方插件做得不好的地方,原生的PDF.js搜索功能其实很强,支持模糊匹配、区分大小写、整词匹配,只是调用起来稍微有点绕,首先要从PDFDocumentProxy对象里拿到文本内容,用pdf.getPage(pageNum).then(page => page.getTextContent()),拿到的是一个TextContent对象,里面有items数组,每个item有transform(位置信息)、str(文本内容)这些属性,我们可以把每一页的TextContent都存到一个数组里,比如textContents,然后遍历所有页面的textContents.items,用正则表达式匹配搜索关键词,匹配到的结果存到searchResults数组里,数组里每个元素要存pageNum(页码)、itemIndex(TextContent里的item索引)、matchIndex(item.str里的匹配位置)这些信息,然后当点击下一个匹配结果的时候,跳到对应的页面,并且在对应的位置画一个高亮框?不对,画高亮框的话要注意canvas是bitmap,每次翻页会清空,所以每次渲染新页面的时候,都要检查这个页面有没有匹配结果,如果有的话,用canvas的fillRect或者strokeRect画高亮,不过如果不想这么麻烦,也可以直接把文本层叠在canvas上面,用CSS控制高亮,这样文字还能复制,体验会更好,文本层怎么叠?其实就是在canvas下面或者上面放一个div容器,里面放很多span元素,每个span对应TextContent里的一个item,用transform把span放在对应的位置,宽度和高度也设好,然后span的背景色默认是透明的,当有匹配结果的时候,把对应的span的背景色设成黄色或者其他颜色就行,这种方法比在canvas上画高亮更简单,而且效果更好,文字也能正常选择复制。 最后是下载功能:这个最简单,如果是用url加载的PDF,直接创建一个a标签,href设为url,download设为PDF的文件名,然后点击a标签就行;如果是用data加载的PDF,就把Uint8Array转成Blob对象,然后用URL.createObjectURL()创建一个临时的url,再用同样的方法创建a标签点击就行,点击完之后记得用URL.revokeObjectURL()释放临时url,避免内存泄漏。

第四步:加一点细节优化,让阅读器更好用

细节优化有很多,

  1. 加载中状态:在PDF加载完成之前,显示一个loading动画或者文字,告诉用户正在加载;
  2. 错误处理:如果PDF加载失败(比如网络错误、PDF文件损坏),显示一个错误提示;
  3. 页面导航栏:可以做一个简单的导航栏,里面有上一页、下一页、页码输入框、总页数显示、缩放按钮、搜索框、搜索上一个/下一个按钮、下载按钮;
  4. 触摸支持:移动端的话,可以加左右滑动翻页、双指缩放的功能,左右滑动翻页可以用touchstart、touchmove、touchend事件实现,双指缩放可以用gesturestart、gesturechange、gestureend事件实现,不过现在浏览器的触摸事件有了新的API,比如Pointer Events,兼容性更好,推荐用这个;
  5. 缓存功能:如果是加载网络PDF,可以把加载过的页面的canvas imageData或者PDFPageProxy对象缓存起来,下次翻到同一页的时候直接渲染,不用重新加载和解析PDF,这样速度会快很多。

第五步:常见的坑怎么解决

刚开始集成PDF.js的时候,很容易遇到几个坑:

  1. worker路径报错:这个是最常见的,刚才讲过了,要么是CDN引入的时候版本不一致,要么是本地引入的时候vite没有正确复制pdf.worker.min.js到dist目录,或者workerSrc的路径写错了;
  2. 高清屏模糊:刚才也讲过了,要把canvas的width和height设为视口的宽高乘以devicePixelRatio,context也要scale(devicePixelRatio, devicePixelRatio);
  3. 渲染大PDF的时候卡顿:这个时候可以用懒加载,只渲染当前页、上一页和下一页,其他页面的PDFPageProxy对象可以缓存,但不要渲染,当用户翻到某一页的时候再渲染;
  4. 文本层和canvas不对齐:这个一般是因为视口的计算或者transform的设置有问题,要仔细检查textContent.items里的transform属性,PDF.js的transform是6个元素的数组,对应CSS transform的matrix(a, b, c, d, e, f),直接把这个值赋给span的style.transform就行,注意span要设成position: absolute;
  5. 打包后体积太大:这个时候可以用按需引入,比如只引入pdfjs.getDocument、pdfjs.getPage这些我们需要的API,不要引入整个pdfjs-dist,vite的tree-shaking功能会帮我们把没用的代码去掉,也可以把pdf.worker.min.js和pdf.js放在CDN上,只打包自己写的组件代码。

Vue3集成PDF.js原生库虽然比用第三方插件包多写了一点代码,但自由度非常高,功能也完全可控,只要跟着步骤走,解决常见的坑,就能搭出一个体验很好的PDF阅读器。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门