实现 Nuxt3 预览PDF文件

  1. 安装必要的库,这里使用PDF.js库

    javascript 复制代码
    npm install pdfjs-dist --save
  2. 为了解决跨域问题,在server/api 下 创建一个请求api, downloadFileByProxy.ts

    TypeScript 复制代码
     
    import { defineEventHandler } from 'h3';
     
    export default defineEventHandler(async event => {
      const { filePath } =  getQuery(event);
      let matches = filePath?.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
      let domain = matches && matches[1]; 
      return proxyRequest(event,`https://${domain}/`, {
        fetch: ()=>fetch(filePath),
      })
    })
  3. 支持现代浏览器,新建pdfPreviewForMordern.vue组件

    javascript 复制代码
    <script setup lang="ts">
      import { isPdf } from '~/utils/is';
      import 'pdfjs-dist/web/pdf_viewer.css';
      import * as pdfjsLib from 'pdfjs-dist';
      // import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js'; // 旧版浏览器需要换成这个导入
      import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer';
      import 'pdfjs-dist/build/pdf.worker.entry';
      import * as pdfjsSandbox from 'pdfjs-dist/build/pdf.sandbox.js';
      // import {  PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
      import { debounce } from 'lodash-es';
    
      const props = defineProps({
        path: {
          type: String,
          default: '',
        },
        preview: {
          type: Boolean,
          default: true,
        },
      });
    
      const SANDBOX_BUNDLE_SRC = pdfjsSandbox;
    
      pdfjsLib.GlobalWorkerOptions.workerSrc = window.pdfjsWorker;
    
      const CMAP_URL = '/pdfjs-dist/cmaps/';
      const CMAP_PACKED = true;
      const STANDARD_FONT_DATA_URL = '/pdfjs-dist/standard_fonts/';
    
      window.pdfjsLib = pdfjsLib;
      window.pdfjsViewer = pdfjsViewer;
    
      const pdfEventBus = new pdfjsViewer.EventBus();
    
      const pdfScriptingManager = new pdfjsViewer.PDFScriptingManager({
        eventBus: pdfEventBus,
        sandboxBundleSrc: SANDBOX_BUNDLE_SRC,
      });
      const pdfLinkService = new pdfjsViewer.PDFLinkService({
        eventBus: pdfEventBus,
      });
    
      // (Optionally) enable find controller.
      const pdfFindController = new pdfjsViewer.PDFFindController({
        eventBus: pdfEventBus,
        linkService: pdfLinkService,
      });
    
      let pdfViewer: pdfjsViewer.PDFViewer | null = null;
      let pdfDocument: PDFDocumentProxy | null = null;
    
      const loading = ref<boolean>(true);
      const visible = ref<boolean>(false);
      const setVisible = (value: boolean): void => {
        if (!props.preview) {
          return;
        }
        visible.value = value;
      };
    
      let oldPath = '';
      const random = ref(Math.floor(Math.random() * 10001));
      const bufferCache = ref(null); // 使用缓存避免多次请求,可以试具体情况优化与否
    
      watch(
        () => props.path,
        async (val) => {
          if (!val || !isPdf(val)) {
            return;
          }
          setTimeout(() => {
            debounceRenderHandle();
          }, 500);
        },
        {
          immediate: true,
        },
      );
    
      const debounceRenderHandle = debounce(() => {
        initPage(props.path, `pdfjs-container-${random.value}`, 'page-height');
      }, 500);
    
      const preview = async () => {
        setVisible(true);
        if (oldPath === props.path) {
          return;
        }
        if (!props.path) {
          return;
        }
        oldPath = props.path;
        setTimeout(() => {
          initPage(props.path, `pdfjs-modal-container-${random.value}`);
        }, 500);
      };
    
      async function getFile(pdfPath: string) {
        // 为了防止跨域需要再次请求
        const { _data } = await $fetch.raw(`/api/downloadFileByProxy`,{
          method: 'get',
          params: {
            // filePath: val.split('/').pop(),
            filePath: pdfPath,
          },
        })
        let blob = _data;
        let buffer = await blob?.arrayBuffer();
        return buffer;
      }
    
      async function initPage(pdfPath: string, domId: string, zoom?: string | number) {
        if (!pdfPath) return;
        try {
          // download pdf from api to prevent CORS
          bufferCache.value = bufferCache.value || (await getFile(pdfPath));
          let container = document.getElementById(domId);
          pdfDocument = await pdfjsLib.getDocument({
            // url: pdfUrl as unknown as URL,
            data: useCloneDeep(bufferCache.value),
            cMapUrl: CMAP_URL,
            cMapPacked: CMAP_PACKED,
            standardFontDataUrl: STANDARD_FONT_DATA_URL,
          }).promise;
          pdfViewer = new pdfjsViewer.PDFViewer({
            container: container as unknown as HTMLDivElement,
            eventBus: pdfEventBus,
            annotationMode: 0,
            annotationEditorMode: 0,
            scriptingManager: pdfScriptingManager,
            linkService: pdfLinkService,
          });
          pdfScriptingManager.setDocument(pdfDocument);
          pdfScriptingManager.setViewer(pdfViewer);
          pdfLinkService.setDocument(pdfDocument);
          pdfLinkService.setViewer(pdfViewer);
    
          pdfViewer.setDocument(pdfDocument);
          pdfEventBus.on('pagesinit', () => {
            if (pdfViewer) {
              loading.value = false;
              // TODO: this code will report error, but not affect results: [offsetParent is not set -- cannot scroll]
              zoom ? pdfLinkService.setHash(`zoom=${zoom}`) : pdfLinkService.setHash(`zoom=100`);
            }
          });
        } catch {
          // Init pdf Page error
        }
      }
    </script>
    
    <template>
      <div class="w-full h-full">
        <div @click="preview" class="absolute inset-0 cursor-pointer z-[100]" v-if="preview"></div>
        <img :src="useRuntimeConfig().public.loadingPicture" class="absolute object-cover w-full h-full" v-if="loading" />
        <div :id="`pdfjs-container-${random}`" class="page-container page-thumbnail-container no-scrollbar">
          <div :id="`pdfViewer-${random}`" class="pdfViewer pdf-thumbnail-viewer"></div>
        </div>
        <a-modal v-model:visible="visible" :footer="null" width="100%" wrap-class-name="ant-full-modal">
          <template #closeIcon>
            <span class="font-semibold bg-white cursor-pointer text-litepie-primary-600 text-[16px]">
              <ms-icon path="close-icon" type="svg" :w="48" :h="48" />
            </span>
          </template>
          <div :id="`pdfjs-modal-container-${random}`" class="page-container page-modal-container">
            <div :id="`pdfModalViewer-${random}`" class="pdfViewer"></div>
          </div>
        </a-modal>
      </div>
    </template>
    
    <style lang="less">
      .ant-full-modal {
        .ant-modal {
          max-width: 100%;
          top: 0;
          padding-bottom: 0;
          margin: 0;
        }
        .ant-modal-content {
          display: flex;
          flex-direction: column;
          height: calc(100vh);
        }
        .ant-modal-body {
          flex: 1;
          padding: 0;
        }
        .ant-modal-close {
          top: 30px;
          right: 30px;
        }
      }
    </style>
    <style lang="less" scoped>
      .page-container {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
      }
      /* another way to scale pdf, still will report error 
      :deep(.pdf-thumbnail-viewer) {
        --scale-factor: 0.5 !important;
        canvas {
          width: 100% !important;
          height: 100% !important;
        }
      }
      */
    </style>
  4. 对于旧版浏览器,新建pdfPreviewForOld.vue,唯一不同的地方是需要替换pdfjsLib导入

    javascript 复制代码
    import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
  5. 新建pdfPreview.vue,导入两个组件

    javascript 复制代码
    <script setup lang="ts">
      const supportedOlderBrowser = computed(() => {
        return getChromeVersion() <= 88 || isSafari();
      });
    </script>
    
    <template>
      <div>
        <!-- don't change v-if order, otherwise will report error -->
        <pdfPreviewForOld v-bind="$attrs" v-if="supportedOlderBrowser"></pdfPreviewForOld>
        <pdfPreviewForMordern v-else v-bind="$attrs"></pdfPreviewForMordern>
      </div>
    </template>
  6. 上面用到的判断浏览器的方法

    TypeScript 复制代码
    /**
     * Determine whether it is safari browser
     * @return {Boolean} true,false
     */
    export const isSafari = () => getUserAgent().indexOf('safari') > -1 && !isChrome(); 
    
    /**
     * Determine whether it is chrome browser
     * @return {Boolean} true,false
     */
    export const isChrome = () => /chrome/.test(getUserAgent()) && !/chromium/.test(getUserAgent());
    
    export const getChromeVersion = () =>{  
      let raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
      return raw ? parseInt(raw[2], 10) : false;
    }
  7. 导入后预览pdf文件

    html 复制代码
    <pdf-preview
    :path="picture.originalUrl"
    >
    </pdf-preview>

待优化的问题:

  1. 为了兼容需要重复写两个组件,试过动态导入的方式行不通
  2. 控制台会报[offsetParent is not set -- cannot scroll]的错误,但是不影响预览
相关推荐
拾荒的小海螺12 小时前
JAVA:探索 PDF 文字提取的技术指南
java·开发语言·pdf
村东头老张12 小时前
Java 实现PDF添加水印
java·开发语言·pdf
好美啊啊啊啊!18 小时前
Thymeleaf模板引擎生成的html字符串转换成pdf
pdf·html
zhentiya1 天前
曼昆《经济学原理》第八版课后答案及英文版PDF
大数据·pdf
三天不学习1 天前
如何解决pdf.js跨域从url动态加载pdf文档
javascript·pdf
吾店云建站1 天前
9个最佳WordPress PDF插件(查看器、嵌入和下载)
程序人生·pdf·创业创新·流量运营·程序员创富·教育电商
007php0071 天前
基于企业微信客户端设计一个文件下载与预览系统
开发语言·python·docker·golang·pdf·php·企业微信
慧都小妮子2 天前
Spire.PDF for .NET【页面设置】演示:更改 PDF 页面大小
前端·pdf·.net
徒步僧2 天前
java实现生成PDF文件
java·开发语言·pdf