How to configure the setting of Nuxt.js environment variables in runtime, or How to do everything not like everything and do not regret

( Illustration )
Senior web developers Anton and Alexey continue the story about the difficult fight against Nuxt. In the previous round of battle with this framework, they showed how to run a project on Nuxt so that everyone was happy. In the new article we will talk about the real application of the framework.
We began to rewrite the project with a huge technical debt. The monthly audience was 6-7 million unique visitors, but the existing platform presented too many problems. Therefore, it was decided to send her to retire. Of course, performance was our greatest concern, but also did not want to squander on SEO.
After a couple of rounds of discussion, we decided not to rely on the traditional approach with only server rendering - but not to drive ourselves into the trap of client rendering. As a result, we began to build a solution based on Nuxt.js .
Good old Nuxt.js
We take already known to us from the previous article “framework for the framework” based on Vue.js for building universal client-server applications. In our case, the application works in conjunction with a rather complicated API (microservice artfulness, but about this some other time) and several layers of caching, it renders editable content and returns static content for lightning-fast performance. Great, right?
In fact, there is nothing new here. But what makes Nuxt.js interesting is the ability to quickly start a project with client-server rendering. Sometimes you need to go against the framework set by the framework. That is what we did.
No time to explain, build once, deploy many!
Somehow the technical team approached us and puzzled: whenever we push changes into the repository, we need to make a build for each of the environments (dev-, stage- and prod-environments) separately. It was slow. But what is the difference between these builds? Yes, only in environment variables! And what he asked to do sounded logical and reasonable. But our first reaction was: O_o
The “Build once, deploy many” strategy makes sense in the software development world. But in the world of Javascript ... We have a whole battery of compilers, transpilers, pre- and post-processors, and also tests and linters. All this takes time to set up for each of the environments. In addition, there are many potential problems with leaking sensitive data (secrets, API keys, and so on, which can be stored in configurations).
And we started
Of course, we started with Google search. Then we talked to the Nuxt.js maintainers, but without much success. What to do - I had to invent a solution on my own, and not to copy from StackOverflow (this is the basis of our activity, isn't it?).
We will understand how Nuxt.js does it
Nuxt.js has a configuration file with the expected name nuxt.config.js. It is used to programmatically transfer configurations to an application:
const config = require('nuxt.config.js')
const nuxt = new Nuxt(config)
It is possible to set the environment through env-variables. In general, a fairly common practice is to include the configuration file dynamically. Then it all goes to the definePlugin webpack and can be used on the client and server, like this:
process.env.propertyName
// or
context.env.propertyName.
These variables are baked during assembly, more information here: Nuxt.js env page .
Notice the webpack? Yes, it means compilation, and this is not what we want.
Try otherwise
Understanding how Nuxt.js works means to us:
- we can no longer use the env inside nuxt.config.js;
- any other dynamic variables (for example, inside head.meta) must be passed to the nuxt.config.js object in runtime.
The code in server / index.js is:
const config = require('../nuxt.config.js')
Change to:
// Импорт расширенных конфигураций Nuxt.jsconst config = require('./utils/extendedNuxtConfig.js').default
Where utils / extendedNuxtConfig.js:
import config from'config'import get from'lodash/get'// Импорт конфигураций Nuxt.jsconst defaultConfig = require('../../nuxt.config.js')
// Расширенные настройкиconst extendedConfig = {}
// Смержим конфигурации Nuxt.js const nuxtConfig = {
...defaultConfig,
...extendedConfig
}
// Финальные манипуляции для мест// где нам не нужны расширенные настройкиif (get(nuxtConfig, 'head.meta')) {
nuxtConfig.head.meta.push({
hid: 'og:url',
property: 'og:url',
content: config.get('app.canonical_domain')
})
}
exportdefault nuxtConfig
Elephant, we did not notice
Well, we solved the problem of obtaining dynamic variables from outside the env properties of the configuration object in nuxt.config.js. But the original problem is still not solved.
It was suggested that some abstract sharedEnv.js would be used for:
- client - create an env.js file that will be loaded globally (window.env.envKey),
- server - imported into modules, where necessary,
- isomorphic code, something like
context.isClient? window.env [key]: global.sharedEnv [key].
Somehow not great. This abstraction would solve the most serious problem - the leakage of confidential data to the client application, since it would be necessary to add value consciously.
Vuex will help us
During the investigation of the problem, we noticed that the Vuex store is exported to a window object. This decision is forced to support the isomorphism of Nuxt, js. Vuex is a Flux inspired data warehouse specifically designed for Vue.js applications.
Well, why not use it for our common variables? This is a more organic approach - the data in the global repository suits us.
Let's start with server / utils / sharedEnv.js:
import config from'config'/**
* Настройка объекта, доступного для клиента и сервера
* Не усложняйте, объект должен быть плоским
* Будьте внимательны, чтобы не произошло утечки конфиденциальной информации
*
* @type {Object}
*/const sharedEnv = {
// ...
canonicalDomain: config.get('app.canonical_domain'),
}
exportdefault sharedEnv
The code above will run during server startup. Then add it to the Vuex repository:
/**
* Получить объект конфигураций.
* Документация подразумевает только выполнение на стороне сервера
* см. больше здесь
* https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action
*
* @return {Object} Shared environment variables.
*/const getSharedEnv = () =>
process.server
? require('~/server/utils/sharedEnv').default || {}
: {}
// ...exportconst state = () => ({
// ...
sharedEnv: {}
})
exportconst mutations = {
// ...
setSharedEnv (state, content) {
state.sharedEnv = content
}
}
exportconst actions = {
nuxtServerInit ({ commit }) {
if (process.server) {
commit('setSharedEnv', getSharedEnv())
}
}
}
We will rely on the fact that nuxtServerInit runs during, hm, server initialization. There is some difficulty: pay attention to the getSharedEnv method, a check for repeated executions on the server is added here.
What happened
Now we have shared variables that can be retrieved in components like
this: $. Store.state.sharedEnv.canonicalDomain
Victory!
Oh, no. What about plugins?
To configure some plugins, environment variables are needed. And when we want to use them:
Vue.use (MyPlugin, {someEnvOption: 'There is no access to the vuex store'})
Great, race condition, Vue.js tries to initialize itself before Nuxt.js registers the sharedEnvobject in the Vuex storage.
Although the function that registers plugins provides access to a Context object containing a link to the repository, sharedEnv is still empty. This is solved quite simply - let's make the plugin an async-function and wait for nuxtServerInit to execute:
import Vue from'vue'import MyPlugin from'my-plugin'/**
* Конфигурация плагина MyPlugin асинхронно.
*/exportdefaultasync (context) => {
// выполнить вручную, чтобы иметь доступ к объекту sharedEnvawait context.store.dispatch('nuxtServerInit', context)
const env = { ...context.store.state.sharedEnv }
Vue.use(MyPlugin, { option: env.someKey })
}
Now it's a victory.