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
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>
-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
...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"))
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 = {}
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",
}
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"))
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"],
},
}
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
},
}
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
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: [],
},
}
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: [],
},
},
],
},
}
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": []
}
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
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.
}
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.
}
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
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
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()],
}
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
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"),
}),
],
}
So let's test this out, run:
npm run build
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;
}
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
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
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 */
}
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
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(),
],
}
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(),
],
}
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
}
}]
}
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)
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!
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"
|
I have read and googled a lot but there are so many issues with versions and all that stuff.
Great article, very useful ;)
Awesome. I hope to read your Babel's article soon. Thank you!
This article was very helpful, Thanks a lot :)