Vue.js applications created via vue-cli
make it very hard to expose environment variables at runtime.
This is a hacky solution I found to make it possible.
The problem
By default vue-cli creates a purely browser-based application, completely served as static files. The built application can be exposed as a static website.
This is really good, unless you need to expose ENV variables defined at runtime.
If you use process.env
anywhere in your application, it gets resolved at compile time, and the result is hard-coded
into the generated js file. Moreover, only variables starting with VUE_APP_
are included. Variables are read
from the envirnoment (in Unix-based systems) and from a .env
file in the root of the project (on any OS).
In my case, I needed to generate a Docker image with an integrated webserver, but I needed to pass a variable based on where the Docker image is run, and I couldn't recompile the image on every run
Serving the static files
The first step is to create a server.js
file in the root of the project. This is the default file served when using
yarn start
(or npm start
). This is the base:
const express = require('express')
const serveStatic = require('serve-static')
const path = require('path')
const app = express()
app.use('/', serveStatic(path.join(__dirname, '/dist')))
const port = process.env.PORT || 5000
app.listen(port)
console.log('Server started on port ' + port)
In this file process.env
is the real environment, but it's not really helpful, as everything Vue uses starts from
src/main.js
, which will become the base for the generated dest/main.js.*
.
The hacky solution
First of all we must serve the real process.env
content from a dedicated endpoint. Modify server.js
:
const express = require('express')
const serveStatic = require('serve-static')
const path = require('path')
const app = express()
app.get('/config.js', (req, res) => {
res.set('Content-type', 'text/javascript')
res.send(`window.CONFIG = ` + JSON.stringify(process.env))
})
app.use('/', serveStatic(path.join(__dirname, '/dist')))
const port = process.env.PORT || 5000
app.listen(port)
console.log('Server started on port ' + port)
The only addition is the app.get('/config.js'...
part, which will export a CONFIG
variable as a global inside the
window
object.
Here, instead of exporting the whole process.env
content, we can filter out just the variables we need exported.
The second step is to add it to public/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>favicon.ico">
<title>My site title</title>
<script src="/config.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Most of the file is the original one created by vue-cli
. We just added the <script src="/config.js"></script>
part.
Now the browser will start complaining that the file /config.js
is missing when running via yarn serve
, so let's
add a semi-empty file to make it happy. Create a new public/config.js
with this content:
window.CONFIG = null
And finally put it to good use in src/main.js
:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// Start support for ENV vars
let CONFIG = !!window.CONFIG ? window.CONFIG : process.env
function getConfig(k, dfl) {
if (CONFIG[k]) {
return CONFIG[k]
} else if (CONFIG['VUE_APP_'+k]) {
return CONFIG['VUE_APP_'+k]
} else {
return dfl
}
}
// End support for ENV vars
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
Most of the file is generated by vue-cli
. We added the part marked between comments.
The let...
line checks if window.CONFIG
is defined. If it is, the file has been served by node.js, so it loaded
the genrated config.js
, and it uses it.
If it's not defined, it means the file has been served by yarn serve
, so window.CONFIG
is null
, and it takes
process.env
as a fallback.
The getConfig())
function allows to use the same name when served via normal ENV (supporting any name) and via
"simulated" ENV (where the variables must start with VUE_APP_
).
How to use it in the code
The getConfig
function in this form can be used in the main.js
, but not in the components.
To make it globally accessible, let's create a plugin. In src/main.js
, add this after the getConfig
definition
and before the new Vue
line:
const envPlugin = {
install(Vue, options) {
Vue.prototype.$env = {
get: function (k, dfl) {
return getConfig(k, dfl)
}
}
}
}
Vue.use(envPlugin)
This will create a global $env
object in every component. For example, to use it in a computed property:
<template>
<h1>Server URL: { { server_url } }</h1>
</template>
<script>
export default {
name: 'my-component',
computed: {
server_url() { return this.$env.get('SERVER') },
}
}
</script>
Note: there should be no space between the {
and }
. It is shown here to avoid Vue.js interpolation.
How to use it at runtime
During development we can use a .env
file to define our variables, for example:
VUE_APP_REST_SERVER=http://localhost:8088
VUE_APP_TOKEN=abc123def456ghi789
and run yarn serve
or pass them on the command line:
VUE_APP_REST_SERVER=http://localhost:8088 VUE_APP_TOKEN=abc123def456ghi789 yarn serve
To prepare the bundle for production we use:
yarn build
At runtime we then define the variables on the command line (or via Kubernetes or whetever is used):
REST_SERVER=http://localhost:8088 TOKEN=abc123def456ghi789 yarn start
WARNING: All the data you pass in this way is visible from the browser. Do not send sensitive information like passwords or personal email addresses