(I've reposted this on my blog, which you may find more pleasant to read: http://devlog.disco.zone/2016/06/01/webpack/)
I was asked on Twitter why I think Webpack is the right approach to build tooling in JavaScript applications. My explanation is, uh, a bit longer than fit in a single tweet.
When I say "right approach," I'm specifically talking about the way Webpack's pipeline functions. There are certainly some deficiencies in various aspects of Webpack: it has a rather unintuitive API, and often requires quite a bit of boilerplate to set up. However, even with these issues, I think the core principles of how Webpack functions are sound.
I should also mention here this argument basically applies to SystemJS as well. I'm skeptical of various aspects of SystemJS, but I've only taken a very surface-level look at it, so I'm gonna withhold judgement until I've had a chance to work on a project with it.
So, pipelines. Webpack defines itself on its website as a "module bundler," but this sells it rather short. Webpack is, at its core, a pipeline that takes a JavaScript file as its input and outputs an entire application.
This is fundamentally different than how a build tool like Grunt, Gulp, or Broccoli functions. for example, using the example Gruntfile from Grunt's documentation:
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
dist: {
src: ['src/**/*.js'],
dest: 'dist/<%= pkg.name %>.js'
}
},
uglify: {
dist: {
files: {
'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
}
}
},
});
// ...
}
We can see that there's a task to take a list of JavaScript files to concatenate and output to a dist/
folder, and then another task that takes that output and minifies it to yet another file. This pattern of defining inputs and outputs as files and directories is cumbersome and difficult to scale.
Broccoli attempts to solve this problem, to an extent. In Broccoli, you define an initial tree or file and an output tree, and imperatively run tasks to get your final output. This allows you to skip defining intermediary representations of the tree, which is managed by Broccoli. This is certainly more powerful and expressive than Grunt or Gulp's task definitions.
However, while Broccoli is powerful, it's complex. The Broccoli sample app's Brocfile is a good example of this. You're still defining trees of source files as inputs, and running various tasks on these trees to get your final output. The imperative nature of Brocfiles make them difficult to analyze and understand at a glance.
Unlike Grunt, Gulp, or Broccoli, Webpack opts to use a JavaScript application as its only input. Instead of defining trees and running tasks based on them, you simply define "loaders" for various assets (in other words, file extensions). Instead of requiring you to define your non-JS assets in a build file, you can define these assets in the source itself.
For example, here's a simple Webpack config that can compile CoffeeScript, SASS, and bundle png
images referenced in your JS or CSS:
module.exports = {
entry: {
app: './src/main.coffee',
},
output: {
path: './build/',
},
resolve: {
extensions: ["", ".coffee", ".js"]
},
module: {
loaders: [
{
test: /\.coffee$/,
loaders: ['coffee']
},
{
test: /\.sass$/,
loaders: ['css', 'sass']
},
{
test: /\.png$/,
loaders: ['file']
}
]
}
}
Note that I didn't have to define where CoffeeScript or SASS originate or end up - they're part of the same "tree" as the rest of my application. I just had to tell Webpack which file extensions map to which files, and that it should be able to require()
CoffeeScript files.
Now, in my CoffeeScript, I can do something like:
# main.coffee
require './styles/app.sass'
And the application will import my SASS files into my bundle. By default, this inlines the SASS code into my bundle, but a little extra configuration will split it out into a separate CSS bundle, if I desire.
Now, this, on its face, may not seem that powerful. But when you start getting even slightly complex, this becomes super useful. For example, let's say my SASS uses a few images in an assets/
folder:
body {
background: url('../../assets/my-cool-bg.png');
}
In Broccoli, if you dropped this into your SASS file, it obviously wouldn't work out of the box unless your served asset paths matched your source's directory organization, which is pretty unlikely. Instead, you'd have to write a Broccoli task that copied and processed your images into an output folder, then update the paths in your SASS file to match the path when served.
However, with Webpack's CSS loader (which you'll note we chained the SASS loader into) and file loader (which is configured to bundle PNGs), that source just works. It will copy your image into the build
folder (with a unique SHA filename), then rewrite the url()
to be the new file path.
This is awesome! And this applies to all sorts of assets. For example, in a music game engine I worked on, a song's configuration file just requires its MP3, and that plus adding a couple characters to a file-loader configuration was all I needed to bundle MP3s in my application with proper paths.
In another game I made that had level data, I used raw-loader, which, as the name implies, just bundles a file as raw data inside your application bundle, to load some special level files which were parsed and loaded at runtime.
Again, you could totally do all this with Broccoli or other build tools, but you would have to define sources and outputs instead of just a single bundle, adding additional configuration overhead.
So, that's the Webpack "approach" that I like so much. Your entire application is the input, not its individual components. And again, Webpack isn't alone in this concept, SystemJS uses the same principles.