应用程序设置-角度的方式?

我想添加一个 App Settings部分到我的应用程序,它将包含一些常量和预定义的值。

我已经阅读了使用 OpaqueToken这个答案,但它在角度上是不被推荐的。这个 article解释了差异,但它没有提供一个完整的例子,我的尝试是不成功的。

下面是我试过的方法(我不知道是否正确) :

//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 {




}

但它不工作,我得到了错误。

Question:

如何以角度方式使用“ app.sets”值?

混蛋

注意: 当然,我可以创建注入服务,并把它放在 NgModule 的提供商,但正如我所说,我想做的 InjectionToken,角度的方式。

148076 次浏览

我弄清楚了如何使用 InjectionTokens (参见下面的例子) ,如果你的项目是使用 Angular CLI 构建的,你可以使用在 /environments中找到的环境文件来进行静态应用范围的设置,比如 API 端点,但是取决于你的项目的需求,你最终很可能会使用两者,因为环境文件只是对象文字,而使用 InjectionToken的可注入配置可以使用环境变量,因为它是一个类,可以根据应用程序中的其他因素应用逻辑来配置它,比如初始 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 { }

Now you can just DI it into any component, service, etc:

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

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

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

Note that you need to provide the config parameters in all environment.* files, e.g.,

返回文章页面环境保护:

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

环境保护署 :

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

并在您的服务中使用它们(自动选择正确的环境文件) :

Api 服务中心

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


@Injectable()
export class ApiService {


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


// ...
}

阅读有关 Github (Angular CLI version 6)正式角度指引(第7版)中的应用程序环境的更多信息。

以下是我的解决方案,从.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"
}

这是我的两个解决方案

1. 存储在 json 文件中

Just make a json file and get in your component by $http.get() method. If I was need this very low then it's good and quick.

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使用。

仅此而已。

不建议在 API URL 配置中使用 environment.*.ts文件。看起来你应该这么做,因为这里提到了“环境”这个词。

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

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

其他一些答案也涉及到这个问题,但区别在于配置需要加载 一旦应用程序启动,这样普通服务就可以随时使用它。

要实现运行时配置:

  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,在配置已经完全加载的情况下,我们需要在应用程序启动时加载配置。重要的是,初始化工厂函数需要返回一个 Promise,这样 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 是一种反模式时,我不能说它足够强烈。使用运行时配置。

穷人的配置文件:

作为 body 标记的第一行添加到 index.html:

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

添加资产/config.js:

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

添加 config.ts:

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


export interface AppConfig {
apiBaseUrl: string
}

我发现,在其他服务提供商需要注入配置的情况下,使用 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中的任何其他提供程序中,它将被定义。例如,我可以用以下方法从 @angular/fire初始化 FIREBASE_OPTIONS注入令牌:

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

我发现对于一个非常常见的需求来说,这整件事情是一件非常困难(而且古怪)的事情。希望在不久的将来会有更好的方法,比如支持异步提供程序工厂。

The rest of the code for completeness...

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

I find this 角度帮助: 可编辑配置文件 from Microsoft Dev blogs being the best solution. You can configure dev build settings or prod build settings.

我们在几年前就遇到了这个问题,当时我还没有加入,而且已经有了一个解决方案,它使用本地存储来存储用户和环境信息。确切的说是角度1.0天。我们以前是在运行时动态创建一个 js 文件,然后将生成的 api url 放入一个全局变量中。我们现在更多的是面向对象的驱动,并且不使用本地存储。

我为确定环境和 api url 创建创建了一个更好的解决方案。

这有什么不同?

除非加载 config.json 文件,否则应用程序不会加载。它使用工厂函数来创建更高级别的 SOC。我可以将其封装到一个服务中,但是我从来没有看到任何原因,当文件的不同部分之间的唯一相似之处是它们在文件中同时存在时。拥有一个工厂函数允许我将函数直接传递到一个模块,如果它能够接受一个函数的话。最后,当工厂函数可以使用时,我可以更容易地设置 InjectionTokens。

缺点呢?

如果您要配置的模块不允许将工厂函数传递到 forRoot ()或 forChild () ,并且没有其他方法可以通过使用工厂函数来配置包,那么使用这种设置(以及大多数其他答案)是不幸的。

说明

  1. 使用提取检索 json 文件,我将对象存储在窗口中并引发一个自定义事件。- 记得安装 whatwg-get 并将其添加到您的 polyfill s.ts 中,以实现 IE 兼容性
  2. 让事件侦听器侦听自定义事件。
  3. 事件侦听器接收事件,从窗口检索要传递给可观察对象的对象,并清除窗口中存储的内容。
  4. Bootstrap Angular

——这就是我的解决方案开始出现分歧的地方——

  1. 创建一个导出接口的文件,该接口的结构表示 config.json ——它确实有助于提高类型的一致性,下一节代码需要一个类型,当您知道可以指定更具体的内容时,不要指定 {}any
  2. 创建将在步骤3中将解析后的 json 文件传递到的 Behavior orSubject。
  3. 使用工厂函数引用配置的不同部分来维护 SOC
  4. 为需要工厂函数结果的提供程序创建 InjectionToken

和/或

  1. 将工厂函数直接传递到能够在其 forRoot ()或 forChild ()方法中接受函数的模块中。

——维他命

在创建事件侦听器之前,我检查了 window [“ Environment”]没有被填充,以允许在 main.ts 中的代码执行之前通过其他方式填充 window [“ Environment”]的解决方案的可能性。

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

环境解决方案

为了冗余,我使用 window [“環境”]将一个值分配给 Behavior orSubject。你可以设计一个解决方案,在你的配置已经被预先加载,并且当你的 Angular 的应用程序代码运行时,windows [“ Environment”]已经被填充,包括 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;
}

- 应用程序模块-为了便于理解

有趣的是!较早版本的 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

Addendum

如何解决 API URL 的创建问题?

我希望能够通过一个评论来理解每个 url 做了什么,并希望能够进行类型检查,因为这是 TypeScript 相对于 javascript (IMO)的最大优势。我还想为其他开发人员创建一种体验来添加新的端点,以及尽可能无缝的 API。

我创建了一个接受环境(dev、 test、 stage、 prod、“”等)的类,并将这个值传递给一系列类[1-N ] ,这些类的工作是为每个 API 集合创建基 URL。每个 ApiCollection 负责为每个 API 集合创建基 URL。可以是我们自己的 API、供应商的 API,甚至是外部链接。该类将创建的基 URL 传递给它包含的每个后续 api。阅读下面的代码可以看到一个基本的示例。一旦设置完成,另一个开发人员就可以非常简单地向 Api 类添加另一个端点,而不需要触及其他任何东西。

TLDR; 用于内存优化的基本 OOP 原则和惰性读取器

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

相当多的文章建议您得到角配置设置使用 AppConfigService 比如这个

But I found that sometimes this just didn't work.

有一个“ Config.json”文件,然后创建一个类来读取它,并返回一个值,这样做更简单,也更可靠,例如我的配置文件看起来像这样:

{
"appName": "Mike's app",
"version": "1.23.4",
"logging_URL" : "https://someWebsite.azurewebsites.net/logs"
}

And I'd access the values using this:

import config from '../../assets/config.json';


@Injectable({
providedIn: 'root'
})
export class AppConfigService {
get appName() {
return config.appName;
}
get appVersion() {
return config.version;
}
get loggingUrl() {
return config.logging_URL;
}
}

(A few months later...)

After patting myself on the back for making a simpler Angular solution, I realised this has a big drawback. If you use an AppConfigService, and you're using CI/CD, then you can get your build process to update the config .json file, and the Angular app will use these settings.

With my version, yes, it's simpler, but there is no config setting file to overwrite. For automated build processes, this might not be desirable.

如果在容器中运行,本文 https://pumpingco.de/blog/environment-variables-angular-docker/展示了如何在入口点命令中使用 envsubst通过环境变量动态修复这个静态编译问题。