It’s considered a best practice to build your application in an immutable way. No matter the environment you are running your app in, you are running the same artifact. It ensures consistency over your different environments and test stages.
You then inject an environment-specific configuration to make sure the app has the correct settings based on the environment like an API endpoint that will be different between production
and development
.
Unfortunately, this is not possible with all webpack
based tools and frameworks when you don’t have any server side rendering (SSR). webpacks
imports all modules during the build phase and has no way to override this due to its nature.
When you are running a JS project with a node backend (Universal, SSR, etc), you can always inject some configuration (like environment variables) at runtime using process.env
but this is not possible when only serving a fully static build.
So how do we inject those specific environment configurations?
- Make everything based on formulas. It can be useful for API urls for example. If your frontend is connecting to
https://api.<the-same-hostname>
, then it is worth is to just replace that bywindow.location.hostname
. But it falls short when the configuration value can’t be guessed. - Hardcode the values with a
switch
. That’s just bad. It can work if you have fixed environment but not with dynamic ones like Platform.sh provide. - Load and inject a configuration file at runtime. We are going to dig deeper into that solution.
Example with Nuxt.js
You can browse this sample code at https://github.com/gmoigneu/js-runtime-config
Load the configuration
So our app runs in the client browser. That means we can’t get any info from the server without actually making a new http
request to a public configuration.
Let’s do this in one of our component:
await axios.get('/run/config.json').then(m => {
...
}
We can now use the JSON values inside that file to customize our runtime. Let’s do a XHR call to our API with the token
we got in our .json
:
await axios.get('/run/config.json').then(m => {
axios.get(window.location.protocol + '//api.' + window.location.hostname + '/?token=' + m.data.token)
.then(response => {
this.message = response.data.message
})
})
We are storing the response message
in our component data
to display it.
But how is this configuration generated?
The json can for sure be written by hand but let’s use the tooling that Platform.sh provides to generate this.
First, we need an environment variable. Let’s define one as JSON. I’ll name it NUXT
:
Please note that I name it env:NUXT
in order to have a real environment variable available.
Then Platform.sh goes through 2 different steps when you deploy:
- The
build
step where environment specific configurations are not available - The
deploy
step that knows the environment and is run just after the new container has been put into service
We are going to define a new deploy
action that will trigger a script:
hooks:
deploy: |
set -e
./build-config.sh
Our build-config.sh
will be like this:
#!/bin/bash
echo $NUXT > ./static/run/config.json
This is a really basic script that just outputs the variable content in a file. Feel free to play with more complex actions by using jq
for example.
This configuration will generate our config.json
file everytime we deploy an environment.
Two important things to note:
- The
config.json
file must be publicly available (thestatic
folder is used on Nuxt for that purpose) - That folder needs to be writeable by the app. Here is the Platform.sh configuration:
mounts:
'static/run':
source: local
source_path: run
The
config.json
being publicly available, you can’t put any secret in it!
Result
As you can see, our frontend JS is now doing a first XHR call to get the config.json
object and then properly calling the API with the right token
.
Improvements
My example is more a proof-of-concept than anything else. Nuxt and Next provide ways of handling this through plugins and modules. You should definitely load the configuration once and then reuse the values, not loading it on each components.
Based on my research on webpack, it should be possible to load a dynamic module through require.ensure
. Unfortunately, I couldn’t get it to work properly. If you have any insight on this, feel free to get in touch!
Previous post
4 out of #100DaysOfCode - Data singleton