Dynamic Environment Variables in Angular
Overview
Normally, Angular uses environment variables by importing them from environment.ts
under environment
folder. If you want to switch environment variables depending on the environment, you should define enviroment.dev.ts
, enviroment.prod.ts
, and so on for that environment, and define fileReplacements
in angular.json
. However, the problem here is that these environment variables are embedded in the build file when Angular is built, so you can't overwrite them later. For example, if the environment variables are different between the staging and production environments, Anuglar must be built separately in the staging and production environments, and the same artifact cannot be brought from the staging environment to the production environment.
This article is mainly about how to solve the above situation so that environment variables can be switched from outside. To summarize the conclusion at first, the following two points are important.
- In
main.ts
, get the dynamic environment variables and rewriteenvrionment.ts
to Provide. - Environment variables are not referenced directly from files, but are injected from DI continer.
Implementation
Rewriting environment variables in main.ts
main.ts
is a file that always exists under scr
of an Angular project, and it is the starting point for launching Angular applications. Normally, this main.ts
is executed, bootstrapping AppModule
, and then AppModule
bootstraps AppComponent
, and so on, to launch the Angular application.
If you open main.ts
, you can see that the main
function is actually defined and executed, and that within the main
function, platformBrowserDynamic()
and the function bootstrapModule()
following it are executed. The latter, bootstrapModule()
, is a function to bootstrap the AppModule
I mentioned earlier, but the former, platformBrowserDynamic()
, can actually Provide for 'platform'
.
In general, in Angular applications, the Injector of AppModule is the Root Injector, and above it is the Null Injector, but if you Provide the value with platformBrowserDynamic()
, you can actually Provide to 'platform'
, which is one level above root. This time, we will rewrite the environment variables in main.ts
(before starting AppModule), and then Provide the environment variables here. For more information about 'platform'
, please refer to the official document or the following video.
The code is as follows.
main.tsimport { enableProdMode, InjectionToken } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { overrideEnv } from './override-env';
if (environment.production) {
enableProdMode();
}
/**
* The definition of InjectionToken is written in main.ts for convenience, but I think it is better to put it in a separate file.
*/
export const ENVIRONMENT = new InjectionToken<Record<string, unknown>>(
'environment'
);
const main = async () => {
overrideEnv(environment).then((dynamicEnv) => {
platformBrowserDynamic([{ provide: ENVIRONMENT, useValue: dynamicEnv }])
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
});
};
main();
The overrideEnv
, which is executed first in the main
function, should be implemented as follows.
override-env.tsexport const overrideEnv = async <T>(env: T): Promise<T> => {
try {
const response = await fetch('https://sample.com/api/client-env');
const dynamicEnv: T = await response.json();
for (const key in dynamicEnv) {
if (Object.prototype.hasOwnProperty.call(dynamicEnv, key)) {
const targetProp = env[key];
if (targetProp !== undefined) {
env[key] = dynamicEnv[key];
}
}
}
} catch (e) {}
return env;
};
This overrideEnv()
takes an environment variable as an argument, rewrites the value of the environment variable retrieved from the server if there is a matching property, and returns the rewritten environment variable. The internal processing of the function (how to retrieve dynamic environment variables and how to merge the retrieved environment variables) needs to be implemented according to the circumstances of each project.
Using environment variables
The environment variable is now ready. In order to use this environment variable, DI using the InjectionToken
that we defined earlier.
sample-component.tsimport { Component, Inject, OnInit } from '@angular/core';
import { ENVIRONMENT } from 'src/main';
@Component({
selector: 'app-sample',
templateUrl: './sample.component.html',
styleUrls: ['./sample.component.scss'],
})
export class SampleComponent implements OnInit {
constructor(@Inject(ENVIRONMENT) env: Record<string, unknown>) {}
ngOnInit(): void {}
handleClick(): void {
console.log(this.env);
}
}
Of course, it is necessary to use the value of the environment variable provide to 'platform'
, so it is necessary to prohibit the direct reference to the environment variable from environment.ts
. Although this is not the case in the example in this article, I think it is possible to specify @deprecated
in environment.ts
by TSDoc/JSDoc in the environment
object itself.
Summary
In order to handle environment variables dynamically, I introduced how to override environment variables in main.ts
and Provide. As a side note, this time I used main.ts
to inject the process before the Angular application is launched, but Angular provides APP_INITIALIZER
, an InjectionToken
, which can be used to inject the process before the AppModule
bootstraps the AppComponent
a little later. However, Angular provides a InjectionToken
called APP_INITIALIZER
, which allows you to insert a process before the AppModule
bootstraps the AppComponent
. This is basically a mechanism for initializing the service, and is not suitable (and probably not possible) for proving values, but it can be useful to know. If you want to use Service to get environment variables, you can initialize the environment variable Service with APP_INITIALIZER
.