大家好!今天给大家拆解下 基于 Vue3 的在线 PDF 编辑 1.0 项目诞生记 里预览图模块的核心代码。不绕弯子,直接带大家 "扒一扒" 这些藏着小惊喜的技术实现!
基于 pdfjs-dist 与 pdf-lib 展示 PDF
在技术选型上,试过 N 种方案后,最终敲定了 pdfjs-dist 与 pdf-lib 组合。先看获取 PDF 地址的核心代码:
ini
/**
* @description: 获取 pdfUrl
* @param {string} url
* @return {*}
*/
const getPdfUrlFunc = async (url: string) => {
const existingPdfBytes = await fetch(url).then((res) => res.arrayBuffer());
pdfDoc = await PDFDocument.load(existingPdfBytes);
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
pdfUrl.value = URL.createObjectURL(blob);
}
这里先把 PDF 转成 Buffer,再捣鼓成 blob 喂给 pdfjsLib 生成预览。我用本地文件测试的,在线 PDF 直接用 pdfjsLib 应该也能搞定,感兴趣的小伙伴可以当 "技术探险家" 试试!
再看渲染 PDF 的关键逻辑:
ini
const rederPdfFunc = async (
scale: number,
canvasRefs: any,
istThumbnail: boolean = false,
startLine: Function,
drawLine: Function,
stopDrwa: Function,
addText: Function
) => {
if (!pdfUrl.value) return;
const loadingTask = pdfjsLib.getDocument(pdfUrl.value);
const pdf = await loadingTask.promise;
const thumbnailArr: string[] = []; // 缩略图
const thumbnailInfoArr: { imgUrl: string; pageIndex: number }[] = [];
pagesCount.value = pdf.numPages;
for (let i = 1; i <= pagesCount.value; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale }); // 设置合适的缩放比例
const canvas = canvasRefs["canvas" + (i - 1)]; // 获取对应的canvas元素
if (!canvas) break;
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
const fabricCanvas = new fabric.Canvas(`annotation-canvas_${i - 1}`, {
width: viewport.width,
height: viewport.height,
isDrawingMode: false,
// backgroundColor: 'transparent'
})
fabricCanvas.selectionColor = 'transparent'
fabricCanvas.selectionBorderColor = 'transparent'
// fabricCanvas.skipTargetFind = true // 禁止选中
fabricCanvas.on('mouse:down', startLine.bind(fabricCanvas, {
page: i - 1,
canvas: fabricCanvas,
})) // 鼠标在画布上按下
fabricCanvas.on('mouse:move', drawLine.bind(fabricCanvas, {
page: i - 1,
canvas: fabricCanvas,
})) // 鼠标在画布上移动
fabricCanvas.on('mouse:up', stopDrwa.bind(fabricCanvas, {
page: i - 1,
canvas: fabricCanvas,
})) // 鼠标在画布上移动
// fabricCanvas.on('mouse:dblclick', addText.bind(fabricCanvas, {
// page: i - 1,
// canvas: fabricCanvas,
// }))
fabricCanvasObj.value[`annotation-canvas_${i - 1}`] = fabricCanvas
const wrapper = canvas.parentElement;
wrapper.style.width = `${viewport.width}px`;
wrapper.style.height = `${viewport.height}px`;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext).promise;
if (istThumbnail) {
const imageUrl = canvas.toDataURL("image/png");
thumbnailArr.push(imageUrl);
thumbnailInfoArr.push({
imgUrl: imageUrl,
pageIndex: i,
});
}
}
if (istThumbnail) {
thumbnailObj.value = {
thumbnail: thumbnailArr,
thumbnailInfo: thumbnailInfoArr,
}
}
}
主体预览用 canvas 绘制,左侧预览默认生成图片。不过从性能角度 "唠一唠",左侧换成 canvas 更香哦!
左侧预览图选中,主题预览图跳转
想实现点击左侧跳转到对应页面?这段代码就是 "幕后推手":
ini
const setPageFunc = (pageRefs: HTMLElement | null, canvasRefs: Record<string, HTMLElement>, currenPage: number) => {
if (!pageRefs || !canvasRefs[`canvas${currenPage - 1}`]) return;
const targetScrollTop = canvasRefs[`canvas${currenPage - 1}`].offsetHeight * (currenPage - 1);
const startScrollTop = pageRefs.scrollTop;
const duration = 300; // 动画持续时间,单位毫秒
const startTime = performance.now();
const animateScroll = (currentTime: number) => {
const elapsedTime = currentTime - startTime;
if (elapsedTime < duration) {
const progress = elapsedTime / duration;
pageRefs.scrollTop = startScrollTop + (targetScrollTop - startScrollTop) * progress;
requestAnimationFrame(animateScroll);
} else {
pageRefs.scrollTop = targetScrollTop;
}
};
requestAnimationFrame(animateScroll);
};
通过计算 canvas 高度,配合丝滑的动画效果,点击左侧预览图,主体页面就能 "嗖" 地一下精准跳转!
主体预览图滚动,选中左侧预览图
反过来,主体滚动时左侧自动选中的功能,靠 IntersectionObserver 这位 "小帮手":
typescript
export const useMountObserve = (pageRefs: HTMLElement,
canvasRefs: any, pagesCount: number,
callback: (arg: string | number) => void) => {
let canvasIndex: string | number = 0
let observer: IntersectionObserver | null = null; // 当前可视窗口最大得canvas页码
/**
* @description: 初始化方法
* @return {*}
*/
const initFunc = () => {
observer = new IntersectionObserver(handleIntersection, {
root: pageRefs,
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
});
for (let i = 0; i < pagesCount; i++) {
const canvas = canvasRefs[`canvas${i}`];
if (canvas) {
observer.observe(canvas);
}
}
}
/**
* @description: 检测当前可视窗口占比最大得canvas
* @param {IntersectionObserverEntry} entries
* @return {*}
*/
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
const ratio = entry.intersectionRatio;
if (ratio > 0.5) {
canvasIndex = entry.target.getAttribute("data-index") || 0;
}
});
callback((+canvasIndex + 1))
};
initFunc()
onUnmounted(() => {
if (observer) {
observer.disconnect();
}
});
}
它实时监测每个 canvas 的可见比例,一旦超过 50%,就立刻通知左侧预览图 "对号入座"!
结语
以上就是预览图生成、翻页交互的核心源码解析啦!后续还会解锁更多功能的技术细节。想深入研究的同学,欢迎到 github.com/xknk/costom... 拉取源码 "把玩"!也期待和各位技术小伙伴交流,一起把这个项目打磨得更酷炫!