记录--前端实现文件预览(word、excel、pdf、ppt、xmind、 音视频、图片、文本) 国际化

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

前言

在这之前公司项目的文档预览的方式都是通过微软在线预览服务,但是微软的在线服务有文件大小限制,想完整使用得花钱,一些图片文件就通过组件库antd实现,因为我们项目存在多种类型的文件,所以为了改善用户的体验,决定把文件预览单独弄一个拆出一个项目出来,我们先看一下最终预览效果。

实现方案

在github找了很多开源项目,发现都比较陈旧,且在项目中不能直接使用,想自己手写这些解析不太现实,且时间也是不允许的,所以只能基于这些项目进行二次开发,并且整合到一起做通用方案,下面是项目中用到的一些预览库。

那么是如何把这些整合到一起实现的呢,准备好瓜子,听我细细分说!!!

入口文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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中获取到文件类型信息

1
2
3
4
5
6
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属性
1
PDFRenderer.fileTypes = ['pdf', 'application/pdf'];

render组件匹配

还记得我们入口文件传入的pluginsRenders吗,这里面存放的就是我们所有的文件render组件,在上面我们已经获取到了当前文件的类型信息,那么下面就来关联起来,这样当前需要渲染的组件就确认的了。

1
2
3
4
5
6
7
const currenrRenderers: DocRenderer[] = [];
pluginRenderers?.map((r) => {
  if (!currentDocument.fileType) return;
  if (r.fileTypes.indexOf(currentDocument.fileType) >= 0 ) {
    currenrRenderers = r;
  }
});
 

组件渲染

存在CurrentRenderer就渲染CurrentRenderer组件,没有则渲染占位组件,这里用到了一些useWindowSize这些hooks,对适口大小的监听进行一些简单的适配工作,这样一个简单的预览就完成了,当然上面只是粘贴出了主要的部分,细节业务逻辑较多,这就不一一粘贴了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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}</>;
};

预览效果

 

音视频

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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传入即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 包含文件信息

 例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

1
2
3
4
5
6
7
<DocViewer
  language={I18NFormat.language}
  pluginRenderers={[ VideoRender ]}
  documents={documents}
  activeDocument={activeDocument}
  onClose={() => { setVisible(false); }}
 />

本文转载于:https://juejin.cn/post/7373623949836517410

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

 


__EOF__

  • 本文作者: 林恒
  • 本文链接: https://www.cnblogs.com/smileZAZ/p/18244549
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • posted @   林恒  阅读(1804)  评论(1编辑  收藏  举报
    相关博文:
    阅读排行:
    · 本地部署 DeepSeek:小白也能轻松搞定!
    · 如何给本地部署的DeepSeek投喂数据,让他更懂你
    · 从 Windows Forms 到微服务的经验教训
    · 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
    · 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee
    历史上的今天:
    2023-06-12 记录--详解 XSS(跨站脚本攻击)
    又是一年情人节,2025年找到对象了嘛~
    点击右上角即可分享
    微信分享提示