Die Oberfläche eines Plugins
Die Schale lädt Plugin-Oberflächen zur Laufzeit über Module Federation. Kein Neuausrollen, keine neue Programmversion. Ihr Remote wird geladen, wenn jemand Ihre Route oder Ihre Seite öffnet, und beim Abmelden wieder abgehängt.
Wenn Ihr Plugin keine eigenen Seiten hat, lassen Sie frontend im Manifest weg. Ein
Konnektor braucht keine Oberfläche: sein Einstellungsformular entsteht aus der Deklaration.
Zwei Modul-Kennungen, mehr nicht
Abschnitt betitelt „Zwei Modul-Kennungen, mehr nicht“Jede Plugin-Oberfläche stellt genau zwei Module bereit:
federation({ name: 'acme_report', filename: 'remoteEntry.js', exposes: { './register': './frontend/register.ts', './View': './frontend/View.tsx' }, shared: { react: { singleton: true, requiredVersion: false }, 'react-dom': { singleton: true, requiredVersion: false }, '@prodlog/ui': { singleton: true, requiredVersion: false }, '@prodlog/sdk': { singleton: true, requiredVersion: false } }})react, react-dom, @prodlog/ui und @prodlog/sdk sind Singletons, vom Host
vorgegeben. Eine zweite Kopie von React wirft im Inneren der Schale einen
invalid-hook-call — das ist die häufigste Ursache für „mein Plugin lädt, zeigt aber
nichts”.
./register — die Laufzeit-Contribution
Abschnitt betitelt „./register — die Laufzeit-Contribution“export const contribution = { id: 'acme.report', path: '/acme.report', view: 'acme.report/View'};
export default contribution;Diese Datei wird gelesen, nachdem das Remote geladen ist. Das Manifest ist das, was der Host beim Installieren prüft; halten Sie beide im Gleichschritt.
./View — was die Schale einhängt
Abschnitt betitelt „./View — was die Schale einhängt“import { useState, type ReactNode } from 'react';import type { ShellContext } from '@prodlog/sdk';import '@prodlog/ui/plugin.css';
export default function View({ ctx }: { ctx: ShellContext }): ReactNode { return <p>{ctx.locale}</p>;}Alles, was pro Einhängung entstehen muss — ein Query-Client, ein Kontext-Provider —, gehört in diese Komponente und nicht auf Modulebene. Module Federation hält ein Remote für die Lebensdauer der Seite vor; ein modulglobaler Zustand überlebt einen Mandantenwechsel im selben Browserfenster.
Der ShellContext
Abschnitt betitelt „Der ShellContext“Das einzige, was die Schale Ihnen reicht:
interface ShellContext { readonly locale: string; // BCP-47, z. B. 'de' oder 'en' notify(input: NotifyInput): void; // level: 'info' | 'success' | 'warn' | 'error' readonly store: ScopedStore; readonly surface: Surface; // 'main' | 'kiosk' | 'capture' readonly log: Logger; readonly correlationId: string; readonly page?: PageInstance; // nur auf der Seitentyp-Route readonly theme?: ThemeControl; // die Farben des Mandanten, für einen Theme-Editor readonly language?: LanguageControl; // Sprache wechseln, wo die Schale keinen Umschalter hat readonly help?: HelpControl; // die Hilfe: Artikel verlinken, Beschriftung erklären readonly configurable?: boolean; // false: Editoren ausblenden (die native App)}Die Felder mit ? sind optional: Eine Schale, die eines nicht anbietet, lässt es weg, und
Ihr Plugin muss ohne es funktionieren.
notify ist die vermittelte Senke für Toasts und ARIA-Live-Meldungen. Bauen Sie keine
eigenen Hinweisbanner — die Schale zeichnet für alle Plugins denselben.
store ist ein einfacher Schlüssel-Wert-Speicher, an Ihren Plugin-Namensraum gebunden.
Werte sind für die Schale undurchsichtig; sie liest sie nie.
await ctx.store.set('user', 'acme.report', 'lastFilter', { line: 'A' });const value = await ctx.store.get('user', 'acme.report', 'lastFilter');Reichweiten: tenant (der ganze Mandant), user (eine Person über ihre Sitzungen hinweg).
client — ein Browsertab — ist flüchtig und wird vom persistenten Speicher abgelehnt;
solcher Zustand gehört in den Tab, nicht in die Datenbank.
surface sagt, wo Sie gezeichnet werden: main ist die normale Schale, kiosk der
rahmenlose Präsentationsmodus, capture eine berührungsorientierte Erfassungsfläche.
Zeichnen Sie im Kiosk keine Bedienleiste.
correlationId wird von der Schale einmal je Einhängung vergeben. Geben Sie sie bei
eigenen Backend-Aufrufen im Kopf x-correlation-id weiter — dann hängt die Zeile, die Ihre
Backend-Route schreibt, an derselben Kennung, und eine Plugin-Sitzung liest sich als eine
Geschichte. Die Körnung ist die Einhängung, nicht der Klick: sie überlebt einen
Sprach- oder Flächenwechsel.
help verbindet Ihre Oberflächen mit dem Hilfe-Panel. Die Hilfe-Bausteine der Schale
können Sie nicht importieren — Ihr Frontend ist ein eigenes Bündel —, deshalb reicht die
Schale Ihnen das hier:
const article = ctx.help?.topic('boards'); // null: kein solcher Artikelconst forLabel = ctx.help?.forKey('widget.none'); // der Artikel zu Ihrer Beschriftungif (article) ctx.help?.open(article.id); // zeigt ihn im Hilfe-PanelJede Antwort trägt id, title, lead (der erste Absatz, als reiner Text) und href (der
Artikel im Help Center). @prodlog/ui zeichnet sie so wie die Schale: HelpTopicLink für
eine Zeile unter einem Fehler oder einem leeren Zustand, HelpTopicLabel für eine
Beschriftung, die beim Zeigen ein Kästchen öffnet.
forKey sucht den Schlüssel in Ihrem Namensraum: Die Schale setzt Ihre Plugin-Kennung
davor, board mit widget.none findet also den Artikel, dessen covers: board:widget.none
nennt. Eine Beschriftung der Schale oder eines anderen Plugins können Sie nicht beanspruchen.
Jede Antwort ist null, solange die Hilfe noch lädt, in einer Installation ohne Help Center
und auf Flächen ohne Hilfe-Panel. Zeichnen Sie dann nichts — nie einen Verweis, der ins Leere
führt. Der Kontext wird neu gebaut, sobald die Hilfe da ist; ein Verweis erscheint dann von
selbst.
Stile: was Ihnen gehört und was nicht
Abschnitt betitelt „Stile: was Ihnen gehört und was nicht“Ein Plugin liefert keine globalen Stile. Importieren Sie @prodlog/ui/plugin.css,
nicht @prodlog/ui/styles.css.
import '@prodlog/ui/plugin.css';Nicht enthalten und ausdrücklich der Schale vorbehalten:
- Preflight — ein globaler Reset gilt dem ganzen Dokument. Ein Remote, das ihn mitbringt, gestaltet Elemente um, die ihm nicht gehören.
- Die Schriften — sonst lädt jedes Remote dieselben Schnitte erneut.
- Der Tokenblock — die Tokens stehen auf
:rootim Dokument der Schale; einhsl(var(--brand))in Ihrem Plugin löst sich durch Vererbung dagegen auf.
Das fertige Stylesheet eines Remotes wird beim Bauen in eine schwächere Kaskadenschicht eingepackt. Ungeschichtete Regeln gewinnen in CSS immer gegen geschichtete, unabhängig von der Reihenfolge — die Schale bleibt ungeschichtet. Damit kann kein Remote die Schale überschreiben, egal wie viele laden und in welcher Folge. Ihre eigenen Elemente gestalten Sie unverändert.
Das ist keine Bitte, sondern von der Schale durchgesetzt. Der Anlass war messbar: eine
unbedingte .hidden{display:none} aus einem Plugin schlug die Regel der Schale, und die
Wortmarke verschwand, sobald einmal eine Plugin-Seite offen war.
Seitentypen statt eines Menüeintrags
Abschnitt betitelt „Seitentypen statt eines Menüeintrags“Früher trug ein Plugin einen Menüeintrag bei, für jeden Mandanten gleich. Ein Seitentyp dreht das um: Ihr Plugin deklariert eine Art von Seite, und der Mandant legt davon so viele benannte Ausprägungen an, wie er will, und hängt sie in seinen eigenen Baum.
{ "category": "pageType", "id": "acme.report", "label": "Report", "meta": { "label": "Report", "defaultIcon": "clipboard-list", "create": { "path": "/reports", "nameField": "name", "refField": "id" } }}Beim Anlegen ruft die Schale POST /api/plugins/<id><create.path> auf, schickt den vom
Mandanten getippten Namen unter create.nameField und liest die Kennung der neuen
Ausprägung aus dem obersten Feld create.refField der Antwort. Nur POST, kein method-
Schalter: etwas anzulegen ist ein POST.
Beim Anzeigen bekommen Sie die Ausprägung im Kontext:
export default function View({ ctx }: { ctx: ShellContext }): ReactNode { if (!ctx.page) { // über die eigene `route`-Contribution erreicht - es gibt hier keine Ausprägung return <Overview />; } return <Report type={ctx.page.type} reference={ctx.page.ref} />;}ctx.page.ref ist genau das, was Ihr eigener Anlegeaufruf zurückgegeben hat. Die Schale
deutet es nie — es ist Ihre Kennung für Ihr eigenes Ding.
Die Richtung ist immer Navigation → Seitentyp, nie zurück. Ihr Plugin erfährt nie, dass ein Menü existiert, bekommt keine Kennung eines Navigationsknotens und hat nichts, wohin es zurückrufen könnte. Ein Menü ist eine Rechtefläche des Mandanten.
Die Route einer Ausprägung ist /p/<pageType>/<pageRef>; die Konstante dafür heißt
PAGE_ROUTE_PREFIX, und pageRoutePath(type, ref) baut sie.
Wenn etwas schiefgeht
Abschnitt betitelt „Wenn etwas schiefgeht“- Nichts wird geladen. Prüfen Sie, ob
frontend.entryim Manifest steht und die Datei unterdist/im Archiv liegt. Fehlt eines von beidem, gibt es für die Schale nichts zu laden, und sie versucht es gar nicht erst. invalid-hook-call. Ein Singleton fehlt inshared, meistensreact-dom.- Die Schale sieht nach dem Öffnen Ihrer Seite kaputt aus. Sie importieren
styles.cssstattplugin.css. - Nach einer Aktualisierung zeigt die Seite den alten Stand. Der Remote-Name wird aus Id und Version gebildet; das ist bewusst so. Prüfen Sie, dass die Version im Manifest wirklich gestiegen ist.