DISCLAIMER: Webpack isn't the only option for module bundling. Module bundling isn't even the only option for solving the client-side module 'issue'. There's a whole lot of ways to do this stuff. I'm gonna take a crack at explaining some Webpack things because that's what I've been working with. + my grammar isn't fantastic, and my vernacular is a mix of wannabe intellectual and obnoxious child. You've been warned.
This was supposed to go up last week, but things happened. 🤷 The original draft was consumed by an unexpected system restart + me not saving my progress on the Dev.to platform, and on the second attempt I realized that trying to fit everything I mentioned at the end of my first post would lead to either a post which wouldn't go deep enough to be valuable, or one which would cause information overload. Instead, I'm going to break that content up into multiple posts.
The goal of this post is to explain what Webpack is by exploring the problem it attempts to solve, and to go over the basics of a Webpack configuration. The content will be targeted towards those who are new to the Webpack ecosystem. I don't know how much further above 'beginner' I would consider myself with Webpack, so if you're fairly seasoned read on regardless and provide feedback. 😃 There are a LOT of other posts similar to this one. I'm standing on the shoulders of slightly deprecated content.
Introduction - The Problem With Client Side Modules
As users have come to expect more from their applications, client-side JavaScript development has evolved to meet those expectations. We're at a point where placing all your code in one JavaScript file can become very unwelcoming, very quickly. Applications are developed by splitting a codebase into small, relevant chunks and placing those chunks in individual files. These files are called JavaScript modules. When a piece of code in fileA.js
is needed in fileB.js
, that code can be imported into fileB
so long as it was exported in fileA
. In order to load these files into the browser, the approach when working with a more manageable number of files would be to add <script>
tags as necessary. This is feasible when working with a handful of files, but applications can quickly grow to a point where manually loading all the files would be very, very complicated. Not only would you be responsible for making sure that all the files were properly referenced in your root document (main index.html
file, whatever you call it), you would also have to manage the order in which they were loaded. I don't want to have to do that with 100+ files.
For example, here is the directory structure from my boilerplate:
├── LICENSE
├── README.md
├── TODO.todo
├── index.html
├── package.json
├── src/
│ ├── components/
│ │ ├── containers/
│ │ │ └── root.js #1
│ │ ├── displayItems.js #2
│ │ ├── hello.js #3
│ │ ├── page2.js #4
│ │ └── styled/
│ │ ├── elements/
│ │ │ ├── listwrapper.js #5
│ │ │ ├── navwrapper.js #6
│ │ │ ├── routerwrapper.js #7
│ │ │ └── styledlink.js #8
│ │ └── index.js #9
│ ├── main.js #10
│ ├── routes/
│ │ └── index.js #11
│ └── store/
│ └── listItems.js #12
├── tslint.json
└── yarn.lock
Twelve JavaScript files for a boilerplate; we're talking about a glorified 'Hello World'. Are these large files? Not at all. Take the files found under src/components/styled/elements/
(full size):
All of the files are under 25 lines of code. In fact, every file inside the src/
folder comes in under 50 lines. I didn't do this for the sake of line count, however. That's a beneficial side-effect of writing modular code. I split my code this way because it gives me a codebase that's easier to maintain, easier to read, easier to navigate, and easier to debug. If I need to change the way my links appear, I know exactly where I need to go to make that change, and I know that once the change is made it will reflect anywhere that a link is created. The folder structure, while probably not all that visually appealing, is nice when programming because it's logical and organized; a styled link element is found under styled/elements/styledlink
. If there's an error or a bug (which there definitely will be), it's easy to trace the problem back to one file/module because they're split up with the intention of giving each module one job. If something breaks, it's probably because I didn't tell someone how to do their job correctly, and it's usually easy to tell where the error is originating from. Once the error is addressed at the module level, I know it's going to be fixed anywhere else that code was reused.
Webpack as a Solution
So how do we get this loaded into the browser without dealing with <script>
tag shenanigans? Webpack! Webpack will crawl through our app starting from the application root, or the initial kick-off point (src/main.js
), following any import
statements until it has a complete dependency graph of the application. Once it has that graph, it will create a bundled file (or files, depending on how you configure Webpack) which can be then be loaded into the browser from inside index.html
. Voilà ! In it's simplest use case, that's what Webpack does. It takes a bunch of JavaScript files and puts them together into one (or a few) files that are more manageable to work with when loading into the browser, while allowing you to maintain the modularity and separation that you like in your codebase.
"Wait a minute, guy. I've seen people use Webpack for CSS, pictures, videos...everything, and you're telling me it only does JS?" Yes! Out of the box, that's what Webpack is capable of understanding. However, at the beginning of my first post I mentioned that Webpack is much more than just a module bundler. With the right configuration settings, plugins, and loaders (more on this later), Webpack can be extended to understand most filetypes that front-end developers come across in order to bundle (and optimize) ALL of your application assets. In most cases, my build process is entirely managed by Webpack & NPM scripts.
A Basic Configuration
Pre-requisites:
- Node
- NPM/Yarn - Installing dependencies, running scripts
- Git - Branch, clone, commit
- Patience - I may be too wordy for my own good sometimes
Example code for this section can be found at: github.com/nitishdayal/webpack-stages-example
The rest of this post assumes you'll be following along by cloning the repo containing the example code. The repo is split into multiple branches that correspond with the upcoming sections.
Initial file layout & directory structure:
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── app
│ │ └── sayHello.js
│ └── index.js
The example provided has a few files worth noting:
-
index.html
-
src/app/sayHello.js
-
src/index.js
Let's break down what's happening in the example:
-
index.html
is an HTML document with two key items:- The
div
HTMLElement with the idroot
- The
script
tag loading a file./build/bundle.js
- The
-
src/app/sayHello.js
exports two items.- An anonymous function which takes one argument and returns a window alert with the message 'Hello ' + the provided argument as the default export.
- A constant
donut
with a string value as a named export.
-
src/index.js
is the file that interacts with the document.- The default export from
src/app/sayHello.js
is imported tosrc/index.js
and is referred to asHello
. - Two variables are declared & defined,
name
with a reference to a string value androot
referencing thediv
HTMLElement w/ an ID of'root'
. - The
Hello
function (default export fromsrc/app/sayHello.js
) is called, and is provided the previously declaredname
variable. - The text content of the
div
HTMLElement referenced byroot
is updated to'Helllloooo ' + name +'!'
- The default export from
Step 1
Branch: Master
First, we'll need to install Webpack. If you're using the example code, run npm install/yarn
from your command line. If you're creating your own project to follow along with, run npm install webpack -D/yarn add webpack -D
. The -D
flag will save Webpack as a developer dependency (a dependency we use when making our application, but not something that the core functionality of the application needs).
NOTE: Sometimes I run Webpack from the command line. I have Webpack installed globally in order to do this. If you want this option as well, run npm install --global webpack/yarn global add webpack
from the command line and restart your terminal. To check if Webpack is installed correctly, run webpack --version
from the command line.
Once Webpack is installed, update the "scripts" section of the package.json
file:
"scripts" {
"build:" "webpack"
},
We've added a script, npm run build/yarn build
, which can be called from the command line. This script will call on Webpack (which was installed as a developer dependency via npm install webpack -D/yarn add webpack -D
). From the command line, run npm run build/yarn build
.
Error message! Woo!
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory.
Use --help to display the CLI options.
As far as error messages go, this one is pretty friendly. Webpack can be run in many ways, two of which are mentioned in this error message; the command line interface (CLI) or a configuration file. We'll be using a mixture of these two options by the end, but for now let's focus on the configuration file. The error message mentions that a configuration file could be named webpack.config.js
; you can name your configuration file whatever you want to. You can name it chicken.cat.js
. As long as that file exports a valid configuration object, just point Webpack in the right direction by using the --config
flag. Example (from command line or as package.json script): webpack --config chicken.cat.js
. If, however, you name your file webpack.config.js
, Webpack will find it without the need for the --config
flag. With great power comes great responsibility, etc.
We know that Webpack failed because we didn't configure it properly, so let's create a configuration file.
Step 2
Branch: init
There's a new file in this branch named webpack.config.js
:
module.exports = env => ({
entry: "./src/index.js",
output: { filename: "./build/bundle.js" },
resolve: { extensions: [".js"] }
});
...wat
Yeah, me too. Let's break this down! First, let's rewrite this without the arrow function and so the output
and resolve
objects are split into multiple lines:
module.exports = function(env){
return {
entry: "./src/index.js",
output: {
filename: "./build/bundle.js"
},
resolve: {
extensions: [".js"]
}
}
};
Currently we're not doing anything with this 'env' argument, but we might use it later. Exporting a function is an option, but at the end of the day all Webpack cares about is getting a JavaScript object with the key/value pairs that Webpack knows. In which case this example could be further simplified to:
// Oh hey look! Somewhere in that mess was a good ol' JavaScript object. The kind
// you can 'sit down and have a beer with'.
module.exports = {
entry: "./src/index.js",
output: {
filename: "./build/bundle.js"
},
resolve: {
extensions: [".js"]
}
};
This object has 3 keys: entry, output, and resolve. Entry defines the entry point of our application; in our case, it's the index.js
file. This is the file that first interacts with the HTML document and kicks off any communication between the other JS files in the application. Output is an object which contains options for configuring how the application's files should be bundled and outputted. In our case, we want our application to be bundled into a single file, bundle.js
, which should be placed inside a folder named build/
. Resolve is an object with an array extensions
that has a single value, '.js'. This tells Webpack that if it comes across any import
statements that don't specify the extension of the file that the import
statement is targeting, assume that it's a .js
file. For example, if Webpack sees this:
import Hello from './app/sayHello';
Given the configuration provided, it would know to treat this as:
import Hello from './app/sayHello.js';
To recap: The file webpack.config.js
exports a function which returns an object (that's what the whole module.exports = env => ({ /*...Webpack config here...*/ })
thing is). The object that is returned consists of key/value pairs which is used to configure Webpack so that it can parse through our application and create a bundle. Currently, we're providing Webpack with the following:
- Where our application starts (entry)
- Where we want our bundle to go and how we want it to look (output.filename)
- How Webpack should treat imports that don't specify file extension (resolve.extensions)
Now, if we call npm run build/yarn build
from the command line, Webpack should be able to do it's thing:
$ npm run build
> example@1.0.0 build /Projects/dev_to/webpack_configs/example
> webpack
Hash: fa50a3f0718429500fd8
Version: webpack 2.5.1
Time: 80ms
Asset Size Chunks Chunk Names
./build/bundle.js 3.78 kB 0 [emitted] main
[0] ./src/app/sayHello.js 286 bytes {0} [built]
[1] ./src/index.js 426 bytes {0} [built]
There should now be a new folder build/
with a file bundle.js
. According to the output from calling npm run build
, this file consists of ./src/app/sayHello.js
and ./src/index.js
. If we look at this file and look at lines 73-90 we see:
"use strict";
/* harmony default export */ __webpack_exports__["a"] = (name => alert(`Hello ${name}`));
const donut = "I WANT YOUR DONUTS";
/* unused harmony export donut */
/**
* Same code, ES5(-ish) style:
*
* var donut = 'I WANT YOUR DONUTS';
*
* module.exports = function(name) {
* return alert('Hello ' + name);
* };
* exports.donut = donut;
*
*/
That's ./src/app/sayHello.js
, and would you look at that, Webpack knew that even though const donut
was exported from the file, it wasn't used anywhere in our application, so Webpack marked it with /* unused harmony export donut */
. Neat! It did some (read: a lot) of other stuff too, like changing the export
syntax into...something else entirely. ./src/index.js
can be seen in lines 97-111. This time, anywhere that a piece of code from ./src/app/sayHello.js
is used, it's been swapped out for something else.
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__app_sayHello__ = __webpack_require__(0);
// Import whatever the default export is from /app/sayHello
// and refer to it in this file as 'Hello'
const name = "Nitish";
// Reference to the <div id="root"> element in
const root = document.getElementById("root");
// Call the function that was imported from /app/sayHello, passing in
// `const name` that was created on line 5.
__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__app_sayHello__["a" /* default */])(name);
root.textContent = `Helllloooo ${name}!`;
Going over everything that is happening in the bundle is best saved for another post; the intention of looking at this file to prove that, yes, Webpack did indeed go through our code and place it all into one file.
If we remember, the index.html
document had a <script>
tag which referenced this bundled JS file. Open index.html
in your browser to be greeted by an alert and a sentence inside a div! Congratulations, you've used Webpack to create a bundle!
EDIT: Part 3 is finally up!
Top comments (1)
Greate aricle. Waiting for next part !