Webpack - Bundling Frontend & Fullstack React Applications.

What is Webpack?

Webpack is a module bundler we can use to bundle our Node projects, such as any app you have been making in React. When you write applications using Node you are essentially writing modules every time you create a new file. Webpack takes all of these modules we write, and transforms it into code browsers can read and execute.

As you may have learned by now, trying to load Node applications by either double-clicking the index.html page, or trying to open it with a live-preview, will fail. This is because modules need to be bundled back together into a cohesive set of javascript, css, and HTML to be read by browsers. In essence, webpack's job is to take our project, (which may include hundreds of files), and compress it down into a index.html and main.js.

Webpack is HIGHLY configurable, so we will be sticking to a boiler plate setup that will allow you to make front-end/full-stack applications using all the skills and modules we teach at V School.

We will only be using webpack for our front end since our backend doesn't need to be bundled for a web browser to read. However, know it can be used on the backend as well to make syntax such as import/export available rather than having to use the Node require.


Dependencies

The following are the needed dependencies used in this write-up. These are all the exact names you would use in an npm i --save-dev. Note, all of these should be installed with the --save-dev flag as they are dependencies only needed for development purposes:

  • webpack: The webpack node module
  • webpack-cli: The command line tool we use to interact with webpack.
  • webpack-dev-server: Used to set up our live dev-server like the one in create-react-app.
  • babel-loader: Loader used to compile es6 down to javascript browsers can read. The next 3 dependencies are all used to configure babel-loader for compiling our javascript.
    • @babel/core
    • @babel/preset-env
    • @babel/preset-react
  • html-webpack-plugin and html-loader: Used together to allow webpack to generate HTML pages.
  • css-loader: interprets @import and url() like import/require() and will resolve them.
  • style-loader: Allows for import ‘./style.css’ syntax.
  • file-loader: Allows for imports and use of files such as .png and .jpg. (.json is already understood by webpack)
  • @babel/plugin-proposal-class-properties: allows for Es6 class syntax and => functions on class methods for auto-binding.

React Dependencies:
- react
- react-dom
- prop-types


Setting up a Node project

To begin, open the console and create a new directory called wpack-intro. cd into wpack-intro and run an npm init -y for a package.json:

myconsole/mkdir wpack-intro
myconsole/cd wpack-intro
myconsole/wpack-intro/
myconsole/wpack-intro/npm init -y

Now let's create a src folder and a .gitignore inside of this directory.

myconsole/wpack-intro/mkdir src
myconsole/wpack-intro/cd src

Your directory should now look like this:

|- wpack-intro
   |- src
   |- package.json
   |- .gitignore

In the .gitignore, add node_modules.

Now install webpack and the webpack-cli with the following command. These modules are for using and interacting with webpack using our console:

npm i --save-dev webpack webpack-cli

Next, create the file needed to write and configure your webpack settings. On the same level of your src folder, create a webpack.config.js file.

Your directory structure should now look like this:

|- wpack-intro
    |- src
    |- node_modules
    |- .gitignore
    |- package.json
    |- package-lock.json
    |- webpack.config.js

Entry and Output

The webpack.config.js file is where our webpack configuration is written. It's here where we teach webpack where to get our code, how to handle different file types (css, js, html, jpg, etc.), and where to put the finalized bundle of code when webpack is done doing it's job. We'll start with our entry and output

// webpack.config.js //
module.exports = {
    entry: '',
    output: {}
}

Entry expects a path to where we want webpack to enter our project for bundling. Convention for this is an index.js, so create a index.js inside of your src folder, then put the string path ./src/index.js as the entry in the webpack.config.js This is the same top level index.js file that you have when you use create-react-app.

Output is where we configure the location and name of the file that webpack will output when it's completed. The standard for this is a dist directory and a main.js file that will hold our bundled code. Update your webpack.config.js to look like the following:

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    }

We have to require('path') as it is not available to use by default. In the output we are telling webpack to create a directory name called dist, and to put the final main.js file inside of it. Note, we do not have to create a dist folder or main.js, webpack will do this for us.

Note: These entry and output settings are the new webpack default, so if you are going to want these settings as they are, you do not have to include them in your webpack.config.js file as webpack will look in for a ./src/index.js and create the /dist/main.js automatically.


package.json scripts

Before we continue with configuring our webpack, we need to add two scripts to our package.json file. Change the scripts section of your package.json to reflect the following:

"scripts": {
    "start": "webpack-dev-server --open --mode development",
    "build": "webpack --mode production"
  },

This allows us to now use npm start to start up a development server( just like create-react-app), and use npm run build to have webpack bundle our code.


Module & Rules

The module section of our webpack is where the majority of our code will go. Inside of module we will define a set of rules that webpack will use when bundling our code. The first rule we will define is our babel-loader, which will teach webpack how to bundle our .js files into javascript the browser can read.

Do a npm i babel-loader --save-dev, and then update your webpack.config.js file to look like the following:

const path = require('path') 

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: { loader: "babel-loader" }
            }
        ],
    }
}

module is an object that accepts a rules key, and rules is an array of objects that define different rules about our webpack. Let's look at the different parts:

  • test(required): This is a regular expression /\.js$/. The $ means this will look for any file that ends with .js, and then use babel-loader when bundling it. The test part of loaders is required so that the loader knows what file-type it is for.
  • exclude(optional): This tells the rule to NOT to check any .js files inside of the node_modules folder.
  • use(required): Here we define which loader this rule should use.

The babel-loader needs some configuration to work with all our js syntax and react code. For this, we will install three more node modules which we can use to configure this loader to work as we need.

Now run npm i @babel/core @babel/preset-env @babel/preset-react --save-dev

These 3 packages are used by babel-loader to read and compile our Javascript from Es6 and React, to a readable Javascript for browsers.

.babelrc

To implement this configuration for your babel-loader, create a file on the same level as your webpack.config.js and name it .babelrc. In the .babelrc file, add the following preset instructions:

{
    "presets": ["@babel/preset-env", "@babel/preset-react"]
}

The .babelrc is the babel-loader configuration file. Typically you do not need much in this file, but let's add one more part here that we'll need later.

Run an npm i @babel/plugin-proposal-class-properties --save-dev, and then update the .babelrc to include this as a plugin.

{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["@babel/plugin-proposal-class-properties"] 
}

This second package is used to configure babel so that we can write fat arrow functions for our methods inside of React classes. Without this package, fat arrow functions do not auto-bind this in class components, so binding in the constructor would still be needed.


HTML, CSS, and Other File Types

Next we need to implement the ability to bundle CSS, produce HTML, and handle other file types such as images.
To begin, run the following npm i's:

npm i html-webpack-plugin --save-dev
npm i html-loader --save-dev
npm i css-loader --save-dev
npm i style-loader --save-dev
npm i file-loader --save-dev

With these modules, we can now set up our rules for html, css, and other files such as .jpg or .png. Add a rule for /\.css$/, /\.html$/, and /\.(jpg|png|jpeg|gif)$/ files, making your webpack.config.js look like the following.

const path = require('path') // needed if using path.resove() from node

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: { loader: "babel-loader" }
            },
            {
                test:  /\.html$/,
                use: { loader: "html-loader" }
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.(png|jpg|gif|jpeg)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            outputPath: './resources/imgs',
                            name: '[name].[ext]'
                        }
                    },
                ]
            }
        ]
    },
}

The css-loader and html-loader rules are pretty self-explanatory as they look almost exactly like our babel-loader rule. However the file-loader has a few more configurations we've added.

First, the regular expression /\.(png|jpg|gif|jpeg)$/ means to look for any file that begins with a ., and ends in either a png, jpg, gif, or jpeg. The | symbol is an or statement. An added options object is given to tell webpack to put the image files it finds into their own directory called resources/imgs, and name the files by their name and extension. This means, if we have a test.png file in our development directory, webpack will create a resources/imgs directory structure inside of the dist folder, and place the images in there naming it what they are. So test.png would still be test.png when bundling was done.


Plugins

Next we need to require html-webpack-plugin and place it in our webpack config so that webpack can generate the needed index.html when it does the bundle process. Add const HtmlWebPackPlugin = require("html-webpack-plugin") to the top of your webpack.config.js file. Then, we will add this as a plugin in our webpack file. The plugins section of our webpack will go after the module section like this:

const HtmlWebPackPlugin = require("html-webpack-plugin")
const path = require('path') 

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: { loader: "babel-loader" }
            },
            {
                test:  /\.html$/,
                use: { loader: "html-loader" }
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.(png|jpg|gif|jpeg)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            outputPath: './resources/imgs',
                            name: '[name].[ext]'
                        }
                    },
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: "./public/index.html",
            filename: "./index.html"
        })
    ],
}

This plugin shows webpack where to find a .html template file for the html file it will output at the end of its bundling process. Because this plugin requires a template, we now need to add this to our exisiting project directory.

Inside of your project directory, create a public folder and put an index.html inside of it with a standard html boiler-plate. Your directory structure should now look like this:

|- wpack-intro
    |- public
        |- index.html
    |- src
        |- index.js
    |- node_modules
    |- .gitignore
    |- package.json
    |- package-lock.json
    |- webpack.config.js
    |- .babelrc

And the index.html file in your public folder should look like the one we use in create-react-app.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Webpack React</title>
</head>
<body>
    
    <div id="root"></div>

</body>
</html>

We will stick with the id="root" so that our index.js of our react app looks the same when we use the ReactDOM render method. etc:

ReactDOM.render(<App />, document.getElementById('root'))

Building & Running your application

Congrats! You now have a functioning webpack you can use to develop your React application, (or any other node program you write) without having to use create-react-app or other pre-built boilerplates.

Now whenever you want to make a new build of your project, you can run npm run build and see a dist folder appear with your bundled app. You will also run npm start just like you do with create-react-app to start up your live dev-server.

Go ahead a create a small react app with your index.js in the src folder just as you would with create-react-app. Once you have that built and can see your application using npm start, try running npm run build. webpack will run for a few moments, and then a dist folder should appear with an index.html and main.js. That dist folder is your bundled app that is ready for deployment!


Using webpack for a full-stack application

While we won't need webpack for our backend, we will need to add an additional configuration to our webpack.config.js if we are making a full stack app in order to proxy to our own API. First expand your directory structure to be for full-stack. Everything we have made so far will need to go in a client folder, and then we can make our server.js, routes, and models directories along with creating a package.json and installing needed backend dependencies. After moving your files around, this will be your full stack file structure:

|- wpack-intro
    |-client
        |- public
            |- index.html
        |- src
            |- index.js
        |- node_modules
        |- .gitignore
        |- package.json
        |- package-lock.json
        |- webpack.config.js
        |- .babelrc
    |- server.js
    |- routes
    |- models
    |- package.json
    |- package-lock.json
    |- node_modules
    |- .gitignore
    |- .env

With this set-up, we only need to make one addition to our webpack.config.js file, and that is to set up the proxy so that we can make a http request to /data on our front end, and hit the endpoint /data on our backend. For this, we need to add another section called devServer to our webpack config under our plugins, so the final webpack.config.js would look like this:

const HtmlWebPackPlugin = require("html-webpack-plugin")
const path = require('path') 

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: { loader: "babel-loader" }
            },
            {
                test:  /\.html$/,
                use: { loader: "html-loader" }
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.(png|jpg|gif|jpeg)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            outputPath: './resources/imgs',
                            name: '[name].[ext]'
                        }
                    },
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: "./public/index.html",
            filename: "./index.html"
        })
    ],
    devServer: {
        proxy: [{
            context: ['/'],
            target: 'http://localhost:7000',
          }]
    }
}

The devServer section takes a proxy array of objects. We define in the first object a context and target. context is what end point request will trigger this proxy to function, and the target is what webpack dev server should prefix on our request. With this setting, we make an axios request like this:

axios.get('/api/todos')

And webpack uses the proxy to make it request to:

axios.get('http://localhost:7000/api/todos')

The reason that context is an array is so that you can add other request enpoints you would like webpack to proxy for you, such as /api.

Additional Resources: