ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PDF.js按需分片加载:300MB大PDF网页秒开实战

PDF.js按需分片加载:300MB大PDF网页秒开实战 网页里要展示一个几十上百MB的PDF直接整包传给前端不仅能卡半天还容易让浏览器内存直接爆掉。我自己上半年就接过一个PDF.js相关的按需分片加载需求用PDFDataRangeTransport配合后端Range响应把300MB政策汇编在网页里秒开这件事做成了。这篇文章会完整拆解前后端源码、开发流程和我在实测过程中踩过的坑目标是让你拿到就能照着复现。这篇内容不是简单给你贴一个getDocument的调用而是带你理解PDF.js的传输层机制——为什么它能看一页取一页背后依靠的HTTP协议能力、PDF文件结构以及你自己实现传输层时需要格外小心的地方。如果你也遇到大PDF加载慢、白屏久、内存爆炸这类问题或者你压根就是想搞懂PDF.js内部的分片逻辑这篇都值得认真看完。1. 大PDF在网页里打不开的真相从一次真实故障说起1.1 一个300MB的PDF把浏览器整卡死了当时接到的需求原话是把公司这份政策汇编做成网页版用户可以翻页浏览不要下载整本。我一开始也没多想直接用PDF.js默认方式加载PDF结果测试环境一跑问题全来了。文件大概280MB包含几千页扫描件加文字层第一次打开白屏了将近一分钟期间浏览器标签页一直转圈CPU占用率飙到100%。等最终渲染出来滚动到中间页时页面开始掉帧切几页之后整个标签页直接无响应。这个场景我估计不少人都遇过。很多人第一反应是PDF.js不行其实冤枉它了。PDF.js默认的加载策略是流式读取会把整个文件从头到尾通过Range请求一块块拉到本地拉完解析完才会渲染页面。换句话说它虽然不要求你把文件全下到内存里一次性渲染但会把所有数据通过网络传输并缓存到本地直到文件全部获取完毕才开始真正渲染。对大文件来说这种默认策略有两个致命问题。一是网络等待时间不可控文件越大首屏可交互时间越长二是内存占用会随着文件大小线性上涨宿主机的内存一旦不够浏览器就开始崩溃。1.2 为什么先转图片的方案也不好用我一度考虑过后端转图片、前端只展示PNG的方案。把PDF每一页转成图片丢给前端看着是解决了渲染压力实际上引入了一堆新麻烦文字层直接没了用户没法搜索和复制转图质量受DPI限制放大就糊高清图上页渲染会非常吃GPU和带宽而且每页图片得单独生成和缓存后端存储和任务调度的复杂度直接拉满。所以我最终还是回到PDF.js但需要搞清楚一个问题能不能让PDF.js不下载完整文件只加载当前用户正在看的页面答案是可以这就牵扯到PDF.js的按需分片加载机制以及HTTP协议里的Range能力。2. 按需分片的核心Range请求与PDF.js的懒加载机制2.1 PDF文件格式天然支持按地址读取先说一个很多人不知道的点PDF这种文件格式天生就是为随机读取设计的。PDF的内部结构大概分成几层文件头、若干对象页面对象、字体对象、图像对象等、交叉引用表xref相当于整本PDF的目录索引以及文件尾部的trailer。PDF.js在解析一份PDF时并不是从头到尾把每个字节都读完而是读取trailer拿到交叉引用表的起始偏移量再从交叉引用表里查到每个对象在文件中的具体偏移位置最后按需读取目标对象。这个结构跟我们读纸质字典的思路很像。你要查一个字先翻到目录页xref查到之后直接翻到对应的页码文件偏移而不是把整本字典从头翻到尾。理解了这一点你就明白为什么PDF能做到按需加载——因为PDF.js知道目标页面对应的对象在文件里大概哪一段只需要通过Range请求把那一段数据拿回来就行。2.2 PDF.js如何用Range请求做到看哪页取哪页PDF.js底层封装了一个NetworkManager它会在加载PDF时先发出一个探测请求检查服务器是否支持Range请求。如果响应头里有Accept-Ranges: bytes并且返回了Content-RangePDF.js就会切换到Range模式后续按需发起Range: bytesxxx-yyy的请求。如果服务器不支持Range它只能退化成整包下载。在这个过程中有一个特别重要的类叫PDFDataRangeTransport。默认情况下你直接给getDocument传URL它内部会自动帮你走这个流程。但如果你想完全掌控请求过程——比如加上身份认证、加上统计埋点、从自己的接口拿数据、或者加载的不是静态文件而是动态生成的PDF——就需要自己实现这个Transport把请求range数据这件事接管过来。我这一版方案里正是自己实现了Transport一方面是要在后端加权限校验另一方面是为了方便后续接入阅读位置记录所以没有走默认的URL自动探测而是自己控制每一步。2.3 disableAutoFetch和rangeChunkSize的正确理解如果你只是想让PDF.js别一次性把数据全拉回来最省事的方式是配置两个参数。const loadingTask pdfjsLib.getDocument({ url: /files/big.pdf, disableAutoFetch: true, rangeChunkSize: 262144 });disableAutoFetch: true的意思是告诉PDF.js非必要不要去预取数据只有渲染当前页面需要的对象数据才去请求。这个参数在很多优化文章里都被说成按需加载的关键它确实有用但要注意它不等同于完全不预取。为了完成渲染PDF.js还是可能拉取一些公共资源比如字体、AcroForm、目录元数据等。所以别指望打开第一页时只发一个请求实际会有几个小请求。rangeChunkSize是每次Range请求的字节数。默认值在不同版本里略有不同大体在64KB到2MB之间。这个值不是越大越好也不是越小越好。设置太小比如32KB会导致滚动翻页时请求频率爆炸TCP连接都来不及建立设置太大比如10MB又等于整包下载失去了分片的意义。我实测下来局域网内2MB很流畅公网环境下1MB比较稳妥弱网/移动网络场景建议256KB~512KB。如果你决定自己实现Transport这两个参数同样有效因为getDocument加载流程是同一个只是数据来源换成你自定义的传输层。3. 前端核心改造从getDocument到PDFDataRangeTransport3.1 常规加载写法以及它的边界先把最基础的写法贴出来大家对比一下后面的方案。import * as pdfjsLib from pdfjs-dist; import pdfjs-dist/web/pdf_viewer.css; pdfjsLib.GlobalWorkerOptions.workerSrc /worker/pdf.worker.min.js; async function loadPdfWithUrl(url) { const loadingTask pdfjsLib.getDocument({ url: url, disableAutoFetch: true, rangeChunkSize: 1024 * 1024 }); const pdf await loadingTask.promise; return pdf; }这种写法在多数中小文件上没有问题PDF.js自动探测Range、自动分片。可一旦需求复杂起来比如我需要知道当前加载了多少比例、需要带上鉴权token、需要在后端做访问审计这种封装就兜不住。因为PDF.js内部发出的请求你不好插一脚它也不会带上你自定义的请求头。3.2 自定义PDFDataRangeTransport的具体实现所以我的做法是绕开URL加载直接传一个自定义Transport给getDocument。import * as pdfjsLib from pdfjs-dist; pdfjsLib.GlobalWorkerOptions.workerSrc /worker/pdf.worker.min.js; export class RangeTransport extends pdfjsLib.PDFDataRangeTransport { constructor(totalLength, initialData, apiBase) { super(totalLength, initialData); this.apiBase apiBase; this.loadedBytes 0; this.totalBytes totalLength; } requestDataRange(begin, end) { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 30000); fetch(${this.apiBase}/api/pdf/range?fileIdxxxbegin${begin}end${end}, { credentials: include, signal: controller.signal }) .then(response { if (!response.ok) throw new Error(Range request failed); return response.arrayBuffer(); }) .then(buffer { clearTimeout(timeoutId); this.loadedBytes buffer.byteLength; // 关键必须调用这两个回调PDF.js渲染流程依赖它们 this.onDataProgress(begin, new Uint8Array(buffer)); }) .catch(err { clearTimeout(timeoutId); if (this.onDataError) { this.onDataError(begin, err); } }); } } async function loadPdfWithTransport(apiBase) { // 第一步先向后端元数据接口拿文件总大小 const metaRes await fetch(${apiBase}/api/pdf/meta?fileIdxxx, { credentials: include }); const meta await metaRes.json(); const transport new RangeTransport(meta.size, null, apiBase); const loadingTask pdfjsLib.getDocument({ data: transport, disableAutoFetch: true, rangeChunkSize: 1024 * 1024 }); return loadingTask.promise; }这里有几个关键点需要注意。requestDataRange(begin, end)是传输层的核心接口PDF.js在内部需要某段数据时会回调这个方法参数就是文件中的绝对字节偏移。你在这个方法里要做的就一件事从服务端拿到对应字节段然后调this.onDataProgress(begin, new Uint8Array(buffer))把数据交给PDF.js。一定要以Uint8Array传给回调这个类型如果传错后面解析会直接报奇怪的类型错误。this.onDataError(begin, err)是错误回调强烈建议实现。PDF.js渲染遇到数据请求失败时会走这个回调你可以在这里做错误上报也可以触发UI上的重试按钮。如果不传PDF.js可能卡在一个真空等待的状态页面表现就是白屏卡住非常难排查。我用AbortController给请求加了30秒超时这是从生产环境学到的一个硬经验。弱网环境下大量的Range请求很容易有一个卡住不返回如果不超时整个PDF.js的加载流程会被这个吊死的请求阻塞页面就一直白屏。加上超时后至少能触发错误回调给用户一个重新加载的反馈路径。3.3 监控加载进度和渲染状态的技巧自定义Transport之后你还可以拿到一个非常有用的信息已加载字节数。上面代码里我维护了this.loadedBytes每次收到数据就累加。你可以把它做成一个进度提示transport.onProgress (loaded, total) { // 这里是PDF.js的回调注意和自己维护的loadedBytes区分 updateProgressBar(loaded, total); };PDFDataRangeTransport自带一个onProgress回调PDF.js内部会周期性调用参数是已加载字节数和总字节数。不过我实测发现它更新的频率不太稳定而且必须等底层有数据流转才会触发。所以我在自定义Transport里自己记录loadedBytes配合一个定时器实现更流畅的进度动画。前端渲染环节用pdfjsLib的PDFViewer和EventBus来承载页面展示这样能省去自己维护canvas渲染的琐碎逻辑。const appContainer document.getElementById(viewer); const eventBus new pdfjsLib.EventBus(); const viewer new pdfjsLib.PDFViewer({ container: appContainer, eventBus, removePageBorders: true }); eventBus.on(pagesinit, () { viewer.currentPageNumber 1; }); eventBus.on(pagechanging, (e) { const currentPage e.pageNumber; console.log(现在看到第, currentPage, 页); // 这里可以触发阅读位置保存后面单独说 });这样搭建出来的前端用户翻页时PDF.js会自动调用Transport请求尚未加载的数据段。也就实现了真正意义上的看哪页取哪页。4. 后端支撑给PDF文件接口加上Range响应能力4.1 用Node.js/Express实现Range请求接口自定义Transport之后后端接口就需要自己写了。核心是正确处理HTTP的Range头。我用的是Node.js Express实现一个支持Range的PDF接口。这里有一点要说明Express默认的res.sendFile和express.static其实已经内置了Range支持如果你只是想把一个磁盘上的静态PDF文件给前端直接用这两个就够了不需要手工解析Range。我之所以还自己解析Range是因为生产环境里PDF路径来自数据库配置而且还要在这个接口里做权限校验、访问审计和某些动态处理直接托管静态文件的方式不够灵活。const express require(express); const fs require(fs); const path require(path); const router express.Router(); // 元数据接口让前端知道文件大小 router.get(/api/pdf/meta, async (req, res) { const filePath path.resolve(__dirname, ../files/big.pdf); const stat await fs.promises.stat(filePath); res.json({ size: stat.size, fileName: big.pdf, contentType: application/pdf }); }); // 核心的Range接口 router.get(/api/pdf/range, async (req, res) { const filePath path.resolve(__dirname, ../files/big.pdf); const begin parseInt(req.query.begin, 10); const end parseInt(req.query.end, 10); if (isNaN(begin) || isNaN(end) || begin 0 || end begin) { return res.status(400).json({ error: invalid range }); } const stat await fs.promises.stat(filePath); if (begin stat.size) { return res.status(416).json({ error: range not satisfiable }); } // 关键接收到的end可能超过文件大小做一下收敛 const safeEnd Math.min(end, stat.size - 1); const chunkSize safeEnd - begin 1; res.writeHead(206, { Content-Type: application/pdf, Content-Length: chunkSize, Content-Range: bytes ${begin}-${safeEnd}/${stat.size}, Accept-Ranges: bytes }); const stream fs.createReadStream(filePath, { start: begin, end: safeEnd }); stream.pipe(res); }); module.exports router;有几个细节特别重要。Content-Range头必须写对格式bytes 起始-结束/总长度这是HTTP协议规定的PDF.js和浏览器都靠这个头判断返回的数据是不是预期的。Accept-Ranges: bytes是给PDF.js探测用的这个头缺失的话PDF.js会认为服务器不支持Range退化成整包下载。状态码必须是206 Partial Content如果你返回200PDF.js也可能根据Content-Range头去拼接数据但浏览器层面的行为就容易出问题实测中最好严格保持206。4.2 解决跨域和预检请求实际部署时前端和服务端大概率不在同一个域名下跨域问题必须处理好。router.use((req, res, next) { res.setHeader(Access-Control-Allow-Origin, https://your-frontend-domain.com); res.setHeader(Access-Control-Allow-Methods, GET, HEAD, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Range, Content-Type, Authorization); res.setHeader(Access-Control-Expose-Headers, Content-Range, Accept-Ranges, Content-Length); res.setHeader(Access-Control-Allow-Credentials, true); if (req.method OPTIONS) { return res.sendStatus(200); } next(); });这里最容易被忽略的是Access-Control-Expose-Headers。如果你只设置了Access-Control-Allow-Origin前端JavaScript是读不到Content-Range和Accept-Ranges这些响应头的因为浏览器默认只暴露一小部分安全响应头其他自定义头必须通过Expose-Headers显式暴露。PDF.js内部要检查Content-Range来判断服务器是否支持Range如果这个头被浏览器屏蔽了PDF.js一样会认为服务端不支持Range然后走整包下载路径。这个坑我一开始也踩过排查了半天后来用curl模拟请求发现响应头都在但网页里就是整包下载最后才定位到是跨域暴露头的锅。4.3 不是所有服务器都支持Range如何检测与降级如果你的PDF文件部署在第三方对象存储或者某些CDN后面事情就没那么可控了。不是所有存储服务都支持Range请求尤其是那些做了额外转发层、或者对Range头处理不规范的网关。检测方法很简单用curl手动发一个Range请求看看响应curl -I -H Range: bytes0-1023 https://your-cdn.example.com/big.pdf如果响应头里有Accept-Ranges: bytes和Content-Range: bytes 0-1023/xxxxxxxx说明这条路是通的。如果返回200且没有Content-Range基本可以判定不支持Range。遇到不支持的存储后端我的做法是套一层自己的Node服务做代理。Node服务从底层存储读文件流自己拼HTTP头的Range逻辑也就是上一节写的那套代码。这样PDF.js面对的还是自家后端接口可以实现Range底层存储是什么就不重要了。代价是中国服务器带宽和存储读IO但这个取舍在很多场景下是值得的。另外如果你的PDF文件体积实在太大比如超过2GB还要考虑到文件系统单文件大小限制、Node.js对大文件读取时流式处理的稳定性等这时建议直接用Nginx的X-Accel-Redirect或者云厂商的对象存储Range能力不要把读取压力都压在Node进程里。5. 实测验证与排查这样确认分片真的生效了5.1 DevTools里看206还要看什么写完了前后端不是能打开页面就完事了你得验证分片加载是不是真的在按需工作。打开Chrome DevTools的Network面板过滤请求类型为Fetch/XHR。加载一个几百MB的大PDF如果方案生效你会看到一串请求状态码大多是206每条请求的Headers里都带Range: bytesxxx-yyyResponse Headers里带Content-Range: bytes 0-65535/300000000这种类似的响应。更进一步的验证方法是观察滚动页面时请求的变化。打开PDF之后先停在第一页记录下Network里已有的请求数量然后直接跳转到第200页。如果此时新增了一批Range请求并且这些请求的偏移量范围跟第200页的内容区域大致对应说明按需分片确实在起作用。如果跳转页面时请求数没有任何变化那说明页面数据早就被预取完了你得回头检查是不是disableAutoFetch没生效或者文件太小没触发分片逻辑。5.2 识别分片了但没分片的假象有一种情况特别迷惑人。打开Network面板看到满屏的Range请求你觉得分片策略工作正常但实际上PDF.js还在暗地里把整个文件都拉回来了。具体表现是虽然请求是分段的但请求数量非常多而且请求区间从头到尾全覆盖一个不落。这种情况通常出在rangeChunkSize设置太小或者disableAutoFetch没有设为true。PDF.js内部在disableAutoFetch为false时会让NetworkManager持续向后请求直到把文件剩余部分全部拉完。你看到的Range请求虽然也是分片的但本质上是流式全量下载只是把下载动作拆碎了。要验证是否全量下载可以直接看浏览器底部状态栏——在自定义Transport方案下如果你自己维护了loadedBytes可以把它和总文件大小对比。我最初测试时就是用的这个方法发现一加载完第一页loadedBytes就以非常快的速度逼近总大小这才意识到disableAutoFetch没有配置上。5.3 常见坑CDN缓存、代理服务器和移动网络分片加载在局域网内测得很顺畅不代表上线到生产环境后就一定没问题。我实际部署中遇到过几个坑。一个是CDN缓存。有些CDN节点对带Range头的请求处理不标准如果CDN缓存了某个Range响应用户后续请求同一范围的字节时可能返回的是一个固定缓存块而不是根据当前文件状态计算的正确范围。表现就是前端渲染缺页、缺图片、或者页面数据错乱。解决办法是在CDN控制台关闭对该PDF路径的缓存或者配置CDN透传Range头。另一个是公司内网的Web代理。某些代理服务器会把Range头剥掉或者把206响应缓存成200导致PDF.js走不了分片。这个问题排查起来很痛苦因为你本地直连正常但走代理就废。我当时是通过对比代理模式和直连模式下的Network请求头差异才定位的。移动网络下还有一个典型的弱网问题TCP连接频繁断开Range请求一直超时重试。这时需要结合前面说的AbortController超时机制并且在前端给用户体验一个加载稍慢的过渡提示。如果文件实在太大且用户网络太差建议退化成纯下载按钮别死磕在线预览。6. 加分项把阅读到第几页记录下来说到网络热词里那句pdf.js如何把阅读到哪一页记录到数据库里这个需求几乎每个阅读类项目都会有。在我们这套分片加载框架下实现起来很简单只需要利用PDFViewer的pagechanging事件。6.1 前端采集阅读位置const SAVE_DELAY 1500; let saveTimer null; eventBus.on(pagechanging, (e) { const currentPage e.pageNumber; // 防抖避免用户快速翻页时疯狂打接口 clearTimeout(saveTimer); saveTimer setTimeout(() { saveReadingPosition(currentPage); }, SAVE_DELAY); }); function saveReadingPosition(page) { fetch(/api/position, { method: POST, headers: { Content-Type: application/json }, credentials: include, body: JSON.stringify({ fileId: xxx, page: page, timestamp: Date.now() }) }); }防抖这里我用了1.5秒的延迟。用户翻页是一个高频操作如果每换一页都立即写库QPS会很难看也没有必要。1.5秒足够平滑掉连续翻页的情况用户停留超过1.5秒才记录一次。还有一个细节是pagechanging事件在滚动页面时会频繁触发但它本质上只是当前可见页编号的变化不代表用户一定阅读了那一页。如果要做更精准的阅读记录可以结合页面可见时间比如连续停留超过5秒才落库。但对大多数场景pagechanging已经够用了。6.2 后端存储与恢复接口后端我用一个简单的positions表来存阅读位置这里假设是MongoDB但换成MySQL也一样。router.post(/api/position, async (req, res) { const { fileId, page } req.body; const userId req.user?.id || anonymous; await db.collection(positions).updateOne( { fileId, userId }, { $set: { page, updatedAt: new Date() } }, { upsert: true } ); res.json({ ok: true }); }); router.get(/api/position/:fileId, async (req, res) { const userId req.user?.id || anonymous; const record await db.collection(positions).findOne({ fileId: req.params.fileId, userId }); res.json(record ? { page: record.page } : { page: 1 }); });前端恢复位置时要注意一个点PDF文档刚加载完成时就直接设currentPageNumber不一定能直接渲染出那一页因为可能正处于分片加载的早期阶段那一页的数据还没取回来。我的处理方法是在pagesinit事件后再跳转。eventBus.on(pagesinit, async () { const res await fetch(/api/position/xxx, { credentials: include }); const data await res.json(); const savedPage data.page || 1; viewer.currentPageNumber savedPage; });如果跳转后目标页的数据还没有加载完PDFViewer会先显示一个空白占位等Transport层把数据拉回来再渲染。这个过程用户基本无感知因为Range请求很快但如果网络不好最好配合一个loading遮罩避免用户以为页面卡死了。6.3 分片加载模式下的独特注意点保存并把恢复搞得比较优雅之后有一个分片加载特有的坑值得单独拎出来说跳页时目标页数据还没拉取而此时正处于Transport的加载队列里如果你又快速跳回第一页可能会打乱PDF.js内部的请求调度。我遇到过的情况是用户上次读到第180页打开PDF后自动跳转到180页然后用户马上点回到首页结果首页渲染了好几分钟。排查后定位原因跳转到180页时Transport开始了一系列大范围Range请求回首页时PDF.js还在等待之前的请求返回新页面的渲染被排队队列卡住了。解决办法是在自定义Transport里做一个小小的高级处理——记录当前正在进行的请求并为其维护一个优先级标记。当用户切换到某页时立刻发起该页面对应偏移范围的请求并把它插入到待处理列表的前面。不过PDFDataRangeTransport的底层回调机制没有暴露优先级这层概念实际工程里更难控制的是onDataProgress的调用顺序。简单粗暴的做法是用户在跳转后如果遇到了长时间空白触发一次viewer.currentPageNumber的重新赋值用重渲染「顶」一下队列。虽然不完美但实测能缓解大部分场景。7. 项目源码结构和扩展思路7.1 前后端源码目录一览整套项目的源码结构如下你可以根据自己的需要做减法。pdf-sharding-demo/ ├── frontend/ │ ├── index.html │ ├── main.js │ ├── range-transport.js │ ├── viewer.js │ └── worker/ │ └── pdf.worker.min.js ├── server/ │ ├── app.js │ ├── routes/ │ │ ├── pdf.js │ │ └── position.js │ └── files/ │ └── big.pdf └── README.mdfrontend/range-transport.js是前端最核心的文件里面实现了RangeTransport类。frontend/main.js负责任务装配包括初始化PDFViewer、绑定事件、加载Transport、监听进度。server/app.js负责启动Express服务、开启跨域配置、挂载路由。server/routes/pdf.js实现了上面说的元数据接口和Range接口。这个结构特别适合拿来做二次开发。你主项目如果是Vue或React可以直接把RangeTransport抽成一个工具类放在公共模块里然后在业务组件里调用。PDFViewer这个UI组件如果要深度集成建议看看PDF.js官方自带的web/viewer.html它是个完整可上手的阅读器外壳把它的viewer.html拷贝到你的工程里替换其中的加载逻辑就能得到一个功能完整的网页PDF阅读器。7.2 可以继续扩展的方向这个项目跑通之后我后续还做了一些扩展觉得挺有价值的列出来供你参考。第一个是给Range请求加鉴权token。由于是我们自己实现的Transport在requestDataRange里可以轻松给每个fetch请求的headers里塞上Authorization字段。静态文件托管的普通方案做不了这个因为它没法对文件内部某一段数据做权限控制。第二个是把Range请求的响应缓存到Redis里。同一段字节如果被多个用户请求直接从Redis读可以节省大量的磁盘IO。不过要注意缓存过期策略因为PDF文件一旦被覆盖所有Range缓存都必须失效。我当时的做法是给PDF加一个内容哈希文件名携带哈希值文件更新后URL变化CDN和Redis缓存自然失效。第三个是把阅读位置解析成阅读深度。前端除了记录页码还可以配合Transport的进度数据判断当前页面数据的加载百分比。用户打开到第300页但这一页只有30%的数据被加载了说明用户可能只浏览了局部。这种数据可以为运营团队提供非常有价值的报告。第四个是给超大PDF做按章加载。如果文件是几千页的报告可以先让后端解析目录结构前端只加载目录信息和当前章节对应的对象范围用户点击某一章再按需加载该章页面。这种方案比纯按页分片加载的体验好很多特别适合电子书场景。实现思路也不复杂后端在PDF解析时提取出书签大纲把章节目录和页区间映射起来前端跳页时按章节范围设置Transport的请求范围即可。我自己的体会是按需分片加载这套方案并不是什么黑魔法核心就是对HTTP Range的合理利用加上对自己数据通道的控制。只要把PDF的结构理解清楚把Transport的回调关系理顺后续的扩展就是水到渠成的事。希望这篇能帮你少走几步弯路。
返回列表