核心原理
- PDF 渲染: 浏览器本身无法直接理解PDF的矢量内容(文字、线条、形状),我们需要一个库来读取PDF文件,并将其中的每一页栅格化成一张图片(通常是PNG或JPEG格式)。
- 翻页UI: 我们需要一个类似书本或杂志的界面,包含左右两个页面区域,当用户点击“下一页”或“上一页”时,我们切换显示对应的图片。
- 交互控制: 监听用户的点击事件、键盘事件(如左右箭头)甚至触摸事件(滑动),来控制页面的切换。
技术选型
实现这个功能,最关键的是选择一个能够将PDF渲染成图片的库,目前最流行和强大的库是 PDF.js。

(图片来源网络,侵删)
- PDF.js: 由Mozilla开发,是开源的,它可以直接在浏览器中解析和渲染PDF,无需任何服务器端支持,我们将使用它来将PDF的每一页渲染成Canvas,然后我们可以将Canvas的内容转换成图片URL。
完整实现步骤与源码
我们将创建一个简单的HTML页面,实现以下功能:
- 用户通过
<input type="file">选择一个本地PDF文件。 - 使用PDF.js加载这个文件。
- 将PDF的每一页渲染成图片,并存储起来。
- 创建一个双页视图,并实现翻页逻辑。
第1步:准备HTML结构
创建一个 index.html 文件,包含基本的页面结构、文件选择器、书本视图区域和控制按钮。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">PDF 翻页阅读器</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>PDF 翻页阅读器</h1>
<!-- 文件选择器 -->
<div class="file-input-wrapper">
<input type="file" id="pdf-upload" accept="application/pdf">
<label for="pdf-upload">选择PDF文件</label>
</div>
<!-- 书本阅读区域 -->
<div class="book-viewer" id="book-viewer" style="display: none;">
<div class="book">
<div class="page left-page" id="left-page">
<!-- 左侧页面内容 -->
</div>
<div class="page right-page" id="right-page">
<!-- 右侧页面内容 -->
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button id="prev-btn">上一页</button>
<span id="page-info">0 / 0</span>
<button id="next-btn">下一页</button>
</div>
</div>
</div>
<!-- 引入 PDF.js 库 -->
<!-- 使用官方的CDN链接 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<!-- 设置PDF.js worker的路径 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<script src="script.js"></script>
</body>
</html>
第2步:添加CSS样式
创建一个 style.css 文件,让我们的书本看起来更美观。
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.container {
text-align: center;
width: 90%;
max-width: 900px;
}
.file-input-wrapper {
margin-bottom: 20px;
}
.file-input-wrapper input[type=file] {
display: none;
}
.file-input-wrapper label {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.file-input-wrapper label:hover {
background-color: #0056b3;
}
.book-viewer {
margin-top: 30px;
}
.book {
display: flex;
justify-content: center;
gap: 20px; /* 页面之间的缝隙 */
perspective: 2000px; /* 3D效果 */
margin-bottom: 20px;
}
.page {
width: 400px;
height: 600px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-image: linear-gradient(#e6e6e6 1px, transparent 1px),
linear-gradient(90deg, #e6e6e6 1px, transparent 1px);
background-size: 20px 20px;
}
.page img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.controls {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
}
.controls button {
padding: 8px 15px;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.controls button:hover {
background-color: #218838;
}
.controls button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
#page-info {
font-size: 1.1em;
font-weight: bold;
}
第3步:编写JavaScript逻辑
这是最核心的部分,创建 script.js 文件,实现PDF加载、页面渲染和翻页控制。

(图片来源网络,侵删)
document.addEventListener('DOMContentLoaded', () => {
const pdfUpload = document.getElementById('pdf-upload');
const bookViewer = document.getElementById('book-viewer');
const leftPage = document.getElementById('left-page');
const rightPage = document.getElementById('right-page');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const pageInfo = document.getElementById('page-info');
let pdfDoc = null;
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
let scale = 1.5; // 缩放比例,可以根据需要调整
let canvas = document.createElement('canvas'); // 创建一个离屏canvas用于渲染
let ctx = canvas.getContext('2d');
let pagesImages = []; // 存储所有页面渲染后的图片URL
// 渲染页面函数
const renderPage = (num) => {
pageRendering = true;
// 获取指定页面的PDF对象
pdfDoc.getPage(num).then(page => {
const viewport = page.getViewport({ scale: scale });
// 设置canvas尺寸
canvas.height = viewport.height;
canvas.width = viewport.width;
// 渲染PDF页面到canvas
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
const renderTask = page.render(renderContext);
// 等待渲染完成
renderTask.promise.then(() => {
// 将canvas内容转换为图片URL并存储
const imgData = canvas.toDataURL('image/png');
pagesImages[num - 1] = imgData; // 数组索引从0开始
pageRendering = false;
if (pageNumPending !== null) {
// 如果有等待渲染的页面,则渲染它
renderPage(pageNumPending);
pageNumPending = null;
}
// 更新显示
updatePageView();
});
});
};
// 如果有另一个页面在渲染,则将当前页面加入队列
const queueRenderPage = (num) => {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
};
// 更新页面显示
const updatePageView = () => {
// 清空当前页面
leftPage.innerHTML = '';
rightPage.innerHTML = '';
// 双页模式:显示当前页和下一页
if (pdfDoc) {
// 左侧页
if (pageNum >= 1) {
const leftImg = document.createElement('img');
leftImg.src = pagesImages[pageNum - 1];
leftPage.appendChild(leftImg);
}
// 右侧页
if (pageNum + 1 <= pdfDoc.numPages) {
const rightImg = document.createElement('img');
rightImg.src = pagesImages[pageNum];
rightPage.appendChild(rightImg);
}
}
// 更新页面信息
pageInfo.textContent = `${pageNum} / ${pdfDoc.numPages}`;
// 更新按钮状态
prevBtn.disabled = pageNum <= 1;
nextBtn.disabled = pageNum >= pdfDoc.numPages;
};
// 上一页
const onPrevPage = () => {
if (pageNum <= 1) {
return;
}
pageNum--;
updatePageView();
};
// 下一页
const onNextPage = () => {
if (pageNum >= pdfDoc.numPages) {
return;
}
pageNum++;
updatePageView();
};
// 事件监听
prevBtn.addEventListener('click', onPrevPage);
nextBtn.addEventListener('click', onNextPage);
// 键盘事件监听
document.addEventListener('keydown', (e) => {
if (bookViewer.style.display === 'none') return;
if (e.key === 'ArrowLeft') {
onPrevPage();
} else if (e.key === 'ArrowRight') {
onNextPage();
}
});
// 文件上传处理
pdfUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = () => {
const typedarray = new Uint8Array(fileReader.result);
// 使用PDF.js加载PDF
pdfjsLib.getDocument(typedarray).promise.then(pdf_ => {
pdfDoc = pdf_;
console.log(`PDF loaded: ${pdfDoc.numPages} pages`);
// 初始化所有页面的图片存储数组
pagesImages = new Array(pdfDoc.numPages);
// 渲染前两页
for (let i = 1; i <= Math.min(2, pdfDoc.numPages); i++) {
queueRenderPage(i);
}
// 显示书本阅读器
bookViewer.style.display = 'block';
updatePageView();
}).catch(error => {
console.error('Error loading PDF:', error);
alert('PDF加载失败,请检查文件是否损坏。');
});
};
fileReader.readAsArrayBuffer(file);
} else {
alert('请选择一个有效的PDF文件。');
}
});
});
如何运行
- 将
index.html,style.css, 和script.js这三个文件放在同一个文件夹下。 - 用浏览器打开
index.html文件。 - 点击“选择PDF文件”按钮,选择你电脑上的一个PDF文件。
- 文件加载后,就会以翻页的形式显示在页面上。
代码解释与扩展
script.js中的pdfjsLib: 我们通过CDN引入了PDF.js,所以可以直接使用全局的pdfjsLib对象。- 离屏Canvas: 我们创建了一个不在DOM中的
<canvas>元素,这样做的好处是,我们可以在后台默默地渲染页面,而不会干扰页面的布局和显示,渲染完成后,我们将其转换为dataURL(base64编码的图片) 存储在pagesImages数组中。 queueRenderPage: 这是一个优化,由于PDF渲染是异步操作,如果用户快速点击翻页,可能会导致多个渲染任务同时进行,造成混乱,这个函数确保了任何时候只有一个页面在渲染,其他的会被排队等待。- 双页视图:
updatePageView函数根据当前页码pageNum,决定左右两个<div>中应该显示哪两张图片,这是实现书本样式的关键。 - 扩展功能:
- 触摸滑动: 可以使用
touchstart和touchend事件来检测用户的滑动方向,实现触摸屏设备的翻页。 - 加载进度条: 在渲染所有页面之前,可以显示一个进度条,告知用户正在准备。
- 缩放功能: 添加按钮来增加或减少
scale变量,然后重新渲染页面,可以实现放大和缩小。 - 目录/书签: PDF.js 也可以提取PDF的目录信息,你可以将其显示在侧边栏,实现快速跳转。
- 触摸滑动: 可以使用
这个例子为你提供了一个坚实的基础,你可以基于它进行更多的定制和功能扩展。

(图片来源网络,侵删)
