阅读 425
Electron 预览pdf、ppt

Electron 预览pdf、ppt

问题

近期公司需要做一个桌面端的需求,需要预览 `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) 支持线上、本地资源

在线资源

新建静态资源目录 publicworker、cmaps 加入进来 (资源从 pdfjs 获取)

image.png

打包后资源

image.png

问题点:

  • 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)
})
复制代码

效果图

image.png

预览 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" />
复制代码

image.png

完整代码

初稿有些需要优化的地方

/**
 * 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
  }
)
复制代码

博文推荐

【笔记不易,如对您有帮助,请点赞,谢谢】

文章分类
前端
文章标签