Pixies, part 1: Just Enough Webpack


What is this Webpack thing, anyway?

Your typical front-end project is made of a bunch of html, css and javascript files. Also, your javascript code undoubtedly calls into various third party libraries to accomplish its purpose, these, in turn, have their own dependencies and so on. You provide necessary <script> links in your html files, carefully making sure that these are included in the right order, so the dependencies are satisfied. Css files also have to be included. Then comes the deployment, you need somehow deploy all these dependencies together with your main html file, making sure the links are not broken. CDNs help in part with that but, I don’t know about you, but for me it always feels uncomfortable that my live app depends on some third party code deployed somewhere on which I have very little control.

Wouldn’t be nice if all my dependencies are packed into a single, compact file, which is easy to track and deploy. That is where Webpack comes into the picture. It pretty much offers a way to generate such file, or bundle as they call it in the community. Granted, you could have achieve the similar effect with the previous generation of the javascript tools, like Gulp or Grunt but, in opinion of many, the Webpack brings this workflow to a new level. So let’s build our front-end app step by step, starting from the empty directory.

Humble beginnings

Go to your pixies/frontend directory and run there:

npm init -y

This will create a package.json file which tracks the 3rd party dependencies and does some other meta stuff, sort of pom.xml if you are doing Java Maven project or .csproj file if .Net is your thing. The -y flag indicates that you are going to automatically accept the defaults as answers for many questions the npm init would ask you. You can modify these later

Now, let’s install the webpack:

npm install webpack webpack-cli webpack-dev-server --save-dev
  • webpack is webpack itself
  • webpack-cli are the command line scripts to interact with the webpack.
  • webpack-dev-server is the server which allows us to run our webpack app during the development
  • --save-dev indicates to save these dependencies in the package.json file so we can reproduce the setup easily

Indeed, if we open the package.json file, it contains the section (actual versions may vary a little)

  "devDependencies": {
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3"
  }

Also it contains the section

"dependencies": {},

There is a subtle difference between these two, for the purposes of this tutorial we’ll modify only devDependencies While in the file package.json, notice the section

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },

Although the testing is important, we are not going to do any automated testing in this tutorial, so replace this section with:

"scripts": {
    "start": "webpack-dev-server --open --mode development",
    "build": "webpack --mode production"
  },

With those, we will have ability to run to serve our project during the development and also produce the final build for development.

Now we are almost ready to produce the first build. But, before doing that, we need to have the entry point file so webpack knows where our app starts. If you check the package.json, it expects the entry point file to be called index.js. So let’s create it, empty initially:

mkdir src
touch src/index.js

Finally, we can produce the build:

npm run build

After few seconds webpack will complete the build and produce the directory dist If we check that directory we’ll see that the result is rather underwhelming. This contains only one file main.js and the content of this file is rather obfuscated javascript. Webpack tries to minify the produced javascript by stripping all necessary parts like whitespace and comments, therefore the result looks quite undecipherable. But don’t worry, The browser will be able to execute that just fine. As our actual “code” is just the empty file for now, this file contains mostly plumbing routines injected by the Webpack.

Where is my index.html?

Ok, we got our javascript bundle file (which does nothing at the moment, but bear with me) We could create an index.html manually and inject a <script> script point to our bundle. but ideally we would like to avoid manual maintenance as much as possible and let Webpack handle that. Well, the Webpack itself is not able to do that, but there is Webpack plugin which will handle maintenance of index.html for us. Let’s install it

npm install html-webpack-plugin --save-dev

The Webpack uses a javascript configuration file to configure plugins. Let’s create this file

touch webpack.config.js

We’ll add more things to it but for now, we just start with the html plugin. Make it look like this:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  plugins: [new HtmlWebpackPlugin()]
};

Build again,

npm run build

You will see that the output directory dist contains, in addition to our main.js, also index.html which contains the properly configured <script> tag.

Instead of rebuilding the project every time we introduce some minor change, we are going to start Webpack’s dev web server by running

npm start

After few seconds, the browser window should pop up showing a blank web page qith the title “Webpack App”. We can look at the source of this page and it indeed shows the link to our main.js bundle which, granted, does nothing. So let’s change that. A very useful feature of the Webpack’s dev server that it watches for the changes in the files and restarts itself once it happens. So let’s open our src/index.js and put some silly javascript code there, like

console.log('Oh, hai!');

Once we save this file, the dev server restarts automatically and we can check in the javascript console that our silly message indeed has been printed.

While we are here, let’s modify the our web page’s title from the generic “Webpack App” to something more interesting. For now, the simplest way to do it is to add a simple configuration to our Html webpack plugin:

module.exports = {
  plugins: [new HtmlWebpackPlugin({ title: 'Pixies' })]
};

On some occasions, especially when the config has been changed, dev server may fail to pick up the change immediately. If it happens, simply restart the dev server.

Let’s add some styling

For the purpose of this tutorial, we are not going to write any sophisticated css. Instead we are going to use the styles provided with the popular Bootstrap library. First, we install the bootstrap:

npm install bootstrap jquery popper.js --save-dev

jquery and popper.js enable extra bootstrap functionality which we may need later. One of the coolest Webpack features is that it allows treat non-javascript assets as if there were javascript. To do that, Webpack utilizes loaders. We will need two loaders, css-loader and style-loader, so lets install them:

npm install css-loader style-loader --save-dev

Now, we need to tell the Webpack that, if it sees something that looks like a css file, use these two loaders. This is added by introducing a rule which specifies the regular expressions to match against the file name and which loaders to apply if a match is found:

{
    test: /\.css$/,
    use: ['style-loader', 'css-loader']
}

That is, if the Webpack sees the file that ends with ‘.css’, it is going to apply css-loader first and style-loader next. As you see, the ‘later’ loader goes earlier in the list. Your entire webpack.config.js file, should look like this now:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },

  plugins: [new HtmlWebpackPlugin({ title: 'Pixies' })]
};

Now, let’s add the bootstrap to our html file. Ordinarily, one would insert the appropriate style and script links, but with the Webpack it is simpler. In your src/index.js file remove our test console log output and replace it with:

import 'bootstrap/dist/css/bootstrap.min.css';

That is pretty much all you have to do in order to import the bootstrap css into your project. Notice that we are (sort of) pretending that the css file is a javascript file. Believe it or not, once we rebuild our bundle, the whole bootstrap css will be neatly packed inside the main.js file. Granted, our webpage is still empty. We will fix this soon. But first, let’s do some tidying up.

Taking care of loose ends

At the moment, Webpack builds our project into the dist directory and the bundle name is main.js. As a matter of taste, I would prefer the destination directory to be called build and the bundle called main.bundle.js Of course, you can substitute this to anything you want. Do this in your webpack.config.js:

const path = require("path"); // new
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: "./src/index.js", // new
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "build")
  }, // new ==================
  module: { // ...
    rules: [

After you build again, you will notice that the name of the output directory and bundle file have changed. Notice that you don’t need to do any changes to reflect these renames in the generated index.htm Html Web Plugin will take care of that for you. Now, you can delete your old output directory

rm -rf dist

While we at it, wouldn’t it be nice that every time we perform a new build, the old files are cleaned so we are sure that no leftovers left. Of course, we can delete the output directory manually every before every build, but Webpack has a plugin which will automate that for us. Let’s install it:

npm install clean-webpack-plugin --save-dev

Enable it in your webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); // new
...
plugins: [
    new HtmlWebpackPlugin({ title: 'Pixies' }),
    new CleanWebpackPlugin() // new
]

Let’s finally display something on the page!

Granted, this still is going to be very modest but we will have all Webpack machinery hooked up so you can use the result as the template for you future projects. Notice that we managed to change the title of the generated page by configuring the Html Webpack plugin, but what if we want to make more complicated changes, say replace something in the html body? Html Webpack plugin has a concept of template, that is the file you provide as a starting point for the plugin. so. create the template file:

touch src/index.html

and put some basic html structure there

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Pixies</title>
  </head>
  <body>
  </body>
</html>

We will also need a html web loader to accept html files as the template (as html is not the only kind of templates supported):

npm install html-loader --save-dev

Now, we need to tell the plugin to use our template, instead of generating everything from scratch. Also, don’t forget the rule which will trigger html loader for your html files. In your webpack.config.js,

  ...
  module: {
      rules: [
          { // new
        test: /\.html$/i,
        loader: 'html-loader',
      },
       ...
      ]
  }
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html", // new
    }),
    new CleanWebpackPlugin()
  ]

Finally, change the index.html so it displays something:

...
<body>
<section>
      <h1>Pixies</h1>
    </section>
</body>
....

When you run

npm start

The new page should pop up with the “Pixies” text proudly displayed.

Module summary

Woohoo!!! Was it worth though? You could’ve probably create a simple html page which displays the same in 5 minutes.

The difference though is that we hooked up the complete Webpack machinery to handle more complicated things in the future.

Another my goal was to try demystifying how the Webpack configuration works. Now, once you painfully worked through this, you have a template which you can simply copy and paste to your future projects, like I do. You can easily and consciously introduce new loaders and plugins and make changes with understanding what is going on.

If you need to drill deeper into some particular loader configuration, you are ready to read the documentation without the What the hell are they talking about?.

Maybe that surprises you, but in the next module we are not going to improve our “website” yet. Instead, we are going to containerize it so we can deploy it anywhere we want easily and reproducibly. Eventually we will deploy it to Heroku so we can access the live deployment with the browser from anywhere in the world. Here comes the Docker.


See also