A Comprehensive Introduction to Webpack, the Module Bundler
Stanislas Bernard10 min read
As a front-end Javascript developer, managing your source files can quickly get tricky. On the back-end side, Node has a built-in module resolver —require
— whereas there is no built-in module resolver in the browsers.
In this article, I will get you started with Webpack, a very powerful tool to organize, compile and package your source files. Webpack is a module bundler that will let you compile, optimize and minify all your files: Javascript, CSS, pictures, etc.
You might have heard about Gulp as another module bundler. Compared to Webpack, Gulp can be seen as a scripting platform, that will let you chain different building tasks. Webpack is an all-in-one solution, capable of handling all your source files. Moreover, Webpack offers some very powerful features that will make your developments faster:
- a module resolver for your front end source code
- a hot reloader, that will replace any modified part of your code directly in the browser when you save the source file, without any page refresh.
Instead of presenting a Webpack boilerplate or starter kit, I will show you how to build a web application step by step from scratch, so that you will end up with a fully working solution that you are comfortable coding with.
Let’s get started!
Basic project structure
First of all, we need to organize our project’s folders. Here is a common directory structure that has proven its worth for many people:
.
├── src // The main source folder, where all our source files will go (Webpack's input)
| └── your-first-js-file.js
├── dist // Built files folder (Webpack's output)
├── package.json
└── webpack.config.js // Webpack config file
We will go into more detail regarding each file later in the article.
Package installation
In order to use Webpack, we first need to fetch the corresponding module. We’ll assume you’re running Node v4+.
All commands will be run from your project’s root folder. If you don’t have a package.json
in your project already, then run npm init
.
Now let’s install Webpack:
npm install --save webpack
Minimum configuration
Now that we have Webpack installed, let’s configure it. This is done with a plain JS file, webpack.config.js
.
The bare minimum to make Webpack compile our sources is to provide it with an entry point as well as an output.
var config = {
entry: './src', // entry point
output: { // output folder
path: './dist', // folder path
filename: 'my-app.js' // file name
}
}
module.exports = config;
We will now add an npm
script to the package.json
to call Webpack compilation:
// package.json
// ...
"scripts": {
"build": "webpack"
}
// ...
Now, when running npm run build
, Webpack will look the webpack.config.js
up and compile our application.
As you set in the config, our application’s entry point is ./src
. We therefore need to define this entry point:
// src/index.js
console.log('Hello Webpack!');
Now that we have our entry point, let’s build the application!
npm run build
As a result, Webpack will bundle up our src/index.js
into dist/my-app.js
. If you look at the produced file, you’ll notice that Webpack has added some code of its own. That is the module resolver. This provides a require
function, exactly as in a NodeJS script!
Add an index.html
to our project, so that we can see the effects of the JS files.
<!-- index.html -->
<html>
<head>
<script type="text/javascript" src="./dist/my-app.js"></script>
</head>
<body>
</body>
</html>
Manually open this file with your favorite web browser, and see the Hello Webpack!
in the console.
Modularity
Let’s recap what we have done so far. We have configured Webpack to bundle the entry point -src/index.js
- into an output bundle -dist/my-app.js
-. We then created an index.html
that calls the bundle. This way, all the code inside src/index.js
is executed in the browser when we manually open the index in a browser.
The next step is to separate our code into multiple files.
To build our source files, Webpack will start by compiling the entry point provided in the configuration file. It will then move from require
to require
(or import
in ES6), and include every ’required’ file in the build pipe.
Let’s see how to include other files to the bundled output:
// src/greet.js
function greet(who) {
console.log('Hello ' + who + '!');
};
module.exports = greet; // Exposes the greet function to a require in another file
// src/index.js
var greet = require('./greet'); // Import the greet function
greet('Webpack');
As you see, we can require local files by providing a relative path. Notice that we don’t need to specify the file extension, since it’s a JS file. Webpack can manage a bunch of default extensions to know which file it will look for.
Another important thing to notice is that you might eventually end up with very long relative paths when your application grows. For example, it is quite common to depend on a file located in a different folder, and you will find yourself writing imports such as require('../../../../components/home/dashboard/title')
.
To avoid such messy paths, it is possible to tell Webpack which folder it can consider as the application’s root folder. In our case, src
is the root folder:
// webpack.config.js
var path = require('path');
var SRC = path.join(__dirname, 'src/');
var NODE_MODULES = path.join(__dirname, 'node_modules/');
// ...
resolve: {
root: [SRC, NODE_MODULES], // root folders for Webpack resolving, so we can now call require('greet')
alias: {
'actions': path.join(SRC, 'actions/'), // sample alias, calling require('actions/file') will resolve to ./src/actions/file.js
// ...
}
},
// ...
With the ability to use modules, we now have very clean code organization. This is because it has been split into several files and folders, depending on what your application is.
Moreover, we can manage our dependencies just as we would do in a Node application. Therefore, we can import external libraries by calling require('library-name')
.
This will ask Webpack to resolve the module library-name
, by looking into the node_modules
folder. (Webpack will look into other default folders too, see there).
Let’s modify our greet
function to use an external library:
// src/greet.js
var moment = require('moment'); // Add momentjs
function greet(who) {
console.log('Hello ' + who + ', it\'s ' + moment().format('h:mm:ss a') + '!');
};
module.exports = greet;
Compilation
So far we have our source files bundled up into a single output file, which is read by the index.html
.
That’s a good starting point for building our web application, but unless we want to write it in plain old ES5 Javascript, we need to actually transform the files while they are bundled together.
Let’s say for instance we want to use the new Javascript specifications, EcmaScript2015 and EcmaScript2016.
Since we are building an application for the web, we need to maximize the browser compatibility of our code. Therefore, we need to transpile any ES6/ES7 code to ES5, which is supported by all modern browsers (do not hesitate to use the excellent caniuse website to check any browser-related compatibility questions).
We will use Babel to transpile ES6 and ES7.
Let’s install Babel and its presets for ES6 and ES7:
npm install --save-dev babel babel-preset-es2015 babel-preset-stage-0
We need to configure it to use these presets:
// package.json
// ...
"babel": {
"presets": [
"es2015", // ES6 compilation ability
"stage-0" // ES7 compilation ability
]
},
// ...
Babel is now configured to transpile JS files, but it’s still not working together with Webpack.
Let me introduce the concept of Webpack loaders. Loaders are used by Webpack in order to handle a given file type.
Every time Webpack reads a require()
or an import
, it will handle the content of the required file with a loader.
By default, Webpack comes with a built-in JS handler, which tells it how to handle plain JS files and bundle them up. In order to handle different file types, or simply deal with JS files in a different manner, we must specify a loader configuration to Webpack.
As you might have guessed, the link between Webpack and Babel will be made by a loader.
It’s called… babel-loader
!
First, we need to download it:
npm install --save-dev babel-loader
Now that we have it as a dependency, we must tell Webpack where to use it.
// webpack.config.js
var config = {
entry: './src',
output: {
path: './dist',
filename: 'my-app.js'
},
module: {
loaders: [
{
test: /\.js$/,
loaders: ['babel'] // note that specifying 'babel' or 'babel-loader' is equivalent for Webpack
}
]
}
}
module.exports = config;
This will add a new loader to Webpack.
Every time Webpack sees a require('xxx.yy')
, it will loop through all its configured loaders and check if xxx.yy
matches the provided test
regexp (in our case, /\.js$/
).
As you can infer now, the babel-loader
will be used to compile every .js
file. We can therefore rewrite our good old ES5 files into ES6/ES7 files!
Managing other file types
Now that we are clear on the loader concept, we can manage other file types. In this article, I’ll show you how to handle some common file types, such as fonts and CSS files; however, keep in mind that there are a LOT of loaders on npmjs for almost any file type.
Style files
Plain CSS files
There are two loaders for .css
files, called style-loader
and css-loader
.
You can try to figure out by yourself how to add these loaders in order to handle .css
files.
Here is a suggestion of configuration:
// webpack.config.js
// ...
{
test: /\.css$/,
loaders: ['style', 'css'] // Note that the order is important here, it means that 'style-loader' will be applied to the ouput of 'css-loader'
},
// ...
NOTE: the CSS will be added to the bundled file; in our case, my-app.js
. The style will be loaded in your browser, but you might be used to having separate .css files. If you want Webpack to output separate style files, please use ExtractTextWebpackPlugin.
SASS/Less/PostCSS
In case you are using SASS, Less or PostCSS, simply add the corresponding loader:
Font files
If you use specific fonts in your project, Webpack can handle them too!
Since the fonts will be downloaded as such by your client application, use the file-loader
:
// webpack.config.js
// ...
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
loader: 'file?name=public/fonts/[name].[ext]'
}
// ...
This will output all font files to the public/fonts
folder.
Images
Similarly to fonts, images are usually served directly by web servers and loaded on demand by the client. The loader configuration is very similar:
// webpack.config.js
// ...
{
test: /\.(jpg|png|svg)$/,
loader: 'file?name=public/images/[name].[ext]'
}
// ...
Other files
We could continue the list of all possible files Webpack loaders can handle, but this is not the aim here. If you want to load other file types, search for ‘yourFiletype loader’ on the Internet, and you will find the corresponding loader. For example, load .jade
files with the jade loader
, .cs
files with the coffee loader
, etc.
Going further
These are the basic usages of Webpack. You might now want to move to more advanced topics about Webpack.
Here is a non-exhaustive list of subjects you might want to read about:
- Optimising your bundles by partially serving them, and more
- Webpack dev server and the hot modules reloader
In the meantime, do not hesitate to read the official documentation to discover new features and grab a deep understanding of Webpack’s multiple options.
Update 05/02/17
I had initially planned to write this article in two parts, putting the more advanced topics in the second parts. Unfortunately, I could not make it to write the second part, so I added two links to great articles dealing with these topics.