pdfjs viewer 开发小结
此文已由作者吴家联授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
1. pdfjs库简介
PDF.js 是由Mozilla 主导推出的可以将PDF文件转换为H5页面进行展示的工具。相比较目前前端可以用的pdf节点方案,pdfjs是非常合适的。它有这么几个优点:1.完全js开发,不依赖其他js库,不使用flash插件。2.代码分层做的较好,官方提供了可以直接使用的封装组件,无需额外开发。3.兼容性也不错,支持canvas和svg渲染,pc和手机端都可以使用。教育这边pc端pdf reader是使用flash开发的,这次产品打算做手机上h5的pdf功能,所以前端决定使用pdfjs来实现。
2. 官方pdfjs viewer分析
按照pdfjs官网上的介绍,pdfjs代码是这样分层的:
当然因为迭代时间的关系,我们并没有研究core和display内部的东东,因为这一块涉及到pdf文件格式规范。我们把重点放在了viewer层。官方提供的viewer层组件是可以直接使用的,功能也很丰富(demo地址:https://mozilla.github.io/pdf.js/web/viewer.html)。原本是喜滋滋的事情,开发都觉得不用干什么活了,但是产品却说这不是我们想要的......因为.......功能太多了,而且跟交互稿长得完全不一样。
好吧,其实产品需要的功能很少,而且交互上有些差别。最终实现的修改和官方组件的差异如下图所示:。
(上边是官方,下边是教育)
如果我们在官方组件的基础上做修改、做配置,那实在是很蛋疼,而且会依赖很多没必要的代码。所以个人决定参照官方viewer的代码,实现一个简单的、符合产品需求的viewer组件(所以本文标题是pdfjs viewer开发小结)。为什么说是“参照”呢?因为pdfjs提供的文档(core和display层)实在太简单了,单看文档中的api,很多还是无法知道其功能和用处,看来官方是建议直接使用组件,不建议自己再开发一个viewer......。
实际大概花了一天阅读了官方viewer的代码。梳理了一下流程和重点的类:
因为产品需要的功能比较少,所以只看了最基本的功能代码,涉及的类不多,具体有这么几个:
pdf_page_view.js:是viewer层最重要的类,封装的是pdf每一页的功能,实现的功能有:设置或者更新pdf page、更新page的缩放倍数和旋转角度、绘制page和管理绘制的状态等。类里面的部分代码是可以去掉的,比如:如果只需要支持canvas绘制,则可以去掉svg绘制部分的代码,如果不需要文本层选中功能,可以去掉textlayer部分的代码。如下图所示的方法:
另外一个重点是page绘制的状态,状态常量有:
INITIAL: 0
RUNNING: 1
PAUSED: 2
FINISHED: 3
Page实例化或者取消绘制时状态变为INITIAL,当调用draw方法时,状态变为RUNNING,这时会调用api中的render方法,返回一个任务对象,在进行绘制时任务对象会触发onContinue回调,并暂停绘制,表示绘制正在进行,是否继续,这时可以处理一些逻辑,比如当用户快速翻页时,当前page还未绘制完就翻下一页了,此时可以在onContinue回调中选择不继续绘制,把page的状态变为PAUSED,当下次翻到该页时再继续绘制。绘制完全完毕后状态变为FINISHED。当然绘制过程中也可能报错,其中的处理都是通过promise实现的。page在进行缩放或者旋转时一般需要重新绘制,重新绘制可以保证清晰度,这时状态也会重置,重新走一遍。需要注意的是在不同浏览器中canvas可以绘制的像素大小上限是不同的,官方取的应该是ie中的上限(几个主流浏览器中的最小值),如果缩放到达上限就不能继续重新绘制了,可能引起浏览器崩溃,只能通过css手段来模拟,但是不能保证清晰度。
pdf_rendering_queue.js:管理page的绘制,比如确定绘制的优先级、预先绘制功能等。如果自己实现这个类可以简化,因为交互形式不同,不需要处理一些情况。
pdf_viewer.css:样式文件注意是实现了page的样式,也就是每一页相关的样式,这个不用多讲。
pdf_viewer.js:处理了pdf文档对象,创建和维护page_view实例,对外提供接口,比如翻页、缩放、旋转等。流程是这样的:从pdf对象里面取出第一页的viewport,做为默认的viewport,用这个默认的viewport实例化全部的page_view对象,之后调用update方法,渲染优先级最高的page,当用户翻页或者其他操作时又会重复update的流程。里面还实现了一个简单的cache功能,将已经绘制完的page对象push到cache队列,队列超过一定长度就删除开头的page,来保证内存消耗不会过大。
ui_utils.js:包含了一些常量和工具方法,比如page滚动功能、全局自定义事件管理、像素处理方法(本人对canvas没有研究,相关代码没有进行解读)等;
3. 教育产品pdfjs viewer的设计
以上分析的几个类里面的功能已经可以满足产品需求了,整理一下时序图:
当然在viewer基础上我们再封装了一层regular组件(教育产品组件规范)作为与用户交互的部分。
4. 踩坑记录(几个大问题)
1. 缓存的限制
官方viewer中设置cache大小(缓存的已经绘制的page_view实例个数)默认是10,在手机上不建议设置这么多,测试发现某些浏览器,比如安卓中的UC,多个page都放大的情况下可能会导致崩溃,或者快速翻页的时候也可能,当然概率很小。
2. 微信中的崩溃问题【重点】
在测试过程中发现安卓微信中打开pdf会崩溃,而且出现的概率很大,如果无法解决根本不能上线,当时真是整个人都要崩溃了...微信x5浏览器果然是新时代的ie6…。排查过程也只能靠猜了,ie6至少网上查查信息还蛮多的,你个x5的信息根本查不到,即使查到有人在微信论坛反馈问题也不一定有人去解决。
猜测可能的原因有这么几个:1.跟pdf文件大小有关,比如文件太大会引起崩溃,2.跟pdf内容有关,比如包含某些内容会引起崩溃,3.pdfjs库依赖的方式有问题,这个可能性很小,4.pdfjs内部实现问题,但是ios和其他安卓浏览器几乎没出现崩溃,如果真是库实现问题也很难查了。
经过一整天的排查,结论是1、2、3推测都不能成立,4的话不好说。最后实在想不出办法了就一点一点看崩溃的现象,然后就在网络代理的抓包信息中发现一个奇怪的现象:崩溃之前,库已经加载完毕,开始加载pdf文件了,但是pdf文件加载到一半就崩溃了,想来想去,猜测是不是加载环节出的问题,因为我直接使用的api中的getDocument方法加载文件,并不清楚内部实现。于是自己写了个简单的xhr加载文件方法,不使用库的api,再测试就不崩溃了。。。真是要泪奔。
不过目前正真崩溃的原因还是不清楚,只找到这一种解决方法。getDocument中应该实现了range加载逻辑(虽然实际测试中没有发现),并不建议自己实现加载方法,正常来说是多此一举,不过面对x5这样的问题也只能用不正常的方案了。
3. 移动端手势的一些问题
目前移动端手势问题主要是在缩放时出现的,部分手机浏览器无法阻止用户缩放,导致放大pdf时整个页面都放大了,这样看起来会很奇怪,虽然通过代码写了meta,也阻止了部分touch事件,但是某些手机还是偶尔会出现,很头大。不知道有没有同事在这方面有经验,可以指导一下。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 适配的那些事
【推荐】 Android TV 开发(2)