Smarter CSS builds with Webpack
Nobody writes CSS in one big file any more.
You, the savvy CSS developer, well-versed in SMACSS, SUIT and BEM, are writing small, isolated modules in separate files.
stylesheets/config/colors.sassmedia_queries.sassmodules/btn.sassdropdown.sassheader.sassutilities/align.sassclearfix.sass
When it comes to building a single bundle.css
file to send down to your users, you're manually specifying all of the individual files that you know your app needs.
If you're using a preprocessor like Sass, you might be using its @import
statement:
@import "vendor/normalize"@import "config/colors"@import "config/media_queries"@import "modules/btn"@import "modules/dropdown"@import "modules/header"@import "utilities/align"@import "utilities/clearfix"
If you're working with Rails or Middleman, you might be using Sprockets' //= require
directives:
//= require vendor/normalize//= require_tree ./modules//= require_tree ./utilities
Or you might have rolled your own asset pipeline, using a tool like Gulp or Grunt to collect, process & concatenate the individual files:
gulp.task('styles', function () {gulp.src('styles/**/*.scss').pipe(sass()).pipe(gulp.dest('./tmp')).pipe(concat('bundle.css')).pipe(autoprefixer()).pipe(gulp.dest('./build'));});
All of these approaches require you to know which bits of CSS your app actually uses. You're tediously maintaining a list of dependencies in a manifest file, or (more likely) glob importing entire directories.
You're potentially including CSS that isn't actually required by your app. You only discover & remove unused CSS by occasionally searching your project's HTML templates for a class name (or having a tool do it for you).
You're keeping your HTML's CSS dependencies in your head and not in code. In my humble opinion, this is a Very Bad Thing™.
But never fear... if you're generating HTML from modular views written in JavaScript, there's a better way! (If you're not, here's a sneak peek of the future!)
Requiring UI dependencies with Webpack
Webpack, the amazing module bundling Swiss army knife that you should probably start using, is all about letting you write modular UI code with explicitly declared dependencies.
You've probably used CommonJS modules before:
var _ = require("underscore");var findTastiestPizza = function (pizzas) {return _.find(pizzas, function (pizza) {return pizza === "hawaiian";});};module.exports = findTastiestPizza;
CommonJS modules sychronously load dependencies, which is fine for Node.js but doesn't work very well in browsers because network requests are asynchronous.
To run our modular code in browsers, we need a module bundler like Webpack or Browserify to bundle up all of our dependencies into a single JavaScript file.
UIs aren't just JavaScript though. Our UI components can also depend on images, fonts and of course CSS. Webpack recognises that and, with the help of loaders, supercharges the require
function so you can explicitly require all of your dependencies; not just the JavaScript ones:
require("stylesheets/modules/btn");var img = require("images/default_avatar.png");
So back to our original problem: generating a single bundle.css
programatically, based on the CSS your HTML actually needs, rather than manually maintaining a manifest.
All you need to do is explicitly require each view's own CSS or Sass dependencies like you would its JS dependencies, and Webpack will build a bundle that includes everything you need, performing any extra pre- or post-processing steps as required.
For example, given this entrypoint into our app...
// index.jsvar React = require("react");var Header = require("./header");var Footer = require("./footer");var Page = React.createClass({render () {return (<div><Header /><Footer /></div>);}});React.render(<Page />, document.querySelector("#main"));
... which requires these components, which in turn require their own Sass dependencies...
// header.jsvar React = require("react");require("stylesheets/modules/header");var Header = React.createClass({render () {return (<div className="header">Header</div>);}});module.exports = Header;
// footer.jsvar React = require("react");require("stylesheets/modules/footer");require("stylesheets/utilities/clearfix");var Footer = React.createClass({render () {return (<div className="footer u-clearfix">Footer</div>);}});module.exports = Footer;
... Webpack will generate a CSS bundle that looks something like this:
.header { /* ... */ }.footer { /* ... */ }.u-clearfix { /* ... */ }
Amazing!
Gotchas
Don't rely on source order
The main caveat to this approach is that you don't have any real control over the generated order of your CSS. Webpack will follow calls to require
in the order that you specify them, appending to the generated CSS bundle as it goes.
So if we switched the order of our header & footer require
calls...
// index.jsvar Footer = require("footer");var Header = require("header");
... the footer's dependencies, including the .u-clearfix
utility, will be included first:
.footer { /* ... */ }.u-clearfix { /* ... */ }.header { /* ... */ }
If you're manually maintaining a manifest, it's common to include different types of classes in a specific order, e.g.
- base
- modules
- utilities
... and rely on that order to ensure that styles further down the list will override any preceding styles.
When you can't rely on that manual ordering, you need to be more disciplined about how you use classes.
I don't like relying on source order anyway. I usually tend to avoid applying classes from different files to the same HTML element, especially if those classes both provide a rule for the same property. But occasionally you'll want to do something like this:
<div class="footer u-clearfix">
In that case, it becomes more important to explicitly increase the specificity of those selectors that you always want to apply. SUIT recommends liberally using !important
for utilities, or you might want to try the duplicated class hack.
No global Sass context
If you're used to building your bundles using Sass @import
, you're probably also used to a shared global context in Sass. You might do something like this...
@import "config/variables"@import "mixins/vertical_align"@import "modules/header"@import "modules/footer"
... so those variables & mixins are available to any modules imported subsequently.
In the Webpack approach, each Sass file is compiled in isolation. This isn't a new idea, but I think it's a much better way of doing Sass. It just means you need to @import
dependencies like variables & mixins wherever you use them:
// header.sass@import "config/colors".headercolor: $red
// footer.sass@import "config/colors".footercolor: $blue
Small, isolated files with explicitly declared dependences are, in my opinion, a Very Good Thing™.
Try it out!
I've created a simple example repo on GitHub that you can play around with.
Alternate versions of this post
- 中文翻译 (Chinese translation) thanks to xunuo!
- Screencast thanks to the fine folks at Webucator!