前端 PDF 预览技巧:标签 vs 插件,如何优雅地展示 PDF 文件
前言
pdf 作为一种常用的文档格式,相信很多同学都在项目中遇到过需要预览 pdf 文件的情况。其实实现的方式有很多,包括传统的标签 iframe 或 embed 方式,也可以运用一些插件,例如 pdf.js、vue-pdf 等等,本文将带大家一起探索不同方式的优劣势以及实现方式和应用场景,帮助你更好地选择适合自己项目的方案,让你在项目中轻松展示 pdf 文件。
一、iframe 标签
<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"
width="100%"
height="700"
/>
</div>
</template>
实现效果
二、embed 标签
<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"
type="application/pdf"
width="100%"
height="700"
/>
</div>
</template>
实现效果
小结
使用 <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>
</template>
<script>
import pdf from "vue-pdf";
export default {
components: {
pdf,
},
};
</script>
实现效果
3.5 加载本地pdf文件
如果直接将 pdf
文件放在任意一个文件夹中因为路径问题会加载不出来。所以需要将 pdf
放在 public > static
下(如下图),并用 /static/xxx.pdf
的路径方式进行引用( /
即已经代表 public
)。
代码示例
<template>
<pdf src="../../../static/ceshi.pdf"></pdf>
</template>
<script>
import pdf from "vue-pdf";
export default {
components: {
pdf,
},
};
</script>
实现效果
3.6 分页预览(多页)
<template>
<div id="document">
<!-- 分页组件 -->
<div class="component" v-show="schedule === 1">
<div @click="pageWay('first')" class="headTail">首页</div>
<div @click="rotateWay('obey')" class="headTail">
<el-tooltip
class="item"
effect="dark"
content="顺时针旋转"
placement="top"
>
<i style="font-size: 18px" class="el-icon-refresh-right"></i>
</el-tooltip>
</div>
<div
@click="pageWay('pre')"
class="fluctuatePage"
:style="pageNumber === 1 ? 'cursor: not-allowed;' : ''"
>
上一页
</div>
<div class="pagination">
<input
v-model.number="pageNumber"
type="number"
class="inputNumber"
@input="inputEvent()"
/>
<span> / {{ pageValue }}</span>
</div>
<div
@click="pageWay('next')"
class="fluctuatePage"
:style="pageNumber === pageValue ? 'cursor: not-allowed;' : ''"
>
下一页
</div>
<div @click="rotateWay('contrary')" class="headTail">
<el-tooltip
class="item"
effect="dark"
content="顺时针旋转"
placement="top"
>
<i style="font-size: 18px" class="el-icon-refresh-left"></i>
</el-tooltip>
</div>
<div @click="pageWay('last')" class="headTail">尾页</div>
</div>
<!-- pdf 组件 -->
<div class="pdfContent">
<pdf
:src="pdfUrl"
ref="pdf"
v-show="schedule === 1"
:rotate="pageRotate"
:page="pageNumber"
@num-pages="pageValue = $event"
@progress="schedule = $event"
@page-loaded="pageNumber = $event"
@loaded="loadPdfHandler"
@link-clicked="pageNumber = $event"
id="pdfID"
></pdf>
</div>
<!-- loading 组件 -->
<div class="progress" v-show="schedule !== 1">
<el-progress
type="circle"
:width="60"
color="#53a7ff"
:percentage="Math.floor(schedule * 100)"
></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">
#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"] {
-moz-appearance: textfield;
}
</style>
实现效果
3.7 打印文件
在 vue-pdf
中,print(dpi, pageList)
方法用于触发打印 pdf
文件的功能。
-
dpi:
表示打印的分辨率(每英寸的点数)。这个参数用于设置打印时的图像质量,通常可以设置为一个整数值,例如
150
、300
等。较高的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>
<pdf ref="myPdfComponent" :src="pdfSrc"></pdf>
</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 加密文件
如果你的 pdf
文件是加密的,那么就可以调用 password
方法。
-
updatePassword
这个函数用于更新
pdf
文件的密码。当用户输入了正确的密码后,可以调用updatePassword
函数来更新pdf
文件的密码。 -
reason
这是一个字符串,表示需要密码的原因。通常会包含一些提示信息,告诉用户为什么需要输入密码才能查看
pdf
文件。
<template>
<pdf :src="pdfUrl" @password="handlePassword" />
</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 无法显示中文内容
一般情况下,是不会出现显示不了中文的问题。但不排除一些特殊的文档,例如票据、合同这类。
<template>
<pdf :src="pdfUrl" />
</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
链接,而是会返回一个流文件(如下图所示),这个时候就需要对原始的流文件进行处理后再进行赋值。
实现代码
<template>
<div>
<button @click="pdfWay">预览pdf</button>
<iframe :src="item.iframeUrl" width="100%" height="550" />
</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
文件的内容。