Quero adicionar uma App Settings
seção em meu aplicativo onde conterá alguns constantes e valores predefinidos.
Já li esta resposta que usa OpaqueToken
Mas está obsoleto no Angular. Este artigo explica as diferenças, mas não fornece um exemplo completo, e minhas tentativas não tiveram êxito.
Aqui está o que tentei (não sei se é o caminho certo):
//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');
E este é o componente onde desejo usar essas consts:
//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 {
}
Mas não funciona e recebo erros.
Questão:
Como posso consumir valores "app.settings" da maneira Angular?
Obs. Claro que posso criar o serviço Injetável e colocar no provedor do NgModule, mas como disse quero fazer com InjectionToken
, do jeito Angular.
javascript
angular
Royi Namir
fonte
fonte
Respostas:
Eu descobri como fazer isso com o InjectionTokens (veja o exemplo abaixo), e se o seu projeto foi construído usando o,
Angular CLI
você pode usar os arquivos de ambiente encontrados em/environments
estáticoapplication wide settings
como um endpoint de API, mas dependendo dos requisitos do seu projeto, você provavelmente acabará usando ambos, já que os arquivos de ambiente são apenas literais de objeto, enquanto uma configuração injetável usandoInjectionToken
's pode usar as variáveis de ambiente e, uma vez que é uma classe, pode ter lógica aplicada para configurá-la com base em outros fatores no aplicativo, como dados de solicitação inicial de http, subdomínio etc.Exemplo de tokens de injeção
/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 { }
Agora você pode simplesmente INICIá-lo em qualquer componente, serviço, 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); } // ... }
Você também pode digitar verificar a configuração usando o AppConfig exportado.
fonte
Se você estiver usando angular-cli, há ainda outra opção:
Angular CLI fornece arquivos de ambiente em
src/environments
(os padrões sãoenvironment.ts
(dev) eenvironment.prod.ts
(produção)).Observe que você precisa fornecer os parâmetros de configuração em todos os
environment.*
arquivos, por exemplo,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__' };
e usá-los em seu serviço (o arquivo de ambiente correto é escolhido automaticamente):
api.service.ts
// ... other imports import { environment } from '../../environments/environment'; @Injectable() export class ApiService { public apiRequest(): Observable<MyObject[]> { const path = environment.apiEndpoint + `/objects`; // ... } // ... }
Leia mais sobre ambientes de aplicativos no Github (Angular CLI versão 6) ou no guia oficial Angular (versão 7) .
fonte
Não é aconselhável usar os
environment.*.ts
arquivos para a configuração do URL da API. Parece que você deveria, porque isso menciona a palavra "ambiente".Usar isso é na verdade uma configuração em tempo de compilação . Se quiser alterar o URL da API, você precisará reconstruí-lo. Isso é algo que você não quer ter que fazer ... pergunte ao seu amigável departamento de controle de qualidade :)
O que você precisa é uma configuração de tempo de execução , ou seja, o aplicativo carrega sua configuração quando é inicializado.
Algumas outras respostas tocam nisso, mas a diferença é que a configuração precisa ser carregada assim que o aplicativo iniciar , para que possa ser usada por um serviço normal sempre que precisar.
Para implementar a configuração do tempo de execução:
/src/assets/
pasta (para que seja copiado na construção)AppConfigService
para carregar e distribuir a configuraçãoAPP_INITIALIZER
1. Adicionar arquivo de configuração a
/src/assets
Você pode adicioná-lo a outra pasta, mas precisa informar a CLI que é um ativo no
angular.json
. Comece usando a pasta de ativos:{ "apiBaseUrl": "https://development.local/apiUrl" }
2. Crie
AppConfigService
Este é o serviço que será injetado sempre que você precisar do valor de configuração:
@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. Carregue a configuração usando um
APP_INITIALIZER
Para permitir que o
AppConfigService
seja injetado com segurança, com a configuração totalmente carregada, precisamos carregar a configuração no momento da inicialização do aplicativo. É importante ressaltar que a função de fábrica de inicialização precisa retornar umPromise
para que o Angular saiba que deve esperar até que termine de resolver antes de terminar a inicialização:@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 { }
Agora você pode injetá-lo onde precisar e toda a configuração estará pronta para ler:
@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; } }
Eu não posso dizer com força suficiente, configurar seus urls de API como configuração em tempo de compilação é um antipadrão . Use a configuração de tempo de execução.
fonte
APP_INITIALIZER
mas não acho que possa facilmente torná-los dependentes uns dos outros. Parece que você tem uma boa pergunta a fazer, então, talvez um link para ela aqui?Esta é minha solução, carrega de .json para permitir alterações sem reconstruir
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); } }
e config.json
{ "apiUrl": "http://localhost:3000/api" }
fonte
environments.prod.ts
afterng build --prod
estará em algum.js
arquivo em algum momento. Mesmo se ofuscados, os dados deenvironments.prod.ts
estarão em texto não criptografado. E como todos os arquivos .js, ele estará disponível na máquina do usuário final.Arquivo de configuração do pobre:
Adicione ao seu index.html como a primeira linha na tag do corpo:
<script lang="javascript" src="assets/config.js"></script>
Adicione ativos / config.js:
var config = { apiBaseUrl: "http://localhost:8080" }
Adicione config.ts:
export const config: AppConfig = window['config'] export interface AppConfig { apiBaseUrl: string }
fonte
Descobri que usar um
APP_INITIALIZER
para isso não funciona em situações em que outros provedores de serviço exigem que a configuração seja injetada. Eles podem ser instanciados antes deAPP_INITIALIZER
serem executados.Já vi outras soluções que usam
fetch
para ler um arquivo config.json e fornecê-lo usando um token de injeção em um parâmetroplatformBrowserDynamic()
antes de inicializar o módulo raiz. Masfetch
não é compatível com todos os navegadores e, em particular, os navegadores WebView para os dispositivos móveis que pretendo.A seguir está uma solução que funciona para mim tanto para PWA quanto para dispositivos móveis (WebView). Nota: Eu só testei no Android até agora; trabalhar em casa significa que não tenho acesso a um Mac para construir.
Em
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();
Este código:
config.json
arquivo.APP_CONFIG
token de injeção, antes da inicialização.APP_CONFIG
pode então ser injetado em quaisquer provedores adicionaisapp-module.ts
e será definido. Por exemplo, posso inicializar oFIREBASE_OPTIONS
token de injeção@angular/fire
com o seguinte:{ provide: FIREBASE_OPTIONS, useFactory: (config: IConfig) => config.firebaseConfig, deps: [APP_CONFIG] }
Acho tudo isso uma coisa surpreendentemente difícil (e hackeada) de fazer para um requisito muito comum. Esperançosamente, em um futuro próximo, haverá uma maneira melhor, como suporte para fábricas de provedores assíncronos.
O resto do código para integridade ...
Em
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');
e em
app/lib/config/config.ts
eu defino a interface para meu arquivo de configuração JSON:export interface IConfig { name: string; version: string; instance: string; firebaseConfig: { apiKey: string; // etc } }
A configuração é armazenada em
assets/config/config.json
:{ "name": "my-app", "version": "#{Build.BuildNumber}#", "instance": "localdev", "firebaseConfig": { "apiKey": "abcd" ... } }
Observação: eu uso uma tarefa do Azure DevOps para inserir Build.BuildNumber e substituir outras configurações para diferentes ambientes de implantação conforme ele está sendo implantado.
fonte
Aqui estão minhas duas soluções para isso
1. Armazene em arquivos json
Basta fazer um arquivo json e entrar em seu componente pelo
$http.get()
método. Se eu precisar disso muito baixo, então é bom e rápido.2. Armazene usando serviços de dados
Se você deseja armazenar e usar em todos os componentes ou em grande uso, é melhor usar o serviço de dados. Como isso :
Basta criar uma pasta estática dentro da
src/app
pasta.Crie um arquivo denominado
fuels.ts
em uma pasta estática. Você pode armazenar outros arquivos estáticos aqui também. Vamos definir seus dados assim. Supondo que você tenha dados de combustíveis.__
export const Fuels { Fuel: [ { "id": 1, "type": "A" }, { "id": 2, "type": "B" }, { "id": 3, "type": "C" }, { "id": 4, "type": "D" }, ]; }
__
import { Injectable } from "@angular/core"; import { Fuels } from "./static/fuels"; @Injectable() export class StaticService { constructor() { } getFuelData(): Fuels[] { return Fuels; } }`
basta importar no arquivo app.module.ts como este e alterar os provedores
import { StaticService } from './static.services'; providers: [StaticService]
Agora use isso como
StaticService
em qualquer módulo.Isso é tudo.
fonte
Considero este Angular How-to: Editable Config Files dos blogs de desenvolvimento da Microsoft a melhor solução. Você pode definir as configurações de desenvolvimento de desenvolvimento ou configurações de desenvolvimento de produto.
fonte
Tivemos esse problema anos atrás, antes de eu entrar e implementar uma solução que usava armazenamento local para informações do usuário e do ambiente. Angular 1,0 dias para ser exato. Anteriormente, estávamos criando dinamicamente um arquivo js em tempo de execução que colocaria os api urls gerados em uma variável global. Estamos um pouco mais orientados para OOP atualmente e não usamos armazenamento local para nada.
Criei uma solução melhor para determinar o ambiente e a criação de APIs.
Como isso difere?
O aplicativo não será carregado a menos que o arquivo config.json seja carregado. Ele usa funções de fábrica para criar um grau mais alto de SOC. Eu poderia encapsular isso em um serviço, mas nunca vi nenhum motivo quando a única semelhança entre as diferentes seções do arquivo é que elas existem juntas no arquivo. Ter uma função de fábrica me permite passar a função diretamente para um módulo se ele for capaz de aceitar uma função. Por último, é mais fácil configurar o InjectionTokens quando as funções de fábrica estão disponíveis para uso.
Desvantagens?
Você está sem sorte ao usar esta configuração (e a maioria das outras respostas) se o módulo que você deseja configurar não permite que uma função de fábrica seja passada para forRoot () ou forChild (), e não há outra maneira de configure o pacote usando uma função de fábrica.
Instruções
- É aqui que minha solução começa a realmente diferir -
{}
ouany
quando você sabe que pode especificar algo mais concreto- e / ou -
- main.ts
Eu verifico que a janela ["ambiente"] não está preenchida antes de criar um ouvinte de evento para permitir a possibilidade de uma solução em que a janela ["ambiente"] seja preenchida por algum outro meio antes que o código em main.ts seja executado.
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)); }); }
--- environment-resolvers.ts
Eu atribuo um valor ao BehaviorSubject usando a janela ["ambiente"] para redundância. Você poderia conceber uma solução em que sua configuração já esteja pré-carregada e a janela ["ambiente"] já esteja preenchida no momento em que qualquer código do aplicativo Angular for executado, incluindo o código em 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 - Simplificado para facilitar a compreensão
Fato engraçado! Versões mais antigas do NGXLogger exigiam que você passasse um objeto para LoggerModule.forRoot (). Na verdade, o LoggerModule ainda o faz! NGXLogger gentilmente expõe LoggerConfig que você pode substituir permitindo que você use uma função de fábrica para configuração.
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
Termo aditivo
Como resolvi a criação de meus URLs de API?
Eu queria ser capaz de entender o que cada url fazia por meio de um comentário e queria verificar o tipo, já que essa é a maior força do TypeScript em comparação com o javascript (IMO). Eu também queria criar uma experiência para que outros desenvolvedores adicionassem novos endpoints e apis que fosse o mais integrada possível.
Eu criei uma classe que leva no ambiente (dev, test, stage, prod, "" e etc) e passei esse valor para uma série de classes [1-N] cujo trabalho é criar o url base para cada coleção de API . Cada ApiCollection é responsável por criar a url base para cada coleção de APIs. Podem ser nossas próprias APIs, APIs de um fornecedor ou até mesmo um link externo. Essa classe passará o url de base criado para cada API subsequente que ela contém. Leia o código abaixo para ver um exemplo básico. Uma vez configurado, é muito simples para outro desenvolvedor adicionar outro endpoint a uma classe Api sem ter que tocar em mais nada.
TLDR; princípios básicos de OOP e lazy getters para otimização de memória
@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/"); } }
fonte