在Next.js里玩转pdf预览

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,但是对于熟练的用户,还是有很多办法绕过这些限制的。具体方法感兴趣的同学可以自行搜索。

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/91743.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/91743.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法竞赛备赛——【图论】求最短路径——Floyd算法

floyd算法 基于动态规划 应用&#xff1a;求多源最短路 时间复杂度&#xff1a;n^3 dijkstra&#xff1a;不能解决负边权 floyd&#xff1a;能解决负边权 不能解决负边权回路问题 求最短路径&#xff1a;dijkstra bfs floyd 思路 1.让任意两点之间的距离变短&#xff1a;引入…

双指针(滑动窗口)相关算法题

双指针算法有时候也叫尺取法或者滑动窗口&#xff0c;是⼀种优化暴力枚举策略的手段&#xff1a;当我们发现在两层 for 循环的暴力枚举过程中&#xff0c;两个指针是可以不回退的&#xff0c;此时我们就可以利用两个指针不回退的性质来优化时间复杂度。因为双指针算法中&#x…

ScratchCard刮刮卡交互元素的实现

效果展示 刮刮卡是⼀种常见的网页交互元素&#xff0c;通过模拟物理世界的刮涂层来揭示下方的内容。这种效果主要依赖于HTML5的 元素来实现。以下是⼀个基于TypeScript的刮刮卡实现示例&#xff0c;包括配置项、初始化方法和核心的刮开逻辑。下面是展示的效果部分刮开效果&…

【Python LeetCode 专题】热题 100,重在思路

哈希1. 两数之和49. 字母异位词分组128. 最长连续序列双指针283. 移动零11. 盛最多水的容器15. 三数之和42. 接雨水滑动窗口3. 无重复字符的最长子串438. 找到字符串中所有字母异位词子串560. 和为 K 的子数组239. 滑动窗口最大值普通数组53. 最大子数组和56. 合并区间189. 轮转…

openEuler 22.03 LTS Rootless Docker 安装指南

openEuler 22.03 LTS Rootless Docker 安装指南 1.创建普通用户&#xff08;用于无根模式&#xff09; sudo useradd -m docker-user sudo passwd docker-user # 设置密码 sudo usermod --add-subuids 100000-165535 docker-user sudo usermod --add-subgids 100000-165535 do…

CMake指令:常见内置命令行工具( CMake -E )

目录 1.简介 2.核心作用 3.常用命令介绍 3.1.文件操作命令 3.2.系统命令执行 3.3.校验与哈希 3.4.流程控制与等待 3.5.路径与文件处理 3.6.归档与压缩 3.7.网络与下载 3.8.实用工具 4.使用示例 5.与 shell 命令的对比 6.在 CMake 脚本中使用 7.总结 相关链接 1…

YOLO融合CAF-YOLO中的ACFM模块

YOLOv11v10v8使用教程&#xff1a; YOLOv11入门到入土使用教程 YOLOv11改进汇总贴&#xff1a;YOLOv11及自研模型更新汇总 《CAF-YOLO: A Robust Framework for Multi-Scale Lesion Detection in Biomedical Imagery》 一、 模块介绍 论文链接&#xff1a;https://arxiv.org…

Webpack 项目构建优化详解

1. 相关面试题 1.1. 做过哪些Webpack打包构建优化? 代码分割:使用 Webpack 的 SplitChunksPlugin 进行代码分割,将第三方库、公共代码与业务代码分离,提高缓存利用率和加载速度。 Tree Shaking:通过配置 mode: production 或使用 TerserPlugin,移除未引用的代码,减少…

【深度学习基础】张量与Tensor的区别?从标量到深度学习的多维世界

目录引言一、张量&#xff08;Tensor&#xff09;的定义与特性1. 数学中的张量2. 深度学习中的Tensor二、标量&#xff08;Scalar&#xff09;是什么&#xff1f;三、深度学习中的其他核心量1. 向量&#xff08;Vector&#xff09;2. 矩阵&#xff08;Matrix&#xff09;3. 高阶…

设计模式一: 模板方法模式 (Template Method Pattern)

模板方法模式是一种行为设计模式&#xff0c;它通过定义一个算法的骨架&#xff0c;而将一些步骤延迟到子类中实现。Template Method 使得子类可以不改变&#xff08;复用&#xff09;一个算法结构 即可重定义&#xff08;override 重写&#xff09;该算法的某些特定步骤。基本…

Linux驱动学习day24(UART子系统)

一、UART硬件理论1.1 作用及功能UART&#xff1a;通用异步收发传输器&#xff0c;简称串口。功能&#xff1a;移植u-boot、内核时&#xff0c;主要使用串口查看打印信息。外接各种模块&#xff0c;比如蓝牙GPS模块。使用UART的时候&#xff0c;要注意1. 波特率 2. 格式&#xf…

NFS共享服务器

目录 任务要求 思路总结 1.NFS共享服务 服务端 (ip 192.168.48.128) 客户端 (ip 192.168.48.130) 2.配置autofs自动挂载 任务要求 1.NFS服务器,可以让PC将网络中的NFS服务器共享的目录挂载到本地端的文件系统中,而在本地端的系统中看来&#xff0c;那个远程主机的目…

FreeRTOS学习笔记之队列

小编正在学习嵌入式软件&#xff0c;目前建立了一个交流群&#xff0c;可以留下你的评论&#xff0c;我拉你进群一、简介队列是为了任务与任务、任务与中断之间的通信而准备的&#xff0c;可以在任务与任务、任务与中断之间消息传递&#xff0c;队列中可以存储有限的、大小固定…

垃圾收集器-ZGC

前言在Java开发中&#xff0c;垃圾收集器的选择对系统性能有着致命的影响。Java 8后&#xff0c;虽然G1 GC成为默认&#xff0c;但是它在延迟性控制上仍有限。ZGC作为最新一代高性能低延迟垃圾收集器&#xff0c;解决了CMS和G1在延迟、垃圾堆容量和吞吐量方面的重大突破。本文将…

计算机“十万个为什么”之跨域

计算机“十万个为什么”之跨域 本文是计算机“十万个为什么”系列的第五篇&#xff0c;主要是介绍跨域的相关知识。 作者&#xff1a;无限大 推荐阅读时间&#xff1a;10 分钟 一、引言&#xff1a;为什么会有跨域这个“拦路虎”&#xff1f; 想象你正在参观一座戒备森严的城堡…

C语言:20250719笔记

字符数组在C语言中&#xff0c;支持字符串常量&#xff0c;不支持字符串变量。如果想要实现类似的字符串变量&#xff0c;C语言提供了两种实现方式&#xff1a;字符数组&#xff1a;char name[] “哪吒”&#xff1b;字符指针&#xff1a;char *name "娜吒"&#x…

decltype是什么,什么作用?

基本概念decltype 是 C11 引入的关键字&#xff0c;用于推导表达式的类型&#xff0c;且会完整保留类型的细节&#xff08;包括 const、引用 &、指针 * 等&#xff09;。语法:decltype(表达式) 变量名核心特点1.推导依据是表达式本身&#xff0c;而非表达式的结果&#xff…

RPC 与 Feign 的区别笔记

一、基本概念 1.1 RPC&#xff08;Remote Procedure Call&#xff09; 定义&#xff1a;远程过程调用&#xff0c;允许像调用本地方法一样调用远程服务的方法。 本质&#xff1a;跨进程通信&#xff0c;隐藏了底层网络通信的复杂性。 常见实现&#xff1a; Java 原生 RMIDub…

高防IP能够防御CC攻击吗?它具备哪些显著优势?

摘要&#xff1a; 面对日益复杂的网络攻击&#xff0c;高防IP作为重要的安全工具&#xff0c;不仅能防御常见的DDoS攻击&#xff0c;还能有效应对CC攻击。本文将解析高防IP防御CC攻击的原理及其核心优势&#xff0c;帮助读者了解其在网络安全中的关键作用。一、高防IP能否防御C…

TypeScript 类型注解(一)

一、TypeScript 类型注解1、什么是TpyeScript类型注解- 是否还记得TypeScript的两个重要特性&#xff1f;- 类型系统、适用于任何规模- 可以说&#xff0c;TS的类型系统是TS最重要的功能&#xff1b;那么什么是类型注解呢&#xff1f;其实就是在声明变量时&#xff0c;将变量的…