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.jsuseEffect(() => {pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';}, []);// 加載 PDFuseEffect(() => {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或遠程URLconst 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,但是對于熟練的用戶,還是有很多辦法繞過這些限制的。具體方法感興趣的同學可以自行搜索。
? ? ? ??