Probably the most understated and under rated aspect of development is the importance of configuration and managing environments. If you really want to get serious about developing great software then its vitally important to become familiar with aspect of development.
Configuration and Environment variables will have a huge impact on your code, making it more manageable, readable, scale-able and undoubtedly less brittle.
A common problem in software development and also a very common cause of bugs are what are commonly referred to as Magic Strings and Numbers. This is an old software development issue and has been around since the beginning and is often discussed in books like Clean Code as to why they are common cause of errors and application bugs.
Clean Code
A Handbook of Agile Software Craftsmanship
software developers irrespective of programming language or discipline should read this book
The problem with Magic Strings and Numbers is that often developers are unaware they have introduced them to their code. I often unintentionally introduce a Magic Strings in code without even thinking but then often during a code review or attempting to Unit Test a function I realise and usually have to admonish myself for doing so.
A fairly recent example of this, was while writing the code for the tutorial on using Vuex and Vue-Router in a Vue Project the original draft release included this code.
I have highlighted the offending line, although strictly speaking there are even further examples contained. However, for the purpose of this tutorial I am going to focus primarily on addressing the line highlighted.
actions: { login({commit}, authData) { axios.post('/verifyPassword?key=[Insert API Key Here]', { email: authData.email, password: authData.password, returnSecureToken: true }) .then(res => { commit('login', { token: res.data.idToken, userId: res.data.localId, expirationDate: res.data.expiresIn, user: res.data.email }); router.push('/dashboard'); }) .catch(error => console.log(error)) },
Problems with Magic Strings in Code
In this particular instance, there are a number of issues with this code. Let me outline them so for the uninitiated can understand.
- You run into the danger of including your secure API key from being included into your source code repository, enabling anyone with access to your source code from viewing and stealing your API key
- We are hard coding a Url Path in our code
- We have introduced complexity because we depend on the developer to insert the correct API key
- This will involve hard coding the API key in code
- Changing these strings requires editing the code, so when changing Environments this code would need to be edited before deployment
a magic string is an input that a programmer believes will never come externally and which activates otherwise hidden functionality
Typically, the issues highlighted in this example can quite easily be fixed and eradicated by making use of Configuration and Environment variables and a slight refactor of the code to implementing the JavaScript Module Pattern but using the ES6 syntax to pull complexity downwards
A Philosophy of Software Design
how to decompose complex software systems into modules that can be implemented relatively independently.
What we will aim to achieve to make our code more testable and more modular which will also make it easier to read. Which in my experience of working with front end code developed by others isn't all that well practised in the wild.
Using Configuration and Environment Variables in Vue
Fortunately if you have generated your Vue Project by either using the Vue-Cli or Nuxt then a lot of the Configuration and Environment Variable functionality is already available to you. We'll be concentrating on making use and integrating this functionality in our project.
The Vue documentation already has great documentation of using Modes and Environment Variables but I thought I would elaborate on that documentation to provide a typical real world development scenario of when and where you would want to make use of it.
Lets address one of these issues at a time , by illustrating how we could refactor our exisitng code sample to remove these issues. For reference our whole staring store.js code is included, which incidentally has a number of Magic Strings included and by the end of this tutorial we will have removed them to improve our code.
import Vue from 'vue' import Vuex from 'vuex' import VuexPersist from 'vuex-persist'; import axios from './services/axios-auth' import localForage from 'localforage'; import router from './router'; Vue.use(Vuex) const vuexStorage = new VuexPersist({ key: 'sourcelink', storage: localForage, }) export default new Vuex.Store({ plugins: [vuexStorage.plugin], state: { token: null, userId: null, user: null }, getters: { loggedIn: state => !!state.token }, mutations: { login(state, userData) { state.token = userData.token; state.userId = userData.userId; state.expirationDate = userData.expirationDate; state.user = userData.user; }, logout(state) { state.token = ''; state.userId = ''; state.expirationDate = ''; state.user = ''; }, }, actions: { login({commit}, authData) { axios.post('/verifyPassword?key=BIzaSyDjPoP8MDry5Khv1X3tqD77nGAry00_7jB', { email: authData.email, password: authData.password, returnSecureToken: true }) .then(res => { commit('login', { token: res.data.idToken, userId: res.data.localId, expirationDate: res.data.expiresIn, user: res.data.email }); router.push('/dashboard'); }) .catch(error => console.log(error)) }, logout({commit}) { commit('logout'); router.push('/login'); } } })
Warning
I have included a fictional API key here, just to illustrate a typical problem we could face.
This is not a real key but is designed to resemble a real key, so if you attempt to use this key your code will not work.
Introducing .env files
The first part of our journey will include add some additional files to the root directory of application. We are going to add 3 files new files
touch .env touch .env.development.local touch .env.production.local
A quick run down of the files we've just created the .env
file will contain general environment configuration settings which are project settings regardless of environment. So effectively regardless whether the project is running in development, staging or production variables contained in this file are made available to the application.
The variables contained in either the .env.development.local
or .env.production.local
will only be available if the application is run within those environments.
I won't repeat the already excellent information in Modes and Environment Variables rather I would urge you to read it, it is extremely helpful.
The first Environment variable we are going to create to solve one of issues in our code, is to add a variable to .env
and name it VUE_APP_STORAGE_KEY
the purpose of this key is provide a name for our key that will be located in the application storage of our choice.
VUE_APP_STORAGE_KEY='sourcelink'
You'll notice we have followed a convention here and prefixed our variable with VUE_APP_
this is because as mentioned in the documentation only variables that start with VUE_APP_
will be statically embedded into the client bundle with webpack.DefinePlugin
which means VUE_APP_*
variables will always be available in your app code so referencing them is really easy.
Lets go head and reference these variables in our code. We'll make a slight refactor to the code of our store.js
as follows:
import Vue from 'vue' import Vuex from 'vuex' import VuexPersist from 'vuex-persist'; import axios from './services/axios-auth' import localForage from 'localforage'; import router from './router'; Vue.use(Vuex); const vuexStorage = new VuexPersist({ key: process.env.VUE_APP_STORAGE_KEY, storage: localForage, }); ........ // rest of the code stays the same
That's it! We have now enabled our application to make use the environment variable. The end result is if we ever wanted to change the name of storage key, we simply edit the .env
restart the application and the new name is made available.
We have also just removed the first magic string from our application! The was simple. Developers will now know that the name for this key is available in the environment variable file.
We're now ready to cover a slightly more complicated example in which we're actually going to use some advanced JavaScript functionality.
Pulling complexity downwards
In reviewing the original code it started to make a little uneasy and I felt the code was getting a little too complicated and the store.js was becoming a little bloated. Even in such a small example file I felt we could do a lot better!
The function that I was most concerned about was the login
function it seemed there was just too much going on there and ideally we need to simplify it so other developers can quickly and easily understand it.
In order to address these concerns lets create a new folder in our application which we will rather imaginatively name modules
and create an empty JavaScript file we will call firebase-authentication.js
mkdir src/modules touch src/modules/firebase-authentication.js
For the purpose of this tutorial we going to add some simple code in our module to begin to illustrate a point. The module we're going to start developing will eventually consist of functions and properties to connect to our Google Firebase API for authentication.
Implement the following code in our module, which is going to be just enough code we need at this stage.
export default { name: 'auth', properties:{ loginUrl : '/verifyPassword?key=${process.env.VUE_APP_FIREBASE_API_KEY}' }, methods: { } }
The line we've highlighted is of particular interest, you'll notice we will be making use of an environment variable again which will contain our API key. We are also making use of an additional ES6 feature of String Interpolation or Template Literals .
We make use `
or what is commonly referred to as the back tick to create a string, then we can add ${}
to access a value from else where. This is common feature in other programming languages but has only been available in JavaScript since ES6.
We will add our new Environment Variable to our .env.development
file, the primary reason why is that it is common to use different credentials during Development and Production, so in our case we are going to accessing a development instance of our API.
VUE_APP_FIREBASE_API_KEY=BIzaSyDjPoP8MDry5Khv1X3tqD77nGAry00_7jB
We can return to edit our store.js
file to make use of our new module.
import Vue from 'vue' import Vuex from 'vuex' import VuexPersist from 'vuex-persist'; import axios from './services/axios-auth' import localForage from 'localforage'; import router from './router'; import Auth from './modules/firebase-authentication'; Vue.use(Vuex); const vuexStorage = new VuexPersist({ key: process.env.VUE_APP_STORAGE_KEY, storage: localForage, }); export default new Vuex.Store({ plugins: [vuexStorage.plugin], state: { token: null, userId: null, user: null }, getters: { loggedIn: state => !!state.token }, mutations: { login(state, userData) { state.token = userData.token; state.userId = userData.userId; state.expirationDate = userData.expirationDate; state.user = userData.user; }, logout(state) { state.token = ''; state.userId = ''; state.expirationDate = ''; state.user = ''; }, }, actions: { login({commit}, authData) { axios.post(Auth.properties.loginUrl, { email: authData.email, password: authData.password, returnSecureToken: true }) .then(res => { commit('login', { token: res.data.idToken, userId: res.data.localId, expirationDate: res.data.expiresIn, user: res.data.email }); router.push('/dashboard'); }) .catch(error => console.log(error)) }, logout({commit}) { commit('logout'); router.push('/login'); } } })
We import our newly created module and assign it the name Auth
the we can simply make use of our loginUrl
property of the file in the highlighted line.
Summary
Making use of few great features of ES6 and Vue.JS we have managed to clean up our code a little. However, I am still not satisfied with the code at this stage and it really can do with some additional refactoring.
The key issue I am concerned about here is that the store.js file is primarily going to be the centre point of our application and potentially the file could swell to over a 1000 lines of code, which will make maintenance difficult and we are not going to earn the support and trust of the developers that come after us. So in the follow up tutorial we are going to expand on what we have learned here and further break down our store.js file into smaller more manageable modules.
If you enjoyed this article and would like to be notified when the follow up this is published or even would like to know when I publish more tutorials why not subscribe to my newsletter.
Alternatively use the comments below to let me know of any omissions or queries or even just to send me some hate mail!
- What is this Directory.Packages.props file all about? - January 25, 2024
- How to add Tailwind CSS to Blazor website - November 20, 2023
- How to deploy a Blazor site to Netlify - November 17, 2023