loading...

Frontend development stack: Introducing complexity to reduce complexity

rapasoft profile image Pavol Rajzak ・9 min read

As a Java developer, I am not a big fan of JavaScript as a language. In it's essence it is really primitive and simplistic. While this is fine for simpler scripting (the original intention of JavaScript) it is not usable for developing a large application. It has a lot of inconsistencies, tricky parts and in general it will make you pull your hair sometimes.

But, as in every language there are tools, libraries and frameworks that will ease your pain. I am really surprised how mature is the ecosystem around JavaScript and that you don't have to understand most of it. There's a handlebar for almost everything and - if we take it to the extreme - you might not know you are developing a JavaScript application at all. You can build a full-blown single page application, without even writing a single line of JavaScript - using TypeScript, for instance.

"How's that?" you might ask, and the answer is in the hidden complexity those "handlebars" bring in. There are plenty of tasks that are done in the background, from writing source code up to creating a "production bundle" that you can deploy.

In this article you'll find answers to these questions:

  • I just want to build web! Should I look into some more complex technologies other than HTML, CSS and plain JavaScript?
  • There are so many tools and libraries, which ones should I pick?
  • Does it have to be so hard to set everything up?

...and some other, hopefully.

Project

Let's work with some specific, running example so that the concepts are more clear. We will develop a frontend application for a blogging platform. Simple UI that will display posts from various authors and categories.

Blog!

The Naive approach

You all remember the good old days, when you wrote HTML, some CSS and then to add some spice into it you've added some JavaScript effects? Well, this is where we are going to start. But the world has changed a bit since then, and now everything is written in JavaScript. It's mostly because you can define reusable parts of code and then just re-use them. For instance, this will be a title (title.js):

const title = `<h1>πŸ‘¨β€πŸ‘¨β€πŸ‘¦β€πŸ‘¦ Friends blog 🌟</h1>`;

and this is how we are going to use it (main.js):

const main = `
    <div class="main container">
        <div class="title">${title}</div>
    </div>
`;

Now in order to use this, we need to put it in some main file, for instance index.js:

document.getElementById("app").innerHTML = main;

And use it in index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Blogging platform!</title>

    <script type="text/javascript" src="components/title.js"></script>
    <script type="text/javascript" src="components/main.js"></script>
</head>
<body>
<div id="app">
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>

We have three .js files, that need to be imported in specific order, because the constants are defined in global scope. Well this is a problem when we'll have additional components defined in other .js files. First, you need to manually add them into index.html and then you need to maintain specific order. What if you define variable with same name? It will overwrite the previous one. Also, we have jumped in it too quickly and are using features from ES6 ("version" of JavaScript) like string templates which isn't supported by older browsers. So, no IE9 support :(.

And we have nothing but title, yet. In a normal world, you want to add more components, styles, some layouting framework (for instance Bootstrap), add specific logic, helper classes and... bundle it all together.

Bundling it all together

Why is this needed? If you have significant amount of .js files, you will need to send a HTTP request for each of them (like in our example title.js, main.js and index.js) which is by nature always slower as if you would put it into one file (bundle.js). The orchestration around one HTTP request/response roundtrip is just too expensive.

We can automate this task by delegating it to a tool! Not too much time ago, there were 2 main competitors and that's Grunt and Gulp. You can define a specific list of tasks that should be executed in specific sequence in order to build a project, for instance:

  • clean build directory from previous runs
  • iterate over list of source .js files
    • and copy them into one bundle.js
    • and compress it's contents
  • iterate over list of source .css files
    • and copy them into one bundle.css
    • and compress it's contents
  • generate and copy index.html that will include link to built bundle.js and bundle.css files

Gulp and Grunt are somewhat similar in what they do, but they use different approach. While with Grunt it's more declarative, in Gulp you are defining procedures that are piped together. Take this config (called gulpfile.js) for instance:

... (gulp plugin definition) ...

gulp.task('buildJs', function () {
    gulp.src([
        './src/components/title.js',
        './src/components/main.js',
        './src/index.js'
    ])
        .pipe(concat('build.js'))
        .pipe(uglify())
        .pipe(gulp.dest('target'))
});

gulp.task('buildCss', function () {
    gulp.src('./src/styles/*.css') // path to your file
        .pipe(minifyCss())
        .pipe(concat('build.css'))
        .pipe(gulp.dest('target'));
});

gulp.task('cleanTarget', function () {
    return gulp.src('target')
        .pipe(clean({force: true}))
});

gulp.task('copyIndexHtml', function () {
    return gulp.src('src/index.html')
        .pipe(gulp.dest('target'));
});

gulp.task('default', gulpSequence('cleanTarget', 'buildJs', 'buildCss', 'copyIndexHtml'));

It is pretty much clear what it does, running gulp in command line will run the default task which combines everything together.

How Gulp works? It's a node.js application, so in order to use it properly you have to have node.js installed. You need to transform your project to node.js project as well (via package.json configuration) and install all the plugins which gulp will used (via npm - node package manager). So we need to install not one, but two tools to automate tasks.

Of course, both gulp and grunt have many plugins that will help you achieve almost everything there's needed. But they are mostly 3rd party plugins that are of various quality and difficulty in terms of setup.

In our setup, we haven't yet tackled the problem with order of .js files (which still needs to be explicit) and also haven't mentioned another problem which is a dependency management.

Some kind of dependency management

There's Bootstrap, jQuery, moment.js, lodash and more cool libraries and frameworks you would like to use in your project. In ideal world what you would do is to download or point to a bootstrap.css and link it in index.html like this:

<head>
    ...
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" />
    ...
</head>

And you're done! But nothing is ideal in frontend world nowadays and the problems will appear as soon as you start adding files. You will need to solve problems like:

  • Is library A compatible with specific version of library B?
  • Why adding jQuery into project disabled all my fancy pop-up menus?
  • Why do we have 4 libraries for creating fancy pop-up menus anyway?
  • I have linked an external library that is not available at this link anymore :(

You will soon realize, you need at least some kind of dependency management. Something that will find the correct version of library you need and link it into project. Not so long time ago these problems were solved by using Bower. Bower is a package manager, which will use it's configuration file (bower.json) to define, download and later install dependencies for your use.

{
  "name": "blogging-platform-frontend",
  "description": "The blog!",
  "main": "index.js",
  "authors": ["Pavol Rajzak"],
  "license": "ISC",
  "keywords": ["javascript", "gulp"],
  "homepage": "",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components"
  ],
  "dependencies": {
    "bootstrap": "^4.0.0"
  }
}

As for Gulp/Grunt you need to install it via npm, then you can run bower command which will create a bower_components directory with specific libraries. You can then use gulp/grunt plugins to wire the dependencies into your project. Sweet! It seems so, but not everything is ideal. Bower is now a de-facto discontinued project, because of several design problems it contained. You should not use it for new development.

Another point of view could be like this: for building the project and maintaining dependencies we need these tools:

  • node.js - runner of .js scripts
  • npm - package manager
  • grunt/gulp - task runner
  • bower - package manager

Well, I tried to make it obvious, that there's some duplication we could avoid. Why do we need 2 task runners and 2 separate dependency management tools? The answer is - we don't.

Introducing webpack

So, we have got rid of gulp/grunt and bower and replaced it with pure npm/node.js solution for frontend part. What can we do with it? Well, not much. The main task - bundling it all together - still needs to be replaced by some more advanced, dedicated tool. An application bundler.

Here's where we introduce Webpack into our story. It's a bundler capable of crawling through your application, finding sources and creating application bundle based on defined dependencies. The main and biggest difference here is that it takes advantage of modular approach, which can be implemented in several ways. The ES6 way is using import/export statements.

Let's look at our changed example from naive approach:

... (title.js) ...
export const title = `<h1>πŸ‘¨β€πŸ‘¨β€πŸ‘¦β€πŸ‘¦ Friends blog 🌟</h1>`;
... (main.js) ...
import {title} from "./title";

export const main = `
    <div class="main container">
        <div class="title">${title}</div>
    </div>
`;
... (index.js) ...
import {main} from "./src/components/main";

document.getElementById("app").innerHTML = main;

Now this is a huge improvement and it solves the problem of having all component definition in global namespace. Now that you can define your dependencies explicitly, you don't have to worry about any namespace clashes, since webpack will bundle it all in safe way.

Webpack can be configured (webpack.config.js) like this:

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['babel-preset-env', 'react'],
                        plugins: [
                            require('babel-plugin-transform-object-rest-spread')
                        ]
                    }
                }
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
        ]
    },
    plugins: [
        new WebpackCleanupPlugin('target', {verbose: true}),
        new CopyWebpackPlugin([{from: 'index.html', to: 'index.html'}])
    ]
};

This is doing very similar thing as gulp/grunt was doing, but instead of tasks/plugins there are loaders used for processing files and plugins for additional tasks.

Most importantly, you define and trigger the build using npm / node.js , so we have now only three very specific tools that will do the job. Here's for instance the package.json configuration:

{
  "name": "blogging-platform-frontend",
  "version": "1.0.0",
  "description": "The simplest UI you can imagine",
  "main": "index.js",
  "scripts": {
    "start": "webpack --watch index.js target/build.js",
    "build": "webpack index.js target/build.js",
  },
  "keywords": [
    "javascript"
  ],
  "author": "Pavol Rajzak",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-jest": "^22.2.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-add-react-displayname": "0.0.5",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "copy-webpack-plugin": "^4.5.1",
    "css-loader": "^0.28.9",
    "style-loader": "^0.20.1",
    "url-loader": "^0.6.2",
    "webpack": "^3.10.0",
    "webpack-cleanup-plugin": "^0.5.1"
  }
}

You might've spotted one addition in the configuration - we are using babel. It is a transpiler (language-to-language compiler) which will compile our ES6 code to a more supported ES5 version of JavaScript. We can now support our application also on IE9, woohoo! <IronyOff />

Also, webpack is able to natively work in watch mode, which will automatically pick-up changes and re-build the bundle. It also supports multiple configurations (development / production), it has numerous plugins and it get's better each version! As opposed to gulp/grunt the configuration process is definitely more deterministic.

It's all Greek to me!

The lifespan of these tools is very short, I am not talking about decades of evolution, but a few years at most. How to track what's going on? If you are not directly employed/involved in frontend development it is very hard. If now webpack is the cool thing to use, in some months it can be prepack. Or npm is now being deprecated in favor of yarn.

Luckily, there are tools/libraries/frameworks even for this! If you want to build React applications, you can start with create-react-app, for Angular you have angular-cli. Everything you might need is already pre-configured for you. You still need to understand the notion of build and dependency management, but at least the burden is a lighter this way.

The bottom line

I personally think, that being a frontend developer means you need to keep track of changes in JS ecosystem even more frequently. While on backend, you can basically jump back in even after few years without studying new trends (which you shouldn't), it is nearly impossible in frontend. If you do, however, lose track of what is the "standard" frontend stack, you can just use boilerplate or generators directly targeting specific frameworks.

Posted on May 14 '18 by:

rapasoft profile

Pavol Rajzak

@rapasoft

Software developer. Mostly Java and JavaScript. Kotlin enthusiast.

Discussion

markdown guide