July 23, 2019

Easy Custom Webpack Setup for React.js Applications

by Lamin Sanneh

Easy Custom Webpack Setup for React.js Applications

It should be self-evident that web applications keep growing in terms of their capabilities.

Web apps are close to or more powerful than their desktop counterparts. With this power, comes a lot of complexity. For a simple application, some of these complexities include: CSS and JavaScript minification, JavaScript and CSS code concatenation, image loading in JavaScript files, file watching, and auto-compilation. We will get into these in more detail later.

In light of this, several tools have been created to make development and shipping easier and more efficient. One such tool is webpack. There are many contenders in this field, Gulp and Browserify being two. In this tutorial, we will be demonstrating how to setup webpack for a React.js application. The reason we are using webpack is that many major web frameworks use it, including the official React.js compiler, create-react-app. Webpack is in fact the most popular build tool according to the 2018 State of JavaScript survey, as pictured below:

Webpack Popularity

Please find the finished project code in this GitHub repo.

Compilation Requirements of a Simple Web Application

  • Minification: This is the process of reducing code filesize. It is done by removing unnecessary whitespace. Other techniques include renaming functions and variable names.
  • Concatenation: This is the method of combining several files into one.
  • Image loading in JavaScript and CSS files: This is a method used to generate URLs for image files based on their configured location.
  • File watching and auto-compilation: This is a method wherein a specified process will self-run when the contents of a file have changed.
  • Auto-reloading: This goes hand in hand with file watching and auto-compilation. The only extra step it adds is that, after compilation, the page is auto-reloaded.

Summary of webpack Concepts

Webpack works with the concept of entry point and output. The entry and output setting are configured in a file called webpack.config.js. Additional configurations are possible in this file and we will look at some of the common ones.

Entry Point

The entry point is a JavaScript file. It is the main file which will import all other required files. Using JavaScript import syntax, webpack knows how to read this entry file. It will also link up all the other files in there.

Output

This is a single JavaScript file. It will be the total of all the files which webpack has managed to process after reading the entry file. This is usually the script which we will end loading on our webpage using <script src="somepath/output.js"></script>, for example. This process where we end up with a single file is called bundling. The resulting single file is usually called a bundle.

Modules

These are sets of rules which control how webpack will behave. An example would be: which file extensions to consider when concatenating JavaScript code.

Plugins

Plugins add extra capability to webpack to what already exists by default.

Setting up webpack for a Simple Web Application

We will begin with a simple React.js application.

Initialize an npm project using:

npm init -y

Install several npm packages below

npm install --save react react-dom prop-types // react stuff
npm install --save-dev webpack webpack-cli // webpack and it's cli
npm install --save-dev css-loader mini-css-extract-plugin // css compilation
npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-react // es6 and jsx stuff
npm install --save-dev html-webpack-plugin //  inserts output script to index.html file
npm install --save-dev clean-webpack-plugin // to cleanup(or empty) the dist(or output) folder before compilation
npm install --save-dev sass-loader node-sass // sass to css compilation
npm install --save-dev file-loader // loading files, e.g. images, fonts
npm install --save-dev papaparse csv-loader xml-loader // xml, csv and tsvs loading
npm install --save-dev webpack-dev-server // webpack development server

In an empty folder, create a webpack config file with the name webpack.config.js and insert the following content;

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
 mode: "development",
 entry: {
   app: "./src/main.js"
 },
 devtool: 'inline-source-map',
 devServer: {
   contentBase: path.join(__dirname, './'), // where dev server will look for static files, not compiled
   publicPath: '/', //relative path to output path where  devserver will look for compiled files
 },
 output: {
   filename: 'js/[name].bundle.js',
   path: path.resolve(__dirname, 'dist'), // base path where to send compiled assets
   publicPath: '/' // base path where referenced files will be look for
 },
 resolve: {
   extensions: ['*', '.js', '.jsx'],
   alias: {
     '@': path.resolve(__dirname, 'src') // shortcut to reference src folder from anywhere
   }
 },
 module: {
   rules: [
     { // config for es6 jsx
       test: /\.(js|jsx)$/,
       exclude: /node_modules/,
       use: {
         loader: "babel-loader"
       }
     },
     { // config for sass compilation
       test: /\.scss$/,
       use: [
         {
           loader: MiniCssExtractPlugin.loader
         },
         'css-loader',
         {
           loader: "sass-loader"
         }
       ]
     },
     { // config for images
       test: /\.(png|svg|jpg|jpeg|gif)$/,
       use: [
         {
           loader: 'file-loader',
           options: {
             outputPath: 'images',
           }
         }
       ],
     },
     { // config for fonts
       test: /\.(woff|woff2|eot|ttf|otf)$/,
       use: [
         {
           loader: 'file-loader',
           options: {
             outputPath: 'fonts',
           }
         }
       ],
     }
   ]
 },
 plugins: [
   new HtmlWebpackPlugin({ // plugin for inserting scripts into html
   }),
   new MiniCssExtractPlugin({ // plugin for controlling how compiled css will be outputted and named
     filename: "css/[name].css",
     chunkFilename: "css/[id].css"
   })
 ]
};

Input JS File

Create an input JavaScript file in src/main.js and paste in the following;

import React from "react";
import ReactDOM from "react-dom";
import Main from "@/components/Main";
import  "./style.scss";

ReactDOM.render(<Main/>, document.getElementById('app'));

if (module.hot) { // enables hot module replacement if plugin is installed
 module.hot.accept();
}

Create a React component file in src/components/Main.jsx with the contents;

import React, { Component } from "react";

export class Main extends Component {
 render() {
   return (
     <div>
       <p className="hello-text">Hello from react!</p>
     </div>
   )
 }
}

export default Main

Compiling React JSX to JavaScript (Presets)

Create a file at .babelrc and put the following content;

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

This sets which features of ES6 to load for React.js. Do not forget the period . in the filename. It allows us to use the special syntax of React in native JavaScript code. Things like:

import Main from "@/components/Main";

<Main/>

Output a Single CSS File

Create a Sass file in src/style.scss with the following contents;

.hello-text {
 color: red;
}

Output a Single JavaScript File

In package.json, add the following to the scripts section;

"dev": "webpack-dev-server"
"production": "webpack --mode production"

When we run the command, npm run dev, the development server will be started. We can see the results of the running project at http://localhost:8080/. Running npm run production compiles the file in production mode and puts the result in the dist directory.

Output Images

In the file src/components/Main.jsx, import an image of your choice using the line:

import imagename from "@/images/imagename.jpg";

Make sure you store the image in the folder src/images/imagename.jpg.

Use the image in the components render function using:

<p><img src={imagename} alt="Image name"/></p>

Now, the image should be visible in the browser.

Output Fonts

For fonts, inside the file src/style.scss, load the fonts using a syntax similar to the following;

@font-face {
 font-family: "Advent Pro";
 font-style: normal;
 font-weight: 400;
 src: url("./fonts/advent-pro-v9-latin-regular.woff2") format("woff2"),
   url("./fonts/advent-pro-v9-latin-regular.woff") format("woff");
}

In the case above, we are loading a font using two font files and giving it the name Advent Pro

Use the new font in the hello-text class:

font-family: "Advent Pro";

Set Up File Watching

Due to the fact that we are using webpack-dev-server, we automatically get file watching and auto-reloading.

Setting up webpack for More Advanced Web Applications

In addition to the above simple setup, let's add more features for a slightly more complex application.

Setting up Hot Module Replacement

This is similar to auto reloading except that it does not reload the page. Instead, it smartly injects only the parts of the files which have changed.

To add the functionality, add the following to the devServer config in the webpack config file webpack.config.js:

hot: true

Splitting Output JavaScript Files into Separate Files

Sometimes, we may want many output files for some reason. An example would be to reduce cache-busting impact because of files that change often. Create another file entry file in src/print.js and add in the following:

console.log("This comes from print file");

This is just a simple log message in the console. In a real application though, we would probably have a lot more code in here.

Then, change the entry config like below;

entry: {
 app: "./src/main.js",
 print: "./src/print.js"
},

Now, we have two script files for the output.

Create Production Files

By now, you will notice that, when we run npm run dev, there are no compiled files in the output folder dist. That is because we are using the development server. If we want files for distribution, we need to use webpack's built-in compiler. We can do that by adding this to the script section of package.json:

"build": "webpack",

Now, when we run npm run build, a dist folder will be created with the distribution files. To prepare that for production, add the flag as below:

"production": "webpack --mode production",

Clear Output Folders Before Regeneration

Sometimes, we may want to clear the dist folder before creating the production files. One example is when you have file names randomly generated. In that case, there will be duplicates in some folders.

To do that, add the following to the list of plugins in the config file;

new CleanWebpackPlugin({
 cleanOnceBeforeBuildPatterns: ["css/*.*", "js/*.*", "fonts/*.*", "images/*.*"]
}),

This is clearing all the folders named js, fonts and images. To test out that it works, add a random JavaScript file to dist/js. For example randomfile.js.

Run npm run build with the plugin configuration above commented out. You will notice that the file still remains.

Now uncomment the plugin configuration and rerun npm run build. The file will now disappear.

Custom HTML Template

Create a file in src/index.html with the following contents:

<!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>Learn Webpack</title>
</head>
<body>
   <div id="app"></div>
</body>
</html>

Now run npm run build. Look at the output of the file in dist/index.html. You will notice that it is not using the source HTML file in src/index.html as a template because the titles are different. To configure that, change the HtmlWebpackPlugin plugin in the webpack config file by passing in an object like below:

new HtmlWebpackPlugin({
 template: "./src/index.html",
 filename: "index.html",
 title: "Learning Webpack"
}),

Now rerun npm run build. You will notice that the titles are now the same.

Serving Other Static Asset Types

You will have noticed that when we build our project, the images and fonts are copied over to the dist folder. We can not only copy over images and fonts but we can access in our code other file types like csv.

To add support for csv, create a file called src/mycsv.csv and paste in some csv as so;

name,company,email,date
Raja,Sem Corporation,[email protected],"January 21st, 2019"
Aladdin,Ut Nulla Corp.,[email protected],"November 21st, 2018"
Plato,Fermentum Fermentum Limited,[email protected],"October 7th, 2019"
Anthony,Fringilla Est Consulting,[email protected],"April 18th, 2018"

Then, add the following settings to the list of loader rules in the webpack config file:

{
 test: /\.(csv|tsv)$/,
 use: ["csv-loader"]
}

Now we can directly import the csv file in our code. In src/main.js, add in these two lines of code:

Import the csv file first:

import CsvData from "./mycsv.csv";

Then, at the bottom of the file, add in console.log(CsvData);

Now, run npm run dev. Open your browser and watch in your console. You should see the csv contents logged.

Protecting webpack Bundle Files

After building your app with webpack, if you open either one of the bundle files, you'll see that the whole logic of the code can easily be accessed. While this likely isn't a concern if you're building small projects, you should pay special attention if you're developing commercial web apps.

By reverse-engineering the application's source code, malicious actors may be able to abuse the app, tamper with the code, or even uncover important business logic (which is both a tendency and a concern in the Enterprise).

Webpack plugins like Uglify or webpack obfuscator only provide basic minification/obfuscation and can be quickly reversed with automated tools, and so fail to properly protect webpack bundle files. On the contrary, Jscrambler provides enterprise-grade JavaScript protection which cannot be reversed by automated tools and provides several layers of security, not just obfuscation.

To use the Jscrambler webpack plugin, first you have to install it:

npm i --save-dev jscrambler-webpack-plugin

Then, in the webpack.config.js file, add this line:

const JscramblerWebpack = require('jscrambler-webpack-plugin');

And finally add the Jscrambler plugin to the plugin array in the same webpack.config.js file:

plugins: [
    new JscramblerWebpack({
      enable: true, // optional, defaults to true
      chunks: ['app', 'print'], // optional, defaults to all chunks
      params: [], 
      applicationTypes: {}
      // and other jscrambler configurations
    })
  ]

During the webpack build process, the Jscrambler client will use the .jscramblerrc config file. For more details, see the full integration tutorial.

Conclusion

By now, we have covered several aspects of webpack. It is a very dynamic script and asset management tool.

We have not used all of its features but these should be enough for your average application. For even more advanced tools, please refer to the official webpack documentation.