בניית מציג PDF: react-pdf, וירטואליזציה ואבטחה

מציגי PDF מובנים באתרים מאכזבים לעתים קרובות: קבצים יורדים במקום להופיע, הממשק מגושם, והעמודים נטענים לאט. אנחנו בונים מציג PDF אמין המבוסס על react-pdf ו-PDF.js עם וירטואליזציה של עמודים והגנה מפני הורדה. הצוות שלנו מספק את הפרויקט במפתח מלא—מאינטגרציה ועד תמיכה מתמשכת—ומבטיח ביצועים יציבים ונוחות למשתמשים שלכם.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
בניית מציג PDF: react-pdf, וירטואליזציה ואבטחה
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1504
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

בניית מציג PDF: react-pdf, וירטואליזציה ואבטחה

צפייה מובנית ב-PDF היא דרישה טיפוסית ללוחות ניהול מסמכים, דפי חוזים, פורטלי מסמכים רגולטוריים ומערכות דיווח. שימוש פשוט ב-<iframe src="file.pdf"> הוא רעיון גרוע: ההתנהגות תלויה בדפדפן, Chrome במובייל מוריד את הקובץ במקום להציג אותו, וממשק המשתמש אינו ניתן לשליטה. במהלך השנים, שיפרנו עשרות אינטגרציות כאלה ואנחנו יודעים איך להפוך אותן לנוחות ומאובטחות. נתקלנו בבעיות ביצועים ואבטחה בכל פעם, ולכן פיתחנו גישה מוכחת המבוססת על react-pdf ו-PDF.js. הפחיתו את עלות הפיתוח הפנימי — הפתרון שלנו משתלם בכך שהוא חוסך זמן ומונע שגיאות.

אנו משתמשים בטכנולוגיות מוכחות: React 18+, TypeScript, react-pdf ו-PDF.js. אנו מגדירים ניווט, זום, בחירת טקסט, חיפוש — הכל כמו ב-Figma או Google Docs. בכל שלב אנו מודדים Core Web Vitals: LCP אינו עולה על 2.5 שניות, TTFB — 0.8 שניות. חיסכון בתקציב על הטמעה יכול להגיע ל-40% בהשוואה לפיתוח פנימי.

למה react-pdf הוא הבחירה הטובה ביותר להטמעת PDF

Mozilla PDF.js הוא התקן דה-פקטו לעיבוד PDF בדפדפן. הוא משתמש ב-Canvas כדי לעבד כל עמוד, תומך בשכבת טקסט והערות. Firefox משתמש בו עבור המציג המובנה שלו. react-pdf הוא wrapper בוגר המשמש את GitHub, CodeSandbox ו-Canva לתצוגות מקדימות. הוא פותר בעיות בין-דפדפנים ונותן שליטה מלאה על ממשק המשתמש.

npm install pdfjs-dist npm install react-pdf # React-обёртка над PDF.js 

איך ליישם מצב עמוד-אחר-עמוד

import { Document, Page, pdfjs } from 'react-pdf'
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'

// Указываем worker — обязательно
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString()

interface PDFViewerProps {
  url: string
}

export function PDFViewer({ url }: PDFViewerProps) {
  const [numPages, setNumPages] = useState<number>(0)
  const [pageNumber, setPageNumber] = useState<number>(1)
  const [scale, setScale] = useState<number>(1.0)
  const [isLoading, setIsLoading] = useState(true)

  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
    setNumPages(numPages)
    setIsLoading(false)
  }

  return (
    <div className="flex flex-col items-center">
      {/* Тулбар */}
      <div className="flex items-center gap-4 p-3 bg-gray-800 text-white w-full">
        <button
          onClick={() => setPageNumber((p) => Math.max(1, p - 1))}
          disabled={pageNumber <= 1}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          ←
        </button>
        <span className="text-sm">
          {pageNumber} / {numPages}
        </span>
        <button
          onClick={() => setPageNumber((p) => Math.min(numPages, p + 1))}
          disabled={pageNumber >= numPages}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          →
        </button>
        <div className="ml-auto flex items-center gap-2">
          <button onClick={() => setScale((s) => Math.max(0.5, s - 0.25))}>−</button>
          <span className="text-sm w-12 text-center">{Math.round(scale * 100)}%</span>
          <button onClick={() => setScale((s) => Math.min(3, s + 0.25))}>+</button>
        </div>
      </div>
      {/* Документ */}
      <div className="overflow-auto bg-gray-200 w-full" style={{ maxHeight: '80vh' }}>
        <Document
          file={url}
          onLoadSuccess={onDocumentLoadSuccess}
          loading={<div className="p-8 text-center">Загрузка...</div>}
          error={<div className="p-8 text-center text-red-500">Ошибка загрузки PDF</div>}
        >
          <Page
            pageNumber={pageNumber}
            scale={scale}
            renderTextLayer={true} // Выделяемый текст
            renderAnnotationLayer={true} // Кликабельные ссылки
            className="shadow-lg mx-auto my-4"
          />
        </Document>
      </div>
    </div>
  )
}

איך ליישם גלילה של כל העמודים

עבור מסמכים ארוכים, גלילה נוחה יותר מפניציה:

function PDFScrollViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [containerWidth, setContainerWidth] = useState(0)
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!containerRef.current) return
    const observer = new ResizeObserver(([entry]) => {
      setContainerWidth(entry.contentRect.width)
    })
    observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  return (
    <div ref={containerRef} className="overflow-auto" style={{ height: '80vh' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        {Array.from({ length: numPages }, (_, i) => (
          <Page
            key={i + 1}
            pageNumber={i + 1}
            width={containerWidth - 32} // Адаптивная ширина
            className="mb-4 shadow mx-4"
            renderTextLayer={true}
          />
        ))}
      </Document>
    </div>
  )
}

למה יש צורך בווירטואליזציה עבור PDF גדולים

עיבוד מלא של PDF בן 100 עמודים יהרוג את זיכרון הדפדפן: הצריכה יכולה לעלות על 500 MB. וירטואליזציה מעבדת רק עמודים גלויים בתוספת מרווח קטן (כברירת מחדל 2 עמודים מעל ומתחת). זה מפחית את העומס ב-90% ומאיץ את העיבוד הראשוני ל-0.5 שניות. אנו משתמשים ב-npm install pdfjs-dist npm install react-pdf # React-обёртка над PDF.js :

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualPDFViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [pageHeight] = useState(842) // A4 при scale=1
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: numPages,
    getScrollElement: () => parentRef.current,
    estimateSize: () => pageHeight + 16, // высота страницы + отступ
    overscan: 2,
  })

  return (
    <div ref={parentRef} style={{ height: '80vh', overflow: 'auto' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
          {virtualizer.getVirtualItems().map((virtualItem) => (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                transform: `translateY(${virtualItem.start}px)`,
                width: '100%',
                padding: '8px 16px',
              }}
            >
              <Page
                pageNumber={virtualItem.index + 1}
                width={600}
                renderTextLayer={false} // Отключаем для скорости
              />
            </div>
          ))}
        </div>
      </Document>
    </div>
  )
}

איך לבחור את מצב התצוגה

עבור מסמכים קטנים (עד 30 עמודים), מצב עמוד-אחר-עמוד עם כפתורי ניווט נוח — פשוט ומדויק. עבור עד 50 עמודים, ניתן להפעיל גלילה של כל העמודים בבת אחת, אך זה דורש יותר זיכרון. עבור מסמכים מעל 50 עמודים, אנו ממליצים בחום על גלילה וירטואלית: רק עמודים גלויים מעובדים, חוסכים עד 90% זיכרון ומאיצים את העיבוד הראשוני ל-0.5 שניות. בפועל, PDF בן 100 עמודים במצב וירטואלי צורך כ-35 MB במקום 280 MB.

מספר עמודים מצב זמן עיבוד ראשוני צריכת זיכרון
10 עמוד-אחר-עמוד 0.6 שניות 30 MB
100 גלילה 2.1 שניות 280 MB
100 גלילה וירטואלית 0.5 שניות 35 MB
500 גלילה וירטואלית 1.2 שניות 70 MB

איך להגן על PDF מפני הורדה

כדי למנוע ממשתמשים להוריד את הקובץ ישירות דרך URL, אנו משתמשים ב-Blob URLs. טוענים את ה-PDF דרך API עם הרשאה, ממירים ל-Blob ויוצרים URL זמני שחי רק בתוך הסשן. הקישור הישיר לקובץ אף פעם לא נחשף לפרונטאנד.

async function loadProtectedPDF(documentId: string): Promise<string> {
  const response = await fetch(`/api/documents/${documentId}/content`, {
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
  })
  if (!response.ok) throw new Error('Access denied')
  const blob = await response.blob()
  return URL.createObjectURL(blob) // Blob URL действителен только в рамках сессии браузера
  // Прямой ссылки нет — только через API с авторизацией
}

בנוסף, אנו יכולים להטיל סימן מים על כל עמוד דרך canvas — זה התאמה אישית לפרויקט שלכם. לדוגמה, סימן מים עם האימייל של המשתמש מפחית את סיכון הדליפה ב-80% לפי התצפיות שלנו.

אם ה-API עבור PDF נמצא בדומיין אחר, הגדירו CORS: import { Document, Page, pdfjs } from 'react-pdf' import 'react-pdf/dist/esm/Page/AnnotationLayer.css' import 'react-pdf/dist/esm/Page/TextLayer.css' // Указываем worker — обязательно pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString() interface PDFViewerProps { url: string } export function PDFViewer({ url }: PDFViewerProps) { const [numPages, setNumPages] = useState<number>(0) const [pageNumber, setPageNumber] = useState<number>(1) const [scale, setScale] = useState<number>(1.0) const [isLoading, setIsLoading] = useState(true) function onDocumentLoadSuccess({ numPages }: { numPages: number }) { setNumPages(numPages) setIsLoading(false) } return ( <div className="flex flex-col items-center"> {/* Тулбар */} <div className="flex items-center gap-4 p-3 bg-gray-800 text-white w-full"> <button onClick={() => setPageNumber((p) => Math.max(1, p - 1))} disabled={pageNumber <= 1} className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40" > ← </button> <span className="text-sm"> {pageNumber} / {numPages} </span> <button onClick={() => setPageNumber((p) => Math.min(numPages, p + 1))} disabled={pageNumber >= numPages} className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40" > → </button> <div className="ml-auto flex items-center gap-2"> <button onClick={() => setScale((s) => Math.max(0.5, s - 0.25))}>−</button> <span className="text-sm w-12 text-center">{Math.round(scale * 100)}%</span> <button onClick={() => setScale((s) => Math.min(3, s + 0.25))}>+</button> </div> </div> {/* Документ */} <div className="overflow-auto bg-gray-200 w-full" style={{ maxHeight: '80vh' }}> <Document file={url} onLoadSuccess={onDocumentLoadSuccess} loading={<div className="p-8 text-center">Загрузка...</div>} error={<div className="p-8 text-center text-red-500">Ошибка загрузки PDF</div>} > <Page pageNumber={pageNumber} scale={scale} renderTextLayer={true} // Выделяемый текст renderAnnotationLayer={true} // Кликабельные ссылки className="shadow-lg mx-auto my-4" /> </Document> </div> </div> ) } והעבירו את הטוקן בכותרת Authorization.

בעיות טיפוסיות ופתרונות

בעיה פתרון
PDF לא מעובד עקב CORS הגדירו את השרת להגיב עם כותרת Access-Control-Allow-Origin
טעינה איטית של PDF גדולים הפעילו וירטואליזציה של עמודים
טקסט לא ניתן לבחירה ודאו ש-renderTextLayer=true ו-TextLayer.css מיובא
קישורים ב-PDF לא עובדים בדקו ש-renderAnnotationLayer=true ו-AnnotationLayer.css

מה כלול בעבודה

  1. ביקורת על ההטמעה הקיימת (אם יש)
  2. אינטגרציה של react-pdf עם המצב הנבחר
  3. סרגל כלים מותאם אישית (ניווט, זום, חיפוש)
  4. הגדרת CORS וטעינה דרך API
  5. תיעוד ודוגמאות שימוש
  6. אחריות על הקוד למשך 6 חודשים

דונו בפרויקט שלכם — אנו נבחר את המצב האופטימלי לתקציב שלכם. צרו קשר כדי לקבל הערכת זמן ראשונית: בין 1 ל-4 ימים תלוי במורכבות. הזמינו אינטגרציה עם אחריות ל-6 חודשים — חסכו עד 40% מהתקציב בהשוואה לפיתוח פנימי.