Why Can’t Anyone Write a Simple Webpack Tutorial?
Godbless @sokra, I wish we all had the brains, ambition and time to fork all of the most popular, and probably a lot of the most unknown repo’s that are in some way eloquently amazing, and create an amazing tool that grows on the user the more they use it. But, for us mere mortals, let’s just learn to use that tool to a level that we can become much more efficient/proficient and have a lot more fun in our current projects without tearing our hair out reading through GitHub issues and a surprisingly small amount of Webpack related StackOverflow posts. Props out to @petehunt for bringing Webpack to the masses with his How To README. But, for those of us who aren't config savant’s and potentially are working on a project where we have to reverse engineer some nasty concat tasks and other legacy build processes into our super hipster `require` or es6 `import` everything….I’d like to put together a “brief” summary of my adventures in Webpack bliss/not so bliss.
My initial adventure down Webpack lane came at the 6 month point on a seemingly uncomplicated project. The task…to get rid of a lot of globals and build a more intelligent architecture, nobody asked me to, but I wanted to do it because something deep down inside of me was knawing away at my JS guts. Ok November 2014, Browserify is super hipster let’s give it a go. So, I have some global packages such as jQuery….ok don’t start yelling at me already because I’m not just `require`ing jQuery, but it came from Bower or some shit and was getting loaded in a script tag before a bunch of other shit. And you know what, for architecthtural reasons, that’s the way it had to be. And there’s some other stuff like jQuery cookie that extends jQuery and other packages that aren’t common.js compatible like History.js and whatever else getting concated and whatever else’d on their way to local dev, staging and production. So, Browserify comes along and after reading this…(PS you have a window into my Chrome bookmarks here)
and a weekend of wasted time, Browserify, or Browserify shim, or some shit wanted me to put a bunch of stuff in my package.json and probably a bunch of other places in order to reverse engineer my concat task and expose some globals, so a bundle.js and page level .js could exist on the same page and live happily ever after together (deep breathe). Well…….this never worked out and a weekend was wasted where I could have been taking down spicy pickle backs at Skinny D’s or polishing the wheels of my fixey instead of filling my package.json full of worthless bullshit.
Anyhow, Webpack had been introduced to me at about the same time but my pea sized brain couldn't quite handle it…..but it was still there for me when I had my Browserify de-piphany. Ok, so here’s where this article actually starts to get interesting. Thank you @shama for actually writing a useful article on Webpack that was relevant to us social misfits who have to move from concat to `require`.
Mind Blown…..insert .GIF here because everyone does => the script loader. Fuck all that bullshit about overloading the require statement. I’ve already loaded jQuery globally in a <script> tag, that’s not gonna change, and I want to extend it with jQuery cookie. So, if jQuery cookie gets loaded in a commonjs environment it’s gonna try to require jQuery, which isn’t going to work unless we want to bundle jQuery again…which isn’t going to happen. So, let’s just load that S.O.B through the script loader and call it a day. Yup, stringifies the whole damn lib, and evals it. Do it with a bunch of it’s friends, all in one damn file and serve it up. Do some require’s in there while you’re at it and have a ball.
Next up I need to load up a bunch of directories at a time rather than `require`ing file by file. Well thanks again @shama let’s throw in some `require.context`. If you’re a real config ninja and/or get pretty savy reading the Webpack docs this will be intuitive but in laymans terms `require.context` allows us to grab hold of all files in a directory, recursively even down through subdirectories if a`true` boolean is passed as the second parameter, and a regex can even be passed as a third parameter to do cool stuff like omit bullshit or include only sweetness from this directory tree. The function that is returned from `require.context` has some cool methods on it (after all most everything in JS is an object….right) like `.keys` that allow us to iterate over the filepath keys obtained from the directory and `require` them individually..
Not so recently, I came across an ambitious module bundler that only until recently did I realize how fantastic it is…dontkry.com
So the result of your bootstrapped script-loader and `require.context` global bundle could be something like this:
Done and done…..let’s go to Hill Country for some margaritas .
Last steps in our global bundle that would make any JS hipster cringe could be some cool plugins like the ProvidePlugin:
where we can be super lazy and do stuff like…..hey I don’t want to have to say `require(‘React’);` in every frick’n file, so Webpack, you’re cool, when you see React, just know that I’m too lazy to say `require` and bundle it for me. How about you do the same with jQuery too (just ignore I said shit about loading jQuery in a script tag earlier…this is just an example of the coolness of Webpack over the lameness of Browserify shimming).
Oh and by the way, there’s this library called Paperjs that I tried to import as a node_module but it wanted me to globally install Cairo and some other crazy shit on my local machine and I didn’t want to, so I added some stuff like this to my webpack.config.js
and all of a sudden I can to stuff like `require` Bower stuff without a relative path and I just so happened to import Paperjs into bower_components.
PS. if there’s one thing you want to know about Webpack is there is a lot of stuff where the key in an object becomes the package/file name and the value is the module name or relative path to the file. This is the case for the `entry` object in the exported Webpack config object and the options object passed to the `ProvidePlugin`.
So anyway, you passed this options object to the ProvidePlugin and now you can just use Paperjs as the global `paper` inside of your modules. And guess what…it won’t be global outside of the Webpack closure (I think…don’t quote me on that), and Webpack is smart enough to use this non-commonjs module that exports a global and supply it to your modules in some magical way that it is useful without installing a bunch of global BS on your machine with HomeBrew or some other god aweful package manager.
I’m not sure what this is exactly supposed to mean but I think it means JS should be fun, expressive…whatever. But, somehow I forgot this when I first came to Webpack when I saw this big config object that seemed to export the same stuff regardless of environment. Hey, in prod I want to Uglify stuff….but in dev I don’t. Oh there’s an Uglify plugin for that, but why not a loader…I thought they did all the stuff in Webpack.
Ok, so first things first let’s just forget about splitting hairs between these weird things => plugins and loaders. I’m sure there’s a difference and some hipster is going to get all cocky nerd on me for even bringing it up…and I hope they do because I would like to know what it is. Point of the story for this paragraph is I want differnt things to happen in prod and dev and how does that work if I’m always exporting this seemingly static object through running this Webpack CLI tool.
Well my mind blown epiphany #2 was checking out the react-starter. What the hell, theres 5 config files:
- weback.config.js (duh…no brainer)
Thanks @sokra for using your big brain to make us all feel so stupid and confused. Basically, if you look in the `package.json` you will see some scripts that use the Webpack CLI (I prefer the API but regardless):
Google up on this shit in the Webpack CLI docs and webpack-dev-server (this can really be a mind bender along with hot-module-replacement and could be a topic of a whole other post) if you want to know what’s going on, but basically webpack is using the ` — config` flag to pass a custom config which passes environmental options to `make-webpack-config`, which in turn exports an object that webpack ingests…Mind Blown!!!
and guess what happens now. Multiply bundles with the cool names you specified as keys….and guess what again, if you want to dynamically add these files to directory paths you can add those coolie slashes in the key name and they will be written to the `pages` and `layouts` directories in your `output.path` accordingly.
Loaders and Chunking Stuff
So a lot of Webpack magic happens in loaders and we all like to use cool shit like Babel, PostCSS, and tons of other hipster stuff you would use a lot of config for in Grunt or CLI land, and a little less in Gulp (PS if you want to read an awesome article about harnessing some experimental powers of the css-loader to locally scope CSS within components you should check out the Seek article on The End of Global CSS).
and then you can magically `@import` modules from Bourbon in your style sheets that are `require`d by Webpack.
For my next and most likely last example, because it’s approaching 2:30AM EST, I’d like to use the `handlebars-loader` and continue down our journey of reverse engineering an older architecture with a minimum of two JS bundles per page. So, maybe I want to load some client side hbs templates that rely on Handlebars and the Handlebars runtime to function (and along the way pass a hipster query param, that by the way is parsed by Webpack `loader-utils`, and these query params specify the path to my client side custom handlebars helpers):
Cool, that looks legitttssss…but I’m `require`ing my hbs templates in both my global bundle and some of my page level bundles. So, what happens if two of those bundles are on the same page?? Yup….you guessed it, the Handlebars lib/runtime gets bundled twice. So, what to do…I ask @altano creator of the `handlebars-loader`? Well, the CommonsChunksPlugin he replies.
Now I cringe at the thought of having to journey back into the Webpack docs once more and I begin to create a little sample repo to sandbox my experiences. Surprise, surprise…not really, it doesn’t work at first. My first try of:
results in 4 bundles being created…..the first three are the key names in the entry object and the 4th is named `commons.js`. The problem is `commons.js` only includes common boilerplate Webpack code that would be shared between the 3 entry modules. To add this to my markup I would do something like this:
Ok, so back to the drawing board, the scripts work but but the entire Handlebars lib is still being bundled inside `bundle-with-hbs.js` and `with-hbs.js`. I dig a bit deeper and find an example:
which seemed to almost work until I got a Webpack error saying something about `fs` in the Handlebars module. After Googling around I found a GitHub issue where @sokra suggested something and then @zalad comes up with the winner:
I don't know exactly what this does and I’ve set:
before in order to inject a `_filename` variable into my modules for logging errors in JS files before, but at this point I don't care. The magic has worked with a few seemingly inconsequential errors left in the console:
WARNING in ./~/handlebars/lib/index.js
require.extensions is not supported by webpack. Use a loader instead.
and here’s the explanation:
- `vendor` is the resulting filename and `require`s handlebars from `node_modules`. The fact that it is in an array is inconsequential (it will work without this but allows multiple modules to be added and chunked out) and is actually a good trick I’ve seen mentioned by @sokra if you are having issues requiring modules between multiple entries. By Webpack convention if multiple items are placed in an array for the `entry` property they will be bundled together.
- the `name` property in `CommonsChunksPlugin` options is set to `vendor`. If this doesn’t happen then the output will result in 5 files being created, the 3 entries, 1 vendor, and 1 commons. Because `vendor` is specified as the name in the Commons Chunks config and it matches the `vendor` key in the `entry` Webpack knows to chunk `vendor` out of any entries that `require` it, even if that is happening through a loader….in this case the `handlebars-loader`.
- the result is four files (commons.js, bundle-with-hbs.js, no-hbs.js, with-hbs.js) but the only bundle that actually contains the Handlebars lib/runtime is commons.js.
Now loading all of these in within script tags results in the motivation to create this blog post.
Long Drawn Out Conclusion
Webpack is awesome…..lots of stuff out there is awesome. You just gotta pick what you like and go with it
This is my webpack.config.js. There are many like it, but this one is mine.
I didn’t begin to mention tons of amazing other features of Webpack such async loading, the webpack dev server/dev tools, deduping/optimization, and built in cache busting. My friend @colindresj (and answerer of most all my Webpack related questions) has a nice example repo of how to use the webpack-dev-server in combination with another development server, which was quite confusing to me at first. There are lots of other posts out there, many of which are linked in this doc so please explore, report back and comment on all the shit I said wrong or could improve on in this post.