Створення PDF-читача, стійкого до збоїв, для великих документів у React Native.
Вивчіть архітектуру для обробки величезних PDF-файлів з кількома тисячами сторінок у React Native, з використанням системи „лінивих“ позначок, пошуку та стабільного навігування на пристроях з обмеженими ресурсами.
Джерела: Rendering and Navigating Large PDFs in React Native Without Crashing the App
П’ятисторінковий PDF легко відображати на екрані у додатку React Native. Однак документ, який складається з сотень чи тисяч сторінок, має глибоко вкладені позначки та посилання між розділами, становить зовсім іншу проблему — такий додаток може швидко стати повільним, нереактивним або навіть часто зависати на менш потужному обладнанні. У цьому посібнику розглядається архітектура, яка безпосередньо вирішує цю проблему: ефективне відображення великих PDF, переход до довільних сторінок, перегляд та пошук у вкладених деревах позначок, а також збереження стабільності роботи навіть на обмежених пристроях.
У одному з тестів використовувався PDF із понад 1 900 сторінками та деревом позначок із багатьма рівнями вкладення. Спроба одночасного розгортання всього цього дерева створювала серйозне навантаження на пам’ять. У наступних розділах описано підхід, який дозволив вирішити ці проблеми.
Що надає, а що не надає react-native-pdf
Перш ніж обирати додаткові бібліотеки, корисно точно знати, де закінчуються вбудовані можливості react-native-pdf, адже саме цей межування визначає решту архітектури:
- За допомогою функції-відповідача
onLoadCompleteвін дійсно надає необроблену інформацію про закладки (через параметрtableContents) на iOS та Android — але не на Windows, і точно не у форматі, який можна було б безпосередньо відобразити на екрані. Це необроблені, неструктуровані дані, які все одно потребують обробки та відображення з боку програми. - Він дійсно пропонує універсальний відповідач для натискання на посилання (
onPressLink), але цей відповідач не може розрізнити внутрішнє переходження всередині документа від зовнішнього URL, і не завжди надає точний номер сторінки для внутрішніх посилань.
З огляду на це, практичним шляхом до отримання надійних даних про закладки та посилання, сумісних з різними платформами, є їх отримання окремо, а не залежність від первинного результату виклику обробника — саме цю роль виконує pdf-lib, про що йтиметься далі. Ця архітектура не передбачає пошук тексту всередині самого вмісту PDF; натомість вона займається пошуком назв закладок, що є значно простішим завданням, яке можна повністю виконати за допомогою інструментів з відкритим кодом.
Вибір пакетів
Для налаштування потрібні три пакети, кожен з яких відповідає за окреме завдання:
react-native-pdf обробляє відображення, використовуючи вбудований двигун PDF платформи замість перетворення сторінок на зображення на основі JavaScript — саме цей нативний підхід дозволяє зберігати розумне споживання пам’яті під час роботи з великими файлами. Він відповідає за відображення сторінок, підтримку зумування, генерацію подій зміни сторінки та можливість програмного переходу на певну сторінку.
react-native-blob-util — це залежність, від якої залежить react-native-pdf для доступу до файлів; вона займається завантаженням, кешуванням та читанням PDF, яке відображається. Це не додатковий елемент, а обов’язкова складова внутрішньої роботи react-native-pdf.
pdf-lib усуває проблему, описану раніше. Замість того, щоб відтворювати будь-що, він дозволяє безпосередньо читати та маніпулювати внутрішньою структурою PDF: каталогом документа, деревом сторінок та — що особливо важливо для цього випадку — словником огляду, де фактично знаходяться закладки всередині PDF, разом із об’єктами анотацій посилань, приєднаними до окремих сторінок, де зберігаються як внутрішні, так і зовнішні посилання. Оскільки react-native-pdf не відображає жодної з цих інформацій у структурованому чи надійному вигляді, pdf-lib використовується для одноразового прослідкування цієї внутрішньої структури та створення чистого JSON-формату дерева закладок — з назвою, цільовою сторінкою та вкладеними дочірніми елементами — а також набору вирішених адрес посилань, усе це незалежно від платформозалежної поведінки нативного рендерера. Цей крок вилучення інформації має виконуватися лише один раз.
Примітка щодо Expo:
react-native-pdfзалежить від нативних модулів, тому він не буде працювати всередині стандартного клієнта Expo Go. Потрібна спеціальна версія для розробки або проєкт у нативному форматі.
Архітектура
Система складається з трьох шарів, кожен з яких ізольований від внутрішньої роботи інших:
- PDF-переглядач — відстежує поточну сторінку, загальну кількість сторінок, стан завантаження та будь-які стани помилок.
- Панель інструментів читача — відображає деталі поточної сторінки та надає доступ до панелі закладок.
- Панель закладок — керує полем пошуку, деревом закладок, яке можна розгортати (створеним на основі даних, отриманих за допомогою
pdf-lib), та надсилає одну інформацію: яку сторінку обрав користувач.
Панель закладок не має уявлення про те, як сам PDF відображається, а програвач PDF не знає, як реалізовано пошук закладок. Саме цей однонапрямковий процес — панель обирає сторінку, а екран наказує програвачу переміститися туди — заважає таким діям, як введення тексту у поле пошуку чи розгортання вузла дерева, спричиняти дороге повторне відображення самого компонента PDF.
Крок 1: Структурувати дані закладок
Кожен закладка, з якою ви працюєте, потребує трьох елементів: назви, цільової сторінки та необов’язкового масиву дочірніх закладок — це проста рекурсивна структура, яка природним чином відповідає тому, що повертає крок вилучення даних з pdf-lib. Найважливішим моментом тут є те, що кожна закладка повинна мати стабільний, унікальний ідентифікатор, і цей ідентифікатор не може бути просто її позицією в масиві. Як тільки користувач починає вводити текст у пошук або відкривати/закривати розділи, позиція конкретної закладки у відображеному списку змінюватиметься. Якщо ви покладаєтесь на індекс масиву як на ідентифікатор, React втрачає здатність розрізняти окремі елементи, стан розгортання/складання приєднується до неправильного елемента, а FlatList починає неправильно повторно використовувати елементи.
Крок 2: Пошук без втрати контексту
Оскільки відповідна закладка часто знаходиться у кількох рівнях глибини дерева, ваша логіка пошуку має перебирати структуру рекурсивно, а не лише сканувати заголовки верхнього рівня. Не менш важливо те, що коли дочірній елемент відповідає терміну пошуку, потрібно також включити до результатів його батьківський елемент, навіть якщо сам заголовок батька не збігається ні з чим. Якщо проігнорувати це, результат з великою глибиною розташування з’явиться без позначення того, де саме він знаходиться у документі.
Щоб зберегти швидкість роботи, обгорніть логіку фільтрації в механізм мемоізації, щоб вона запускалася заново лише тоді, коли змінюється рядок пошуку або дані закладки. Для дерев із великою кількістю елементів додайте також механізм затримки введення тексту, щоб не перефільтровувати всю структуру після кожного натискання клавіші.
Крок 3: Розплющити лише видимі елементи
FlatList є ідеальним інструментом у цьому випадку через причини продуктивності, але він очікує плоский масив, а не вкладену деревоподібну структуру. Тож потрібен етап обробки, який пройдеся по дереву закладок та створить плоский масив, що міститиме лише ті вузли, які наразі мають бути видимими — діти вузла включатимуться лише тоді, коли цей вузол наразі розгорнутий. Кожен елемент плоского масиву також має мати значення depth, щоб ви знали, наскільки його потрібно відступити для візуального представлення.
Цей крок, мабуть, є найважливішим рішенням щодо продуктивності в усій реалізації. Якби ви спростували весь дерево та просто сховали рядки, що належать до згорнутих гілок, FlatList все одно зберігав би тисячі рядків, які ніколи не з’являються на екрані. Завдяки спрощенню лише тих елементів, які наразі розгорнуті, дерево закладок із тисячами записів може у будь-який момент передавати FlatList лише кілька десятків рядків.
Крок 4: Створення панелі закладок
Реалізуйте панель як модальне вікно, розташоване над переглядачем PDF, а не як екран, який його замінює, та обмежте її висоту приблизно 80% розміру екрана, замість того щоб дозволити їй заповнювати весь екран. Це залишає видиму смугу PDF під нею для орієнтації та дає можливість розмістити очевидну кнопку закриття, замість того щоб панель виглядала як повністю окремий екран.
Усередині модального вікна потрібно: поле пошуку, підключене до логіки з кроку 2; FlatList, який відображає рядки, отримані на кроці 3, налаштований за допомогою параметрів вікна — initialNumToRender, maxToRenderPerBatch, windowSize та removeClippedSubviews — щоб на екрані завжди відображалися лише рядки, розташовані близько до видимої області; а також стан порожнечі на випадок, якщо пошук нічого не повертає. Кожен рядок має мати два окремі елементи для натискання: натискання на тіло рядка вибирає відповідну сторінку та закриває панель, а натискання на маленьку іконку розгортання перемикає її дочірні елементи, щоб відкриття підрозділу не спричиняло випадкової навігації. Обгорніть кожен компонент рядка у memo, щоб розгортання одного підрозділу не змушувало переробляти відображення всього списку. Крім того, коли поле пошуку не порожнє, автоматично розгорніть будь-який підрозділ, який...
Крок 5: Створення екрана читача PDF
Саме на цьому екрані відбувається фактичне відтворення контенту за допомогою компонента Pdf. Використовуйте функцію обробки зміни сторінки, щоб відстежувати поточну сторінку та загальну кількість сторінок, відображати вбудований індикатор завантаження бібліотеки під час завантаження документа, а також підключіться до функції обробки помилок, щоб у разі невдачі екран показував зрозуміле повідомлення „Не вдалося відкрити цей документ“ із опцією повторної спроби — не дозволяйте індикатору завантаження крутитися безкінечно без альтернативи.
Кожного разу, коли ви переходите на сторінку з закладки, обмежте номер цільової сторінки так, щоб він знаходився у діапазоні від 1 до відомої загальної кількості сторінок. Це захищає від пошкодженої чи застарілої мети — чи то в самих даних закладки, чи то у результаті обробки внутрішнього посилання — яка намагається направити користувача на номер сторінки, якого просто не існує. Також варто увімкнути кешування PDF-джерела, щоб документ, який вже було завантажено, не завантажувався знову щоразу під час відображення цього екрана.
Що стосується візуального оформлення, то воно, звісно, залежатиме від власної системи дизайну вашого додатку — але з точки зору структури, для зручності використання важливо тримати висоту панелі близько 80%, надати зрозумілий спосіб її закриття, забезпечити кожному рядку (особливо його іконці розширення) зручну зону для натискання та створити спокійний, чітко визначений стан помилки.
Крок 6: Вилучення один раз, а не при кожному відображенні
Крок вилучення даних за допомогою pdf-lib, про який йшлося раніше, має виконуватися один раз, поза циклом відображення — а не всередині компонента, який перевідображається щоразу, коли користувач здійснює пошук чи розгортає певний елемент. Зберігайте отриманий JSON-файл у форматі закладок у кеші замість того, щоб постійно перераховувати його; у стані React зберігайте лише незначну кількість полів (id, назви, номери сторінок), уникайте зберігання сирих байтових даних PDF у стані та очищуйте всі тимчасові ресурси після того, як екран більше не використовується. Якщо ваш бекенд може виконувати цей крок вилучення замість вас, це зазвичай є більш ефективним підходом — тоді мобільний додаток потребуватиме лише отримання невеликого JSON-даних.
Крок 7: Збереження позиції користувача
Закриття панелі закладок чи поворот пристрою не повинні скидати читач на першу сторінку. Як мінімум, необхідно зберігати поточну сторінку, найнещодавніше розгорнуте гілку закладки та зсув прокрутки панелі. Зберігайте ці значення на рівні батьківського екрана чи стану навігації, а не всередині самого компонента панелі закладок — таким чином вони залишаться навіть у разі видалення панелі.
Крок 8: Гідне оброблення помилок
Плануйте можливі проблеми в реальних умовах, а не лише ідеальний сценарій: відсутні чи закінчені терміни дії URL-адрес файлів, пошкоджені чи захищені паролем документи, пристрої з малою кількістю вільного простору для зберігання, інтернет-з’єднання, які перериваються під час завантаження, закладки, що ведуть на недійсні сторінки, чи посилання, які просто не відкриваються. Що б не трапилося, уникайте ситуацій, коли користувач залишається з індикатором завантаження, який ніколи не закінчується. Якщо у документа немає закладок, скажіть про це прямо — щось на кшталт «У цьому документі немає закладок». Якщо пошук нічого не повертає, покажіть «Закладки не знайдено». Чітко визначений стан порожнечі чи помилки набагато кращий, ніж екран, який просто виглядає так, ніби застряг.
Крок 9: Тестування реальних сценаріїв збою
Не перевіряйте свою реалізацію лише на одному зручному PDF-файлі. Розширте тестування, враховуючи наступні аспекти:
- Документи: файли без жодних закладок, файли з сотнями сторінок, глибоко вкладені ієрархії закладок, великі вбудовані зображення, захист паролем, навмисне пошкодження даних, внутрішні посилання, зовнішні посилання та заголовки, що містять спеціальні символи
- Навігація: переход до закладок кореневого рівня та глибоко вкладених закладок, на першу та останню сторінку, на номер сторінки поза діапазоном, повторне відкриття панелі після її закриття, пошук вкладеної запису та пошук, який не дає результатів
- Пристрої: телефон Android із низькою кількістю пам’яті, сучасний флагманський пристрій, як iPhone, так і iPad, режими вертикального та горизонтального форматування, а також переходи між фоновим та переднім планом
Тут тестування на фізичному обладнанні має більше значення, ніж під час роботи над більшістю функцій — емулятор просто не зможе відтворити тиск на пам’ять та обмеження щодо зберігання, які спостерігаються на справжньому бюджетному пристрої.
Часто ставлені запитання
Чому просто не відображати кожну сторінку у вигляді зображення? Нативний двигун відображення малює лише той контент, який наразі потрібен на екрані, замість того щоб змушувати React керувати окремим компонентом зображення для кожної з тисячі сторінок.
Чому використовувати pdf-lib замість даних закладок, які вже надає react-native-pdf? Тому що ці вбудовані дані залишаються непропрацьованими, поводяться неоднаково на різних платформах (у Windows вони зовсім не повертаються), і не пропонують надійного способу визначення місцезнаходження внутрішніх посилань. pdf-lib аналізує структуру огляду та приміток посилань PDF безпосередньо, за один прохід, надаючи чисті та передбачувані дані, якими ви повністю керуєте.
Чи можлива пошук повного тексту всередині самого документа? Наразі це неможливо за допомогою суто відкритих інструментів — ця функція наразі доступна лише у комерційних SDK. Описана тут техніка дозволяє шукати за назвами закладок, що є більш обмеженою проблемою, яку можна повністю вирішити самостійно.
Чи повинна кожна гілка в дереві закладок розгортатися автоматично? Ні — необхідно розгортати лише ту гілку, яку користувач активно відкриває. На пристроях із обмеженою пам’яттю доцільно обмежити інтерфейс так, щоб одночасно могла бути розгорнута лише одна гілка на рівні кореня.
Чи гарантує щось із цього, що додаток ніколи не зависне? Жоден підхід не може дати таку гарантію для всіх можливих комбінацій документів та пристроїв. Мета — зменшити ризик: уникати передчасної обробки даних, зберігати мінімум інформації в пам’яті, отримувати дані лише один раз, віртуалізувати список закладок та прямо обробляти кожен можливий варіант збою.
Заключні міркування
Правильна оптимізація продуктивності — це не просто вибір однієї бібліотеки: react-native-pdf відповідає за відображення, pdf-lib забезпечує надійну інформацію про закладки та посилання, а справжня стабільність залежить від того, як ви керуєте станом, віртуалізацією та обробкою помилок у поєднанні з цими інструментами.
Практичні правила, яких слід дотримуватися:
- Виконуйте вилучення закладок та посилань один раз за допомогою
pdf-lib, ніколи не як частину процесу відображення. - Ніколи не відображайте всю структуру закладок одночасно — спростуйте її та показуйте лише розгорнуті частини.
- Використовуйте віртуалізований
FlatListдля зберігання списку закладок. - Зовсім не включайте сирі байтові дані PDF у стан React.
- Перевіряйте місце розташування сторінки перед тим, як переходити до неї.
- Зберігайте лише мінімальний стан, необхідний для відновлення позиції читача.