import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/web/pdf_viewer.css';
// 配置 worker
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'; // 注意这里路径!
type Props = {
fileData: ArrayBuffer; // 后端返回的文件流
};
export default function LazyPdfViewer({ fileData }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [pdf, setPdf] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
const observerRef = useRef<IntersectionObserver | null>(null);
useEffect(() => {
const loadingTask = pdfjsLib.getDocument({ data: fileData });
loadingTask.promise.then(setPdf);
}, [fileData]);
useEffect(() => {
if (!pdf || !containerRef.current) return;
const container = containerRef.current;
const renderPage = async (pageDiv: HTMLDivElement, pageNumber: number) => {
if (pageDiv.dataset.rendered) return; // 防止重复渲染
pageDiv.dataset.rendered = 'true';
const page = await pdf.getPage(pageNumber);
const scale = window.innerWidth < 768 ? 1.2 : 1.5; // 手机小一点
const viewport = page.getViewport({ scale });
// 创建canvas
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
const context = canvas.getContext('2d')!;
// 渲染PDF页到canvas
await page.render({ canvasContext: context, viewport }).promise;
// 渲染超链接
const annotationLayerDiv = document.createElement('div');
annotationLayerDiv.className = 'annotationLayer';
const annotations = await page.getAnnotations();
pdfjsLib.AnnotationLayer.render({
viewport,
div: annotationLayerDiv,
annotations,
page,
linkService: new pdfjsLib.SimpleLinkService(),
renderInteractiveForms: true,
});
pageDiv.appendChild(canvas);
pageDiv.appendChild(annotationLayerDiv);
};
const unrenderPage = (pageDiv: HTMLDivElement) => {
pageDiv.innerHTML = '';
pageDiv.dataset.rendered = '';
};
observerRef.current?.disconnect();
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const pageDiv = entry.target as HTMLDivElement;
const pageNumber = Number(pageDiv.dataset.pageNumber);
if (entry.isIntersecting) {
renderPage(pageDiv, pageNumber);
} else {
unrenderPage(pageDiv); // 不可见了卸载
}
});
},
{
root: container,
rootMargin: '200px 0px', // 提前加载
threshold: 0.1,
}
);
// 创建每页的div
container.innerHTML = '';
for (let i = 1; i <= pdf.numPages; i++) {
const pageDiv = document.createElement('div');
pageDiv.className = 'pdf-page';
pageDiv.dataset.pageNumber = String(i);
pageDiv.style.minHeight = '100vh'; // 初始撑开
container.appendChild(pageDiv);
observerRef.current.observe(pageDiv);
}
}, [pdf]);
return (
<div
ref={containerRef}
style={{
overflowY: 'auto',
height: '100vh',
position: 'relative',
}}
/>
);
}
.pdf-page {
position: relative;
margin: 20px 0;
}
.annotationLayer {
position: absolute;
top: 0;
left: 0;
pointer-events: auto;
}
canvas {
display: block;
margin: 0 auto;
}
import LazyPdfViewer from './LazyPdfViewer';
import { useState, useEffect } from 'react';
export default function App() {
const [pdfData, setPdfData] = useState<ArrayBuffer | null>(null);
useEffect(() => {
// 模拟后端取文件流
fetch('/your-pdf-url.pdf')
.then((res) => res.arrayBuffer())
.then(setPdfData);
}, []);
return (
<div>
{pdfData ? <LazyPdfViewer fileData={pdfData} /> : <p>Loading...</p>}
</div>
);
}