Angular 18:APP_INITIALIZER后设置InjectionToken失败问题
问题描述
在Angular 18中,需要从config.json加载API URL,在应用初始化完成后将其设置到API_URL_TOKEN并存储到localStorage,但当前API_URL_TOKEN的工厂函数会在APP_INITIALIZER执行前被调用,导致无法获取正确的API URL。启动日志显示配置加载成功,但刷新时依赖API_URL_TOKEN的服务(如API调用服务、拦截器)拿到的是null值。
期望效果:API_URL_TOKEN能被正确注入到服务中使用,例如:
loginUser(data: { username: string; password: string }) { return this.http.post(`${this.apiUrl}/auth`, data).pipe( tap((res) => { if (res.success) { this.addToken(res.data); } }) ); }
现有实现代码
main.ts
bootstrapApplication(AppComponent, appConfig)
app.config.ts
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch(), withInterceptors([authInterceptor, errorInterceptor])), provideAppInitializer(), provideApiUrl(), provideRouter(routes), provideAnimationsAsync(), provideCharts(withDefaultRegisterables()), ], };
app-initializer.ts
const CONFIG_URL = '/config.json'; function appInitializer(http: HttpClient, storageService: StorageService) { return async () => { try { const config = await firstValueFrom(http.get<{apiUrl: string}>(CONFIG_URL)); storageService.apiUrl = config.apiUrl; console.log('Config loaded successfully'); } catch (error) { console.error('Error loading configuration:', error); throw error; } }; } export function provideAppInitializer(): Provider { return { provide: APP_INITIALIZER, useFactory: appInitializer, deps: [HttpClient, StorageService], multi: true, }; }
app-url-token.ts
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN'); function apiUrlFactory(storageService: StorageService): string { const apiUrl = storageService.apiUrl; if (apiUrl) { return apiUrl; } throw new Error('API URL not found in configuration'); } export function provideApiUrl(): Provider { return { provide: API_URL_TOKEN, useFactory: apiUrlFactory, deps: [StorageService], multi: false, }; }
storage.service.ts
import { inject, Injectable, InjectionToken } from '@angular/core'; const LOCAL_STORAGE = new InjectionToken<Storage>('Browser Storage', { providedIn: 'root', factory: () => localStorage, }); const API_URL = 'apiUrl'; @Injectable({ providedIn: 'root', }) export class StorageService { private readonly storage = inject<Storage>(LOCAL_STORAGE); get(key: string) { return this.storage.getItem(key); } set(key: string, value: string): void { this.storage.setItem(key, value); } remove(key: string): void { return this.storage.removeItem(key); } clear() { return this.storage.clear(); } get apiUrl(): string | null { return this.storage.getItem(API_URL) || null; } set apiUrl(url: string) { this.storage.setItem(API_URL, url); } }
问题根源
provideHttpClient在providers数组中排在provideAppInitializer之前,而HTTP拦截器(如authInterceptor)会在HttpClient初始化时被实例化。如果拦截器依赖API_URL_TOKEN,Angular会提前调用API_URL_TOKEN的工厂函数,此时APP_INITIALIZER还未执行,StorageService中没有存储API URL,导致获取失败。
解决方案
方案1:在应用启动前预加载配置(推荐)
直接在main.ts中提前加载配置,并存入localStorage,再将API URL作为常量注入API_URL_TOKEN,确保所有服务初始化前已拿到正确值。
修改main.ts:
import { HttpClient, provideHttpClient } from '@angular/common/http'; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { appConfig } from './app/app.config'; import { firstValueFrom } from 'rxjs'; import { API_URL_TOKEN } from './app/app-url-token'; async function bootstrap() { // 初始化HttpClient加载配置 const http = new HttpClient({}); let apiUrl: string; try { const config = await firstValueFrom(http.get<{ apiUrl: string }>('/config.json')); apiUrl = config.apiUrl; // 存入localStorage localStorage.setItem('apiUrl', apiUrl); } catch (error) { console.error('加载配置失败:', error); throw error; } // 更新appConfig,添加API_URL_TOKEN的提供者 const updatedConfig = { ...appConfig, providers: [ ...appConfig.providers, { provide: API_URL_TOKEN, useValue: apiUrl } ] }; bootstrapApplication(AppComponent, updatedConfig); } bootstrap();
同时修改app-url-token.ts,移除原有工厂函数逻辑:
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN'); // 移除原有的provideApiUrl函数,直接在main.ts中提供useValue
最后在app.config.ts中移除provideApiUrl():
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch(), withInterceptors([authInterceptor, errorInterceptor])), // 移除provideAppInitializer(),因为配置已在main.ts加载 provideRouter(routes), provideAnimationsAsync(), provideCharts(withDefaultRegisterables()), ], };
方案2:调整依赖顺序,确保APP_INITIALIZER优先完成
如果需要保留APP_INITIALIZER的方式,可将API URL的加载逻辑整合到API_URL_TOKEN的工厂函数中,并确保APP_INITIALIZER完成后再提供值:
修改app-url-token.ts:
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN'); async function loadAndStoreApiUrl(http: HttpClient, storageService: StorageService): Promise<string> { let apiUrl = storageService.apiUrl; if (!apiUrl) { const config = await firstValueFrom(http.get<{ apiUrl: string }>('/config.json')); apiUrl = config.apiUrl; storageService.apiUrl = apiUrl; } return apiUrl; } export function provideApiUrl(): Provider[] { return [ // 将配置加载作为APP_INITIALIZER的一部分 { provide: APP_INITIALIZER, useFactory: (http: HttpClient, storageService: StorageService) => () => loadAndStoreApiUrl(http, storageService), deps: [HttpClient, StorageService], multi: true, }, // 确保API_URL_TOKEN在APP_INITIALIZER完成后被实例化 { provide: API_URL_TOKEN, useFactory: (storageService: StorageService) => { const apiUrl = storageService.apiUrl; if (!apiUrl) { throw new Error('API URL未初始化,请检查APP_INITIALIZER配置'); } return apiUrl; }, deps: [StorageService], } ]; }
然后在app.config.ts中移除provideAppInitializer(),只保留provideApiUrl():
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch(), withInterceptors([authInterceptor, errorInterceptor])), provideApiUrl(), // 包含APP_INITIALIZER和API_URL_TOKEN的配置 provideRouter(routes), provideAnimationsAsync(), provideCharts(withDefaultRegisterables()), ], };
这种方式利用Angular等待所有APP_INITIALIZER异步操作完成后再初始化应用的特性,确保API_URL_TOKEN的工厂函数执行时,API URL已被存入StorageService。
内容的提问来源于stack exchange,提问作者skdonthi

