核心原理:为什么不能直接用 <img>

PDF 是一种复杂的文档格式,它包含矢量图形、文本层、字体、加密信息等,普通的图片标签 <img> 无法解析这些复杂结构,直接显示 PDF 文件通常会导致浏览器下载它,而不是在页面上预览。

网站实现在线pdf浏览
(图片来源网络,侵删)

我们需要借助“翻译器”——JavaScript PDF 渲染库,将 PDF 的内容“翻译”成浏览器可以理解的格式(通常是 HTML5 Canvas 或 SVG),然后渲染到网页上。


主流实现方案对比

目前主要有三种主流的在线 PDF 浏览方案,各有优劣,适用于不同的场景。

方案 核心技术 优点 缺点 适用场景
客户端渲染 PDF.js - 纯前端,无需服务器处理,减轻服务器负担
- 开源免费,社区活跃
- 功能强大,可高度定制
- 首次加载时需要下载 PDF.js 库,可能影响首屏速度
- 大文件或多页渲染可能占用较多客户端内存和CPU
- 静态文档展示(如产品手册、电子书)
- 对加载速度要求不高的场景
- 需要离线访问或数据隐私要求高的场景
服务端渲染 PDF to Image - 首屏加载极快,用户体验好
- 服务器端处理,客户端负担小
- 服务器开销大,CPU 和内存消耗高
- 服务器需要安装额外库(如 pdf2pic, pdf2image
- 无法进行文本选择、复制等交互
- 需要快速加载的文档门户
- 对客户端性能要求低的场景
- PDF 文件不经常变动
第三方服务 Google Docs Viewer, PDF.js Viewer - 集成最简单,几行代码即可实现
- 免费,有大型公司技术支持(如 Google)
- 依赖外部服务,有可用性风险
- 存在隐私和安全问题,文件会上传到第三方服务器
- 自定义程度低,有广告或品牌限制
- 快速原型验证
- 内部工具,对隐私要求不高的场景
- 不想自己维护代码的情况

客户端渲染 (推荐,最常用)

我们以 Mozilla 开源的 PDF.js 为例,这是目前最流行、功能最强大的前端 PDF 渲染库。

实现步骤:

准备工作

网站实现在线pdf浏览
(图片来源网络,侵删)
  • 一个 HTML 文件 (e.g., index.html)
  • 一个 PDF 文件 (e.g., sample.pdf),放在与 index.html 同一个目录下,或者一个可公开访问的 URL。

引入 PDF.js 库

最简单的方式是通过 CDN 引入,在 <head> 标签中添加:

<!-- 引入 PDF.js 的核心库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<!-- 设置 worker 脚本的路径,PDF.js 使用 Web Worker 来在后台处理 PDF,避免阻塞主线程 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>

创建 HTML 结构

<body> 中,我们需要一个容器来放置渲染出的 PDF 页面,以及一个 <canvas> 元素来绘制每一页。

网站实现在线pdf浏览
(图片来源网络,侵删)
<div class="pdf-container">
  <div id="pdf-viewer"></div>
</div>

编写 JavaScript 代码

这是最关键的一步,我们将编写脚本来加载 PDF 文件,逐页渲染到 canvas 上。

<script>
// PDF.js 的全局 worker 路径配置
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
// PDF 文件的路径
const pdfUrl = 'sample.pdf'; // 或者使用网络 URL,如 'https://example.com/files/document.pdf'
// 获取渲染容器
const pdfViewer = document.getElementById('pdf-viewer');
// 异步函数,用于加载和渲染 PDF
async function loadPDF() {
  try {
    // 1. 获取 PDF 文件的数据
    const pdf = await pdfjsLib.getDocument(pdfUrl).promise;
    // 2. 获取 PDF 的总页数
    const numPages = pdf.numPages;
    console.log(`PDF 总页数: ${numPages}`);
    // 3. 循环渲染每一页
    for (let pageNum = 1; pageNum <= numPages; pageNum++) {
      // 获取指定页
      const page = await pdf.getPage(pageNum);
      // 设置缩放比例,1.0 表示 100%
      const scale = 1.5;
      const viewport = page.getViewport({ scale: scale });
      // 为每一页创建一个 canvas 元素
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');
      // 设置 canvas 的尺寸
      canvas.height = viewport.height;
      canvas.width = viewport.width;
      // 将 canvas 添加到容器中
      pdfViewer.appendChild(canvas);
      // 渲染 PDF 页面到 canvas 上
      const renderContext = {
        canvasContext: context,
        viewport: viewport
      };
      await page.render(renderContext).promise;
    }
  } catch (error) {
    console.error('加载或渲染 PDF 时出错:', error);
    pdfViewer.innerHTML = '<p>无法加载 PDF 文件,请检查文件路径或网络连接。</p>';
  }
}
// 调用函数,开始加载
loadPDF();
</script>

添加基本样式 (可选)

为了让 PDF 页面垂直排列,可以添加一些 CSS:

<style>
  body {
    font-family: sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    margin: 0;
    padding: 20px;
  }
  .pdf-container {
    border: 1px solid #ccc;
    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
    padding: 20px;
    max-width: 100%;
    overflow: auto; /* PDF 很宽,允许滚动 */
  }
  canvas {
    display: block; /* 去除 canvas 底部的小间隙 */
    margin-bottom: 20px; /* 页与页之间的间距 */
    border: 1px solid #eee;
  }
</style>

将以上所有代码整合到一个 index.html 文件中,用浏览器打开,你就能看到 PDF 的内容了。


服务端渲染 (以 Node.js 为例)

这种方案需要你的后端环境支持 Node.js。

安装依赖

在你的 Node.js 项目中安装 pdf2pic 库:

npm install pdf2pic

编写服务端代码 (e.g., server.js)

const express = require('express');
const pdf2pic = require('pdf2pic').fromPath;
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
// 静态文件服务,用于存放生成的图片和原始PDF
app.use(express.static('public'));
// 存放原始PDF的目录
const pdfDir = path.join(__dirname, 'pdfs');
// 存放生成图片的目录
const outputDir = path.join(__dirname, 'public', 'images');
// 确保目录存在
if (!fs.existsSync(pdfDir)) fs.mkdirSync(pdfDir);
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir);
// 转换单个PDF页面的路由
app.get('/convert', async (req, res) => {
  const { pdfName, page, options = { width: 800, height: 1000 } } = req.query;
  if (!pdfName || !page) {
    return res.status(400).send('PDF name and page number are required.');
  }
  const pdfPath = path.join(pdfDir, pdfName);
  if (!fs.existsSync(pdfPath)) {
    return res.status(404).send('PDF not found.');
  }
  const converter = pdf2pic(pdfPath, options);
  try {
    const result = await converter(page, 'image');
    // result.path 是生成图片的绝对路径
    // 我们需要返回相对于 public 目录的 URL
    const imageUrl = path.join('images', path.basename(result.path));
    res.json({ imageUrl: imageUrl });
  } catch (error) {
    console.error('Conversion error:', error);
    res.status(500).send('Error converting PDF page.');
  }
});
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

创建前端页面 (e.g., public/index.html)

前端通过 AJAX 请求来获取每一页的图片 URL 并显示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">Server-Side PDF Viewer</title>
    <style>
        body { font-family: sans-serif; text-align: center; }
        #pdf-viewer { display: flex; flex-direction: column; align-items: center; }
        #pdf-viewer img { max-width: 100%; border: 1px solid #ccc; margin-bottom: 10px; }
    </style>
</head>
<body>
    <h1>Server-Side PDF Viewer</h1>
    <div id="pdf-viewer"></div>
    <script>
        // 假设我们想查看 'sample.pdf' 的前 5 页
        const pdfName = 'sample.pdf';
        const totalPages = 5;
        const viewer = document.getElementById('pdf-viewer');
        async function loadPDFPages() {
            for (let i = 1; i <= totalPages; i++) {
                try {
                    const response = await fetch(`/convert?pdfName=${pdfName}&page=${i}`);
                    const data = await response.json();
                    if (data.imageUrl) {
                        const img = document.createElement('img');
                        img.src = data.imageUrl;
                        viewer.appendChild(img);
                    }
                } catch (error) {
                    console.error(`Failed to load page ${i}:`, error);
                    viewer.innerHTML += `<p>Failed to load page ${i}.</p>`;
                }
            }
        }
        loadPDFPages();
    </script>
</body>
</html>

重要注意事项

  1. 文件路径

    • PDF 文件在服务器上,确保 Web 服务器有正确的读取权限。
    • PDF 文件在 CDN 或其他域,需要确保该域允许跨域访问(CORS),否则 PDF.js 会因为安全策略而无法加载。
  2. 大文件性能

    • 对于几百页或非常大的 PDF 文件,一次性全部渲染会导致页面卡顿和内存溢出。
    • 解决方案:实现分页加载懒加载,只渲染当前可视区域内的页面,当用户滚动时再加载后续页面。
  3. 用户体验

    • 在 PDF 加载完成前,最好显示一个加载动画(如一个旋转的图标),告知用户“正在加载,请稍候...”。
    • 考虑添加一个简单的工具栏,包含上一页、下一页、缩放等功能。
  4. 安全性

    PDF 包含敏感信息,请仔细评估使用第三方服务的风险,客户端渲染(PDF.js)通常是更安全的选择,因为文件不会离开用户的浏览器(除非你上传到自己的服务器)。

  • 新手或快速实现:直接使用 PDF.js CDN 方案,代码简单,功能强大,是绝大多数场景下的首选。
  • 追求极致首屏速度:如果你的应用对加载速度有苛刻要求,且服务器资源充足,可以考虑服务端渲染方案。
  • 不想自己折腾:可以使用 Google Docs Viewer,但需接受其隐私和定制性限制。

希望这份详细的指南能帮助你成功实现网站上的在线 PDF 浏览功能!