🔥 用 Vue2 + PDF.js 手撸一个「PDF 连续预览器」,自适应屏幕、支持缩放,直接拿来用!

1,567 阅读4分钟

🌈 前言

最近做项目,产品丢过来一个需求: “我们要一个像掘金文章那样的 PDF 连续预览功能,要自适应屏幕,还要支持放大缩小!”

我心想,这需求简单啊,直接搜一波现成的库,结果一看,要么太重,要么不支持连续预览,要么缩放体验差,要么样式丑。

于是,干脆自己撸一个!用 Vue2 + PDF.js,花了一个下午,效果还不错,直接分享给大家,代码已经整理好了,复制粘贴就能用!

🚀 效果展示

  • ✅ 支持 PDF 文件上传预览
  • 连续滚动 显示所有页面,像浏览网页一样
  • 自适应屏幕宽度,窗口大小变化自动调整
  • 支持缩放(放大、缩小、重置)
  • 加载进度提示,体验更友好
  • 样式美观,代码简洁易读

📦 技术栈

  • Vue2:熟悉的响应式框架
  • PDF.js:Mozilla 出品的 PDF 渲染库,功能强大
  • FontAwesome:图标库,让按钮更美观

🛠️ 核心实现思路

1. 引入依赖

<!-- Vue2 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- PDF.js -->
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js"></script>
<!-- FontAwesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">

2. 文件上传

使用 <input type="file" accept=".pdf"> 实现文件选择,通过 FileReader 读取文件内容:

  const file = e.target.files[0];
  if (!file || file.type !== 'application/pdf') {
    this.errorMessage = '请上传PDF格式的文件';
    return;
  }

  const fileReader = new FileReader();
  fileReader.onload = (event) => {
    const typedArray = new Uint8Array(event.target.result);
    this.loadPdf(typedArray);
  };
  fileReader.readAsArrayBuffer(file);
}

3. 渲染 PDF 页面

使用 PDF.js 的 getDocument 方法加载 PDF,然后逐页渲染:

renderPage(pageNum, containerWidth) {
  this.pdfDoc.getPage(pageNum).then(page => {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    // 计算缩放比例,自适应容器宽度
    const viewport = page.getViewport({ scale: 1 });
    const baseScale = Math.min(containerWidth / viewport.width, 2);
    const scale = baseScale * this.zoom;
    const scaledViewport = page.getViewport({ scale });

    canvas.height = scaledViewport.height;
    canvas.width = scaledViewport.width;

    const renderContext = {
      canvasContext: context,
      viewport: scaledViewport
    };

    page.render(renderContext).promise.then(() => {
      this.loadedPages++;
    });
  });
}

4. 缩放功能

通过调整 zoom 值,重新渲染所有页面:

zoomIn() {
  if (this.zoom < 3) {
    this.zoom += 0.1;
    this.reRenderAllPages();
  }
},
zoomOut() {
  if (this.zoom > 0.3) {
    this.zoom -= 0.1;
    this.reRenderAllPages();
  }
},
resetZoom() {
  this.zoom = 1.0;
  this.reRenderAllPages();
}

5. 自适应屏幕

监听窗口大小变化,自动重新渲染:

mounted() {
  window.addEventListener('resize', () => {
    if (this.pdfDoc && this.showViewer) {
      this.reRenderAllPages();
    }
  });
}

📝 使用方式

  1. 复制文章最后提供的完整 HTML 代码
  2. 保存为 index.html
  3. 直接双击打开即可使用

🏁 结语

一个简单的 PDF 连续预览器就完成了,代码不到 300 行,功能却很实用。如果你有更好的想法或者遇到问题,欢迎在评论区留言交流!

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF 直接预览器</title>
    <!-- 引入Vue 2 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <!-- 引入PDF.js -->
    <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js"></script>
    <!-- 引入Font Awesome图标 -->
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1000px;
            margin: 0 auto;
        }
        .pdf-viewer {
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 20px;
            margin-top: 20px;
        }
        .url-input-area {
            margin-bottom: 20px;
            display: flex;
            gap: 10px;
        }
        #pdf-url {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        .toolbar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        }
        .btn {
            background-color: #42b983;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            margin: 0 3px;
        }
        .btn:hover {
            background-color: #359e75;
        }
        .btn i {
            margin-right: 5px;
        }
        .zoom-controls {
            display: flex;
            align-items: center;
        }
        .pdf-container {
            width: 100%;
            overflow-x: hidden;
            overflow-y: auto;
            padding: 10px 0;
            max-height: calc(100vh - 200px);
        }
        .pdf-page {
            box-shadow: 0 0 8px rgba(0,0,0,0.1);
            margin: 0 auto 15px;
            transition: all 0.3s ease;
            background-color: white;
        }
        .loading {
            text-align: center;
            padding: 50px 0;
            color: #666;
        }
        .page-count {
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container" id="app">
        <h1>PDF 直接预览器</h1>
        
        <!-- PDF链接输入区域 -->
        <div class="url-input-area">
            <input type="text" id="pdf-url" v-model="pdfUrl" 
                   placeholder="请输入PDF文件的URL地址" 
                   @keyup.enter="loadPdfFromUrl">
            <button class="btn" @click="loadPdfFromUrl">
                <i class="fa fa-eye"></i> 预览PDF
            </button>
        </div>
        
        <!-- PDF 预览区域 -->
        <div class="pdf-viewer" v-if="showViewer || loading">
            <!-- 工具栏 -->
            <div class="toolbar" v-if="showToolbar">
                <div>
                    <span class="page-count">共 {{ totalPages }} 页</span>
                </div>
                
                <div class="zoom-controls">
                    <button class="btn" @click="zoomOut">
                        <i class="fa fa-search-minus"></i> 缩小
                    </button>
                    <button class="btn" @click="resetZoom">
                        <i class="fa fa-home"></i> 重置
                    </button>
                    <button class="btn" @click="zoomIn">
                        <i class="fa fa-search-plus"></i> 放大
                    </button>
                    <span class="page-info">{{ Math.round(zoom * 100) }}%</span>
                </div>
            </div>
            
            <!-- PDF 内容区域 - 连续显示所有页面 -->
            <div class="pdf-container">
                <div v-if="loading" class="loading">
                    <i class="fa fa-spinner fa-spin text-2xl"></i>
                    <p>加载中... 共 {{ totalPages }} 页,正在加载第 {{ loadedPages }} 页</p>
                </div>
                
                <!-- 所有PDF页面将被渲染到这里 -->
                <div ref="pdfContent" class="pdf-pages-container"></div>
            </div>
        </div>
        
        <div v-if="!showViewer && !loading && !errorMessage" class="loading">
            <i class="fa fa-file-pdf-o text-4xl text-gray-300"></i>
            <p>请输入PDF文件的URL地址并点击预览按钮</p>
        </div>
        
        <div v-if="errorMessage" style="color: red; margin-top: 20px; padding: 10px; background-color: #ffebee; border-radius: 4px;">
            <i class="fa fa-exclamation-circle"></i> {{ errorMessage }}
        </div>
    </div>

    <script>
        // 配置PDF.js工作器
        pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js';
        
        new Vue({
            el: '#app',
            data() {
                return {
                    pdfUrl: 'https://501351981.github.io/vue-office/examples/dist/static/test-files/test.pdf', // 默认示例PDF
                    pdfDoc: null,          // PDF文档实例
                    totalPages: 0,         // 总页数
                    loadedPages: 0,        // 已加载页数
                    zoom: 1.0,             // 缩放比例
                    loading: false,        // 加载状态
                    showViewer: false,     // 是否显示预览器
                    showToolbar: false,    // 是否显示工具栏
                    errorMessage: ''       // 错误信息
                }
            },
            methods: {
                // 从URL加载PDF
                loadPdfFromUrl() {
                    if (!this.pdfUrl.trim()) {
                        this.errorMessage = '请输入有效的PDF URL地址';
                        return;
                    }
                    
                    this.errorMessage = '';
                    this.loading = true;
                    this.showViewer = true;
                    this.loadedPages = 0;
                    
                    this.$nextTick(() => {
                        // 清空之前的内容
                        this.$refs.pdfContent.innerHTML = '';
                        
                        // 检查URL是否以.pdf结尾
                        if (!this.pdfUrl.toLowerCase().endsWith('.pdf')) {
                            this.errorMessage = '请输入以.pdf结尾的有效URL';
                            this.loading = false;
                            return;
                        }
                        
                        // 加载PDF文档
                        pdfjsLib.getDocument({
                            url: this.pdfUrl,
                            cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/cmaps/',
                            cMapPacked: true
                        }).promise
                        .then(pdfDoc => {
                            this.pdfDoc = pdfDoc;
                            this.totalPages = pdfDoc.numPages;
                            this.showToolbar = true;
                            
                            // 渲染所有页面
                            this.renderAllPages();
                        })
                        .catch(error => {
                            console.error('PDF加载错误:', error);
                            this.errorMessage = '加载PDF失败: ' + (error.message || '可能是跨域问题或URL无效');
                            this.loading = false;
                            this.showViewer = false;
                        });
                    });
                },
                
                // 渲染所有页面
                renderAllPages() {
                    // 计算适应容器的基础缩放比例
                    const containerWidth = this.$refs.pdfContent.clientWidth - 40; // 减去边距
                    
                    // 逐个渲染页面
                    for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
                        this.renderPage(pageNum, containerWidth);
                    }
                },
                
                // 渲染单页
                renderPage(pageNum, containerWidth) {
                    this.pdfDoc.getPage(pageNum)
                        .then(page => {
                            // 创建页面容器和canvas
                            const pageContainer = document.createElement('div');
                            pageContainer.className = 'pdf-page-container';
                            
                            const canvas = document.createElement('canvas');
                            canvas.className = 'pdf-page';
                            canvas.dataset.pageNum = pageNum;
                            pageContainer.appendChild(canvas);
                            
                            this.$refs.pdfContent.appendChild(pageContainer);
                            
                            const context = canvas.getContext('2d');
                            if (!context) {
                                throw new Error('无法初始化画布上下文');
                            }
                            
                            // 计算缩放比例
                            const viewport = page.getViewport({ scale: 1 });
                            const baseScale = Math.min(containerWidth / viewport.width, 2); // 最大2倍缩放
                            const scale = baseScale * this.zoom;
                            const scaledViewport = page.getViewport({ scale });
                            
                            // 设置canvas尺寸
                            canvas.height = scaledViewport.height;
                            canvas.width = scaledViewport.width;
                            
                            // 渲染页面
                            const renderContext = {
                                canvasContext: context,
                                viewport: scaledViewport
                            };
                            
                            return page.render(renderContext).promise;
                        })
                        .then(() => {
                            this.loadedPages++;
                            
                            // 所有页面加载完成后隐藏加载状态
                            if (this.loadedPages === this.totalPages) {
                                this.loading = false;
                            }
                        })
                        .catch(error => {
                            console.error(`页面${pageNum}渲染错误:`, error);
                            this.errorMessage = `渲染第 ${pageNum} 页失败: ${error.message}`;
                            this.loadedPages++;
                            
                            if (this.loadedPages === this.totalPages) {
                                this.loading = false;
                            }
                        });
                },
                
                // 重新渲染所有页面(用于缩放)
                reRenderAllPages() {
                    this.loading = true;
                    this.loadedPages = 0;
                    this.$refs.pdfContent.innerHTML = '';
                    this.renderAllPages();
                },
                
                // 放大
                zoomIn() {
                    if (this.zoom < 3) { // 限制最大缩放
                        this.zoom += 0.1;
                        this.reRenderAllPages();
                    }
                },
                
                // 缩小
                zoomOut() {
                    if (this.zoom > 0.3) { // 限制最小缩放
                        this.zoom -= 0.1;
                        this.reRenderAllPages();
                    }
                },
                
                // 重置缩放
                resetZoom() {
                    this.zoom = 1.0;
                    this.reRenderAllPages();
                }
            },
            mounted() {
                // 监听窗口大小变化,重新渲染以适应新尺寸
                window.addEventListener('resize', () => {
                    if (this.pdfDoc && this.showViewer) {
                        this.reRenderAllPages();
                    }
                });
                
                // 自动加载默认PDF
                this.loadPdfFromUrl();
            }
        });
    </script>
</body>
</html>