Bundles im Überblick

JavaScript und Styles der Komponenten werden zur Laufzeit zu wenigen Dateien zusammengefasst. Dahinter steht LigerShark.WebOptimizer und ein schmaler Aufsatz in FMH.Komponente.Infrastruktur, der die Beiträge der Pakete einsammelt.

Wie eine Komponente in ein Bundle kommt

Eine Komponente legt eine Klasse an, die IJavaScriptProvider oder IScssStyleProvider implementiert. Mehr ist nicht zu tun — insbesondere gibt es nichts in Program.cs einzutragen.

Beim Start läuft dann:

  1. Assemblies einsammeln. ReferencedAssemblyProvider liest den Abhängigkeitsgraphen der Anwendung aus der .deps.json — lokale Projektverweise ebenso wie NuGet-Pakete. Eine IL-Referenz auf die Klasse ist nicht nötig.
  2. Provider instanzieren. ReflectionProviderDiscovery sucht alle nicht-abstrakten Klassen, die die gesuchte Schnittstelle implementieren, und erzeugt sie mit Activator.CreateInstance.
  3. Gruppieren und sortieren. Alle Provider mit demselben BundleName bilden ein Bundle; innerhalb davon wird nach Order sortiert.
  4. Anmelden. Die Pfadliste geht an AddJavaScriptBundle bzw. AddScssBundle.

Der letzte Schritt ist der ganze Kern:

var groups = discovery.Discover<IJavaScriptProvider>()
    .GroupBy(provider => provider.BundleName)
    .Select(group => (Name: group.Key, Paths: group.OrderBy(provider => provider.Order)
        .SelectMany(provider => provider.GetJavaScriptPaths())
        .ToArray()))
    .Where(group => group.Paths.Length > 0);

foreach (var (name, paths) in groups)
{
    pipeline.AddJavaScriptBundle($"/bundle/{name}.js", paths);
}

Der Bundle-Name ist die Adresse. /bundle/{BundleName}.js für JavaScript, /bundle/{BundleName}.css für SCSS. Beachte den Singular: /bundle/, nicht /bundles/.

Zwei Konsequenzen aus dem Reflection-Ansatz

  • Öffentlicher, parameterloser Konstruktor ist Pflicht. Die Provider werden nicht über die Dependency Injection erzeugt; Konstruktor-Injektion funktioniert nicht.
  • Die Klasse wird gefunden, sobald das Paket referenziert ist. Wer eine Komponente einbindet, bekommt deren Assets automatisch mit. Umgekehrt heisst das: Eine versehentlich angelegte Provider-Klasse wirkt sofort, ohne dass sie irgendwo registriert wäre.

Aktivierung in der Hostanwendung

Drei Zeilen in Program.cs:

// Ohne das findet WebOptimizer die _content-Pfade der Razor Class Libraries nicht.
builder.WebHost.UseStaticWebAssets();

builder.Services.AddBundledAssets();

// Muss VOR UseStaticFiles stehen, sonst liefert der Static-File-Handler zuerst einen 404.
app.UseWebOptimizer();
app.UseStaticFiles();

AddBundledAssets() richtet nebenbei die JavaScript-Engine (V8) ein, die der Sass-Compiler braucht. In der Komponente selbst ist dafür nichts zu tun.

Die sechs Standard-Bundles

Konstante Adresse Eingebunden über
ScssBootstrapBundleName /bundle/fmh-bootstrap.css _HeadIncludes
ScssBundleName (Default "fmh") /bundle/fmh.css _HeadIncludes
JqueryBundleName /bundle/fmh-jquery.js _HeadIncludes
JqueryValidationBundleName /bundle/fmh-jquery-validate.js _HeadIncludes
FmhScriptBundleName /bundle/fmh-scripts.js _BodyIncludes
SiteSetupBundleName /bundle/fmh-sitesetup.js _BodyIncludes

_HeadIncludes und _BodyIncludes sind Partials der Basis-Komponente und werden im Layout über BasisAssetKonstanten.HeadIncludes bzw. .BodyIncludes gerendert. Sie kennen nur diese sechs Adressen — jeder andere Bundle-Name muss von Hand eingebunden werden.

Order: wer steht wo im Bundle

Order sortiert die Provider innerhalb eines Bundles; kleinere Werte kommen zuerst. Die Reihenfolge der Pfade innerhalb eines Providers ist die Reihenfolge im Array.

Aktuell belegte Werte:

Bundle Paket Order Pfade
fmh-scripts Basis (Popper, Bootstrap, DOMPurify, Sanitizer) 1 4
fmh-scripts Tabelle (DataTables) 100 3
fmh-scripts FileUpload (Dropzone) 110 1
fmh-scripts RichText (Quill) 120 1
fmh-sitesetup Basis 20 5
fmh-jquery Basis 0 2
fmh-jquery-validate Basis 10 14
fmh (CSS) RichText (quill.snow.css) 0 1
fmh (CSS) Basis 1 1
fmh (CSS) Tabelle 200 1
fmh (CSS) FileUpload 300 2
fmh-bootstrap (CSS) Basis 0 2

Nur fmh-scripts und fmh haben überhaupt mehrere Provider — dort entscheidet Order tatsächlich etwas.

Zwei Fälle im Repo zeigen beide Richtungen: Quill-JavaScript steht mit 120 hinter der Basis, weil es Bootstrap braucht. Das zugehörige quill.snow.css steht mit 0 vor der Basis, weil deren _richtext_override.scss das Grundlayout des Editors ans Corporate Design anpasst — käme es danach, wäre die Anpassung wirkungslos.

Warum das kein Detail ist

Aus BasisBundleFmhScriptSetup:

// Popper muss VOR Bootstrap geladen werden: Bootstrap 5 (UMD) liest window.Popper beim Laden
// ein. Ist Popper dann noch nicht definiert, fehlt createPopper und Tooltips/Dropdowns/Popover
// können nicht positioniert werden (das Tooltip landet am Body-Ende statt beim Element).

Ein solcher Fehler äussert sich nicht als Ausnahme, sondern als leicht falsch positioniertes Bedienelement — entsprechend lange sucht man danach.

Gleicher Order ist unbestimmt

Haben zwei Provider im selben Bundle denselben Order, entscheidet die Reihenfolge der Assemblies — und die ist nicht zugesichert. Deshalb Abstände lassen (Zehner- oder Hunderterschritte) statt fortlaufend zu nummerieren.

Was das Bundle nicht kann

Der Bundler minifiziert mit NUglify, und NUglify versteht import/export nicht. Native ES-Module gehören deshalb nicht in ein Bundle.

Dafür gibt es einen zweiten, nebenläufigen Weg mit derselben Denkweise: den Modulkatalog. Eine Komponente implementiert IModuleScriptProvider statt IJavaScriptProvider, wird von derselben Reflection-Suche gefunden und sortiert ebenfalls über Order. Statt einer zusammengesetzten Datei entsteht daraus eine verwaltete Adresse. Wie das geht, steht in «Native ES-Module einbinden».