Создание настраиваемых модулей Angular элементов с использованием шаблона стратегии.
Уборка дома требует усилий, но результат бесценен. То же самое касается чистого кода. Это требует усилий, но шаблоны определенно увеличивают продолжительность жизни кода. Когда мы создаем некоторые функциональные модули Angular и хотим использовать их повторно, нам часто приходится переписывать модуль либо обновляя конфигурацию или логику внутри некоторых классов в соответствии с нашими потребностями. Мы должны стараться изо всех сил избегать подобного рода практики для лучшего обслуживания кода.
Одним из аспектов чистого кода является обеспечение того, чтобы наш код был закрыт для модификации, но открыт для расширения. Давайте создадим модуль служебных функций, который будет инкапсулировать стратегии функций и, в зависимости от варианта использования, позволит нам выбирать соответствующие стратегии без каких-либо изменений.
Сегодня для демонстрации этой статьи мы создадим классический менеджер сессий с несколькими типами классов хранения. Каждый класс хранилища может обращаться к определенному хранилищу, например, к локальному хранилищу, хранилищу сеансов или файлов cookie. Когда этот диспетчер сеансов активно загружается корневым модулем, можно передать конфигурацию относительно того, какое хранилище он будет использовать во время выполнения.
Шаблон стратегии определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми. Стратегия позволяет алгоритму варьироваться независимо от клиентов, которые его используют.
Перед тем, как мы углубимся в код Angular, вот UML-диаграмма того, как мы планируем реализовать менеджер сессий, используя шаблон стратегии.
SessionStrategy - это интерфейс, который реализуется такими классами хранения, как LocalStorage и SessionStorage. Класс SessionContext будет просто ссылаться на этот интерфейс стратегии, что делает класс SessionContext независимым от того, как реализованы стратегии.
Затем мы создадим клиента, ответственность за который будет состоять в том, чтобы инициировать конкретный объект стратегии сеанса, такой как LocalStorage / SessionStorage, и передать его в контекст. Это было бы что-то вроде этого:
const context = new SessionContext();
const session = context.loadStorage();
const token = session.get('access_token');
Преимущества использования паттерна стратегии здесь заключаются в том, что мы разделили проблемы, можем со временем добавлять разные классы хранения и избегать сложных условий, чтобы каждый раз инициировать их.
Теперь давайте перейдем к Angular. Наверняка мы видели этот код много раз:
const routes: Routes = [{
path: 'heroes',
component: HeroesComponent
}];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Мы можем видеть функцию RouterModule.forRoot
, которая принимает маршруты типа Routes, которые мы можем определить в каждом проекте.
type Routes = Route[];
Таким образом, маршрутизатор принимает массив маршрутов и URL-адрес и пытается создать RouterState. Все это для того, чтобы приложение могло отобразить компонент при переходе к соответствующему URL.
Цель совместного использования этого фрагмента кода - показать, как forRoot() принимает объект конфигурации службы и возвращает ModuleWithProviders, который является простым объектом со свойствами, такими как ngModule, и провайдерами.
Давайте напишем наш SessionManagerModule. Он должен иметь доступ либо к локальному хранилищу, либо к хранилищу сеансов на основе наших конфигураций. Сначала мы создаем модуль диспетчера сеансов и создаем в нем структуру папок. Я объясню назначение этих папок по мере продвижения.
session-manager
- config
- storages
- strategy
session-manager.module.ts
session.service.ts
Таким образом, в папке session-manager у нас будет два класса: LocalStorage и SessionStorage. Каждый класс будет иметь функции для чтения / записи данных браузера.
Однако, прежде чем писать эти классы, давайте создадим интерфейс с именем SessionStrategy, который будет реализован обоими классами.
session-manager
- config
- storages
- strategy
- session-strategy.ts
session-manager.module.ts
session.service.ts
Мы создали файл session-strategy.ts и теперь давайте напишем этот класс.
export interface SessionStrategy {
get(key: string);
set(key: string, value: string | Object);
remove(key: string);
removeAll();
}
Теперь мы вернулись в папке storages создадим еще два класса:
session-manager
- config
- storages
- local-storage.ts
- session-storage.ts
- strategy
- session-strategy.ts
session-manager.module.ts
session.service.ts
Мы будем реализовывать SessionStrategy в наших двух новых классах:
import { SessionStrategy } from '../strategy/session-strategy';
export class LocalStorage implements SessionStrategy {
static setup(): any {
return LocalStorage;
}
public getStorage() {
return window.localStorage;
}
public get(key: string): any {
return this.getStorage().getItem(key);
}
...
И класс SessionStorage:
import { SessionStrategy } from '../strategy/session-strategy';
export class SessionStorage implements SessionStrategy {
static setup(): any {
return SessionStorage;
}
public getStorage() {
return window.sessionStorage;
}
public get(key: string): any {
return this.getStorage().getItem(key);
}
...
Теперь давайте создадим файл класса контекста в папке strategy.
session-manager
- config
- storages
- local-storage.ts
- session-storage.ts
- strategy
- session-strategy.ts
- session-context.ts
session-manager.module.ts
session.service.ts
Контекстный класс будет просто ссылаться на интерфейс стратегии для выполнения стратегии и ее функций.
import { SessionStrategy } from './session-strategy';
export class SessionContext {
public sessionStrategy: SessionStrategy;
constructor(sessionStrategy: SessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public loadStorage() {
return this.sessionStrategy;
}
}
Теперь мы будем создавать клиента, то есть конкретный объект стратегии сеанса, и передавать его в контекст. Наш SessionManagerModule заменит стратегию, связанную с контекстом, когда мы сделаем наш модуль настраиваемым.
Мы будем использовать Dependency Injection, чтобы сделать наш модуль настраиваемым. Для этого мы будем использовать InjectionToken.
Но сначала мы создадим два файла в папке config. Один интерфейс конфигурации называется session-manager-config.ts, а затем создайте токен, который реализует его, под названием session-manager-token.ts.
session-manager
- config
- session-manager-config.ts
- session-manager-token.ts
- storages
- local-storage.ts
- session-storage.ts
- strategy
- session-strategy.ts
session-manager.module.ts
session.service.ts
Чтобы создать InjectionToken
, давайте определим тип и имя токена. Назовем его SessionManagerConfig и запишем это в session-manager-config.ts.
import { SessionStrategy } from '../strategy/session-strategy';
export interface SessionManagerConfig {
storage: SessionStrategy;
// refreshTokenUrl: string;
}
Здесь вы можете видеть, что мы объявили один интерфейс, который будет иметь класс хранения SessionStrategy
, а также он может иметь дополнительные строки, такие как refreshTokenUrl
. Я просто добавил его, чтобы показать множество конфигов, которые мы можем использовать для DI.
Итак, давайте напишем токен.
import { InjectionToken, ModuleWithProviders } from '@angular/core';
import { SessionManagerConfig } from './session-manager-config';
export const configService = new InjectionToken('config');
Таким образом, мы создали InjectionToken типа SessionMangerConfig
. Теперь мы можем написать наш класс session.service.ts, который будет действовать как наш клиент, поскольку он будет инициализировать любую стратегию, внедряемую через модуль.
import { Injectable, Inject } from '@angular/core';
import { configService } from './config/session-manager-token';
import { SessionContext } from './strategy/session-context';
import { SessionStrategy } from './strategy/session-strategy';
@Injectable({
providedIn: 'root'
})
export class SessionService {
public static session: SessionStrategy;
private context: SessionContext;
constructor(
@Inject(configService) public config,
public http: HttpClient
) {
const storage = new config.storage();
this.context = new SessionContext(config.storage);
SessionService.session = this.context.loadStorage();
}
public getAccessToken(): string {
return SessionService.session.get('access_token');
}
public setAccessToken(value: string): void {
return SessionService.session.set('access_token', value);
}
...
Здесь в конструкторе, вы можете увидеть как контекст инициируется на основе конфигурации. Эта конфигурация, которая является объектом хранения, будет исходить из конфигурации модуля. В зависимости от того, как вы объявили SessionManagerModule
, getRefreshToken
будет получать значение refresh_token из localStorage или SessionStorage.
Давайте перейдем к нашему SessionManagerModule
и объявим функцию forRoot() и ожидаем один параметр с именем config типа SessionManagerConfig, который мы объявили ранее.
import { SessionService } from './session.service';
import { SessionManagerConfig } from './config/session-manager-config';
import { configService } from './config/session-manager-token';
import { LocalStorage } from './storages/local-storage';
import { SessionStorage } from './storages/session-storage';
export class SessionManagerModule {
static forRoot(config: SessionManagerConfig): ModuleWithProviders {
return {
ngModule: SessionManagerModule,
providers: [
LocalStorage,
SessionStorage,
SessionService,
{
provide: configService,
useValue: config || null
}
]
};
}
}
Итак, теперь мы обновляем AppModule и показываем, как использовать этот SessionManagerModule.
import { SessionManagerModule } from './session-manager/session-manager.module';
import { SessionManagerConfig } from './session-manager/config/session-manager-config';
import { LocalStorage } from './session-manager/storages/local-storage';
import { SessionStorage } from './session-manager/storages/session-storage';
const sessionConfig: SessionManagerConfig = {
storage: SessionStorage.setup()
};
@NgModule({
imports: [
SessionManagerModule.forRoot(sessionConfig),
],
declarations: [AppComponent],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Здесь мы могли бы передать экземпляр класса SessionStorage, если бы использовали компилятор JIT, но в AOT это не сработало бы:
const sessionConfig: SessionManagerConfig = {
storage: new SessionStorage()
};
Поскольку в AOT вызовы функции не поддерживаются в декораторах, мы вызываем установку статической функции, чтобы обеспечить проходы сборки AOT.
const sessionConfig: SessionManagerConfig = {
storage: SessionStorage.setup()
};
Таким образом, в конце, в зависимости от того, как вы объявили SessionManagerModule, getRefreshToken будет получать значение refresh_token из localStorage или SessionStorage.
Вы можете увидеть код в GitHub репозитории и отредактировать код напрямую, используя Stackblitz:
Contribute to Saad-Amjad/session-manager development by creating an account on GitHub.
В заключение, есть много способов внедрения зависимостей в Angular, для этой статьи мы показали вам использование InjectionToken. Чтобы настроить модуль, мы использовали forRoot() и передали его в конфигурацию. Вы можете проверить Angular Docs, чтобы понять, когда использовать forRoot() / forChild() в ваших приложениях. Вместо сложных операторов switch, чтобы сделать заменяемую реализацию хранилищ, которые мы получаем из config, мы использовали шаблон стратегии, гарантирующий, что модуль открыт для расширения, но закрыт для модификации.