Tworzenie czytnika PDF odpornego na błędy do dużych dokumentów w React Native
Przyswoj architekturę do renderowania ogromnych PDF-ów liczących tysiące stron w React Native, wykorzystującą opóźnione drzewka zakładek, funkcję wyszukiwania oraz stabilną nawigację na urządzeniach o niskich specyfikacjach.
Źródła: Rendering and Navigating Large PDFs in React Native Without Crashing the App
Czteropagowy plik PDF można z łatwością wyświetlić na ekranie w aplikacji React Native. Dokument składający się z setek lub tysięcy stron, zawierający głęboko zagnieżdżone zakładki oraz łącza między poszczególnymi sekcjami stanowi zupełnie inny wyzwanie – może szybko stać się powolny, nieodpowiedni na interakcje lub wręcz podatny na awarie na mniej wydajnym sprzęcie. Ten przewodnik przedstawia architekturę, która bezpośrednio adresuje to wyzwanie: efektywne renderowanie dużych plików PDF, szybkie przechodzenie do dowolnych stron, przeglądanie i wyszukiwanie zagnieżdżonych drzew zakładek oraz utrzymanie stabilności całego doświadczenia nawet na urządzeniach o ograniczonych możliwościach.
W jednym z testów użyto pliku PDF liczącego ponad 1 900 stron oraz drzewa zakładek z wieloma poziomami zagnieżdżenia. Próba rozszerzenia całego tego drzewa jednocześnie powodowała poważne obciążenie pamięci. Poniższe sekcje opisują podejście, które rozwiązało te problemy.
Czym react-native-pdf się zajmuje — i czym nie
Zanim zdecydujesz się na dodatkowe biblioteki, przydatne jest dokładne poznanie granic wbudowanych możliwości react-native-pdf, ponieważ to one określają dalszy kierunek projektowania:
- Dzięki swojemu callbackowi
onLoadCompletefaktycznie udostępnia surowe informacje o zakładkach (przez parametrtableContents) na iOS i Android — ale nie na Windows, a z pewnością nie w formie, którą można by bezpośrednio wyświetlić na ekranie. Są to surowe, niestrukтуrowane dane, które nadal wymagają przetwarzania i renderowania po stronie użytkownika. - Faktycznie oferuje uniwersalny callback do obsługi kliknięć w linki (
onPressLink), ale ten callback nie potrafi odróżnić skoku wewnątrz dokumentu od zewnętrznego adresu URL, a także nie dostarcza w sposób pewny numeru strony docelowej dla linków wewnętrznych.
Biorąc to pod uwagę, praktycznym sposobem na uzyskanie niezawodnych danych o zakładkach i linkach między platformami jest pobieranie ich niezależnie, zamiast polegać na surowym wyniku funkcji zwrotnej renderera — dokładnie tę rolę pełni pdf-lib, o którym będzie mowa dalej. Ta architektura nie próbuje wyszukiwać tekstu w samym treści PDF; zajmuje się natomiast wyszukiwaniem tytułów zakładek, co jest znacznie prostszym zadaniem, które można w pełni zrealizować za pomocą narzędzi open-source.
Wybór pakietów
Ustawienie to opiera się na trzech pakietach, z których każdy jest odpowiedzialny za jedno konkretne zadanie:
react-native-pdf obsługuje wyświetlanie poprzez używanie wbudowanego silnika PDF platformy zamiast konwertowania stron na obrazy oparte na JavaScript — to właśnie ten natywny podejście zapewnia umiarkowane zużycie pamięci przy pracy z dużymi plikami. Jest odpowiedzialny za pokazywanie stron, obsługę powiększania, wywoływanie zdarzeń zmiany strony oraz umożliwienie programowego przechodzenia do konkretnej strony.
react-native-blob-util to zależność, od której sam react-native-pdf polega w celu uzyskania dostępu do plików — zajmuje się pobieraniem, przechowywaniem w pamięci cache oraz odczytywaniem wyświetlanego PDF. Nie jest to dodatek opcjonalny; stanowi wymóg wbudowany w sposób działania react-native-pdf na poziomie interno.
pdf-lib eliminuje wskazaną wcześniej lukę. Zamiast renderować cokolwiek, umożliwia bezpośrednie czytanie i modyfikowanie wewnętrznej struktury PDF: katalogu dokumentów, drzewa stron oraz — co jest kluczowe w tym przypadku — słownika struktury, w którym faktycznie znajdują się zakładki wewnątrz PDF, a także obiektów adnotacji linków przypisanych do poszczególnych stron, gdzie przechowywane są zarówno linki wewnętrzne, jak i zewnętrzne. Ponieważ react-native-pdf nie prezentuje żadnych z tych elementów w sposób uporządkowany i wiarygodny, pdf-lib służy do jednorazowego przeszukania tej wewnętrznej struktury i utworzenia czystej reprezentacji w formacie JSON drzewa zakładek — tytułu, strony docelowej oraz zagnieżdżonych elementów — wraz ze zestawem rozwiązanych destynacji linków, wszystko to niezależnie od specyficznej dla platformy zachowania natywnego renderera. Ten krok wydobywania danych powinien zostać wykonany tylko raz rath
Uwaga dotycząca Expo:
react-native-pdfzależy od modułów natywnych, więc nie będzie działać w standardowym kliencie Expo Go. Wymagane jest własne środowisko rozwojowe lub projekt typu bare/native.
Architektura
System jest zorganizowany w trzy warstwy, z których każda jest odseparowana od wewnętrznego funkcjonowania pozostałych:
- Przeglądarka PDF – śledzi aktualną stronę, łączną liczbę stron, stan ładowania oraz ewentualne błędy.
- Pasek narzędzi czytelnika – pokazuje informacje o aktualnej stronie i umożliwia dostęp do panelu zakładek.
- Panel zakładek – zarządza polem wyszukiwania, rozbudowalnym drzewem zakładek (tworzonym na podstawie danych wydobytych za pomocą
pdf-lib) i przekazuje jedną informację: którą stronę wybrał użytkownik.
Paneel zakładek nie ma pojęcia o tym, w jaki sposób sam plik PDF jest renderowany, a przeglądarka PDF nie wie, jak implementowana jest funkcja wyszukiwania zakładek. Ten jednokierunkowy proces — panel wybiera stronę, a ekran poleca przeglądarce przejście na nią — sprawia, że działania takie jak wpisywanie tekstu do pola wyszukiwania lub rozszerzanie węzła drzewa nie powodują kosztownego ponownego renderowania samego komponentu PDF.
Krok 1: Ustrukturyzowanie danych zakładek
Każdy zakładka, z którym pracujesz, wymaga trzech elementów: tytułu, strony docelowej oraz opcjonalnego zbioru podzakładek — prostej struktury rekurencyjnej, która naturalnie odpowiada temu, co zwraca krok wydobywania danych z pdf-lib. Najważniejszą kwestią jest to, że każda zakładka musi mieć stabilny, unikalny identyfikator, a tym identyfikatorem nie może być po prostu jej pozycja w zbiorze. Gdy użytkownik zaczyna wpisywać tekst w polu wyszukiwania lub zmienia stan otwartych/zamkniętych gałęzi, pozycja danej zakładki na wyświetlanej liście ulega zmianie. Jeśli polegasz na indeksie w zbiorze jako identyfikatorze, React traci możliwość odróżnienia jednego wiersza od drugiego, stan rozszerzania/zamykania zostaje przypisany niewłaściwemu węzłowi, a FlatList zaczyna błędnie recyklingować wiersze.
Krok 2: Wyszukiwanie bez utraty kontekstu
Ponieważ pasujący zakładka jest często umieszczany na kilku poziomach wewnątrz drzewa, logika wyszukiwania musi przeszukiwać strukturę rekurencyjnie, a nie tylko tytuły najwyższego poziomu. Równie ważne jest to, że gdy dowolny węzeł potomny odpowiada terminowi wyszukiwania, należy uwzględnić również jego węzeł nadrzędny w wynikach, nawet jeśli sam tytuł tego węzła nadrzędnego nie odpowiada niczemu. Jeśli to pominąć, wynik znajdujący się głęboko w strukturze pojawi się bez żadnej informacji o tym, gdzie dokładnie znajduje się w dokumencie.
Aby zachować dobrą responsywność, otocz logikę filtrowania hookiem memoizacji, dzięki czemu będzie ona uruchamiana ponownie tylko wtedy, gdy zmieni się ciąg wyszukiwania lub dane zakładki. W przypadku drzew z dużą liczbą wpisów dodaj również mechanizm debouncingu do pola tekstowego, aby uniknąć ponownego filtrowania całej struktury przy każdym naciśnięciu klawisza.
Krok 3: Spłaszczenie tylko widocznych węzłów
FlatList jest odpowiednim narzędziem ze względu na wydajność, ale wymaga prostego tablicy, a nie zagnieżdżonej struktury drzewiastej. Dlatego potrzebny jest proces przeglądania drzewa zakładek i tworzenia spłaszczonej tablicy zawierającej jedynie węzły, które powinny być obecnie widoczne — dzieci węzła są uwzględniane tylko wtedy, gdy dany węzeł jest obecnie rozwinięty. Każda spłaszczona pozycja powinna również zawierać wartość depth, aby można było określić, o ile należy ją wizualnie wciągnąć.
To kroku można uznać za najważniejszą decyzję dotyczącą wydajności w całej implementacji. Gdybyś spłaszczył cały drzewo i po prostu ukrył wiersze należące do złożonych gałęzi, FlatList nadal przechowywałby tysiące wierszy, które nigdy nie pojawiają się na ekranie. Dzięki spłaszczaniu jedynie tych elementów, które są obecnie rozwinięte, drzewo zakładek z tysiącami pozycji może w danym momencie przekazać FlatList zaledwie kilkadziesiąt wierszy.
Krok 4: Stworzenie panelu zakładek
Zaimplementuj panel jako modalne okno umieszczone nad przeglądarką PDF, a nie jako ekran, który ją zastępuje, oraz ogranicz jego wysokość do około 80% rozdzielczości ekranu, zamiast pozwalać mu zajmować cały ekran. Dzięki temu poniżej panela pozostaje widoczny fragment PDF, który ułatwia orientację, a także jest miejsce na wyraźną кнопkę zamknięcia, zamiast sprawiać wrażenie zupełnie oddzielnego ekranu.
Wewnątrz modalu powinno znajdować się: pole wyszukiwania połączone z logiką z kroku 2; FlatList wyświetlający uporządkowane wiersze utworzone w kroku 3, skonfigurowany z atrybutami okna — initialNumToRender, maxToRenderPerBatch, windowSize oraz removeClippedSubviews — tak aby renderowano tylko te wiersze znajdujące się w obszarze widoczności; oraz stan domyślny dla przypadku, gdy wyszukiwanie nie przynosi żadnych wyników. Każdy wiersz powinien mieć dwa oddzielne elementy do kliknięcia: kliknięcie treści wiersza wybiera daną stronę i zamyka panel, natomiast kliknięcie małej ikony rozszerzenia przełącza jej zawartość, dzięki czemu otwieranie gałęzi nie powoduje przypadkowego przenoszenia między stronami. Każdy komponent wiersza należy otoczyć tagiem memo, aby rozszerzanie jednej gałęzi nie zmuszało do ponownego renderowania całej listy. Ponadto, gdy pole wyszukiwania nie jest puste, automatycznie należy rozszerzyć każdą odpowiadającą mu gałąź.
Krok 5: Stworzenie ekranu czytnika PDF
Tutaj odbywa się faktyczne renderowanie za pomocą komponentu Pdf. Użyj funkcji callback przy zmianie strony, aby śledzić aktualną stronę i całkowitą liczbę stron, wyświetlać wbudowany wskaźnik ładowania biblioteki podczas wczytywania dokumentu oraz skonfiguruj funkcję callback przy błędzie, tak aby w przypadku niepowodzenia ekran pokazywał wyraźną wiadomość „nie udało się otworzyć tego dokumentu” wraz z opcją ponownej próby — nie pozwól, by ikona ładowania kręciła się bez końca bez żadnego rozwiązania awaryjnego.
Gdy tylko przejdziesz na stronę z zakładki, ogranicz numer strony docelowej tak, aby mieścił się w zakresie od 1 do znanej całkowitej liczby stron. Dzięki temu unikniesz sytuacji, gdy uszkodzony lub przestarzały adres — czy to pochodzący z samych danych zakładki, czy z rozwiązanej wewnętrznej linki — spróbuje skierować użytkownika na numer strony, który po prostu nie istnieje. Warto również włączyć cacheowanie pliku PDF, aby dokument już pobraany nie był pobierany na nowo za każdym razem, gdy otwiera się ta strona.
Jeśli chodzi o styl wizualny, zależy on oczywiście od własnego systemu projektowego twojej aplikacji — ale pod względem strukturalnym kluczowe dla użyteczności są: utrzymanie wysokości panelu na poziomie około 80%, dostarczenie oczywistego sposobu na jego zamknięcie, zapewnienie każdej wierszowi (zwłaszcza ikonie rozszerzenia) odpowiednio dużego obszaru do kliknięcia oraz stworzenie spokojnego, wyraźnie rozpoznawalnego stanu błędu.
Krok 6: Wyodrębnianie tylko raz, a nie przy każdym renderowaniu
Krok wyodrębniania za pomocą pdf-lib, o którym mówiono wcześniej, powinien zostać wykonyany dokładnie jednorazowo, poza cyklem renderowania — a nie wewnątrz komponentu, który jest ponownie renderowany za każdym razem, gdy użytkownik wykonuje wyszukiwanie lub rozwija węzeł. Zamiast obliczać ten plik JSON wielokrotnie, należy go zapisać do pamięci podręcznej, przechowywać w stanie React tylko lżejsze pola (identyfikatory, tytuły, numery stron), unikać przechowywania surowych danych w formacie PDF w stanie oraz usuwać wszelkie tymczasowe zasoby po rozmontowaniu ekranu. Jeśli Twój backend może wykonać ten krok wyodrębniania, jest to zazwyczaj lepsze rozwiązanie — aplikacja mobilna będzie wtedy musiała otrzymać jedynie niewielki plik JSON.
Krok 7: Zachowanie pozycji użytkownika
Zamknięcie panelu zakładek lub obrót urządzenia nie powinno przywracać czytnika na pierwszą stronę. Przynajmniej należy zachować aktualną stronę, najnowszy rozwinięty gałąź zakładki oraz pozycję przewijania panelu. Zachowuj te wartości na poziomie głównego ekranu lub stanu nawigacji, a nie wewnątrz samego komponentu panelu zakładek — w ten sposób pozostaną dostępne nawet w przypadku usunięcia panelu.
Krok 8: Grzeczne radzenie sobie z błędami
Planuj na rzeczywiste awarie, a nie tylko na pomyślny przebieg: URL plików, które są brakujące lub wygasłe, dokumenty uszkodzone lub zabezpieczone hasłem, urządzenia z małą ilością miejsca na przechowywanie, połączenia sieciowe, które przerywają się w trakcie pobierania, zakładki wskazujące na nieważne strony lub linki, które po prostu się nie otwierają. Bez względu na to, co się stanie, unikaj sytuacji, gdy użytkownik musi patrzeć na ikonę spinacza, która nigdy się nie zakończy. Jeśli dokument nie zawiera zakładek, powiedz o tym wprost — na przykład: „Ten dokument nie zawiera zakładek”. Jeśli wyszukiwanie nic nie zwraca, pokaż komunikat „Nie znaleziono zakładek”. Dobrze zdefiniowany stan pusty lub błędowy jest o wiele lepszy niż ekran, który wygląda tak, jakby utknął.
Krok 9: Testowanie rzeczywistych scenariuszy awarii
Nie weryfikuj swojej implementacji na podstawie jednego wygodnego pliku PDF. Rozszerz testy o następujące aspekty:
- Dokumenty: pliki bez żadnych zakładek, pliki liczące setki stron, głęboko zagnieżdżone hierarchie zakładek, duże wmontowane obrazy, ochrona hasłem, celowe uszkodzenie danych, linki wewnętrzne, linki zewnętrzne oraz tytuły zawierające specjalne znaki
- Nawigacja: przechodzenie do zakładek na poziomie korzenia lub głęboko zagnieżdżonych, do pierwszej i ostatniej strony, do numeru strony poza zakresem, ponowne otwieranie panelu po jego zamknięciu, wyszukiwanie elementu zagnieżdżonego oraz wyszukiwanie, które nie daje żadnych wyników
- Urządzenia: telefon Android z małą ilością pamięci, nowoczesne urządzenie flagowe, zarówno iPhone, jak i iPad, tryb pionowy i poziomy oraz przejścia między tłem a pierwszym planem
Tutaj testowanie na rzeczywistym sprzęcie ma większe znaczenie niż przy większości zadań związanych z funkcjonalnościami — emulator po prostu nie będzie w stanie odtworzyć presji na pamięć i ograniczeń przechowywania, które występują w rzeczywistym urządzeniu budżetowym.
Często zadawane pytania
Dlaczego po prostu nie renderować każdej strony jako obrazka? Natywny silnik renderowania wyświetla tylko treść aktualnie potrzebną na ekranie, zamiast zmuszać React do zarządzania oddzielnym komponentem obrazka dla każdej z tysiąca stron.
Dlaczego używać pdf-lib zamiast danych zakładek dostarczanych już przez react-native-pdf? Ponieważ te wbudowane dane są nieprzetworzone, zachowują się niespójnie na różnych platformach (Windows w ogóle nic nie zwraca) i nie oferują wiarygodnego sposobu na rozwiązywanie problemów z adresami linków wewnętrznych. pdf-lib analizuje bezpośrednio strukturę zarysu oraz adnotacji linków w pliku PDF, w jednej operacji, dostarczając czyste, przewidywalne dane, nad którymi masz pełną kontrolę.
Czy możliwa jest wyszukiwanie pełnego tekstu w samym dokumencie? Na razie nie przy użyciu wyłącznie narzędzi open source – ta funkcja jest obecnie przeznaczona dla komercyjnych SDK. Opisana tutaj metoda obejmuje wyszukiwanie według tytułów zakładek, co jest bardziej ograniczonym problemem, który możesz w pełni rozwiązać samodzielnie.
Czy każda gałąź w drzewie zakładek powinna się automatycznie rozwijać? Nie — należy rozwijać tylko tę gałąź, którą użytkownik aktywnie otwiera. Na urządzeniach o ograniczonej pamięci warto ograniczyć interfejs tak, aby w danym momencie mogła być rozwinięta tylko jedna gałąź na poziomie korzenia.
Czy cokolwiek z tego gwarantuje, że aplikacja nigdy się nie zawali? Żaden podejście nie może zagwarantować tego w przypadku każdej możliwej kombinacji dokumentu i urządzenia. Celem jest zmniejszenie ryzyka: unikanie szybkiego renderowania, utrzymywanie minimalnego stanu przechowywanego, pobieranie danych tylko raz, wirtualizacja listy zakładek oraz wyraźne radzenie sobie z każdym modelem awarii.
Ostateczne uwagi
Osiągnięcie odpowiedniej wydajności nie polega na wyborze jednej biblioteki — react-native-pdf zajmuje się renderowaniem, pdf-lib dostarcza wiarygodnych informacji o zakładkach i linkach, a prawdziwa stabilność pochodzi od sposobu zarządzania stanem, wirtualizacją oraz obsługą błędów w związku z obiema tymi bibliotekami.
Praktyczne zasady, których należy przestrzegać:
- Zrealizuj wyodrębnianie zakładek i linków raz za pomocą
pdf-lib, nigdy jako część procesu renderowania. - Nigdy nie renderuj całego drzewa zakładek jednocześnie — spłaszcz i wyświetl tylko rozszerzone części.
- Zastosuj wirtualizowany
FlatListdo przechowywania listy zakładek. - Całkowicie unikaj przechowywania surowych danych w postaci bajtów PDF w stanie React.
- Zwaliduj cel strony przed przechodzeniem do niej.
- Zachowaj tylko minimalny stan potrzebny do przywrócenia pozycji czytelnika.
Literatura pokrewna
- Nitro Modules: Jak Static Bindings przewyższają React Native TurboModules — Wyjaśnia, w jaki sposób Nitro Modules wykorzystują uprzednio skompilowane static bindings zamiast dynamicznych wyszukiwań, dzięki czemu znacznie przewyższają TurboModules i Expo Modules w React Native.