Introduction

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.

What is this article about?

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 targetted 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 smaller amount of assets - usually just one. That's what later on the browser serves.

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, the 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 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 decompossition 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 in 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 represents 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 in 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 you 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 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 specifi which files to be targetted 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 tranform it in order 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 you 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 occured

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".

Subscribe for our newsletter to not missed it out! :)

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 = {
  // old configuration here
  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 = {
  // ...other configuration here
  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 = {
  // ...other configuration here
  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 in its wide documenation 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 })
  ]
};

Check this demo project to clarify everything.