前端 PDF 预览技巧:标签 vs 插件,如何优雅地展示 PDF 文件

水星记_ 2024-09-14 16:33:01 阅读 87

前言

pdf 作为一种常用的文档格式,相信很多同学都在项目中遇到过需要预览 pdf 文件的情况。其实实现的方式有很多,包括传统的标签 iframeembed 方式,也可以运用一些插件,例如 pdf.jsvue-pdf 等等,本文将带大家一起探索不同方式的优劣势以及实现方式和应用场景,帮助你更好地选择适合自己项目的方案,让你在项目中轻松展示 pdf 文件。


一、iframe 标签

<code><iframe> 标签规定一个内联框架。一个内联框架被用来在当前 HTML 文档中嵌入另一个文档。

1.1 属性

属性 描述
src 在 iframe 中显示的文档的 URL。
height iframe 的高度。
width iframe 的宽度。
name iframe 的名称。
seamless iframe 看起来像是父文档中的一部分。
srcdoc 规定页面中的 HTML 内容显示在 iframe 中。
sandbox 对 iframe 的内容定义一系列额外的限制。allow-forms(允许在iframe中提交表单)、allow-same-origin(允许iframe与包含它的页面具有相同的源,这意味着它可以访问与包含页面相同的资源)、allow-scripts(允许在iframe中执行脚本)、allow-top-navigation(允许iframe导航到顶级浏览上下文,即顶级窗口)
scrolling HTML5 不支持。规定是否在 iframe 中显示滚动条。yes、no、auto
align HTML5 不支持。HTML 4.01 已废弃。 规定如何根据周围的元素来对齐iframe。left、right、top、middle、bottom
frameborder -HTML5 不支持。规定是否显示 iframe 周围的边框。
longdesc HTML5 不支持。规定一个页面,该页面包含了有关 iframe 的较长描述。
marginheight HTML5 不支持。规定 iframe 的顶部和底部的边距
marginwidth HTML5 不支持。规定 iframe 的左侧和右侧的边距。

1.2 代码实现

<template>

<div>

<iframe

src="https://s4.aconvert.com/convert/p3r68-cdx67/agb6w-9i4xt.pdf"code>

width="100%"code>

height="700"code>

/>

</div>

</template>

实现效果

在这里插入图片描述


二、embed 标签

<code><embed> 标签定义了一个容器,用来嵌入外部应用或者互动程序(插件)。

2.1 属性

属性 描述
height 规定嵌入内容的高度。
src 规定被嵌入内容的 URL。
type 规定嵌入内容的 MIME 类型。注:MIME = Multipurpose Internet Mail Extensions。
width 规定嵌入内容的宽度。

2.2 代码实现

<template>

<div>

<embed

src="https://s4.aconvert.com/convert/p3r68-cdx67/abeub-nb503.pdf"code>

type="application/pdf"code>

width="100%"code>

height="700"code>

/>

</div>

</template>

实现效果

在这里插入图片描述


小结

使用 <code><embed><iframe> 标签浏览器会自动调用内置的PDF阅读器插件来显示 pdf 内容。相对于使用插件来显示 pdf 文件有各自的优劣势。

优势:

简单易用: 使用 <embed><iframe> 标签可以很容易地嵌入 pdf 文件,而且不需要额外的插件或库;跨平台兼容性: <embed><iframe> 标签通常具有很好的跨平台兼容性,可以在各种浏览器和设备上正常显示 pdf 文件;快速集成: 直接使用标签嵌入 pdf 文件可以快速集成到现有的网页中,不需要额外的学习成本。

劣势:

定制性差: 使用 <embed><iframe> 标签显示 pdf文件的定制性相对较差,无法轻松实现一些高级功能,如自定义样式、交互等;功能受限: <embed><iframe> 标签提供的功能有限,无法实现一些复杂的 pdf 显示需求,如搜索、缩略图预览、无法禁止打印等;样式控制困难: 使用 <embed><iframe> 标签嵌入的 pdf文件样式控制相对困难,难以实现与网页样式的统一。


三、vue-pdf 插件

vue-pdf 是一个基于 pdf.js 实现用于在 vue 应用程序中显示 pdf 文件的库。它提供了一个 vue 组件,可以轻松地将 pdf 文件嵌入到你的应用程序中,以便用户可以查看和与 pdf 文件交互。

3.1 安装

npm install --save vue-pdf

如果有版本问题可以降低版本,推荐安装以下版本:

npm install --save vue-pdf@4.2.0


3.2 引入注册

局部

import pdf from "vue-pdf";

export default {

components: {

pdf,

},

};

全局

import pdf from 'vue-pdf';

Vue.component('pdf', pdf);


3.3 常用的属性及事件

Props 属性

属性 描述
:src PDF 文件的 URL。
:page 要显示的页码。
:rotate 页面旋转的角度,仅限90度的倍数。 例如:90, 180, 270, 360, …

Events 事件

事件 描述
@password (updatePassword, reason) updatePassword: 调用以输入 PDF 密码的函数。reason: 函数调用的原因,可能是 ‘NEED_PASSWORD’ 或 ‘INCORRECT_PASSWORD’
@progress Number 文档加载进度,范围从 0 到 1。
@loaded 文档加载完成时触发。
@page-loaded Number 页面加载完成后触发。
@num-pages Number PDF 的总页数。
@error Object 发生错误时触发。
@link-clicked Number 点击内部链接时触发。

3.4 基础使用(单页)

<template>

<pdf src="https://s4.aconvert.com/convert/p3r68-cdx67/a0jo0-a0e3v.pdf"></pdf>code>

</template>

<script>

import pdf from "vue-pdf";

export default { -- -->

components: {

pdf,

},

};

</script>

实现效果

在这里插入图片描述


3.5 加载本地pdf文件

如果直接将 <code>pdf 文件放在任意一个文件夹中因为路径问题会加载不出来。所以需要将 pdf 放在 public > static 下(如下图),并用 /static/xxx.pdf 的路径方式进行引用( / 即已经代表 public)。

在这里插入图片描述

代码示例

<code><template>

<pdf src="../../../static/ceshi.pdf"></pdf>code>

</template>

<script>

import pdf from "vue-pdf";

export default { -- -->

components: {

pdf,

},

};

</script>

实现效果

在这里插入图片描述


3.6 分页预览(多页)

<code><template>

<div id="document">code>

<!-- 分页组件 -->

<div class="component" v-show="schedule === 1">code>

<div @click="pageWay('first')" class="headTail">首页</div>code>

<div @click="rotateWay('obey')" class="headTail">code>

<el-tooltip

class="item"code>

effect="dark"code>

content="顺时针旋转"code>

placement="top"code>

>

<i style="font-size: 18px" class="el-icon-refresh-right"></i>code>

</el-tooltip>

</div>

<div

@click="pageWay('pre')"code>

class="fluctuatePage"code>

:style="pageNumber === 1 ? 'cursor: not-allowed;' : ''"code>

>

上一页

</div>

<div class="pagination">code>

<input

v-model.number="pageNumber"code>

type="number"code>

class="inputNumber"code>

@input="inputEvent()"code>

/>

<span> / { -- -->{ pageValue }}</span>

</div>

<div

@click="pageWay('next')"code>

class="fluctuatePage"code>

:style="pageNumber === pageValue ? 'cursor: not-allowed;' : ''"code>

>

下一页

</div>

<div @click="rotateWay('contrary')" class="headTail">code>

<el-tooltip

class="item"code>

effect="dark"code>

content="顺时针旋转"code>

placement="top"code>

>

<i style="font-size: 18px" class="el-icon-refresh-left"></i>code>

</el-tooltip>

</div>

<div @click="pageWay('last')" class="headTail">尾页</div>code>

</div>

<!-- pdf 组件 -->

<div class="pdfContent">code>

<pdf

:src="pdfUrl"code>

ref="pdf"code>

v-show="schedule === 1"code>

:rotate="pageRotate"code>

:page="pageNumber"code>

@num-pages="pageValue = $event"code>

@progress="schedule = $event"code>

@page-loaded="pageNumber = $event"code>

@loaded="loadPdfHandler"code>

@link-clicked="pageNumber = $event"code>

id="pdfID"code>

></pdf>

</div>

<!-- loading 组件 -->

<div class="progress" v-show="schedule !== 1">code>

<el-progress

type="circle"code>

:width="60"code>

color="#53a7ff"code>

:percentage="Math.floor(schedule * 100)"code>

></el-progress>

<p>{ -- -->{ loadingTxt }}</p>

</div>

</div>

</template>

<script>

import pdf from "vue-pdf";

export default {

components: {

pdf,

},

data() {

const loadingText = "加载文件中,文件较大请耐心等待...";

return {

remindText: {

loading: loadingText, // 加载中提示语

refresh: "若卡住不动,可刷新页面重新加载...", // 刷新提示语

},

loadingTxt: loadingText, // 初始加载提示语

pageRotate: 0, //旋转角度

pageNumber: 0, // 当前页数

pageValue: 0, // 总页数

schedule: 0, // 加载进度

timerId: "", // 定时器 ID

pdfUrl: "https://s4.aconvert.com/convert/p3r68-cdx67/a2mdb-i5v70.pdf", // pdf 文件路径

};

},

// 在组件销毁时清除定时器

destroyed() {

clearInterval(this.timerId);

},

mounted() {

this.prohibit();

this.timerId = setInterval(() => {

// 设置定时器,每隔一段时间切换加载提示语

this.loadingTxt === this.remindText.refresh

? (this.loadingTxt = this.remindText.loading)

: (this.loadingTxt = this.remindText.refresh);

}, 4000);

this.listenerFunction(); // 调用监听滚动条事件的方法

},

methods: {

// 监听输入事件

inputEvent() {

// 输入页数大于总页数时,设置为总页数,输入页数小于 1 时,设置为 1

this.pageNumber = Math.max(1, Math.min(this.pageNumber, this.pageValue));

},

// 上一页、下一页、首页、尾页事件

pageWay(val) {

if (val === "pre" && this.pageNumber > 1) {

this.pageNumber--; // 上一页

} else if (val === "next" && this.pageNumber < this.pageValue) {

this.pageNumber++; // 下一页

} else if (val === "first") {

this.pageNumber = 1; // 首页

} else if (val === "last" && this.pageNumber < this.pageValue) {

this.pageNumber = this.pageValue; // 尾页

}

this.toTop(); // 滚动到顶部

},

// 顺/逆旋转

rotateWay(val) {

this.pageRotate += val === "obey" ? 90 : -90;

},

// 滚动顶部

toTop() {

const container = document.getElementById("document").parentElement;

container.scrollIntoView({ behavior: "smooth", block: "start" });

},

// 监听 pdf 加载完成事件

loadPdfHandler() {

this.pageNumber = 1; // 加载 pdf 时,设置当前页数为 1

},

// 禁止特定的操作

prohibit() {

// 禁用右键菜单

document.oncontextmenu = function () {

return false;

};

// 禁用按键

document.onkeydown = function (e) {

// 定义需要禁用的按键码数组

const forbiddenKeys = [65, 67, 73, 74, 80, 83, 85, 86, 117, 18, 123];

// 判断按下的按键是否在禁用数组中,如果是则返回 false 禁用按键

if (e.ctrlKey && forbiddenKeys.includes(e.keyCode)) {

return false;

}

};

},

// 监听滚动条事件

listenerFunction(e) {

document

.getElementById("document")

.addEventListener("scroll", function (event) {

// 监听滚动事件

console.log(event);

});

},

},

};

</script>

<style scoped lang="less">code>

#document { -- -->

overflow: auto;

min-height: 100vh;

width: 100%;

display: flex;

position: relative;

.component {

user-select: none;

color: #ffffff;

position: fixed;

bottom: 5%;

left: 50%;

margin-left: -250px;

display: flex;

align-items: center;

justify-content: space-around;

background: rgba(0, 0, 0, 0.7);

border-radius: 30px;

width: 420px;

padding: 15px 40px;

z-index: 99;

.pagination {

position: relative;

top: 1px;

.inputNumber {

border-radius: 5px;

border: 1px solid #8b8b8b;

width: 36px;

height: 16px;

text-align: center;

background: transparent;

}

.inputNumber:focus {

border: 1px solid #00aeff;

background: rgba(18, 163, 230, 0.1);

outline: none;

transition: 0.2s;

}

}

.headTail {

border-radius: 50%;

display: flex;

align-items: center;

justify-content: center;

}

.fluctuatePage {

border-radius: 50%;

display: flex;

align-items: center;

justify-content: center;

}

.fluctuatePage:hover,

.headTail:hover {

transition: 0.3s;

color: #409eff;

cursor: pointer;

}

}

.pdfContent {

width: 100%;

}

.progress {

width: 222px;

position: absolute;

top: 50%;

left: 50%;

margin-left: -111px;

text-align: center;

}

.progress p {

color: #199edb;

font-size: 14px;

}

}

/*在谷歌下移除input[number]的上下箭头*/

input::-webkit-outer-spin-button,

input::-webkit-inner-spin-button {

-webkit-appearance: none !important;

margin: 0;

}

/*在firefox下移除input[number]的上下箭头*/

input[type="number"] { -- -->code>

-moz-appearance: textfield;

}

</style>

实现效果

在这里插入图片描述


3.7 打印文件

在 <code>vue-pdf 中,print(dpi, pageList) 方法用于触发打印 pdf 文件的功能。

dpi:

表示打印的分辨率(每英寸的点数)。这个参数用于设置打印时的图像质量,通常可以设置为一个整数值,例如 150300 等。较高的 dpi 值会产生更清晰的打印效果,但也会增加打印文件的大小。

pageList:

表示要打印的页面列表。这个参数是一个数组,用于指定要打印的页面序号。例如,如果你想打印第 1 页和第 3 页,可以将 pageList 设置为 [1, 3]。如果想打印所有页面,可以使用类似于 [1, 2, 3, ...] 的方式来表示所有页面。

// 设置打印分辨率为 150dpi,打印第1页和第3页

this.$refs.myPdfComponent.print(150, [1, 3]);

<template>

<div>

<button @click="printPdf">打印 pdf</button>code>

<pdf ref="myPdfComponent" :src="pdfSrc"></pdf>code>

</div>

</template>

<script>

import pdf from "vue-pdf";

export default { -- -->

components: {

pdf,

},

data() {

return {

pdfSrc: "https://s4.aconvert.com/convert/p3r68-cdx67/a1eqq-4rahm.pdf",

};

},

};

</script>

实现效果

在这里插入图片描述


3.8 加密文件

如果你的 <code>pdf 文件是加密的,那么就可以调用 password 方法。

updatePassword

这个函数用于更新 pdf 文件的密码。当用户输入了正确的密码后,可以调用 updatePassword 函数来更新 pdf 文件的密码。

reason

这是一个字符串,表示需要密码的原因。通常会包含一些提示信息,告诉用户为什么需要输入密码才能查看 pdf 文件。

<template>

<pdf :src="pdfUrl" @password="handlePassword" />code>

</template>

<script>

import pdf from "vue-pdf";

export default { -- -->

components: {

pdf,

},

data() {

return {

pdfUrl: "https://s4.aconvert.com/convert/p3r68-cdx67/a9tag-qf2jw.pdf",

};

},

methods: {

handlePassword(updatePassword, reason) {

if (reason === "NEED_PASSWORD") {

// 这里可以提示用户输入密码,然后使用输入的密码来更新PDF文档

let password = prompt("请输入PDF文档的密码:", "");

updatePassword(password);

}

},

},

};

</script>

实现效果

在这里插入图片描述


3.9 无法显示中文内容

一般情况下,是不会出现显示不了中文的问题。但不排除一些特殊的文档,例如票据、合同这类。

<code><template>

<pdf :src="pdfUrl" />code>

</template>

<script>

import pdf from "vue-pdf";

export default { -- -->

components: {

pdf,

},

data() {

return {

pdfUrl: "",

};

},

methods: {

getPdfPort() {

// 解析 PDF

const taskData = pdf.createLoadingTask({

url: res.data,

cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.5.207/cmaps/",

cMapPacked: true,

});

// 把解析后的地址进行赋值

this.pdfUrl = taskData;

},

},

};

</script>

上面代码通过 pdf.createLoadingTask() 方法创建一个任务,该任务负责加载和解析指定的 pdf 文件。通过这个方法,可以异步地加载 pdf 文件并获取解析后的数据,以便在页面中显示 pdf 内容。

pdf.createLoadingTask() 方法参数

参数 描述
url 要加载的 pdf 文件的 url。
cMapUrl 字符映射文件的 url,用于将 pdf 文件中的字符编码映射到可显示的字符。
cMapPacked 布尔值,指示是否使用压缩的字符映射文件。

拓展

后台返回文档流

很多时候,考虑到安全性、实时性,后台可能不一定会直接返回一个 url 链接,而是会返回一个流文件(如下图所示),这个时候就需要对原始的流文件进行处理后再进行赋值。

在这里插入图片描述

实现代码

<code><template>

<div>

<button @click="pdfWay">预览pdf</button>code>

<iframe :src="item.iframeUrl" width="100%" height="550" />code>

</div>

</template>

<script>

export default { -- -->

data() {

return {

iframeUrl: "",

};

},

methods: {

pdfWay() {

previewPort().then((res) => { // 接口方法

var binaryData = [];

binaryData.push(res);

this.iframeUrl = window.URL.createObjectURL(

new Blob(binaryData, { type: "application/pdf" })

);

});

},

},

};

</script>

在上面的代码中,new Blob(binaryData, { type: "application/pdf" }) 会创建了一个 Blob 二进制对象,binaryData 是一个包含 pdf 文件二进制数据的数组,{ type: "application/pdf" } 指定了这个 Blob 对象的类型为 pdf 文件类型。然后我们调用了 createObjectURL() 方法,该方法接受一个 Blob 对象作为参数,并返回一个包含该 Blob 对象数据的 url。这个 url 是一个临时的、唯一的 url,可以用于在浏览器中直接访问和展示 Blob 对象的内容。通过这个 url,我们就可以直接在页面上展示 pdf 文件的内容。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。