DI и общая учебная сессия
Список и будущая карточка должны одинаково понимать выбранную учебную роль. Если в каждом компоненте завести отдельный signal role, кнопка в шапке изменит только один экземпляр. Выделим Session и получим его через dependency injection, сокращённо DI. Это механизм предоставления зависимостей: компонент объявляет, какой объект ему нужен, а настроенный injector решает, как этот объект создать и где разделять его между потребителями.
В этой главе Session не выполняет вход пользователя. Он хранит публичный переключатель двух вымышленных ролей: viewer и dispatcher. Такой объект помогает изучить область состояния и позднее объяснить интерфейсную границу записи. Безопасную аутентификацию он не создаёт. Различие особенно важно потому, что код браузера доступен пользователю: скрытый метод или недоступная кнопка не могут заменить серверную проверку права на конкретное действие.
Зависимость с одной областью жизни
Ниже полный файл src/app/session.ts конечной версии. В нём используется Injectable с providedIn root, поддерживаемый выбранным Angular 22. Экземпляр предоставляется на уровне приложения. App и DetailPage вызывают inject(Session) в инициализаторах своих полей, поэтому получают зависимость в контексте создания компонентов. Вызывать inject из случайной обычной функции после клика нельзя без предусмотренного injection context. Обработчик пользуется уже полученным объектом Session.
import { Injectable, computed, signal } from '@angular/core';
import { demoTokens } from './config';
@Injectable({providedIn: 'root'})
export class Session {
readonly role = signal<'viewer' | 'dispatcher'>('viewer');
readonly canEdit = computed(() => this.role() === 'dispatcher');
readonly token = computed(() => demoTokens[this.role()]);
}
Поле role — исходное состояние, canEdit — вычисляемый ответ на вопрос о возможности интерфейсного сохранения, token — выбранное значение из demoTokens. При переключении роли все три чтения остаются согласованы. Если хранить canEdit отдельным изменяемым boolean, можно случайно установить viewer и забыть снять право интерфейса. Computed выражает простое правило одним местом. При этом настоящее разрешение API всё равно зависит от проверки сервером полученного токена.
Ожидается, что нажатие «Диспетчер» в App изменит подпись выбранной роли и позволит DetailPage показать кнопку сохранения. Возвращение к наблюдателю уберёт её. Оба компонента читают один root-экземпляр, хотя находятся в разных частях дерева представления. Это отличается от создания new Session() в каждом файле: обычный конструктор создаст новые независимые signals, а DI использует объявленную область предоставления. Совпадение имён классов не объединяет их экземпляры автоматически.
Класс сервиса и его потребитель
У Session нет template и selector. Его задача — предоставить состояние и правило, которыми пользуется интерфейс. Компонент отвечает за кнопки и поясняющий текст, сервис — за выбранное значение. Такая граница позволяет изменить оформление шапки без переписывания расчёта canEdit. Но сервис не должен постепенно превращаться в контейнер любого состояния кабинета. Поиск списка, незавершённый черновик и роль имеют разную жизнь и разные причины изменения.
В конечном TicketsApi аналогично внедряется HttpClient. Это другая зависимость с другой ответственностью: транспорт отправляет запросы и проверяет возвращённую wire-модель. Session не становится HTTP-клиентом лишь потому, что authInterceptor читает из него токен. Напротив, interceptor связывает две границы в одном конкретном месте: получает текущую учебную сессию и клонирует только запросы к /api. Такая связка позволяет понять направление зависимостей по содержанию операции.
Angular injector может предоставлять объект в более узкой области. Если добавить Session в providers отдельного компонента, возникнет местный экземпляр, который способен скрыть root-предоставление для потомков. В результате переключатель шапки и редактор внутри другого поддерева могут читать разные роли. Это не сбой signal, а выбранная конфигурация DI. Для нашей лаборатории намеренно оставим одну root-область. Локальное предоставление пригодилось бы самостоятельной панели с независимым состоянием, но такой панели сейчас нет.
Тип и ключ зависимости
В простом случае класс Session одновременно служит описанием объекта и ключом, по которому injector ищет provider. Для настроек можно использовать InjectionToken: он позволяет выразить зависимость от значения без конструктора сервиса. В конечном архиве настройки представлены обычным экспортом config, поскольку их два и они статичны. Это осознанное упрощение. Не нужно создавать токен и фабрику только для демонстрации всех вариантов DI, если задача уже понятно решена текущей формой.
Обратите внимание на тестируемую логическую границу даже без запуска тестов. Компоненту не требуется знать, где именно лежит массив demoTokens: он читает session.canEdit() или session.role(). Транспорт получает session.token(). При будущем переходе на серверную сессию потребуется изменить источник и lifecycle, однако не каждую строку шаблона. Такое отделение зависимостей сокращает число мест, где знание об учебной авторизации может случайно превратиться в предметное правило.
Root-область не означает сохранение между перезагрузками браузера. Новый bootstrap создаст новое приложение и новый Session с viewer по умолчанию. Мы не записываем роль в localStorage и не обещаем восстановление входа. Даже если добавить такое сохранение, доверять записанной пользователем роли серверу нельзя. Хранилище браузера подходит для предпочтений отображения, но содержимое может изменяться вне интерфейса. В лаборатории начальная роль после каждого нового открытия помогает явно увидеть учебный режим.
Рассмотрите упражнение с двумя потребителями: добавьте в шапку текст выбранной роли, а рядом со списком текст canEdit. Ожидается согласованное изменение обоих значений при одном нажатии. Затем мысленно поместите Session в providers только списка и объясните, почему связь нарушится. Задача раскрывает область объектов значительно лучше, чем добавление ещё одного случайного сервиса. К концу главы вы должны различать имя зависимости, способ создания и конкретную область экземпляра.
С появлением Router страницы будут создаваться и уничтожаться при переходах, а Session продолжит жить в приложении. Это полезно: выбор учебной роли не должен сбрасываться только из-за открытия rq-101. Однако черновик карточки привяжется к её текущей основе и получит другие правила. На следующем шаге мы дадим списку и заявке собственные адреса. DI уже подготовил общую зависимость, которая не принадлежит ни одному из этих маршрутов отдельно.
Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.
Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.