🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
前言
在這之前公司專案的文件預覽的方式都是透過微軟線上預覽服務,但是微軟的線上服務有檔案大小限制,想完整使用得花錢,一些圖片檔案就透過元件庫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
請求到 documentURI
。HEAD
請求只請求資源的頭部資訊,不獲取實際的內容,從返回的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) })
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
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。