Seiten
Inhalt
@xpulse/controller – Component Spec
Status: CONCEPT · Erstellt März 2026 Übergeordnet:
GLOBAL_concept-ecosystem.md,GLOBAL_concept-web.md
Übersicht
Controller-Discovery und Basis-Klasse für das xPulse MVC Stack.
Eine Aufgabe: Controller finden, Routes ableiten, Views aufrufen – nicht mehr.
Discovert src/controllers/**/*.js, leitet Routes aus Dateipfad und
Methodenname ab, meldet sie bei @xpulse/router an und stellt die
Controller Basis-Klasse bereit.
Dependencies
| @xpulse/controller |
| ├── @xpulse/router ← Routes anmelden |
| ├── @xpulse/template ← this.render() delegiert hierhin |
| └── @xpulse/event ← auf template:render:before lauschen |
API
Initialisieren
| import controller from '@xpulse/controller'; |
| await controller.init(); |
Discovert rekursiv src/controllers/**/*.js, leitet Routes ab und
registriert sie bei @xpulse/router. Wird von @xpulse/app aufgerufen.
Filesystem-Routing
Der Dateipfad bestimmt die Basis-Route des Controllers. Der Methodenname bestimmt die Sub-Route.
Regeln
index()Methode → kein URL-Suffix (Root der Controller-Route)- Andere Methoden → Methodenname wird zum URL-Suffix
define()→ optionaler Override für einzelne Methoden-Routes- Unterordner → werden Teil der Route
Mapping-Beispiele
| src/controllers/ |
| index.js → IndexController |
| index() → GET / |
| info.js → InfoController |
| index() → GET /info |
| team() → GET /info/team |
| bla() → GET /info/bla |
| tool/ |
| chat.js → ChatController |
| index() → GET /tool/chat |
| guide() → GET /tool/chat/guide |
| contact.js → ContactController |
| index() → GET /contact |
| submit() → POST /contact |
Response Object
Alle Controller-Methoden geben ein Response Object zurück –
@xpulse/http ist der einzige der es tatsächlich sendet.
Der Controller beschreibt die Response, nie sendet er sie direkt.
| // Intern – alle Helper geben dieses Format zurück: |
| { |
| status: 200, |
| headers: { 'Content-Type': 'text/html' }, |
| body: '<html>...', |
| } |
Controller Basis-Klasse
| import { Controller } from '@xpulse/controller'; |
| // Datei: src/controllers/info.js |
| // Basis-Route: /info |
| export default class InfoController extends Controller { |
| define() { |
| this.overrideRoute('team', '/info/ueber-uns'); |
| this.overrideMethod('submit', 'POST'); |
| } |
| // → GET /info |
| async index() { |
| return this.render('info/index', { |
| title: 'Info', |
| }); |
| } |
| // → GET /info/bla |
| async bla() { |
| return this.render('info/bla', { |
| title: 'Bla', |
| items: ['a', 'b', 'c'], |
| }); |
| } |
| // → GET /info/ueber-uns (via overrideRoute) |
| async team() { |
| return this.render('info/team', { |
| title: 'Über uns', |
| }); |
| } |
| } |
POST + JSON Beispiel
| // Datei: src/controllers/contact.js |
| export default class ContactController extends Controller { |
| define() { |
| this.overrideMethod('submit', 'POST'); |
| } |
| // → GET /contact |
| async index() { |
| return this.render('contact/index', { |
| title: 'Kontakt', |
| }); |
| } |
| // → POST /contact |
| async submit() { |
| const { name, message } = this.req.body; |
| if (!name || !message) { |
| return this.render('contact/index', { |
| title: 'Kontakt', |
| error: 'Bitte alle Felder ausfüllen.', |
| }); |
| } |
| return this.redirect('/contact/danke'); |
| } |
| // → GET /contact/api – JSON Response ohne Template |
| async api() { |
| return this.json({ status: 'ok' }); |
| } |
| } |
Controller Basis-Klasse API
Response Helper – geben immer ein Response Object zurück:
| Methode | Beschreibung |
|---|---|
this.render(view, data) |
Template rendern via @xpulse/template → Response Object |
this.response(html, options?) |
Eigenes HTML → Response Object, kein Template |
this.json(data, options?) |
JSON → Response Object, kein Template |
this.redirect(url) |
Redirect → Response Object (301) |
Route Overrides – nur in define():
| Methode | Beschreibung |
|---|---|
this.overrideRoute(method, path) |
Route für eine Methode überschreiben |
this.overrideMethod(method, httpMethod) |
HTTP-Methode für einen Handler überschreiben |
Request Zugriff:
| Eigenschaft | Beschreibung |
|---|---|
this.req.path |
Aktueller URL-Pfad – /info/bla |
this.req.params |
URL-Parameter – { page: 'bla' } |
this.req.query |
Query-String – { lang: 'de' } |
this.req.method |
HTTP-Methode – GET, POST |
this.req.body |
Request Body – bei POST geparst |
this.req.headers |
Request Headers |
Automatische Route-Info in Template-Daten
@xpulse/controller lauscht auf template:render:before und injiziert
_route in die Template-Daten – ohne manuellen Aufwand in jeder View-Methode,
und ohne dass @xpulse/template den Controller kennen muss.
| // Intern in @xpulse/controller – einmalig beim Init: |
| event.on('template:render:before', ({ view, data }) => { |
| data._route = { |
| path: this.req.path, |
| params: this.req.params, |
| query: this.req.query, |
| method: this.req.method, |
| }; |
| }); |
Im Template abrufbar zur Laufzeit – praktisch zum Debuggen und für aktive Navigation-States:
| <!-- Aktiven Nav-Link markieren --> |
| <a href="/info" class="{{ _route.path === '/info' ? 'active' : '' }}">Info</a> |
| <!-- Debug-Info --> |
| <p>Route: {{ _route.path }}</p> |
_ Prefix signalisiert: System-Wert, kein User-Daten.
Gefeuerte Events
| Event | Payload | Wann |
|---|---|---|
controller:init |
{ root } |
Discovery beginnt |
controller:discovered |
{ name, routes } |
Controller gefunden |
controller:ready |
{ count, routes } |
Alle registriert |
controller:called |
{ traceId, controller, action, method, path, name } |
Handler aufgerufen |
controller:navigation:collected |
{ items } |
Navigation-Items gesammelt |
controller:navigation:ready |
{ items } |
Navigation bereit |
Zusammenspiel im Request Flow
| @xpulse/http empfängt GET /info/bla |
| ↓ |
| @xpulse/router matched → InfoController@bla |
| ↓ |
| @xpulse/controller ruft InfoController.bla() auf |
| ↓ |
| InfoController this.render('info/bla', { title: 'Bla' }) |
| ↓ |
| @xpulse/template rendert HTML |
| ↓ |
| controller gibt Response Object zurück { status: 200, body: html } |
| ↓ |
| @xpulse/http sendet Response |
Debug Integration
Wenn @xpulse/debug als devDependency installiert ist, stellt @xpulse/controller
automatisch einen DataCollector bereit. @xpulse/debug discovert ihn via
node_modules/@xpulse/controller/src/datacollectors/ – keine Konfiguration nötig.
| "optionalDependencies": { |
| "@xpulse/debug": "^1.0.0" |
| } |
Der ControllerCollector (name: 'controller', icon: '⚙️') zeigt im Web Profiler:
- This Request (kv): Controller, Action, Route, Dispatch Time
- All Controllers (table): alle discoverten Controller-Routen
Dispatch Time misst die Zeit von http:request bis controller:called –
das schließt Routing und Dispatch ein, nicht die reine Handler-Execution-Zeit.
Dies ist im Panel als (request start → controller called) ausgewiesen.
Außerdem schreibt der ControllerCollector einen Waterfall-Span (kind: 'controller')
für die Dispatch-Zeit.
Badge in der Toolbar: InfoController@index · 3ms
Paket-Struktur
| @xpulse/controller/ |
| src/ |
| index.js ← default export: controller-Objekt |
| discovery.js ← rekursives Laden aus src/controllers/ |
| resolver.js ← Filesystem-Pfad → Route ableiten |
| base.js ← Controller Basis-Klasse |
| test/ |
| discovery.test.js |
| resolver.test.js |
| base.test.js |
| docs/ |
| index.md |
| api.md |
| _meta.json |
| Dockerfile |
| Makefile |
| README.md |
| package.json |
| xpulse.json |