iframe无法显示大pdf原因及解决办法

💡 解决iframe无法预览大pdf(>10M)问题,解决思路通用,我使用React举例

背景

1、对于 PDF 在线预览,网上有很多种方案,比如 react-pdfpdfh5PDF.js 等等。但它们都有不同程度的缺点,比如功能不全需要自定义覆盖、对于某些格式的 PDF 无法展示全(文字丢失、签章丢失等)。

2、实际上浏览器自带的预览功能已经可以满足绝大部分的需求,于是可以通过 <iframe> 标签预览 PDF,效果非常好。但是实际使用中,我的同事发现对于较大的 PDF<iframe> 无法显示并展示空白。


 

原因

当时这个问题其实没人说是什么原因,我感觉既然浏览器能完美预览 PDF,怎么会达到某个阈值就完全空白了,于是我一路追踪检索到 stackoverflow,在一众讨论中获得了答案。

㊙️ 之前在使用 <iframe> 标签预览 PDF 的时候传递了一个参数 src,我们在这里传入了 PDF 的 base64 串,该 base64 串会拼接到路由地址后面(<iframe> 相当于嵌入了一个网页,拼接到该网页的路由地址,主页面是看不到的)。

㊙️ 然而浏览器对地址有一个最大长度限制(不同浏览器限制不一),当地址长度过长则会被截断,这也是为什么过大的 PDF 展示空白,因为其 base64 串被截断了,<iframe> 拿到的是一个无效的 src。 


 

解决方案

✅ 既然 base64 过长,那我们就考虑不传递 base64 串,如果是远程的 PDF 文件(ossobs)或者本地的 PDF 文件可以直接拼接其访问地址;

✅ 如果 PDF 文件由后端传递过来,就是一个 base64 串呢(我所在的项目就是如此),正确的做法是将其转化为一个 blob 对象,然后创建出该对象的访问 url,这个 url 是一个固定长随机串,这样我们就能正常访问了。 


 

完整组件代码(React)

import React, { useEffect, useState } from 'react';
import { Button, Result } from 'antd';
import { DownloadOutlined } from 'dw-mx-icons';
import styles from './style/index.less';
import { request } from 'dw-mx-request';

interface PropTypes {
    // 传入pdf的base64串
    src: string,
    // 是否开启工具栏
    openToolBar?: boolean,
    // 是否全屏展示
    fullScreen?: boolean,
    // 是否使用自定义toolbar覆盖
    useCustomToolBar?: boolean,
    // 文件名
    fileName?: string,
    // 下载地址
    downloadUrl?: string,
    // 文件id
    fileId?: string,
}

/**
 * @description pdf预览组件(使用iframe并解决大文件空白问题)
 * @author LFH
 * @date 2024-04-22
 */
function PDFViewer(props: PropTypes) {
    const {
        src,
        openToolBar = true,
        fullScreen = false,
        useCustomToolBar = false,
        fileName = '附件材料',
        downloadUrl,
        fileId
    } = props;

    const config = {
        openToolBar: openToolBar && !useCustomToolBar ? '' : '#view=FitH,top&type=accesspdf#scrollbars=0&toolbar=0&statusbar=0'
    };
    const [zoomLevel, setZoomLevel] = useState('page-width'); // 默认缩放级别

    // 根据base64创建Blob
    const dataURLtoBlob = (dataUrl) => {
        const arr = dataUrl.split(',');
        if (arr.size < 2) {
            arr[1] = arr[0];
            arr[0] = 'data:application/pdf;base64';
        }
        const suffix = arr[0].match(/\/(.*?);/)[1];
        const mime = arr[0].match(/:(.*?);/)[1];

        const bstr = atob(arr[1]);
        let n = bstr.length;
        const u8arr = new Uint8Array(n);

        while (n--) {
            u8arr[n] = bstr.charCodeAt(n);
        }
        return new Blob([u8arr], { type: mime });
    }

    // 工具栏按钮处理函数
    const handleZoomIn = () => {
        let newZoomLevel = parseInt(zoomLevel);
        if (isNaN(newZoomLevel)) {
            newZoomLevel = 100; // 如果不是数字,则默认从100%开始
        }
        newZoomLevel += 10;
        setZoomLevel(`${newZoomLevel}`);
    };

    const handleZoomOut = () => {
        let newZoomLevel = parseInt(zoomLevel);
        if (isNaN(newZoomLevel)) {
            newZoomLevel = 100; // 如果不是数字,则默认从100%开始
        }
        newZoomLevel -= 10;
        setZoomLevel(`${Math.max(20, newZoomLevel)}`); // 最小缩放级别为20%
    };

    const handleDownload = () => {
        if (downloadUrl) {
            request.download(downloadUrl, {
                bge066: fileId
            });
        } else {
            const a = document.createElement('a');
            a.href = objURL;
            a.download = fileName; // 设置文件名
            a.click();
        }
    };

    const [objURL, setObjURL] = useState(null);

    useEffect(() => {
        if (src?.length) {
            if (objURL) {
                window.URL.revokeObjectURL(objURL);
            }
            setObjURL(window.URL.createObjectURL(dataURLtoBlob(src)));
            return () => {
                // 组件销毁时执行 删除浏览器中blob对象
                window.URL.revokeObjectURL(objURL);
            }
        }
    }, [src]);


    return (
        <div style={{height:fullScreen?'calc(100vh - 0px)':'calc(100vh - 142px)', width: '100%'}}>
            {objURL ?
                <>
                    {openToolBar && useCustomToolBar && (
                        <div className={`${styles['custom-toolbar']}`}>
                            <Button type={'text'} icon={<DownloadOutlined />} onClick={handleDownload}>下载</Button>
                        </div>
                    )}
                    <iframe height={'100%'} width={'100%'} src={objURL + config.openToolBar} />
                </>
                : <Result
                    status='404'
                    title='暂无数据'
                    subTitle='该pdf暂时无法查看或已损坏'
                />
            }
        </div>
    );
}

export default PDFViewer;

 

说明

💫 其中我为了可以控制浏览器工具栏的显隐,加入了一些逻辑。

💫 这里有个小问题,在用户通过浏览器提供的按钮触发下载的时候,默认下载的当前文件的本地 blob 对象,并且文件名是那串定长随机串。考虑到有原文件为 word,只是预览转为 pdf 预览的情况(pdf 预览效果好),用户触发下载仍然希望下载原 word 文件,所以我们需要重写浏览器工具栏行为或者覆盖浏览器的工具栏。

💫 因为浏览器的工具栏我尝试了很多方法都无法通过 js 控制或重写行为,所以我考虑覆盖它,上面的下载函数中可以用自己项目里的 Ajax 或者 fetch 实现。


 

注意事项(必读)

⚠️ 注意,要在组件的生命周期末尾销毁该对象,不然频繁创建可能会造成 OOM。在 React 中,我在 useEffect 中的 return 回调中执行的销毁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值