App.settings - Angular способ?

87

Я хочу добавить App Settingsв свое приложение раздел, в котором он будет содержать некоторые константы и предварительно определенные значения.

Я уже читал этот ответ, в котором используется, OpaqueTokenно он устарел в Angular. Эта статья объясняет различия, но не дает полного примера, и мои попытки не увенчались успехом.

Вот что я пробовал (не знаю, правильно ли это):

//ServiceAppSettings.ts

import {InjectionToken, OpaqueToken} from "@angular/core";

const CONFIG = {
  apiUrl: 'http://my.api.com',
  theme: 'suicid-squad',
  title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');

И это компонент, в котором я хочу использовать эти константы:

//MainPage.ts

import {...} from '@angular/core'
import {ServiceTest} from "./ServiceTest"

@Component({
  selector: 'my-app',
  template: `
   <span>Hi</span>
  ` ,  providers: [
    {
      provide: ServiceTest,
      useFactory: ( apiUrl) => {
        // create data service
      },
      deps: [

        new Inject(API_URL)
      ]
    }
  ]
})
export class MainPage {


}

Но это не работает, и я получаю ошибки.

Вопрос:

Как я могу использовать значения app.settings в Angular?

плунжер

NB. Конечно, я могу создать Injectable-сервис и поместить его в провайдер NgModule, но, как я уже сказал, я хочу сделать это с InjectionTokenпомощью Angular.

Ройи Намир
источник
Вы можете проверить мой ответ здесь, основанный на текущей официальной документации
JavierFuentes
@javier нет. У вашей ссылки есть проблема, если два поставщика предоставляют одно и то же имя, поэтому теперь у вас есть проблема. Entring opaquetoken
Royi Namir
вы знаете, что [OpaqueToken устарел]. ( angular.io/api/core/OpaqueToken ) В этой статье рассказывается о том, как предотвратить конфликты имен в поставщиках Angular
JavierFuentes
Да, я знаю, но все же связанная статья неверна.
Рой Намир
2
может быть ниже ссылка может быть полезна для всех, кто любит использовать новую архитектуру схемы конфигурации angular devblogs.microsoft.com/premier-developer/…
M_Farahmand

Ответы:

56

Я понял, как это сделать с помощью InjectionTokens (см. Пример ниже), и если ваш проект был построен с использованием, Angular CLIвы можете использовать файлы среды, найденные в, /environmentsдля статических, application wide settingsтаких как конечная точка API, но в зависимости от требований вашего проекта вы, скорее всего, закончите с использованием обоих, поскольку файлы среды являются просто объектными литералами, в то время как вводимая конфигурация с использованием InjectionToken's может использовать переменные среды и, поскольку это класс, может иметь логику, применяемую для его настройки на основе других факторов в приложении, таких как исходные данные HTTP-запроса, поддомен , так далее.

Пример токенов инъекций

/app/app-config.module.ts

import { NgModule, InjectionToken } from '@angular/core';
import { environment } from '../environments/environment';

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

export class AppConfig {
  apiEndpoint: string;
}

export const APP_DI_CONFIG: AppConfig = {
  apiEndpoint: environment.apiEndpoint
};

@NgModule({
  providers: [{
    provide: APP_CONFIG,
    useValue: APP_DI_CONFIG
  }]
})
export class AppConfigModule { }

/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppConfigModule } from './app-config.module';

@NgModule({
  declarations: [
    // ...
  ],
  imports: [
    // ...
    AppConfigModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Теперь вы можете просто ввести его в любой компонент, службу и т. Д .:

/app/core/auth.service.ts

import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';

@Injectable()
export class AuthService {

  constructor(
    private http: Http,
    private router: Router,
    private authHttp: AuthHttp,
    @Inject(APP_CONFIG) private config: AppConfig
  ) { }

  /**
   * Logs a user into the application.
   * @param payload
   */
  public login(payload: { username: string, password: string }) {
    return this.http
      .post(`${this.config.apiEndpoint}/login`, payload)
      .map((response: Response) => {
        const token = response.json().token;
        sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
        return this.handleResponse(response); // TODO:  unset token shouldn't return the token to login
      })
      .catch(this.handleError);
  }

  // ...
}

Затем вы также можете ввести проверку конфигурации, используя экспортированный файл AppConfig.

mtpultz
источник
Нет, но вы можете буквально скопировать и вставить первую часть в файл, импортировать ее в свой файл app.module.ts, а также использовать DI в любом месте и вывести на консоль. Мне потребовалось бы больше времени, чтобы установить это в плункер, чем сделать эти шаги.
mtpultz
Ой, я думал, у тебя уже есть плункер для этого :-) Спасибо.
Рой Намир
Для желающих: plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=preview
Royi
1
Я не верю, что вам нужно экспортировать интерфейс / класс AppConfig. Вам определенно не нужно использовать его при выполнении DI. Чтобы это работало в одном файле, это должен был быть класс, а не интерфейс, но это не имеет значения. Фактически, руководство по стилю предлагает использовать классы вместо интерфейсов, поскольку это означает меньше кода, и вы все равно можете использовать их для проверки типа. Что касается его использования InjectionToken через дженерики, это то, что вы захотите включить.
mtpultz
1
Я пытаюсь динамически внедрить конечную точку API, используя переменные среды Azure и функции преобразования JSON, но похоже, что этот ответ просто получает apiEndpoint из файла среды. Как бы вы взяли его из конфига и экспортировали?
Арчибальд
138

Если вы используете , есть еще один вариант:

Angular CLI предоставляет файлы среды src/environments(по умолчанию environment.ts(dev) и environment.prod.ts(production)).

Обратите внимание, что вам необходимо предоставить параметры конфигурации во всех environment.*файлах, например,

environment.ts :

export const environment = {
  production: false,
  apiEndpoint: 'http://localhost:8000/api/v1'
};

environment.prod.ts :

export const environment = {
  production: true,
  apiEndpoint: '__your_production_server__'
};

и используйте их в своем сервисе (правильный файл среды выбирается автоматически):

api.service.ts

// ... other imports
import { environment } from '../../environments/environment';

@Injectable()
export class ApiService {     

  public apiRequest(): Observable<MyObject[]> {
    const path = environment.apiEndpoint + `/objects`;
    // ...
  }

// ...
}

Подробнее о средах приложений читайте на Github (Angular CLI версии 6) или в официальном руководстве по Angular (версия 7) .

тило
источник
2
он работает нормально.Но при перемещении сборки он также изменяется как бандл.Я должен изменить конфигурацию в моей подаче, а не в коде, после перехода на производство
Камалав
43
Это своего рода антипаттерн в обычной разработке программного обеспечения; URL-адрес API - это просто конфигурация. Чтобы перенастроить приложение для другой среды, не требуется повторная сборка. Он должен быть собран один раз, развернут много раз (pre-prod, staging, prod и т. Д.).
Мэтт Тестер,
3
@MattTester Это то, что сейчас является официальной историей Angular-CLI. Если у вас есть лучший ответ на этот вопрос: не стесняйтесь размещать его!
tilo
7
можно ли его настроить после сборки ng?
NK
1
Хорошо, я неправильно прочитал комментарии. Я согласен, что это дает антипаттерн, я думал, что есть история с динамическими конфигурациями времени выполнения.
Йенс Бодал
85

Не рекомендуется использовать environment.*.tsфайлы для конфигурации URL-адреса API. Похоже, вы должны это сделать, потому что здесь упоминается слово «среда».

Это фактически конфигурация времени компиляции . Если вы хотите изменить URL-адрес API, вам нужно будет перестроить. Это то, чего вы не хотите делать ... просто спросите свой дружелюбный отдел контроля качества :)

Что вам нужно, так это конфигурация времени выполнения , то есть приложение загружает свою конфигурацию при запуске.

Некоторые другие ответы касаются этого, но разница в том, что конфигурация должна быть загружена сразу после запуска приложения , чтобы она могла использоваться обычной службой всякий раз, когда ей это нужно.

Чтобы реализовать конфигурацию времени выполнения:

  1. Добавьте файл конфигурации JSON в /src/assets/папку (чтобы он был скопирован при сборке)
  2. Создайте файл AppConfigServiceдля загрузки и распространения конфигурации
  3. Загрузите конфигурацию с помощью APP_INITIALIZER

1. Добавьте файл конфигурации в /src/assets

Вы можете добавить его в другую папку, но вам нужно будет сообщить CLI, что это актив в angular.json. Начните с папки с ресурсами:

{
  "apiBaseUrl": "https://development.local/apiUrl"
}

2. Создайте AppConfigService

Это сервис, который будет внедряться всякий раз, когда вам понадобится значение конфигурации:

@Injectable({
  providedIn: 'root'
})
export class AppConfigService {

  private appConfig: any;

  constructor(private http: HttpClient) { }

  loadAppConfig() {
    return this.http.get('/assets/config.json')
      .toPromise()
      .then(data => {
        this.appConfig = data;
      });
  }

  // This is an example property ... you can make it however you want.
  get apiBaseUrl() {

    if (!this.appConfig) {
      throw Error('Config file not loaded!');
    }

    return this.appConfig.apiBaseUrl;
  }
}

3. Загрузите конфигурацию с помощью APP_INITIALIZER

Чтобы обеспечить AppConfigServiceбезопасное внедрение с полностью загруженной конфигурацией, нам нужно загрузить конфигурацию во время запуска приложения. Важно отметить, что функция фабрики инициализации должна возвращать, Promiseчтобы Angular знал, что нужно дождаться завершения разрешения перед завершением запуска:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [AppConfigService],
      useFactory: (appConfigService: AppConfigService) => {
        return () => {
          //Make sure to return a promise!
          return appConfigService.loadAppConfig();
        };
      }
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Теперь вы можете ввести его куда угодно, и вся конфигурация будет готова к чтению:

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {

  apiBaseUrl: string;

  constructor(private appConfigService: AppConfigService) {}

  ngOnInit(): void {
    this.apiBaseUrl = this.appConfigService.apiBaseUrl;
  }

}

Я не могу сказать это достаточно четко, настройка URL-адресов API в качестве конфигурации времени компиляции - это анти-шаблон . Используйте конфигурацию времени выполнения.

Мэтт Тестер
источник
4
Локальный файл или другая служба, конфигурация времени компиляции не должна использоваться для URL-адреса API. Представьте, что ваше приложение продается как продукт (покупатель устанавливает), вы не хотите, чтобы они его компилировали и т. Д. В любом случае вы не хотите повторно компилировать то, что было создано 2 года назад, только потому, что URL-адрес API изменился. Риск!!
Мэтт Тестер
1
@Bloodhound У вас может быть больше одного, APP_INITIALIZERно я не думаю, что вы легко можете сделать их зависимыми друг от друга. Похоже, у вас есть хороший вопрос, так что, может быть, ссылка на него здесь?
Мэтт Тестер
2
@MattTester - Если Angular когда-либо реализует эту функцию, это решит нашу проблему: github.com/angular/angular/issues/23279#issuecomment-528417026
Майк Бекатти
2
@CrhistianRamirez Это с точки зрения приложения: конфигурация неизвестна до времени выполнения, а статический файл находится за пределами сборки и может быть установлен разными способами во время развертывания. Статический файл подходит для нечувствительной конфигурации. API или какая-либо другая защищенная конечная точка возможна с помощью того же метода, но ваша следующая задача - как пройти аутентификацию, чтобы сделать ее защищенной.
Мэтт Тестер
1
@DaleK Если читать между строк, вы развертываете с помощью Web Deploy. Если вы используете конвейер развертывания, например Azure DevOps, то в качестве следующего шага можно правильно настроить файл конфигурации. За настройку конфигурации отвечает процесс / конвейер развертывания, который может переопределять значения в файле конфигурации по умолчанию. Надеюсь, что это прояснит.
Мэтт Тестер,
8

Вот мое решение, загружается из .json, чтобы разрешить изменения без перестройки

import { Injectable, Inject } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '@angular/common';

@Injectable()
export class ConfigService {

    private config: any;

    constructor(private location: Location, private http: Http) {
    }

    async apiUrl(): Promise<string> {
        let conf = await this.getConfig();
        return Promise.resolve(conf.apiUrl);
    }

    private async getConfig(): Promise<any> {
        if (!this.config) {
            this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
        }
        return Promise.resolve(this.config);
    }
}

и config.json

{
    "apiUrl": "http://localhost:3000/api"
}
PJM
источник
1
Проблема с этим подходом в том, что config.json открыт для всего мира. Как бы вы помешали кому-либо ввести www.mywebsite.com/assetts/config.json?
Альберто Л. Бонфлио
1
@ AlbertoL.Bonfiglio вы настраиваете сервер так, чтобы он не разрешал доступ извне к файлу config.json (или помещаете его в каталог, который не имеет общего доступа)
Alex Pandrea
Это тоже мое любимое решение, но все же не могу не беспокоиться о рисках безопасности.
Viqas
7
Пожалуйста, не могли бы вы помочь мне разобраться? Чем это опаснее традиционного для угловых сред? Полное содержание environments.prod.tsafter ng build --prodбудет на некоторых.js то момент файле. Даже если они запутаны, данные environments.prod.tsбудут в виде открытого текста. Как и все файлы .js, он будет доступен на машине конечного пользователя.
igann
5
@ AlbertoL.Bonfiglio Поскольку приложение Angular по своей природе является клиентским приложением, и для передачи данных и конфигурации будет использоваться JavaScript, в нем не должно быть секретной конфигурации; все определения секретных конфигураций должны находиться за уровнем API, где браузер пользователя или инструменты браузера не могут получить к нему доступ. Такие значения, как базовый URI API, подходят для публичного доступа, потому что API должен иметь свои собственные учетные данные и безопасность на основе входа пользователя в систему (токен-носитель по https).
Tommy Elliott
4

Файл конфигурации бедняги:

Добавьте в свой index.html в качестве первой строки в теге body:

<script lang="javascript" src="assets/config.js"></script>

Добавьте assets / config.js:

var config = {
    apiBaseUrl: "http://localhost:8080"
}

Добавьте config.ts:

export const config: AppConfig = window['config']

export interface AppConfig {
    apiBaseUrl: string
}
Матиас
источник
Серьезно, +1 за разложение решения на самые простые компоненты и при этом поддержание согласованности типов.
Luminous
4

Я обнаружил, что использование APP_INITIALIZERдля этого не работает в ситуациях, когда другие поставщики услуг требуют ввода конфигурации. Их можно создать до APP_INITIALIZERзапуска.

Я видел другие решения, которые используются fetchдля чтения файла config.json и предоставления его с помощью токена внедрения в параметре platformBrowserDynamic()до начальной загрузки корневого модуля. Ноfetch не поддерживается во всех браузерах и, в частности, браузерах WebView для мобильных устройств, на которые я нацелен.

Ниже приведено решение, которое подходит как для PWA, так и для мобильных устройств (WebView). Примечание. Пока я тестировал только на Android; работа из дома означает, что у меня нет доступа к Mac для сборки.

В main.ts:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { APP_CONFIG } from './app/lib/angular/injection-tokens';

function configListener() {
  try {
    const configuration = JSON.parse(this.responseText);

    // pass config to bootstrap process using an injection token
    platformBrowserDynamic([
      { provide: APP_CONFIG, useValue: configuration }
    ])
      .bootstrapModule(AppModule)
      .catch(err => console.error(err));

  } catch (error) {
    console.error(error);
  }
}

function configFailed(evt) {
  console.error('Error: retrieving config.json');
}

if (environment.production) {
  enableProdMode();
}

const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();

Этот код:

  1. запускает асинхронный запрос config.jsonфайла.
  2. Когда запрос завершается, анализирует JSON в объект Javascript
  3. предоставляет значение, используя APP_CONFIGтокен внедрения, до начальной загрузки.
  4. И, наконец, загружает корневой модуль.

APP_CONFIGзатем может быть введен в любые дополнительные поставщики, app-module.tsи он будет определен. Например, я могу инициализировать FIREBASE_OPTIONSтокен инъекции @angular/fireследующим образом:

{
      provide: FIREBASE_OPTIONS,
      useFactory: (config: IConfig) => config.firebaseConfig,
      deps: [APP_CONFIG]
}

Я считаю все это на удивление сложным (и хакерским) делом для очень распространенного требования. Надеюсь, что в ближайшем будущем появится лучший способ, например, поддержка фабрик поставщиков асинхронных программ.

Остальной код для полноты ...

В app/lib/angular/injection-tokens.ts:

import { InjectionToken } from '@angular/core';
import { IConfig } from '../config/config';

export const APP_CONFIG = new InjectionToken<IConfig>('app-config');

и в app/lib/config/config.tsя определяю интерфейс для моего файла конфигурации JSON:

export interface IConfig {
    name: string;
    version: string;
    instance: string;
    firebaseConfig: {
        apiKey: string;
        // etc
    }
}

Конфиг хранится в assets/config/config.json:

{
  "name": "my-app",
  "version": "#{Build.BuildNumber}#",
  "instance": "localdev",
  "firebaseConfig": {
    "apiKey": "abcd"
    ...
  }
}

Примечание. Я использую задачу Azure DevOps для вставки Build.BuildNumber и замены других параметров для различных сред развертывания по мере ее развертывания.

Гленн
источник
2

Вот два моих решения для этого

1. Хранить в файлах json

Просто создайте файл json и войдите в свой компонент по $http.get()методам. Если бы мне было нужно это очень мало, то это было бы хорошо и быстро.

2. Хранить с помощью служб передачи данных

Если вы хотите хранить и использовать все компоненты или широко использовать, тогда лучше использовать службу данных. Как это :

  1. Просто создайте статическую папку внутри src/appпапки.

  2. Создайте файл с именем fuels.ts в статической папке. Здесь вы также можете хранить другие статические файлы. Позвольте определить ваши данные вот так. Предполагая, что у вас есть данные о топливе.

__

export const Fuels {

   Fuel: [
    { "id": 1, "type": "A" },
    { "id": 2, "type": "B" },
    { "id": 3, "type": "C" },
    { "id": 4, "type": "D" },
   ];
   }
  1. Создайте имя файла static.services.ts

__

import { Injectable } from "@angular/core";
import { Fuels } from "./static/fuels";

@Injectable()
export class StaticService {

  constructor() { }

  getFuelData(): Fuels[] {
    return Fuels;
  }
 }`
  1. Теперь вы можете сделать это доступным для каждого модуля

просто импортируйте в файл app.module.ts вот так и измените провайдеров

import { StaticService } from './static.services';

providers: [StaticService]

Теперь используйте это как StaticService в любом модуле.

Вот и все.

amku91
источник
Хорошее решение, так как вам не нужно перекомпилировать. Окружение похоже на жесткое кодирование в коде. Противно. +1
Terrance00
0

У нас была эта проблема много лет назад, до того, как я присоединился к нам, и у нас было решение, которое использовало локальное хранилище для информации о пользователях и среде. Angular 1.0 дня, если быть точным. Раньше мы динамически создавали js-файл во время выполнения, который затем помещал сгенерированные URL-адреса api в глобальную переменную. В наши дни мы немного больше ориентируемся на ООП и не используем локальное хранилище ни для чего.

Я создал лучшее решение как для определения среды, так и для создания URL-адресов API.

Чем это отличается?

Приложение не загрузится, пока не будет загружен файл config.json. Он использует фабричные функции для создания более высокого уровня SOC. Я мог бы инкапсулировать это в службу, но я никогда не видел причины, по которой единственное сходство между различными разделами файла состоит в том, что они существуют вместе в файле. Наличие фабричной функции позволяет мне передавать функцию непосредственно в модуль, если он способен принять функцию. Наконец, мне проще настроить InjectionTokens, когда заводские функции доступны для использования.

Минусы?

Вам не повезло с этой настройкой (и большинством других ответов), если модуль, который вы хотите настроить, не позволяет передавать заводскую функцию в forRoot () или forChild (), и нет другого способа настроить пакет с помощью заводской функции.

инструкции

  1. Используя fetch для получения файла json, я сохраняю объект в окне и вызываю настраиваемое событие. - не забудьте установить whatwg-fetch и добавить его в свой polyfills.ts для совместимости с IE
  2. Попросите слушателя событий прослушивать настраиваемое событие.
  3. Слушатель событий получает событие, извлекает объект из окна для перехода к наблюдаемому и очищает то, что было сохранено в окне.
  4. Bootstrap Angular

- Вот здесь мое решение начинает действительно отличаться -

  1. Создайте файл, экспортирующий интерфейс, структура которого представляет ваш config.json - это действительно помогает с согласованностью типов, и в следующем разделе кода требуется тип, и не указывайте, {}или anyкогда вы знаете, что можете указать что-то более конкретное
  2. Создайте объект BehaviorSubject, в который вы передадите проанализированный файл json на шаге 3.
  3. Используйте заводские функции, чтобы ссылаться на различные разделы вашей конфигурации для поддержки SOC
  4. Создайте InjectionTokens для поставщиков, которым нужен результат ваших фабричных функций

- и / или -

  1. Передайте фабричные функции непосредственно в модули, способные принимать функцию в методах forRoot () или forChild ().

- main.ts

Я проверяю, что window ["environment"] не заполнено перед созданием прослушивателя событий, чтобы разрешить возможность решения, в котором window ["environment"] заполняется другими способами до того, как код в main.ts когда-либо будет выполнен.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';

var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
  window["environment"] = data;
  document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());

/*
  angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope. 
  Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are 
  ran at a higher level of scope bypassing the behavior of not loading AppModule when the 
  configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)

  example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under 
  app and will ONLY load that dependency, making AppModule an empty object.

  if(window["environment"])
  {
    if (window["environment"].production) {
      enableProdMode();
    }
    configurationSubject.next(window["environment"]);
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
  }
*/
if(!window["environment"]) {
  document.addEventListener('config-set', function(e){
    if (window["environment"].production) {
      enableProdMode();
    }
    configurationSubject.next(window["environment"]);
    window["environment"] = undefined;
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
  });
}

--- среда-resolvers.ts

Я присваиваю значение BehaviorSubject, используя window ["environment"] для избыточности. Вы можете разработать решение, в котором ваша конфигурация уже предварительно загружена, а окно ["environment"] уже заполнено к моменту запуска любого кода вашего приложения Angular, включая код в main.ts

import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";

const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
  const env = configurationSubject.getValue().environment;
  let resolvedEnvironment = "";
  switch (env) {
 // case statements for determining whether this is dev, test, stage, or prod
  }
  return resolvedEnvironment;
}

export function resolveNgxLoggerConfig() {
  return configurationSubject.getValue().logging;
}

- app.module.ts - Урезано для облегчения понимания

Интересный факт! Более старые версии NGXLogger требовали, чтобы вы передавали объект в LoggerModule.forRoot (). Фактически, LoggerModule все еще работает! NGXLogger любезно предоставляет LoggerConfig, который вы можете переопределить, позволяя вам использовать заводскую функцию для настройки.

import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
@NgModule({
    modules: [
        SomeModule.forRoot(resolveSomethingElse)
    ],
    providers:[
        {
            provide: ENVIRONMENT,
            useFactory: resolveEnvironment
        },
        { 
            provide: LoggerConfig,
            useFactory: resolveNgxLoggerConfig
        }
    ]
})
export class AppModule

Дополнение

Как я решил создать свои URL-адреса API?

Я хотел понять, что делает каждый URL-адрес, через комментарий, и хотел проверить тип, поскольку это самая сильная сторона TypeScript по сравнению с javascript (IMO). Я также хотел создать для других разработчиков возможность добавлять новые конечные точки и API, которые были бы максимально удобными.

Я создал класс, который принимает среду (dev, test, stage, prod, "" и т. Д.), И передал это значение серии классов [1-N], задача которых - создать базовый URL-адрес для каждой коллекции API. . Каждый ApiCollection отвечает за создание базового URL-адреса для каждой коллекции API. Это могут быть наши собственные API, API поставщика или даже внешняя ссылка. Этот класс будет передавать созданный базовый URL-адрес в каждый последующий api, который он содержит. Прочтите приведенный ниже код, чтобы увидеть простой пример. После настройки другому разработчику очень просто добавить еще одну конечную точку в класс Api, не касаясь чего-либо еще.

TL; DR; основные принципы ООП и ленивые геттеры для оптимизации памяти

@Injectable({
    providedIn: 'root'
})
export class ApiConfig {
    public apis: Apis;

    constructor(@Inject(ENVIRONMENT) private environment: string) {
        this.apis = new Apis(environment);
    }
}

export class Apis {
    readonly microservices: MicroserviceApiCollection;

    constructor(environment: string) {
        this.microservices = new MicroserviceApiCollection(environment);
    }
}

export abstract class ApiCollection {
  protected domain: any;

  constructor(environment: string) {
      const domain = this.resolveDomain(environment);
      Object.defineProperty(ApiCollection.prototype, 'domain', {
          get() {
              Object.defineProperty(this, 'domain', { value: domain });
              return this.domain;
          },
          configurable: true
      });
  }
}

export class MicroserviceApiCollection extends ApiCollection {
  public member: MemberApi;

  constructor(environment) {
      super(environment);
      this.member = new MemberApi(this.domain);
  }

  resolveDomain(environment: string): string {
      return `https://subdomain${environment}.actualdomain.com/`;
  }
}

export class Api {
  readonly base: any;

  constructor(baseUrl: string) {
      Object.defineProperty(this, 'base', {
          get() {
              Object.defineProperty(this, 'base',
              { value: baseUrl, configurable: true});
              return this.base;
          },
          enumerable: false,
          configurable: true
      });
  }

  attachProperty(name: string, value: any, enumerable?: boolean) {
      Object.defineProperty(this, name,
      { value, writable: false, configurable: true, enumerable: enumerable || true });
  }
}

export class MemberApi extends Api {

  /**
  * This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
  */
  get MemberInfo() {
    this.attachProperty("MemberInfo", `${this.base}basic-info`);
    return this.MemberInfo;
  }

  constructor(baseUrl: string) {
    super(baseUrl + "member/api/");
  }
}
Светящийся
источник