Angular предоставляет мощный механизм работы с сервисами и их зависимостями, который помогает строить масштабируемые и легко тестируемые приложения. В основе этого механизма лежит инверсия управления (IoC) и внедрение зависимостей (DI), что позволяет централизованно управлять жизненным циклом объектов и их зависимостями. Этот подход не только повышает модульность, но и улучшает повторное использование кода.
Сервисом в Angular называют класс, предоставляющий определённую функциональность, которая может быть использована в различных частях приложения. Сервисы не привязаны напрямую к представлению (UI) и выполняют задачи, такие как управление данными, работа с внешними API или выполнение бизнес-логики.
Для создания сервиса в Angular используется класс с методами, которые реализуют требуемую функциональность. Пример простого сервиса:
@Injectable({
providedIn: 'root'
})
export class DataService {
private data: string[] = ['Item 1', 'Item 2', 'Item 3'];
getData(): string[] {
return this.data;
}
addData(item: string): void {
this.data.push(item);
}
}
В этом примере сервис DataService управляет коллекцией
данных. Методы getData() и addData()
обеспечивают доступ к этим данным. Аннотация @Injectable
сообщает Angular, что этот класс может быть внедрён в другие компоненты
или сервисы.
Внедрение зависимостей — это паттерн, который позволяет объектам получать свои зависимости из внешнего источника, а не создавать их самостоятельно. В Angular это реализуется через механизм DI. Когда компоненты или сервисы требуют какие-либо зависимости (например, другие сервисы или данные), Angular автоматически предоставляет их, используя контейнер зависимостей.
Основная цель DI — обеспечить слабую связанность компонентов и упростить тестирование. Вместо того чтобы создавать объекты внутри компонентов, их можно получить через конструктор.
Пример внедрения сервиса в компонент:
@Component({
selector: 'app-data-list',
templateUrl: './data-list.component.html'
})
export class DataListComponent {
data: string[];
constructor(private dataService: DataService) {
this.data = this.dataService.getData();
}
}
Здесь компонент DataListComponent получает экземпляр
DataService через конструктор. Angular автоматически
подставляет нужный сервис в момент создания компонента, благодаря
механизму DI.
Сервисы могут быть предоставлены на разных уровнях видимости. Это позволяет контролировать, сколько экземпляров сервиса будет создано в приложении. В Angular есть несколько вариантов управления областью видимости сервисов:
root (Корневой уровень): сервис будет доступен во всей приложении. Это стандартный способ предоставления сервиса.
@Injectable({
providedIn: 'root'
})
export class DataService { ... }Module (Модульный уровень): сервис будет
доступен только в определённом модуле. Для этого сервис нужно добавить в
массив providers модуля.
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [DataService],
bootstrap: [AppComponent]
})
export class AppModule { }Component (Компонентный уровень): сервис может быть ограничен областью видимости конкретного компонента. Это полезно, если сервис должен быть использован только в одном компоненте и его дочерних компонентах.
@Component({
selector: 'app-data-list',
providers: [DataService],
templateUrl: './data-list.component.html'
})
export class DataListComponent { ... }Жизненный цикл сервисов в Angular управляется с помощью DI
контейнера. Когда сервис предоставлен на уровне root,
Angular создаёт его экземпляр при первом запросе и использует его
повторно в течение всей жизни приложения. Если сервис предоставлен на
уровне компонента или модуля, то Angular создаёт новый экземпляр при
каждом создании компонента или модуля.
Пример управления жизненным циклом:
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string): void {
console.log(message);
}
}
Если сервис предоставлен на уровне root, его экземпляр
будет существовать на протяжении всего жизненного цикла приложения. Это
полезно для сервисов, таких как LoggerService, которые
должны быть одноразовыми для всего приложения.
Одной из ключевых причин для использования DI является лёгкость тестирования компонентов. В процессе тестирования можно подменять реальные сервисы на их моки (фейковые реализации), что позволяет изолировать тестируемую логику.
Пример тестирования компонента с использованием мок-сервиса:
class MockDataService {
getData() {
return ['Mock Item 1', 'Mock Item 2'];
}
}
describe('DataListComponent', () => {
let component: DataListComponent;
let fixture: ComponentFixture<DataListComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DataListComponent],
providers: [{ provide: DataService, useClass: MockDataService }]
});
fixture = TestBed.createComponent(DataListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display mock data', () => {
expect(component.data).toEqual(['Mock Item 1', 'Mock Item 2']);
});
});
Здесь MockDataService используется вместо настоящего
DataService. Это позволяет протестировать логику
компонента, не обращаясь к реальному сервису, и облегчает изоляцию
тестов.
Часто сервисы выполняют асинхронные операции, такие как запросы к
серверу. В таких случаях сервисы возвращают Observable или
Promise, что позволяет компонентам реагировать на изменения
данных, не блокируя выполнение программы.
Пример сервиса с асинхронным запросом:
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
getData(): Observable<any> {
return this.http.get('https://api.example.com/data');
}
}
В компоненте данные могут быть получены асинхронно с использованием
async pipe или подписки на Observable.
@Component({
selector: 'app-data-list',
template: `
<div *ngIf="data$ | async as data">
<ul>
<li *ngFor="let item of data">{{ item }}</li>
</ul>
</div>
`
})
export class DataListComponent {
data$: Observable<any>;
constructor(private apiService: ApiService) {
this.data$ = this.apiService.getData();
}
}
В этом примере используется асинхронная пайпа async,
которая подписывается на Observable, и автоматически
обновляет данные при их изменении.
Когда сервис имеет состояние, которое нужно обновлять и отслеживать в реальном времени, важно продумать, как это состояние будет управляться и как оно будет доступно другим частям приложения.
Пример сервиса с состоянием:
@Injectable({
providedIn: 'root'
})
export class StateService {
private count = 0;
getCount(): number {
return this.count;
}
increment(): void {
this.count++;
}
reset(): void {
this.count = 0;
}
}
Этот сервис управляет состоянием счётчика и предоставляет методы для
его увеличения и сброса. Состояние сохраняется между вызовами методов и
доступно в любой части приложения, где используется
StateService.
Для лучшей абстракции и тестируемости в Angular часто используется подход с интерфейсами. Это позволяет разработчикам определить контракты, которые должны реализовывать все сервисы, и сделать код более гибким и расширяемым.
Пример интерфейса для сервиса:
export interface IDataService {
getData(): string[];
addData(item: string): void;
}
@Injectable({
providedIn: 'root'
})
export class DataService implements IDataService {
private data: string[] = [];
getData(): string[] {
return this.data;
}
addData(item: string): void {
this.data.push(item);
}
}
Здесь IDataService определяет контракт для сервиса
данных. Это упрощает создание альтернативных реализаций сервиса и
улучшает поддержку кода.
Сервисы и внедрение зависимостей являются неотъемлемой частью разработки в Angular. Эти механизмы обеспечивают модульность, повторное использование кода и упрощают тестирование, что критически важно при создании масштабируемых и поддерживаемых прилож