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

cnblogs 2024-06-13 08:11:00 阅读 75

🧑‍💻 写在开头

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

前言

在这之前公司项目的文档预览的方式都是通过微软在线预览服务,但是微软的在线服务有文件大小限制,想完整使用得花钱,一些图片文件就通过组件库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 data-test {...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 >{/* No Documents */}</div>;

} else {

return (

<>

<DocumentNav loading={documentLoading}>

{CurrentRenderer ? <CurrentRenderer mainState={state} /> : <NotRender />}

</DocumentNav>

</>

);

}

}, [CurrentRenderer, state]);

return (

<Container 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 >

</Container>

);

};

预览效果

预览ppt

代码实现

import FileViewer from 'polaris-offices-viewer';

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

if (!currentDocument) return null;

return (

<Container >

<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 last={_pageNum >= numPages}>

{!paginated && (

<PageTag >

{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

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。