Monday, 9 October, 2017 UTC


Like it or not, webpack is a staple of the modern front-end JavaScript toolset, turning countless little files into one (or more) cohesive bundles. Yet to many, it’s workings are an enigma. The rise of CLI tools that produce complex ready-made configurations has helped both assuage and exacerbate this problem. But when all is said and done, do you really understand what that myriad of configuration files are even doing? Would you be able to build your own configuration by hand? Well hopefully, after this article, you will be.
Starting Off
Let’s create a quick project with vue-cli’s webpack-simple template.
The webpack-simple template is about as basic of a webpack setup as you can get away with for Vue. It is, as the name implies, pretty simple, but it gets the job done remarkably well. I have to admit that I’ve used it in production once even though you’re not supposed to…
# Install vue-cli globally if you haven't already. $ npm install -g vue-cli # Create a new project called "demistify-project" with the "webpack-simple" template. $ vue init webpack-simple demistify-project # Install the dependencies for the project $ cd demistify-project $ npm install 
Alright, now, if you want, fire up the development build with npm run dev and play around with the code, hot-reload, and whatnot. Don’t worry, I’ll wait.
The ✨Mystical Config✨
You done? Alright, let’s go ahead and take a peek at webpack.config.js, the file that does most of the heavy-lifting. I’ll paste the file I have here for good measure. (Generated with version 1.0.0 of the webpack-simple template.)
var path = require('path') var webpack = require('webpack') module.exports = { entry: './src/main.js', output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js' }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { loaders: { } // other vue-loader options go here } }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.(png|jpg|gif|svg)$/, loader: 'file-loader', options: { name: '[name].[ext]?[hash]' } } ] }, resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' } }, devServer: { historyApiFallback: true, noInfo: true }, performance: { hints: false }, devtool: '#eval-source-map' } if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' // module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), new webpack.LoaderOptionsPlugin({ minimize: true }) ]) } 
Okay, so there’s quite a bit to take in there. And honestly, you’d do well to leave most of it alone. I’ll try and explain it to the best of my ability though, so if you really do feel like messing with something, you’ll at least have a vague idea of what you’re messing with.
Anyway, webpack.config.js is just a file that exports an object that is interpreted by webpack to build your project. That means you can use any node modules in it and compose it from as many files and places as you want. That makes Webpack incredibly flexible and powerful, but it also means that almost no-one uses the exact same conventions when writing their build configurations.


Stepping through that object, the first property we hit is entry. This tells Webpack which files to start the bundling process from. You can also pass an array of strings, or use an object to specify different chunks.
entry: './src/main.js', // Different chunks: entry: { main: './src/main.js', vendor: './vendor/index.js' } 


The next section is a bit more complicated and confusing. The output property is where we specify where (and how) the generated bundle(s) end up. As it’s used in the template, the output section looks like this:
output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js' }, 
  • path - The root directory for all generated assets. We use __dirname and path here to make sure everything ends up in the right place regardless of the current working directory.
  • publicPath - This is the path the development HTTP server will serve the contents of output.path on. You might need to change this a bit for your particular setup. Basically: ./dist/build.js in the filesystem becomes /dist/build.js.
  • filename - The name of the file to bundle everything into. This can be customized to include a hash (build-[hash].js) or use the chunk name (build-[name].js) as well as other things
I’ve found that the majority of my problems stem from mis-configured entry and output sections, so it’s a good idea to get real familiar with those, and look there first if something goes wrong.

Module, rules, and loaders. Oh my!

  • Module Documentation
  • Rule Documentation
This bit here is essentially the core of a webpack build. The file loaders. Loaders are essentially packages that transform whatever files are routed through them into some other form. This is how all those cool pre-processors like babel and vue-loader work with webpack.
This section looks complicated but to be honest it’s really predictable and easy to understand.
Let’s go ahead and step through each of the rules:
{ test: /\.vue$/, loader: 'vue-loader', options: { loaders: { } // other vue-loader options go here } }, 
Ah yes, good ‘ol vue-loader the secret sauce of Vue.js that turns Single-File-Components into JS and render functions.
Here’s what each property does:
  • test - A regular expression that checks if a filename ends with the .vue extension. That makes sure vue-loader only runs against .vue files.
  • loader - The name of the loader to use. No surprises here.
  • options - Options you can pass to the loader.
Since vue-loader lets you add in other loaders as pre-processors for parts of templates, hence the options.loaders property.
The format of options.loaders is as follows:
loaders: { preprocessorName: Loader Definition | Array<Loader Definition> } 
For example, if you wanted to use SCSS in Vue components, you’d set options.loaders like so:
loaders: { sass: [ { loader: 'css-loader' }, { loader: 'sass-loader', options: { indentedSyntax: false // Set to true to use indented SASS syntax. } } ] } 
That first processes the styles with sass-loader to turn them into valid CSS, then lets webpack’s css-loader do whatever it needs to with them.
Okay, moving on to the next rule. The one for babel-loader.
As specified by test, webpack will run all .js files through babel-loader.
We also see a new property, exclude . This is a regex that tells webpack what patterns to ignore. In this case, it excludes any .js files in node_modules.
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, 
The final loader is file-loader it takes any files imported or linked to and outputs them in the output directory.
In this configuration, it will run for four common image formats, outputting them in the build directory with the hash of the files contents appended. (This makes it easier to know when a file has changed.) It also modifies any references to these files so they include the hash.
For example, ./src/alligator.svg might become ./dist/alligator.svg?uqTyWCLN8jVxGHFU4kiN1DXB0G6qzDae4Y4kFxZaP4g=
{ test: /\.(png|jpg|gif|svg)$/, loader: 'file-loader', options: { name: '[name].[ext]?[hash]' } } 

Miscellaneous bits & bobs

  • Resolve Documentation
  • DevServer Documentation
  • Performance Documentation
  • Devtool Documentation
At the end of the configuration object are a few more properties. These are more related to optional features of webpack, but let’s go through them anyway.
resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' } }, devServer: { historyApiFallback: true, noInfo: true }, performance: { hints: false }, devtool: '#eval-source-map' 
  • resolve / alias - The resolve object allows you to configure how webpack’s module resolution works. In this case, we’re aliasing the package vue to vue/dist/vue.esm.js, which provides Vue in ES2017 Module format.
  • devServer - The devServer allows us to configure webpack’s development server. In this case, we’re just telling it to fall back to sending index.html for 404 errors (historyApiFallback). All noInfo does it tell webpack to not output a bunch of stuff we don’t need to the terminal each time a hot-reload happens.
  • performance - A property like performance looks important right? Sorry to disappoint. All it really does is configure webpack’s performance hints, which mostly tell us when we’re bundling files that are kinda big. We’re disabling that here for now.
  • devtool - The devtool property lets you decide which method of source mapping to use. There are like twelve different options for this which vary in speed, quality, and production-readiness. We’re using eval-source-map here which honestly isn’t great, but doesn’t create any new files or modify the source.

Production configuration

Congratulations! We’re almost done! The very last bit of the file are changes to the configuration that are made if we’re building the production version of the code. It looks a lot scarier than it is. In fact, I think I’ll just throw comments in it instead of writing everything down separately.
// Make sure to set the NODE_ENV environment variable to 'production' // when building for production! if (process.env.NODE_ENV === 'production') { // Use standard source mapping instead of eval-source-map. module.exports.devtool = '#source-map' // // Add these plugins: module.exports.plugins = (module.exports.plugins || []).concat([ // Let's your app access the NODE_ENV variable via. window.process.env. new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), // Crumples your bundled source code into a tiny little ball. // (Minifies it.) new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), // This is just here for compatibility for legacy webpack plugins // with an options format that isn't compatible with Webpack 2.x new webpack.LoaderOptionsPlugin({ minimize: true }) ]) } 
There you go! Hopefully you now know enough to create your own Webpack + Vue setup.
When in doubt, don't shy away from reading the docs. They've improved quite a bit recently.