webpack By Example: Part 3

This article is part of a series (starting with webpack By Example: Part 1) of articles that, through a number of progressively more complicated examples, explores the purpose and functionality of webpack. The examples’ configuration files and source code is available as a GitHub repository.

At this point we have a way to build projects consisting of a HTML template and one or more of: JavaScript files, CSS files, and assets, e.g., images.

In this article, we work towards a production-ready configuration.

extract-text

One of the unusual questions when using the webpack-cli init command is about use this in production (don’t we all expect to use this in production?) and the related question is about bundle your CSS files. Both of these questions end up being related to the use of extract-text-webpack-plugin.

To illustrate the purpose of this, we setup index.js and index.css as we did in the css example:

src/index.js

require('./index.css');
window.console.log('hello world');

src/index.css

body {
background-color: yellow;
}
#root {
height: 400px;
background-color: purple;
}

Made the following choice during the execution of webpack-cli init; not sure why prod was the default name for the name (changed it back to config).

? Will you be creating multiple bundles? No
? Which module will be the first to enter the application? ./src/index.js
? Which folder will your generated bundles be in? [default: dist]:
? Are you going to use this in production? Yes
? Will you be using ES2015? No
? Will you use one of the below CSS solutions? CSS
? If you want to bundle your CSS files, what will you name the bundle? (press en
ter to skip)
? Name your 'webpack.[name].js?' [default: 'prod']: config

We end up with a webpack.config.js file:

note: Simplified the css-loader to not use the sourceMap option; seems to be a non-standard and unnecessary configuration.

const webpack = require('webpack');
const path = require('path');
/*
* We've enabled UglifyJSPlugin for you! This minifies your app
* in order to load faster and run less javascript.
*
* https://github.com/webpack-contrib/uglifyjs-webpack-plugin
*
*/
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
/*
* We've enabled ExtractTextPlugin for you. This allows your app to
* use css modules that will be moved into a seperate CSS file instead of inside
* one of your module entries!
*
* https://github.com/webpack-contrib/extract-text-webpack-plugin
*
*/
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: 'css-loader',
fallback: 'style-loader'
})
}
]
},
plugins: [new UglifyJSPlugin(), new ExtractTextPlugin('styles.css')]
};

Instead of adding functionality in the JavaScript bundle to inject the style tag into the DOM (as was the case in the css example), this configuration extracts the CSS into a separate stylesheet named style.css.

Executing the command ./node_modules/.bin/webpack from the command line in the project folder will create main.bundle.js and style.css files in the dist folder. To run the example, create the index.html file into the dist folder and open it a browser; the developer console will display hello world and the expected yellow and purple styling has been applied.

dist/index.html

note: Observe the new link tag.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>webpack Patterns</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div id="root"></div>
<script src="main.bundle.js"></script>
</body>
</html>

At this point, we have exercised all the features of the webpack-cli init command except several more advanced topics (save these for a later article):

  • Multiple Bundles
  • ES2015
  • CSS compiling, e.g., SASS

html

The previous example illuminated a problem with our pattern of manually adding the index.html file to the dist folder; we have to make sure to include the stylesheet link and the JavaScript script tags. While in this specific case it is a trivial problem; as we introduce cache management techniques later, it will become increasingly burdensome.

The solution comes in the form of yet another plugin: html-webpack-plugin. Starting from the previous example, we install the plugin; npm install --save-dev html-webpack-plugin and update webpack.config.js.

webpack.config.js (add to top with other imports)

...
const HtmlWebpackPlugin = require('html-webpack-plugin');
...

webpack.config.js (add to array of plugins)

new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
})

Finally, we create a new folder public with an updated index.html.

public/index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>webpack Patterns</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

Executing the command ./node_modules/.bin/webpack from the command line in the project folder will create main.bundle.js, style.css, and index.html (with link and script tags injected) files in the dist folder.

clean

In the previous examples, we should have been continually deleting the dist folder before a fresh build to ensure that we did not retain an unused file from an earlier build. To automate; starting from the previous example, we install the plugin; npm install --save-dev clean-webpack-plugin and update webpack.config.js.

webpack.config.js (add to top with other imports)

...
const CleanWebpackPlugin = require('clean-webpack-plugin')
...

webpack.config.js (add to array of plugins)

new CleanWebpackPlugin(['dist'])

Executing the command ./node_modules/.bin/webpack from the command line in the project folder will now delete the dist folder before proceeding.

development

Another pain point is that every time we make a code change, we have to re-run ./node_modules/.bin/webpack and reload the browser; not practical if we are actively developing. We also want to have performant (building) JavaScript source maps so that we can use our browser’s debugger.

Continuing from our clean example, we update webpack.config.js as follows and install webpack-dev-server, npm install --save-dev webpack-dev-server.

webpack.config.js (add to module.exports)

note: This devtool option provides for performant yet reasonable source maps; for production builds we still want to use source-map.

devtool: 'cheap-eval-source-map',

webpack.config.js (update plugin with option)

new UglifyJSPlugin({sourceMap: true}),

Executing the command ./node_modules/.bin/webpack-dev-server --open from the command line in the project folder will now build the project, serve it up through a web server, and open your preferred browser to the application. Any changes to the source code will automatically trigger a new build and reload of the browser.

note: Hot Module Replacement (HMR) is a more advanced development topic that we will cover in a later article.

production-ready

As a reminder, we started this article by selecting the production option with the webpack-cli init command; we then supplemented it with:

  • html-webpack-plugin: Injects link and script tags
  • clean-webpack-plugin: Deletes the dist folder before each build
  • webpack-dev-server (and cheap-eval-source-map): Provides for rapid development with automatic builds and browser updating.

From the previous articles we can incorporate:

  • -p build option: optimized production build
  • source-map: A more accurate source map
  • file-loader: Supporting loading assets, e.g., images, as files.
  • url-loader: Supports loading assets, e.g., images, as data URLs.

In order to use different devtool values between development (non-production) and production builds, we export a function taking env as parameter that returns the configuration in webpack.config.js.

webpack.config.js

const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = function(env) {
return ({
devtool: env === 'production' ? 'source-map' : 'cheap-eval-source-map',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: 'css-loader',
fallback: 'style-loader'
})
},
{
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
loader: 'url-loader',
options: {
limit: 10000
}
}
]
},
plugins: [
new UglifyJSPlugin({sourceMap: true}),
new ExtractTextPlugin('styles.css'),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
}),
new CleanWebpackPlugin(['dist'])
]
});
}

To build in development we simply run ./node_modules/.bin/webpack-dev-server --open and in production ./node_modules/.bin/webpack -p --env production.

Yarn

To ensure that the development environment remains consistent from installation to installation, we can install and use Yarn to lock down the dependency tree; yarn install .

Deployment and Caching

With a production build, our example (including the cute image) has the following files in dist.

Asset                                 Size
d09dbe3a95308bb4abd216885e7d1c34.jpg 35.3 kB
main.bundle.js 739 bytes
styles.css 65 bytes
main.bundle.js.map 7.45 kB
styles.css.map 87 bytes
index.html 343 bytes

Deploying this simply involves copying these files to a folder on a web server with index.html being the file the browser loads first.

Finally, we need to think about how browsers cache resources. Without any caching related headers the browsers will resort to repeatedly requesting the files (may be sent the file again or a not modified message). While this is good in that we will want to be able to push out new versions of the files, we will explore in a later article how to have the browser cache the files indefinitely and then “bust the cache” when we want to update a file.

The Next Part

In the next part, webpack By Example: Part 4, we will explore JavaScript transpiling, e.g, ES2015.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.