App.settings - Angular 的方式?

IT技术 javascript angular
2021-01-25 04:50:16

我想在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 {


}

但它不起作用,我收到错误。

问题:

如何以 Angular 的方式使用“app.settings”值?

笨蛋

NB 当然我可以创建 Injectable 服务并将它放在 NgModule 的提供者中,但正如我所说,我想用InjectionTokenAngular 方式来做

6个回答

如果您正在使用 ,还有一个选择:

Angular CLI 提供环境文件src/environments(默认为environment.ts(dev) 和environment.prod.ts(production))。

请注意,您需要在所有environment.*文件中提供配置参数,例如,

环境.ts

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

环境.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)上阅读更多关于应用程序环境的信息

它工作正常。但是在移动构建时它也被更改为捆绑包。我应该在我的服务中更改配置而不是在转移到生产后的代码中
2021-03-14 04:50:16
@MattTester 这实际上是现在官方的 Angular-CLI 故事。如果您碰巧对这个问题有更好的答案:请随时发布!
2021-03-21 04:50:16
在正常的软件开发中,这在某种程度上是一种反模式;API url 只是配置。不应该重新构建来为不同的环境重新配置应用程序。它应该构建一次,多次部署(预生产、暂存、生产等)。
2021-03-28 04:50:16
ng build 之后可以配置吗?
2021-04-08 04:50:16
哦,好吧,我误读了评论。我同意这有助于反模式,我认为动态运行时配置有一个故事。
2021-04-09 04:50:16

不建议将这些environment.*.ts文件用于 API URL 配置。似乎您应该这样做,因为这提到了“环境”一词。

使用它实际上是编译时配置如果要更改 API URL,则需要重新构建。这是您不想做的事情......只需询问您友好的 QA 部门 :)

您需要的是运行时配置,即应用程序在启动时加载其配置。

其他一些答案也涉及到这一点,但不同之处在于需要在应用程序启动后立即加载配置,以便正常服务在需要时可以使用它。

实现运行时配置:

  1. 将 JSON 配置文件添加到/src/assets/文件夹(以便在构建时复制)
  2. 创建一个AppConfigService以加载和分发配置
  3. 使用一个加载配置 APP_INITIALIZER

1. 添加配置文件 /src/assets

您可以将它添加到另一个文件夹,但您需要通过更新angular.json配置文件来告诉 Angular CLI 它是一项资产开始使用资产文件夹:

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

2.创建 AppConfigService

这是将在您需要配置值时注入的服务:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@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安全注入,在配置完全加载的情况下,我们需要在应用程序启动时加载配置。重要的是,初始化工厂函数需要返回 aPromise以便 Angular 知道在完成启动之前等待它完成解析:

import { APP_INITIALIZER } from '@angular/core';
import { AppConfigService } from './services/app-config.service';

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

}

我不能说它足够强烈,将您的API url配置为编译时配置是一种反模式使用运行时配置。

@DaleK 字里行间,你正在使用 Web Deploy 进行部署。如果您使用的是部署管道,例如 Azure DevOps,则可以在下一步中正确设置配置文件。配置的设置是部署过程/管道的责任,它可以覆盖默认配置文件中的值。希望澄清。
2021-03-15 04:50:16
@MattTester - 如果 Angular 实现了这个功能,它将解决我们的问题:github.com/angular/angular/issues/23279#issuecomment-528417026
2021-03-16 04:50:16
@CrhistianRamirez 从应用程序的角度来看:直到运行时才知道配置,并且静态文件在构建之外,可以在部署时以多种方式设置。静态文件适用于非敏感配置。API 或其他受保护的端点可以使用相同的技术,但如何进行身份验证以使其受到保护是您的下一个挑战。
2021-03-20 04:50:16
本地文件或不同的服务,编译时配置不应用于 API url。想象一下,如果您的应用程序作为产品出售(购买者安装),您不希望他们编译它,等等。无论哪种方式,您都不想重新编译 2 年前构建的东西,只是因为API 网址已更改。风险!!
2021-04-01 04:50:16
@Bloodhound 您可以拥有多个,APP_INITIALIZER但我认为您无法轻易让它们相互依赖。听起来你有一个很好的问题要问,所以也许在这里链接到它?
2021-04-01 04:50:16

我想出了如何使用 InjectionTokens 来做到这一点(请参见下面的示例),如果您的项目是使用 构建的,Angular CLI您可以像 API 端点一样使用/environments静态环境文件application wide settings,但根据您项目的要求,您很可能最终会使用两者,因为环境文件只是对象文字,而使用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 { }

现在您可以将其 DI 到任何组件、服务等中:

/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 键入检查配置。

我正在尝试使用 Azure 的环境变量和 JSON 转换功能动态注入 API 端点,但看起来这个答案只是从环境文件中获取 apiEndpoint。你将如何从配置中获取它并导出它?
2021-03-21 04:50:16
2021-03-24 04:50:16
不,但您可以将第一部分复制并粘贴到文件中,将其导入您的 app.module.ts 文件,然后将其 DI 放在任何地方并将其输出到控制台。我需要更长的时间在一个plunker中设置它然后它会执行这些步骤。
2021-03-26 04:50:16
哦,我以为你已经有了一个 plunker :-) 谢谢。
2021-03-31 04:50:16
我不相信您需要导出 AppConfig 接口/类。在做 DI 时你绝对不需要使用它。为了在一个文件中完成这项工作,它必须是一个类而不是一个接口,但这并不重要。事实上,样式指南建议使用类而不是接口,因为这意味着更少的代码,您仍然可以使用它们进行类型检查。关于它通过泛型被 InjectionToken 使用,这是您想要包含的内容。
2021-04-02 04:50:16

我发现APP_INITIALIZER在其他服务提供商需要注入配置的情况下,使用 an不起作用。它们可以在APP_INITIALIZER运行之前实例化

我已经看到其他解决方案fetch用于读取 config.json 文件并platformBrowserDynamic()在引导根module之前使用参数中的注入令牌提供它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. 最后引导根module。

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 并在部署时为不同的部署环境替换其他设置。

谢谢,虽然有问题。 configListener()应该是configListener(response: any),你应该解析,const configuration = JSON.parse(response.target.responseText)因为没有this.responseText. 明确地说,您实际上可以@Inject(APP_CONFIG) private config: IConfig在任何类(组件、服务)的构造函数中使用来访问配置值。
2021-03-17 04:50:16
谢谢@Glenn,这正是我所需要的。
2021-04-02 04:50:16

这是我的解决方案,从 .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"
}
拜托,你能帮我做对吗?对于有角度的环境,它比传统的风险有多大?environments.prod.tsafter的全部内容ng build --prod.js在某个时候出现在某个文件中。即使被混淆,来自的数据environments.prod.ts也将是明文。和所有 .js 文件一样,它将在最终用户机器上可用。
2021-03-16 04:50:16
@AlbertoL.Bonfiglio 您将服务器配置为不允许从外部访问 config.json 文件(或将其放置在没有公共访问权限的目录中)
2021-03-18 04:50:16
这也是我最喜欢的解决方案,但仍然担心安全风险。
2021-03-21 04:50:16
@AlbertoL.Bonfiglio 因为 Angular 应用程序本质上是一个客户端应用程序,并且将使用 JavaScript 来传递数据和配置,所以其中不应该使用任何秘密配置;所有机密配置定义都应位于用户浏览器或浏览器工具无法访问的 API 层之后。API 的基本 URI 等值可供公众访问,因为 API 应该拥有自己的凭据和基于用户登录的安全性(https 上的承载令牌)。
2021-03-23 04:50:16
这种方法的问题在于 config.json 对世界开放。您将如何阻止某人输入 www.mywebsite.com/assetts/config.json?
2021-03-25 04:50:16