- Published on
Using JavaScript (or TypeScript) in 2021 often involves many (many many many!) layers of build tools between what you write and what gets deployed.
One of the most common tools to build your bundles is webpack
.
Here is an introduction to webpack, including the key features you will need to understand in order to set up and configure webpack.
Table of Contents
Webpack overview
- Webpack lets you bundle assets.
- You give it an 'entry point' (or multiple entry points)
- It builds up a dependency graph, starting from the entry point. This means it finds every file included (such as
import Something from 'something'
,const SomethingElse = require('something-else')
). These can be JS, TypeScript, CSS, images or other assets. - It will then process them, and output them in 1 or more bundles. (Often in your
./dist/
directory). - In a very basic app you might output
index.js
which combines all of your JS, andindex.css
which combines all your CSS. - Config is done via
webpack.config.js
- (or more rarely in
package.json
)
- (or more rarely in
Key topics to understand:
Entry or Entry Point
- This is your
index.js
(e.g. in./src/index.js
). It will probably include other files (such asimport Something from './something.js'
) - You can have multiple entry points. Maybe one for your main app, and also a separate one for your admin side.
There are a few ways to set this up. The most simple is with a simple key:value.
The example below (entry: '...'
) is actually setting it with the name main
.
// Note: this is shorthand for the next ('named entry')
// example with `main` as the name...
module.exports = {
entry: './path/to/my/entry/file.js',
};
You can use named entries (this example is equivalent to the previous example)
module.exports = {
entry: {
main: './path/to/my/entry/file.js',
},
};
Example of using named entries with two different files:
module.exports = {
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js',
},
};
Multiple entries (multi-main entry). This is when you have multiple entries, outputting the same bundle.
module.exports = {
entry: ['./src/file_1.js', './src/file_2.js'],
output: {
filename: 'bundle.js',
},
};
You can also do things such as requiring the output of one entry and using it in another entry (look for dependOn
in their docs).
Output
The output is what webpack should do with your asset(s) once it has bundled it.
The default is to put your JS in ./dist/main.js
and any other assets (images, css etc) in ./dist
You might not like the default output name. You can set this up with the output
config:
const path = require('path')
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
}
That might be useful if you have just one... but if you have multiple entries you will probably want to use properties such as the entry name...
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
filename: '[name].[contenthash].bundle.js',
},
}
There are multiple substitutions that you can use. The previous example used:
[name]
which is the entry name (such asapp
oradminApp
from a previous example). Default name ismain
.[contenthash]
The hash of the chunk, including only elements of this content type
Output path vs publicPath
The output config says where to put the bundled files (often in ./dist
).
But sometimes your code and scripts need to reference them for production. You can use publicPath
(often with filename
) to tell Webpack what url it can be found at (once deployed)
const path = require('path');
module.exports = {
output: {
path: path.resolve(__dirname, 'public/assets'),
publicPath: 'https://cdn.example.com/assets/',
},
};
Loaders
Webpack treats all imports (import ... from ...
or require()
) as modules.
But it doesn't know how to import every kind of module, and this is where loaders come in.
-
Webpack by default supports only JS and JSON. But it is likely you will want to include files such as the following from your entry files:
-
SVG
-
other images
-
CSS
-
Typescript
-
and more
You can set up a loader to tell webpack what to do when it sees an import of that file type.
You add loaders in your webpack config. You have to tell it what files to run a loader on.
- the
test
- usually a regex which if it matches your filename, it will run the loader - the
use
which tells webpack what loader to use. This is often the name of a npm package.
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [
{
test: /\.txt$/, // does the filename end in .txt?
use: 'raw-loader', // use the 'raw-loader' loader
}
],
},
};
If you have const txtData = require('./mydata.txt')
, when webpack sees that it will test the mydata.txt
path against the /\.txt$\
regex (it will match!). As it matches, it will use the raw-loader
loader to process that import.
How to import CSS in webpack with the CSS loader
First you need to add the css loader:
yarn add -D css-loader
then update your webpack config:
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
],
},
};
Configuring loaders
You can add options to loaders like this:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use:
{
loader: 'css-loader',
options: {
modules: true // << options here
}
},
}
]
}
};
Ordering of loaders
- You can chain multiple loaders
- They are run right to left
- loaders are separated by
!
, or an array
examples:
module: {
rules: [
{
test: /\.html$/,
use: ['html-loader', 'ejs-loader'] // with array chaining
}
]
},
Or
module: {
rules: [
{
test: /\.html$/,
use: 'html-loader!ejs-loader' // with ! chaining
}
]
},
In both examples above the ejs-loader
will run first, then it will pass its results to the html-loader
loader to run on that.
Inline loaders
You can also define what loader to use in your imports, such as:
import Styles from 'style-loader!css-loader?modules!./styles.css';
(this uses the chaining via !
)
Other important things to note about loaders:
- Loaders can be synchronous or asynchronous.
- Loaders can do anything you can do in Node
- Loaders can emit additional arbitrary files.
Plugins
At first it can be a bit confusing when it comes to plugins and loaders.
But it can be summarised like this:
- Loaders are used to tell webpack how to import a file type (e.g.
.css
). It just helps import a single file at a time. - Plugins can work on a bundle/chunk, and can modify the entire bundle/chunk. These plugins often work near the end of the bundling process (after loaders).
Plugins are set up in your webpack config by importing it (via require()
, then passing it to the plugins config):
Here is an example of html-webpack-plugin
(which must be included via npm/yarn):
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
You will tend to use the new
keyword with plugins, as you may instantiate it multiple times within your webpack config.
Some examples of plugins that you might see:
DefinePlugin
(official webpack plugin) to pass down env vars to your JS linkProgressPlugin
provides a way to customize how progress is reported during a compilation linkTerser
to minify your JS npm)optimize-css-assets-webpack-plugin
to optimize / minimize CSS assets npmbabel-loader
to transpile your JS with Babel - useful if you want to write modern JS but then use babel to transpile to JS that works with older browsers npmimagemin-webpack-plugin
to compress your image assets github
Mode
You will make use of mode
to set your app to either development
, production
or none
.
It will default to production
.
It will set process.env.NODE_ENV
to the value you provide.
This will use the webpack.DefinePlugin
to set these values.
Using the mode in your webpack config
You can access it by turning your config into functions:
var config = {
entry: './app.js',
//...
};
module.exports = (env, argv) => {
if (argv.mode === 'development') {
config.devtool = 'source-map';
}
if (argv.mode === 'production') {
//...
}
return config;
};
webpack-dev-server & the webpack CLI
Something which seems to often be glossed over in tutorials is the difference between generating your assets for deployment and local dev with webpack-dev-server
.
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload.
What this means is that you can make changes in your code, and your browser will reload certain parts of your JS (without reloading the page), which greatly improves developer experience.
There are a few ways to set this up, but the most common is to use webpack-dev-server
. This is basically the same as building your static assets (like you probably will for production deploys) - but it spins up its own server and handles the module updates.
You would use the webpack-dev-server
just for local development.
Most of the configuration used for the CLI tool & webpack-dev-server
are exactly the same. There are a few which are only for one (like the hot
option which is only for webpack-dev-server
).
Sometimes hot module replacement won't work (you lose state/data etc) - in those cases you can set webpack-dev-server
to reload the entire page (like hitting cmd + r
/ f5
).