記錄--前端實現檔案預覽(word、excel、pdf、ppt、xmind、 音影片、圖片、文字) 國際化

林恒發表於2024-06-12

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

前言

在這之前公司專案的文件預覽的方式都是透過微軟線上預覽服務,但是微軟的線上服務有檔案大小限制,想完整使用得花錢,一些圖片檔案就透過元件庫antd實現,因為我們專案存在多種型別的檔案,所以為了改善使用者的體驗,決定把檔案預覽單獨弄一個拆出一個專案出來,我們先看一下最終預覽效果。

實現方案

在github找了很多開源專案,發現都比較陳舊,且在專案中不能直接使用,想自己手寫這些解析不太現實,且時間也是不允許的,所以只能基於這些專案進行二次開發,並且整合到一起做通用方案,下面是專案中用到的一些預覽庫。

那麼是如何把這些整合到一起實現的呢,準備好瓜子,聽我細細分說!!!

入口檔案

import React, { FC, useEffect } from 'react';
import styled, { ThemeProvider } from 'styled-components';
import { HeaderBar } from './components/HeaderBar';
import { ProxyRenderer } from './components/ProxyRenderer';
import CloudDocRenderer from './plugins/cloud-doc';
import { AppProvider } from './state';
import { defaultTheme } from './theme';
import { DocViewerProps } from './types';
import { IStyledProps } from './types';
import { DocViewerRenderers as pluginRenderers } from './plugins';
import { i18n, I18nextProvider } from './i18n'

const DocViewer: FC<DocViewerProps> = (props) => {
  const { documents, theme, language } = props;

  if (!documents || documents === undefined) {
    throw new Error(
      "Please provide an array of documents to DocViewer.\ne.g. <DocViewer documents={[ { uri: 'https://mypdf.pdf' } ]} />"
    );
  }

  useEffect(() => {
      i18n.changeLanguage(language ?? 'zh');
  }, [language]);

  return (
    
      <AppProvider pluginRenderers={pluginRenderers} {...props} >
        <ThemeProvider theme={theme ? { ...defaultTheme, ...theme } : defaultTheme}>
        <I18nextProvider i18n={i18n}>
          <Container id="react-doc-viewer" data-testid="react-doc-viewer" {...props}>
            <HeaderBar />
            <ProxyRenderer />
          </Container>
          </I18nextProvider>
        </ThemeProvider>
      </AppProvider>
  );
};
  

export default DocViewer;

const Container = styled.div`
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: ${(props: IStyledProps) => props.theme.bg_100};
  height: 100%;
`;

export { DocViewerRenderers } from './plugins';
export * from './types';
export * from './utils/fileLoaders';
export { default as BMPRenderer } from './plugins/bmp';
export { default as HTMLRenderer } from './plugins/html';
export { default as ImageProxyRenderer } from './plugins/image'
export { default as JPGRenderer } from './plugins/jpg';
export { default as MSDocRenderer } from './plugins/ppt'
export { default as MSGRenderer } from './plugins/msg';
export { default as PDFRenderer } from './plugins/png';
export { default as PNGRenderer } from './plugins/png';
export { default as TIFFRenderer } from './plugins/tiff';
export { default as TXTRenderer } from './plugins/txt';
export { default as CloudDocRenderer } from './plugins/cloud-doc';

從我們的入口檔案可以看出,這裡的有很多檔案型別的render元件,分別處理不同的型別檔案,具體實現我後面會講到,必傳引數是documents就是我們需要預覽的檔案資訊,那麼我們這裡需要做的是把render元件和documents檔案資訊做關聯關係。

fetch請求獲取檔案資訊

首先我們需要拿到當前的需要預覽檔案的資訊,從我們的檔案資訊中拿到檔案的線上地址,然後使用 fetch 方法傳送一個 HEAD 請求到 documentURIHEAD 請求只請求資源的頭部資訊,不獲取實際的內容,從返回的content-type中獲取到檔案型別資訊

 fetch(documentURI, { method: 'HEAD', signal }).then((response) => {
    const contentTypeRaw = response.headers.get('content-type');
    const contentTypes = contentTypeRaw?.split(';') || [];
    let contentType = contentTypes.length ? contentTypes[0] : undefined;
    handleCurrentDocument(contentType)
 })
拿到檔案型別之後就與之對應的render元件進行匹配,我們這裡給元件定義了fileTypes屬性
PDFRenderer.fileTypes = ['pdf', 'application/pdf'];

render元件匹配

還記得我們入口檔案傳入的pluginsRenders嗎,這裡面存放的就是我們所有的檔案render元件,在上面我們已經獲取到了當前檔案的型別資訊,那麼下面就來關聯起來,這樣當前需要渲染的元件就確認的了。

    const currenrRenderers: DocRenderer[] = [];
    pluginRenderers?.map((r) => {
      if (!currentDocument.fileType) return;
      if (r.fileTypes.indexOf(currentDocument.fileType) >= 0 ) {
        currenrRenderers = r;
      }
    });

元件渲染

存在CurrentRenderer就渲染CurrentRenderer元件,沒有則渲染佔位元件,這裡用到了一些useWindowSize這些hooks,對適口大小的監聽進行一些簡單的適配工作,這樣一個簡單的預覽就完成了,當然上面只是貼上出了主要的部分,細節業務邏輯較多,這就不一一貼上了。

import React, { FC, useCallback } from 'react';
import styled from 'styled-components';
import { setRendererRect } from '../state/actions';
import { useDocumentLoader } from '../hooks/useDocumentLoader';
import { useWindowSize } from '../hooks/useWindowSize';
import { DocumentNav } from './DocumentNav';
import NotRender from './NotRender';

export const ProxyRenderer: FC<{}> = () => {
  const { state, dispatch, CurrentRenderer } = useDocumentLoader();
  const { documents, documentLoading } = state;

  const size = useWindowSize();

  const containerRef = useCallback(
    (node) => {
      node && dispatch(setRendererRect(node?.getBoundingClientRect()));
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [size]
  );
  const Contents = useCallback(() => {
    if (!documents.length) {
      return <div id="no-documents">{/* No Documents */}</div>;
    } else {
      return (
        <>
          <DocumentNav loading={documentLoading}>
            {CurrentRenderer ? <CurrentRenderer mainState={state} /> : <NotRender />}
          </DocumentNav>
        </>
      );
    }
  }, [CurrentRenderer, state]);

  return (
    <Container id="proxy-renderer" ref={containerRef}>
      <Contents />
    </Container>
  );
};

const Container = styled.div`
  display: flex;
  flex: 1;
  overflow-y: hidden;
  height: calc(100% - 68px);
  width: 100%;
`;

預覽docx

程式碼實現

import { renderAsync } from 'polaris-docx-preview';

const Container = styled.div`
  width: 100%;
  height: 100%;
  overflow-y: auto;
`

const MSDocRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {

  useEffect(() => {
    const element = document.getElementById('doc-renderer');
    if(element && currentDocument?.uri) {
      fetch(currentDocument.uri).then((response) => {
        let docData = response.blob();
        renderAsync(docData, element)
      })
    }
  }, [])
  if (!currentDocument) return null;

  return (
    <Container id="doc-renderer">
    </Container>
  );
};

預覽效果

預覽ppt

程式碼實現

import FileViewer from 'polaris-offices-viewer';

const MSDocRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {

  if (!currentDocument) return null;

  return (
    <Container id="msdoc-renderer">
      <FileViewer 
        filePath={currentDocument?.uri}
        errorComponent={<>errorc錯誤</>} 
      />
    </Container>
  );
};

預覽效果

預覽pdf

程式碼實現

import { PDFAllPages } from './PDFAllPages';
import PDFSinglePage from './PDFSinglePage';

const PDFPages: FC<{}> = () => {
  const {
    state: { mainState, paginated },
    dispatch,
  } = useContext(PDFContext);
  const { t } = useTranslation()

  const currentDocument = mainState?.currentDocument || null;

  useEffect(() => {
    dispatch(setNumPages(initialPDFState.numPages));
  }, [currentDocument]);

  if (!currentDocument || currentDocument.fileData === undefined) return null;

  return (
    <DocumentPDF
      file={currentDocument.fileData}
      onLoadSuccess={({ numPages }) => dispatch(setNumPages(numPages))}
      loading={<span>{t('loading')}...</span>}>
      {paginated ? <PDFSinglePage /> : <PDFAllPages />}
    </DocumentPDF>
  );
};

單頁

PDFSinglePage

import React, { FC, useContext } from 'react';
import { Page } from 'react-pdf';
import styled from 'styled-components';
import { IStyledProps } from '../../../../types';
import { PDFContext } from '../../state';
import { useTranslation } from 'react-i18next';

interface Props {
  pageNum?: number;
}

const PDFSinglePage: FC<Props> = (props) => {
  const { pageNum } = props;

  const { t } = useTranslation()

  const {
    state: { mainState, paginated, zoomLevel, numPages, currentPage },
  } = useContext(PDFContext);

  const rendererRect = mainState?.rendererRect || null;

  const _pageNum = pageNum || currentPage;
  const defaultWidth = rendererRect?.width || 100;
  const width = defaultWidth > 940 ? 940 : rendererRect?.width;

  return (
    <PageWrapper id="pdf-page-wrapper" last={_pageNum >= numPages}>
      {!paginated && (
        <PageTag id="pdf-page-info">
          {t('page')} {_pageNum}/{numPages}
        </PageTag>
      )}
      <Page
        pageNumber={_pageNum || currentPage}
        scale={zoomLevel}
        height={(rendererRect?.height || 100) - 100}
        width={width}
      />
    </PageWrapper>
  );
};

多頁

PDFAllPages

import React, { FC, useContext } from 'react';
import { PDFContext } from '../../state';
import PDFSinglePage from './PDFSinglePage';

interface Props {
  pageNum?: number;
}

export const PDFAllPages: FC<Props> = (props) => {
  const {
    state: { numPages },
  } = useContext(PDFContext);

  const PagesArray = [];
  for (let i = 0; i < numPages; i++) {
    PagesArray.push(<PDFSinglePage key={i + 1} pageNum={i + 1} />);
  }

  return <>{PagesArray}</>;
};

預覽效果

音影片

程式碼實現

import styled from "styled-components";
import Artplayer from 'artplayer';
import type { DocRenderer } from "../..";

const VideoRenderer: DocRenderer = ({ mainState: { currentDocument, language } }) => {
  useEffect(() => {
    const lang = language === 'zh' ? 'zh-cn' : language
    var art = new Artplayer({
      container: '#video-renderer',
      url: currentDocument?.uri || '',
      volume: 0.5,
      lang
  });
  art.on('click', (event) => {
    console.info('click', event);
});
  art.on('screenshot', (dataUri) => {
    art.screenshot();
  });

  }, [])
  if (!currentDocument) return null;

  return (
    <Container className="video-renderer">
      <div id='video-renderer'>
      </div>
    </Container>
  );
};

預覽效果

自定義render

如果這些基礎的文件渲染render元件,不符合業務需求,你也可以自定義render元件在你自己的專案中,然後跟隨pluginsRenders傳入即可

import type { FC } from 'react';
import React from 'react';

import { Modal } from 'antd';
import type { IDocument } from 'polaris-doc-viewer';
import DocViewer from 'polaris-doc-viewer';

import { I18NFormat } from '@polaris-pm/shared';

import { prefixCls } from '../_util/config';
import VideoRender from './VideoRender';
import './style';

const Styles = `${prefixCls}-viewer`;

interface IDocviewerModal {
  documents: IDocument[];
  activeDocument?: IDocument;
  visible: boolean;
  setVisible: (value: boolean) => void;
}

const FileViewerModal: FC<IDocviewerModal> = ({ documents, activeDocument, visible, setVisible }) => {
  return (
    <Modal destroyOnClose className={`${Styles}-wrap`} visible={visible} width={'100vw'} footer={null}>
      <div style={{ height: '100vh' }}>
        <DocViewer
          language={I18NFormat.language}
          pluginRenderers={[
            VideoRender
          ]}
          documents={documents}
          activeDocument={activeDocument}
          onClose={() => {
            setVisible(false);
          }}
        />
      </div>
    </Modal>
  );
};

export default FileViewerModal;

定義的元件需要需要提供可供使用的檔案型別fileTypes,元件裡面的 mainState 包含檔案資訊

例如

import React, { useEffect } from "react";
import styled from "styled-components";
import type { DocRendererProps } from "polaris-doc-viewr";

const Video = styled.video`
  width: 100%;
  height: 100%;
  border: 0;
`;

const VideoRenderer: DocRendererProps = ({ mainState: { currentDocument, language } }) => {


  }, [])
  if (!currentDocument) return null;
  
  return (
    <Container className="video-renderer">
     <Video controls src={currentDocument.uri} />
    </Container>
  );
};


export default VideoRenderer;

VideoRenderer.fileTypes = [ 
 'mp4', "video/mp4", 'quicktime', "video/quicktime", 'x-msvideo',
];

國際化

目前僅支援中文/英文,在使用DocViewer元件時傳入language即可 zh | en

<DocViewer 
  language={I18NFormat.language} 
  pluginRenderers={[ VideoRender ]} 
  documents={documents} 
  activeDocument={activeDocument} 
  onClose={() => { setVisible(false); }} 
 />

本文轉載於:https://juejin.cn/post/7373623949836517410

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

記錄--前端實現檔案預覽(word、excel、pdf、ppt、xmind、 音影片、圖片、文字) 國際化

相關文章