DEV Community

Cover image for The Common Man Guide to Webpack (Webpack for Beginners)
Ivan Kušt
Ivan Kušt

Posted on

The Common Man Guide to Webpack (Webpack for Beginners)

So you've learned some React concepts and can make pretty nice apps using create-react-app, but now it's time to make your own bundler setup for custom deployment (and/or development). Maybe you want to bundle your vanilla JS code for easier code management and dont wish to have a million of script tags in your HTML file or you just need a convenient way to manage Babel-compiled code...

The time has come to use a code bundler.

There are plenty to choose from, including the likes of Rollup, Browserify, Parcel and ofcourse...Webpack.

About this tutorial

This tutorial is aimed at everybody who is starting out in Webpack but finds himself/herself, lost in the official documentation. In here we will cover everything you need to get you started with bundling using Webpack.

This "guide" is written in form of tutorial as to give you hands on expreience (instead of just boilerplate code) through which you will learn the basics and be able to configure Webpack for your own specific needs. Everything will be done step by step, explained in plain english with some behind-the-scenes logic.

This is a beginners tutorial and is by no means an exhaustive guide. If you wish to dive deeper I would recommend the official docs, and will provide links to specific "further reading" at the end of this article.

For those of you that use TypeScript (and there's every reason to do so), I will provide side notes, but our configuration won't really differ much from the basic one.

The common man tutorials

This is the first tutorial in the series titled "The common man guide to {Webpack, Babel, ES-Lint, etc.}". The idea behind the series is to help you utilize these incredible tools without blood, sweat and tears of reading through official docs (not that there's anything wrong with the docs, but they can be quite daunting, frustrating even, for somebody who is just starting out).

What the hell is Webpack anyway

Webpack is a module bundler, meaning...you've guessed it: It bundles JavaScript code, among other things (CSS, images, etc.), as we will see later in the tutorial.

Eons ago, when JavaScript evolved from being a small "library" (used to animate small sections on your static web page) to the great programming language we all know and love today, it became a really good practice to slice up your code into smaller chunks called modules. Aside from custom modules, every modern JavaScript framework, based in Node.js, also uses Nodes built-in modules as dependencies. Loading these modules (both your custom .js files and dependencies) into an HTML web page manually would mean you'd have to manually include each module in <script> tags, as well as watch for the right order in which these modules are included. For production ready sites, with large codebases and a zillion of modules, that is just not acceptable. This is where module bundlers, like Webpack, come into play. Under the hood, Webpack follows your import / export statements (module.exports and require() for CommonJS), creates dependency graph and bundles all modules into one* minified .js file.

Other than bundling code, Webpack offers some other features, such as webpack-dev-server - used in development to preview changes to your code, served from localhost/ with optional hot reloading feature (hot reloading feature enables you to instantly preview changes to your code in the browser every time you save). npm start script in create-react-app uses webpack-dev-server under the hood and for these purposes, so will we.

*well...on larger projects and progressive web apps, the code is bundled into multiple chunks and progressively loaded, according to priority, using AJAX(or similar) requests, but code spliting and isomorphing scope beyond this tutorial. For these puropses I suggest you consult the official docs (https://Webpack.js.org/guides/code-splitting/), or as every other great developer: Google it!

Note: I will be using React for this tutorial, but the same principles will be aplicable to any kind of JavaScript code. You don't even need to know React and can just copy/paste the code.

So, without further ado...

Let's get started

First things first

Webpack runs in Node enviorment, so you will need to have the Node installed globally. To check this go to your terminal and run node -v. This will print out the version of Node you have installed. If you need to install Node, you can download it from here: https://nodejs.org/en/download/

With Node installed, we can start setting up our project. For this part, you can follow along, or you can clone the git repo with starter code: https://github.com/ikusteu/webpack_intro and run npm install inside of webpack_intro folder.

Let's create a root of our project, I will call mine webpack_intro. So I'll:

mkdir webpack_intro

and

cd webpack_intro

To initalize our project and create package.json file let's run:

npm init -y

-the -y flag fills basic project info with default input, you can edit this later

Let's create /src folder to contain our .js files, index.html template and style.css, so let's:

mkdir src
cd src
Enter fullscreen mode Exit fullscreen mode

In our /src folder we will create index.html, style.css, and two JavaScript files: index.js and App.js for a simple React App, so let's:

touch index.html style.css index.js App.js

We won't need this code until the end of the tutorial but let's get it out of the way. index.html will serve as our template so let's just populate it with basic HTML 5 scaffolding, containing div with id of "app" to render our app to:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

-notice how we didn't include our index.js nor App.js into HTML...later we will instruct Webpack to do that for us

Next, let's create a simple React app, we will:

npm install react react-dom --save

--save flag will automatically save installed packages to package.json dependencies

With React installed, in App.js write:

// App.js
import React from "react"

const App = () => {
  return <h1>Hello Webpack</h1>
}

export default App
Enter fullscreen mode Exit fullscreen mode

...and let's render our app to html, in index.js write:

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"

ReactDOM.render(<App />, document.getElementById("app"))
Enter fullscreen mode Exit fullscreen mode

We will leave style.css empty for now, and we're ready to start with Webpack.

Note: The starter code ends here, from here on out, do follow along.

The config file

For better part of this tutorial, we will be setting up our webpack.config.js file and going through each option with brief explanation. After the setup we will play around with it a bit to see it in action.

Before we start configuring Webpack, we need to install it first, so let's cd to our root directory and run:

npm install webpack --save-dev

the --save-dev flag will save Webpack to the list of dev dependencies in package.json

With Webpack installed let's create a config file:

touch webpack.config.js

When bundling, Webpack will, unless specified otherwise, look for a config file in our root directory (the one that contains package.json file) with the default name of webpack.config.js . There are ways around that, but I will cover that, as well as working with multiple config files, in a future tutorial.

Note: We can use Webpack without the config file (by either utilizing default presets or CLI), but in this tutorial I cover this approach.

The main part of the config file is basically an object containing various options. We will explore all basic options in the following sections as we add them to our file so for now, let's just add and export an empty object and move on:

// webpack.config.js
module.exports = {}
Enter fullscreen mode Exit fullscreen mode

Entry

The first thing we need to specify is an entry.

So, what is an entry?

Webpack is ran as a process in Node enviorment. It starts at an entry point and creates a dependency graph (this is how Webpack creates a bundle and ensures all modules are loaded in the right order). By specifying an entry point, we tell Webpack where to start graphing dependencies, in other words, where does our application start.

In this case our app starts at index.js which renders our app into the DOM. So, let's tell Webpack to start there by defining an entry point in our config file as our index.js:

// webpack.config.js
module.exports = {
  entry: "/src/index.js",
}
Enter fullscreen mode Exit fullscreen mode

To understand how Webpack will treat this, let's take a closer look at index.js and analize the order in which it is executed:

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"

ReactDOM.render(<App />, document.getElementById("app"))
Enter fullscreen mode Exit fullscreen mode

The execution starts at ReactDOM.render(<App />, document.getElementById("app")) which renders our App component to the DOM. In order to render an app to the DOM, we utilize the ReactDOM.render() function imported from the react-dom module, making react-dom a dependency. Our app component is declared in a separate file so we need to import from ./App.js, making it a dependency as well. Finally, in order to understand our App, which is a JSX, React component we need to import react, making it yet another dependency.

What Webpack will now do is, it will start to graph at index.js, read the three imported modules (treating them as dependencies) and look into each dependency to find their dependencies, their dependencies' dependencies and so on, until it has crated a full tree of imports. With all imports mapped, Webpack will then resolve the absolute path to each dependency, which conveniently brings us to the next point, the resolve option.

Note: You can also use multiple entries, but for those cases, refer to the docs.

With Typescript: If you're using TypeScript, Webpack can process .ts and .tsx files so your entry point would look something like index.ts or index.tsx (no need to precompile your files to .js).

Resolve

After creating a dependency graph, Webpack will resolve every dependency's absolute path. While resolver allows for a few options in configuration, we will take a look at one in particular, and that is extensions option. This allows us to specify an array of extensions, telling Webpack which extensions to autocomplete when creating an absolute path. Let's show this in practice. If we add resolve: {extesions: []} option in this way:

// webpack.config.js
module.exports = {
  entry: "/src/index.js",
  resolve: {
    extensions: [".js", ".jsx"],
  },
}
Enter fullscreen mode Exit fullscreen mode

and then we use import App from './App', Webpack will automatically look for a file in local directory ./, titled App with extension of .js or .jsx and find our App.js, making it as if we specified import App from './App.js'.

There are some more pretty cool options for resolver, such as aliasing path to often used directories (to avoid heavy use of relative paths in your imports), so if you'd like, do some research on your own on the subject (https://Webpack.js.org/configuration/resolve/)

With TypeScript: If using TypeScript you would also specify .ts and .tsx (for React). However, please note that even though you might only use .ts and .tsx extensions in your code base, you still need to add .js to your extensions. Otherwise, Webpack will throw an error while compiling since it won't be able to resolve any of the node modules, including its own modules because they're all .js files.

Output

So far we have given Webpack an information on where to start building a dependency graph, which will then be compiled and bundled, as well as provided extensions which to autocomplete while resolving. Now we need to specify where to save or output the bundle.

So, let's add an output option. Add this to our config file:

// webpack.config.js
const path = require("path")

module.exports = {
  /* ...entry and resolve options */
  output: {
    path: path.join(__dirname, "dist"), // directory where our output file will be saved
    filename: "bundle.js", // specifies the name of the output file
    publicPath: "./", // path to bundle.js relative to index.html
  },
}
Enter fullscreen mode Exit fullscreen mode

What have we done here?

In an output option we need to specify a path to the output directory. This is needed because Webpack creates a new directory for which it needs an absolute path (unlike entry, which can be relative to our root folder). To create absolute path we utilize one of Node's core modules called path. In this case, __dirname (a Node core variable) gives us an absolute path of 'this' file's directory (this being file we are reading, in this case webpack.config.js file) which is joined with 'dist' string creating a path that looks like this '<...absoute-path-to-root-directory>/dist'. filename is the name of our bundle, where publicPath specifies a path to the output folder, relative to our index.html file (this is used for auto importing of our bundle into our HTML file using <script> tags), in this case './' means both our HTML file and bundle.js file are in the same folder.

Note: Don't get confused if you see path.resolve() instead of path.join() with same argument as above, which, in this case, does the same thing since path.resolve() resolves full path whereas path.join() simply concatenates paths, but since `dirname` is absolute, the result is the same (an absolute path).

Loaders

Now that Webpack knows where to start looking for dependencies and where to save compiled bundle, we need to tell it how to process these dependencies before bundling. This is where loaders come into play. Loaders tap into the compilation process by adding certain rules/templates on how to process each module. We will use different loaders for different file extensions. Right now, we will only add babel-loader for .js and come back later. In this tutorial, we will use some of the most common loaders, but there are plenty out there so you can do some resarch on your own.

First, let's install babel-loader. Aside from babel-loader itself, we will need to install Babel, with some of its presets, as well.
Run:

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
Enter fullscreen mode Exit fullscreen mode

Let's add module option with rules array inside to our config file right below output:

// webpack.config.js
import path from "path"

module.exports = {
  /* ...entry, resolve and output options */
  module: {
    rules: [],
  },
}
Enter fullscreen mode Exit fullscreen mode

The module option contains all rules regarding modules (how they are loaded, processed etc.). In rules array we tell Webpack how and when to apply each loader. We will use Babel to precompile JavaScript (well, technically, 'transpile' would be the right term). For those who are not familiar, Babel is a great tool which transplies newer JavaScript syntax (ES6, JSX, ESNext...you name it) to vanilla JavaScript. I will not go too much in depth on it right now, since I plan on writing a Babel-focused tutorial as well. For purpose of this tutorial, we will just copy/paste basic config.

Let's add a first rule to our rules array:

// webpack.config.js
const path = require("path")

module.exports = {
  /* ...entry, resolve and output options */
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        options: {
          presets: ["@babel/env", "@babel/react"],
          plugins: [],
        },
      },
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

First we tell Webpack to test for files with regex for .js and .jsx extensions (you don't need .jsx if you're not using React). With loader, we tell Webpack which loader to load these files with. At last, we specify options for our loader, in this case Babel presets.

Note: since babel-loader uses Babel, we can also utilize Babel's config file. This is actually best practice with Babel in any scenario, but for purposes of this tutorial (to illustrate applying options to loader in webpack config file), I went with this approach. If you want to do it the "right" way, you would omit options property, create babel.config.json and inside write the same options, so it would look like this:

// babel.config.json
{
  "presets": ["@babel/env", "@babel/react"],
  "plugins": []
}
Enter fullscreen mode Exit fullscreen mode

There are a few ways to set up Babel config file, but more on this in Babel tutorial.

With TypeScript: If you're using TypeScript, here you would test for .ts and .tsx file extensions instead of .js / .jsx and either install and use ts-loader instead of Babel or configure Babel to process TypeScript with @babel/preset-typescript preset. (More on that in my Babel tutorial)

Scripts

Finally, we have the basic config and are able to start bundling our code.

Now, to start our Webpack process, we need to configure script(s). For this tutorial we will use only one script and we will call it build.

Note: I will explain scripts some more and look into using multiple scripts for different tasks in a more advanced tutorial

To be able use Webpack's scripts we need to install Webpack's CLI module, so let's do just that, run:

npm install webpack-cli --save-dev
Enter fullscreen mode Exit fullscreen mode

This package lets us run Webpack from our terminal or, in this case, add a custom script, so let's navigate to scripts in our package.json file, it should look something like this:

// package.json
{
  // ...name, description etc.
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ...keywords, dependencies etc.
}
Enter fullscreen mode Exit fullscreen mode

We will delete test script, since we don't need it now, and replace it with build script so our package.json should look something like this:

// package.json
{
  // ...name description etc.
  "scripts": {
    "build": "webpack --mode production"
  }
  // ...keywords, dependencies etc.
}
Enter fullscreen mode Exit fullscreen mode

What we have done here is: We have created a script called "build" which runs a Webpack CLI command webpack --mode production (this is also how create-react-app build script runs in a nutshell). The --mode production compiles our code using production default options, I will look into different modes in an advanced tutorial, but for now let's run our script:

npm run build
Enter fullscreen mode Exit fullscreen mode

At this point, if you've followed everything correctly and don't have any typos in your code, Webpack should have ran a compilation, and you should have gotten a message in your terminal looking something like this:

webpack <Webpack version> compiled successfully in <execution time in ms>

If you recieved this message, navigate to your root directory, and you should see that Webpack created a /dist folder, like we've instructed it to. When we cd to /dist folder, we should see our bundle.js file and when we open the file, we see a bunch of minified code. 'Et voilà', we've created our first bundle.

We are not done yet, however. There are still some tweaks we would like to do. For instance, we still need to manually import this bundle to our HTML file. If we were using our bundle as some added feature on our page, we would be perfectly fine with this. However, if our code is central to our app, like in this case, when creating a React app, we would like Webpack to spit out an index.html file with our bundle.js, automatically included using <script> tags. Luckily, we can do this by utilizing a Webpack plugin, so let's jump to the next section.

Plugins

Webpack plugins "...do everything a loader doesn't". Without getting too much into how plugins work, they, like loaders, tap into compilation process and provide additional templates and, most often serve as loaders and sometimes spit out additional files, like in this example.

The first plugin we will use is html-webpack-plugin. This plugin will spit out an index.html file in our /dist folder, with our bundle included in <script> tags.

Let's install the plugin:

npm install --save-dev html-webpack-plugin
Enter fullscreen mode Exit fullscreen mode

After we've installed the plugin, we need to import it to our config file, and initialize an instance of it in our plugins array:

// webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  /* ...options ending with 'module' option */
  plugins: [new HtmlWebpackPlugin()],
}
Enter fullscreen mode Exit fullscreen mode

Let's take a look at what we just did. We've imported html-webpack-plugin as HtmlWebpackPlugin. We have also added plugins array at the bottom of our config object. You can probably guess by now...this array holds initializations of instances of plugins. To elaborate: Webpack plugins are sort of like classes (not entirely, but for purposes of this tutorial, you can think of them as such), therfore, we need to initialize an instance of a plugin. Let's try this out. Save the config file and run:

npm run build
Enter fullscreen mode Exit fullscreen mode

After Webpack compiles, take a look at the changes in the /dist folder. You should now see index.html file. When we run index.html in browser we see that it has our bundle already included in script but nothing is rendered to the screen yet, as if our bundle isn't working...

How does Webpack know where to find the bundle?

This is thanks to specifying publicPath property in output option we talked about earlier.

Why didn't anything get rendered then?

Well, the created index.html is an HTML 5 template provided by the plugin and doesn't contain <div id="app"></div>. Remember that, in our index.js we use this <div id="app"> to tell React where to render everything, so how do we sove this. Luckily, Webpack plugins, behaving like classes, allow us to pass parameters to a constructor-like function. This way we can pass our own /src/index.html as a template HTML file. Let's add template to our config file like this:

// webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  /* ...options ending with 'module' option */
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src/index.html"),
    }),
  ],
}
Enter fullscreen mode Exit fullscreen mode

So let's test this out, run:

npm run build
Enter fullscreen mode Exit fullscreen mode

If you open /dist/index.html you should see the difference applied with the document now containig <div id="app"></div> and ofcourse when we run this index.html file we now see that everything renders perfectly.

Now that we have an HTML template, let's add some styling to our page.

In /src/style.css write:

.title-box {
  width: 100%;
  text-align: center;
}

.title {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

also refactor our App.js a bit to look like this:

import React from "react"
import "./style.css"

const App = () => {
  return (
    <div className="title-box">
      <h1 className="title">Hello Webpack</h1>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

and run our build script.

If you've done everything right...Webpack should throw an error saying, in a nutshell, that it doesn't recognize this ".css thing".

You've probably guessed the solution, and that is to utilize a loader for .css. Let's go ahead and install it. Run:

npm install css-loader --save-dev
Enter fullscreen mode Exit fullscreen mode

and add a rule in our config file to test for .css and use css-loader as loader.

You should be able to do this on your own by now, so do try it.

After applying a rule, our config file should look like this:

// webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  /* ...entry, resolve, etc. */
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        options: {
          presets: ["@babel/env", "@babel/react"],
          plugins: [],
        },
      },
      {
        test: /\.css$/,
        loader: "css-loader",
      },
    ],
  },
  /* plugins */
}
Enter fullscreen mode Exit fullscreen mode

Let's run build script and inspect out HTML file.

As you can see, we've managed to mitigate the compilation error but we don't see any CSS applied to our document. What happend here is, we told Webpack to process CSS using css-loader. css-loader told Webpack how to process CSS, but it didn't tell it what to do with CSS when processed, for that we need another loader. This is a common convention with Webpack loaders - each loader does exactly one thing, but we can chain them together for a desired effect. One solution here would be to use style-loader, which will write our compiled style.css at the begining of our index.html between <style> tags. This is a good solution, but we will use something more interesting.

We will utilize mini-css-extract-plugin

For practise, do install the said plugin, import it in our config file and initialize inside plugins array (you don't need to pass any parameters to plugin initialization) and then check the steps below.

Installation:

npm install --save-dev mini-css-extract-plugin
Enter fullscreen mode Exit fullscreen mode

config file:

// webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  /* ...other options */
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src/index.html"),
    }),
    new MiniCssExtractPlugin(),
  ],
}
Enter fullscreen mode Exit fullscreen mode

Now we've created an instance of mini-css-extract-plugin, but didn't tell it what to do.

Remember our statement about plugins doing everything loaders don't do, well here's an example. Mini CSS plugin extracts precompiled CSS from Webpack bundle to a separate main.css file, combined with HTML plugin it links said file to /dist/index.html. It acts as a loader, to "take in" the compiled CSS and spits it out to a file. In order for its loader to work properly it needs to be chained after css-loader. To acomplish this, let's refactor our CSS rule in config file to look like this:

// webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  /* ...other options */
  module: {
    rules: [
      /* .js, .jsx rule */
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src/index.html"),
    }),
    new MiniCssExtractPlugin(),
  ],
}
Enter fullscreen mode Exit fullscreen mode

What you see here, is a rule with chained loaders, the difference here is that we didn't pass loader property, instead we added our loaders to use array. This is how we use chained loaders. The important thing to note here is that chained loaders are applied from right to left (or bottom up) so here CSS gets loaded by css-loader and the output is then passed to MiniCssExtractPlugin.loader to extract it to a main.css file.

Note: If we wanted to pass custom options to each loader, inside of our use array, we could, instead of queueing loaders as strings queue objects containing loader property and options property for each loader like this:

// instead of this
{
  test: /\.css?/,
  use: ["loader2", "loader1"]
},
// we write it like this
{
  test: /\.css?/,
  use: [
    {
      loader: "loader2",
      options: {
        // loader2 options
      },
    },
    {
      loader: "loader1"
      options: {
        // loader1 options
      }
      }]
}
Enter fullscreen mode Exit fullscreen mode

If we run our build script, we should now see style.css created in /dist folder and applied to index.html

Now that we've covered all of the basic Webpack concepts and config, options, feel free to move your files around and edit the config file to practise what you have just learned. Hopefully you now posses firm understanding of how everything works so you can build on that and get into more advanced topics.

Where to go from here?

One excercise you can do is set up an image loader so that you can import images from local drive into React using (commonjs/ES6 imports). Do try it on your own and I will upload the final code base (with solution to this excercise) as a branch on the starter code git repo: https://github.com/ikusteu/webpack_intro

I plan to make another tutorial on some of the use cases we haven't covered in this one.
Other resources I would recommend are official docs: https://webpack.js.org/, and this video from the creators of Webpack: https://www.youtube.com/watch?v=gEBUU6QfVzk&t=562s

That's it,

Happy Coding :)

Top comments (5)

Collapse
 
gschnei profile image
Gavi Schneider

This is by far the most 'beginner friendly' Webpack introduction / tutorial I've ever encountered. I've been searching forever for something like this. I actually feel like I now understand how to add Webpack to my projects. Thank you!

Collapse
 
g33ksuperstar profile image
g33ksuperstar

Damn, I can't make it past the first npm run build, I get the following:

ERROR in ./src/index.js 6:16
Module parse failed: Unexpected token (6:16)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See webpack.js.org/concepts#loaders
| import App from "./App"
|

ReactDOM.render(, document.getElementById('app'))

I have read and googled a lot but there are so many issues with versions and all that stuff.

Collapse
 
parkadzedev profile image
Michael Parkadze

Great article, very useful ;)

Collapse
 
ericpatrick profile image
Eric Patrick

Awesome. I hope to read your Babel's article soon. Thank you!

Collapse
 
rajeshmanne profile image
Rajesh-Manne

This article was very helpful, Thanks a lot :)