常见的前端处理文件预览的方案-pdf,excel,doc [bysking]

247 阅读3分钟

说明

浏览器方案

利用<iframe>标签预览文件是一种常见的方法,它允许在一个HTML页面中嵌入另一个HTML页面或者指定资源的呈现。这种方式支持多种文件类型,但主要取决于浏览器的能力和配置。以下是一些常见文件类型的预览支持情况及限制:

支持的文件格式:

  1. Web页面 (*.html*.htm): 直接嵌入网页内容。
  2. 图片 (*.jpg*.jpeg*.png*.gif*.svg): 浏览器原生支持显示图片。
  3. PDF文档 (*.pdf): 大多数现代浏览器可以直接在<iframe>中显示PDF,无需插件。
  4. Office文档 (*.docx*.xlsx*.pptx): 需要浏览器支持或Office Online等在线服务来预览。
  5. 文本文件 (*.txt*.csv): 可以直接显示,但可能缺乏格式化。
  6. 视频与音频 (*.mp4*.webm*.mp3*.wav): 媒体文件可嵌入并播放,但通常推荐使用<video><audio>标签而非<iframe>

下面介绍一下前端代码实现预览的参考方案

PDF文件预览 (pdfjs-dis)

const pdfjsLib = require('pdfjs-dist');
require('pdfjs-dist/build/pdf.worker');

const RenderPdf = (props: { file: any }) => {
  const refCanvas = useRef();
  const refFileData = useRef();
  const [pageTotal, setPageTotal] = useState(0);
  const [curPage, setCurPage] = useState(1);
  const [url, setUrl] = useState('');

  const initFile = async () => {
    const fileUrl = await getFileData(props.file?.fileId);
    console.log(fileUrl, 'fileUrl');
    refFileData.current = fileUrl;
    setUrl(fileUrl);

    const pdfjsLibModule = await pdfjsLib;
    let loadingTask = pdfjsLibModule.getDocument(refFileData.current);
    loadingTask.promise.then(function (pdfDoc) {
      setPageTotal(pdfDoc.numPages || 0);
    });

    loadingTask.promise.catch(function (error) {
      return Promise.resolve();
    });
  };

  useEffect(() => {
    initFile();
  }, []);

  const renderPagePdf = async (page: number) => {
    const pdfjsLibModule = await pdfjsLib;
    return new Promise((resolve, reject) => {
      try {
        let loadingTask = pdfjsLibModule.getDocument(refFileData.current);
        loadingTask.promise.then(function (pdfDoc) {
          pdfDoc.getPage(page).then((page) => {
            // 创建画布
            let canvas = refCanvas.current;
            let ctx = canvas.getContext('2d');
            const viewport = page
              .getViewport({
                scale: 2,
              })
              .clone();

            canvas.style.height = viewport.height / 3 + 'px';
            canvas.style.width = viewport.width / 3 + 'px';
            // canvas画大两倍,解决绘画模糊
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            viewport.scale = viewport.scale * 2;

            let renderPromise = page.render({
              canvasContext: ctx,
              viewport: viewport,
            }).promise;

            renderPromise.then(() => {
              resolve({});
            });

            renderPromise.catch((err) => {
              reject(err);
            });
          });
        });
      } catch (err) {
        reject(err);
      }
    });
  };

  const onSizeChange = async (page: number) => {
    await renderPagePdf(page);
  };

  const toNext = () => {
    if (curPage < pageTotal) {
      setCurPage(curPage + 1);
    }
  };
  const toPre = () => {
    if (curPage > 1) {
      setCurPage(curPage - 1);
    }
  };

  useEffect(() => {
    if (curPage > 0 && refFileData.current) {
      setTimeout(() => {
        onSizeChange(curPage);
      });
    }
  }, [curPage, pageTotal]);

  return (
    <div className=''>
      <div className='flex justify-center items-center'>
        <Button type='link' onClick={toPre}>
          上一页
        </Button>
        {curPage} / {pageTotal}
        <Button type='link' onClick={toNext}>
          下一页
        </Button>
        <Button
          type='link'
          onClick={() => {
            window.open(url, '_blank');
          }}
        >
          新窗口全屏查看
        </Button>
      </div>

      <div className='flex justify-center'>
        <canvas
          style={{
            border: '1px solid #eee',
          }}
          ref={refCanvas}
        ></canvas>
      </div>
    </div>
  );
};

doc预览 (docx-preview)

import * as doxLib from 'docx-preview';
const RenderDoc = (props: { file: any }) => {
  const refDom = useRef();
  const refFileData = useRef();

  const processBlob = (blobData: Blob) => {
    doxLib.renderAsync(blobData, refDom.current, null, {
      // inWrapper: '',
      // ignoreWidth: '',
      // ignoreHeight: '',
      // ignoreFonts: '',
      // breakPages: true,
      // debug: '',
      // experimental: '',
      // className: '',
      // trimXmlDeclaration: '',
      // renderHeaders: true,
      // renderFooters: true,
      // renderFootnotes: '',
      // renderEndnotes: '',
      // ignoreLastRenderedPageBreak: true,
      // useBase64URL: '',
      // useMathMLPolyfill: '',
      // renderChanges: true,
    });
  };

  const initFile = async () => {
    const fileUrl = await getFileData(props.file?.fileId);
    const blobData = await getBlobData(fileUrl);
    refFileData.current = blobData;
    processBlob(refFileData.current);
  };

  useEffect(() => {
    initFile();
  }, []);

  return (
    <div
      style={{
        border: '1px solid #eee',
        height: '70vh',
        overflowY: 'auto',
      }}
      ref={refDom}
    ></div>
  );
};

视频预览 (react-player)

/** 视频预览 */
import ReactPlayer from 'react-player';
const RenderVideo = (props: { file: any }) => {
  const [url, setUrl] = useState('');

  const initFile = async () => {
    const blobData = await getFileData(props.file?.fileId);
    setUrl(blobData);
  };

  useEffect(() => {
    initFile();
  }, []);

  return (
    <ReactPlayer
      width="100%"
      height={'70vh'}
      controls={true}
      playing
      url={url}
    />
  );
};

文本预览

/** 文本预览 */
const RenderText = (props: { file: any }) => {
  const [text, setText] = useState('');

  const getText = async (blobData: Blob) => {
    return new Promise((resolve, reject) => {
      try {
        const reader = new FileReader();
        reader.readAsText(blobData, 'UTF-8');

        reader.onload = function (e) {
          const result = e.target.result;
          resolve(result);
        };
      } catch {
        reject();
      }
    });
  };

  const initFile = async () => {
    const textUrl = await getFileData(props.file?.fileId);
    const blobData = await getBlobData(textUrl);
    const textVal = await getText(blobData).catch(() => {
      setText('预览失败,请下载文件到本地后查看!');
    });
    setText(textVal);
  };

  useEffect(() => {
    initFile();
  }, []);

  return (
    <div style={{ height: '100%', whiteSpace: 'pre-wrap', overflow: 'auto' }}>
      {text}
    </div>
  );
};

excel预览 (xlsx)

import * as XLSX from 'xlsx';
const RenderXlsx = (props: { file: any }) => {
  const refDom = useRef();
  const [loading, setLoading] = useState(false);
  const refWorkbookIns = useRef<XLSX.WorkBook>();
  const [sheets, setSheets] = useState<string[]>([]);
  const [sheetIndex, setSheetsIndex] = useState(0);

  // 表头
  const headers = useMemo<string[]>(() => {
    const sheet = sheets[sheetIndex];
    const colHeader = XLSX.utils.sheet_to_json(
      refWorkbookIns.current?.Sheets?.[sheet],
      {
        header: 1,
      },
    )[0];
    return colHeader || [];
  }, [sheetIndex, refWorkbookIns.current]);

  // 表头格式化提供antd使用
  const columns = headers?.map((kVal: string) => {
    return {
      title: kVal,
      dataIndex: kVal,
    };
  });

  // 表格数据
  const data = useMemo(() => {
    const sheet = sheets[sheetIndex];

    if (!refWorkbookIns.current || !sheet) {
      return [];
    }
    return XLSX.utils.sheet_to_json(refWorkbookIns.current?.Sheets?.[sheet]);
  }, [refWorkbookIns.current, sheetIndex]);

  const processBlob = (blobData: ArrayBuffer) => {
    refWorkbookIns.current = XLSX.read(blobData, { type: 'array' });
    // // 获取所有 Sheet
    const sheets = refWorkbookIns.current.SheetNames || [];
    setSheets(sheets);
  };

  const initPage = (file: File) => {
    const reader = new FileReader();
    reader.onload = (e: any) => {
      setLoading(false);
      const blob = new Uint8Array(e.target.result);
      processBlob(blob);
    };
    reader.readAsArrayBuffer(file);
  };

  const items: TabsProps['items'] = sheets?.map((sName, index) => {
    return {
      key: index + '',
      label: sName,
      children: null,
    };
  });

  const initFile = async () => {
    const fileUrl = await getFileData(props.file?.fileId);
    const blobData = await getBlobData(fileUrl);

    initPage(blobData);
  };

  useEffect(() => {
    setLoading(true);
    initFile();
  }, []);

  return (
    <div
      style={{
        height: '640px',
        overflowY: 'auto',
      }}
      ref={refDom}
    >
      <div className="h-full flex flex-col">
        <Tabs
          defaultActiveKey="0"
          items={items}
          onChange={(val: string) => {
            setSheetsIndex(val);
          }}
        />
        <div className="flex-1 overflow-auto">
          <Table
            size="small"
            dataSource={data}
            scroll={{ y: 530, x: 'max-content' }}
            pagination={false}
            columns={columns}
            loading={loading}
          ></Table>
        </div>
      </div>
    </div>
  );
};

备注

当然还有其他的在线预览的付费方案,可推荐ali云方案