前言
最近来了个需求,要求实现h5端pdf预览,支持滑动翻页,双指缩放,最好是不进行路由跳转,作为组件被加载出来。
调(goo)研(gle)了一番后,发现H5端预览pdf主要实现方式为以下几种:
- 借助 npm 包实现,大部分都是基于 pdfjs 进行二次封装的。(这一实现思路主要问题是很多包不兼容 vue3 )
- iframe 借助第三方服务实现,比如 google doc(需翻墙),office view.officeapps.live.com/op/view.asp… 优点就是快捷方便,缺点是样式喝功能不可控
- 第三种其实和第一种差不多,只不过是直接代码下载放在了 public,主要是为了修改源码,兼容vue3
我实现的方案是用了第一种,主要是借助了 pdfjs 这个 npm 包。综合看下来,安装指定版本 @2.5.207 这一版本问题较少,兼容性也不错。
缩放是通过 style zoom属性进行双向绑定 双指缩放主要是通过 touch 事件进行的处理。
废话不多说。来看下代码方面
渲染pdf
html 方面 主要是写一个 canvas
<div class="pdf-viewer" ref="Pdfcontent">
<template v-for="item in totalPageNum" :key="item">
<canvas
class="pdf-item"
:style="{ zoom: zoom / 10 }"
:id="`pdf-canvas-${item}`"
@touchstart="touchstarthandle"
@touchmove="touchmovehandle"
@touchend="touchendhandle"
@touchcancel="touchendhandle"
></canvas>
</template>
</div>
</template>
js 代码
import { reactive, toRefs, nextTick, watchEffect, ref } from 'vue';
import * as pdfjs from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
name: 'PdfViewer',
props: {
url: { type: String, default: '' }, // pdf文件路径
},
setup(props, { emit }) {
const state = reactive({
pdfCtx: null,
currentPageNum: 0, // 当前页
totalPageNum: 0,
zoom: 10, // 目前暂时采用css方式缩放页面
minZoom: 10, // 缩放最小值 一倍
maxZoom: 50, // 缩放最大值 五倍
});
const Pdfcontent = ref(null);
const resolvePdf = (url) => {
const loadingTask = pdfjs.getDocument(url);
loadingTask.promise.then((pdf) => {
state.pdfCtx = pdf;
state.totalPageNum = pdf.numPages;
state.currentPageNum = 1;
// 动态计算scale
pdf.getPage(1).then((res) => {
let boxWidth = Pdfcontent.value.clientWidth - 20;
const [x1, , x2] = res._pageInfo.view;
const pageWidth = x2 - x1;
state.scale = (boxWidth * (state.maxZoom / 10)) / pageWidth;
});
nextTick(() => {
renderPdf();
});
});
};
const renderPdf = (num = 1) => {
state.pdfCtx.getPage(num).then((page) => {
const canvas = document.getElementById(`pdf-canvas-${num}`);
const ctx = canvas.getContext('2d');
const viewport = page.getViewport({ scale: state.scale });
// 画布大小
canvas.width = viewport.width;
canvas.height = viewport.height;
// 画布的dom大小,这里是根据引用此组件的父级节点设置的
const clientWidth = Pdfcontent.value.clientWidth;
canvas.style.width = clientWidth + 'px';
// 根据pdf每页的宽高比例设置canvas的高度
canvas.style.height = clientWidth * (viewport.height / viewport.width) + 'px';
page.render({
canvasContext: ctx,
viewport,
});
if (num < state.totalPageNum) {
renderPdf(num + 1);
} else {
emit('onRendered');
// 渲染完毕了,如果有loading 啥的可以在这里直接结束
}
});
};
watchEffect(() => {
if (props.url) {
//这里可以开启loading。如果文件过大,会有一阵子空白,
resolvePdf(props.url);
}
});
return {
...toRefs(state),
Pdfcontent,
};
},
};
</script>
这里就可以直接渲染出pdf了。
同时也解决了移动端模糊的问题,但是不确定带有公章的pdf是否能正常渲染。 此处代码主要是参(复)考(制)这个文章
接下来就是双指缩放
双指缩放
双指缩放是基于touch事件来进行处理的,核心逻辑就在于判断几个问题
- 是不是双指触摸
- 是缩小还是放大
这块代码主要是通过参(复)考(制)张鑫旭的博客,鑫哥YYDS,具体链接在这,讲的非常清晰,可以打开来看看。
然后我们的代码如下
const touchstarthandle = (e) => {
console.log('触摸开始');
const touches = e.touches;
const events = touches[0];
const events2 = touches[1];
// e.preventDefault();
// 如果阻止默认事件,单指滑动就会失效
state.pageX = events.pageX;
state.pageY = events.pageY;
state.moveable = true;
if (events2) {
state.pageX2 = events2.pageX;
state.pageY2 = events2.pageY;
}
state.originzoom = state.zoom;
};
// 用来获取坐标之间的比例,判断是缩小还是放大
const getDistance = (start, stop) => {
return Math.hypot(stop.x - start.x, stop.y - start.y);
};
const touchmovehandle = (e) => {
if (!state.moveable) {
return;
}
const touches = e.touches;
const events = touches[0];
const events2 = touches[1];
// 判断是否双指移动 是的话阻止默认事件
// 否则不阻止 不然容器中的canvas无法滚动翻页
// 如果不阻止冒泡,在 iOS 设备中双指缩放会引起整个 webview 缩放
if (events2) {
e.preventDefault();
if (!state.pageX2) {
state.pageX2 = events2.pageX;
}
if (!state.pageY2) {
state.pageY2 = events2.pageY;
}
//获取坐标之间的比例
var zoom =
getDistance(
{
x: events.pageX,
y: events.pageY,
},
{
x: events2.pageX,
y: events2.pageY,
},
) /
getDistance(
{
x: state.pageX,
y: state.pageY,
},
{
x: state.pageX2,
y: state.pageY2,
},
);
let newScale = state.originzoom * zoom;
//限制缩放倍数
if (newScale > 50) {
newScale = 50;
} else if (newScale < 5) {
newScale = 5;
}
state.zoom = newScale;
}
};
const touchendhandle = () => {
state.moveable = false;
delete state.pageX2;
delete state.pageY2;
};
全部代码
OK,到了这里,我们的功能基本上完成了,奉上全部代码,终极缝合怪
<template>
<div class="pdf-viewer" ref="Pdfcontent">
<template v-for="item in totalPageNum" :key="item">
<!-- <canvas
class="pdf-item"
:id="`pdf-canvas-${item}`"
:style="{ zoom: zoom / 10 }"
></canvas> -->
<canvas
class="pdf-item"
:style="{ zoom: zoom / 10 }"
:id="`pdf-canvas-${item}`"
@touchstart="touchstarthandle"
@touchmove="touchmovehandle"
@touchend="touchendhandle"
@touchcancel="touchendhandle"
></canvas>
</template>
</div>
</template>
<script>
import { reactive, toRefs, nextTick, watchEffect, ref } from 'vue';
import * as pdfjs from 'pdfjs-dist';
// import {GlobalWorkerOptions, getDocument} from 'pdfjs-dist'
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
// import { Toast } from 'vant';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
name: 'PdfViewer',
props: {
url: { type: String, default: '' }, // pdf文件路径
},
setup(props, { emit }) {
const state = reactive({
pdfCtx: null,
currentPageNum: 0, // 当前页
totalPageNum: 0,
zoom: 10, // 目前暂时采用css方式缩放页面
minZoom: 10, // 缩放最小值 一倍
maxZoom: 50, // 缩放最大值 五倍
});
const Pdfcontent = ref(null);
const resolvePdf = (url) => {
const loadingTask = pdfjs.getDocument(url);
loadingTask.promise.then((pdf) => {
state.pdfCtx = pdf;
state.totalPageNum = pdf.numPages;
state.currentPageNum = 1;
// 动态计算scale
pdf.getPage(1).then((res) => {
let boxWidth = Pdfcontent.value.clientWidth - 20;
const [x1, , x2] = res._pageInfo.view;
const pageWidth = x2 - x1;
state.scale = (boxWidth * (state.maxZoom / 10)) / pageWidth;
console.log(boxWidth, state.scale, '5');
});
nextTick(() => {
renderPdf();
});
});
};
const renderPdf = (num = 1) => {
state.pdfCtx.getPage(num).then((page) => {
const canvas = document.getElementById(`pdf-canvas-${num}`);
const ctx = canvas.getContext('2d');
const viewport = page.getViewport({ scale: state.scale });
// 画布大小,默认值是width:300px,height:150px
canvas.width = viewport.width;
canvas.height = viewport.height;
// 画布的dom大小, 设置移动端,宽度设置铺满整个屏幕
const clientWidth = Pdfcontent.value.clientWidth;
canvas.style.width = clientWidth + 'px';
// 根据pdf每页的宽高比例设置canvas的高度
canvas.style.height = clientWidth * (viewport.height / viewport.width) + 'px';
page.render({
canvasContext: ctx,
viewport,
});
if (num < state.totalPageNum) {
renderPdf(num + 1);
} else {
emit('onRendered');
// Toast.clear();
}
});
};
const touchstarthandle = (e) => {
console.log('触摸开始');
const touches = e.touches;
const events = touches[0];
const events2 = touches[1];
// e.preventDefault();
state.pageX = events.pageX;
state.pageY = events.pageY;
state.moveable = true;
console.log('sksks');
if (events2) {
state.pageX2 = events2.pageX;
state.pageY2 = events2.pageY;
}
state.originzoom = state.zoom;
console.log('触摸开始', state);
};
const getDistance = (start, stop) => {
return Math.hypot(stop.x - start.x, stop.y - start.y);
};
const touchmovehandle = (e) => {
if (!state.moveable) {
return;
}
const touches = e.touches;
const events = touches[0];
const events2 = touches[1];
//双指移动 阻止默认事件,否则不阻止 不然容器中的canvas无法滚动翻页
console.log('jssk');
if (events2) {
e.preventDefault();
if (!state.pageX2) {
state.pageX2 = events2.pageX;
}
if (!state.pageY2) {
state.pageY2 = events2.pageY;
}
//获取坐标之间的比例
var zoom =
getDistance(
{
x: events.pageX,
y: events.pageY,
},
{
x: events2.pageX,
y: events2.pageY,
},
) /
getDistance(
{
x: state.pageX,
y: state.pageY,
},
{
x: state.pageX2,
y: state.pageY2,
},
);
let newScale = state.originzoom * zoom;
if (newScale > 50) {
newScale = 50;
} else if (newScale < 5) {
newScale = 5;
}
console.log(state, zoom, newScale, '缩放');
state.zoom = newScale;
}
};
const touchendhandle = () => {
state.moveable = false;
delete state.pageX2;
delete state.pageY2;
};
watchEffect(() => {
if (props.url) {
// Toast.loading({
// message: '文件加载中...',
// overlay: true,
// forbidClick: true,
// duration: 0,
// });
resolvePdf(props.url);
}
});
return {
...toRefs(state),
Pdfcontent,
touchstarthandle,
touchmovehandle,
touchendhandle,
};
},
};
</script>
<style lang="less" scoped>
.pdf-viewer {
padding-right: 20px;
width: 100%;
height: 100%;
overflow: scroll;
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
}
.pdf-viewer::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
</style>
这样代码功能实现了,但其实会面临性能问题,如果加载大型pdf ,生成几十个 cavas 并绘制,性能直接爆炸,所以可以在此基础上进行优化,将递归方式改变为 虚拟渲染,先渲染在视图区域内的cavas,这是接下来待实现优化方向。
谢谢观看,有什么问题可以留言~