Handling von Javascript-Abhängigkeiten in CMS?

Hallo zusammen,

mich würde mal interessieren, wie die unterschiedlichen Systeme Javascript-Abhängigkeiten verwalten. Bei Pone war es früher so, dass alles einfach in einer bestimmten Reihenfolge eingebunden und eventuell zu großen dateien zusammengemischt wurde. Seit Plone 5 wird für das ganze requirejs eingesetz, was sich um die Abhängigkeiten innerhalb eines Bundles kümmert. Das ist zwr etwas intelligenter, verursacht aber oft mehr Schmerzen als es nützlich ist, da es da draußen einfach zu viele unterschiedliche Javascript varianten gibt.

Wie macht Ihr das?
Wichtig ist und bleibt es, dass man über einzelne Plugins/Add-ons das System erweitern kann und diese Add-ons auch Javascript mit sich bringen können.

In Drupal deklarieren wir in unseren Addons sogenannte Libraries in einer project.libraries.yml. Dort werden Name, Version, Lizenz, Download Link, Include und anderes und auch die Abhängigkeiten zu anderen Libraries deklariert.

Ab Version 7 wird es bei uns wie folgt aussehen (im Moment managen wir das JavaScript händisch):

In Java-Projekten mit Maven ist das relativ simpel. Über das frontend-maven-plugin kann während des Builds NPM (oder Yarn) aufgerufen werden. Das Plugin installiert außerdem Node + NPM (oder Yarn) lokal für das Projekt, so dass auf einem Build-System Node & Co. nicht unbedingt installiert sein müssen.

Bei uns gibt es daher in jedem Modul das JavaScript enthält, zusätzlich zur POM eine package.json. Die Skripts werden gebundelt, und dann am Ende in die JAR des jeweiligen Moduls gepackt. Das JavaScript für jedes Modul soll dabei in sich geschlossen sein.

JavaScript-Resourcen, die von anderen Modulen verwendet werden sollen, liegen in separaten Maven-Modulen, die aber nur ein “Wrapper” für das NPM-Modul sind (das macht das Builden einfacher). Die mehrfach verwendbaren Module werden auch über NPM verfügbar sein. Ab Beta-Release Stand über das öffentliche Repository. Zusätzlich betrieben wir unsere eigene Package-Registry unter packages.libreccm.org. Dort läuft ein Nexus 3 Repository-Manager, auf den jede Nacht über unseren Jenkins die aktuellen Snapshots deployed werden. Nexus hat den Vorteil das man damit so ziemlich alles an Pakettypen abdecken kann, was es gibt (Maven, NPM, PyPi etc).

In Neos haben wir für die Backend UI ein Package neos-ui, welches als Mono-Repo fungiert. Wir nutzen dann lerna und yarn zur Verwaltung. Der Code ist dann weitestgehend react in TypeScript mit ES6 code.

Als build Tool haben wir Webpack 4 und die Abhängigkeiten sind dann halt einfach in den ganzen package.json Dateien. Lerna hilft dann dabei alles zu organisieren. Falls ich das noch nicht genug erläutert hab einfach nachfragen.

neos-ui:

lerna:

hi Markus, das funktioniert aber nur als Entwickler, also bei einem Framework, nicht so sehr bei einer durch Anwender erweiterbaren Anwendung wie Plone oder Typo3. Dort ist ja das Problem, dass man eine Hauptanwendung hat und diverse Extensions die sich da einklinken sollen. wenn jetzt extentionA und B beide von einer library abhängen muss die ja unabhängig davon verwaltet werden. Der Ansatz von Jens ist eine Lösung, aber diese bringt einiges an Komplexität in den Stack, weil man hier Javascript und andere Sprachen und deren Pakete mischt.

Dinge wie webpack &co funktionieren nur auf Projektbasis, was hier brauche ist eine Lösung für Extensions die sich ohne nen bundle abzuwerfen installieren lassen. Aber da dreht man sich im Kreis.

Eine Lösung ist, keine externen Abhängigkeiten zu haben. jede Extension bringt mit was es braucht. Man will natürlich nicht unbedingt das jeder sein react/angular oder vuejs mit bringt, aber Ansätze wie die von Svelte ermöglichen es hier schlanke Komponenten zu bauen, die keine Abhängigkeiten haben. Und auch die anderen Frameworks arbeiten in diese Richtung. Dann kann man wieder bundler innerhalb seiner Extension einsetzen, wenn man denn will/muss. Auch eine Mischung aus beidem ware denkbar. Plone verwendet aktuell eine liste von namen, von dingen die als gegen angesehen werden können und daher nicht installiert werden müssen. So ähnlich könnte man auch die großen wichtigen Frameworks bereitstellen und den rest in seiner Extension mit liefern. dann würde eine Extension mit vuejs nur den app code + eventuell dependencies bauen und erwarten das die vuejs-runtime vorhanden ist. aber da kommt man dann wieder in die Situation dass man die runtime in verschiedenen Versionen haben möchte.

wie wird das dann zusammen gebaut?

Unsere Module deklarieren Libraries in einer module.libraries.yml.
https://www.drupal.org/docs/creating-custom-modules/adding-stylesheets-css-and-javascript-js-to-a-drupal-module#library

Ist das Modul installiert, stehen die darin deklarierten Libraries zur Verfügung.

Drupal baut bei einem Request einen sogenannten Render-Array auf, der in abstrakter Syntax die komplette Seite enthält. Inklusive Cacheing-Informationen und benötigter Libraries. Wird die Seite dann tatsächlich gerendert, werden alle Libraries und deren Abhängigkeiten aufgelöst und eingebunden. Je nach Einstellung des Systems, wird dann noch minifiziert, oder halt nicht.
Wir haben dafür verschiedene Services implementiert, keine externen Anhängigkeiten.

Wie eine externe JS-Library allerdings in die Codebase kommt haben wir nicht klar geregelt - in das Repository auf drupal.org darf nur, was der GPL-2 unterliegt.
Üblicherweise werden sie aber über das Paketmanagement (composer, packagist, asset-packagist) eingebunden, oder es gibt eine CLI-Command, der den Download vollführt und die Library im Dateisystem entpackt.

1 Like

Eines der Herausforderungen ist ja, dass man die verschieden Abhängigkeiten von Add-on’s ja zusammen bringen möchte und das am besten ohne viele Doppellungen zu haben, nicht zu viele requests und Dinge nur dann und erst dann zu laden wenn sie verwenden werden.

Ein weg ist sicher das ganze in einem Projekt zu bundlen und zu optimieren, aber das wiederspricht einer einfachen deployment und addon-story und kann nicht vom System her gemacht werden, sondern liegt dann in der Hand des developers/admins/devops usw.

Ein interessanter Ansatz dies zur Laufzeit zu machen ist module federation: Module Federation | webpack