Nitish Dayal
Competent but unconfident. JavaScript, Python, Swift. WOOO
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.
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.
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.
Pre-requisites:
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:
div
HTMLElement with the id root
script
tag loading a file ./build/bundle.js
src/app/sayHello.js
exports two items.
donut
with a string value as a named export.src/index.js
is the file that interacts with the document.
src/app/sayHello.js
is imported to src/index.js
and is referred to as Hello
.name
with a reference to a string value and root
referencing the div
HTMLElement w/ an ID of 'root'
.Hello
function (default export from src/app/sayHello.js
) is called, and is provided
the previously declared name
variable.div
HTMLElement referenced by root
is updated to 'Helllloooo ' + name +'!'
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.
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:
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!
(Pt. 3: Source Maps, Loaders, & Plugins coming soon to a Dev.to near you)