Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: printing with react-pdf #1924

Merged
merged 61 commits into from
Feb 13, 2022
Merged

Conversation

carlobeltrame
Copy link
Member

@carlobeltrame carlobeltrame commented Sep 13, 2021

Refs #1241
Fixes #2023

Ich bin bei der tausendsten Suche nach custom PDF renderers für Vue 3 auf react-pdf gestossen. Das ist quasi ein Modul für React-Apps, das es erlaubt einen Komponentenbaum mit PDF als Zielformat statt HTML zu rendern. Leider gibt es für Vue bisher kein auch nur ansatzweise ähnlich ausgereiftes Package, wohl unter anderem weil Custom Renderers erst seit Vue 3 möglich sind.

Eine separate React-App analog unserem Nuxt Print Preview Service wäre natürlich denkbar. Aber ich wollte mal sehen wie weit ich komme wenn ich eine inline Vorschau möchte (#microfrontends 😆).

Um react-pdf in unserem Frontend nutzbar zu machen habe ich hier eine solche React-Komponente in eine Web Component verpackt. Web Components ist ein eher neuer Web-Standard. Es ist ein Komponentensystem (wie Vue-Komponenten oder React-Komponenten) das aber direkt nativ von Browsern untersützt wird. Web Components erlauben es, Custom HTML Elemente zu definieren und zu nutzen. Wenn die PDF-Preview-React-Komponente erst mal als solches Custom HTML Element verfügbar ist, kann man sie in Vue ganz normal nutzen, wie jedes andere HTML-Element: <ecamp3-print-preview />
Um react-pdf in unserem Frontend nutzbar zu machen, habe ich eine solche React-Komponente in eine Vue-Komponente gewrappt, welche die "Übersetzung" macht. Das war gar nicht mal so schwierig, abgeschaut habe ich bei https://github.com/alkin/vue-react/blob/master/src/vue-react.js
Um react-pdf in unserem Frontend nutzbar zu machen habe ich zwei simple Vue-Komponenten für Print Preview und Print Download Button geschrieben, die dann React verwenden um ein PDF zu rendern. React wird nie verwendet um HTML zu rendern, nur PDF. Ausserdem kann man in diesen beiden Komponenten separat via Boolean umschalten, ob das PDF im Main Thread generiert werden soll (gut für Debugging) oder in einem separaten Thread in einem Web Worker (gut für mittlere bis grosse PDFs und für Produktion).

Das Ganze funktioniert meiner Meinung nach erstaunlich gut. Inline PDF Preview und PDF Download Link kommen beides mit react-pdf mit, die habe ich einfach nach Vue portiert und noch etwas vereinfacht. Alle Anforderungen die wir in #479 (comment) festgehalten haben werden abgedeckt (CSS Grid Layout ist nicht unterstützt, aber für Tabellen gibts eine Erweiterung, und den Picasso habe ich mit Flexboxen und Absolute Positioning gemacht, welche beide sehr gut unterstützt sind).

Screenshot 2021-10-29 at 18-16-32 eCamp3

react-pdf.mp4

Pros

  • Sehr viel schneller als puppeteer / weasy (~5 Sekunden um ein mittelgrosses PDF zu generieren wenn die Daten aus der API schon da sind)
  • Komplett clientseitig, daher kein Workload, keine Storage und keine Job Worker Container auf Server mehr nötig
  • Nötige Daten aus der API sind bereits beim User auf dem Gerät (aber teilweise könnte Refresh vor dem Drucken vielleicht Sinn machen)
  • Authentifizierung kein Problem mehr
  • ~hal-json-vuex und Translations können direkt auch in React verwendet werden, dafür gebe ich sie einfach an React weiter. Ist ja sowieso nur Lesezugriff benötigt.~~ das geht im Web Worker leider nicht mehr so einfach. Für den Moment habe ich kleine Dummy-Implementationen geschrieben die read-only auf den rohen Vuex- und Übersetzungs-Daten arbeiten können. Ist ein offener Punkt für die Zukunft.
  • Komponenten sind einigermassen angenehm um damit zu arbeiten. Sicher besser als TCPDF, da deklarativ.
  • Komponenten-Prinzip passt gut zu ContentNode-Baumstrukturen
  • Kein Kampf mit paged.js Browser-Spezifika, konsistentes Seitenlayout für alle User ist simpel zu erreichen
  • Unterstützt fortlaufende Seitennummern, unterschiedliche Header/Footer auf allen Seiten, Links innerhalb des PDFs sowie Wechsel zwischen Hoch- und Querformat ohne Probleme
  • Für fortgeschrittene Layout-Themen und Grafiken sind sogar (Subsets von?) Canvas und SVG unterstützt
  • Kein Deaktivieren von CSS Sanitization in einem echten Browser, Styles werden in JS in einem CSS-ähnlichen Format spezifiziert
  • Worttrennungsregeln sind pro Sprache konfigurierbar

Cons

  • Custom Font Support ist etwas buggy
    • Aktuell nur TTF supported obwohl Tooling eigentlich WOFF2 könnte (?)
    • Race conditions beim Fonts registrieren und benutzen, Workaround gefunden
  • Memory Konsum auf Endnutzergeräten, insbesondere Handys noch unklar. Es gibt Issues die von Memory Leaks berichten wenn wiederholt viele PDFs generiert werden. Ich vermute aber dass das Memory Leak in der PDF-Vorschau-Komponente von react-pdf liegt. Somit könnte es sein dass wir das Problem nicht haben.
  • Mix aus Technologien (Vue, Web Components, React), weitere Technologien zu lernen für Einsteiger. Wäre bei Weasy und ein Stück weit bei paged.js aber auch so
  • Grössere Bundle-Size, hier sollten wir denke ich unbedingt mit asynchron nachgeladenen Chunks arbeiten der Web Worker wird bereits erst on demand geladen
  • Externes PDF einbetten geht glaube ich nicht weil eines der zugrundeliegenden Tools das nicht unterstützt (haben wir aber fürs MVP nicht vorgesehen). Müsste man dann vielleicht doch mit einem externen Service oder noch einer anderen Library lösen.
  • Kalender und andere Vuetify-Komponenten können nicht verwendet werden (Kalender müssen wir aber vermutlich sowieso neu machen)
  • Live reloading während der Entwicklung funktioniert nicht, muss jeweils mit F5 neu laden. Machts aber bei vielen Arten von Änderungen schon automatisch.

@carlobeltrame carlobeltrame added the Meeting Discuss Am nächsten Core-Meeting besprechen label Sep 13, 2021
@carlobeltrame carlobeltrame mentioned this pull request Sep 13, 2021
54 tasks
@BacLuc
Copy link
Contributor

BacLuc commented Sep 13, 2021

Cool, react ist schon geil. Und Web Components kommen auch immer mehr, toll dass es teilweise interop zwischen den Frameworks ermöglicht.
Hast du das an einem anderen Ort schon eingesetzt?

@manuelmeister
Copy link
Member

Ich habe react + angular + storybook schon zusammen verwendet. Das klappt mehrheitlich.

@carlobeltrame
Copy link
Member Author

Ich habe hier zum ersten Mal etwas "echtes" mit React und mit Web Components gemacht. Kombiniert sowieso zum ersten Mal. Habe auch nicht die ganze Komplexität von React verwendet denke ich, aber ging mit Vorwissen von Vue recht schnell. Und JSX ist cooler als ich gedacht hätte als ich mich mal drauf eingelassen habe.

@manuelmeister
Copy link
Member

manuelmeister commented Oct 3, 2021

Meeting Discussion

@manuelmeister manuelmeister removed the Meeting Discuss Am nächsten Core-Meeting besprechen label Oct 3, 2021
@carlobeltrame carlobeltrame force-pushed the poc-react-pdf branch 3 times, most recently from 639d5ea to aab62e4 Compare October 10, 2021 23:34
@carlobeltrame carlobeltrame added the deploy! Creates a feature branch deployment for this PR label Jan 18, 2022
@manuelmeister manuelmeister added Meeting Discuss Am nächsten Core-Meeting besprechen and removed Meeting Discuss Am nächsten Core-Meeting besprechen labels Jan 18, 2022
@BacLuc BacLuc removed the deploy! Creates a feature branch deployment for this PR label Jan 25, 2022
@carlobeltrame
Copy link
Member Author

carlobeltrame commented Feb 9, 2022

PR ist grundsätzlich ready zum reviewen und mergen. Ich räume über die nächsten Tage noch weiter auf, solange es noch nicht gemerged ist. Aber zurückhalten müsst ihr euch trotzdem nicht, ist nicht gerade klein zu rebasen ;) Wenn gemerged eröffne ich dann einen neuen PR mit weiteren Verbesserungen.

Aktuell ist die Print Preview auf dem Print-Tab implementiert, sowie der "Generate PDF" Button auf dem Grobprogramm. Beides läuft in einem Web Worker, kann aber für Entwicklung via Boolean in LocalPrintPreview.vue und LocalPDFDownloadButton.vue zurück auf Main Thread geschaltet werden.

@usu ein Tipp: git rebase --rebase-merges -i <some-commit-hash> hat mir sehr geholfen beim Entfernen von zwischenzeitlichen Merge Commits, ohne dass ich alle conflict resolution die nichts mit mir zu tun hatte erneut anwenden musste.

Copy link
Member

@usu usu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Output ist bei mir im Moment aber nur "Hello world! I was rendered in a background worker...". By purpose oder noch was schief?

api/src/Entity/Day.php Show resolved Hide resolved
frontend/src/components/activity/content/ColumnLayout.vue Outdated Show resolved Hide resolved
@@ -6,6 +6,7 @@
group="contentNodes"
class="draggable-area d-flex flex-column pb-10"
:class="{ 'min-height': layoutMode }"
:invert-swap="true"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hab die Docu gelesen und verstehe immer noch nicht, was das macht? 😅
Was macht das genau?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bei mir hats auch erst bei dieser Erklärung mit Animation geklickt: https://github.com/SortableJS/Sortable/wiki/Swap-Thresholds-and-Direction

Beispiel das mich zu dieser Änderung bewegt hat: Sagen wir ich habe ein zweispaltiges ColumnLayout, in der linken Spalte ist ein grosses SiKo und die rechte Spalte ist leer. Unter dem ColumnLayout ist ein grosses Storyboard. Sowohl ColumnLayout wie auch Storyboard sind alleine schon höher als der Screen. Wenn ich das Storyboard in die rechte Spalte des ColumnLayouts draggen will, dann geht das nur mit invert-swap.
Ohne invert-swap ist die Drop Zone für "über dem ColumnLayout" bereits am unteren Rand des ColumnLayouts. Also noch bevor ich das Storyboard bis zur leeren Spalte gezogen habe, würde das Storyboard bereits komplett über dem ColumnLayout platziert.
Mit invert-swap ist die Drop Zone für "über dem ColumnLayout" am oberen Rand des ColumnLayouts. So kann ich das Storyboard problemlos in die verschachtelte Drop Zone für die rechte Spalte ziehen, ohne dass es vorschnell ganz über das ColumnLayout platziert wird.

Standardmässig ist die Drop Zone 50% der Höhe des Elements, also wenn ich vorhin "am oberen Rand" gesagt habe bedeutet es eigentlich "in der oberen Hälfte". Die Höhe der Drop Zone kann man mit swap-threshold einstellen, Standardwert ist 0.5 (50%). Das können wir dann in Zukunft mal noch optimieren.

Only works in Chromium-based browsers, not in Firefox. For Firefox
support, we need to add https://www.npmjs.com/package/vite-plugin-worker

Currently, i18n and hal-json-vuex don't work, because the worker thread
does not have access to the memory and objects of the main thread. So we
need to pass the translations and vuex store contents to the worker
thread and construct instances of vue-i18n and hal-json-vuex there.
Also reimplements the pdf download button and pdf preview components in
Vue. This means we do not have to "mix" and translate between Vue and
React anymore. React is now purely used for PDF rendering, never HTML.

Also prepares for easily switching between web worker and main thread in
development.
We don't need this dependency anymore, now that we don't use React for
HTML rendering anymore.
By avoiding all n+1 queries, we can improve the loading time from F5
until finished pdf preview in a full-fledged weekend camp from ~35 to
~18 seconds.
@carlobeltrame
Copy link
Member Author

Output ist bei mir im Moment aber nur "Hello world! I was rendered in a background worker...". By purpose oder noch was schief?

Ich habe jetzt noch Dummy-Implementationen für Vue-i18n und hal-json-vuex im Web Worker eingefügt, und somit kann wieder das volle PDF gerendert werden. Bei hal-json-vuex habe ich nur die read-only Features ohne Loading Handling etc. in 120 Zeilen re-implementiert. Für vue-i18n mache ich aktuell einfach ein lodash.get, also noch ohne Placeholder replacement. Beides noch sehr temporär.

@BacLuc bezüglich Performance, bei mir auf dem Laptop dauert das reine generieren des PDFs mit Web Worker 5-6 Sekunden, und das bei beiden Beispiel-Lagern der Dev-Daten. Vorher müssen aber natürlich viele Daten aus der API geladen werden. Ich habe da noch etwas optimiert, und von F5 bis fertige PDF-Preview (ohne dass Vite oder API Platform etwas neu kompilieren müssen) dauerts bei mir normalerweise um die 18 Sekunden für ein ca. 11-seitiges PDF, davon ca. 6 bis sich die Seite aufgebaut hat (Vuetify), 6-7 bis alle Daten aus der API geladen sind und eben 5-6 Sekunden fürs rendern des PDFs im Web Worker. Ich habe noch gelesen dass die Startup-Zeit von Web Workers auf Android recht schlecht ist, das müssen wir sicher noch im Auge behalten sobald das mal deployed ist.

Ich habe noch diverse technische Schulden und Problemchen aufgeschrieben, und werde die dann noch in #1241 festhalten um sie zu tracken.

@BacLuc
Copy link
Contributor

BacLuc commented Feb 13, 2022

In Firefox gibt es noch warnings, aber die beeinträchtigen die Funktionalität nicht:

This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills [498e4546.iife.js:171352:23](http://localhost:3000/node_modules/.vite/.bundle.iife/498e4546.iife.js)
This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills

@@ -29,18 +29,23 @@
:title="result.title"
class="mt-2" />
</div>
<local-print-preview :config="config"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Als hint für die, die das auch ausprobieren.
Das PDF Rendern wird automatisch gestartet, sobald man die Seite öffnet.
Im Firefox ist das Feedback für "PDF ist fertig gerendert" nicht so offensichtlich.

Im chrome sieht man dann das preview (aber nicht in den downloads), in firefox sieht man das pdf es nicht im preview, aber in den downloads

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bin nicht ganz sicher was du meinst. Die preview wird bei mir in firefox wie auch chromium im iframe angezeigt, wenn auch mit unterschiedlichen pdf viewers.

Ich glaube auf mobile hatte es das preview pdf bei mir auch mal heruntergeladen statt angezeigt, meinst du so etwas?

@carlobeltrame
Copy link
Member Author

In Firefox gibt es noch warnings, aber die beeinträchtigen die Funktionalität nicht

Ja, habe ich in #1241 auch so festgehalten. Müssten wir möglicherweise noch shimmen oder so. Die funktionen werden nicht gebraucht in unserem fall, aber React überprüft sofort beim Laden ob sie verfügbar sind.

@usu
Copy link
Member

usu commented Feb 13, 2022

Können wir mergen?

@carlobeltrame carlobeltrame merged commit a171195 into ecamp:devel Feb 13, 2022
@carlobeltrame carlobeltrame deleted the poc-react-pdf branch February 13, 2022 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

React PDF komplizierte Beispiele ausprobieren
4 participants