You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 18:APP_INITIALIZER后设置InjectionToken失败问题

Angular 18 初始化后设置API URL未生效问题

问题描述

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:04:56