优雅地在网页中嵌入pdf且适配移动端浏览器

一、不需要考虑移动端显示效果的方法

1、使用对象数据嵌入pdf

<object data="https://zzhanzhang.top/YOU_PDF.pdf"  height="800px" width="100%"></object>

优缺点:语法简洁,无需额外依赖,支持翻页缩放基础交互;但移动端适配差可控性低,安卓浏览器直接下载pdf文件了。

2、典中典<iframe>

<iframe>通过创建独立窗口加载 PDF,本质与<object>类似,都是依赖浏览器原生渲染。

<iframe src="https://zzhanzhang.top/YOU_PDF.pdf"  height="800px" width="100%"></iframe>

优缺点:与主页面隔离,避免 PDF 渲染影响页面其他元素,与<object>一样简洁,但移动端问题与<object>完全一致:渲染错乱、会触发直接下载;

3、<embed>标签嵌入

<embed>是早期用于嵌入多媒体资源的标签,功能与<object>高度重叠,现在已较少单独使用。

<embed src="https://zzhanzhang.top/YOU_PDF.pdf" type="application/pdf" width="100%"  height="800px" />

优缺点:移动端适配问题更严重:很多端浏览器会直接忽略<embed>标签,显示空白;兼容性最差。

二、借助第三方工具嵌入pdf并且自适应移动端浏览器

这里借助smallpdf这个在线工具生成一个容器可以放在网页中的任何部分,并且会自适应移动端浏览器的显示效果(本质是用 JS 封装了渲染逻辑)

免费将 PDF 嵌入你的网站

<div 
  style="width: 100%; height: 580px; max-width: 1200px; margin: 0 auto;"  <!-- 加max-width避免桌面端过宽 -->
  class="smallpdf-widget" 
  data-pdf-url="https://zzhanzhang.top/YOU_PDF.pdf"
  data-download="false"  <!-- 可选:是否显示下载按钮(true/false) -->
  data-print="false"     <!-- 可选:是否显示打印按钮(true/false) -->
></div>
<!-- 引入SmallPDF的渲染脚本 -->
<script src="https://smallpdf.com/api/embed-widget.js"></script>

如图:

SmallPDF嵌入示例

优点:

  1. 移动端会自动隐藏多余工具栏,仅保留翻页和缩放按钮;桌面端显示完整控件
  2. 无需写 JS 逻辑,配置属性即可自定义功能;
  3. 自动响应屏幕宽度,控件布局随设备调整;
  4. 支持基础自定义,可隐藏下载 / 打印按钮。

缺点:

  1. 若 SmallPDF 的 CDN 故障,PDF 将无法加载;
  2. 无法自定义主题色、无法监听页面切换事件;
  3. 查看器会带 SmallPDF 水印。

三、高级玩法,使用PDF.js达到完美的显示效果

pdf.js简介:它是 Mozilla 开源的 PDF 渲染库,完全基于 JS 实现(不依赖浏览器原生能力),可精准控制移动端渲染逻辑。

这里需要引入PDF.js库和PDF工作者(worker):

  1. pdf.min.js:主库,负责 PDF 解析和渲染逻辑;
  2. pdf.worker.min.js:工作线程,负责异步解析 PDF(避免阻塞主线程)。

以下代码已优化移动端体验:自动适配屏幕宽度、支持垂直滚动、优化文本清晰度;

<div id="pdfContainer" style="width: 100%; height: 100vh; overflow-y: auto; overflow-x: hidden;"></div>

<!-- PDF.js库 -->
<script src="https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.min.js"></script>
<script>
    const pdfjsLib = window['pdfjs-dist/build/pdf'];
    pdfjsLib.GlobalWorkerOptions.workerSrc = 
        'https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.worker.min.js';
    const url = 'https://zzhanzhang.top/YOU_PDF.pdf';
    const container = document.getElementById('pdfContainer');
    const devicePixelRatio = window.devicePixelRatio || 1;
    const loadingTask = pdfjsLib.getDocument(url);
    loadingTask.promise.then(pdf => {
        const totalPages = pdf.numPages;
        for(let pageNum = 1; pageNum <= totalPages; pageNum++) {
            pdf.getPage(pageNum).then(page => {
                const canvas = document.createElement('canvas');
                canvas.id = `pdfCanvas-page-${pageNum}`;
                
                // 获取设备像素比
                const viewportWidth = window.innerWidth;
                const viewport = page.getViewport({ scale: 1.0 });
                const scale = (viewportWidth / viewport.width) * devicePixelRatio;
                const adaptedViewport = page.getViewport({ scale: scale });
                canvas.width = adaptedViewport.width;
                canvas.height = adaptedViewport.height;
                canvas.style.width = '100%';
                canvas.style.height = 'auto';
                canvas.style.display = 'block';
                canvas.style.margin = '0 auto';
                container.appendChild(canvas);
                // 渲染页面
                const renderContext = {
                    canvasContext: canvas.getContext('2d'),
                    viewport: adaptedViewport,
                    imageSmoothingEnabled: true,
                    textLayer: true
                };
                page.render(renderContext);
            });
        }
    });
</script>

优点:

  1. 100% 可控,可自定义工具栏(如添加页码导航、缩放按钮)、主题色、批注功能;
  2. 移动端和桌面端渲染效果完全一致,无浏览器差异;
  3. 可下载库到本地部署,不依赖第三方服务;

缺点:

  1. 需手动处理渲染逻辑、错误捕获、交互优化;
  2. 若 PDF 文件与网页不同域名,需要解决跨域问题;
  3. 针对大文件需自己实现分片加载或进度条(默认是一次性加载整个 PDF)。

四、版本pdfjs-dist@3.4.120遇到的一个渲染错位问题

问题 1:并发渲染导致资源错误

错误信息:Uncaught (in promise) Error: Requesting object that isn't resolved yet pattern_p0_91.

原因分析:当使用for循环并发调用pdf.getPage()时,多个页面同时渲染会导致 PDF 内部资源(如 Type3 字体、图案 pattern)竞争。某些资源还没完全加载就被引用,从而抛出错误。

解决方案:使用 async/await 串行渲染

loadingTask.promise.then(async pdf => {
    const totalPages = pdf.numPages;
    for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
        try {
            const page = await pdf.getPage(pageNum);
            
            // ... 创建 canvas 和配置 viewport ...
            
            const renderContext = {
                canvasContext: canvas.getContext('2d', { alpha: false }),
                viewport: adaptedViewport,
                imageSmoothingEnabled: true
            };
            await page.render(renderContext).promise;  // 等待渲染完成
        } catch (error) {
            console.warn(`第${pageNum}页渲染失败:`, error);
        }
    }
});

关键点:

  • 使用async/await确保上一页渲染完成后再渲染下一页;
  • 添加try-catch捕获单页错误,避免中断整个流程;
  • 移除无效参数textLayer: true(PDF.js 会忽略它)。

问题 2:requestAnimationFrame 性能警告

警告信息:[Violation] 'requestAnimationFrame' handler took <N>ms

原因:PDF 渲染计算量大,尤其是包含复杂图形或高分辨率图片时,单次渲染可能超过 50ms,触发浏览器性能警告。

优化方案:

  1. 降低渲染质量:将 scale 乘以 0.8,减少 20% 计算量;
  2. 添加页间延迟:每页渲染后等待 100ms,让浏览器处理其他任务;
  3. 移除 alpha 优化:某些 PDF 与getContext('2d', { alpha: false })不兼容,改回标准getContext('2d')

问题 3:PDF.js 版本升级

如果上述方案仍无法解决,考虑升级到最新版 PDF.js(如 5.4.394),新版本修复了大量渲染 bug 和性能问题。

升级步骤:

<!-- HTML 中使用 ES Module 导入 -->
<script type="module">
    import * as pdfjsLib from './js/pdfjs-dist@5.4.394/files/build/pdf.min.mjs';
    window['pdfjs-dist/build/pdf'] = pdfjsLib;
</script>

<!-- JS 中配置 worker -->
<script>
    const pdfjsLib = window['pdfjs-dist/build/pdf'];
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'js/pdfjs-dist@5.4.394/files/build/pdf.worker.min.mjs';
    
    // 添加加载参数增强稳定性
    const loadingTask = pdfjsLib.getDocument({
        url: url,
        useSystemFonts: true,    // 使用系统字体,减少下载
        disableRange: false,
        disableStream: false,
        disableAutoFetch: false,
        isEvalSupported: true
    });
</script>

注意:ES Module 路径必须以./..//开头,否则会报错Failed to resolve module specifier

五、我们还能做的其它事情

  • 压缩文件大小(用 SmallPDF、Adobe Acrobat 压缩),避免移动端加载过慢;
  • 大 PDF(超过 20 页)可实现「懒加载」(只渲染可视区域的页面),减少初始加载时间;