TL;DR I created a repository with the example here :)
Many people seem to like Webpack and use it for their everyday web bundling process, but I heard from many others that they simple can't comprehend how to use it in the first place. So I had the idea of creating a (very) basic project and write about parts of the config, just HTML, CSS and JS, nothing fancy.
Installation
For this example project I used the following dependencies:
"devDependencies": {
"css-loader": "^0.28.4",
"style-loader": "^0.18.2",
"html-webpack-plugin": "^2.28.0",
"http-server": "^0.10.0",
"webpack": "^3.0.0"
}
Two loader modules to be able to load CSS via JS.
One Webpack plugin, that lets me create HTML files that will automatically have script tags for the created bundles.
A HTTP server, that simply serves static files.
And finally, Webpack itself.
While Webpack and http-server are global Node modules - they are run via the command line - you can install them locally in the devDependencies
, they will be pseudo-global accessible for npm scripts.
"scripts": {
"setup": "npm i && npm run build && npm start",
"build": "webpack",
"start": "http-server -c-1 ./build"
},
npm run build
is just an alias for webpack
, but it works without installing Webpack globally. Same goes for npm start
, which is just an alias for the http-server call.
Basic Config Structure
The Webpack config file, often named webpack.config.js
, is just a JS file which gets executed inside Node.js. It has to export a configuration object.
What this means first and foremost is, that you can basically use all your Node modules in it and write JavaScript as you are used to. This gives you much flexibility for the creation of the config object.
A basic config file could look like this:
const path = require("path");
const HtmlPlugin = require("html-webpack-plugin");
const html = new HtmlPlugin({ template: "./modules/index.html" });
const outputPath = path.resolve(__dirname, "build");
module.exports = {
entry: {
application: "./modules/index.js"
},
output: {
filename: "[name].[chunkhash].js",
path: outputPath
},
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
plugins: [html]
};
Imports and Pre-Config
const path = require("path");
const HtmlPlugin = require("html-webpack-plugin");
const html = new HtmlPlugin({ template: "./modules/index.html" });
const outputPath = path.resolve(__dirname, "build");
First you import the modules you need for the creation of the config object.
Here I import the Node path module and the Webpack HTML plugin.
Next you write the things that need to be done before the config object can be created. You can do this in-line if you like, I just prefer it like this. Often you have many plugins and this can get unwieldy, if they're all created inline.
In the example I create an instance of the HTML plugin and get the absolute path to my output directory.
Creating the Config Object
The next part is the creation of the config object. It has 4 important sections: entry
, output
, module
and plugins
.
Entry
entry: {
application: "./modules/index.js"
},
The entry
tells Webpack where to start. For this you have to understand that Webpack works with a tree structure. It takes one or more entries and looks in these entry files if some other files are imported, this goes down to til no file is importing another anymore.
If nothing different was configured elsewhere, Webpack will create one bundle-file for every entry, only one in this example.
Another reason for more than one bundle-files are dynamic imports. If you use import("my-module").then(myModule => ...)
instead of import myModule from "my-module"
somewhere, Webpack will automatically create additional files, that are imported when import
is called.
Output
output: {
filename: "[name].[chunkhash].js",
path: outputPath
},
Here we configure the names of the bundle files. You can use a fixed name, or some placeholders. I used [name]
and [chunkhash]
.
[name]
will be replaced with either a key from the entry
object, or with a dynamically generated ID. For example if you used dynamic imports, they will be named automatically by Webpack.
[chunkhash]
will be replaced with a hash, that reflects the content of this bundle. Which means it changes every time you changed a file that went into this. This forces every new version into a new bundle file, which helps when you want your users to only download the latest version. In dev mode I often just use [name]
so I won't end up with hundreds of bundles.
The path
has to be a absolute path to your output directory, so I generated it with the help of Nodes path
module on run-time.
Module
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
Here the modules to are defined. Often they just consist of a bunch of rules, which are associated with loaders.
When I import a CSS file, please run it through the style-loader
and css-loader
.
Webpack only knows about JavaScript, so you have to tell it what else you need. Often the loaders implement some mechanisms to embed non-JavaScript assets into JavaScript, or make them accessible from JS.
This can also be used with Babel or TypeScript, so your ES2015, or .ts
files are compiled down to ES5 or something. It even works with LESS, SASS, images, fonts, etc. pp.
It all works on a per file basis, which means, the loader only looks at one file at a time and tries to convert it somehow.
Plugins
plugins: [html]
Plugins can work on multiple files. This allows things like extracting all CSS texts from the bundles and putting them into a separate CSS file or to create a HTML file, that will include all the created JavaScript bundles.
In the example I used [chunkhash]
as part of the bundle file names, which leads to different file names every time I build with changed code.
The HTML plugin allows me to create a HTML file from a template of my liking and fills it with script tags for the bundle files. So every time I run Webpack, I get a new index.html
that already includes the right file names.
Non-Config Factors
Like I mentioned above, the config is not the only thing that influences your output.
If you're using dynamic imports, this leads to implicit code splitting.
In my example, I used 4 pages. Home, about, contact and loading. Home and loading are imported statically, about and contact dynamically.
Webpack can infer from the import("./pages/" + variable)
that the dynamic pages are all inside of ./pages
so it creates bundles for all files in that directory, but without the ones that are already imported statically.
When you access the example on a web-server, you see that the 2 dynamic bundles are only loaded after a link was clicked. Other intermediate bundles Webpack also created as part of its build process are not loaded by the browser.
Remarks
You can run Webpack with webpack --watch
, which will trigger a compilation every time you change a file. Webpack even has its own dev-server, which can be used to improve the dev process even further, with hot module replacements on the fly etc.
You can have multiple config files and run them via webpack --config <name>
or create them based one some environment variables. Some libraries run in development mode by default and require you to do things differently when compiling for production, so often you won't get away with one simple config file.
Conclusion
Webpack is a powerful tool and often hard to grasp, especially if you're just learning JavaScript and suddenly are forced to get the Webpack concepts in your head, but when looking closer, there aren't many of them and even dynamic code splitting and loading is often nicely handled without explicit configuration.