Compile the Front-End: From Gulp to Webpack

Published Jan 19, 2020

Webpack vs. Gulp

This blog explains the differences of Gulp and Webpack from the perspective of moving from Gulp to Webpack.

In general, Gulp is an automation tool. You can use it to encode how to compile front-end code step-by-step. On the other hand, Webpack is a compilation tool. You can define a set of rules that tells Webpack what you want.

Gulp

Gulp is more of an automation tool than a compiling tool. It makes no effort to understand your code. You need to tell Gulp how to put all your front-end code together explicitly.

Here is an example: we have a module A.js, which depends on functions in lib.js:

+---+     +-----+
| A | --> | lib |
+---+     +-----+

If we want to put A.js and lib.js into one index.js file for production, we need to specify their sequence in Gulp:

var gulp = require('gulp');
var concat = require('gulp-concat');

gulp.task('js', function() {
  return gulp.src(['lib.js', 'A.js'])
    .pipe(concat('index.js'))
    .pipe(gulp.dest('dist/index.js'));
});

The js task concatenates lib.js and A.js, then saves the resulting index.js file to the dist/ folder.

Note that if we write gulp.src(['A.js', 'lib.js']), the resulting code in index.js might fail. This is because the code in A.js might use variables and functions defined in lib.js, but in index.js, these variables and functions are not yet defined when we execute the code that was originally in A.js.

Simple as it is, Gulp saved us a great amount of time at deployment. It was also pleasant to see all the tasks done by just typing those four letters “gulp” and hitting enter.

Webpack

Webpack, on the other hand, analyzes the target code before compiling it.

This is a completely different concept compared to Gulp. When I used Webpack for the first time, I could not find a way to tell Webpack to concat A.js and lib.js and put the resulting file in a given folder. Then I learned that I needed a different mindset: in Webpack, we do not have to specify what to do step-by-step. If module A.jsdepends on lib.js, Webpack will know to load lib.js before calling A.js by analyzing the import and export clauses in each module. This means that we have less control over the exact steps executed at compile time. On the other hand, we do not have to worry about how to put the modules together in one file. In other words, Webpack is more declarative than imperative. This is a great trade-off because in many cases, all the control we need is to specify the file sequence based on dependency. Webpack frees us from maintaining such dependency explicitly, and we can happily give up control without many complaints.

Consider again the previous example: A.js depends on lib.js. Assume the code is as follows:

// A.js
import lib from './lib.js';

lib.greetFrom('Smarking');
// lib.js
export function greetFrom(host) {
  console.log(`${host} says Hello!`);
}

Webpack config file is as follows:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  entry: './A.js',
  output: { filename: './dist/index.js' },
};

With this simple example, Webpack generates the ./dist/index.js file as follows:

(function(modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function _webpackrequire__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, _webpackrequire__);
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
  }
  // expose the modules object (_webpackmodules__)
  _webpackrequire__.m = modules;
  // expose the module cache
  _webpackrequire__.c = installedModules;
  // define getter function for harmony exports
  _webpackrequire__.d = function(exports, name, getter) {
    if(!_webpackrequire__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter
      });
    }
  };
  // getDefaultExport function for compatibility with non-harmony modules
  _webpackrequire__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    _webpackrequire__.d(getter, 'a', getter);
    return getter;
  };
  // Object.prototype.hasOwnProperty.call
  _webpackrequire__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  // _webpackpublic_path__
  _webpackrequire__.p = "";
  // Load entry module and return exports
  return _webpackrequire__(_webpackrequire__.s = 0);
})
(
  [
    /* 0 */
    (function(module, _webpackexports__, _webpackrequire__) {
      "use strict";
      Object.defineProperty(_webpackexports__, "__esModule", { value: true });
      var _WEBPACKIMPORTED_MODULE_0__lib_js__ = _webpackrequire__(1);

      _WEBPACKIMPORTED_MODULE_0__lib_js__["a" /* default */].greetFrom('Smarking');
    }),

    /* 1 */
    (function(module, _webpackexports__, _webpackrequire__) {
      "use strict";
      _webpackexports__["a"] = greetFrom;
      function greetFrom(host) {
        console.log(`${host} says Hello!`);
      }
    })
  ]
);

The generated code demos how Webpack put all the modules together. I won’t explain the webpackBootstrap code in detail here. You can dive into the code and try to understand it if you’re interested. Note that Module 0 is our A.js, and Module 1 is our lib.js. Webpack transformed our code based on the export and import relationships. When the bootstrap code processes Module 0 and finds out that Module 0 requires Module 1, it loads Module 1 before continuing to process Module 0. Neat!

From this example, we can see that within Webpack, we do not need to write down how to compile our code, but to tell it what we want (entry and output), then let Webpack do the heavy lifting for us. This is the main difference from Gulp, in which we need to write down how to compile the code, step-by-step.

Moving from Gulp to Webpack

Although letting Webpack figure out module dependency is cool and convenient, we still need several automation steps to move from Gulp to Webpack:

  1. Compile our ES6 code to ES5

    This is where the Webpack “module” configuration comes into play. We can use the “module” configuration to define how to transform each module that Webpack encounters at compile time.

    We can specify transformation rules for our js and jsx files as follows:

    // webpack.config.js
    module.exports = {
      entry: ...,
      output: ...,
    
      module: {
        rules: [{
          test: /\.jsx?$/,
          use: 'babel-loader'
        }]
      },
    
      ...
    }

    Using this configuration, Webpack will transform our js and jsx files, which match the /\.jsx?$/ regular expression, using babel-loader, which leverages Babel to convert our ES6 code into ES5. (In order to use the babel-loader, you need to install it: npm install babel-loader --save-dev.)

    Similarly, we can add rules to convert each Stylus/LESS/SASS file into css.

  2. Include js and css files in index.html

    This is where the Webpack “plugin” configuration comes into play. Plugins are responsible for tasks that are not specific to individual modules. For example, you can set environment variables, perform post-processing tasks such as minifying js and css files, move files around, or generate html files that refer to js and css files, like what we are about to do here.

    We use the HtmlWebpackPlugin and its extension HtmlWebpackIncludeAssetsPlugin to include all js and css files in our index.html:

    // webpack.config.js
    module.exports = {
      entry: ...,
      output: ...,
      module: ...,
    
      plugins: [
        new HtmlWebpackPlugin({
          template: 'index.ejs',
          filename: 'dist/index.html'
        }),
    
        new HtmlWebpackIncludeAssetsPlugin({
          assets: glob.sync('*.min.+(js|css)', { cwd: './dist' }),
        })
      ],
    
      ...
    }

    If the template file index.ejs looks like this:

    <!DOCTYPE html5>
    <html>
      <head>
        <title>Smarking Analytics Dashboard</title>
      </head>
      <body>
        <div id="root"></div>
      </body>
    </html>

    The HtmlWebpackPlugin plugin will automatically refer to the generated js and css files in the dist/index.html file:

    <!DOCTYPE html5>
    <html>
      <head>
        <title>Smarking Analytics Dashboard</title>
        <link href="dist/index.css" rel="stylesheet"/>
      </head>
      <body>
        <div id="root"></div>
        <script type="text/javascript" src="dist/index.js"></script>
      </body>
    </html>

    We often need to include some extra third-party libraries and style files. We can use the HtmlWebpackIncludeAssetsPlugin plugin to achieve that. Our configuration tells the plugin to add references to all files ending with .min.js and .min.css under the dist/ folder in the dist/index.html file:

    <!DOCTYPE html5>
    <html>
      <head>
        <title>Smarking Analytics Dashboard</title>
        <link href="dist/index.css" rel="stylesheet"/>
        <link href="dist/thirPartyLibrary.min.css" rel="stylesheet"/>
      </head>
      <body>
        <div id="root"></div>
        <script type="text/javascript" src="dist/index.js"></script>
        <script type="text/javascript" src="dist/thirPartyLibrary.min.js"></script>
      </body>
    </html>

Epilogue

That’s it, a high-level comparison between the concepts of Gulp and Webpack. I left out many details such as how to include style files and add configurations to extract and convert them into a single css file. There are plenty of examples online that can help with that once we have a good understanding of the concept of Webpack.

Our journey from Gulp to Webpack was bumpy at best, but we are happy with the results. The moving procedure described above was just a first step though. Our tech stack includes Docker and Nginx. They encapsulate our front-end code and its communication with our backend in a universal virtual environment. In order to configure a good front-end development environment using features such as hot module reload with short reloading time, we needed to do more than just moving automation scripts. I’ll write about the integration of Webpack with Docker and Nginx in future articles.

  • front-end
  • gulp
  • webpack