Webpack configuration for Multi Page Application. All you need to start your project

Last Updated On

Table of contents

Foreword

If you're developer/designer who wants to focus only on building the product, use the latest and greatest technology stack and don't spend much time on configuration this article is for you. This is about webpack - a big and very powerful monster that can take a lot of your time away if you're not familiar with its concepts. To be honest, it's very difficult to grasp at first, since webpack itself is just a bundler and more often is used in conjunction with other plugins inside nodejs script. However, the goal of this article is not to overwhelm you but provide a production-ready configuration that you can "copy-paste", change few lines of code with your flavor and build the next awesome thing.

Before we move on, I need to mention that this webpack setup is suitable only for specific types of projects and technologies. In other words, if you:

  1. Build MPA (multi page application, powered by CMS like WordPress, static site generators like Jekyll, Hexo or any other custom "back-end first" engine)
  2. Use ES6 and later versions of javascript with an ability to transpile down to ES5
  3. Use SCSS
  4. Want "live-reload" your browser on any JS or CSS/SCSS change compatible with your Back-end solution
  5. Need development/production environment set up (where in development all sourcemaps are available and the code isn't minified as in production it's all obfuscated, minified and javascript/css file names have "hash" in their names to bust the client's browser cache)

This config will definitely help you. However, if you:

  1. Build SPA (single page application)
  2. Need to use TypeScript, JSX or something different from regular Javascript
  3. Need to bundle images, htmls
  4. Need to have html-hot-reload

This config won't help you (although you can get some ideas from it).

Folder structure

There're some things that are very similar for each MPA: the folder structure for the web part. It might be hidden very deeply behind the back-end code or stay very close to the project root, but basically, it looks like this:

|-- src
    |-- js
        whatever.js
        main.js
    |-- scss
        whatever.scss
        style.scss

I completely understand, that example above is an extremely simplified version of real production code and location of javascript/scss files might be different, but the idea is there must be an entry point for each of them somewhere: main.js (or index.js or entry.js, etc.) and style.scss (or main.scss, etc.). That what will be specified in the webpack configuration.

The output folder for compiled resources will be called dist (you can adjust it in the way you like). Taking that into account, the final folder structure will turn into this:

|-- dist // generated javascript and css.
|-- src // source code for ES6 and SCSS
    |-- js
        whatever.js
        main.js
    |-- scss
        whatever.scss
        style.scss

Cachebusting

As I mentioned before, one of the "must to have" features for production is a cachebusting (if you're unfamiliar with the concept this article might help). Here's a logic how it's going to work: Every time when webpack will go through the build process it will write a json file with random string(hash) in there. There is an internal mechanism inside the webpack that will take care of a hash generation. The end file will have a name cachebuster.json (you can pick any name you prefer) and contain the following info:

{
  "resourcesHash":"3b94fe477630f0672c42"
}

Later on, back-end engine will parse this json file and insert a hash (in this case 3b94fe477630f0672c42) into css/js paths.

One last thing about javascript

In this example, I used babel to compile new javascript into ES5. In order to successfully use the configuration provided in this article you need to create .babelrc file (at the same level as webpack configuration file) with the following content:

{
  "presets": ["es2015"]
}

Config file

var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var LiveReloadPlugin = require('webpack-livereload-plugin');

var inProduction = process.env.NODE_ENV === 'production';
var CASHBUSTER_FILE = './cachebuster.json';

// Function that extracts unique hash generated by webpack
// and writes it into a file. It also performs a check
// whether cachebuster.json existed before and if so,
// replaces the hash value. Otherwise writes a new file.
function generateResourcesHash(hash) {
  if (!fs.existsSync(CASHBUSTER_FILE)) {
    fs.openSync(CASHBUSTER_FILE, 'w');
    fs.writeFileSync(
      path.join(__dirname, "", CASHBUSTER_FILE),
      "{}"
    );
  }
  
  var cacheBuster = JSON.parse(fs.readFileSync(path.join(__dirname, CASHBUSTER_FILE), "utf8"));
  
  cacheBuster.resourcesHash = hash
  
  fs.writeFileSync(
    path.join(__dirname, "", CASHBUSTER_FILE),
    JSON.stringify(cacheBuster)
  );
}

// Entry point in the webpack configuration
module.exports = {
  devtool: inProduction ? '' : 'source-map',
  entry: {
    main: [
      './src/js/main.js', // Javascript entry point
      './src/scss/style.scss' // SCSS entry point
    ]
  },
  output: {
    path: path.resolve(__dirname, './dist'), // output folder is named as dist
    filename: inProduction ? 'main.bundle.[hash].js' : 'main.bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      },
      {
        test: /\.s[ac]ss$/,
        use: ExtractTextPlugin.extract({
          use: [{
            loader: 'css-loader',
            options: {
              sourceMap: inProduction ? false : true,
              minimize: inProduction ? true : false
            }
          }, {
            loader: 'sass-loader',
            options: {
              sourceMap: inProduction ? false : true
            }
          }],
          publicPath: '/dist'
        })
      },
    ]
  },
  plugins: []
}

// Environment specific config for plugins and
// hash value update.
if (inProduction) {
  module.exports.plugins.push(
    new webpack.optimize.UglifyJsPlugin(),
    function() {
      this.plugin("done", function(statsData) {
        var stats = statsData.toJson();
        if (!stats.errors.length) {
          generateResourcesHash(stats.hash);
        }
      });
    },
    new ExtractTextPlugin({
      filename: function (getPath) {
        var hash = getPath('[hash]');
        generateResourcesHash(hash);
        return getPath('style.[hash].css');
      }
    })
  )
} else {
  module.exports.plugins.push(
    new ExtractTextPlugin('style.css'),
    new LiveReloadPlugin(),
  )
}

How to use

  • Run webpack for development mode (one-time build).
  • Run webpack --watch for development watch mode (will watch auto-recompile on every scss/js change).
  • Run NODE_ENV=production webpack for production build.
  • To insert hash in production, add this logic to your project (this is pseudo code to give you an idea):

    if ('production') {
        <link rel="stylesheet" type="text/css" href="/dist/style.{{resourcesHash}}.css">
    } else {
        <link rel="stylesheet" type="text/css" href="/dist/style.css">
    }
  • Install chrome plugin live-reload to support live-reload on each js/css change

Conclusion

Dealing with environment set up is not easy, especially when the project has custom requirements. However, sometimes it's even hard to start with basics when dealing with webpack. This article won't solve configuration problem for you codebase, but it might help you with the general idea which way to go. You can also find source code on github with all dependencies listed in package.json and start using it. Happy coding.