Even though It's not that difficult, webpack could be so intimidating.
We just need to understand a couple of things.
At the end of the post you will be able to fully understand how to write a webpack.config.js
file.
This is the full repo: https://github.com/maximomartinezsoria/webpack-boilerplate
Feel free to check it out if you feel lost at any point.
Let's get started.
Setup
First of all, we'll need to create a new folder and initialize git and npm.
We'll also create a src
folder with a javascript file where our code is going to live.
# create a new folder and move into it
$ mkdir webpack-boilerplate
$ cd webpack-boilerplate
# Initialize git and npm
$ git init
$ npm init
# create src folder, move into and create a file
$ mkdir src
$ cd src
$ touch index.js
Then, we'll install webpack
and webpack-cli
. The first one, is the core library and the other will help us interact with webpack.
# i: install
# -D: development dependencies
$ npm i -D webpack webpack-cli
Finally, let's create a configuration file at the same level of package.json
.
$ touch webpack.config.js
You should finish with somthing like this:
- webpack-boilerplate
- node_modules
- ....
- src
- index.js
- package.json
- package.lock.json
- webpack.config.js
Getting into Webpack
Let's work a little bit in webpack.config.js
file.
// webpack.config.js
const path = require('path')
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].js',
},
}
This is a node file, so we are using commonjs instead of es6 import / export syntax.
Entry, is where Webpack is going to look for the code.
From now on, if you want a code to pass through Webpack, you'll need to import it in that file.
CSS, preprocessors, and other sort of files could be imported as well. Just need the proper loader.
Output, is where Webpack is going to drop the resultant files.
Notice that we are using a placeholder. This one will be replaced with the name of the file by Webpack.
Giving it a shot
We need to create some commands in order to start using webpack.
// package.json
{
...
"scripts": {
"dev": "webpack"
}
...
}
As simple as that.
$ npm run dev
The command will throw you a warning message saying that you should specify the mode
option.
Go ahead and add a flag to the command specifying either development
or production
.
We are going to create specific configurations for both modes later, so it's not important right now.
Loaders and Plugins
Loaders allow us to load and pre-process different kinds of files, and plugins extends their possibilities.
I know that this doesn't make much sense right now, but you'll understand soon. Let's go to the code.
Handle HTML
As you now know, the entry point and main file of the app is index.js
.
As you also know, browsers need html files. At least one.
The first thing that we are going to do, is to export a html file.
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].js',
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
}),
],
}
Remember to install the plugin.
$ npm i -D html-webpack-plugin
Babel for modern JavaScript
Not all people use modern browsers. That's why we need Babel to transpile our modern JavaScript into ES5.
First, we need to install some dependencies:
$ npm i -D babel-loader @babel/core @babel/preset-env
As I told you, loaders allow us to load different kinds of files.
This time we have babel-loader
, which loads JavaScript files into @babel-core
, which transpiles our modern JavaScript into ES5 using the rules defined in @babel/preset-env
.
After that little explanation, let's go to the code.
In order to use a loader, we need to use the key module
and set an array of rules inside it.
Each rule has some configurations. In this case we are using the followings:
- test: tells Webpack which kind of files is this rule for.
- use: which loader will be applied in this rule.
- exclude: we are excluding the
node_modules
folder to improve performance.
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
]
},
plugins: [
...
],
}
Ok. We are asking Webpack to use babel. Now we need to tell babel what to do.
All babel configurations need to be set in a specific file called .babelrc
in the root folder.
In addition to presets, we can use plugins to increase presets functionality.
In this case, lets install @babel-plugin-transform-runtime
, which allows us to use async/await.
We are also installing @babel/runtime
. It's needed by the plugin and it's highly recommended to install it as a production dependency.
$ npm i -D @babel/plugin-transform-runtime
$ npm i @babel/runtime
// .babelrc
{
"plugins": [
"@babel/plugin-transform-runtime"
],
"presets": [
"@babel/preset-env"
]
}
Styles
Every website or app needs styles.
As you already know, loaders allow us to import different kinds of files into webpack.
Does that mean that we can import css into JavaScript? Yes, it does.
It sounds weird and a little bit crazy, but importing css in JS is how we include our styles into the bundle.
$ npm i -D style-loader css-loader
Importing css into JavaScript is actually weird. That's why we need a couple of things.
css-loader
is the one who handle the importing, and style-loader
injects css in the html file that we've already generated with HtmlWebpackPlugin
.
If you want to use a preprocessor, you need one more loader to handle that file and the specific preprocessor library itself.
SASS / SCSS
$ npm i -D sass-loader node-sass
STYLUS
$ npm i -D stylus-loader stylus
LESS
$ npm i -D less-loader less
The code is quite similar than the last time. We are just adding more rules.
// webpack.config.js
...
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
],
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
],
},
{
test: /\.styl$/,
use: [
'style-loader',
'css-loader',
'stylus-loader'
],
},
...
]
}
...
Let's recap.
First, we've used a specific loader for a preprocessor, which turns the code into css. Then, css-loader
handles that code and finally style-loader
injects css into html.
Remember to import these files into your main JS file.
// index.js
import './index.css'
import './index.scss'
import './index.less'
import './index.styl'
Images, videos and fonts
In order to use images, videos or fonts in our css, we need to use a specific loader.
In this case, we are using an object in the use
key. This allows us to use additional configurations like the output path.
$ npm i -D file-loader
// webpack.config.js
...
module: {
rules: [
...
{
// '?' means that 'e' is optional. So we can use jpg or jpeg
test: /\.jpe?g|png|gif|woff|eot|ttf|svg|mp4|webm$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'assets/'
}
},
},
...
]
}
...
Development vs Production
We've already learned a lot about Webpack. We are able to use lots of files as well as modern Javascript and CSS preprocessors.
But the real power of Webpack is to make a better developer experience in development mode and a better user experience in production mode. That's why we are going to make two different configurations from now on.
Development
Let's create a new configuration file for development purposes.
The standard name is usually webpack.dev.config.js
, but you can use anyone.
We need all the things that we've done before. So I'm going to rename the file that I was using, as oposed to create a new one.
Since we are working in development mode, let's get Webpack to know that, using the mode
key.
//webpack.config.js
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].js',
},
mode: 'development',
...
}
It's also time to change our package.json
script.
// package.json
...
"scripts": {
"dev": "webpack-dev-server --config ./webpack.dev.config.js"
},
...
The config
option defines the path to the config file.
Let's talk about webpack-dev-server
.
Development server
The first and one of the most important things that a developer needs is a local server.
That's what we use webpack-dev-server
for.
$ npm i -D webpack-dev-server
We also need to write the configuration for the server.
//webpack.config.js
module.exports = {
...
mode: 'development',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
open: true,
},
...
}
contentBase
defines the directory where is going to look for the files and open
in true, means that when we run the command Webpack is going to open a browser automatically.
From now on, Webpack is going to create a new bundle every time you save a file.
Production
Our development configuration is done.
But for a better user experience we can change some things for production.
As we are going to change lots of things, let's create another file and make a new script. I'll call it webpack.config.js
.
// package.json
...
"scripts": {
"dev": "webpack-dev-server --config ./webpack.dev.config.js",
"build": "webpack"
},
...
As long as you use the default name (webpack.config.js
), config
option is not required.
We can reuse some options that we've already written, so my new file it's just a duplication from webpack.dev.config.js
.
Some important changes that we need before getting started are:
- mode: set to
production
. - devServer: remove the entire object. We don't need a local server in production.
- output: is a good practice to use hash in production files in order to avoid cache problems.
...
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[hash].js',
},
mode: 'production',
..
Now we are ready to start.
Extracting styles
The first difference between development and production is how styles are handled.
In development, we inject the css in order to accelerate things. But in production we need a css file. That's why, we are using a new loader that will extract the css into a new file.
$ npm i -D mini-css-extract-plugin
So, is it a plugin or a loader? Actually, both. You'll see.
We need to pass it as a plugin to define both the output filename
and the chunkFilename
.
The hash
placeholder, allows us to avoid cache problems.
We need to pass it as a loader also. And, as you can see, we are not using style-loader
any more.
// webpack.config.js
const MiniCSSExtractPlugin = require('mini-css-extract-plugin')
...
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCSSExtractPlugin.loader
},
'css-loader',
],
},
{
test: /\.scss$/,
use: [
{
loader: MiniCSSExtractPlugin.loader
},
'css-loader',
'sass-loader'
],
},
{
test: /\.less$/,
use: [
{
loader: MiniCSSExtractPlugin.loader
},
'css-loader',
'less-loader'
],
},
{
test: /\.styl$/,
use: [
{
loader: MiniCSSExtractPlugin.loader
},
'css-loader',
'stylus-loader'
],
},
...
]
},
plugins: [
...
new MiniCSSExtractPlugin({
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[id].[hash].css'
})
],
}
So, let's recall.
We are using css-loader
or the specific loaders for preprocessors to allow importing those files into JavaScript. Then, we use MiniCSSExtractPlugin
to extract css and create a file with it. Finally, that new file will be linked to the html automatically.
Cleaning
We are almost done. Let's add two more fixes.
The first one is related with hashes. As you know, we are using hashes everywhere to avoid cache problems. But now we have a problem with hashes. It's a bug or a feature?
The problem is that we are creating new files every time we run Webpack, but we are just using the last one. So, if you run a couple of times the command and see the css
or js
folder, you are going to find lots of garbage files.
It would be great to erase all of those files before each run of Webpack. Let's do it.
$ npm i -D clean-webpack-plugin
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
...
plugins: [
...
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*']
})
]
...
That does exactly what we want. It cleans the whole dist
folder before each bundle creation.
Note that you can modify the pattern (**/*
) to match just the things you want. This might be useful when you are using dll for example.
Minify CSS
The second fix is that css is not being minified.
Minification process removes all unnecessary spaces in the file so it becomes smaller.
$ npm i -D optimize-css-assets-webpack-plugin
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
...
output: {
...
},
optimization: {
minimizer: [ new OptimizeCSSAssetsPlugin() ]
},
...
optimization
is a new key that we didn't use before. I recommend you to take a look at the docs to know more about this key.
The perfect configuration
since this is a very general configuration, we're done.
But this isn't the perfect Webpack configuration. Actually, that doesn't exists.
Each project has its own configuration so you can do whatever you need.
I'm preparing another example of Webpack configuration for React. Stay tuned.
Top comments (0)