1.背景
在项目开发中,pdf预览是一个很常见的业务。各大公司为了保护自己的知识产权,也会对pdf预览进行限制,比如:不允许下载、打印,不允许提取文字等等。要想在实现预览功能的基础上还要附加这些限制,有很多中可选的方法。本篇文章主要从前端视角谈谈怎么实现这个业务。
在开始讲解之前,需要先明确一点:使用纯前端的方法是无法完全避免用户窃取pdf的内容的,只能通过一些配置和脚本增加用户获取的难度。更安全的方法是后端对于pdf资源的请求加以限制,或者对pdf增加水印等等。
2.技术栈
本篇文章是在Next.js(React框架)的基础上借助pdf.js三方包演示怎么实现pdf预览和限制下载的。读者需要对React,JavaScript语法有基本的了解。
3.实现pdf预览
3.1. 安装pdf.js依赖
npm install pdfjs-dist
# 或者
yarn add pdfjs-dist
3.2. 创建 PDF 查看器组件
在 components/PdfViewer.js
中创建组件:
'use client'; // 必须标记为客户端组件
import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
export default function PdfViewer({ pdfUrl }) {
const canvasRef = useRef(null);
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [scale, setScale] = useState(1.5);
// 初始化 PDF.js
useEffect(() => {
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
}, []);
// 加载 PDF
useEffect(() => {
if (!pdfUrl) return;
const loadPdf = async () => {
const loadingTask = pdfjsLib.getDocument(pdfUrl);
const pdf = await loadingTask.promise;
setNumPages(pdf.numPages);
renderPage(pdf, currentPage);
};
loadPdf().catch(console.error);
}, [pdfUrl, currentPage]);
// 渲染指定页面
const renderPage = async (pdf, pageNum) => {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
};
const goToPrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const goToNextPage = () => {
if (currentPage < numPages) {
setCurrentPage(currentPage + 1);
}
};
return (
<div className="pdf-viewer">
<div className="pdf-controls">
<button onClick={goToPrevPage} disabled={currentPage <= 1}>
上一页
</button>
<span>
第 {currentPage} 页 / 共 {numPages} 页
</span>
<button onClick={goToNextPage} disabled={currentPage >= numPages}>
下一页
</button>
<select
value={scale}
onChange={(e) => setScale(parseFloat(e.target.value))}
>
<option value="0.5">50%</option>
<option value="1.0">100%</option>
<option value="1.5">150%</option>
<option value="2.0">200%</option>
</select>
</div>
<div className="pdf-canvas-container">
<canvas ref={canvasRef} />
</div>
</div>
);
}
3.3. 创建样式文件
在 components/PdfViewer.module.css
中:
.pdf-viewer {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.pdf-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.pdf-canvas-container {
border: 1px solid #ddd;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
overflow: auto;
max-height: 80vh;
}
button {
padding: 5px 10px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
select {
padding: 5px;
}
3.4. 在页面中使用组件
在 app/page.js
中:
import PdfViewer from '../components/PdfViewer';
import styles from './page.module.css';
export default function Home() {
// 可以是本地public文件夹中的PDF或远程URL
const pdfUrl = '/sample.pdf'; // 确保PDF文件放在public文件夹中
return (
<main className={styles.main}>
<h1>PDF 查看器</h1>
<PdfViewer pdfUrl={pdfUrl} />
</main>
);
}
3.5.注意事项
在组件的第一行要声明‘use client’告诉next这是客户端组件(因为pdf.js调用的是浏览器的canvas用来绘制pdf后显示的,所以必须在浏览器环境下才能运行)
4.全局事件限制pdf
// 全局事件监听
useEffect(() => {
// 禁用鼠标右键
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
return false;
};
// 禁用键盘快捷键
const handleKeyDown = (e: KeyboardEvent) => {
if (
(e.ctrlKey && (e.key === 's' || e.key === 'S')) ||
(e.ctrlKey && (e.key === 'p' || e.key === 'P')) ||
(e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'i')) ||
e.key === 'F12' ||
(e.ctrlKey && e.shiftKey && (e.key === 'C' || e.key === 'c')) ||
(e.ctrlKey && e.shiftKey && (e.key === 'J' || e.key === 'j')) ||
(e.ctrlKey && (e.key === 'u' || e.key === 'U')) ||
(e.ctrlKey && (e.key === 'a' || e.key === 'A')) ||
(e.ctrlKey && (e.key === 'c' || e.key === 'C')) ||
(e.ctrlKey && (e.key === 'v' || e.key === 'V')) ||
(e.ctrlKey && (e.key === 'x' || e.key === 'X'))
) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
// 禁用文本选择
const handleSelectStart = (e: Event) => {
e.preventDefault();
return false;
};
// 禁用拖拽
const handleDragStart = (e: DragEvent) => {
e.preventDefault();
return false;
};
document.addEventListener('contextmenu', handleContextMenu, true);
document.addEventListener('keydown', handleKeyDown, true);
document.addEventListener('selectstart', handleSelectStart, true);
document.addEventListener('dragstart', handleDragStart, true);
window.addEventListener('keydown', handleKeyDown, true);
return () => {
// 组件注销时,清除事件方式内存泄漏
document.removeEventListener('contextmenu', handleContextMenu, true);
document.removeEventListener('keydown', handleKeyDown, true);
document.removeEventListener('selectstart', handleSelectStart, true);
document.removeEventListener('dragstart', handleDragStart, true);
window.removeEventListener('keydown', handleKeyDown, true);
};
}, []);
通过这些全局事件的引用可以很好的限制普通用户对于下载pdf,但是对于熟练的用户,还是有很多办法绕过这些限制的。具体方法感兴趣的同学可以自行搜索。