This Webpack tutorial is my attempt to document what I learnt and is the blog I wish I'd found when I first started my Webpack journey, all those months ago.
When I first started working at ag-Grid (which is a great place to work!) I had to ramp up on many technologies and frameworks that I hadn't used before. One of these was Webpack - a powerful bundler used in many applications & frameworks.
We here at ag-Grid use Webpack to bundle our own products, as well as using it with some of our framework examples. Although there are alternatives to Webpack, it is still very popular and with version 2.2 recently released I believe it will remain so for quite a while yet.
Webpack is a module bundler. It takes disparate dependencies, creates modules for them and bundles the entire network up into manageable output files. This is especially useful for Single Page Applications (SPAs), which is the defacto standard for Web Applications today.
All code for the blog can be found at the Webpack Tutorial: Understanding How it Works repository on GitHub.
Let's assume we have an application that can peform two simple mathematical tasks - sum and multiply. We decide to split these functions into separate files for easier maintenance:
The output of this would be:
From the above code you can see that both multiply.js and index.js depend on sum.js. We can show the dependency hierarachy in a simple diagram here:
If we get the order wrong in index.html our application won't work. If index.js is included before either of the other dependencies, or if sum.js is included after multiply.js we will get errors.
Now imagine that we scale this up to an actual fully blown Web Application - we may have dozens of dependencies, some of which depend on each other. Maintaining order would become a nightmare!
Finally, by using global variables, we risk other code overwriting our variables, causing hard to find bugs.
Webpack can convert these dependencies into modules - they will have a much tighter scope (which is safer). Additionally by converting our dependencies into Modules, Webpack can manage our dependencies for us - Webpack will pull in the dependant Modules at the right time, in the correct scope (we'll see this in more detail later).
If we take a look at index.html, we can see that we'll need to pull down 3 separate files. This is fine but now imagine again that we have many dependencies - the end user would have to wait until all of the dependencies had been downloaded before the main application could run.
The other main feature Webpack offers is bundling. That is, Webpack can pull all of our dependencies into a single file, meaning that only one dependency would need to be downloaded.
Bundling and Modularisation are Webpack's main features. Through plugins & loaders we can further extend this (we'll see this later) but primarily this is what Webpack is for.
For our initial setup, we'll use the CommonJS module syntax. There are other options (AMD, ES2015) but for now we'll use CommonJS and later move to ES2015.
module.exports to export - or make available - functions or variables to other
require to then pull in these exported values.
Notice that we've made both
multiply available to other code and we've
in these exported functions in both multiple.js and index.js.
Notice too that our index.html now only needs to pull in a single file - bundle.js.
This is great! We now no longer have to worry about dependency order. We can expose what we want and keep other code effectively private. We also reduce web calls from 3 (sum.js, multiply.js and index.js) to a single call - this will help speed loading times.
For the above to work, we need to do some initial Webpack configuration:
At a minimum, we need to tell Webpack what our application entry point is and what the resulting output should be.
entry: This is the main entry point of our application. This is where our initial loading and
application logic will be. Webpack uses this as a starting point for its dependency tree walking. It will
up a dependency graph and create modules as necessary.
output.path: An absolute path for the resulting bundle. To make this cross platform and easy to
use, we use a built-in Node.js
path). This will help us to dynamically create an absolute path, relative to where we
output.filename: The filename of the resulting bundle. This can be anything, but by convention
it's called 'bundle.js'
__dirname is a Node.js utility variable - it is the directory name of the
Looking at the resulting bundle.js can be very instructional (prettified and commented for easier navigation):
From this you can see that Webpack wraps each of our files into a module and passes them into the Webpack bootstrap as an array of Modules. For each module, it adds them to the Webpack, executes them and makes them available to other modules.
__webpack_require__(0) which looking at the array of modules is our index.js.
The result is the output we started with, but with far easier dependency management and less web traffic!
Babel can do a great deal other than simply transpiling ES2015 code into ES5, but covering that is beyond the scope of this blog. Please refer to the Babel site for more information about Babel.
First, let's convert our ES5 code into ES2015:
Here we're using Arrow Functions, the const keyword, Template Strings and the es2015 import/export module format, all of which are ES2015 features.
We need 3 Babel dependencies in order to use it with Webpack:
babel-loader: The interface between Babel and Webpack
babel-core: Understands how to read & parse code, and generate corresponding output
babel-preset-es2015: Rules for Babel on how to process ES2015 code and convert it into ES5
The webpack configuration with the Babel Loader in place looks like this:
As we can have a number of Loaders in Webpack, the values provided are in an array - in our case we're only providing one Loader initially.
loader: The loader to use - in this case the Babel Loader
exclude: We don't want Babel to process any files under node_modules
query.presets: which Babel Preset (or rules) we want to apply - in our case we're looking for Babel to convert ES2015 code
Looking at our bundle.js again (and this time only looking at the part that contains sum.js) we can see the following:
So the Babel Loader has converted our ES2015 code back into ES5 code - great! The best of both worlds.
Let's expand our example to actually output the results of our calculations. We'll create a body on the page, and then add the results of the product and sum to spans, which we'll add to the body:
The output would be the same as before, but on a page:
We can improve this with CSS - let's ensure each result is on a new line, and add a border around each result.
Our CSS will look like this:
We need to pull this CSS into our application. We could of course simply add a
link tag to our
but if we import it and then use Webpack to process it, we'll benefit from what Webpack can offer.
An additional benefit of importing the CSS in our code is that we (developers) can see where the association between the CSS and its usage is. It's worth noting that the CSS is not scoped to the module it's imported to (it's still global), but from a developers perspective the relationship is clearer.
The only change from before is that we're now importing the CSS.
We need two Loaders to process our CSS:
css-loader: Knows how to process CSS imports - takes the imported CSS and loads the file contents
style-loader: Takes CSS data(from imports) and adds them to the HTML document
Our Webpack config now looks like this:
test: as before, we need to tell the Loaders that we only want it to process CSS files - this regex will only process .css files
loaders: the loaders to use. Note that this time it's plural as we're supplying an array of Loaders. Also note that Webpack processes Loaders from right to left, so the results of
css-loader(the file contents) are passed to
style-loader(adding the styles to the HTML document)
If we now run Webpack and reload our application the results will look like this:
Behind the scenes these two Loaders have dynamically added the styles to the HTML document. If we inspect the resulting HTML in Chrome we can see the following:
This is clever, but there are other ways for us to process the CSS. We can split the CSS in cache busting (files with unique hashes) and then include these files into our resulting bundle.
For now, let's just extract the CSS and output it into a file that we can then import. To do this, we're
going to make use of a Plugin:
Loaders are used for pre-processing data before it's output into our bundle. Plugins however can keep output from appearing in our bundle.
Our Webpack config now looks like this:
At the top we're importing the ExtractTextPlugin. We've also changed the loader for CSS to use this plugin:
This tells Webpack to pass the results off the css-loader to the ExtractTextPlugin. At the bottom we configure the plugin:
Looking at dist/style.css we can see:
Which of course is the content of our CSS. To make use of this we need to modify our index.html to import this CSS:
The output will be the same as before.
Let's add some images to our application - and get Webpack (together with a suitable loader) to process them for us.
Let's add two new images to our project a small one and a large one - one for summing and one for multiplication, just to add a little colour to our output.
In order to process these images we're going to make use of two Loaders:
image-webpack-loader: will try to automatically compress large images for us
url-loader: will inline the results from
image-webpack-loaderif the results are small, and include the image in the output directory if they are large
We have two new images we want to add - multiply.png which is relatively large (about 32kb) and sum.png which is relatively small (about 13kb).
First, let's add a new image utility class - this will create a new image for us and add it to the document:
Let's import both the new image utility as well as the images we want to add to our application:
Finally, let's configure Webpack to process these images with the two new Loaders:
output.publicPathAllows the url-loader to know what prefix to add for files that will be saved to disk. For example, a resulting img.src would be img.src='dist/output_file.png'
test: as before, we need to tell the Loaders that we only want it to process image files - this regex will only process .png files. We can make this more complicated by adding support for other image formats, for our purposes this simple regex will do
loaders: our loaders to use - remember that Webpack processes Loaders from right to left, so the results of
image-webpack-loaderwill be passed to
If we now run Webpack we will see something like the following:
If we open 38ba485a2e2306d9ad96d479e36d2e7b.png we should find that it is our large image - multiply.png. The smaller image, sum.png, has been inlined in bundle.js as follows:
Which would be equivalent to having:
When we run our application output is:
From this Webpack Tutorial, you can see what Webpack can offer us as application developers. With a fairly small amount of configuration we've been able to process ES2015 code, bundle it, handle CSS and process both large and small images, all in an easy to understand method.
We've achieved all this and we've only just scratched the surface of what Webpack can do. We can minify & uglify code, split code into cache busting filename, process TypeScript and Angular - there are so many options!
Give Webpack a go - I'm sure you'll find it an indispensable tool in your developer toolkit.
I'm an experienced full stack technical lead with an extensive background in enterprise solutions. Over 19 years in the industry has taught me the value of quality code and good team collaboration. The bulk of my background is on the server side, but like Niall am increasingly switching focus to include front end technologies.
Currently work on ag-Grid full time.