此文已由作者吴家联授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

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+款云产品!

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 【大数据之数据仓库】GreenPlum PK DeepGreen(TPCH)
【推荐】 白木彰:具有普遍性的设计力

pdfjs viewer 开发小结的更多相关文章

  1. vue开发小结(下)

    前言 继前几天总结了vue开发小结(上)后,发现还有很多的点没有能列举出来,于是还是打算新建一个下篇,再补充一些vue开发中需要注意的细节,确实还是都是细节的问题,我只是在这里强调下,希望对大家有帮助 ...

  2. Android 即时通讯开发小结(二)

    <Android 即时通讯开发小结>基于IM Andriod 开发的各种常见问题,结合网易云信即时通讯技术的实践,对IM 开发做一个全面的总结. 相关推荐阅读:. Android 即时通讯 ...

  3. Android 即时通讯开发小结(一)

    <Android 即时通讯开发小结>基于IM Andriod 开发的各种常见问题,结合网易云信即时通讯技术的实践,对IM 开发做一个全面的总结. 相关推荐阅读:. Android 即时通讯 ...

  4. 5Spring动态代理开发小结

    5Spring动态代理开发小结 1.为什么要有动态代理? 好处 1.利于程序维护 2.利于原始类功能的增强 3.得益于JDK或者CGlib等动态代理技术使得程序扩展性很强 为什么说使得程序扩展性很强? ...

  5. 无插件的大模型浏览器Autodesk Viewer开发培训-武汉-2014年8月28日 9:00 – 12:00

    武汉附近的同学们有福了,这是全球第一次关于Autodesk viewer的教室培训. :) 你可能已经在各种场合听过或看过Autodesk最新推出的大模型浏览器,这是无需插件的浏览器模型,支持几十种数 ...

  6. 移动Web开发小结

    以下是做移动端Web开发过程中小结的几个事项:希望能够帮助到大家,同时也方便自己查看: 1,在移动开发页面中,主体盒子的max-width与min-width的设置原因: ①设置max-width是为 ...

  7. ipad开发小结

    项目小结 :布局的时候最后要用CGRectDivi.. :控制器的生命周期---->(init-->(当self.view==nil调用 loadView viewdidload)--&g ...

  8. redis开发小结

    随着缓存在web服务中用的越来越广泛,redis可以说成为了目前最流行的NoSQL数据库!redis与memcached最大的不同在于redis支持更多的数据类型,包括string.hash.list ...

  9. H5嵌入原生开发小结----兼容安卓与ios的填坑之路

    一开始听说开发H5,以为就是做适配现代浏览器的移动网页,心想不用管IE了,欧也.到今天,发现当初too young too simple,兼容IE和兼容安卓与IOS,后者让你更抓狂.接下来数一下踩过的 ...

随机推荐

  1. 常见的问题:https://localhost:1158/em 无法打开

    常见的问题:https://localhost:1158/em 无法打开 安装了Oracle Database 11g,里面有个基于网页的管理工具“https://localhost:1158/em” ...

  2. web 前端的一些问题

    1. HTML 和 JS 一个网页显示出来的静态的内容为html创见的静态object 对这些object的操作通过JS来响应 2. HTTP cookie cookie是由server set, 由 ...

  3. solr管理界面下统计多个时间段的数据 facet.query

    在Raw Query Parameters参数里面输入时间段即可 如下图所示: facet.query=publishTime:[2017-06-05T00:00:00Z TO 2017-06-07T ...

  4. hashTable 和 hashMap的区别

    HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,HashMap把Hashtable的contains方法去掉了,改成containsvalue和contai ...

  5. AJPFX总结string类和简单问题

    String表示字符串,所谓字符串,就是一连串的字符;String是不可变类,一旦String对象被创建,包含在对象中的字符序列(内容)是不可变的,直到对象被销毁://一个String对象的内容不能变 ...

  6. RPC之远程过程调用

    一. 简介 将一个函数运行在远程计算机上并且等待获取那里的结果,这个称作远程过程调用(Remote Procedure Call)或者 RPC. RPC是一个计算机通信协议. 1. 类比: 将计算机服 ...

  7. knockout Observable Array(监控数组)

    Observable Array(监控数组)的作用 列表操作是经常会遇到的一个场景,使用监控数组,你可以: 保存列表对象,并且使用Ko提供的丰富的API操作列表元素(支持内建js Array的方法,以 ...

  8. HYSBZ 1588 营业额统计 (Splay树)

    题意:给出一个公司每一天的营业额,求每天的最小波动值之和.该天的最小波动值= min { 绝对值| 该天以前某一天的营业额-该天的营业额 | }.第一天的最小波动值就是其自己. 思路:Splay伸展树 ...

  9. Apache的HttpClient的使用

    Apache的HttpClient可以被用于从客户端发送HTTP请求到服务器端,其中封装了客户端发送http的get和post请求 使用Apache的HttpClient发送GET和POST请求的步骤 ...

  10. 1.1 NLP基础技能,字符串的处理

    #!/usr/bin/env python # coding: utf-8 # # 字符串操作 # ### 去空格和特殊字符 # In[8]: s = " hello world! &quo ...