前言
公司项目,需要在app的内嵌H5项目中,实现预览pdf文件的需求
思考
这个需求拿到手上的一瞬间,我是懵的,因为这是一个我从来没有涉足过的领域,故有了一下的一些思考:
1.预览pdf应该是有插件的,先搜索一波能在vue中使用的插件:结果有vue-pdf,pdfjs
2.再明确一下需求:可能会有多个pdf文件,每个pdf文件可能不止一页,那么,至少在第一层肯定是循环了
实践
最开始,是打算用vue-pdf的,这个玩意儿其实我也根本不会使用。但是吧,有搜索这个方便的工具在,这个问题都不是问题。于是乎,开始一波疯狂的搜索行为,然而,理想很丰满,现实很骨感,搜索到的方法,我没一个成功实现了需求的。
既然这个不行,那就再试试pdfjs这个最原始的吧(vue-pdf似乎就是基于这个弄出来的)。按照教程方法,先下载一波文件,然后解压,放到项目中。一顿操作猛如虎,一看输入只有5,哭泣。
没办法,继续百度找方法,最后找到了另外一种pdfjs的引入方式,即pdfjs-dist,我也不知道这个和pdfjs有啥不一样的,貌似就是一个东西。
看了下这个,不用下载源代码放到项目中,似乎操作也比较简单,于是再次开始实践
操作
第一步:引入
npm i pdfjs-dist
其实这里也有个坑,我开始下载了最新版本的,但是出现了蜜汁bug,后面看了前辈大佬们的文章,找了一个指定版本2.2.228
第二步:使用
看了很多文章,都是结合了canvas来使用的,所以我也不会开辟一个新的使用方法(我不会啊,想开也开不动)
// html部分
<template v-if="pdfList.length > 0">
<div v-for="(pdfPages, index) in pdfList" :key="index" class>
<canvas
v-for="page in pdfPages"
:key="page"
:id="'pdfCanvas' + page"
></canvas>
</div>
</template>
// 1.引入pdfjs
import PDFJS from "pdfjs-dist";
created() {
PDFJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.min.js");
// 不要问我为啥在这里写,我表示不是很清楚,我最开始也是看文章中写在import下面的,但是我试了下不行,所以试了一下写在这里,结果试ok的
}
最开始,我并没有封装canvas部分,全部写在了一个组件中,除了造成组件代码量庞大以外,此时还未出现其他问题
// 请求接口,获取pdf文件流(数据是第三方的,而且还是个明文的链接,数据敏感,考虑到安全性,我们这边是不存服务器的,后端只是转发而已)
async getFileUrl() {
try {
const res = await this.$http.get(url);
console.log(res, "getFileUrl");
if (xxx.code === 200) {
let urlList = xxx.data;
urlList.forEach(item => {
this.handlerUrl(item);
});
return;
}
} catch (error) {
console.log(error, "getFileUrl");
}
},
// 数据除了pdf,还有ofd的,先忽略ofd吧
handlerUrl(item) {
if (item.fileFormat === "PDF") {
this.getPdfFile(item.licenseId);
} else if (item.fileFormat === "OFD") {
this.getOfdFile(item.licenseId);
}
},
接下来就是渲染pdf文件了
async getPdfFile(licenseId) {
const url = `xxx`;
try {
const res = await this.$http({
url,
method: "get",
params: {
licenseId
},
responseType: "blob"
});
// 将文件流处理一下,变成url
if (res.status == 200) {
const content = res.data;
let file = new Blob([content], {
type: "application/pdf;charset-UTF-8"
});
const objectURL = URL.createObjectURL(file);
this.pdfList.push(objectURL);
this._loadFile(objectURL);
return;
}
} catch (error) {
console.log(error, "getPdfFile");
}
},
_loadFile(url) {
PDFJS.getDocument(url).promise.then(pdf => {
this.pdfDoc = pdf;
const pdfPages = this.pdfDoc.numPages;
this.pdfList = pdfPages;
this.$nextTick(() => {
this._renderPage(1);
});
});
},
_renderPage(num) {
this.pdfDoc.getPage(num).then(page => {
let canvas = document.getElementById("pdfCanvas" + num + this.index);
var vp = page.getViewport({ scale: 1 });
let ctx = canvas.getContext("2d");
let dpr = window.devicePixelRatio || 1;
let bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
let ratio = dpr / bsr;
let viewport = page.getViewport({
scale: window.innerWidth / vp.width
});
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);
let renderContext = {
canvasContext: ctx,
viewport: viewport
};
console.log(canvas, "------");
if (canvas) {
page.render(renderContext);
}
if (this.pdfList > num) {
this._renderPage(num + 1);
}
});
}
好家伙,本以为这样OK的,因为当时测试数据返回只有一条,渲染出来一点毛病都没有,非常的nice。然而吧,其实数组里面是可能存在多条的,所以此时高兴的太早了。
当数据出现了两条的时候,此时,问题来了,先看下控制台报错是什么
这,这,这,这怎么就报错了呢?然后页面渲染的pdf也是错乱的,直接旋转了180度,而且显示也不全了,Σ(っ °Д °;)っ
遇事不决先百度,发现,难道就没有人遇到过这个问题吗?还是说我这个需求比较奇葩?不行我再找找,然而,我失败了,我没找到。/(ㄒoㄒ)/~~
没办法,自己来吧。先翻译一波这是啥意思:
错误:在多个render()操作期间不能使用相同的画布。使用不同的画布或确保先前的操作已被取消或完成。
操作期间不能使用相同的画布,emmmm,啥意思?我不是循环了吗?怎么会是同一个画布?
难道是高度问题?于是我设置了固定高度,失败。
难道是我循环嵌套的问题?于是我把canvas进行了封装,失败
<div v-if="pdfList.length > 0">
<div
v-for="(pdfPages, index) in pdfList"
:key="index"
class="pdf-preview"
>
<CanvasPreviewPdf :pdfData="pdfPages" :index="index" />
</div>
</div>
等等,我似乎忽略了什么重要的信息,多个render操作,相同的画布,我的画布为什么会相同?这是个好问题。于是我仔细对比了一下渲染出来的canvas标签,好家伙,设置的id,渲染出来的时候是一样的,就是在父组件中循环的时候,里面的canvas,每一组的id都是一样的,如下图(这是我修改过后的,修改前,pdfCanvas10和pdfCanvas11都叫做pdfCanvas1,这也就意味着重复了),于是乎,我把父组件中循环的index拼接在了后面,问题解决,封装组件代码如下
<template>
<div>
<div v-for="page in pdfList" :key="page">
<canvas :id="'pdfCanvas' + page + index"></canvas>
</div>
</div>
</template>
<script>
import PDFJS from "pdfjs-dist";
export default {
name: "PreviewPdf",
components: {},
data() {
return {
pdfList: []
};
},
props: {
pdfData: {
type: Object
},
index: {
type: Number
}
},
created() {
PDFJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.min.js");
this.getPdfFile(this.pdfData.licenseId);
},
methods: {
// pdf文件
async getPdfFile(licenseId) {
const url = ``;
try {
const res = await this.$http({
url,
method: "get",
params: {
licenseId
},
responseType: "blob"
});
if (res.status == 200) {
const content = res.data;
let file = new Blob([content], {
type: "application/pdf;charset-UTF-8"
});
const objectURL = URL.createObjectURL(file);
this.pdfList.push(objectURL);
this._loadFile(objectURL);
return;
}
} catch (error) {
console.log(error, "getPdfFile");
}
},
_loadFile(url) {
PDFJS.getDocument(url).promise.then(pdf => {
this.pdfDoc = pdf;
const pdfPages = this.pdfDoc.numPages;
this.pdfList = pdfPages;
this.$nextTick(() => {
this._renderPage(1);
});
});
},
_renderPage(num) {
this.pdfDoc.getPage(num).then(page => {
let canvas = document.getElementById("pdfCanvas" + num + this.index);
var vp = page.getViewport({ scale: 1 });
let ctx = canvas.getContext("2d");
let dpr = window.devicePixelRatio || 1;
let bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
let ratio = dpr / bsr;
let viewport = page.getViewport({
scale: window.innerWidth / vp.width
});
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);
let renderContext = {
canvasContext: ctx,
viewport: viewport
};
console.log(canvas, "------");
if (canvas) {
page.render(renderContext);
}
if (this.pdfList > num) {
this._renderPage(num + 1);
}
});
}
}
};
</script>
<style scoped></style>
后记
讲道理,真没想到是这么个问题。记录一下,或许能帮到和我遇到同样问题的伙伴