In älteren Angular-Versionen (vor Angular 14) war das NgModule-Konzept das zentrale Organisationsprinzip einer Angular-Anwendung. Module dienten als Container, die zusammengehörige Komponenten, Direktiven, Pipes und Services gruppierten. Diese Herangehensweise wurde aus gutem Grund entwickelt - sie ermöglichte effizientes Lazy Loading und half, die Anwendung zu strukturieren.
Mit der Einführung von Standalone Components in Angular 14 und deren vollständiger Unterstützung in Angular 15 hat sich dieser Ansatz grundlegend verändert. Standalone Components repräsentieren einen Paradigmenwechsel in der Art und Weise, wie Angular-Anwendungen strukturiert werden.
Betrachten wir die traditionelle modulbasierte Herangehensweise:
// feature.module.ts
@NgModule({
declarations: [
FeatureComponent,
HelperDirective,
FormatPipe
],
imports: [
CommonModule,
SharedModule
],
providers: [
FeatureService
],
exports: [
FeatureComponent
]
})
export class FeatureModule { }
// feature.component.ts
@Component({
selector: 'app-feature',
templateUrl: './feature.component.html'
})
export class FeatureComponent { }In diesem Modell musste jede Komponente in einem Modul deklariert werden. Das Modul definierte dann, welche anderen Module importiert werden, welche Services als Provider bereitgestellt werden und welche Komponenten nach außen exportiert werden sollten.
Die neue Standalone-Herangehensweise sieht deutlich anders aus:
// feature.component.ts
@Component({
selector: 'app-feature',
templateUrl: './feature.component.html',
standalone: true,
imports: [
CommonModule,
SharedComponent,
HelperDirective,
FormatPipe
],
providers: [
FeatureService
]
})
export class FeatureComponent { }Der Unterschied ist signifikant: Die Komponente selbst deklariert nun alle ihre Abhängigkeiten direkt, ohne den Umweg über ein umgebendes Modul.
Der @Component-Decorator ist das Herzstück jeder
Angular-Komponente. Mit der Einführung von Standalone Components haben
sich einige wichtige Parameter hinzugesellt:
@Component({
// Grundlegende Eigenschaften
selector: 'app-example', // Der HTML-Selector zum Einbinden der Komponente
templateUrl: './example.component.html', // Pfad zum HTML-Template
styleUrls: ['./example.component.css'], // Pfade zu CSS-Dateien
// Standalone-spezifische Eigenschaften
standalone: true, // Markiert die Komponente als standalone
imports: [...], // Importierte Abhängigkeiten
// Dependency Injection
providers: [...], // Lokale Service-Provider
viewProviders: [...], // Provider, die nur für die View dieser Komponente gelten
// Weitere wichtige Eigenschaften
changeDetection: ChangeDetectionStrategy.OnPush, // Change-Detection-Strategie
encapsulation: ViewEncapsulation.Emulated // Style-Encapsulation
})
export class ExampleComponent { }standalone und importsstandalone: true ParameterDie standalone: true Eigenschaft ist der Schlüssel zur
modulfreien Angular-Welt. Sie signalisiert Angular:
imports anderer
Standalone-Komponenten oder in der bootstrapComponent
verwendet werden@Component({
selector: 'app-root',
templateUrl: './app.component.html',
standalone: true,
imports: [
// Hier werden alle benötigten Abhängigkeiten importiert
]
})
export class AppComponent { }imports
ParameterDer imports Parameter ist das Herzstück des
Standalone-Konzepts. Er definiert, welche anderen Komponenten,
Direktiven oder Pipes innerhalb dieser Komponente verwendet werden
können:
@Component({
selector: 'app-feature',
templateUrl: './feature.component.html',
standalone: true,
imports: [
// Angular-eigene Module
CommonModule, // Für ngIf, ngFor etc.
RouterModule, // Für Router-Direktiven
ReactiveFormsModule, // Für Formulare
// Andere Standalone-Komponenten
HeaderComponent,
FooterComponent,
// Standalone-Direktiven
HighlightDirective,
// Standalone-Pipes
FormatDatePipe
]
})
export class FeatureComponent { }Die imports definieren den “Sichtbarkeitsbereich”
innerhalb des Templates. Nur was hier importiert wurde, kann im Template
verwendet werden.
Provider sind ein zentrales Konzept in Angular’s Dependency Injection (DI) System. Sie teilen Angular mit, wie Instanzen bestimmter Klassen erzeugt werden sollen, wenn sie angefordert werden.
Der häufigste Provider-Typ ist der ClassProvider, der
eine Klasse direkt bereitstellt:
providers: [
UserService, // Kurzform für { provide: UserService, useClass: UserService }
]Es gibt aber auch andere Arten von Providern: -
ValueProvider: Stellt einen festen Wert bereit -
FactoryProvider: Verwendet eine Fabrikfunktion zur
Erstellung - ExistingProvider: Leitet auf einen
existierenden Provider weiter
In einer Standalone Component können Provider direkt im
providers-Array definiert werden:
@Component({
selector: 'app-feature',
templateUrl: './feature.component.html',
standalone: true,
providers: [
FeatureService,
{ provide: CONFIG_TOKEN, useValue: { apiEndpoint: '/api' } }
]
})
export class FeatureComponent { }Diese Provider sind in der gesamten Komponenten-Hierarchie unterhalb dieser Komponente verfügbar - ein wichtiger Punkt für das Verständnis des “Provider-Baums” in Angular.
Ein entscheidender Unterschied zwischen verschiedenen Angular-Elementen:
Implizite Provider: - Komponenten - Direktiven - Pipes
Diese werden automatisch (implizit) im Dependency Injection System
registriert, wenn sie in imports aufgeführt sind.
Explizite Provider: - Services - Tokens - Andere Werte
Diese müssen explizit im providers-Array einer
Komponente oder der Bootstrap-Komponente registriert werden.
// Implizite Provider durch Import
@Component({
standalone: true,
imports: [
UserComponent, // Komponente wird implizit provided
HighlightDirective, // Direktive wird implizit provided
FormatPipe // Pipe wird implizit provided
]
})
export class AppComponent { }
// Explizite Provider
@Component({
standalone: true,
providers: [
UserService, // Service muss explizit provided werden
{ provide: API_URL, useValue: 'https://api.example.com' } // Token muss explizit provided werden
]
})
export class AppComponent { }Angular’s Dependency Injection System funktioniert hierarchisch, ähnlich wie die Komponenten-Hierarchie selbst. Dieses Hierarchie-Prinzip ist entscheidend für das Verständnis, wie Provider und Services in Angular funktionieren.
// bootstrap.ts
bootstrapApplication(AppComponent, {
providers: [
// Root-Provider, global für die gesamte Anwendung
{ provide: APP_CONFIG, useValue: { production: environment.production } },
provideRouter(routes),
provideAnimations()
]
});
// app.component.ts
@Component({
standalone: true,
providers: [
// Diese Provider sind für AppComponent und alle Kind-Komponenten verfügbar
UserService
]
})
export class AppComponent { }
// feature.component.ts
@Component({
standalone: true,
providers: [
// Diese Provider sind nur für FeatureComponent und deren Kind-Komponenten verfügbar
FeatureService
]
})
export class FeatureComponent { }Wenn eine Komponente einen Service anfordert, sucht Angular: 1. Zuerst im eigenen Element-Injector 2. Dann im Element-Injector der Eltern-Komponente 3. So weiter nach oben bis zur Root-Komponente 4. Schließlich im Root-Injector (definiert beim Bootstrap)
Dies erklärt, warum Services “singleton” sein können oder mehrfach instanziert, je nachdem, wo sie bereitgestellt werden.
Diese Vorgehensweise ist in gewisser Weise mit Spring Boot vergleichbar:
Spring Boot: - Verwendet Annotation-Scanning, um
Komponenten zu finden - @Component, @Service,
@Controller etc. markieren Klassen für DI - Spring erstellt
einen Anwendungskontext mit Beans - Hierarchische Bean-Container
ermöglichen unterschiedliche Sichtbarkeiten
Angular Standalone: - Verwendet explizite Imports statt Scanning - Komponenten, Direktiven und Pipes werden automatisch registriert - Services müssen explizit als Provider definiert werden - Hierarchischer Injector-Baum mit definierten Sichtbarkeitsbereichen
Der Hauptunterschied liegt im “expliziten” Ansatz von Angular gegenüber dem “impliziten” Scanning-Ansatz von Spring Boot. Angular erfordert, dass Abhängigkeiten explizit importiert werden, während Spring Boot Komponenten durch Classpath-Scanning findet.
Betrachten wir ein vollständiges Beispiel einer Feature-Komponente mit Standalone-Ansatz:
// user.service.ts
@Injectable()
export class UserService {
getUsers() {
return of([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
}
}
// highlight.directive.ts
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() highlightColor = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
// format-date.pipe.ts
@Pipe({
name: 'formatDate',
standalone: true
})
export class FormatDatePipe implements PipeTransform {
transform(value: Date): string {
return new Intl.DateTimeFormat('de-DE').format(value);
}
}
// user-list.component.ts
@Component({
selector: 'app-user-list',
template: `
<h2 appHighlight>Benutzerliste</h2>
<p>Letztes Update: {{ lastUpdate | formatDate }}</p>
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`,
standalone: true,
imports: [
CommonModule, // Für ngFor
HighlightDirective, // Unsere Standalone-Direktive
FormatDatePipe // Unsere Standalone-Pipe
],
providers: [
UserService // Der Service wird explizit bereitgestellt
]
})
export class UserListComponent implements OnInit {
users: any[] = [];
lastUpdate = new Date();
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
}
// app.component.ts
@Component({
selector: 'app-root',
template: `
<h1>Meine Anwendung</h1>
<app-user-list></app-user-list>
`,
standalone: true,
imports: [
UserListComponent // Wir importieren unsere Feature-Komponente
]
})
export class AppComponent {}
// main.ts
bootstrapApplication(AppComponent, {
providers: [
// Globale Provider hier
provideRouter([
{ path: '', component: AppComponent }
])
]
});In diesem Beispiel: 1. Wir haben eine vollständige Anwendung ohne ein
einziges NgModule 2. Jede Komponente, Direktive und Pipe ist standalone
3. Die Abhängigkeiten werden explizit importiert, wo sie benötigt werden
4. Services werden explizit als Provider bereitgestellt 5. Die Anwendung
beginnt mit bootstrapApplication anstelle von
NgModule-Bootstrap
Zum Abschluss eine direkte Gegenüberstellung beider Ansätze:
// user.module.ts
@NgModule({
declarations: [
UserListComponent,
HighlightDirective,
FormatDatePipe
],
imports: [
CommonModule
],
providers: [
UserService
],
exports: [
UserListComponent
]
})
export class UserModule { }
// app.module.ts
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
UserModule
],
bootstrap: [AppComponent]
})
export class AppModule { }// user-list.component.ts (und andere Komponenten)
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
standalone: true,
imports: [
CommonModule,
HighlightDirective,
FormatDatePipe
],
providers: [
UserService
]
})
export class UserListComponent { }
// app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
standalone: true,
imports: [
UserListComponent
]
})
export class AppComponent { }
// main.ts
bootstrapApplication(AppComponent, {
providers: [
// Globale Provider
]
});Die Umstellung auf Standalone Components kann in bestehenden Projekten herausfordernd sein. Angular bietet daher Hilfsmittel für den Übergang:
// Einbinden von NgModule-basierten Komponenten in Standalone
@Component({
standalone: true,
imports: [
// Import eines gesamten Moduls
importProvidersFrom(LegacyModule)
]
})
export class NewComponent { }
// Umwandlung eines NgModule in Standalone-Komponenten
const providers = convertProviders(LegacyModule);