A couple of years ago there was a huge leap in the way how front-end part of web apps is developed. We went from small helper JS libraries downloaded directly from the Internet to extensive frameworks managed through a plethora of additional tools. At first everything was changing fast, way too fast. But, thankfully, things have settled down and matured immensely. In this article I will attempt to shed some light on the core concepts of every modern front-end solution.

1. The package manager

The problem

During the stone age of front-end development we didn’t have large libraries. And not because of some arbitrary technical limitations. It stemmed rather from the fact that managing large codebases was something of a nightmare for both the author and the user. Why?

First of all - dependencies. You can’t import/include JS in other JS dynamically (not in pure JS at least). The way it typically works is that either the user downloads and includes all of the dependencies in correct versions and order on his page or you concatenate everything to a single file and distribute it in such form. This is all fine and dandy - but only up to a point. Also, both solutions are not really viable when you want to use multiple libraries with the same dependencies.

Now, what about updating? If there’s an update available in the main library, you have to stalk its website (considering you remembered all of them in the first place) for the updated files and then swap them manually. And what happens when there is an update to the dependency? If it’s not bundled - even more websites for you to stalk, if it is - you still have to wait for the author of the library to do that. Inconvenient? A bit.

Finally, there is the problem of wasted space. You clutter your repository with code that isn’t yours and since it is most probably going to get modified in the future a lot, it will obfuscate your entire commit history as well. And what if the dependencies of your libraries change over time, rendering some of the files no longer needed? Obviously, it would be better if everything external stayed… well… external.

The solution

So the big question is: how do we get out of all this mess? Quite simply - by doing what everyone else had been doing for a long time, that is introducing a package manager. It is a tool which uses the globally available repository of libraries and knows how to download and update them along with their dependencies.

In the front-end world the package manager and the global repository are called NPM. It is “similar” to NuGet from .NET, Maven from Java, PIP from Python, Composer from PHP, etc.

This tool reads or modifies package.json - a file which should be located in your project’s folder - and describes your dependencies, downloading all of the required stuff to the local folder: node_modules. That’s it really. You can import the files from this folder and call it a day.

However, that’s not all that can be done to make your life even better. Read on :)

2. The module bundler

The problem

Pure JavaScript does not have modules or even namespaces. You can isolate private stuff in a single file using IIFEs, but if you want to share something across multiple files - you have to pollute the global namespace. What if another thing in your application wants to add something else to that very namespace with the same name? Whichever JS file is included later on, it will simply override what was already in the globals, leading to an undefined behavior.

The other problem, which is present especially in libraries, is that not all shared stuff may be used by the end application. For example, you have a library with 100 components and the user needs only 1 of them. You may split your library into multiple small files and the user may include only them, but what if those files use 12 out of 125 functions from your library’s internal dependency? What about the next levels, even deeper in the hierarchy? Obviously, it’s impossible to manage manually.

Also, in libraries with multiple dependencies, how do we know the correct order in which the user should load them? Do we really want to tell the user: “Hey, here’s our 144 files, this is the order”? I’m gonna say nope.

The solution

All of these problems are solved by module bundlers. There are a few of them, but the most important one is definitely Webpack. So, what is it exactly?

This is a both run-time and build-time tool which:

Gives you syntax to define a module and its public API

See export below:

function privateFunction() { /*...*/ }
export function publicFunction() { /*...*/ }

This function can now be dynamically imported from your file. Also, nothing from this file is added to the global namespace!

Gives you syntax to dynamically import stuff from other modules:

See imports below (assume both myModule1 and myModule2 contain example code from above):

import EntireFile from "./myModule1.js";
import { publicFunction } from "./myModule2.js";

EntireFile.publicFunction();
publicFunction();

Those are in JS! No more manual script loading in HTML.

Keeps track of what exactly you need from the modules

Module bundler looks at each import recursively (so also at things deep in libraries) and figures out which files you need. When you build your application using Webpack, unused code won’t go into the finished bundle.

Ensures that every imported module is loaded once and once only

The final bundle will contain only one instance of your module, even if two files import different things from that module.

Gives you tools to bundle your final application’s JS to a small number of files (often just one)

With a single command it can get everything you need, throw out what you don’t need, correctly wrap every file so that it doesn’t pollute the global namespace and concatenate it to a single JS file which you add to your HTML.

Summing up, module bundler solves most of your problems, but that is still not the end of its magic. Read on :)

3. Transpilers and loaders

The problem

JavaScript is currently the only scripting language which is native to every browser. However, many people new to the front-end world do not realize that JavaScript, like any other programming language, has many standards and browsers lag multiple versions behind the current one.

Moreover, there are other languages in which you can write web apps, but they all have to transform down to regular JS. How do we achieve that?

Finally, thanks to Webpack, we solved the problem of managing dependencies between JS files, but what about CSS and other files, like images or fonts?

The solution

Since module bundler is the thing which interprets your imports/exports and in build-time loads your files into the bundle, it can simultaneously perform some transformations to your code. Actually, it always does that by default because import and export are not part of ES5 JavaScript standard which is supported by browsers and it has to replace it with some runtime function calls.

So, what kind of transformations are we talking about? First of all, transpilation - a process of “translating” code of one language to another. For example, we can use newer standards of JavaScript (e.g. ES7) or even completely different languages (TypeScript, CoffeeScript, ELM) and transpile them down to ES5 for browsers. Cool, right? Moreover, you can even use those different languages simultaneously, which is useful when you’re migrating legacy codebases.

We set up transformations for different file extensions in Webpack configuration using loaders which are simply 3rd party functions dedicated to Webpack. In the config it looks more or less like this:

module: {
    rules: [
        {
            test: /\.jsx?$/, 
            exclude: /node_modules/, 
            use: [{ loader: 'babel-loader' }]
            //...
        }
    ]
}

So for all .jsx files everywhere except node_modules, the folder will be transformed with babel-loader. Babel is a transpiler of newer standards of JS. Of course the transformations will be run only for files which are imported somewhere in your app.

What about that “CSS and other files” problem? Well, there are loaders for these too:

    {
        test: /\.(jpe?g|png|gif)$/i, 
        use: [{
            loader: 'file-loader',

            options: {
                name: '[name].[ext]'
            }
        }]
    },

Remember that Webpack uses loaders only for imported files? So, how do we import non-JS files? It turns out, that like basically everything else, you can import those from your JS:

import pathToLogo from "./pathToLogo.png";

And the file-loader will:

  • grab the file from the imported folder and put it into the bundle’s folder
  • assign the resulting path to the image to the pathToLogo variable.

Which solves all our problems mentioned before.

4. Minification and optimization

The problem

JavaScript goes to the browser in a raw text-based format, but a large portion of scripts’ contents doesn’t have any meaning to the browser at all. For example, all indentations, tabs, empty lines and even spaces are completely unnecessary. They help only you - the developer. The same goes for variables or function names - as long as they’re unique in their scope, they can be as short as a single character. So, it would be great if some automagic optimized that for us and through this allowed us to save bandwidth, wouldn’t it?

Also, the other common problem is the number of files. The more separate files you have, the more requests the browser will have to make, significantly increasing load times of your application. However, a single file sometimes may not be optimal as well, because, for instance, different routes in the app use different modules (e.g. “public” part and the back office for admins). Also, if some of the files don’t change often (like external libraries), browsers of your users could cache them.

The solution

Like before, the problem is handled through the module bundler. You can simply configure how the code should be minified in the config. You can also set up sophisticated rules on how it should be split. For example, you can define that everything from node_modules (so pretty much all external libraries) will go to a separate file, which, as I mentioned before, will be cached by browsers because that will not change often.

5. Summary

Every concept described in this article applies to every modern front-end tech like Angular 2+, React, Vue, Ember, etc. Even if some of these frameworks have their own CLI, they all use these things under the hood. You also can now take advantage of your newly gained knowledge and apply some of these tools to a legacy codebase as it will work even with Angular 1.x.

Hopefully, this article helped you better understand how the modern front-end works and that there are reasons why it has to be that way.

Let me know your thoughts in the discussion below, and if you know any other lifehacks to make developers’ work easier, you can share them too. See you next time! :)