vue-pdf(vue预览pdf文件)的使用和注意事项
- 需求简介
- vue-pdf简介
- 其它pdf预览方式
- 一、HTML标签
- 二、插件形式
- vue-pdf的安装(有坑)
- vue-pdf的使用
- 一、安装file-loader(坑)
- 二、本地测试pdf预览,两种方式(坑)
- 1. pdf文件在public文件夹下
- 2. 使用java后台返回文件流
- 二、线上测试
需求简介
近期公司有一个需求需要在移动端实现pdf文件预览,因为移动端是用vue开发的,所以最终选择使用vue-pdf插件进行开发。
vue-pdf简介
vue-pdf是一个pdf预览的插件,主要在vue中使用,开发者是国外的(vue生态系统很大,也有国外的大佬参与了一些插件开发,主要是一些npm包),使用起来较为简单,就是坑有点多,如果公司要求没有那么严格,只是简单的预览功能,对样式也没有过多要求的话这个插件很不错。
官网链接:vue-pdf,https://hub.fgit.cf/FranckFreiburger/vue-pdf?tab=readme-ov-file
如果打不开,可以使用下面这个网址将链接复制进去加速访问
https://github.ur1.fun/
尽量面向官网学习,这里有最为权威和全面的教程,就是英文页面有点烦人,适应一下,如果遇到问题可以百度看看解决方案。
简单总结一下官网列出的一些方法和属性
:src
,用于绑定文件的URL:page
,用于展示第几页pdf,如果一个页面只渲染一页这个就会用到,翻页就是改变这个绑定的值- :
rotate
,旋转的角度,不常用 @password
,当文档有密码时使用,不常用@process
,文档加载的进度,不常用@loaded
,当文档第一次加载时触发,不常用@page-loaded
,当页面加载时使用,当pdf翻页时会使用@num-pages
,pdf文件的总页数@error
,文件加载错误时触发@lin-clicked
,点击内部链接时触发,不常用createLoadingTask
,最常用的方法,加载pdf时使用print
,打印pdf的方法
其它pdf预览方式
一、HTML标签
- iframe 标签
- embed 标签
- object 标签
这三种都是使用HTML原生的标签,使用起来要么就是兼容性很差,要么就是不好理解或者不好开发,一般不建议使用。
二、插件形式
- pdf.js
- PDFObject
大多数我们使用的就是插件,开发速度较快,其中最有名的就是pdf.js,现在很多pdf预览的第三方插件都是基于pdf.js封装的(包括vue-pdf),有兴趣的伙伴可以研究一下。
vue-pdf的安装(有坑)
安装命令:
npm install --save vue-pdf
这是最基本的安装命令,但是这样安装极有可能会在最开始引入的时候import pdf form "vue-pdf
就报错,如下:
MainTemplate.hooks.hotBootstrap has been removed (use your own RuntimeModule instead
然后就是一顿找为什么会报这个错,网上大多数都是说版本的问题,需要降低版本,因为我们默认安装的话是最新版本(现在是4.3.0),这个版本兼容性不是很好,所以出现这个错误我们首先需要做的是:
- 使用命令卸载之前安装的vue-pdf
npm uninstall vue-pdf
- 指定版本安装(4.2.0)
npm install --save vue-pdf@4.2.0
npm install pdfjs-dist@2.5.207
这里一定要卸载之前的版本,有时候就会有冲突,觉得不妥的伙伴可以在卸载后删除node_modules文件夹,重新加载一下再安装。
一般情况下,执行上述命令就可以解决这个问题,但有时候在执行完后还是不行,这时候如果你执意要搜索为什么,答案普遍都是这个,有可能还有有一个说是脚手架版本太高,也就是@vue/cli
版本是4.5以上,需要降一下版本,这时候可不要贸然去降低脚手架版本了,原因可能不是这个,注意了
,vue-pdf的最新版本是2020年发布的,距离现在已经有3年了,我们都忽视了node版本,那时候的node版本可没有我们现在安装的那么高,所以就会出现不兼容的情况,(当时我使用的是node16,切换到14版本就可以了)这时候换个node版本,删除node_modules文件夹重新编译就可以解决引入报错的问题,如果不想卸载node可以参考我之前的nvm教程,快速切换node版本。
nvm使用
vue-pdf的使用
一、安装file-loader(坑)
npm install file-loader
这里安装file-loader是做后面的本地测试用的,如果不安装则会导致本地测试的时候报错,无法识别pdf文件。Warning: Indexing all PDF objects
webpack配置
- 如果没有使用webpack链式编程,那在vue-config.js里面加一个
module.exports = defineConfig({
···
configureWebpack: {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|pdf)$/i,
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
},
},
],
}
}
})
- 如果使用了webpack链式编程,也就是chainWebpack,配置如下
二、本地测试pdf预览,两种方式(坑)
提前准备好一个pdf文件,实在找不到就新建一个docx文件,使用office导出为pdf文件。
注意了
:这里本地测试也分两种,如果是直接将pdf文件放在vue的工作环境下,必须放在public文件夹下面,不然使用require会报错;另外一种是后台返回文件流,之后会说怎么操作。
1. pdf文件在public文件夹下
<pdf
v-for="i in pageCount"
:src="pdfUrl"
:key="i + 'pdf'"
:page="i"
class="pdf-item"
></pdf>
vue代码
/** pdf加载 */
async previewFile() {
try {
let loadingTask = pdf.createLoadingTask(this.pdfUrl);
loadingTask.promise.then(pdf => {
this.showPreviewFile = true;
this.pageCount = pdf.numPages;
}).catch((e) => {
console.log("pdf初始化错误", e);
Notify("文件初始化失败,请返回下载该文件查看")
})
} catch (e) {
this.$router.go(-1)
console.log("pdf加载出错了", e);
}
},
js代码
pdfUrl是我的父组件传过来的(之后会附上完整代码),父组件中我的url是这样处理的
var testPdfUrl = require('@process/public/A票_8089139370.pdf')
这样写看起来没有什么毛病,使用require是为了得到pdf的路径,如果你是直接使用在标签上是没有什么问题的,但在实际操作中肯定是要把链接作为变量定义在data中,可是这样写报错了。
这里实际上是有一个坑的,如果你的require是使用在scipt标签中,解析出来的可不是一个string类型的连接哦,是一个对象,在对象中包含链接的属性,在控制台打印出来如下:
其实最终file-loader会将pdf文件解析成一个物理路径返回,所以在真实测试的时候,需要将上面的代码稍微改造一下:
var testPdfUrl = require('@process/public/A票_8089139370.pdf').default
这样就可以实现基本的pdf预览,但一般的公司需求这样是满足不了的,大多数pdf都会有水印和签名的需求,现在的写法是展示不了水印和签名的,所以我们针对水印进行再次改造。
import CMapReaderFactory from 'vue-pdf/src/CMapReaderFactory.js' //引入水印依赖
在vue中引入水印依赖,之后改造pdf加载函数。
let loadingTask = pdf.createLoadingTask({
url: this.pdfUrl,
CMapReaderFactory
});
loadingTask.promise.then(pdf => {
this.pdfUrl = loadingTask
this.showPreviewFile = true;
this.pageCount = pdf.numPages;
}).catch((e) => {
console.log("pdf初始化错误", e);
Notify("文件初始化失败,请返回下载该文件查看")
})
在createLoading函数中增加了水印的配置,这样就可以正常加载水印的样式。而签名有点麻烦,经过不断查询资料最终选择了换包,将vue-pdf替换为vue-pdf-signature
,这是一位大佬在npm发版的支持签名的包,经过测试没有出现问题,这里我们只需要在package.json
中加入"vue-pdf-signature": "^4.2.7",
直接npm i即可。最终改造的引入就是
import pdf from "vue-pdf-signature";
import CMapReaderFactory from "vue-pdf-signature/src/CMapReaderFactory"
2. 使用java后台返回文件流
这种实现方式就是前台发起请求,java后台返回一个文件流,之后前台处理成URL使用,当然一般这种使用的较少,如果你的pdf文件不大可以使用,有时候用来解决跨域问题
比较好使,这个之后会说。
后台代码
@GetMapping("/testWordView")
@ResponseBody
public void testWordView(HttpServletResponse response, HttpServletRequest request) {
File file = new File("D:/personFile/word/test.docx");
if (file.exists()) {
OutputStream os = null;
try {
FileUtilService.setDownLoadName(request, response, "测试word文件.docx");
os = response.getOutputStream();
os.write(FileUtils.readFileToByteArray(file));
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
前台代码:注意,要使用await关键字
var res = await testWordView()
this.pdfUrl = encodeURIComponent(this.getObjectURL(res));
// 将返回的流数据转换为url
getObjectURL (file) {
var binaryData = [];
binaryData.push(file);
let url = null;
if (window.createObjectURL !== undefined) { // basic
url = window.createObjectURL(new Blob(binaryData, {type: 'application/pdf,utf-8'}));
} else if (window.webkitURL !== undefined) { // webkit or chrome
try {
url = window.webkitURL.createObjectURL(new Blob(binaryData, {type: 'application/pdf,utf-8'}));
} catch (error) {
}
} else if (window.URL !== undefined) { // mozilla(firefox)
try {
url = window.URL.createObjectURL(new Blob(binaryData, {type: 'application/pdf,utf-8'}));
} catch (error) {
}
}
return url;
}
二、线上测试
刚才本地测试已经通过了,如果放到测试环境可能会出现一个问题,跨域!一般搜索资料会让我们去修改一下源码,这个方式不可取,放到线上就废了,有人可能说我给线上换包,那如果遇上升级node版本啥的,就不好使了,不能采用哦,而且据我根据网上的教程去找那段代码,在4.2.0版本是没有的。
这种就是vue-pdf常见的跨域问题,出现的情况就是一般公司的文件是会单独拎出来一个服务器的,如果直接请求就会出现这种跨域问题,解决方式一般有三种
- 服务器直接配置支持跨域(不推荐,很不安全)
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Expose-Headers: Accept-Ranges, Content-Encoding, Content-Length, Content-Range
使用的话就是ip加端口再加你的文件地址。
2. 使用文件流,就是上面测试使用的那种。
3. nginx代理(推荐使用)
location /files {
proxy_pass 目标服务器以及端口/;
}
这是公司常用的手段,一般肯定会有的,直接交给运维去配置,我们直接使用即可!
注意了:千万别再本地用测试环境的URL进行测试,除非你用文件流,因为无论你怎么配置都有跨域问题,我试验了好久,最后放到测试环境其实就OK了,pdf-js底层对这里进行了处理,这时候放到测试环境测试就可以了,不要像我一样在本地死磕啊!
以上就是我对vue-pdf使用的一些总结,涵盖了大部分使用当中的坑,这里附上我封装的pdf组件。
<template>
<div class="process-fileView-pdf-container" ref="pdfView">
<div v-if="showPreviewFile" ref="pdf">
<pdf
v-for="i in pageCount"
:src="pdfUrl"
:key="i + 'pdf'"
:page="i"
class="pdf-item"
></pdf>
</div>
<div class="pdf-button-group" ref="fileViewBtnGroup">
<div class="div-item"><van-button class="btn-item back" type="info" @click="goBack">返回</van-button></div>
<div class="div-item"><van-button class="btn-item big" type="info" @click="scaleD">放大</van-button></div>
<div class="div-item"><van-button class="btn-item small" type="info" @click="scales">缩小</van-button></div>
</div>
</div>
</template>
<script>
import pdf from "vue-pdf-signature";
import CMapReaderFactory from "vue-pdf-signature/src/CMapReaderFactory"
import {Notify} from "vant";
export default {
name: "PdfView",
components: {
pdf
},
data() {
return {
// pdf总页数
pageCount: 1,
// 缩放
scale: 100,
timer: null,
showPreviewFile: false
}
},
mounted() {
this.previewFile()
},
props: {
pdfUrl: {
type: String,
default: ''
},
},
watch: {
pdfUrl: {
handler(newVal, oldVal) {
if (newVal) {
this.previewFile()
}
},
immediate: false
}
},
methods: {
/** pdf加载 */
async previewFile() {
try {
let loadingTask = pdf.createLoadingTask({
url: this.pdfUrl,
CMapReaderFactory
});
loadingTask.promise.then(pdf => {
this.pdfUrl = loadingTask
this.showPreviewFile = true;
this.pageCount = pdf.numPages;
}).catch((e) => {
console.log("pdf初始化错误", e);
Notify("文件初始化失败,请返回下载该文件查看")
})
} catch (e) {
this.$router.go(-1)
console.log("pdf加载出错了", e);
}
},
/** PDF放大 */
scaleD() {
if(this.scale>150) return false
this.scale += 10;
this.$refs.pdf.style.width = parseInt(this.scale) + "%";
},
/** PDF缩小 */
scales() {
if(this.scale<40) return false
this.scale -= 10;
this.$refs.pdf.style.width = parseInt(this.scale) + "%";
},
/** 返回 */
goBack() {
this.$router.go(-1)
},
}
}
</script>
<style lang="scss" scoped>
.process-fileView-pdf-container {
width: 100%;
height: 100%;
.pdf-item{
height: 100vh;
display: block !important;
}
.pdf-button-group {
position: fixed;
top: 10px;
right: 20px;
.btn-item {
width: 50px;
height: 50px;
border-radius: 100%;
}
.div-item {
margin-bottom: 8px;
}
::v-deep .van-button__text{
line-height: 20px;
}
}
}
</style>