!!! DISCLAIMER !!!
Dear reader,
Keep in mind that this article was written some time ago and uses Webpack v3. The current state of Webpack may have significantly changed.
We highly recommend you to use create-react-app
, in case you want to develop with React. It's a great well-maintained tool that will handle most of the Webpack use-cases that you might face.
This article is still a good source that may help you understand some of the basics behind Webpack and how it can be used along with React.
Best of luck!
What is this article about?
Recently we’ve started a new SPA (single-page application) project with Redux, React, and React Router (v4).
Until now I’ve been working mainly on the backend part of apps so this was a new challenge. In this blog post, I will share my knowledge with you.
TL;DR You can check this demo project that uses the Webpack configuration that we are going to achieve.
The first thing we had to think of was how do we build and serve the app? We decided to use Webpack to bundle everything.
$ npm i -s webpack
In the following article, I will talk about the struggles we had and share some tricks we’ve learned during the process. In the end, we’ll have a ready-to-use webpack configuration.
NOTE: I’m going to write only about the Webpack configuration in the article. Yes, I will connect it with Redux + React + Router but it is not the main purpose. We will blog more about them in the future but this post is targeted mainly to Webpack.
What is “Webpack”?
This had been maybe the first question (and most likely yours, too) that I had had before I dived into the frontend world.
Webpack is a module bundler for modern JavaScript applications (from the docs).
In more simple words, webpack takes all files and modules (JavaScript, CSS, images) that your app needs and bundles (mixes) them into a smaller amount of assets – usually just one. That's what's being served on the browser later.
You can use it as a dev-server and as an engine for building your code. I will show you how to do so after a while.
Once upon a time
In the past we used to include the JS dependencies in our app using the <script>
tag. What was the most disturbing? We had to follow a strict order of including the files…:
<script src="jquery.min.js"></script>
<script src = "jquery.debug.js"></script>
<script src="main.js"></script>
It was really slow because of the overhead of HTTP requests. Also, strict ordering is something that can be easily messed up even by an experienced programmer.
The Dependency graph
The core concept of the webpack is the so-called dependency graph. You can check this article for a more detailed and scientific explanation.
TL;DR It’s a structure that represents the dependencies between several objects.
In the JS world, it allows us to use the require()
keyword so we can build small files where we can separate our code. No more strict ordering of <script>
tags!
The problem is that the browsers don’t support require()
. That’s where we use tools such as Webpack in order to transform the files and give the browser one bundled file which it can understand.
Configure it!
Webpack makes this process of decomposition highly configurable. A lot programmers have the belief that this process is very hard and complicated but let me give you the basics and maybe you will see the bright light at the end of the tunnel.
Your rules have to be placed in webpack.config.js
(it should be situated in the main directory of your app). They are described in JSON
format and represent the so-called Webpack configuration object.
The main structure
Once we have the webpack.config.js
file Webpack will use it to build its dependency graph that is used to bundle the app. The Webpack configuration object can be a JS object or a function that returns such object.
The most important thing is to export your configuration at the end of the file!
Let’s start configuring!
There are a lot of properties that you can add to your Webpack configuration object (see the docs) but there are 4 that you will always need:
- entry
- output
- loaders
- plugins
The entry property
entry
is used to define which is/are the main file/files of your application.
For example, if you are building a Redux app you may have a file index.jsx
where you’ve declared your store and your main <App \>
component. Then the entry
will look like this:
module.exports = {
entry: './path/to/index.jsx'
};
You may want to declare more than one file. For example, you want to give all your main React components as an entry
. You can do so by passing a list of paths as a value to the entry property
:
module.exports = {
entry: [
'./path/to/ComponentA.jsx',
'./path/to/ComponentB.jsx',
'./path/to/ComponentC.jsx'
]
};
Don’t hardcode the paths
Before we continue, it’s pretty common to use the path
module instead of hardcoding the paths. This way you can easily reuse your configuration. That is the basic usage of the module:
var path = require('path');
var APP_DIR = path.resolve(__dirname, './src');
module.exports = {
entry: APP_DIR + '/index.jsx'
};
Since the webpack.config.js
file lives in your main directory the path.resolve(__dirname)
is resolved to it. The second argument './src'
is concatenated to it. You can give whatever you like but that’s where my source files live.
The output property
output
is used to define where you want your app to be bundled. You can specify several files but in most cases, you will give just one:
var path = require('path');
var APP_DIR = path.resolve(__dirname, './src');
var BUILD_DIR = path.resolve(__dirname, './dist');
module.exports = {
entry: APP_DIR + '/index.jsx',
output: {
path: BUILD_DIR,
filename: 'bundle.js',
}
};
This tells Webpack to create a directory called dist/
(or whatever you want to name it) in your main app directory and bundle everything in a file called bundle.js
.
The loaders
What do loaders really are and why do we need them?
Well, loaders
in Webpack transform non-JavaScript files (.html
, .jsx
, .css
, .sass
, .png
, etc.) into valid JS. They replace the require()
of the static asset into a URL string. If the file is an image, for example, they put it in the dist/
folder (the one specified in the output
).
They do so because Webpack only understands JavaScript and cannot add other file types in the bundled file.
Loaders
are responsible to put these files in the dependency graph.
Let’s add them to the webpack.config.js
Loaders
have two main properties (this may vary depending on your Webpack version, but I highly recommend you to use the latest one):
test
– where you specify what kind of files this loader must transform (usually by a regex)use
– where you specify exactly which loader is used (you must install it before that!)
Of course, loaders
have other properties, too, (e.g. include
where you specify which files to be targeted by the loader) but you will mainly use these two.
They are defined under the module.rules
property of your Webpack config object:
var path = require('path');
var APP_DIR = path.resolve(__dirname, './src');
var BUILD_DIR = path.resolve(__dirname, './dist');
module.exports = {
entry: APP_DIR + '/index.jsx',
output: {
path: BUILD_DIR,
filename: 'bundle.js',
},
module: {
rules :[
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.scss$/,
use: [ 'style-loader', 'css-loader', 'sass-loader' ]
},
{
test: /\.jsx$/,
use : 'babel-loader'
}
]
}
};
$ npm i -s style-loader css-loader sass-loader babel-loader
As you may have already noticed, my entry index
file has .jsx
extension. I use babel-loader
to transform to make webpack be able to understand it. You have to add .babelrc
file to describe how you want your .jsx
files to be treated but check Babel docs for more.
$ npm i babel-loader babel-core babel-preset-env
The plugins property
Plugins
are used to perform some custom actions on your bundled modules. As loaders, they have to be installed in your node_modules
in order to be used.
For example, a pretty useful plugin I found is called HtmlWebpackPlugin
. It’s installed via npm and just creates a .html
file in your bundler directory and injects the bundled output file in it by a <script>
tag.
$ npm i -s html-webpack-plugin
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var APP_DIR = path.resolve(__dirname, './src');
var BUILD_DIR = path.resolve(__dirname, './dist');
module.exports = {
entry: APP_DIR + '/index.jsx',
output: {
path: BUILD_DIR,
filename: 'bundle.js',
},
module: {
rules :[
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.jsx$/,
use : 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: APP_DIR + '/index.html'
}),
]
};
This will generate a dist/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
Another usage of plugins – you may want to minify your big bundle.js
file for production. It’s easily done by using the UglifyJsPlugin
from webpack:
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');
var APP_DIR = path.resolve(__dirname, './src');
var BUILD_DIR = path.resolve(__dirname, './dist');
module.exports = {
entry: APP_DIR + '/index.jsx',
output: {
path: BUILD_DIR,
filename: 'bundle.js',
},
module: {
rules :[
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.jsx$/,
use : 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: APP_DIR + '/index.html'
}),
new webpack.optimize.UglifyJsPlugin({ minimize: true })
]
};
A problem just occurred
Oops! We just minified the bundle.js
file which is not that useful for development and debugging… What can we do? We obviously need more than one webpack.config.js
file. You can check the solution I decided to take in my next article – “Split webpack configuration for development and production”.
Some extra properties
Well, that’s pretty much everything you need for a working webpack configuration
. Here are some useful properties I also use.
The resolve property
resolve
is used to control how your imports are resolved. For example, if you want to import the Todos
(using EC2016) component in your file you can: js import { Todos } from './path/to/component';
And you don’t need to specify the extension of the ./path/to/component
file. Here is how to use it in the webpack.config.js
:
module.exports = {
// rest of the configuration
resolve: {
extensions: ['.js', '.jsx']
}
};
Must-haves for React Router
$ npm i -s react-router react-router-dom
As I told you in the beginning, we decided to use React Router v4 for our app. In order to make it possible and to use it in the browser, there are some configuration details we had to add to webpack.config.js
. First of all:
module.exports = {
// rest of the configuration
devServer: {
historyApiFallback: true
}
};
This will allow us to access the routes from the browser search. Otherwise, we will get the nasty Cannot resolve /url
error.
The other think we need to specify is the publicPath
in the output
property:
module.exports = {
// rest of the configuration
output: {
// old "output" configuration here
publicPath: '/'
},
devServer: {
historyApiFallback: true
}
};
What have we just done? Firstly, you had better check the publicPath documentation. In very simple words, publicPath
is used to define from where you want to load images, external files, etc.
In most cases, you will set it to '/'
like we do for the React Router. It’s also really helpful if you are using a CDN to host your assets. In this case you won’t set '/'
as a publicPath
– it will be the CDN’s URL.
Develop your app
We have a ready and working Webpack – it’s time to use it! The most common way to do so is to add scripts
property to your package.json
and use it via npm.
That’s how a simple command that runs your app looks like:
{
"scripts": {
"build": "webpack -d",
}
}
Now you can easily build your app by using npm run build
. This will make a dist/
directory in your main app directory and it should contain the bundled bundle.js
file (and index.html
if you’ve used HtmlWebpackPlugin
).
Serve your app
For development we don’t only want to build the app – we want to serve it. The most common approach here is to use webpack-dev-server + webpack-livereload-plugin. It gives you live reloading of the page when a change occurs and must be used only for development.
$ npm i -s webpack-livereload-plugin webpack-dev-server
Add the livereaload-plugin
to your webpack plugins:
plugins: [
new HtmlWebpackPlugin({
template: APP_DIR + '/index.html'
}),
new LiveReloadPlugin(),
new webpack.optimize.UglifyJsPlugin({ minimize: true })
]
Add the following script to the package.json
:
{
"scripts": {
"build": "webpack -d",
"serve": "webpack-dev-server -d --open"
}
}
Now you can serve your application by npm run serve
(remove --open
flag if you don’t want your default browser to be opened automatically when you use it).
Conclusion
That’s pretty much everything you need as a base to write your own Webpack configuration. Go deeper into its wide documentation and get your application working!
Here is the final state of the webpack.config.js
we just did:
var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var LiveReloadPlugin = require('webpack-livereload-plugin');
var APP_DIR = path.resolve(__dirname, './src');
var BUILD_DIR = path.resolve(__dirname, './dist');
module.exports = {
entry: APP_DIR + '/index.jsx',
output: {
path: BUILD_DIR,
filename: 'bundle.js',
publicPath: '/'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules :[
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.jsx$/,
use : 'babel-loader'
}
]
},
devServer: {
historyApiFallback: true
},
plugins: [
new HtmlWebpackPlugin({
template: APP_DIR + '/index.html'
}),
new LiveReloadPlugin(),
new webpack.optimize.UglifyJsPlugin({ minimize: true })
]
};