Seitenlaufzeit für Node und Laravel
Node und PHP/Laravel sind eigenständige SSR-Hosts. Node behält seine Compilerzugänge, JavaScript-Services und Streams. Laravel verwendet im Produktionsbetrieb vorgebaute RMT-Artefakte und das Composer-Paket; ein Node-Prozess ist dort nach dem Build nicht erforderlich.
Die Seitenlaufzeit ergänzt die bestehenden Node- und PHP-Adapter. Ihr Protokoll ist XTend-spezifisch und verwendet keine Inertia-Abhängigkeit.
Build und Verträge
xt pages build --root /pfad/zur/app --target both --json verwendet den bestehenden vNext-Compiler und Importresolver. node, php und both sind Renderziele; --host laravel wählt den Laravel-Ausgabepfad. Die bisherigen CLI-Serverziele bleiben erhalten.
Eine xtend.pages.json beschreibt die Zuordnung:
{
"schema": "xtend.page-build.v1",
"target": "both",
"pages": {
"Orders/Detail": {
"source": "pages/order.rmt",
"inputs": ["orders.detail"]
}
}
}
Der Build erzeugt xtend.page-manifest.v2 und TypeScript-Seiten-/Layoutzuordnungen. Standardziel ist .xtend-build/pages.json, bei Laravel bootstrap/xtend/pages.json. Controllerdaten ersetzen deklarierte Eingänge; fehlende Eingänge behalten ihre Defaults. Der Projektindex verbindet Konfiguration, Seitennamen, RMT-Quelle, Renderziel und Artefakt. Composer-vendor-Verzeichnisse werden nicht als Projektquellen erfasst.
Die portable Projektion heißt xtend.rmt.portable-render.v2. Sie verwendet bestehende Compilerdeskriptoren, keinen PHP-RMT-Parser. Textknoten bilden Strings, Zahlen und Boolesche Werte ab; null, fehlende Werte und strukturierte Werte ergeben dort leeren Text. Für strukturierte Daten stehen Bindungen, Attribute, Listen und ausdrückliche Formatierung bereit. JSON-Objekte bleiben bei der PHP-Dekodierung von Listen unterscheidbar.
PHP unterstützt die im portablen Renderer ausgewiesenen Ausdrücke. Beispielsweise Regex-Ersetzung und JavaScript-Funktionen gehören nicht zu diesem Ziel. Ein PHP-Build meldet solche Fähigkeiten als Diagnose. Node-Compiler-/Adapterzugänge bleiben nutzbar.
assets.entry und assets.css enthalten URLs desselben Ursprungs. Alternativ übernimmt "vite": {"manifest": "public/build/manifest.json", "entry": "resources/js/app.js"} den Einstieg und seine CSS-Abhängigkeiten aus einem zuvor ausgeführten Vite-Build. vite.base ist standardmäßig /build/. assetRoot bezeichnet das lokale öffentliche Verzeichnis, standardmäßig public. Asset-, Konfigurations-, Quell- und Laufzeitfingerprints gehen in die Buildversion ein. Nach einem Paketwechsel müssen Seitenartefakte mit dem tatsächlich ausgelieferten Paket neu gebaut werden.
Node-Integration
Das Laufzeitpaket exportiert @ccslabs/xtend-rmt/node-page-host; das Gesamtpaket bietet denselben Zugang unter @ccslabs/xtend/rmt/node-page-host.
import { createNodePageHost } from '@ccslabs/xtend-rmt/node-page-host';
import { Prop } from '@ccslabs/xtend-rmt/page-contract';
const pages = createNodePageHost({
manifest,
createContext: (request, signal) => host.requestContext(request, signal),
resolvePage: async context => ({
page: 'Orders/Detail',
props: { 'orders.detail': Prop.once(() => host.loadOrder(context)) }
}),
validate: (context, fields) => host.validateOnly(context, fields),
appServiceHost,
cleanup: context => host.release(context)
});
// Im bestehenden HTTP-Host:
// if (!await pages.handle(request, response)) nextHandler(request, response);
host, manifest und appServiceHost sind Integrationspunkte der Anwendung. createNodePageHost() startet keinen Server. Der Requestkontext liefert einen opaken, an Anwendung, Benutzer und Mandant gebundenen contextKey; optional einen csrfToken. Authentifizierung, Routing, Uploadlimits und fachliche Validierung bleiben beim HTTP-Host. appServiceHost kann eine bestehende createNodeAppServiceHost()-Instanz sein.
Resolver dürfen { redirect }, { download } mit Node-/Web-Stream oder eine Seite zurückgeben. Provider erhalten ein Abbruchsignal. Requestdeadline und Cleanupbudget sind getrennt konfigurierbar. Synchron blockierende Anwendungscallbacks können nicht durch ein JavaScript-Abbruchsignal unterbrochen werden.
createNodePageRouteManifest() übernimmt explizit aus dem Router gelesene benannte Routen. Laravel exportiert unabhängig seine Route Collection. Beide verwenden xtend.page-routes.v1 mit unterscheidbarem Host.
Laravel-Integration
scripts/build_laravel_package.js /absoluter/frischer/ausgabepfad stellt ccslabs/xtend-laravel samt kanonischer PHP-Dateien zusammen. Das Paket kann anschließend über ein Composer-Repository installiert werden. Für lokale Pakettests wird ein Composer-Path-Repository mit symlink: false verwendet. Eine Veröffentlichung auf Packagist ist ein separater Schritt.
use Ccslabs\XTend\Facades\XTend;
use Ccslabs\XTend\Data\Prop;
XTend::share('account', fn ($request) => $request->user()?->only('id', 'name'));
return XTend::render('Orders/Detail', [
'orders.detail' => ['text' => $order->name, 'tone' => 'neutral'],
'statistics' => Prop::defer(fn () => $order->statistics(), 'statistics'),
]);
Registriere Ccslabs\XTend\HandleXTendRequests in der Web-Middleware nach der Session-Middleware. Der ServiceProvider wird über Composer entdeckt. FormRequests, Redirects, Error Bags, Flash und UploadedFile behalten den Laravel-Lifecycle; API-Routen ohne diese Middleware bleiben JSON-Routen. Bei Livevalidierung kommt Laravels HandlePrecognitiveRequests hinzu.
xtend:install veröffentlicht die Konfiguration. xtend:routes exportiert die in xtend.routes benannten Routen. xtend:doctor prüft Manifest, Renderziel und PHP-Quellfingerprints aus xtend.php-package-sources.v1. Das Blade-Root kann über xtend.root_view ersetzt werden. Eine Compiler-Bridge ist ausdrücklich nur ein konfigurierbares Entwicklungswerkzeug; die Seitenintegration rendert Produktionsartefakte.
Gemeinsamer Browser-Lifecycle
import { createPageClient } from '@ccslabs/xtend-rmt/page-client';
import { createPageForm } from '@ccslabs/xtend-rmt/page-form';
const client = createPageClient({
initialPage: JSON.parse(document.getElementById('xtend-page-data').textContent),
encryptHistory: true
});
await client.start();
const form = createPageForm({ client, errorBag: 'edit', defaults: { name: '' } });
form.bind(document.querySelector('form'), { action: '/orders/1' });
xtend.page-response.v1 verbindet Seite, Props, Layout, Head, Version und Kontext. Interne Links, Abbruch überholter Besuche, Back/Forward, Scrollregionen (data-xtend-scroll) und Anker verwenden denselben Client. data-xtend-native belässt einen Link beim Browser. Formulare liefern Dirty-, Processing-, Success-, Fehler- und Uploadzustände; spätere Eingaben werden nicht von alten Antworten zurückgesetzt. bind() unterstützt native Formulare und x-form-Submit-Ereignisse.
Layouts werden über layouts und page.layout deklariert. Beim RMT-Build benennt outlet genau einen kompilierten Knoten. Dessen Position ist der Seiteninhalt. Ressourcen lassen sich mit registerResource() an Seite oder Layout binden. Ein Benutzer-/Mandantenwechsel gibt auch persistentes DOM frei.
Prop.lazy, Prop.defer, Prop.merge und Prop.once haben JavaScript- und PHP-Zugänge. reload({ only: [...] }) und Deferred-Gruppen werten nur gewählte Provider aus. prefetch(), poll() und whenVisible() verwenden begrenzte Lade-/Cacheverwaltung. Ein Prefetch-Besuch prüft den Hostkontext erneut und verbraucht vorher keine Flash-Daten. loadMore() verwendet pagination.next, previous und props; Laravel bietet Ccslabs\XTend\Pagination::from() für Offset- und Cursor-Paginatoren.
Livevalidierung erfolgt über createNodePageValidator() beziehungsweise createPrecognitionValidator() als Formularprovider. Übergib bei wechselnden Sessiontokens csrfToken: () => client.page.csrfToken. Optimistische Änderungen verwenden client.optimistic(). Ein älterer Fehler setzt keinen neueren Zustand zurück. Instant Visits und View Transitions sind explizite Besuchsoptionen; reduzierte Bewegung wird berücksichtigt.
History speichert gezielt remember()-Daten und Scrollpositionen. Passwörter, Tokenfelder und Dateien werden ausgeschlossen. Optionale WebCrypto-Verschlüsselung ersetzt keine Autorisierung. invalidate() verwirft Cache und gespeicherten Zustand. Hintergrundabfragen melden einen Deploymentwechsel; sie erzwingen keinen Dokumentwechsel über ungespeicherte Formulare hinweg. Initiales Resume verwendet den bestehenden Signatur-/Integritätsvertrag und dessen kontrollierten Hydration-Fallback.
Abnahme und Betrieb
Die Auswahl liegt in scripts/test-runner/catalog.json: ssr-pages:node, ssr-pages:laravel und ssr-pages:laravel-browser. Die ausführbaren Referenzhosts und derselbe CRUD-Browserablauf liegen unter tests/ssr-pages/. Die Laravel-Matrix umfasst 12/PHP 8.2–8.5 und 13/PHP 8.3–8.5; Browsernachweise laufen zusätzlich je Hauptversion. Lokale PHP-8.3-Ergebnisse sind kein Ersatz für diese CI-Matrix.
Für PHP-FPM/Proxy-Streaming müssen die Infrastruktur-Timeouts synchron blockierende Provider begrenzen; Frame- und Cleanupbudgets allein können eine laufende PHP-Funktion nicht unterbrechen. Die PHP-FPM-Prüfung kann mit XTEND_PHP_FPM_BINARY=/pfad/zu/php-fpm vor dem Laravel-Browserprofil aktiviert werden. Der Test startet ausschließlich einen eigenen temporären Pool.
Ressourcenmessung: node tests/ssr-pages/measure_resources.js .xtend-test-results/ssr-pages-resources.json vergleicht 50 Renderdurchläufe derselben Liste in beiden Laufzeiten und misst das Browserbundle. PHP-Vergleiche verwenden skalare Identitäten; Listenidentität muss als Schlüssel ausgedrückt werden. String-Slices müssen Unicode-Skalargrenzen erhalten.
Der Node-Testpfad benötigt kein PHP. Die zusätzliche Suite ssr-pages-php prüft die gemeinsame Renderparität einmal pro PHP-/Laravel-Umgebung. Einen vorhandenen x-router vor dem Einfügen mit router.pageClient = client verbinden und client.start() ausführen; danach verwaltet die Seitenlaufzeit Links und History.
createPageClient({ initialPage, viewTransitions: true }) aktiviert Seitenanimationen über die bestehende RMT AnimationEngine und XUtils. data-xtend-page-transition markiert den animierten Inhalt innerhalb des Seitenroots; eine persistente Shell bleibt dadurch ruhig. startMaracaPageApplication() reicht dieselbe Option weiter. Hintergrundabfragen animieren nicht; fehlende Animationsunterstützung, fehlende XUtils und reduzierte Bewegung verwenden die reguläre Aktualisierung. Die Navigation wartet nicht auf die Animation; ein neuer Besuch oder Dispose beendet laufende Effekte. Ein Besuch kann mit { transition: false } aussteigen. Eine eigene transition: async update => { /* … */ await update(); }-Hostfunktion bleibt verfügbar und hat Vorrang; sie wird mit { transition: true } aktiviert.
Links und native GET-Formulare können data-xtend-preserve-scroll="true", data-xtend-preserve-state="true" und data-xtend-transition="none" ("false" wird ebenfalls akzeptiert) deklarieren. Submit-Buttons können die Formularwerte überschreiben. Damit aktualisieren etwa Produktvarianten weiterhin URL, Daten und Metadaten, während Scrollposition und gespeicherter Zustand erhalten bleiben. Die Attribute gelten für interne Navigation; ohne JavaScript bleiben die ursprünglichen Links und Formulare nutzbar.
Maraca-Seiten und XTend.store
createMaracaPageClient() aus @ccslabs/xtend/maraca/page-client verbindet den Seitenclient mit einem Maraca-Bundle. Das Seitenmanifest kann pro Seite oder Layout maraca: { entry: "/build/maraca/xtend.maraca.mjs" } angeben. Das Bundle und die portable Projektion verwenden denselben Compilerstand; createRmtCompilationSession() hält die Analyse innerhalb eines Builds gemeinsam vor. Die Konfiguration und alle referenzierten Assets fließen in die Buildversion ein.
Die Seitenlaufzeit verwaltet URL, History und Transport, Maraca den UI-Zustand und DOM-Commits. Ein Root hat einen Controller. Unveränderte Shells bleiben bei Navigation erhalten. Abgelöste Aktivierungen werden verworfen und Remote-Surfaces bei Navigation freigegeben. Der generierte Einstieg verwendet startMaracaPageApplication() aus @ccslabs/xtend/maraca/page-bootstrap: Er installiert Capture vor dem Laden der Komposition, prüft P-256-Signaturen mit dem öffentlichen Buildschlüssel und nutzt bei fehlgeschlagener Integritätsprüfung den vorhandenen einmaligen Hydration-Fallback.
Der gemeinsame Head-Vertrag unterstützt zusätzlich Canonical-Links (tag: "link", rel: "canonical", HTTP(S)-URL) und benannte JSON-LD-Datensätze (tag: "json-ld", key, data). Node, PHP und Browser deduplizieren nach Identität. JSON wird sicher in Script-Tags serialisiert; Eventattribute und ausführbare Canonical-URLs werden abgelehnt.
XTend.store zeigt diese Integration mit Laravel, Gastwarenkorb und einem separaten PHP-DemoPay-Provider. Der Node-SSR-Adapter und createNodePageHost() bleiben eigenständige Zugänge. Der Shop ist eine zusätzliche Referenzanwendung.
Maraca-Seiten übernehmen auch native GET-Such- und Filterformulare in die Seitennavigation. Die Basisseiten-API schaltet dies mit forms: true ein; navigationAction kann vor einem Besuch deklarierte RMT-Surfaces schließen. POST-Formulare behalten ihren Host- beziehungsweise RMT-Vertrag.
Kompakter Transport und bedingte Ansichten
Node-Hosts aktivieren compactResponses: true, Laravel verwendet compact_responses => true. Die HTML-Bootstrapdaten nutzen dann xtend.page-wire.v1: Eine antwortbezogene Tabelle speichert identische JSON-Objekte und Arrays einmalig. Referenzen in Kindwerten zeigen auf einen Tabellenindex. encodePageWire() und decodePageWire() sind über das Page-Contract-Modul verfügbar. Vor Validierung und Signaturprüfung werden die bestehenden Seiten- und Resume-Envelopes rekonstruiert; deren Verträge bleiben erhalten. Referenzen gelten ausschließlich innerhalb einer Antwort. Der Decoder begrenzt Tiefe, Knotenzahl und logische Expansion und weist unsichere Schlüssel sowie zyklische oder ungültige Referenzen zurück.
Der Seitenclient handelt das Format mit X-XTend-Page-Wire: 1 aus. Ältere JSON-Verbraucher erhalten weiterhin normale Seitenantworten. Bei ausgehandelten Folgebesuchen mit portablem Artefakt senden die Hosts Eingabedaten und Renderartefakt ohne einen weiteren SSR-Envelope zu erzeugen. HTML-Erstaufrufe enthalten weiterhin vollständiges SSR und signierte Resume-Daten. Die Option benötigt einen kompatiblen Bootstrap; eigene Laravel-Root-Views serialisieren dafür $pageData (mit $page als Rückfall für ältere Integrationen).
Portable conditional-Deskriptoren materialisieren nur die aktuelle Ansicht. Der DOM-Renderer versteht sowohl conditional als auch seine bisherige Schreibweise when; vom Compiler aufgelöste Portale können eine bedingte Verzweigung adressieren. Zustand bleibt im RMT-Modell erhalten, während Controls nicht im DOM stehen. Eine unveränderte Verzweigung aktualisiert ihre bestehenden Knoten. Dies reduziert DOM und Seitendaten, teilt ein gemeinsames Maraca-Bundle aber nicht automatisch in getrennt geladene Codepakete auf.
Laravels optionale Einstellung style_nonce => true ergänzt den Dokument-Nonce in style-src und entfernt dort unsafe-inline. Der Shop setzt zusätzlich style-src-attr 'none'. Vertrauenswürdige XTend-Komponentenstyles übernehmen den Bootstrap-Nonce; beliebiges HTML wird nicht automatisch freigegeben. Eigene Komponenten benötigen vor Aktivierung der Richtlinie entsprechende feste Stylevorlagen oder externe Stylesheets. Browsertests prüfen blockierte Style-Injektionen und den vollständigen Zahlungsablauf.
Page-Lifecycle und Surface-Lifecycle
Ein Seitenwechsel beendet page-eigene Requests; layout-eigene Ressourcen dürfen entsprechend ihrem Besitzer weiterleben. Innerhalb einer Maraca-Seite schützt zusätzlich die automatische Abort Boundary jede verwaltete Surface vor verspäteten Worker-, Hydration- und DOM-Commits. Ein schneller Close oder Fokus benötigt weder ein neues Seitenartefakt noch das Ende fachlicher Services.
Präsentationsepochen ersetzen keine Datenversion oder fachliche Service-Cancellation. Gemeinsames Prewarming darf gültige Daten behalten. Eigene Integrationen prüfen den Surface-Token nach asynchronen Antworten und unmittelbar vor sichtbaren Commits. Die reine clientseitige Maraca-Ausführung verwendet dieselben Guards auch ohne SSR.
Migration auf XTend 0.9
Seiten und Browser-Assets gemeinsam neu bauen. Neue Builds schreiben page-manifest v2, portable-render v2 mit Renderer v2 und page-response v2. Kompakte Page-API-Antworten verwenden den verlustlosen page-wire v2; v1 bleibt mit response v1 verbunden. Der initiale Resume-Envelope xtend.page-initial-resume.v1 wird mit decodePageInitialDocument gelesen, getrennt vom Page-Wire-Codec. Seine SSR-/Chunk-Daten tragen keine allgemeinen Response-Tags kind/version und enthalten nur markup.descriptor. Vollständige API-Antworten behalten HTML und Textinhalt.
Im Renderer v2 sind Strings in attributes/attrs Literale: type: "search" bleibt auch bei einem Modellschlüssel search unverändert. Dies gilt für den DOM-Renderer, Node SSR und portable Node/PHP-Projektionen mit v2-Vertrag. Implizite Bindungen wie value: "search.query" auf value: "$model.search.query" umstellen und neu bauen. Weitere explizite Formen sind $item.path, ${model.path} und Expression-Records; der allgemeine Value-/Property-Resolver bleibt unverändert. Bestehende portable v1-Artefakte behalten ihre skalare implizite Bindungsabsicht. Direkte Renderer-Integrationen wählen während der Migration ausdrücklich rendererSchema: "xtend.epic18.rmt-dom-descriptor-renderer.v1" und erhalten informative Migrationsdiagnosen. Strukturierte native/ARIA-Attribute werden in beiden Pfaden abgewiesen; Komponenten-Properties und strukturierte data-* bleiben verfügbar.
Vorhandene ausführbare Beispiele und Regressionstests: Node-SSR-Attributliterale, Bindings und Interpolation, portable Node/PHP-Projektion und v1/v2-Kompatibilität einschließlich ausgelieferter DOM-Renderer.
SSR-Coverage über getRmtSsrCoverage(result) aus dem Node-SSR-Adapter lesen. Der Datensatz liegt eigenständig in fabricTelemetryHints.coverage; Hydration v1 bleibt unverändert. Nutzer der noch unveröffentlichten Ergänzung hydration.coverage müssen diesen Zugriff migrieren. Coverage zählt ausgegebene Markierungen; tatsächlichen Browser-Resume und Recovery gesondert prüfen.
Für einen schrittweisen Rollout das v1-Manifest mit passenden alten Clients behalten. Neue Clients lesen alte vollständige Dokumente, v1-Wire-Antworten und den neuen Envelope. V1-Manifeste lehnen v2-Artefakte ab; Schema-IDs niemals von Hand umetikettieren. V2-Manifeste können bestehende v1-Artefakte verwenden und behalten dafür den vollständigen initialen Transport. Unbekannte Verträge werden vor initialen DOM-/State-Änderungen abgewiesen.
Legacy-Verträge bleiben mindestens in 0.9 und 0.10 unterstützt. Entfernung erst mit 1.0 oder später nach dokumentierter Migration. Ausgelieferte JSON-Schemas liegen unter @ccslabs/xtend-rmt/contracts/*.json. Die Architekturentscheidung hält Vertragsgraph und Release-Gates fest.
Optionaler Cache für öffentliche Ressourcen
createPageResourceCache aus @ccslabs/xtend/rmt/page-resource-cache speichert ausdrücklich ausgewählte JSON-Ressourcen im RAM. TTL, LRU und UTF-8-Bytegrenzen begrenzen den Speicher. Lesen und Schreiben liefern Kopien; Lesen verlängert die TTL nicht. stats() gibt Zähler zurück, niemals Schlüssel.
PageClient erhält resourceCache: {resolve, project, clear}. resolve(url, context) bekommt {page, signal, cache} und gibt für nicht freigegebene Routen null zurück. project(resource, context) liefert eine vollständige Seite gleichen Origins, Versions- und Kontextstandes ohne SSR-Envelopes und CSRF-Token, mit bereits geladenen Seitenartefakten. visit(url, {cache: 'reuse'}) bzw. data-xtend-cache="reuse" aktivieren die Wiederverwendung; Standard ist reload. Auch Reload-Navigationen erreichen die Policy, damit sie den betreffenden Eintrag löschen kann. History versucht Wiederverwendung ausschließlich bei installierter Policy. Seiten mit CSRF-Token verwenden diesen Pfad nicht. Die Prefetch-Autorisierungsprüfung bleibt unverändert.
Dispose, Kontext-/Versionswechsel und invalidate() rufen clear auf. Policyfehler fallen auf den normalen Transport zurück; überholte Auflösungen dürfen nicht committen. DOM, SSR-/Resume-Payloads, Event-Replay, Zugangsdaten, Formularentwürfe und geschützte Seiten gehören nicht in den Cache. Veränderliche Freigaben werden bei Bedarf über einen Metadaten-App-Service geprüft. Der Maraca-Bootstrap reicht resourceCache und fetch weiter.
Siehe das Maraca-App-Services-Beispiel mit zwei Ansichten.
Bei kompakten Antworten nutzt das Initial-Resume-Envelope xtend.page-initial-wire.v1. decodePageInitialDocument expandiert die begrenzte Referenztabelle vor der Envelope-Validierung. Signierter Zustand und Deskriptoren bleiben unverändert; normale PageResponse-Decoder akzeptieren weiterhin keine reduzierten SSR-Chunks.