实现 Nuxt3 预览PDF文件
- 安装必要的库,这里使用PDF.js库
npm install pdfjs-dist --save
- 为了解决跨域问题,在server/api 下 创建一个请求api, downloadFileByProxy.ts
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), }) })
- 支持现代浏览器,新建pdfPreviewForMordern.vue组件
<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>
- 对于旧版浏览器,新建pdfPreviewForOld.vue,唯一不同的地方是需要替换pdfjsLib导入
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
- 新建pdfPreview.vue,导入两个组件
<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>
- 上面用到的判断浏览器的方法
/** * 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; }
- 导入后预览pdf文件
<pdf-preview :path="picture.originalUrl" > </pdf-preview>
待优化的问题:
- 为了兼容需要重复写两个组件,试过动态导入的方式行不通
- 控制台会报[offsetParent is not set -- cannot scroll]的错误,但是不影响预览