webserver.js Dynamische Services
Dynamische Services erweitern webserver.js mit eigenständigen, hot-reload-fähigen Modulen, die Request-Handler, HTTP-Endpunkte und Zugriffssteuerung bereitstellen, ohne den Serverkern zu verändern oder den Manager neu zu starten.
Überblick
Ein dynamischer Service ist ein eigenständiges Modul, das von webserver.js zur Laufzeit erkannt, geladen und verwaltet wird. Jeder Service kann Folgendes bereitstellen:
- Request-Handler — WebSocket-Nachrichten-Handler, die in der Request-Handler-Registry registriert werden.
- HTTP-Endpunkte — Express-ähnliche Routen, die auf dem Webserver eingehängt werden.
- Zugriffssteuerungseinträge — Deklarative ACL-Regeln für die Endpunkte des Services.
Services werden aus einem definierten Verzeichnis geladen, auf Änderungen überwacht und automatisch neu geladen, wenn sich die kompilierte Ausgabe ändert. Dies ermöglicht einen Entwicklungsworkflow, bei dem Codeänderungen ohne Neustart des webserver.js Managers wirksam werden.
Dynamische Services bestehen neben dem statischen Erweiterungsmuster, das in webserver.js Kundenerweiterungen beschrieben ist. Statische Erweiterungen (Server-Subclassing) eignen sich für Kernanpassungen, die immer vorhanden sein müssen. Dynamische Services eignen sich für optionale, unabhängig bereitstellbare Features, die hot-reload-fähig sein sollen.
Service-Verzeichnis
Services befinden sich im Unterverzeichnis services/ von javascript/webserver-js/ auf jeder Ebene der WinCC OA Projekthierarchie (Installation, Unterprojekte, Projekt). Der Service-Manager durchsucht alle Hierarchieebenen und führt die Ergebnisse zusammen.
<project>/
javascript/
webserver-js/
services/
my-service-a/
package.json
dist/
my-service-b/
package.json
dist/
Jedes Unterverzeichnis, das eine package.json mit einem
"webserverjs-service"-Schlüssel enthält, wird als Service
behandelt.
Service-Manifest
Das Service-Manifest wird in der package.json des Services
unter dem Schlüssel "webserverjs-service" definiert. Es deklariert,
was der Service bereitstellt:
{
"name": "my-alarm-export",
"version": "1.0.0",
"webserverjs-service": {
"version": 1,
"outDir": "dist",
"requestHandlers": [
{
"source": "alarmExportHandler.js",
"className": "AlarmExportHandler"
}
],
"httpEndpoints": [
{
"mountPath": "/alarms/export",
"source": "alarmRoutes.js",
"routesFunction": "routes"
}
],
"acl": [
{
"path": "/alarms/export",
"allowUsers": "*"
}
]
}
}
Manifest-Felder:
| Feld | Standard | Beschreibung |
|---|---|---|
version |
1 |
Manifest-Schema-Version (muss 1 sein). |
outDir |
"dist" |
Verzeichnis mit der kompilierten JavaScript-Ausgabe, relativ zum
Service-Stammverzeichnis. Quellpfade in
requestHandlers und
httpEndpoints werden relativ zu diesem
Verzeichnis aufgelöst. |
requestHandlers |
[] |
Array von WebSocket-Request-Handler-Definitionen. Jeder Eintrag
gibt eine source-Datei und einen
className an. |
httpEndpoints |
[] |
Array von HTTP-Endpunkt-Definitionen. Jeder Eintrag gibt einen
mountPath, eine source-Datei
und optional einen routesFunction-Namen an
(Standard: "routes"). |
acl |
[] |
Array von Zugriffssteuerungseinträgen für die HTTP-Endpunkte des Services. |
Service-Aktivierung
Eine services.json-Datei im Verzeichnis services/ steuert, welche Services aktiviert oder deaktiviert sind:
{
"my-alarm-export": { "enabled": true },
"legacy-connector": { "enabled": false }
}
Die Aktivierung wird pro Hierarchieebene ausgewertet. Eine services.json auf einer höheren Prioritätsebene (Projekt) überschreibt Einstellungen von niedrigeren Ebenen (Installation). Services, die in keiner services.json aufgeführt sind, sind standardmäßig aktiviert.
Änderungen an services.json werden vom File-Watcher erkannt und wirken sofort — neu aktivierte Services werden geladen, neu deaktivierte Services werden entladen.
Service-Lebenszyklus
Jeder Service durchläuft die folgenden Zustände:
- Discovered — Das Service-Verzeichnis wurde gefunden und das Manifest wurde geparst.
- Loading — Das Ausgabeverzeichnis wird überprüft und JavaScript-Module
werden importiert. Der
initialize()-Hook wird aufgerufen. - Running — Handler sind registriert, HTTP-Routen eingehängt und ACL-Einträge gesetzt. Der Service verarbeitet Anfragen.
- Draining — Eine Codeänderung oder Deaktivierung wurde erkannt. Neue Anfragen werden in eine Warteschlange eingereiht, während laufende Anfragen abgeschlossen werden (mit konfigurierbarem Timeout).
- Unloading — Der
dispose()-Hook wird aufgerufen. Handler werden abgemeldet, Routen ausgehängt und ACL-Einträge entfernt.
Nach dem Entladen kehrt der Service in den Zustand Discovered zurück, wenn er neu geladen wird, oder wird vollständig entfernt, wenn sein Verzeichnis gelöscht wurde.
Wenn ein Service nicht geladen werden kann (ungültiges Manifest, fehlendes
Ausgabeverzeichnis oder initialize()-Fehler), wechselt er in den
Zustand Error. Der Service-Manager protokolliert den Fehler und fährt mit
dem Laden anderer Services fort. Der fehlgeschlagene Service wird automatisch
erneut versucht, wenn sich seine Quelldateien ändern.
Das IWsjService-Interface
Ein Service-Modul kann optional eine Klasse oder ein Objekt exportieren, das das
IWsjService-Interface für die Lebenszyklusverwaltung
implementiert:
export interface IWsjService {
/**
* Called after the module is loaded and before handlers/endpoints
* are registered. Use for one-time setup.
*/
initialize?(context: WsjServiceContext): Promise<void>;
/**
* Called before the service is unloaded. Use for cleanup:
* close connections, cancel timers, release resources.
*/
dispose?(): Promise<void>;
}
Beide Hooks sind optional. Wenn ein Service nur deklarative Handler und Endpunkte über das Manifest bereitstellt, ist keine Code-Level-Lebenszyklusverwaltung erforderlich.
Der WsjServiceContext, der an initialize()
übergeben wird, bietet Zugriff auf:
winccoa— DieWinccoaManager-Instanz für Datapoint-Operationen.manifest— Das geparste Service-Manifest.serviceDir— Absoluter Pfad zum Stammverzeichnis des Services.acl— DieWsjAccessControlListfür programmatische ACL-Verwaltung.app— Die Express-App für erweiterte Routing-Anforderungen.log— Ein auf den Service-Namen bezogener Logger.
Request-Handler
Dynamische Services registrieren WebSocket-Request-Handler über die bestehende
WsjRequestHandlerRegistry. Jeder Handler muss
WsjRequestHandlerBase erweitern und ein eindeutiges
prefix definieren:
import { WsjRequestHandlerBase, WsjRequestResult,
WsjConnectionContext } from '@wincc-oa/backend';
export class AlarmExportHandler extends WsjRequestHandlerBase {
public get prefix(): string {
return 'alarms.export.';
}
public async handleRequest(
command: string,
params: Record<string, unknown>,
result: WsjRequestResult,
context: WsjConnectionContext
): Promise<WsjRequestResult> {
// Handle commands like 'alarms.export.csv', 'alarms.export.pdf'
result.setSuccess({ format: command, data: '...' });
return result;
}
}
Das Handler-Prefix muss über alle Services hinweg eindeutig sein. Wenn zwei Services versuchen, dasselbe Prefix zu registrieren, schlägt die zweite Registrierung fehl und der Konflikt wird protokolliert.
HTTP-Endpunkte
HTTP-Endpunkte sind Express-ähnliche Routen, die in einer Routes-Funktion definiert
werden. Der Service-Loader injiziert die Router-Factory als erstes
Argument, da das intern verwendete HTTP-Framework gebündelt ist und nicht direkt
importiert werden kann:
export function routes(Router: any) {
const router = Router();
router.get('/status', (req: any, res: any) => {
res.json({ status: 'ok' });
});
router.post('/export', async (req: any, res: any) => {
// Handle export request
res.json({ result: 'exported' });
});
return router;
}
Der Funktionsname muss mit dem routesFunction-Wert im Manifest
übereinstimmen (Standard: "routes"). Die Routen werden unter dem
im Manifest angegebenen mountPath eingehängt.
Service-Routen werden nach allen statischen Routen eingehängt, sodass statische Routen immer Vorrang haben.
Zugriffssteuerung
Jeder Service kann ACL-Regeln für seine HTTP-Endpunkte im Manifest deklarieren. Diese Regeln werden beim Laden des Services registriert und beim Entladen entfernt:
"acl": [
{
"path": "/alarms/export",
"allowUsers": "*"
},
{
"path": "/alarms/public/*",
"allowDefault": true
},
{
"path": "/alarms/admin/*",
"allowUsers": ["root", "admin"],
"denyUsers": ["guest"]
}
]
Verfügbare ACL-Felder:
path— Das URL-Pfadmuster, das geschützt werden soll (unterstützt Wildcards).allowUsers— Benutzer mit Zugriffsberechtigung."*"erfordert Authentifizierung ohne Einschränkung auf bestimmte Benutzer.denyUsers— Benutzer, denen der Zugriff explizit verweigert wird.allowDefault— Beitrueist der Zugriff ohne Authentifizierung erlaubt (offener Zugriff).allowUnknown— Zugriff für unbekannte/anonyme Benutzer erlauben.allowDisabled— Zugriff für deaktivierte Benutzerkonten erlauben.oidcAuthCodeFlow— OIDC-Authorization-Code-Flow für diesen Pfad aktivieren.
Hot-Reload
Der Service-Manager überwacht alle Service-Ausgabeverzeichnisse auf Änderungen von JavaScript-Dateien (.js, .mjs, .cjs). Wenn eine Änderung erkannt wird:
- Das Ereignis wird entprellt (Standard: 1000 ms), um schnelle aufeinanderfolgende Änderungen zusammenzufassen.
- Der Service wechselt in den Zustand Draining — neue Anfragen werden in eine Warteschlange eingereiht, während laufende Anfragen abgeschlossen werden.
- Nachdem alle ausstehenden Anfragen abgeschlossen sind (oder nach einem
Timeout), wird der alte Service entladen (
dispose()wird aufgerufen, Handler und Routen werden entfernt). - Das neue Modul wird geladen,
initialize()wird aufgerufen und Handler und Routen werden neu registriert. - In der Warteschlange befindliche Anfragen werden von den neuen Handlern verarbeitet.
Dies ermöglicht einen nahtlosen Entwicklungsworkflow: Ein TypeScript-Build-Tool im Watch-Modus ausführen, und Änderungen werden automatisch wirksam, ohne den Server neu zu starten.
"webserverjs-service"-Schlüssel ändert, wird der Service
neu geladen. Wenn package.json gelöscht wird, wird der Service
entladen.Erstellen von Services
Der Service-Manager lädt vorkompiliertes JavaScript aus dem Ausgabeverzeichnis. Das Erstellen (TypeScript-Kompilierung, Bundling, Abhängigkeitsinstallation) liegt in der Verantwortung des Entwicklers. Jedes Build-Tool kann verwendet werden (tsc, esbuild, rollup, etc.).
Wenn ein Service npm-Pakete benötigt:
- Die Pakete werden im Feld
dependenciesder package.json des Services deklariert. npm installwird im Service-Verzeichnis vor dem Start des Webservers ausgeführt.
Das Verzeichnis node_modules/ bleibt über Server-Neustarts hinweg bestehen.
Services können importieren von:
- Node.js Built-ins (
node:fs,node:path, etc.) - Eigene node_modules
- Die öffentliche webserver.js API über
@wincc-oa/backend winccoa-manager(immer im Hostprozess verfügbar)
ultimate-express) ist in den
Webserver gebündelt und kann nicht von Services importiert werden. Stattdessen ist
der injizierte Router-Parameter in der Routes-Funktion zu
verwenden.Scaffolding eines neuen Services
Mit dem Scaffolding-Tool wird ein neuer Service mit der korrekten Verzeichnisstruktur und dem Manifest erzeugt:
npx @wincc-oa/create-backend-service my-service
Dadurch wird erstellt:
my-service/
package.json (vorbefüllt mit "webserverjs-service"-Schlüssel)
src/
exampleHandler.ts
exampleRoutes.ts
dist/ (vorkompiliert, bereit zum Laden)
exampleHandler.js
exampleRoutes.js
Das generierte Verzeichnis wird in javascript/webserver-js/services/ im Projekt platziert. Der Service wird automatisch erkannt und geladen.
Beispiel: Weather Alerts Service
Das folgende Beispiel zeigt einen vollständigen dynamischen Service, der eine Echtzeit-Wetterwarnung-Integration bereitstellt.
Verzeichnisstruktur:
javascript/webserver-js/services/
weather-alerts/
package.json
dist/
weatherAlertHandler.js
weatherRoutes.js
src/
weatherAlertHandler.ts
weatherRoutes.ts
package.json:
{
"name": "weather-alerts",
"version": "1.0.0",
"description": "Weather alert integration for SCADA dashboards",
"dependencies": {
"node-fetch": "^3.0.0"
},
"webserverjs-service": {
"version": 1,
"requestHandlers": [
{
"source": "weatherAlertHandler.js",
"className": "WeatherAlertHandler"
}
],
"httpEndpoints": [
{
"mountPath": "/weather/current",
"source": "weatherRoutes.js",
"routesFunction": "routes"
}
],
"acl": [
{
"path": "/weather/*",
"allowUsers": "*"
}
]
}
}
src/weatherAlertHandler.ts:
import { WsjRequestHandlerBase, WsjRequestResult,
WsjConnectionContext } from '@wincc-oa/backend';
export class WeatherAlertHandler extends WsjRequestHandlerBase {
public get prefix(): string {
return 'weather.alerts.';
}
public async handleRequest(
command: string,
params: Record<string, unknown>,
result: WsjRequestResult,
context: WsjConnectionContext
): Promise<WsjRequestResult> {
switch (command) {
case 'current':
const fetch = (await import('node-fetch')).default;
const response = await fetch(
'https://api.weather.gov/alerts/active'
);
const data = await response.json();
result.setSuccess(data);
return result;
default:
return super.handleRequest(command, params, result, context);
}
}
}
src/weatherRoutes.ts:
export function routes(Router: any) {
const router = Router();
router.get('/', async (req: any, res: any) => {
const fetch = (await import('node-fetch')).default;
const response = await fetch(
`https://api.weather.gov/points/${req.query.lat},${req.query.lon}`
);
const data = await response.json();
res.json(data);
});
return router;
}
Debugging
Detailliertes Logging des Service-Lebenszyklus wird mit dem Debug-Flag
WSJ_SVC aktiviert:
-dbg WSJ_SVC
Dadurch werden Trace-Meldungen für Service-Erkennung, Laden, Entladen, File-Watcher-Ereignisse und ACL-Registrierung ausgegeben.
Service-Status-Endpunkt
Der integrierte Endpunkt GET /_services gibt den Status aller
erkannten Services zurück:
{
"services": [
{
"name": "my-alarm-export",
"state": "running",
"version": 1,
"handlers": ["alarms.export."],
"endpoints": ["/alarms/export"],
"loadedAt": "2026-03-26T10:15:30.000Z",
"source": "/path/to/services/my-alarm-export"
}
]
}
Dieser Endpunkt erfordert Authentifizierung, wenn die Zugriffssteuerung aktiviert ist.
Konfiguration
Das Verhalten des Service-Ladens wird im Abschnitt [webserverjs]
der Projektkonfigurationsdatei konfiguriert. Details siehe Konfiguration dynamischer Services.
Migration von statischen Erweiterungen
Bestehende Kunden, die das CustomerDashboardServer-Subclass-Muster
verwenden, werden weiterhin unverändert unterstützt. Dynamische Services sind
additiv und ersetzen den statischen Erweiterungsmechanismus nicht.
Um einen statischen Handler schrittweise zu einem dynamischen Service zu migrieren:
- Ein Service-Verzeichnis mit einer package.json erstellen,
die den
"webserverjs-service"-Schlüssel enthält. - Die Handler-Quelldatei in das Verzeichnis src/ des Services verschieben.
- Die Handler-Registrierung aus
registerStandardHandlers()entfernen. - Den Service erstellen. Der Handler ist jetzt hot-reload-fähig.
