Перейти к содержимому

32. Injection Tokens

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');

tokens.ts
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;
}

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',
};
}
}
]
});

// Фабрика, зависящая от других провайдеров
{
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 запускает функции до рендера приложения:

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];
}
}

// 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 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>
);
}