How to setup Webpack config

Bundling assets and code has been pretty common in recent days. It allows creating portable packages that not only are easy to reuse and transfer but also great for fast delivery and thus better user experience (performance).

It has gained exceptional boost since the release of ES6 modules specification - standardized way of providing modularity to your JS code. While not being rapidly adopted by the browsers, they quickly gained popularity among developers, replacing other inferior systems, such as AMD and CommonJS. With better modularity also came greater demand for bundlers. Webpack, due to its great functionality and extendability, quickly gained the upper hand. But with the number of plugins, extensions, loaders etc. at your disposal, it's not easy to provide one proper solution or more specific configuration for all users with different needs. That's why Webpack configuration can be a little bit hard and exhausting for some to be dealt with. And that's why this tutorial even exists. Here I'll try to introduce you to the basics of creating your Webpack config. I really advise you to read this from top to bottom because there's a prize waiting at the end. 😂 Without further ado, let's first take a look at Webpack itself.

Webpack & company

Webpack is advertised as a static module bundler for modern JavaScript applications. It's a popular tool for bundling web apps. With support for ES6 modules, CommonJS, AMD and @imports it can pretty much handle all resources used by everyday web apps. It has a wide community behind it with a really vast ecosystem of plugins and loaders for many different assets. With that being said, it's not the only right tool for the work. There are plenty more high-quality bundlers out there. One of which being Rollup.js. It's just another bundler, but a bit more tailored towards bundling libraries and other JS tools rather than web apps. There's also a new player in the field called Parcel.js. It can be a perfect solution for everybody who doesn't like configuration and stuff. Parcel.js provides true out-of-the-box support for many different assets and formats. These 3 are my favorites and while there's definitely more other and based-on tools out there, I won't be naturally listing them all. 🙃 Now, that you know of possible alternatives, here's how to configure your Webpack step by step.

Config

To be more specific, let's define what exactly our config should do. The following configuration should fulfill every demand of our project. In this case, it'll be a simple SPA and PWA based on React and written in TypeScript. We'll also use SCSS (with no support for CSS whatsoever) for a better experience while defining our styles. Let's begin! 😁

Take a look at a skeleton of Webpack config file.

const path = require('path');

module.exports = {
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  },
  resolve: {
      extensions: []
  },
  module: {
      rules: []
  },
  plugins: []
}

So here you have it. The basic structure of our config. It's located in the webpack.config.js file which utilizes CommonJS syntax to export our config object. Inside it, we have the entry field relatively pointing to the file where the bundler should start its work from. Then we have the output object with the proper path and filename for the generated bundle. The name uses [name] placeholder to indicate that the name of the output should correspond to the name of our module (main by default). Resolve.extensions section is basically an array of file extensions that Webpack should read and process. Next, we have module.rules which is arguably one of the most important places in the whole config. It is here where we define our loaders that should process specific files. At the end comes the plugins field where all Webpack plugins will find their place. Now, let's populate it with some content, shall we?

// ...
    resolve: {
        extensions: [ '.tsx', '.ts', '.js', '.jsx' ]
    },
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: ['babel-loader', 'ts-loader'],
            exclude: /node_modules/
        }]
    },
// ...

And... that's mostly all that's required to process TypeScript! Let's take a closer look at what's going on. In extensions, we've added all possible extensions we're going to use in the future. In the rules, we provided our first rule. Its an object with 3 properties. The test is a regexp that matches all files that end with .ts or .tsx extensions and processes them with ts-loaderand then babel-loader provided in the use field. Using two processors gives us the ability to process code outputted from the TS compiler using Babel. Remember that loaders are used from the last to the first provided in the array. Finally, we exclude node_modules from matching, because who would possibly need to process these and lag his system? 😂 It's worth mentioning that you don't need to require ts-loader in any way, just to install it. And while we're talking about installing, I might have forgotten to mention anything about Webpack installation, so let's fix all that with one simple command:

npm install --save-dev webpack webpack-cli typescript @babel/core babel-loader ts-loader

Now let's add support for SCSS!

// ...
{
    test: /\.scss$/,
    use: [
        'style-loader',
        { loader: 'css-loader', options: { importLoaders: 1 } },
        'sass-loader',
    ],
},
// ...

Here, we need to use as much as 3 loaders, so let's install them first and don't forget about node-sass for processing SCSS!

npm install --save-dev node-sass style-loader css-loader sass-loader

Generally what we're doing right here is processing SCSS files using sass-loader with the node-sass lib, transform all @imports and URLs with css-loader and actually use/insert our styles with style-loader. The importLoaders option for css-loader is indicating how many loaders are used before the CSS one. In our example, it's just one - sass-loader. Take a look at the syntax for providing loader with additional options.

Lastly, let's get fancy and add support for bundling images aka static files!

npm install --save-dev file-loader
// ...
{
    test: /\.(jpe?g|png|gif|svg)$/i,
    loader: 'file-loader'
},
// ...

With the file-loader, Webpack processes every matching import into proper URLs. Notice, that loader field can be used instead of use when defining single loader.

Also, don't forget about other config files, such as tsconfig.json for TypeScript...

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": false,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react",
        "lib": ["es5", "es6", "dom"]
    },
    "include": [
        "./src/**/*"
    ],
}

...and .babelrc for Babel:

npm install --save-dev @babel/preset-env @babel/preset-react @babel/preset-typescript
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "env": {
    "development": {
      "presets": ["@babel/preset-typescript"]
    }
  }
}

I won't cover these as they're a bit out-of-topic, check out links to their pages if you want to know more - all of the tools listed in this article have awesome docs. 📙⚡

Let's get onto plugins now.

npm install --save-dev clean-webpack-plugin html-webpack-plugin
workbox-webpack-plugin webpack-pwa-manifest
const CleanPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const WebpackPwaManifest = require('webpack-pwa-manifest');
// ...
plugins: [
    new CleanPlugin(["dist"]),
    new HtmlWebpackPlugin({
        filename: 'index.html',
        title: 'Webpack Config',
        template: './src/index.html'
    }),
    new WebpackPwaManifest({
        name: 'Webpack Config',
        short_name: 'WpConfig',
        description: 'Example Webpack Config',
        background_color: '#ffffff'
    }),
    new WorkboxPlugin.GenerateSW({
        swDest: 'sw.js',
        clientsClaim: true,
        skipWaiting: true,
    })
],
// ...

In the snippet above we're greeted with as much as 4 plugins! Each of them has its own specific purposes. Clean-webpack-plugin is responsible for cleaning output directory - a simple task. Html-webpack-plugin setups our HTML file using provided data and template file.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta 
          name="viewport" 
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        >
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
    <noscript>
        You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
</body>

</html>

That's our template file BTW with the title taken straight from the plugin's config object. Finally, workbox-webpack-plugin and webpack-pwa-manifest provide PWA functionalities for offline service-workers and app manifest respectively. Some of these have a lot of customization options, so go to their project pages to learn more if you plan to use them.

Production

At this point, we can safely say that our config is quite operational. But it's not enough. With Webpack you can have multiple configs for different use-cases. The most popular example is to have 2 configs for production and development as each environment has its own specific requirements. Let's break our webpack.config.js into 3 pieces.

Webpack.common.js will contain configuration that is the same for both development and production configs.

const CleanPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const WebpackPwaManifest = require('webpack-pwa-manifest');
const path = require("path");

module.exports = {
  entry: "./src/index.tsx",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js"
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js", ".jsx"]
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { importLoaders: 1 } },
          "sass-loader"
        ]
      },
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        loader: "file-loader"
      }
    ]
  },
  plugins: [
    new CleanPlugin(["dist"]),
    new HtmlWebpackPlugin({
        filename: 'index.html',
        title: 'Webpack Config',
        template: './src/index.html',
    }),
    new WebpackPwaManifest({
        name: 'Webpack Config',
        short_name: 'WpConfig',
        description: 'Example Webpack Config',
        background_color: '#ffffff'
    }),
    new WorkboxPlugin.GenerateSW({
        swDest: 'sw.js',
        clientsClaim: true,
        skipWaiting: true,
    })
  ]
};

Now, let's create our webpack.prod.js config. We'll need to merge it with our common config. To do this we can utilize webpack-merge - a tool for doing just that. So let's install it and 2 other plugins we'll later use.

npm install --save-dev webpack-merge uglifyjs-webpack-plugin hard-source-webpack-plugin
const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'production',
    devtool: 'source-map',
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: ["babel-loader", "ts-loader"],
            exclude: /node_modules/
        }]
    },
    optimization: {
        minimizer: [new UglifyJsPlugin({
            sourceMap: true
        })],
    },
});

Here we can see two new properties - mode and devtool. Mode indicates our current environment - its either "production""development" or "none". This allows some tools to apply optimizations proper for chosen env. Devtool property refers to the way of generating source maps. Webpack has many options built-in for this property. There are also many plugins that provide additional functionalities. But "source-map" option that produces source maps from content files, is enough for us right now. Then we have our old-fashioned .ts files loader. It's followed by new, self-explaining fields in our config. The optimization.minimizer allows us to specify a plugin used to minimize our files, which is naturally useful when targeting production. Here I'll use uglifyjs-webpack-plugin which is well battle-tested and has good performance with solid output. Don't forget about sourceMap option for this plugin, without that your source maps won't be generated! Now, let's go over to the development config file - webpack.dev.js.

const merge = require('webpack-merge');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'eval-source-map',
    module: {
        rules: [{
            test: /\.tsx?$/,
            loader: "babel-loader",
            exclude: /node_modules/
        }]
    },
    plugins: [
        new HardSourceWebpackPlugin()
    ]
});

At development, we only care about speed. No optimizations need to be done at that point. We only want for our code to be bundled fast. The same applies to source mapping which this time uses much faster, but the not-so-optimized "eval-source-map" option. Then, when defining our loader for TypeScript, we use only one, single loader - babel-loader. By doing this we only transpile our .ts files without type-checking them, which has a huge impact on bundling speed. That's why earlier I defined the @babel/preset-typescript to be used on the development stage in the .babelrc file. Lastly, we have the hard-source-webpack-plugin which provides an easy way for caching our files, so our second bundling will be even faster!

And... that's it! We have our proper environment-specific configs ready to be used!

Hot reload 🔥

So we've got nice configs, but who needs a fast development config without hot reloading!? That's right - it's getting hot!🔥 So, let's put aside our production config for now and let's implement this wonderful feature, shall we? Using webpack-dev-server it's really simple! You can install it with:

npm install --save-dev webpack-dev-server

For configuration add devServer config object to our webpack.dev.js file.

// ...
devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
}
// ...

Here we provide basic options like port, directory to serve and if compression should be done. And that's it! To finish it let's add two scriptsto our package.json for easier development.

"scripts": {
    "start": "webpack-dev-server --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
}

By using --config option we provide the location of our env-specific Webpack config.

So here you have it! Your very own Webpack config with support for TS/TSX, SCSS, optimize production and development settings and HMR! As a side-note, our HMR works just fine, but when it comes to React-specific stuff, there's room for improvement. For example, if you would like to preserve your components' states across reloads. For this, you can use react-hot-loader and follow this awesome guide while using the config you've already created here.

A gift 🎁

So, as you can see by following this tutorial, creating Webpack config isn't difficult. It's just a bit time-consuming process that can require some googling from time to time. But it can also be fun for some. But if you're in the other group I have something special for you. I've created a simple CLI tool for creating basic boilerplate for your Webpack config. By using this you won't have to spend time setting the same things up yourself over and over again. It's called webpack-suit-up and you can download it from NPM. So, yeah, check it out if you're interested.

I hope this tutorial helped you with the process of configuring your Webpack. For more info on Webpack, you can check out its official website. But, just as I said on the beginning, there are many other great tools that may not even require configuration. There are even those which are based on Webpack and automatically configures it. Also, even Webpack itself from v4 doesn't require configuration, but it's really necessary in most cases. Maybe you would like to see a complete list of interesting web bundlers out there? Or rather a guide on configuring Rollup.js? Write in the comments below. Share this article, so that others can discover it quicker. Also, follow me on Twitter or on my Facebook Page for more up-to-date content.

By :  Areknawo

Getting Started with Webpack | Javascript modules

Getting Started with Webpack | Javascript modules

In this video, we'll be discussing the modules in JavaScript. JavaScript, often abbreviated as JS, is a high-level, interpreted scripting language that conforms to the ECMAScript specification.

Getting Started with Webpack | Javascript modules

In this video, we'll be discussing the modules in JavaScript. JavaScript, often abbreviated as JS, is a high-level, interpreted scripting language that conforms to the ECMAScript specification. JavaScript has curly-bracket syntax, dynamic typing, prototype-based object-orientation, and first-class functions

How to write simple modern JavaScript apps with Webpack and progressive web techniques

How to write simple modern JavaScript apps with Webpack and progressive web techniques

How to write simple modern JavaScript apps with Webpack and progressive web techniques - Have you thought about making modern JavaScript applications with the simplest setup possible for your next project?

How to write simple modern JavaScript apps with Webpack and progressive web techniques - Have you thought about making modern JavaScript applications with the simplest setup possible for your next project?

If so, you have come to the right place!

JavaScript frameworks exist to help us build applications in a generalized way with most of the common features. But most of the applications may not need all the powerful features of a framework. It may be overkill to just use a framework for specific requirements (especially small to medium scale projects).

Today I am going to show an approach to how you can use modern features and build your own customized Web Applications. You can also build your own framework on top of the sample applications if you want to. That is purely optional. The power of Vanilla JavaScript enables us to follow our own coding style irrespective of the tools used.

What We Need

Before starting out, let us quickly skim through the features we need.

Architectural Planning

To ensure fast loading and consistent experiences, we’ll use the following patterns:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

Build Setup

We need a good custom build setup, so we will be using Webpack with the following requirements:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

Bare Minimum JavaScript Features

We will be touching on minimal JavaScript features to get us off the ground and produce the output we require. I will show you how we can use existing JavaScript ES6 features in our day to day vanilla applications. Here they are:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

At the end of this article there is a sample application demo along with its source code on GitHub. Let’s dig deeper, shall we? 🙌

Architectural Planning

The advent of Progressive Web Applications has helped bring new architectures in order to make our first paint more effective. Combining App Shell and PRPL patterns can result in consistent responsiveness and app-like experiences.

What is App Shell & PRPL?

App Shell is an architectural pattern for building Progressive Web Applications where you ship the minimal critical resourcesin order to load your site. This basically consists of all the necessary resources for the first paint. You may cache the critical resources as well using a service worker.

PRPL refers to the following:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

What Do These Architectures Look Like In Code?

The App Shell and PRPL pattern are both used together to achieve the best practices.

The App shell looks somewhat like the following piece of code:

<!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" />
    <!-- Critical Styles -->
    <style>
        html {
            box-sizing: border-box;
        }
        *,
        *:after,
        *:before {
            box-sizing: inherit;
        }
        body {
            margin: 0;
            padding: 0;
            font: 18px 'Oxygen', Helvetica;
            background: #ececec;
        }
        header {
            height: 60px;
            background: #512DA8;
            color: #fff;
            display: flex;
            align-items: center;
            padding: 0 40px;
            box-shadow: 1px 2px 6px 0px #777;
        }
        h1 {
            margin: 0;
        }
        .banner {
            text-decoration: none;
            color: #fff;
            cursor: pointer;
        }
        main {
            display: flex;
            justify-content: center;
            height: calc(100vh - 140px);
            padding: 20px 40px;
            overflow-y: auto;
        }
        button {
            background: #512DA8;
            border: 2px solid #512DA8;
            cursor: pointer;
            box-shadow: 1px 1px 3px 0px #777;
            color: #fff;
            padding: 10px 15px;
            border-radius: 20px;
        }
        .button {
            display: flex;
            justify-content: center;
        }
        button:hover {
            box-shadow: none;
        }
        footer {
            height: 40px;
            background: #2d3850;
            color: #fff;
            display: flex;
            align-items: center;
            padding: 40px;
        }
    </style>
    <title>Vanilla Todos PWA</title>
</head>

<body>

    <body>
        <!-- Main Application Section -->
        <header>
            <h3><a class="banner"> Vanilla Todos PWA </a></h3>
        </header>
        <main id="app"></main>
        <footer>
            <span>&copy; 2019 Anurag Majumdar - Vanilla Todos SPA</span>
        </footer>
      
        <!-- Critical Scripts -->
        <script async src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>

        <noscript>
            This site uses JavaScript. Please enable JavaScript in your browser.
        </noscript>
    </body>
</body>

</html>

Lines 4–17: The install event of service workers helps to cache all the static assets. Here, you can cache the app shell resources (CSS, JavaScript, images, etc.) for the first route (as per App shell). Also, you can cache the remainder of the assets of the application ensuring the whole app can run offline too. This caching of static assets apart from the main app shell ensures the Pre-cache stage of the PRPL pattern.

Lines 19–38: The activate event is the place for cleaning up of unused caches.

Lines 40–63: These lines of code help in fetching resources from the cache if they are in cache or go to network. Also, if a network call is made, then the resource is not in cache and put into a new separate cache. This scenario helps to cache all dynamic data for an application.

All in all, most of the parts of the architecture have been covered. The only part left is the Lazy loading stage of the PRPL pattern. I will discuss this with regards to JavaScript.

Our Build Setup

What is a good architectural structure without a build setup? Webpack to the rescue. There are other tools like Parcel, Rollup etc. out there, but whatever concepts we apply to Webpack can be applied to any such tool.

I will map the concepts used to the plugins so that you can get hold of the basics used for setting up of the workflow. This is the most important step to getting started with a good reusable build config for your own application for the future.

I know how difficult it is for developers like us to configure Webpack or any tool for that matter from scratch. The following article was an inspiration which helped me to create my own build setup:

A tale of Webpack 4 and how to finally configure it in the right way. Updated.

Do refer to the above link if you get stuck anywhere with the build setup. For now let us check out the concepts required for the build.

ES6 & Dynamic Imports support

Babel is a popular transpiler which is there to help us with transpiling ES6 features down to ES5. We will need the following packages to enable babel working with webpack:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

Here is a sample babelrc for reference:

{
    "presets": ["@babel/preset-env"],
    "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

During babel setup, we need to feed the following 2nd line in presets to enable babel to transpile ES6 down to ES5 and the 3rd line inplugins to enable the dynamic import support with Webpack.

Here is how babel is used with Webpack:

module.exports = {
    entry: {
        // Mention Entry File
    },
    output: {
        // Mention Output Filenames
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },
    plugins: [
        // Plugins
    ]
};

Lines 10–17: The babel loader is used to set up the babel transpilation process in webpack.config.js. For simplicity, the other parts of the config have been eliminated or commented out.

SASS & CSS Support

For setting up SASS and CSS you need the following packages:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

Here is how the config looks like:

module.exports = {
    entry: {
        // Mention Entry File
    },
    output: {
        // Mention Output Filenames
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css'
        }),
    ]
};

Lines 17–25: This is the area where the loaders are registered.

Lines 29–31: Since we are using a plugin to extract a CSS file, we are using the MiniCssExtractPlugin here.

Custom Development & Production Setup

This is the most important section of the build process. We all know that we need a development and production build setup for developing applications and also deploying the final distributable to the web.

Here are the packages that will get used:

  • Application Shell Architecture
  • PRPL (Push, Render, Pre-cache, Lazy loading) pattern

Here is the final Webpack config file:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackMd5Hash = require('webpack-md5-hash');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = (env, argv) => ({
    entry: {
        main: './src/main.js'
    },
    devtool: argv.mode === 'production' ? false : 'source-map',
    output: {
        path: path.resolve(__dirname, 'dist'),
        chunkFilename:
            argv.mode === 'production'
                ? 'chunks/[name].[chunkhash].js'
                : 'chunks/[name].js',
        filename:
            argv.mode === 'production' ? '[name].[chunkhash].js' : '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin('dist', {}),
        new MiniCssExtractPlugin({
            filename:
                argv.mode === 'production'
                    ? '[name].[contenthash].css'
                    : '[name].css'
        }),
        new HtmlWebpackPlugin({
            inject: false,
            hash: true,
            template: './index.html',
            filename: 'index.html'
        }),
        new WebpackMd5Hash(),
        new CopyWebpackPlugin([
            // {
            //     from: './src/assets',
            //     to: './assets'
            // },
            // {
            //     from: 'manifest.json',
            //     to: 'manifest.json'
            // }
        ]),
        new CompressionPlugin({
            algorithm: 'gzip'
        })
    ],
    devServer: {
        contentBase: 'dist',
        watchContentBase: true,
        port: 1000
    }
});

Lines 9–77: The entire webpack config is a function which takes two arguments. Here I have used the argv i.e., the arguments sent while running the webpack or webpack-dev-server commands.

The below image shows the scripts section in package.json.

Accordingly, if we run npm run build it will trigger a production build, and if we run npm run serve it will trigger a development flow with a local development server.

Lines 44–77: These lines show how the plugins and the development server config needs to be setup.

Lines 59–66: These lines are any resources or static assets which need to be copied over from the application source.

Custom Service Worker Build

Since we all know how tedious it is to write the names of all the files again for caching, I made a custom service worker build script for catching hold of the files in the dist folder and then adding them as contents of the cache in the service worker template. Finally, the service worker file will get written to the dist folder.

The concepts regarding the service worker file we talked about will be the same. Here is the script in action:

const glob = require('glob');
const fs = require('fs');

const dest = 'dist/sw.js';
const staticAssetsCacheName = 'todo-assets-v1';
const dynamicCacheName = 'todo-dynamic-v1';

let staticAssetsCacheFiles = glob
    .sync('dist/**/*')
    .map((path) => {
        return path.slice(5);
    })
    .filter((file) => {
        if (/\.gz$/.test(file)) return false;
        if (/sw\.js$/.test(file)) return false;
        if (!/\.+/.test(file)) return false;
        return true;
    });

const stringFileCachesArray = JSON.stringify(staticAssetsCacheFiles);

const serviceWorkerScript = `var staticAssetsCacheName = '${staticAssetsCacheName}';
var dynamicCacheName = '${dynamicCacheName}';
self.addEventListener('install', function (event) {
    self.skipWaiting();
    event.waitUntil(
      caches.open(staticAssetsCacheName).then(function (cache) {
        cache.addAll([
            '/',
            ${stringFileCachesArray.slice(1, stringFileCachesArray.length - 1)}
        ]
        );
      }).catch((error) => {
        console.log('Error caching static assets:', error);
      })
    );
  });
  self.addEventListener('activate', function (event) {
    if (self.clients && clients.claim) {
      clients.claim();
    }
    event.waitUntil(
      caches.keys().then(function (cacheNames) {
        return Promise.all(
          cacheNames.filter(function (cacheName) {
            return (cacheName.startsWith('todo-')) && cacheName !== staticAssetsCacheName;
          })
          .map(function (cacheName) {
            return caches.delete(cacheName);
          })
        ).catch((error) => {
            console.log('Some error occurred while removing existing cache:', error);
        });
      }).catch((error) => {
        console.log('Some error occurred while removing existing cache:', error);
    }));
  });
  self.addEventListener('fetch', (event) => {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return response || fetch(event.request)
          .then((fetchResponse) => {
              return cacheDynamicRequestData(dynamicCacheName, event.request.url, fetchResponse.clone());
          }).catch((error) => {
            console.log(error);
          });
      }).catch((error) => {
        console.log(error);
      })
    );
  });
  function cacheDynamicRequestData(dynamicCacheName, url, fetchResponse) {
    return caches.open(dynamicCacheName)
      .then((cache) => {
        cache.put(url, fetchResponse.clone());
        return fetchResponse;
      }).catch((error) => {
        console.log(error);
      });
  }
`;

fs.writeFile(dest, serviceWorkerScript, function(error) {
    if (error) return;
    console.log('Service Worker Write success');
});

Lines 8–18: This is the place where all the contents of the dist folder are captured as an array staticAssetsCacheFiles.

Lines 24–87: This is the service worker template we talked about before. The concepts are exactly the same, just that we are introducing variables into the template so that we can reuse the service worker template and make it handy for future use. This template was also required since we needed to add dist folder contents to the cache as per line 33.

Lines 89–92: Finally, a new service worker file will get written to the dist folder along with its contents from the service worker template serviceWorkerScript.

The command to run the above script is node build-sw and it should be run after webpack --mode production is done.

This service worker build script really helped me a lot in caching files easily. I am currently using this for my own side projects due to its simplicity and great ease of tackling the caching problem.

If you guys want to use a library for Progressive Web Application related features, you can go for Workbox. This library does some real neat stuff and has amazing features which you can take control of.

Final Look At The Packages

Here is a sample package.json file with all dependencies:

{
  "name": "vanilla-todos-pwa",
  "version": "1.0.0",
  "description": "A simple todo application using ES6 and Webpack",
  "main": "src/main.js",
  "scripts": {
    "build": "webpack --mode production && node build-sw",
    "serve": "webpack-dev-server --mode=development --hot"
  },
  "keywords": [],
  "author": "Anurag Majumdar",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/preset-env": "^7.2.3",
    "autoprefixer": "^9.4.5",
    "babel-core": "^6.26.3",
    "babel-loader": "^8.0.4",
    "babel-preset-env": "^1.7.0",
    "clean-webpack-plugin": "^1.0.0",
    "compression-webpack-plugin": "^2.0.0",
    "copy-webpack-plugin": "^4.6.0",
    "css-loader": "^2.1.0",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.5.0",
    "node-sass": "^4.11.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "terser": "^3.14.1",
    "webpack": "^4.28.4",
    "webpack-cli": "^3.2.1",
    "webpack-dev-server": "^3.1.14",
    "webpack-md5-hash": "0.0.6"
  }
}

Remember that Webpack gets updated frequently, and changes keep happening in the community with new plugins replacing existing ones. So it is important to keep a note of the concepts required for a build setup rather than the actual packages used.

JavaScript Features

We all have a choice: either to write our own framework for certain features to be used by our application such as change detection, routing, storage patterns, redux etc, or pull already existing packages for such features.

Now I’ll speak about the bare minimum features required in order to structure the layout of our application and get it going. Later on you can add your own frameworks or packages to the application.

ES6 Modules

We will use ES6 import and export statements and treat each file as an ES6 module. This feature is commonly used by popular frameworks like Angular and React and is pretty handy. With the power of our Webpack config, we can fully utilize the power of import and export statements.

import { appTemplate } from './app.template';
import { AppModel } from './app.model';

export const AppComponent = {
  // App Component code here...
};

Object Literal Syntax Or ES6 Class Syntax

Building components is a very important part of our application. We can choose to go with the latest web standards like Web Components too, but to keep things simple we can go ahead and use object literal syntax or ES6 class syntax.

The only thing with class syntax is that we need to instantiate it and then export it. So to keep things even simpler, I went ahead with object literal syntax for component architecture.

import { appTemplate } from './app.template';
import { AppModel } from './app.model';

export const AppComponent = {

    init() {
        this.appElement = document.querySelector('#app');
        this.initEvents();
        this.render();
    },

    initEvents() {
        this.appElement.addEventListener('click', event => {
            if (event.target.className === 'btn-todo') {
                import( /* webpackChunkName: "todo" */ './todo/todo.module')
                    .then(lazyModule => {
                        lazyModule.TodoModule.init();
                    })
                    .catch(error => 'An error occurred while loading Module');
            }
        });

        document.querySelector('.banner').addEventListener('click', event => {
            event.preventDefault();
            this.render();
        });
    },

    render() {
        this.appElement.innerHTML = appTemplate(AppModel);
    }
};

Lines 4–32: We export an object called AppComponent which is immediately available for use in other parts of our application.

You can go ahead and use ES6 class syntax or standard Web Components too and achieve a more declarative way of writing code here. For simplicity’s sake, I chose to write the demo application in a more imperative approach.

Dynamic Imports

Remember I talked about missing out on the “L” of the PRPL pattern? Dynamic import is the way to go ahead and lazy load our components or modules. Since we used the App Shell and PRPL together to cache the shell and other route assets, dynamic imports import the lazy component or module from the cache instead of the network.

Note that if we only used App Shell architecture, the remaining assets of the application i.e., the contents of chunksfolder, would not have been cached.

Lines 15–19: Refer to App Component code; this is the place where dynamic imports shine. If we click on a button having the class btn-todo, then only this TodoModule gets loaded. By the way, TodoModule is just another JavaScript file which consists of a set of object components.

ES6 Arrow Functions & ES6 Template Literals

Arrow functions should be used especially where we want to make sure of the this keyword inside the function, which should refer to the surrounding context where the arrow function is declared. Apart from that, these functions really help in creating neat shorthand syntax.

export const appTemplate = model => `
    <section class="app">
        <h3> ${model.title} </h3>
        <section class="button">
            <button class="btn-todo"> Todo Module </button>
        </section>
    </section>
`;

The above example is a template function defined as an arrow function which accepts a model and returns an HTML string consisting of the model data in it. String interpolation is carried out with the help of ES6 template literals. The real benefit of template literals is multi-line strings and interpolation of model data into the string.

Here’s a micro tip for handling component templating and generation of reusable components: use the reducefunction to accumulate all HTML strings as per the following example:

const WorkModel = [
    {
        id: 1,
        src: '',
        alt: '',
        designation: '',
        period: '',
        description: ''
    },
    {
        id: 2,
        src: '',
        alt: '',
        designation: '',
        period: '',
        description: ''
    },
    //...
];


const workCardTemplate = (cardModel) => `
<section id="${cardModel.id}" class="work-card">
    <section class="work__image">
        <img class="work__image-content" type="image/svg+xml" src="${
            cardModel.src
        }" alt="${cardModel.alt}" />
    </section>
    <section class="work__designation">${cardModel.designation}</section>
    <section class="work__period">${cardModel.period}</section>
    <section class="work__content">
        <section class="work__content-text">
            ${cardModel.description}
        </section>
    </section>
</section>
`;

export const workTemplate = (model) => `
<section class="work__section">
    <section class="work-text">
        <header class="header-text">
            <span class="work-text__header"> Work </span>
        </header>
        <section class="work-text__content content-text">
            <p class="work-text__content-para">
                This area signifies work experience
            </p>
        </section>
    </section>
    <section class="work-cards">
        ${model.reduce((html, card) => html + workCardTemplate(card), '')}
    </section>
</section>
`;

The above piece of code does a great deal of work indeed. Simple yet intuitive. It does follow a little inspiration from the frameworks out there.

Lines 1–10: This is a sample model array on which the reduce function can run in order to give the reusable template feature.

Line 32: This line does all the magic in generating multiple reusable components into one HTML string. The reduce function takes in the accumulator as the first argument, and each value of the array as the second argument.

Thanks to these simple features, we already have an application structure in place. The best way to learn a feature is to put it in action they say, so here we are. 😎

Application Demo

Here is a demo of the to-do application built with all the features as discussed in this article. Click here to visit the site.

Click here for the link to GitHub repository. Feel free to clone the repository and go through the code for a better understanding of the conceptual examples mentioned in the article.

Sample Production App

The production site is a portfolio which was designed, developed and engineered from scratch using the exact features as specified in this article. The Single Page Application is broken down into custom modules and components. The flexibility and power that comes with Vanilla JavaScript is something unique and does help in producing some astonishing results.

Click here to go to the site. Here is the site in action:

Do visit the site to get a feel for it. The colors are not accurately produced in the demo here. The engineering put into this site produced the following results:

Never scored a perfect 100 before in any subject. 🙏

Conclusion

There are several projects we might like to build using Vanilla JavaScript instead of frameworks in order to achieve certain results quickly. I wrote this article to help developers use a simple custom setup to build their future projects.

The best part about the Vanilla framework is that developers have the freedom to shape their engineering thought patterns according to various use cases. Be it Imperative or Declarative style of programming, creating or using of latest existing features. As long as we produce consistent and performant applications with good code maintainability, our job is done for the day.

Happy hacking! 😄How to write simple modern JavaScript apps with Webpack and progressive web techniques - Have you thought about making modern JavaScript applications with the simplest setup possible for your next project?

===============================================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Learn More

☞ Svelte.js - The Complete Guide

☞ The Complete JavaScript Course 2019: Build Real Projects!

☞ Advanced CSS and Sass: Flexbox, Grid, Animations and More!

☞ CSS - The Complete Guide (incl. Flexbox, Grid & Sass)

Why you need Webpack?

Why you need Webpack?

In this video, we'll be discussing as to why we exactly need Webpack and what all it brings to us, also we will be seeing how Webpack is diffrent and how we can use it.

In this video, we'll be discussing as to why we exactly need Webpack and what all it brings to us , also we will be seeing how Webpack is diffrent and how we can use it.