记录--前端实现文件预览(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
请求到 documentURI
。HEAD
请求只请求资源的头部信息,不获取实际的内容,从返回的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) }) |
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> ); }; |
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__
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee
2023-06-12 记录--详解 XSS(跨站脚本攻击)