The Fine Art of the Webpack 2 Config
Alexander Flenniken
Posted on February 9, 2017
Like this article? Check out blog.flennik.com for more content like this!
In the constantly mutating world of webdev, skepticism and conservatism will protect you from fads, platform-jumping syndrome, endless refactoring and getting stuck on abandoned, unsupported tools. The Next Big Thing™ invokes zombie developers mindlessly banging their rotting fists onto their late model MacBook Pros, hopelessly migrating from framework1 to framework2 to this to that. No evaluation about whether it is necessary. No respect for the power of their existing tools or the pain of rebuilding everything from scratch every six months in a silly empty pursuit of coolness.
We have love for our existing solutions. We like Gulp. We understand it. It's helped us out. We invested a lot of time into it, we put it on our resumes. We know it does some things better than any alternative, for many years to come.
Of course, eventually and with great pain, sometimes we are forced to admit that the hype is well-founded. That perhaps this shiny new technology truly does change things.
Here is a timeline of my relationship with Webpack:
- Day 1-50: Completely ignorant, except for the name: "Sounds stupid, I don't like it."
- Day 50-100: More positive buzz around the community: "Wow, people won't shut up about this... Everybody must be stupid."
- Day 101: I guess I'll at least take a look: "Isn't this more complicated, what do you get for all this config?"
- Day 102: My God, this is so much simpler than Gulp.
- Day 103: I love Webpack!
Webpack's genius is that it does two jobs at once. First, it bundles. You write import
there in your js, and well, now you need a bundler. And it does css too, great. You cut down on HTTP requests. Your files come together nicely, and that's that.
But oh baby, this is just the start.
The secret weapon of Webpack, the piece that makes it revelation, is it makes babel, typescript, sass, less, jade and every other compiled file type as easy to use as vanilla js. You are already using Webpack to bundle your JS, but now you get so much more for free. Before, if you wanted to add Jade, PostCSS modules, sass, two overlapping JS frameworks, a development server and a custom build process, managing that complexity would become a full time job the day you started. Now you supply a list of loaders and plugins for your frameworks and you're done. The complexity is completely gone, neatly rolled into 80 lines of config. You might as well be using jQuery in terms of trouble, that is to say, it is very, very easy.
Want to see this in action?
Do you want to see some actual finished code so you can follow along with the article? Check out the webpack config for Au7 on Github, a mutant combination of Aurelia, Framework7 and Webpack I built that aims to deliver a unique combination of simplicity and power to webdev.
Ah, Upgrades
Before, Webpack had two major problems. First, the API was a bit odd. Their exclamation point syntax (where the first item is evaluated last, by the way) and question mark syntax are immediately confusing and hostile to newcomers.
css?modules-true!postcss-loader!sass
This is not clean code, I'm sorry.
While Webpack offered some incredible benefits, it makes you think of brilliant NASA engineers building enormous rockets, calculating trajectories, controlled burns and gravity slingshots... but putting the bathroom sixty meters from the living quarters.
The other problem, of course, was documentation, which simultaneously
managed to be too high-level and too basic to be helpful.
Both these problems have been completely smashed by Webpack 2.
The new website was clearly built with these criticisms in mind, as it carefully and methodically introduces you to the benefits, concepts, gotchas and options necessary to use the tool. It's comprehensive, straddles the line between too basic and not advanced enough, and offers a useable search function.
The API, while still mostly backwards compatible, offers much more sensible naming conventions that fit with established practices. Instead of exclamation points, you get this:
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
And instead of the question mark syntax, each loader accepts its own options object with key value pairs. Very clean, very powerful. It still evaluates the first item last, but I can live with that.
Combine this with downright sensible syntax changes, like changing the word loaders to rules and requiring the word loader in css-loader be fully written out for consistency, and you have a much more user-friendly interface. Webpack is rapidly becoming hard to criticize out of hand.
So how can we unlock the full power of Webpack while maintaining that simplicity that is so important?
Say Goodbye to Paragraphs-Long Commands
If you are anything like me, you have a text document somewhere listing multiple paragraph-long commands for your project that you copy and paste into the terminal to make it do things. Or even better, maybe you hold the up arrow, trying to find that one command you used last week, wading through git status
, git status
, git log
, cd
, ls
, cd
, ls
, an endless parade of commands, where is it?!
Webpack can be like that, and even with a simple setup you can end up with something like
NODE_ENV=development webpack-dev-server -d --config webpack.dev.config.js --progress --open
While this is fairly readable, nobody wants to write this twenty times a day.
Enlightenment is here, and its name is npm scripts. Get that mess into your package.json, and now all you need to type is npm run dev
, and the whole big command magically executes. And the benefits go beyond simple saved keystrokes.
By putting a name on the command, you suggest purpose. This makes it easy for others to understand. It doesn't take a rocket scientist to figure out the difference between npm run build:ios
and npm run build:web
. And that's good. Even if you're capable of deciphering a complex command, do you really want to? Be lazy. Lazy is good.
Second, it enforces consistency. There are officially-sanctioned publicly-accessible commands tied directly to your app. No one is going to need to ask you how to run your project or argue about which arguments to use.
Finally, it's simpler. Simple code is easy to read, easy to document, and fun to use.
In other news, I can now type npm run dev
faster than my own name. I think that means I'm turning into a code.
You're Going to Want One File
Okay. The default suggested method of using separate configs is not going to cut it.
The idea is that you have two files, webpack.dev.js and webpack.prod.js. And then you copy and paste the config from one file the other and make the one or two changes... wait. Did I just say copy and paste? Bad developer! Bad developer!
Normally, the webpack config is actually divided between three files when you include webpack.common.js, which is imported by webpack.dev.js and webpack.prod.js. Most of your config data will be in common, and then the dev and prod files will import the data, make the few necessary changes and export the config.
This is a good solution, if you're smart. And if you have a complicated config. But my config is simple. Additionally, I'm stupid.
I'll be honest, I'm not smart enough to mentally split the flow of the code across three files... and when the difference between development and production is a single plugin, you end up with boilerplate (repeated in two files) and your data has all been abstracted in a way that no longer matches the Webpack documentation. Quite a sacrifice.
When this article first went online, I recommended a solution for setting environment variables in a cross-platform way with the command cross-env, and I tweeted a thank you to the creator of the library, Kent C. Dodds, who quickly and cheerfully informed me that his library, for all its uses, is no longer the best option here. It makes me chuckle a bit that the source of this information is the creator of the library himself, but he is right, and I have confirmed it myself. There is a better way.
No, NODE_ENV=production webpack
, with its simplicity and all its cross-platform issues and failure on Windows, is no longer recommended, and neither is the previous workaround cross-env NODE_ENV=production webpack
. Webpack 2, with almost no fanfare and uncharacteristically little documentation, has solved the problem of injecting enviroment variables into the Webpack config, and the solution is far simpler than the vast majority of tutorials will suggest.
The command line option --env
allows you to control as many variables as you like, such as --env.production
or --env.platform=web
, that will be accessible by the webpack config. You put in a simple check like:
const isProduction = env.production === true
And now you have a handy isProduction
boolean you can use in your config.
There is, however, a single change you will have to make to your setup for this to work. The typical Webpack config exports the configuration object like this: module.exports = { objectGoesHere }
. The env variable, however, needs to be passed by Webpack into your file. So instead you must change module.exports
into a function. It looks like this:
module.exports = function (env) {
return { objectGoesHere }
}
There are, however, two big changes we can make to improve this even further. First, we need to provide a default value for env
, because if no environment variables are set by the command line, it will cause an error whenever we attempt to use env.production
or any similar key. Second, we can use a slick arrow function because we are firmly in ES6 land. Here's what it looks like:
module.exports = (env = {}) => {
// Use your env variables here
return { objectGoesHere }
}
By the way, since documentation on this feature is sparse, you can set your variables like this: --env.production
without an equals sign sets env.production to true. --env.platform=web
sets env.platform
to web
, and well, you get the idea.
So much simpler, so much shorter, we are left with commands like this:
webpack -p --env.production --env.platform=web --progress
It is easy to see how simple this API is compared to the previous version of the same command:
cross-env NODE_ENV=production PLATFORM=web webpack -p --progress.
And people complain about the shifting nature of the web? That's crazy! Just look at how things are improving!
Some of these functions are built into webpack-config-utils — for a more complicated setup that is the way to go.
And thanks to Sean T. Larkin, covering this Webpack 2 feature is now a priority. Open source for the win!
Put the Code in the Thing
Now that your code has been handily narrowed down into a single file, you need to introduce conditional data into your module.exports
. Some tutorials create arrays of plugins at the head of the file, feed them into a function called, say, getPlugins()
, and then call the function in the plugins section of the config. This works, but there is a much nicer way.
That way is self-executing code blocks.
By the way, if you want your girlfriend to roll her eyes at you, try telling her how cool self-executing code blocks are.
You may know them as the first few characters of the good majority of js libraries, the (function(){})()
that wraps the code, keeping its variables out of the global scope. It is a function followed by ()
that causes it to execute. This is a known pattern that most devs will understand, and since the function is not named, it can't be called anywhere else. You know that this code is executed here, and only here. This is a tool, combined with return, that we can use while managing js objects to conditionally output data. An example:
{
testVar: (function(){
return "success"
})()
}
And just like that, testVar
is set to success
. And now that we are in a function, we can use if statements, loops, array concat, all the tools of the language, and then return the data we need.
Does it seem strange to anyone else that the function needs to be wrapped in ()
like (functionGoesHere)
for the self-executing to work? Thanks to Dan Wellman, who explained why on Twitter!
A Webpack example next, and, thanks to Node's ES6 support, we can make this even cleaner by using the ES6 syntax, the (() => {})()
arrow function that removes the word function
entirely.
Let's say we want to enable hidden sourcemaps for production.
devtool: (() => {
if (isProduction) return 'hidden-source-map'
else return 'cheap-module-eval-source-map'
})()
I mean, that is clean. You can just read it, it's beyond clear what it does. This is what we want from our config. Dead simple.
In contrast, the getPlugins()
solution moves the list of plugins into the head of the config. That's great and all, but if you're new to the codebase and checking which plugins are in use, where are you going to look first? You will check the plugins section and find a function call instead of the data you want. Yes, you will see plugins: getPlugins()
and know that you can use find and the function name to see the plugins, but that's one more step that you shouldn't need to have to take. And what if you change the name of the function? Are you certain the function isn't called anywhere else? I mean, it seems unlikely, but are you SURE? No, this way is nicer.
Webpack Blocks - What NOT to do
It's a valid criticism of Webpack that there is enough boilerplate that you end up copying and pasting entire configs from project to project. I wish there were a simpler way to tell Webpack to load all file types with their respective default loaders. That would save the headache of 40 copy and pasted lines across a dozen projects, God forbid if I discover I forgot to include woff in my file-loader rule and have to go back and update all of them. Webpack plays coy about css, demurely claiming not to know what it is and what it does. C'mon, Webpack, we both know that you're not that innocent. I know you've been there. I know you've done that.
The solution, however, is certainly not Webpack blocks or easy_webpack, which replaces your 80 lines of configuration (and your fine-grain control) with ten to fifteen npm packages that magically come together to generate your config.
As problematic as Webpack's boilerplate may be, these solutions rob you of your control and flexibility.
Imagine if you install your packages and run your command only to see an avalanche of ERR! in the terminal. Maybe you've encountered some incompatibility or mistakenly combined packages that are meant to be exclusive. How long will it take you to figure that out? Or, even more likely, let's say you press go and everything works, except your sass doesn't appear in your bundle. What steps can you take to fix this, when you no longer have direct access to your config?
Wow, I'm actually freaking myself out thinking about this. Getting cold sweats over here.
Yes, the API might be nicer, in a vacuum. But Webpack now has an excellent set of docs. And there are hundreds of guides and thousands of questions and answers on Github and Stack Overflow. And the knowledge is readily transferable between jobs.
In addition, you almost invariably will want to set part of your project up in a novel way that no previous author could have expected or allowed for. Yes, this is possible with these tools. But now you are going to have to learn the Webpack APIs and write a dedicated config for yourself anyway. So now you have to learn two APIs instead of one.
The most odious part of this, however, is that these solutions do not solve your problem. You still need to copy large amounts of boilerplate. The code just moved from your Webpack config into your package.json. You will never remember fifteen packages offhand. You know what you'll do. You'll copy and paste it.
In the end, we like to aim for perfection, but sometimes we have to settle for almost perfect. Perhaps the solution to those 40 lines is to simply learn to live with them.
What Do You Think?
Building Au7 and sharing it with the community has been a wonderful experience. Putting these frameworks together was so effortless and simple, I get excited just thinking about it. This is an exciting time for web development, and Webpack may be the catalyst. The simplicity that it affords is making projects possible that were never possible before. After all, as engineers, the primary currency we deal with is complexity. Complexity controls what features we can add without introducting a cascade of bugs and errors. It controls the speed that we can work, and thus the budget of our projects. It decides whether our project requires a week of work... or a team of 50 working for four years.
Hit me up on Twitter, my handle is @alflennik.
If you are interested in cross platform development, in that holy grail of reusing the same code across every platform, check out Au7.
Happy hacking.
Posted on February 9, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.