A little webpack intro

Webpack is a module bundler. For a better understanding of what it does, you can view it as a giant function. It takes your dependencies (scripts, images, stylesheets, fonts, etc.) as its input and produces a bundle. Your bundle can be only one file with everything in it, or it can be multiple files, with cache-busting filenames and lazy-loaded assets. Webpack can do all sorts of things, but setting it up can be a real pain.

Webpack configuration

Webpack’s config lives in a .js file. The extension is pretty important, because it means you can (and will) write code to configure your module.exports.

Example app

(Note: I’ll be using the ES6 export/import syntax. See this link for more info.)

Assume all of these live in the same folder.

This is the entry point to our app

The entire app.js file should be pretty simple to understand. We generate two random numbers, place them into an array and then find the bigger of the two, needlessly complicating our application so we can use the ES6 spread (…) operator. Then we use jQuery to set the html of #number.

Something is iffy though. We don’t have a html file. These two lines also ring some bells:

import './styles';

import { magic } from './magic';

First off, there are no extensions. Second, we’re importing a .scss file. WTF?

package.json

Before we dive into configuring webpack, let’s take a look at our package.json:

We will explain what each of the scripts does in the following sections. Note that I haven’t set up a linter, to make this article a bit shorter. If I wanted a linter, I’d follow my own advice from part 2.

webpack.common.js

We will use a different configuration for development (build:d, watch) and production (build:p) environments. A good chunk is shared between the two, so we extract it into a separate file.

entry: {

app: ['./app.js'],

vendor: ['jquery']

}

The entry property (docs) signals webpack where our application begins. It can be a string, for a single entry point, an array, for multiple entry points (single file, all loaded at startup) or an object (multiple files, all loaded at startup).

We want two files: one with our code, the other with the vendor code. This is a common setup which allows your users to cache the vendor file (which rarely, if ever, changes), while always getting your latest code. More about this technique here.

output: {

path: path.resolve(__dirname, 'dist'),

publicPath: '/'

}

The output property (docs) tells webpack where to write our bundle, as well as which public path it will be served from. Note that we’re missing the filename attribute. It will be different for development and production builds, which is why we haven’t defined it here.

resolve: {

extensions: ['', '.js', '.webpack.js', '.scss']

}

The resolve property (docs) is the magic that allows us to drop the extension when importing a file. It tells webpack: “Hey, if you can’t find the file as the developer named it (‘’), try appending these extensions.”

module: {

loaders: [

{

test: /\.js$/,

exclude: /node_modules/,

loader: 'babel',

query: {

presets: ['latest', 'stage-0'],

cacheDirectory: true

}

},

{

test: /\.scss$/,

loaders: ['style', 'css', 'sass',

'postcss?parser=postcss-scss']

}

]

},

postcss: function () {

return {

plugins: [cssnext]

};

}

Loaders (docs) are probably the most important part of your webpack config. When webpack encounters a file which matches the test regex, it pipes it through the defined loader(s) before bundling it. Loaders are invoked right-to-left and can be configured using a query object (in case of a single loader) or query strings. If there is no appropriate loader for the filetype you’ve imported, webpack will complain. There are many loaders available.

For our .js files, we use babel-loader to transform everything to ES5. We specifically exclude the node_modules directory since npm dependencies come precompiled.

For .scss files, we use postcss-loader with the postcss-scss parser which allows us to use the cssnext plugin. We transform the resulting scss into css using sass-loader and then use css-loader and style-loader to inject the css into our page.

plugins: [

new htmlPlugin({

title: 'My awesome app',

filename: 'index.html',

template: 'index.ejs'

})

]

Plugins (docs) are similar to loaders, except that they can work with the whole bundle, while loaders only work with individual files. There are many plugins available.

html-webpack-plugin generates a html file with the appropriate link and script tags. We use the following template:

Development build

We have two dev related scripts in our package.json:

"build:d": "npm run clean && webpack --display-error-details --progress --config webpack.dev.js",

"watch": "webpack-dev-server --progress --inline --hot --config webpack.dev.js --display-error-details"

The display-error-details flag shows us more detailed errors, which is very useful while developing. The progress flag shows progress as the bundle is compiling. The config flag points to the config file webpack should use.

webpack-dev-server builds and serves our entire bundle from memory. Specifying the inline flag adds a second entry point to our bundle, causing our page to listen via websockets. When webpack-dev-server detects a change, it signals for a refresh of the page. By specifying the hot flag, we allow our changes to get displayed without refreshing the page, if the loader for the changed file supports it.

config.output.filename = `bundle.[hash].js`;

config.plugins.push(new webpack.optimize

.CommonsChunkPlugin('vendor', 'vendor.[hash].js'));

First, we set the filename of our bundle. Next, we use the CommonsChunk plugin to extract the vendor bundle into a separate file. [hash] will be replaced by the hash of the compilation.

devServer: {

port: 8080

},

devtool: 'eval-source-map',

debug: true

We tell webpack-dev-server to listen on port 8080. We also want to inspect our original code in developer tools, so we specify eval-source-map (we could’ve also used source-map, but eval-source-map has better rebuild speed, which saves time in development). Finally, we switch all loaders into debug mode, which will help us diagnose problems should they arise.

Production build

We have two production related scripts in our package.json, although only one gets used directly:

"build:p": "npm run clean && webpack --config webpack.prod.js && npm run minify-html",

"minify-html": "html-minifier --collapse-whitespace --sort-attributes --sort-class-name --remove-comments --remove-empty-attributes --remove-redundant-attributes dist/index.html > dist/final.html && mv dist/final.html dist/index.html"

The build:p script builds our app and afterwards minifies the resulting html using html-minifier. html-webpack-plugin also supports minifying, but I haven’t been able to set it up correctly.

config.output.filename = `bundle.[chunkhash].js`;

config.plugins.push(new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.[chunkhash].js'));

First, we set the filenames of our files, but this time using [chunkhash] instead of [hash]. [hash] changes with every compilation, while [chunkhash] remains the same as long as the file contents don’t change.

config.plugins = [

new md5Hash(),

new extractText('styles.[chunkhash].css'),

new webpack.optimize.UglifyJsPlugin({

compress: {

warnings: false

}

}),

new webpack.optimize.OccurenceOrderPlugin(true),

new webpack.optimize.DedupePlugin(),

new webpack.DefinePlugin({

'process.env.NODE_ENV': JSON.stringify('production')

}),

...config.plugins

]; config.module.loaders[1].loaders = undefined;

config.module.loaders[1].loader = extractText.extract(['css', 'sass', 'postcss?parser=postcss-scss']);

Due to a bug where the [chunkhash] changes when it shouldn’t, we use the webpack-md5-hash plugin which replaces [chunkhash] with the md5 of the file, achieving the effect we wanted.

We extract our styles to a separate file using the extract-text plugin. The plugin needs to be defined as a loader and isn’t compatible with style-loader, which is why we modify scss loaders.

We want to minify our files in production, which is why we use the uglify-js plugin. It switches all loaders to minify mode and uglifies our js.

The OccurenceOrder optimizes the occurence order of our code.

The Dedupe plugin removes duplicate or similar files from our output.

Finally, the DefinePlugin allows us to do compile-time dependency injection. Some libraries (eg. React) have code that looks like this:

if (process.env.NODE_ENV !== 'production') {

// show warning

}

By defining process.env.NODE_ENV to production the if branch turns into dead code which gets picked up and removed by uglify-js. The JSON.stringify part is important. Without it we would get: