32. Injection Tokens
🔑 InjectionToken и провайдеры в Angular
Заголовок раздела «🔑 InjectionToken и провайдеры в Angular»InjectionToken позволяет инжектировать не-классовые зависимости: строки, числа, объекты конфигурации, функции. Это мощный инструмент для создания гибкой архитектуры.
🎯 Проблема без InjectionToken
Заголовок раздела «🎯 Проблема без InjectionToken»// ❌ Нельзя инжектировать примитивы напрямую@Injectable()export class ApiService { constructor(@Inject('API_URL') private apiUrl: string) {} // строка не работает надёжно}InjectionToken решает это:
// ✅ Создаём типизированный токенimport { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');export const MAX_RETRIES = new InjectionToken<number>('MAX_RETRIES');export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');🔧 Создание и регистрация токена
Заголовок раздела «🔧 Создание и регистрация токена»import { InjectionToken } from '@angular/core';
export interface AppConfig { apiUrl: string; maxRetries: number; debug: boolean; featureFlags: { darkMode: boolean; betaFeatures: boolean; };}
export const APP_CONFIG = new InjectionToken<AppConfig>('AppConfig', { // Factory — значение по умолчанию providedIn: 'root', factory: () => ({ apiUrl: 'https://api.example.com', maxRetries: 3, debug: false, featureFlags: { darkMode: true, betaFeatures: false, } })});Или регистрируем в providers:
// main.ts (standalone)bootstrapApplication(AppComponent, { providers: [ { provide: APP_CONFIG, useValue: { apiUrl: environment.apiUrl, maxRetries: 3, debug: !environment.production, featureFlags: { darkMode: true, betaFeatures: false } } } ]});
// app.module.ts@NgModule({ providers: [ { provide: APP_CONFIG, useValue: { ... } } ]})export class AppModule {}📥 inject() — современный способ получить токен
Заголовок раздела «📥 inject() — современный способ получить токен»import { inject } from '@angular/core';import { APP_CONFIG } from './tokens';
@Injectable({ providedIn: 'root' })export class ApiService { private config = inject(APP_CONFIG);
getApiUrl() { return this.config.apiUrl; }
fetch(endpoint: string) { return this.http.get(`${this.config.apiUrl}/${endpoint}`); }}
// В компоненте@Component({ ... })export class DashboardComponent { private config = inject(APP_CONFIG); isDebug = this.config.debug;}🏭 useFactory — динамическое значение
Заголовок раздела «🏭 useFactory — динамическое значение»export const LOCALE_CONFIG = new InjectionToken<LocaleConfig>('LocaleConfig');
bootstrapApplication(AppComponent, { providers: [ { provide: LOCALE_CONFIG, useFactory: () => { // Берём локаль из браузера const userLocale = navigator.language.split('-')[0]; return { locale: userLocale, currency: userLocale === 'ru' ? 'RUB' : 'USD', dateFormat: userLocale === 'ru' ? 'dd.MM.yyyy' : 'MM/dd/yyyy', }; } } ]});🔗 useFactory с зависимостями
Заголовок раздела «🔗 useFactory с зависимостями»// Фабрика, зависящая от других провайдеров{ provide: HTTP_INTERCEPTORS, useFactory: (authService: AuthService) => { return new AuthInterceptor(authService); }, deps: [AuthService], multi: true}
// Или с inject() (Angular 14+){ provide: SOME_TOKEN, useFactory: () => { const authService = inject(AuthService); return new SomeService(authService); }}🌍 APP_INITIALIZER — код при старте
Заголовок раздела «🌍 APP_INITIALIZER — код при старте»APP_INITIALIZER запускает функции до рендера приложения:
import { APP_INITIALIZER, Provider } from '@angular/core';
// Загружаем конфигурацию с сервераfunction initializeApp(configService: ConfigService): () => Promise<void> { return () => configService.loadConfig();}
bootstrapApplication(AppComponent, { providers: [ { provide: APP_INITIALIZER, useFactory: (config: ConfigService) => () => config.loadConfig(), deps: [ConfigService], multi: true // ← ОБЯЗАТЕЛЬНО для APP_INITIALIZER } ]});@Injectable({ providedIn: 'root' })export class ConfigService { private config: AppConfig | null = null;
loadConfig(): Promise<void> { return fetch('/assets/config.json') .then(r => r.json()) .then(config => { this.config = config; }); }
get(key: keyof AppConfig) { return this.config?.[key]; }}📊 Scopes провайдеров
Заголовок раздела «📊 Scopes провайдеров»// 1. root (синглтон для всего приложения)@Injectable({ providedIn: 'root' })export class AuthService {}
// 2. platform (синглтон для платформы — между несколькими Angular приложениями)@Injectable({ providedIn: 'platform' })export class PlatformService {}
// 3. any (отдельный экземпляр для каждого lazy-loaded модуля)@Injectable({ providedIn: 'any' })export class FeatureService {}
// 4. Component-level (отдельный экземпляр для каждого компонента)@Component({ providers: [CartService] // Не синглтон! Создаётся для каждого экземпляра компонента})export class CartComponent {}🔧 Multi providers — несколько значений для одного токена
Заголовок раздела «🔧 Multi providers — несколько значений для одного токена»export const VALIDATORS = new InjectionToken<ValidatorFn[]>('validators');
bootstrapApplication(AppComponent, { providers: [ { provide: VALIDATORS, useValue: requiredValidator, multi: true }, { provide: VALIDATORS, useValue: emailValidator, multi: true }, { provide: VALIDATORS, useValue: customValidator, multi: true }, // inject(VALIDATORS) вернёт [requiredValidator, emailValidator, customValidator] ]});⚙️ ENVIRONMENT_INITIALIZER
Заголовок раздела «⚙️ ENVIRONMENT_INITIALIZER»Запуск кода при инициализации environment injector:
import { ENVIRONMENT_INITIALIZER, inject } from '@angular/core';
bootstrapApplication(AppComponent, { providers: [ { provide: ENVIRONMENT_INITIALIZER, useValue: () => { const router = inject(Router); router.events.subscribe(event => { if (event instanceof NavigationEnd) { analytics.trackPageView(event.url); } }); }, multi: true } ]});export default function InjectionTokenPlayground() { const [config, setConfig] = React.useState({ apiUrl: 'https://api.example.com', maxRetries: 3, debug: true, darkMode: true, betaFeatures: false, });
const [injected, setInjected] = React.useState({}); const [scope, setScope] = React.useState('root'); const [log, setLog] = React.useState([]); const [instanceCount, setInstanceCount] = React.useState({ root: 1, component: 0 });
const addLog = (msg, color = '#94a3b8') => { setLog(prev => [{ msg, color, id: Date.now() + Math.random() }, ...prev].slice(0, 8)); };
const updateConfig = (key, value) => { setConfig(prev => ({ ...prev, [key]: value })); addLog(`APP_CONFIG token обновлён: ${key} = ${JSON.stringify(value)}`, '#7dd3fc'); };
const injectToken = (tokenName) => { const values = { 'APP_CONFIG': config, 'API_URL': config.apiUrl, 'MAX_RETRIES': config.maxRetries, 'FEATURE_FLAGS': { darkMode: config.darkMode, betaFeatures: config.betaFeatures }, }; setInjected(prev => ({ ...prev, [tokenName]: values[tokenName] })); addLog(`inject(${tokenName}) → получено значение`, '#22c55e'); };
const mountComponent = () => { if (scope === 'component') { setInstanceCount(prev => ({ ...prev, component: prev.component + 1 })); addLog(`Новый компонент создан → новый экземпляр FeatureService (providedIn: 'any')`, '#f59e0b'); } else { addLog(`Компонент создан → inject(AuthService) → СИНГЛТОН из root`, '#7dd3fc'); } };
const tokens = [ { name: 'APP_CONFIG', type: 'AppConfig', description: 'Вся конфигурация приложения' }, { name: 'API_URL', type: 'string', description: 'Базовый URL API' }, { name: 'MAX_RETRIES', type: 'number', description: 'Кол-во повторных попыток' }, { name: 'FEATURE_FLAGS', type: 'FeatureFlags', description: 'Флаги функциональности' }, ];
return ( <div style={{ background: '#0f172a', minHeight: 500, padding: 24, borderRadius: 12, fontFamily: 'system-ui', color: '#e2e8f0' }}> <div style={{ color: '#dd0031', fontWeight: 700, fontSize: 16, marginBottom: 20 }}> 🔑 InjectionToken — конфигурация через DI </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}> <div> {/* Config editor */} <div style={{ background: '#1e293b', borderRadius: 10, padding: 16, border: '1px solid #334155', marginBottom: 16 }}> <div style={{ color: '#7dd3fc', fontSize: 12, fontWeight: 700, marginBottom: 12 }}> 📝 APP_CONFIG = new InjectionToken('AppConfig') </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div> <label style={{ fontSize: 11, color: '#94a3b8', display: 'block', marginBottom: 4 }}>apiUrl: string</label> <input value={config.apiUrl} onChange={e => updateConfig('apiUrl', e.target.value)} style={{ width: '100%', background: '#0f172a', border: '1px solid #334155', color: '#e2e8f0', padding: '6px 10px', borderRadius: 6, fontSize: 12, boxSizing: 'border-box', outline: 'none' }} /> </div> <div> <label style={{ fontSize: 11, color: '#94a3b8', display: 'block', marginBottom: 4 }}>maxRetries: number</label> <div style={{ display: 'flex', gap: 6 }}> {[1, 3, 5].map(v => ( <button key={v} onClick={() => updateConfig('maxRetries', v)} style={{ background: config.maxRetries === v ? '#dd0031' : '#334155', color: 'white', border: 'none', padding: '4px 14px', borderRadius: 6, cursor: 'pointer', fontSize: 12 }}> {v} </button> ))} </div> </div> {[ { key: 'debug', label: 'debug: boolean' }, { key: 'darkMode', label: 'featureFlags.darkMode' }, { key: 'betaFeatures', label: 'featureFlags.betaFeatures' }, ].map(({ key, label }) => ( <div key={key} style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <input type="checkbox" checked={config[key]} onChange={e => updateConfig(key, e.target.checked)} style={{ accentColor: '#dd0031' }} /> <label style={{ fontSize: 12, color: '#94a3b8' }}>{label}: <span style={{ color: config[key] ? '#22c55e' : '#f87171' }}>{String(config[key])}</span></label> </div> ))} </div> </div>
{/* Scopes */} <div style={{ background: '#1e293b', borderRadius: 10, padding: 16, border: '1px solid #7c3aed' }}> <div style={{ color: '#a78bfa', fontSize: 12, fontWeight: 700, marginBottom: 12 }}>📊 Scopes провайдеров</div> <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}> {['root', 'component'].map(s => ( <button key={s} onClick={() => setScope(s)} style={{ background: scope === s ? '#7c3aed' : '#334155', color: 'white', border: 'none', padding: '5px 14px', borderRadius: 6, cursor: 'pointer', fontSize: 12 }}> {s} </button> ))} </div> <div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 10 }}> {scope === 'root' ? '@ Injectable({ providedIn: "root" }) → синглтон' : '@ Component({ providers: [Service] }) → каждый компонент = новый'} </div> <button onClick={mountComponent} style={{ background: '#dd0031', color: 'white', border: 'none', padding: '7px 16px', borderRadius: 8, cursor: 'pointer', fontSize: 12 }}> + Создать компонент </button> <div style={{ marginTop: 10, fontSize: 12, color: '#64748b' }}> {scope === 'root' ? `AuthService: 1 экземпляр (синглтон)` : `FeatureService: ${instanceCount.component} экземпляр(ов)`} </div> </div> </div>
<div> {/* Token injection */} <div style={{ background: '#1e293b', borderRadius: 10, padding: 16, border: '1px solid #334155', marginBottom: 16 }}> <div style={{ color: '#22c55e', fontSize: 12, fontWeight: 700, marginBottom: 12 }}>💉 inject(TOKEN)</div> <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}> {tokens.map(t => ( <button key={t.name} onClick={() => injectToken(t.name)} style={{ background: injected[t.name] ? '#15803d20' : '#1e293b', color: injected[t.name] ? '#22c55e' : '#94a3b8', border: `1px solid ${injected[t.name] ? '#22c55e' : '#334155'}`, padding: '8px 12px', borderRadius: 6, cursor: 'pointer', textAlign: 'left', fontSize: 12 }} > <div style={{ fontFamily: 'monospace', marginBottom: 2 }}>inject({t.name}): {t.type}</div> <div style={{ fontSize: 10, color: '#475569' }}>{t.description}</div> </button> ))} </div>
{Object.keys(injected).length > 0 && ( <div style={{ background: '#0f172a', borderRadius: 6, padding: 12 }}> <div style={{ fontSize: 11, color: '#64748b', marginBottom: 6 }}>Полученные значения:</div> {Object.entries(injected).map(([k, v]) => ( <div key={k} style={{ marginBottom: 8 }}> <div style={{ fontSize: 11, color: '#22c55e', fontFamily: 'monospace' }}>{k}:</div> <pre style={{ fontSize: 10, color: '#94a3b8', margin: '2px 0 0 12px', whiteSpace: 'pre-wrap' }}> {JSON.stringify(v, null, 1)} </pre> </div> ))} </div> )} </div>
{/* Log */} <div style={{ background: '#1e293b', borderRadius: 8, padding: 14, border: '1px solid #334155' }}> <div style={{ color: '#7dd3fc', fontSize: 12, marginBottom: 8 }}>📋 DI Events</div> {log.length === 0 && <div style={{ color: '#475569', fontSize: 12 }}>Взаимодействуй с токенами...</div>} {log.map((l, i) => ( <div key={l.id} style={{ fontSize: 12, color: i === 0 ? '#e2e8f0' : '#475569', marginBottom: 4 }}>{l.msg}</div> ))} </div> </div> </div> </div> );}