加载中...

vue3中如何使用pdfjs来展示pdf文档

vue3中如何使用pdfjs来展示pdf文档

New Image

更新:

摘要

在项目开发中碰到一个需求是在页面中展示pdf预览功能,本人的项目使用的是vue3,实现pdf预览使用的是pdf预览神器 pdfjs-dist

以下,将详细介绍如何在项目中使用pdfjs,主要包括以下内容:

  • 单页pdf加载
  • 多页pdf加载
  • pdf页面懒加载

👉直达完整源码:

参考资料: pdfjs源码及使用文档

1. 准备工作

1.1 pdfjs-dist 安装

百度搜索 npm pdfjs-dist,进入npm官方网站,即可查看pdfjs的安装方法:

New Image

安装命令:

bash
代码解读
复制代码
npm i pdfjs-dist

2. 在vue3中使用pdfjs-dist查看pdf文档

2.1 基本页面代码

首先把基本的页面代码准备起来,具体代码如下:

js
代码解读
复制代码
<template> <div class="pdf-container"> <canvas id="pdf-canvas"></canvas> </div> </template> <script lang="ts" setup> import * as PDFJS from "pdfjs-dist"; import * as PdfWorker from "pdfjs-dist/build/pdf.worker.js"; import { nextTick, onMounted, ref } from "vue"; import PdfBook from "@/assets/JavaScript.pdf"; window.pdfjsWorker = PdfWorker; // 在 vite4.x 及以上版本需要显示指定 let pdfDoc: any = null; const pdfPages = ref(0); const pdfScale = ref(1.5); const pdfContainerRef = ref<HTMLElement | null>(null); </script>

2.2 pdfjs工作原理简述

pdfjs展示pdf文档的原理,实际上是将pdf中的内容渲染到解析,然后渲染到 canvas 中进行展示,因此我们使用pdfjs渲染出来的pdf文件,实际上是一张张canvas图片。

2.3 pdf文件展示(单页)

pdfjs的使用主要涉及到2个方法,分别是loadFile()renderPage()

2.3.1 loadFile()

loadFile() 主要用来加载pdf文件,其实现如下:

js
代码解读
复制代码
const loadFile = (url: any) => { // 设定pdfjs的 workerSrc 参数 PDFJS.GlobalWorkerOptions.workerSrc = PdfWorker; const loadingTask = PDFJS.getDocument(url); loadingTask.promise .then(async (pdf: any) => { pdf.loadingParams.disableAutoFetch = true; pdf.loadingParams.disableStream = true; pdfDoc = pdf; // 保存加载的pdf文件流 pdfPages.value = pdfDoc.numPages; // 获取pdf文件的总页数 await nextTick(() => { renderPage(1); // 将pdf文件内容渲染到canvas }); }) .catch((error: any) => { console.warn(`[upthen] pdfReader loadFile error: ${error}`); }); };

以上代码因为使用了 ts ,有部分函数参数类型的设定,在使用过程中,如遇到ts的报错,可以直接把类型设置为 any.

需要注意:

  1. 以上部分的 workerSrc 赋值部分,需要特别注意,在pdfJs的使用示例中明确指出, workerSrc 的值要手动指定,如果没有指定此值,则会出现 workerSrc 未定义的bug, 另外,要注意,赋值时一定要赋值为 pdf.worker.entry (以entry结尾,表示入口)

New Image

  1. 在使用 vite4.x 的情况下,因为 vite 某些打包方面的升级,需要显式给 window.pdfjsWorker 赋值,详情可查看这篇文章:pdfjs-dist 在vite4.x 下的使用问题 - 掘金 (juejin.cn)
2.3.2 renderPage()

renderPage() 方法主要用来将pdf文件的内容渲染到canvas上,其实现如下:

js
代码解读
复制代码
// num 表示渲染第几页 renderPage (num: any): void { pdfDoc.getPage(num).then((page: any) => { const canvas: any = document.getElementById(''pdf-canvas'') // 获取页面中的canvas元素 // 以下canvas的使用过程 const ctx: any = canvas.getContext(''2d'') const dpr = window.devicePixelRatio || 1 const bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1 const ratio = dpr / bsr const viewport = page.getViewport({ scale: pdfScale.value }) // 设置pdf文件显示比例 canvas.width = viewport.width * ratio canvas.height = viewport.height * ratio canvas.style.width = viewport.width + ''px'' canvas.style.height = viewport.height + ''px'' ctx.setTransform(ratio, 0, 0, ratio, 0, 0) // 设置当pdf文件处于缩小或放大状态时,可以拖动 const renderContext = { canvasContext: ctx, viewport: viewport } // 将pdf文件的内容渲染到canvas中 page.render(renderContext) }) }

2.4 完整实现代码

点击展开/折叠代码
javascript
代码解读
复制代码
<template> <div class="pdf-container"> <canvas id="pdf-canvas"></canvas> </div> </template> <script lang="ts"> import * as PDFJS from "pdfjs-dist"; import * as PdfWorker from "pdfjs-dist/build/pdf.worker.js"; import { nextTick, onMounted, ref } from "vue"; import PdfBook from "@/assets/JavaScript.pdf"; window.pdfjsWorker = PdfWorker; let pdfDoc: any = null; const pdfPages = ref(0); const pdfScale = ref(1.5); const pdfContainerRef = ref<HTMLElement | null>(null); const loadFile = (url: any) => { // 设定pdfjs的 workerSrc 参数 PDFJS.GlobalWorkerOptions.workerSrc = PdfWorker; const loadingTask = PDFJS.getDocument(url); loadingTask.promise .then(async (pdf: any) => { pdf.loadingParams.disableAutoFetch = true; pdf.loadingParams.disableStream = true; pdfDoc = pdf; // 保存加载的pdf文件流 pdfPages.value = pdfDoc.numPages; // 获取pdf文件的总页数 await nextTick(() => { renderPage(1); // 将pdf文件内容渲染到canvas }); }) .catch((error: any) => { console.warn(`[upthen] pdfReader loadFile error: ${error}`); }); }; const renderPage = (num: any) => { pdfDoc.getPage(num).then((page: any) => { page.cleanup(); const canvas: any = document.getElementById(`pdf-canvas`); if (canvas) { const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; const bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; const ratio = dpr / bsr; const viewport = page.getViewport({ scale: pdfScale.value }); canvas.width = viewport.width * ratio; canvas.height = viewport.height * ratio; canvas.style.width = viewport.width + "px"; canvas.style.height = viewport.height + "px"; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); const renderContext = { canvasContext: ctx, viewport: viewport, }; page.render(renderContext); } }); }; onMounted(() => { loadFile(PdfBook); }); </script>

2.5 效果

New Image

3.多页pdf加载

接下来记录如何实现多页pdf展示,

3.1 基本思路

多页的实现主要基于单页pdf。单页pdf中,renderPage传入的参数 num 正是pdf文档的页数。renderPage方法首先获取template中的canvas元素,然后从pdf文件中解析出第 num 页的内容,将pdf文件的内容渲染到canvas画布上。那么多页pdf只需要先根据pdf文档的页数,生成多个canvas画布,然后在渲染pdf文件的时候,只需要根据num去获取对应的 canvas 画布和对应的pdf文件内容,将pdf内容渲染到canvas上就可以了。在加载pdf文件的时候,从第1页开始渲染,然后递归调用渲染函数,在每一次调用渲染函数的末尾,都将 num 的值加1,然后继续调用renderPage方法,直到所有的pdf页面渲染完毕为止。

下面看下具体的代码实现:

3.2 实现代码

  • template 部分
点击展开/折叠代码
html
代码解读
复制代码
<template> <div class="pdf-container" ref="pdfContainerRef"> <canvas v-for="pageIndex in pdfPages" :id="`pdf-canvas-${pageIndex}`" :key="pageIndex" /> </div> </template> <script setup lang="ts"> import * as PDFJS from "pdfjs-dist"; import * as PdfWorker from "pdfjs-dist/build/pdf.worker.js"; import { nextTick, onMounted, ref } from "vue"; import PdfBook from "@/assets/JavaScript.pdf"; window.pdfjsWorker = PdfWorker; let pdfDoc: any = null; const pdfPages = ref(0); const pdfScale = ref(1.5); const pdfContainerRef = ref<HTMLElement | null>(null); const loadFile = (url: any) => { // 设定pdfjs的 workerSrc 参数 PDFJS.GlobalWorkerOptions.workerSrc = PdfWorker; const loadingTask = PDFJS.getDocument(url); loadingTask.promise .then(async (pdf: any) => { pdf.loadingParams.disableAutoFetch = true; pdf.loadingParams.disableStream = true; pdfDoc = pdf; // 保存加载的pdf文件流 pdfPages.value = pdfDoc.numPages; // 获取pdf文件的总页数 await nextTick(() => { renderPage(1); // 将pdf文件内容渲染到canvas }); }) .catch((error: any) => { console.warn(`[upthen] pdfReader loadFile error: ${error}`); }); }; const renderPage = (num: any) => { pdfDoc.getPage(num).then((page: any) => { page.cleanup(); if (pdfContainerRef.value) { pdfScale.value = pdfContainerRef.value.clientWidth / page.view[2]; } const canvas: any = document.getElementById(`pdf-canvas-${num}`); if (canvas) { const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; const bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; const ratio = dpr / bsr; const viewport = page.getViewport({ scale: pdfScale.value }); canvas.width = viewport.width * ratio; canvas.height = viewport.height * ratio; canvas.style.width = viewport.width + "px"; canvas.style.height = viewport.height + "px"; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); const renderContext = { canvasContext: ctx, viewport: viewport, }; page.render(renderContext); if (num < pdfPages.value) { renderPage(num + 1); } } }); }; onMounted(() => { loadFile(PdfBook); }); </script> <style scoped> .pdf-container { height: 100%; overflow-y: scroll; overflow-x: hidden; canvas { display: flex; flex-direction: column; align-items: center; justify-content: center; } } </style>

3.3 效果

🤣pdf页数较多时加载有点慢。

New Image

4 懒加载pdf页面

4.1 核心思路

前面简单讲过,pdfjs-dist 展示pdf的实现思路是将pdf页面绘制到 canvas 上,canvas 本质上还是图片,所以同样按照懒加载图片的思路实现。具体实现思路如下:

  • 页面加载时仅渲染少量页面,3 页或者 5 页;
  • 监听页面滚动事件(addEventListener(''scroll'', () => {})),当页面滚动到距离底部一定距离时,触发加载方法,动态加载一定数量的页面;
  • 当页面加载完毕或离开页面时,销毁事件监听;

4.2 代码实现

点击展开/折叠代码
html
代码解读
复制代码
<template> <div class="on-demand-pdf-container" ref="pdfContainerRef"> <canvas v-for="pageIndex in renderedPages" :id="`pdf-canvas-${pageIndex}`" :key="pageIndex" /> </div> </template> <script setup lang="ts"> // 为了优化加载性能,这2个文件可以在页面挂载时动态导入 // import * as PDFJS from "pdfjs-dist"; // import * as PdfWorker from "pdfjs-dist/build/pdf.worker.js"; import { nextTick, onMounted, ref, computed, onUnmounted } from "vue"; import PdfBook from "@/assets/JavaScript.pdf"; let pdfDoc: any = null; const pdfPages = ref(0); const pdfScale = ref(1.5); const pdfContainerRef = ref<HTMLElement | null>(null); const loadedNum = ref(0); const preloadNum = computed(() => { return pdfPages.value - loadedNum.value > 3 ? 3 : pdfPages.value - loadedNum.value; }); const loadFished = computed(() => { const loadFinished = loadedNum.value + preloadNum.value >= pdfPages.value; if (loadFinished) { removeEventListeners(); } return loadFinished; }); const renderedPages = computed(() => { return loadFished.value ? pdfPages.value : loadedNum.value + preloadNum.value; }); let loadingTask; const renderPage = (num: any) => { pdfDoc.getPage(num).then((page: any) => { page.cleanup(); if (pdfContainerRef.value) { pdfScale.value = pdfContainerRef.value.clientWidth / page.view[2]; } const canvas: any = document.getElementById(`pdf-canvas-${num}`); if (canvas) { const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; const bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; const ratio = dpr / bsr; const viewport = page.getViewport({ scale: pdfScale.value }); canvas.width = viewport.width * ratio; canvas.height = viewport.height * ratio; canvas.style.width = viewport.width + "px"; canvas.style.height = viewport.height + "px"; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); const renderContext = { canvasContext: ctx, viewport: viewport, }; page.render(renderContext); if (num < loadedNum.value + preloadNum.value && !loadFished.value) { renderPage(num + 1); } else { loadedNum.value = loadedNum.value + preloadNum.value; } } }); }; const initPdfLoader = async (loadingTask: any) => { return new Promise((resolve, reject) => { loadingTask.promise .then((pdf: any) => { pdf.loadingParams.disableAutoFetch = true; pdf.loadingParams.disableStream = true; pdfDoc = pdf; // 保存加载的pdf文件流 pdfPages.value = pdfDoc.numPages; // 获取pdf文件的总页数 resolve(true); }) .catch((error: any) => { reject(error); console.warn(`[upthen] pdfReader loadFile error: ${error}`); }); }); }; const distanceToBottom = ref(0); const calculateDistanceToBottom = () => { if (pdfContainerRef.value) { const containerHeight = pdfContainerRef.value.offsetHeight; const containerScrollHeight = pdfContainerRef.value.scrollHeight; distanceToBottom.value = containerScrollHeight - containerHeight - pdfContainerRef.value.scrollTop; console.log(distanceToBottom.value); } }; const lazyRenderPdf = () => { calculateDistanceToBottom(); if (distanceToBottom.value < 1000) { renderPage(loadedNum.value); } }; const removeEventListeners = () => { pdfContainerRef.value?.removeEventListener("scroll", () => { lazyRenderPdf(); }); }; onMounted(async () => { // 设定pdfjs的 workerSrc 参数 let PDFJS = await import("pdfjs-dist"); window.pdfjsWorker = await import("pdfjs-dist/build/pdf.worker.js"); PDFJS.GlobalWorkerOptions.workerSrc = window.pdfjsWorker; loadingTask = PDFJS.getDocument(PdfBook); if (await initPdfLoader(loadingTask)) { renderPage(1); } pdfContainerRef.value.addEventListener("scroll", () => { lazyRenderPdf(); }); }); onUnmounted(() => { removeEventListeners(); }); </script> <style lang="scss" scoped> .on-demand-pdf-container { height: 100%; overflow-y: scroll; overflow-x: hidden; canvas { display: flex; flex-direction: column; align-items: center; justify-content: center; } } </style>

4.3 效果展示

😊秒出

New Image

5 分片加载

前面提到一些在项目中使用 pdfjs-dist 渲染 pdf 文件的方案,适合渲染页码比较少的 pdf 文件,对于较大的 pdf 文件,比如几十到上百兆,不适合一次性加载,而是需要分片加载。这种方案需要服务端接口配合实现文件的分片。

此前因为受限于个人时间以及后端能力的掌握程度,一直没有实现过这种方案,最近在项目中因为使用以上的方案时,在 pdf 文件较大时,出现了一些性能问题, 不得不回来继续考虑实现 分片加载渲染 pdf 的方式。

得益于 ChatGPT 的发展,我很方便的构建了一个基于 express 的后端服务,并让其帮我实现了 pdf 文件分片的接口。因此终于可以把我一直想补齐的这一块内容实现了。不过,听同事说直接使用 nginx 起个静态代理服务加载 pdf,可以通过 nginx 配置自动实现文件分片,这里不展开研究。

5.1 服务端支持分片加载

以下是我使用 ChatGPT 生成的一个基于 express 实现的分片加载 pdf 文件的后端服务代码。 完整代码见本文底部 git 仓库。

ts
代码解读
复制代码
// server/app.js const express = require("express"); const app = express(); const port = 3005; const fs = require("fs"); const path = require("path"); const os = require("os"); ... app.get("/getPdf", (req, res) => { console.log("request received", req.headers); const filePath = path.join(__dirname, "../src/assets/JavaScript.pdf"); const stat = fs.statSync(filePath); const fileSize = stat.size; const range = req.headers.range; if (range) { const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunkSize = end - start + 1; res.writeHead(206, { "Content-Range": `bytes ${start}-${end}/${fileSize}`, "Accept-Ranges": "bytes", "Content-Length": chunkSize, "Content-Type": "application/octet-stream", }); const fileStream = fs.createReadStream(filePath, { start, end }); fileStream.pipe(res); } else { res.writeHead(200, { "Content-Length": fileSize, "Accept-Ranges": "bytes", "Content-Type": "application/pdf", }); const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); } }); ...

5.2 前端请求支持懒加载

这里基于 pdfjs-dist 去实现前端的渲染,整体实现基于 按需懒加载 部分代码去做。

之前实现都是将文件全部加载下来之后再渲染,其中一个重点内容就是 PDF.getDocument(url) 这部分内容。 这部分是用来获取 pdf 文件资源的。使用时,将文件服务地址,本地文件引用或一个文件流转化的 Unit8Array 作为 url。

懒加载 pdf 分片内容,意味着不能再使用以前传全量的文件流的方式。需要使用传递文件服务地址的方式,把请求文件交给 pdfjs-dist 内部去处理,也就是如下这种方式。

ts
代码解读
复制代码
loadingTask = PDFJS.getDocument("http://10.5.67.55:3005");

但是这个方式写存在 2 个问题:

  • 我的文件服务不是裸奔的,需要携带 token,需要设置其他的响应头等参数,这些该如何做?
  • pdfjs-dist 支持懒加载需要设置 2 个参数:disableAutoFetch 和 disableStream,又该在哪里配置?

这 2 个问题在网上都没知道答案,但我知道 pdfjs-dist 肯定是支持的,因为全量使用 pdfjs 这个库时是可以这样做的,而 pdfjs-dist 是 pdfjs 的核心库。带着我的疑惑,我去阅读了这个核心库的源码,找到了 getDocument() 这个 api 的具体实现。

😜 还是先给答案吧,源码什么的,感兴趣的接着看就可以了.

通过阅读 getDocument api 发现,这个方法大致的流程就 2 步

  • 接收一个 src 参数;
  • 基于 src 和默认参数配置,生成 worker;
  • 基于 worker 返回 promise 任务异步处理文件加载;

src 支持传一个对象,可传入 urlhttpHeaderdisableAutoFetchdisableStreamrangeChunkSize 等参数。

基于以上,我们的实现方案就呼之欲出了。对前文所述方案进行简单改造:

ts
代码解读
复制代码
onMounted(async () => { .... // getDocument 这里不止可以传一个 url, 还可以传一个对象,如果请求需要带header,可以这样传 loadingTask = PDFJS.getDocument({ url: "http://10.5.67.55:3005/getPdf", httpHeaders: { Authorization: "Bearer 123" }, // 以下2个配置需要显式配置为true disableAutoFetch: true, disableStream: true, }); .... });

需要注意的是,在启用 disableAutoFetch 时,同时要启用 disableStream, 这 2 个属性默认是 false, 需要配置启用。

感兴趣的同学,可以看一下这块的源码实现,这个 api 实现比较长,可先大致阅读一下,再重点分析我们关注的内容。

点击展开/折叠代码
ts
代码解读
复制代码
// build/pdf.js function getDocument(src) { var task = new PDFDocumentLoadingTask(); // 一个文档加载的实例,是个 promise,意味着可以异步去加载文件 var source; // 这里可以看到,getDocument 支持的参数有 4 种类型,之前一直以为只有 1 种😂 if (typeof src === "string" || src instanceof URL) { source = { url: src, }; } else if ((0, _util.isArrayBuffer)(src)) { source = { data: src, }; } else if (src instanceof PDFDataRangeTransport) { source = { range: src, }; } else { if (_typeof(src) !== "object") { throw new Error( "Invalid parameter in getDocument, " + "need either string, URL, Uint8Array, or parameter object." ); } if (!src.url && !src.data && !src.range) { throw new Error( "Invalid parameter object: need either .data, .range or .url" ); } source = src; } var params = Object.create(null); var rangeTransport = null, worker = null; // 一个 for 循环设置一些参数 for (var key in source) { var value = source[key]; switch (key) { case "url": if (typeof window !== "undefined") { try { params[key] = new URL(value, window.location).href; continue; } catch (ex) { (0, _util.warn)(''Cannot create valid URL: "''.concat(ex, ''".'')); } } else if (typeof value === "string" || value instanceof URL) { params[key] = value.toString(); continue; } throw new Error( "Invalid PDF url data: " + "either string or URL-object is expected in the url property." ); case "range": rangeTransport = value; continue; case "worker": worker = value; continue; case "data": if ( _is_node.isNodeJS && typeof Buffer !== "undefined" && value instanceof Buffer ) { params[key] = new Uint8Array(value); } else if (value instanceof Uint8Array) { break; } else if (typeof value === "string") { params[key] = (0, _util.stringToBytes)(value); } else if ( _typeof(value) === "object" && value !== null && !isNaN(value.length) ) { params[key] = new Uint8Array(value); } else if ((0, _util.isArrayBuffer)(value)) { params[key] = new Uint8Array(value); } else { throw new Error( "Invalid PDF binary data: either typed array, " + "string, or array-like object is expected in the data property." ); } continue; } params[key] = value; } // 可以看到这里有超多自定义参数,理论上都是可以通过 getDocument api 传参实现的 params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE; params.CMapReaderFactory = params.CMapReaderFactory || DefaultCMapReaderFactory; params.ignoreErrors = params.stopAtErrors !== true; params.fontExtraProperties = params.fontExtraProperties === true; params.pdfBug = params.pdfBug === true; params.enableXfa = params.enableXfa === true; if ( typeof params.docBaseUrl !== "string" || (0, \_display_utils.isDataScheme)(params.docBaseUrl) ) { params.docBaseUrl = null; } if (!Number.isInteger(params.maxImageSize)) { params.maxImageSize = -1; } if (typeof params.isEvalSupported !== "boolean") { params.isEvalSupported = true; } if (typeof params.disableFontFace !== "boolean") { params.disableFontFace = \_api_compatibility.apiCompatibilityParams.disableFontFace || false; } if (typeof params.ownerDocument === "undefined") { params.ownerDocument = globalThis.document; } if (typeof params.disableRange !== "boolean") { params.disableRange = false; } // 禁止流式加载 if (typeof params.disableStream !== "boolean") { params.disableStream = false; } // 禁止自动加载 if (typeof params.disableAutoFetch !== "boolean") { params.disableAutoFetch = false; } (0, \_util.setVerbosityLevel)(params.verbosity); if (!worker) { var workerParams = { verbosity: params.verbosity, port: \_worker_options.GlobalWorkerOptions.workerPort, }; worker = workerParams.port ? PDFWorker.fromPort(workerParams) : new PDFWorker(workerParams); task.\_worker = worker; } var docId = task.docId; worker.promise .then(function () { if (task.destroyed) { throw new Error("Loading aborted"); } var workerIdPromise = _fetchDocument( worker, params, rangeTransport, docId ); var networkStreamPromise = new Promise(function (resolve) { var networkStream; if (rangeTransport) { networkStream = new _transport_stream.PDFDataTransportStream( { length: params.length, initialData: params.initialData, progressiveDone: params.progressiveDone, contentDispositionFilename: params.contentDispositionFilename, disableRange: params.disableRange, disableStream: params.disableStream, }, rangeTransport ); } else if (!params.data) { // 如何设置响应头,这里找到答案了 // 果然提供了 httpHeaders 参数供用户定制 networkStream = createPDFNetworkStream({ url: params.url, length: params.length, httpHeaders: params.httpHeaders, withCredentials: params.withCredentials, rangeChunkSize: params.rangeChunkSize, disableRange: params.disableRange, disableStream: params.disableStream, }); } resolve(networkStream); }); return Promise.all([workerIdPromise, networkStreamPromise]).then( function (_ref) { var _ref2 = _slicedToArray(_ref, 2), workerId = _ref2[0], networkStream = _ref2[1]; if (task.destroyed) { throw new Error("Loading aborted"); } var messageHandler = new _message_handler.MessageHandler( docId, workerId, worker.port ); messageHandler.postMessageTransfers = worker.postMessageTransfers; var transport = new WorkerTransport( messageHandler, task, networkStream, params ); task._transport = transport; messageHandler.send("Ready", null); } ); }) ["catch"](task._capability.reject); return task; }

5.3 效果演示

New Image

6 性能对比

6.1 性能比较

以下针对上面的几种方案做一个性能比较

测试文档:112 页,开发环境测试,本地一次性加载文件,不考虑各类其他性能优化手段。

  • 无懒加载

New Image

  • 懒加载

New Image

✨ 以上 2 种是在一次性请求完全部文件流后进行 pdf 渲染

  • 从服务端分片加载文件,分片大小设为 1024 字节

New Image

6.2 结论

  • 超小型文件(10 页之类):可不考虑懒加载
  • 小型到中型文件(5 - 20M): 需要考虑懒加载,可不分片
  • 大型文件 (20 - 几百兆):一定需要分片了,同时要考虑其他性能优化手段,如页数过多时需要动态移出一些不可见的页数,以免造成页面卡顿。

源码

✨全新源码,基于vue3.4 setup 语法编写,包含 3 种pdf文件展示方式;

  • 直接使用 iframe 展示pdf,调用浏览器原生能力加载pdf文件;
  • 基于 pdfjs-dist 进行渲染多页pdf;
  • 基于 pdfjs-dist 按需懒加载渲染多页pdf;

upthen/use-pdfjs-in-vue3: A Tutorial Show How to Use Pdfjs-dist in Vue3 (github.com)

🎉 如果看了源码觉得有用,麻烦点个收藏+关注吧😊,github 给个星星也不错,拜托拜托。 New Image