欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React實現(xiàn)PDF預(yù)覽功能與終極優(yōu)化

 更新時間:2025年05月30日 16:21:06   作者:小堯1  
在前端開發(fā)中,PDF 預(yù)覽是個常見需求,本文主要來帶大家認(rèn)識一個基于 react-pdf 的自定義 PDF 預(yù)覽組件 PDFView,感興趣的小伙伴可以了解下

 在前端開發(fā)中,PDF 預(yù)覽是個常見需求。簡單粗暴的方案是用 標(biāo)簽直接嵌入,但你有沒有遇到過這樣的問題:樣式不好調(diào)、功能太單一、用戶體驗不夠友好?今天,我要帶你認(rèn)識一個基于 react-pdf 的自定義 PDF 預(yù)覽組件 PDFView,它不僅支持翻頁、縮放、全屏,還能無縫集成到你的項目中。我們會拆解它的實現(xiàn),對比 的優(yōu)劣,最后用一個 Demo 展示它的實力。準(zhǔn)備好了嗎?讓我們一起把 PDF 預(yù)覽玩出新花樣吧!

為什么需要自定義 PDF 預(yù)覽?

先說說需求場景。假設(shè)你有個文件管理系統(tǒng),用戶上傳 PDF 后需要在線預(yù)覽。你可能會直接寫:

<embed src="file.pdf#toolbar=0" type="application/pdf" width="100%" height="700px" />

這行代碼確實能用,但問題不少:

  • 樣式控制弱:背景、邊框不好調(diào)整,工具欄難以隱藏。
  • 交互性差:沒有翻頁按鈕、縮放功能,用戶體驗一般。
  • 功能單一:無法動態(tài)調(diào)整頁面大小或全屏展示。

而我們的 PDFView 組件,基于 react-pdf,用 React 的方式解決問題,提供更靈活的控制和更優(yōu)雅的體驗。接下來,我們拆解它的代碼,看看它是怎么“打敗” 的!

核心代碼拆解:從設(shè)計到實現(xiàn)

問題驅(qū)動開發(fā)

初版本的痛點:

  • 性能瓶頸:大文件一次性加載全部頁面,內(nèi)存占用高,加載慢。
  • 功能缺失:沒有頁面旋轉(zhuǎn),方向不對只能干瞪眼;沒有多頁預(yù)覽,翻頁全靠手動。

這些問題在實際場景中很常見。比如,用戶上傳一個 50 頁的合同 PDF,如果加載卡頓,或者需要旋轉(zhuǎn)查看簽名頁,原始版本就有點“力不從心”。優(yōu)化后的 PDFView 將通過分頁加載提升性能,新增旋轉(zhuǎn)和縮略圖功能,讓體驗飛起來!

優(yōu)化后的核心實現(xiàn)

1. 性能優(yōu)化:分頁加載

問題:原始版本用 一次性加載所有頁面,大文件時容易卡頓。

解決:引入 loadedPages 狀態(tài)(Set 類型),只加載當(dāng)前頁和用戶訪問過的頁面。

實現(xiàn)

  • 初始化僅加載第 1 頁。
  • 用戶翻頁或跳轉(zhuǎn)時動態(tài)添加加載頁面。
  • 縮略圖模式下未加載頁面顯示占位符,點擊時加載。
useEffect(() => {
  if (pageNumber && !loadedPages.has(pageNumber)) {
    setLoadedPages(prev => new Set(prev).add(pageNumber));
  }
}, [pageNumber]);

2.功能增強:頁面旋轉(zhuǎn)

需求:支持用戶調(diào)整頁面方向(比如橫向文檔)。

實現(xiàn)

  • 新增 rotation 狀態(tài),默認(rèn) 0°。
  • 提供 rotateLeft(-90°)和 rotateRight(+90°)函數(shù)。
  • 通過 Page 組件的 rotate 屬性應(yīng)用旋轉(zhuǎn)。
const rotateLeft = () => setRotation((prev) => (prev - 90) % 360);
const rotateRight = () => setRotation((prev) => (prev + 90) % 360);

3.功能增強:多頁預(yù)覽

需求:用戶想快速瀏覽所有頁面,像縮略圖一樣。

實現(xiàn)

  • 新增 showThumbnails 狀態(tài),切換單頁和縮略圖模式。
  • 縮略圖模式下渲染所有頁面(小尺寸),點擊跳轉(zhuǎn)到對應(yīng)頁。
{showThumbnails ? (
  <div className={styles.thumbnailContainer}>
    {Array.from({ length: numPages }, (_, i) => i + 1).map((page) => (
      <div key={page} className={styles.thumbnail} onClick={() => { setPageNumber(page); setShowThumbnails(false); }}>
        {loadedPages.has(page) ? (
          <Page pageNumber={page} width={150} rotate={rotation} loading={<Spin />} />
        ) : (
          <div className={styles.thumbnailPlaceholder}>加載中...</div>
        )}
        <span>第 {page} 頁</span>
      </div>
    ))}
  </div>
) : (
  <Page pageNumber={pageNumber} width={pageWidth} rotate={rotation} loading={<Spin size="large" />} />
)}

4.按需加載:只渲染當(dāng)前頁

思路:用 visiblePages 控制渲染頁面,初始只加載元信息,動態(tài)加載當(dāng)前頁。

實現(xiàn)

  • 移除 loadedPages,用 visiblePages 精確控制。
  • 單頁模式只渲染 pageNumber,縮略圖模式限制前后幾頁。
useEffect(() => {
  if (!showThumbnails) {
    setVisiblePages([pageNumber]);
  } else {
    const start = Math.max(1, pageNumber - 2);
    const end = Math.min(numPages, pageNumber + 2);
    setVisiblePages(Array.from({ length: end - start + 1 }, (_, i) => start + i));
  }
}, [pageNumber, showThumbnails, numPages]);

5.禁用多余渲染:輕量化頁面

  • 思路:關(guān)閉文本層和注釋層,只渲染圖像內(nèi)容。
  • 實現(xiàn):在 組件中設(shè)置 renderTextLayer={false} 和 renderAnnotationLayer={false}。
<Page
  pageNumber={pageNumber}
  width={pageWidth}
  rotate={rotation}
  loading={<Spin size="large" />}
  renderTextLayer={false}
  renderAnnotationLayer={false}
/>

6.優(yōu)化縮略圖:避免過載

思路:縮略圖模式下不一次性加載所有頁面,用占位符替代未加載頁。

實現(xiàn):僅渲染當(dāng)前頁附近的頁面,其他顯示靜態(tài)文本。

  {visiblePages.includes(page) ? (
    <Page pageNumber={page} width={150} rotate={rotation} loading={<Spin />} renderTextLayer={false} renderAnnotationLayer={false} />
  ) : (
    <div className={styles.thumbnailPlaceholder}>第 {page} 頁</div>
  )}

展示完整代碼

import React, { useEffect, useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { Spin, Tooltip, Input } from 'antd';
import {
  LeftOutlined,
  RightOutlined,
  PlusCircleOutlined,
  MinusCircleOutlined,
  FullscreenExitOutlined,
  FullscreenOutlined,
  CloseCircleOutlined,
  ExclamationCircleOutlined,
  RotateLeftOutlined,
  RotateRightOutlined,
  UnorderedListOutlined,
} from '@ant-design/icons';
import  './index.less';
import { Document, Page, pdfjs } from 'react-pdf';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';

pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;

const PDFView = ({
  file,
  parentDom,
  onClose,
}: {
  file?: string | null;
  parentDom?: HTMLDivElement | null;
  onClose?: () => void;
}) => {
  const defaultWidth = 600;
  const pageDiv = useRef<HTMLDivElement>(null);
  const [numPages, setNumPages] = useState<number>(0);
  const [pageNumber, setPageNumber] = useState<number>(1);
  const [pageWidth, setPageWidth] = useState<number>(defaultWidth);
  const [fullscreen, setFullscreen] = useState<boolean>(false);
  const [rotation, setRotation] = useState<number>(0);
  const [showThumbnails, setShowThumbnails] = useState<boolean>(false);
  const [visiblePages, setVisiblePages] = useState<number[]>([1]); // 控制可見頁面

  const parent = parentDom || document.body;

  // 加載 PDF 元信息,不渲染全部頁面
  const onDocumentLoadSuccess = useCallback(({ numPages }: { numPages: number }) => {
    setNumPages(numPages);
  }, []);

  const lastPage = () => pageNumber > 1 && setPageNumber(pageNumber - 1);
  const nextPage = () => pageNumber < numPages && setPageNumber(pageNumber + 1);
  const onPageNumberChange = (e: { target: { value: string } }) => {
    let value = Math.max(1, Math.min(numPages, Number(e.target.value) || 1));
    setPageNumber(value);
    setVisiblePages([value]); // 只加載當(dāng)前頁
  };

  const pageZoomIn = () => setPageWidth(pageWidth * 1.2);
  const pageZoomOut = () => pageWidth > defaultWidth && setPageWidth(pageWidth * 0.8);
  const pageFullscreen = () => {
    setPageWidth(fullscreen ? defaultWidth : parent.offsetWidth - 50);
    setFullscreen(!fullscreen);
  };

  const rotateLeft = () => setRotation((prev) => (prev - 90) % 360);
  const rotateRight = () => setRotation((prev) => (prev + 90) % 360);
  const toggleThumbnails = () => setShowThumbnails(!showThumbnails);

  // 動態(tài)更新可見頁面
  useEffect(() => {
    if (!showThumbnails) {
      setVisiblePages([pageNumber]);
    } else {
      // 縮略圖模式下限制加載數(shù)量,避免卡頓
      const start = Math.max(1, pageNumber - 2);
      const end = Math.min(numPages, pageNumber + 2);
      setVisiblePages(Array.from({ length: end - start + 1 }, (_, i) => start + i));
    }
  }, [pageNumber, showThumbnails, numPages]);

  useEffect(() => setPageNumber(1), [file]);
  useEffect(() => {
    if( pageDiv.current){
     (pageDiv.current.scrollTop = 0)
    }
  }, [pageNumber]);

  const renderContent=()=>(<div className='view'>
    <div className='viewContent' >
      <div className='pageMain' ref={pageDiv}>
        <div className='pageContainer'>
            <Document
              file={file}
              onLoadSuccess={onDocumentLoadSuccess}
              error={
                <div style={{ textAlign: 'center', width: defaultWidth + 'px' }}>
                  <ExclamationCircleOutlined style={{ fontSize: '150px', color: '#fe725c', margin: '100px' }} />
                </div>
              }
              loading={<div style={{ textAlign: 'center', width: defaultWidth + 'px' }}><Spin size="large" style={{ margin: '200px' }} /></div>}
            >
              {showThumbnails ? (
                <div className='thumbnailContainer'>
                  {Array.from({ length: numPages }, (_, i) => i + 1).map((page) => (
                    <div
                      key={page}
                      className='thumbnail'
                      onClick={() => {
                        setPageNumber(page);
                        setShowThumbnails(false);
                      }}
                    >
                      {visiblePages.includes(page) ? (
                        <Page
                          pageNumber={page}
                          width={150}
                          rotate={rotation}
                          loading={<Spin />}
                          renderTextLayer={false} // 禁用文本層,提升性能
                          renderAnnotationLayer={false} // 禁用注釋層
                        />
                      ) : (
                        <div className='thumbnailPlaceholder'>第 {page} 頁</div>
                      )}
                      <span>第 {page} 頁</span>
                    </div>
                  ))}
                </div>
              ) : (
                <Page
                  pageNumber={pageNumber}
                  width={pageWidth}
                  rotate={rotation}
                  loading={<Spin size="large" />}
                  renderTextLayer={false} // 禁用文本層
                  renderAnnotationLayer={false} // 禁用注釋層
                  error={() => setPageNumber(1)}
                />
              )}
            </Document>
        </div>
      </div>
      <div className='pageBar'>
        <div className='pageTool'>
          <Tooltip title={pageNumber === 1 ? '已是第一頁' : '上一頁'}>
            <LeftOutlined onClick={lastPage} />
          </Tooltip>
          <Input
            value={pageNumber}
            onChange={onPageNumberChange}
            onPressEnter={onPageNumberChange as any}
            type="number"
          />{' '}
          / {numPages}
          <Tooltip title={pageNumber === numPages ? '已是最后一頁' : '下一頁'}>
            <RightOutlined onClick={nextPage} />
          </Tooltip>
          <Tooltip title="放大">
            <PlusCircleOutlined onClick={pageZoomIn} />
          </Tooltip>
          <Tooltip title="縮小">
            <MinusCircleOutlined onClick={pageZoomOut} />
          </Tooltip>
          <Tooltip title="向左旋轉(zhuǎn)">
            <RotateLeftOutlined onClick={rotateLeft} />
          </Tooltip>
          <Tooltip title="向右旋轉(zhuǎn)">
            <RotateRightOutlined onClick={rotateRight} />
          </Tooltip>
          <Tooltip title={showThumbnails ? '關(guān)閉縮略圖' : '顯示縮略圖'}>
            <UnorderedListOutlined onClick={toggleThumbnails} />
          </Tooltip>
          <Tooltip title={fullscreen ? '恢復(fù)默認(rèn)' : '適合窗口'}>
            {fullscreen ? <FullscreenExitOutlined onClick={pageFullscreen} /> : <FullscreenOutlined onClick={pageFullscreen} />}
          </Tooltip>
          {onClose && (
            <Tooltip title="關(guān)閉">
              <CloseCircleOutlined onClick={onClose} />
            </Tooltip>
          )}
        </div>
      </div>
    </div>
  </div>)
  if(parentDom){
    return renderContent()
  }
  return createPortal(
    renderContent(),
    parent,)
};

export default PDFView;

優(yōu)化后的樣式 (index.less)

.view {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 999;
}

.viewContent {
  position: relative;
  width: 100%;
  height: 100%;
}

.pageMain {
  display: flex;
  justify-content: center;
  width: 100%;
  height: 100%;
  overflow: auto;
  background: #444;
}

.pageContainer {
  width: max-content;
  max-width: 100%;
  margin: 25px 0;
  background: #fff;
  box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px 0px;
  // :global {
  //   .react-pdf__Page__textContent { display: none; }
  // }
}

.pageBar {
  position: absolute;
  bottom: 35px;
  width: 100%;
  text-align: center;
}

.pageTool {
  display: inline-block;
  padding: 8px 15px;
  color: white;
  background: rgba(66, 66, 66, 0.5);
  border-radius: 15px;
  box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px 0px;
  span {
    margin: 0 5px;
    padding: 5px;
    &:hover { background: #333; }
  }
  input {
    display: inline-block;
    width: 50px;
    height: 24px;
    margin-right: 10px;
    text-align: center;
  }
  input::-webkit-outer-spin-button,
  input::-webkit-inner-spin-button { -webkit-appearance: none; }
  input[type='number'] { -moz-appearance: textfield; }
}

.thumbnailContainer {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 20px;
  padding: 20px;
}

.thumbnail {
  cursor: pointer;
  text-align: center;
  background: #fff;
  padding: 10px;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  &:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); }
}

.thumbnailPlaceholder {
  width: 150px;
  height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f0f0f0;
  color: #666;
}

1. 組件設(shè)計:靈活與可控

輸入?yún)?shù)

  • file:PDF 文件的 URL 或數(shù)據(jù)。
  • parentDom:渲染的目標(biāo)容器,默認(rèn) document.body。
  • onClose:關(guān)閉回調(diào)。

渲染方式:用 createPortal 將組件掛載到指定 DOM,實現(xiàn)模態(tài)效果。

2. 狀態(tài)管理:交互的核心

  • numPages 和 pageNumber:控制總頁數(shù)和當(dāng)前頁。
  • pageWidth:動態(tài)調(diào)整頁面寬度,默認(rèn) 600px。
  • fullscreen:切換全屏狀態(tài)。

3. 功能實現(xiàn):用戶體驗的加分項

  • 翻頁:lastPage 和 nextPage 控制前后翻頁,Input 支持手動輸入頁碼。
  • 縮放:pageZoomIn(放大 1.2 倍)、pageZoomOut(縮小 0.8 倍,限制最小值)。
  • 全屏:pageFullscreen 切換寬度至容器大小。
  • 滾動重置:頁面切換時自動滾動到頂部。

4. UI 與樣式:美觀與實用并存

  • 布局:深色背景、白底頁面、居中展示。
  • 工具欄:懸浮底部,包含翻頁、縮放、全屏按鈕,帶 Tooltip 提示。
  • 加載與錯誤:用 Spin 和圖標(biāo)提示,提升用戶感知。

Embed vs 自定義:誰更勝一籌?

我們用一個表格對比 和 PDFView:

特性PDFView
實現(xiàn)方式原生 HTML 標(biāo)簽React 組件,基于 react-pdf
樣式控制有限(僅寬高)完全自定義(背景、工具欄、頁面樣式)
交互功能內(nèi)置工具欄(可隱藏但不靈活)自定義翻頁、縮放、全屏,手動控制頁碼
加載提示支持加載和錯誤提示
全屏支持依賴瀏覽器一鍵切換全屏
代碼維護性無需維護React 組件化,易擴展
依賴性無需額外庫依賴 react-pdf 和 pdfjs-dist

選擇 PDFView 的理由

  • 靈活性:自定義樣式和交互,適配復(fù)雜需求。
  • 用戶體驗:翻頁、縮放、全屏一應(yīng)俱全,加載和錯誤狀態(tài)友好。
  • 可維護性:組件化設(shè)計,易于集成和擴展。

適合簡單場景,但一旦需求復(fù)雜,它就顯得力不從心。PDFView 則是“全能選手”,尤其在需要深度定制的項目中表現(xiàn)亮眼。

使用場景:從 Demo 看效果

如何使用這個組件?

該組件已集成到 react-nexlif 開源庫中。你可以通過以下方式引入并使用:

示例代碼

import React, { useState,useRef } from 'react';
import { PDFView } from 'react-nexlif';
import { Button, Modal } from 'antd';
const App: React.FC = () => {
  const [fileUrl, setFileUrl] = useState<string | null>(null);
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(false);
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      setFileUrl(URL.createObjectURL(file))
    };
  };
  return (
    <div ref={ref} style={{ position: 'relative', height: '100%',width: '100%' }}>
      <input  type="file" accept=".pdf" onChange={handleFileChange} />
      
          <div ref={ref} style={{ position: 'relative', minHeight: '100vh',width:1100,height:'100%'}}>
       {fileUrl&& <PDFView
          parentDom={ref.current}
          file={fileUrl}
          onClose={() => {
            setFileUrl(null)
          }}
        />}
        </div>
    </div>
  );
};

export default App;

使用效果

?編輯

  • 上傳大文件:加載 50 頁 PDF,僅渲染當(dāng)前頁,響應(yīng)迅速。
  • 翻頁與跳轉(zhuǎn):左右箭頭或輸入頁碼切換,滾動自動歸頂。
  • 旋轉(zhuǎn):點擊旋轉(zhuǎn)按鈕,頁面順時針或逆時針調(diào)整。
  • 縮略圖:點擊列表圖標(biāo),顯示所有頁面預(yù)覽,點擊跳轉(zhuǎn)。
  • 縮放與全屏:放大縮小頁面,或一鍵鋪滿屏幕。

性能對比:優(yōu)化前后

特性優(yōu)化前優(yōu)化后
30 頁加載卡頓數(shù)秒秒開,僅加載當(dāng)前頁
內(nèi)存占用高(全量解析)低(按需加載)
縮略圖性能全渲染,易卡部分渲染,輕量快捷
響應(yīng)速度

優(yōu)化后,30 頁 PDF 從“卡到懷疑人生”變成了“快如閃電”,用戶體驗和性能雙雙起飛!

技術(shù)亮點:為什么它這么強

1.性能飛躍

  • 分頁加載避免內(nèi)存爆炸,大文件也能輕松應(yīng)對。
  • 動態(tài)加載邏輯清晰,體驗流暢。

2.功能升級

  • 頁面旋轉(zhuǎn)解決方向問題,實用性拉滿。
  • 多頁預(yù)覽提供全局視角,操作更直觀。

3.用戶體驗

  • 縮略圖模式與單頁模式無縫切換。
  • 工具欄新增圖標(biāo),交互更友好。

總結(jié)

優(yōu)化后的 PDFView 堪稱 PDF 預(yù)覽的“性能王”,30 頁大文件不卡,加載快如閃電。通過按需加載和輕量化渲染,它解決了卡頓難題;加上旋轉(zhuǎn)和多頁預(yù)覽,功能也更強大。試著把它丟進你的項目,上傳個大 PDF 測試一下,感受性能飛躍的快感吧!有其他需求或優(yōu)化思路?歡迎留言,我們一起把它打磨得更牛!

到此這篇關(guān)于React實現(xiàn)PDF預(yù)覽功能與終極優(yōu)化的文章就介紹到這了,更多相關(guān)React預(yù)覽PDF內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論