问题
近期公司需要做一个桌面端的需求,需要预览 `pdf、ppt` 等,并且的支持`线上本地`资源
基于 vite-reactts-electron-starter 搭建的项目
对 Electron 的注意点:
开发环境启动的是一个本地服务器,打包之后是一个文件服务
const port = process.env.PORT || 3000
const url = isDev ? `http://localhost:${port}` : join(__dirname, '../../renderer/index.html')
// and load the index.html of the app.
isDev ? window?.loadURL(url) : window?.loadFile(url)
方案
预览 pdf
iframe 缺点不支持本地资源
<iframe src="https://public-static-edu.codemao.cn/dev/17/sdk_upload/1627992652000/人月神话.pdf" />
react-pdf(pdfjs) 支持线上、本地资源
在线资源
新建静态资源目录 public 将 worker、cmaps 加入进来 (资源从 pdfjs 获取)
打包后资源
问题点:
WebWorker资源的引入、不然会失败
注意路径不需要静态资源的根目录
pdfjs.GlobalWorkerOptions.workerSrc = 'pdf/pdf.worker.min.js'
cMapUrl资源的配置(不配置、中文字体) 注意路径不需要静态资源的根目录
<Document
options={{
cMapUrl: 'pdf/cmaps/',
cMapPacked: true
}}
/>
svg方式渲染会导致英文重叠 用canvas方式渲染替代
<Document
renderMode="canvas"
/>
- 当渲染
pdf文件过大的时候、如何处理-
分页
-
虚拟列表
-
分批渲染(这里采用的分批渲染)
利用
requestAnimationFrame函数动态的往数据源里面添加资源
function addPageSource() { const allPageSourceLen = allPageSource.length const length = allPage < allPageSourceLen + MIN_PAGE ? allPage - allPageSourceLen : MIN_PAGE allPageSource.push(...new Array(length).fill('')) setAllPageSource([...allPageSource]) if (allPageSource.length >= allPage) return requestAnimationFrame(() => { addPageSource() }) }useMemo函数的使用 使每一页的渲染只受总页数的影响,当其他状态改变时不会引起page的重新渲染
const generatePage = useMemo(() => { return allPageSource.map((page, index) => ( <Page key={index} pageNumber={index + 1} scale={1} loading="" onRenderError={() => { console.log('onRenderError') }} onRenderSuccess={() => { console.log('onRenderSuccess') }} /> )) }, [allPageSource]) -
本地资源
通过渲染进程与主进程之间的通信,读取文件传递给渲染进程,然后构建成 `Blob` 然后渲染
渲染进程
useEffect(() => {
window.Main.send('pdf', '')
window.Main.on('pdf', (fromMain: string) => {
console.log(typeof fromMain)
setFile(new Blob([fromMain]))
})
}, [])
<Pdf file={file} />
主进程
ipcMain.on('pdf', (event: IpcMainEvent) => {
const file = readFileSync(join(__dirname, '/../../renderer/pdf/lalala课件.pdf'))
setTimeout(() => event.sender.send('pdf', file), 500)
})
效果图
预览 ppt
微软的 Office 网页版免费 缺点不支持本地资源
https://view.officeapps.live.com/op/view.aspx?src=+ 资源地址(需要经过encodeURIComponent编码)- 必须是线上资源
<iframe src="https://view.officeapps.live.com/op/view.aspx?src=https%3A%2F%2Fdev-static-edu.codemao.cn%2Fdev%2F17%2Fsdk_upload%2F1628485563000%2F%E6%BB%9A%E8%9B%8B.pptx%3Fe%3D1628486784%26token%3DZ7VJBYDfapvJhzJLXXsy-M4OqxaUPlrRP6pslxKr%3A8BvbZnX53Tlf4kDV8EClklKbma0%3D" />
完整代码
初稿有些需要优化的地方
/**
* pdf 预览组建
*
* 支持文件状态
* 1. 线上文件 url
* 2. 本地文件 blob
*/
import { FC, useState, useRef, memo, KeyboardEvent, ChangeEvent, useMemo, useEffect } from 'react'
import { Document, Page } from 'react-pdf'
import Style from './index.module.css'
import classnames from 'classnames'
const MULTIPLE_STEP = 0.15
const MIN_PAGE = 10
const MIN_WIDTH = 400
interface PdfType {
file: string | Blob
}
enum ScaleType {
NARROW,
ENLARGE
}
export const Pdf: FC<PdfType> = memo(
({ file: propsFile }) => {
// 总页数
const [allPage, setAllPage] = useState<number>(0)
const [allPageSource, setAllPageSource] = useState<string[]>([])
// 输入页码值
const [currentInputVal, setCurrentInputVal] = useState<number | string>(1)
// 滚动盒子
let { current: pdfScrollWrapEl } = useRef<HTMLDivElement>(null)
// pdf 内容盒子
let { current: pdfMainEl } = useRef<HTMLDivElement>(null)
// 当前页码
let pageNumber = useRef<number>(1)
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
setAllPage(numPages)
}
function addPageSource() {
const allPageSourceLen = allPageSource.length
const length = allPage < allPageSourceLen + MIN_PAGE ? allPage - allPageSourceLen : MIN_PAGE
allPageSource.push(...new Array(length).fill(''))
setAllPageSource([...allPageSource])
if (allPageSource.length >= allPage) return
requestAnimationFrame(() => {
addPageSource()
})
}
useEffect(() => {
addPageSource()
}, [allPage])
const generateScrollPage = useMemo(() => {
return allPageSource.map((page, index) => (
<Page
key={index}
pageNumber={index + 1}
scale={1}
loading=""
onRenderError={() => {
console.log('onRenderError')
}}
onRenderSuccess={() => {
console.log('onRenderSuccess')
}}
/>
))
}, [allPageSource])
// 上一页
function handlePre() {
const tempPageNumber = pageNumber.current - 1
handleScroll(tempPageNumber)
}
function handleChange(event: ChangeEvent<InputEvent>) {
const value = event.target.value
if (value === '') return setCurrentInputVal('')
setCurrentInputVal(Number(value) || 0)
}
function handleEnterEvent(e: KeyboardEvent<InputEvent>) {
if (e.keyCode === 13) handleScroll(currentInputVal as number)
}
// 下一页
function handleNext() {
const tempPageNumber = pageNumber.current + 1
handleScroll(tempPageNumber)
}
// 滚动到某一页
function handleScroll(page: number) {
if (page > allPage) return
if (page < 1) return
pageNumber.current = page
const clientHeight = document.querySelector('.react-pdf__Page')?.clientHeight || 0
pdfScrollWrapEl && (pdfScrollWrapEl.scrollTop = (page - 1) * (clientHeight + 10))
setCurrentInputVal(page)
}
// 缩放
function handleScale(scaleType: ScaleType) {
if (!pdfMainEl) return
const width = pdfMainEl.clientWidth
const scaleWidth = width * MULTIPLE_STEP
let newWidth = 0
if (ScaleType.NARROW === scaleType) {
newWidth = width - scaleWidth
} else {
newWidth = width + scaleWidth
}
pdfMainEl.style.width = newWidth < MIN_WIDTH ? `${MIN_WIDTH}px` : `${newWidth}px`
}
function generatePagination() {
return (
<div className={Style.page_wrap}>
<div className={Style.left}>
<span className={classnames(Style['small-btn'], Style['prev'])} onClick={handlePre} />
<input
type="number"
min="1"
max={allPage}
className={Style['page-num']}
onChange={handleChange}
onKeyDown={handleEnterEvent}
value={currentInputVal}
/>
/<span>{allPage}页</span>
<span className={classnames(Style['small-btn'], Style['next'])} onClick={handleNext} />
</div>
<div className={classnames(Style.right)}>
<div
className={classnames(Style['tool-btn'], Style['narrow'])}
onClick={() => handleScale(ScaleType.NARROW)}
/>
<div
className={classnames(Style['tool-btn'], Style['enlarge'])}
onClick={() => handleScale(ScaleType.ENLARGE)}
/>
</div>
</div>
)
}
return (
<div className={Style.pdf_wrap}>
<div className={Style.pdf_container}>
<div
className={Style.pdf_scroll_wrap}
ref={(el: HTMLDivElement) => {
pdfScrollWrapEl = el
}}
>
<Document
// 中文不显示是字体文件导致的
// svg 方式渲染会导致英文重叠 用 canvas 方式渲染替代
// cMapUrl 注意路径问题
className={Style.pdf_main}
inputRef={(el: HTMLDivElement) => {
pdfMainEl = el
}}
options={{
cMapUrl: 'pdf/cmaps/',
cMapPacked: true
}}
file={propsFile}
// file="https://public-static-edu.codemao.cn/dev/17/sdk_upload/1627992652000/人月神话.pdf"
// file="https://public-static-edu.codemao.cn/dev/17/sdk_upload/1628149495000/sllwqy.pdf" // 文件路径或者base64
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={console.error}
error={<div>资源出错了</div>}
renderMode="canvas"
loading="正在努力加载中..."
externalLinkTarget="_blank"
noData="暂无数据"
>
{/* 生成页面 */}
{generateScrollPage}
</Document>
</div>
</div>
{/* 分页器 */}
{generatePagination()}
</div>
)
},
(newProps, oldProps) => {
return newProps.file === oldProps.file
}
)
博文推荐
- Vue + TypeScript + Element-ui + Axios 搭建前端项目基础框架
- 基于 node 实现项目下载、自动化路由、项目发布脚手架
- 封装一个简单的 WebSocket 库
- 笔记:Vue 常见面试题汇总及解析
- Vue3.0 中 Object.defineProperty 的代替方案 Proxy
- vue 3.0 —— 之初体验一
- 一张图搞懂原型、原型对象、原型链
- Promise 原理篇 = 从 0 到 1 构建一个 Promise
【笔记不易,如对您有帮助,请点赞,谢谢】