Practical WEBPACK Basics: Pure Javascript frontend
Tags: webdev, javascript, webpack, beginners
Published: 2022-06-20 18:46:42

## Contents

1. [Inspiration](#inspiration)
2. [Prerequisites](#prerequisites)
3. [Setup](#setup)
4. [Webpack basics](#webpack-basics)
5. [Sample App and Bundling](#sample-app-and-bundling)
6. [Configurations](#configurations)
7. [Development](#development)
8. [Module and Loaders](#module-and-loaders)
9. [Extra Bits](#extra-bits)


## Inspiration

About two years ago I created a Webpack setup to suit my specific needs when developing with react. Since then I've mainly been on large projects and because I used react, I never needed to revisit. Recently I decided to do some small experiments and found myself struggling a bit to create a pure Javascript Webpack setup. I didn't want to copy someone else's without actually understanding what it is it was doing. So after revisiting in some depth here I am, creating a guide that's would be easy to digest, practical and would maybe even serve me later down if I eventually forget and need to return 😅.



## Prerequisites

To start you'll need **Node**, **npm** and preferably **npx**, you can skip to the next section if you already have these installed ( [next section](#How-to-think-of-Webpack) )

### Installing Node
#### OPTION A: (Recommended NVM ( Node Version Manager)
It's generally recommended that you use nvm to install and manage versions of node. You can see instructions on how to install for you OS [here](https://github.com/nvm-sh/nvm#installing-and-updating). Definitely use the above link if you can, however if not you can try running these...
 
**install via curl**
`curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash`
**reload your terminal**
`source ~/.bashrc`
**check the install**
`nvm -v`
**use nvm to install a version of node** ( e.g. 16 )
`nvm install 16`
OR
**use nvm to install the latest version of node**
`nvm install node`

**use nvm to use a version of Node it installed** ( e.g. 16 )
`nvm use 16`

#### OPTION B: Direct Install
You can visit [here](https://nodejs.org/en/download/package-manager/) for installation instructions for your specific OS.

### npm and npx
npm and npx are usually installed alongside node, you can test with `npm --version` and `npx --version`.

Note: **Node**, **npm** and **npx** are all different things, **Node** is the execution environment ( basically the thing that runs the code ); **npm**, Node Package Manager, manages packages for **node**; **npx**, Node Package Execute, allows us to run installed **node** packages. The versions of each of these things are (mostly) independent and therefore when you run `npm --version` or `node --version` or `npx --version` DON'T EXPECT to see the same number.

Dependending on which option you chose **npx** may not be installed, as such you can run the following: 

**install npx globally** ( DO NOT RUN IF YOU ALREADY HAVE npx INSTALLED, again check with `npx --version` )
`npm install -g npx`



## Setup

Create a folder for our tutorial, let's say `sample-frontend`, go into `sample-frontend` and initialize this folder by running `npm init`, this will take you through some questions to setup your project.

Once you're finished you can create a `sample_index.html` file then a `src` folder and inside `src` create a `sample_index.js` file. 

Right now your folder should look something like this 
```
sample-frontend
  |- package.json
  |- sample_index.html
  |- /src
   |- sample_index.js
```

### sample files

`sample_index.html`

```HTML
<!DOCTYPE html>
<html>
    <head></head>
    <body>
        I am body !
        <script type='text/javascript' src="./src/sample_index.js"></script>
    </body>
</html>
```
`sample_index.js`

```javascript
window.onload = ev => {
    init();
}

const init = () => {
    console.log('hello I am a sample index.js');
}
```


Check your page and ensure everything is okay by visiting /<path_to_your_folder>/sample_index.html on your browser.




## Webpack basics

What is Webpack ? well according to the [official documentation](https://webpack.js.org/guides/getting-started/)

> At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph from one or more entry points and then combines every module your project needs into one or more bundles, which are static assets to serve your content from.

To generalize and simplify ( for starters ) 
> Webpack looks at all your code and outputs a single something. 

Some questions that you might ask are (1) How does it work (2) what does it output (3) how is this helpful 

### How does it work and what does it output

Let's install webpack and find out.

**Install webpack and it's cli**
`npm install --save-dev webpack webpack-cli`

navigate to the root of your project `sample_project` and run the following:
`npx webpack --entry ./src/sample_index.js --mode production`

This command and it's output tells us a lot. It says please take a look at my code starting from `sample_index.js` and output something that's ready for production.

If we look at our project directory we'll see a folder `dist` and inside a file `main.js`.

```
sample-frontend
  |- package.json
  |- package-lock.json
  |- sample_index.html
  |- /src
    |- sample_index.js
  |- /dist
    |- main.js
  |- /node_modules...
```

`main.js` should contain something like this
`(()=>{window.onload=l=>{o()};const o=()=>{console.log("hello I am a sample index.js")}})();`

_main.js_ is minified and obfuscated version of our code base. ( Minification - helps load time by removing unnecessary characters from code; Obfuscation - helps protect your code/logic by making it difficult to read/understand )

We can edit our `sample_index.html` to use `main.js` and it will work in the same way.

`sample_index.html`

```html
<!DOCTYPE html>
<html>
    <head></head>
    <body>
        I am body !
        <!-- <script type='text/javascript' src="./src/sample_index.js"></script> -->
        <script type='text/javascript' src="./dist/main.js"></script>
    </body>
</html>
```

( you can test by visiting your page in the browser )

There's something else going on ( the main thing actually 😅 ) but we need to add dependencies to see it in action. We'll add two dependencies, one being a node package we install and another being our own, we will use these to build an art search app.


## Sample App and Bundling

Note: Since this tutorial isn't about writing code itself, explanations on how the app works will be documented in the comments within the files themselves.
Let's add our first dependency, a package to help us make requests
`npm install --save axios`

Now our own, in `src` create a file `art_helper.js`.

```
sample-frontend
  |- package.json
  |- package-lock.json
  |- sample_index.html
  |- /src
    |- sample_index.js
    |- art_helper.js
  |- /dist
    |- main.js
  |- /node_modules...
```

Edit your _art_helper.js_ to include the following:

```javascript
import Axios from 'axios';

export const searchArt = ({ title }) => {
    const params = { q: title, limit: 5, fields: 'id,title,image_id,artist_display' }; // sample set of params, limits the number of results to 5, and only returns the id, title, image_id, and artist_display fields
    return Axios.request({
        url: 'https://api.artic.edu/api/v1/artworks/search',
        params
    }).then(res => {
        const { config, data } = res.data;
        const artPieces = data.map(artPiece => ArtPiece({ config, ...artPiece })); // map the data to an array of HTML strings where each HTML string is an art piece (see ArtPiece function below)
        return { artPieces };
    }).catch(err => {
        console.log(err);
    });
}


// Takes a config and the data for an art piece and returns an HTML string
const ArtPiece = ({ config, title, image_id, id, artist_display }) => {
    const image_url = `${config.iiif_url}/${image_id}/full/843,/0/default.jpg`;
    console.log(image_url);
    return (`<div class='art_piece'>
        <img src="${image_url}" />    
        <h3>${title}</h3>
        <p>- ${artist_display}</p>
    </div>`);
}


/**
 * Fetches art from The Art Institute of Chicago API ( https://api.artic.edu/docs )
 * First queries the API for art pieces with the given title ( https://api.artic.edu/api/v1/artworks/search )
 * Then returns an array of ArtPiece objects
 * The image URL for each ArtPiece is constructed from the IIIF URL ( config.iiif_url ) and the image_id
 */
```

Edit your `sample_index.html` to reflect below:

```html
<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <div id='root'>
            <input type='text' id='title-search-input'/>
            <button id='title-search-button'>Search</button>
            <div id='search-results'>
            </div>
        </div>

        <!-- <script type='text/javascript' src="./src/sample_index.js"></script> -->
        <script type='text/javascript' src="./dist/main.js"></script>
    </body>
</html>
```

Finally Edit your `sample_index.js` to the following:

```javascript
import { searchArt } from './art_helper';

window.onload = ev => {
    init();
}

const init = () => {
    console.log('hello I am a sample index.js vn');
    document.getElementById('title-search-button').addEventListener('click', handleSearchClick);
}

const handleSearchClick = (ev) => {
    const title = document.getElementById('title-search-input').value;
    searchArt({ title }).then(renderResults);
}

const renderResults = ({ artPieces }) => {
    document.getElementById('search-results').innerHTML = artPieces.join('');
}
```

Once more at the root of your project run

`npx webpack --entry ./src/sample_index.js --mode production`

then go to your page refresh and try it out.

We now have a bare bones art search app, a couple things to note: We did not have to use script tags in our `sample_index.html` for our dependencies, _art_helper.js_  and _Axios_, along with that we did not have to worry about the order in which they were included. Also if we view our _main.js_ file, though minified and obfuscated making it difficult to read, we see that our ES6+ syntax has been translated down allowing us to use all of ES6+ without having to worry about compatibility issues across browsers.

Starting with our entry point `sample_index.js` our code has been thoroughly parsed all the while building a graph of our exact dependencies to bundle, transpile (ES6+ JS to browser compatible version), obfuscated and minified into one file `main.js` with which we can use.




## Configurations 

Running quick commands are fine however when we want to specify and do a lot more, configuration files become a necessity, especially when working with others, it's easier to debug a config file than an exceptionally verbose command. Let's translate our build command into configuration, in the project directory `sample-frontend` create a file `webpack.config.js`.

```
sample-frontend
  |- package.json
  |- package-lock.json
  |- sample_index.html
  |- webpack.config.js
  |- /src
    |- sample_index.js
    |- art_helper.js
  |- /dist
    |- main.js
  |- /node_modules...
```

Edit webpack.config.js to reflect the following:

```javascript
const path = require('path');

module.exports = {
    entry: {
        main: './src/sample_index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
    },
    mode: 'production'
}
```

Now you can run  `npx webpack --config webpack.config.js` and achieve the same result.

What's happening here ? We're exporting an object that describes what we would like to happen.

### entry

Where should Webpack start, which, can be rephrased as where "does my app start ?", in this case './src/sample_index.js'. 

You'll notice entry is an object ( a set of key value pairs ) and we've called our entry point 'main', this is because by default 'main' is name given to the bundled javascript file. However we can rename this to whatever we like, a popular name or term is simply 'bundle'.


```javascript
const path = require('path');

module.exports = {
    entry: {
        bundle: './src/sample_index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
    },
    mode: 'production'
}
```

If you run `npx webpack --config webpack.config.js` now in the _dist_ folder you'll see two files `main.js` and `bundle.js`. Now we have two problems with this (1) if we generate a file with a different name the old one stays and (2) our `sample_index.html` will need to be updated to load our new file name. We can solve one by configuring the next section of the configuration object **output**.

### output

In output we describe what we would like webpack to output (crazy right ?). Currently we have one specification here, the path, i.e. where do we want our bundled file to be output, `path.resolve(__dirname, 'dist')` which means the get the absolute path to the current directory then output to folder _dist_.
Now after reconfiguring **entry** above we saw that the old files generated under a different name are just left there, to fix this we have to tell webpack to clean up after us.

```javascript
const path = require('path');

module.exports = {
    entry: {
        bundle: './src/sample_index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    mode: 'production'
}
```

Now if we run `npx webpack --config webpack.config.js` we'll see that `main.js` was removed, however we're still left with the other problem of having to edit `sample_index.html` whenever the bundled filename changes. To get around this we move to a new section **plugins**


### plugins

Plugins like the name suggest can add a wide array of functionality, in this case we would like to tell webpack to generate an index.html file for us and automatically inject our generated bundle into this file.

First we need to install the plugin needed for this `html-webpack-plugin`

`npm install --save-dev html-webpack-plugin`

Now we can edit out `webpack.config.js` to use the `html-webpack-plugin`
```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        bundle: './src/sample_index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './sample_index.html',
            filename: 'index.html'
        })
    ],
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    }
}
```

As shown above, plugins accepts a LIST of plugin objects, we only have one, our html-webpack-plugin which is configured to use our `sample_index.html` as a template for creating a new file called `index.html` which will contain a script tag that correctly links whatever generated bundle. This means we must edit our `sample_index.html` and remove our manual efforts of doing such.

`sample_index.html` now becomes

```html
<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <div id='root'>
            <input type='text' id='title-search-input'/>
            <button id='title-search-button'>Search</button>
            <div id='search-results'>
            </div>
        </div>
    </body>
</html>
```

Now if we run `npx webpack --config webpack.config.js` we'll see both our javascript bundle and an index.html file in `dist`. Now you can open `index.html` in the browser and test it out.


## Development

So far we have been manually refreshing our page and re-opening html files, that's ridiculous, let's fix that. Instead of using some other tool and creating some complex pipeline of building with one then live & hot reloading using another, thankfully Webpack provides a solution within the same ecosystem `webpack-dev-server`.

Let's install `webpack-dev-server`

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

Now copy your current configuration into a new file called `webpack.dev.config.js` and rename your old file `webpack.prod.config.js`

```
sample-frontend
  |- package.json
  |- package-lock.json
  |- sample_index.html
  |- webpack.dev.config.js
  |- webpack.prod.config.js
  |- /src
    |- sample_index.js
    |- art_helper.js
  |- /dist
    |- main.js
  |- /node_modules...
```

Edit  `webpack.dev.config.js` to reflect the following:

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        bundle: './src/sample_index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './sample_index.html',
            filename: 'index.html'
        })
    ],
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    devtool: 'inline-source-map', // allows error messages to be mapped to the code we wrote and not compiled version
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'dist'),
        },
        port: 3000,
        host: 'localhost',
        hot: true,
        open: true
    }
}
```

As you can see we added a **devServer** section to our configuration and in it described how we would like our development server to behave. Firstly `static` tells it where to find our code i.e. the same place where our code builds to, `host` and `port` are self explanatory, setting `hot` to true enables hot reloading ( keep the app running and to inject new versions of the files that you edited at runtime ), and finally setting `open` to true will open the browser when we run this configuration.

We've also added a devtool inline-source-map, this maps output ( logs and errors ) to the source code instead of the bundle, this way we can actually use our console to debug.

Now we have two different configurations a dev and prod

we can start the development server by running

`npx webpack serve --config webpack.dev.config.js`

Your browser should open on localhost:3000 with your app running, now whenever edit your javascript files the app will automatically reload with the changes.
Now note that I specifically said javascript files, this is because as is webpack will only understand javascript and json files. This is why we haven't used any CSS for example, we can use CSS by doing it the normal way ( adding a tag for it ) but we would loose out on the optimizations that webpack would provide when building our output. To make utilize files we need to make use of another section **module**.



## Module and Loaders

Webpack by default only accepts javascript and json files, loaders change this. It does this by adding the ability to load and process these other file types treating each almost as it were another javascript **module**. The ability to view these other files (e.g. a CSS file) as modules allows webpack to add these to their dependency graph and optimize our build.
Similar to plugins, there exists a wide array of loaders for various file types and the functionality. A loader, somewhat similar to plugin needs to be installed and then setup in our webpack configurations.

Let's add two loaders to help us with CSS 

`npm install --save-dev style-loader css-loader`

Now let's use them in our configurations

first `webpack.dev.config.js`

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        bundle: './src/sample_index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './sample_index.html',
            filename: 'index.html'
        })
    ],
    module: {
        rules: [
            { test: /\.css$/, use: ['style-loader','css-loader'] }
        ]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    devtool: 'inline-source-map', // allows error messages to be mapped to the code we wrote and not compiled version
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'dist'),
        },
        port: 3000,
        host: 'localhost',
        hot: true,
        open: true
    }
}
```

then `webpack.prod.config.js`

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        bundle: './src/sample_index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './sample_index.html',
            filename: 'index.html'
        })
    ],
    module: {
        rules: [
            { test: /\.css$/, use: ['style-loader','css-loader'] }
        ]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
}
```

As you can see we added a **module** section, where we define **rules** on what files we want read and how we would like our loaders to be used on those files. The two parts of each **rule** `test` and `use`, tell us exactly which files to look at and which loaders to use respectively. For our rule `test` basically says use any file that ends with '.css' and use the `style-loader` and `css-loader` to process these files. Rules and Loaders can be a bit tricky for example it executes in the order they are included in the configuration and a different ordering can produce a very different result.

Here some more [documentation on loaders](https://webpack.js.org/concepts/loaders/#configuration) and heres a [list of some loaders](https://webpack.js.org/loaders/).

Let's add some style, create a `style.css` in `src`

```
sample-frontend
  |- package.json
  |- package-lock.json
  |- sample_index.html
  |- webpack.dev.config.js
  |- webpack.prod.config.js
  |- /src
    |- sample_index.js
    |- art_helper.js
    |- style.css
  |- /dist
    |- main.js
  |- /node_modules...
```

You can do whatever you like with `style.css` tbh :P but here's a quick something in case you're busy

```css
#title-search-input {
    width:50vw
}
#title-search-button {
    width:30vw
}

#search-results{
    width: 100%;
    text-align: center;
}

.art_piece{
    width: 100%;
    margin: 30px 30px 30px 30px;
    padding:3%;
    -webkit-box-shadow: 0px 10px 18px 2px rgba(0,0,0,0.67); 
    box-shadow: 0px 10px 18px 2px rgba(0,0,0,0.67);
}
```

(edit: thanks to cswalker21 for reminding me to show this step)
Finally edit `sample_index.js` and import the `style.css` like such

```javascript
import './style.css';
```

I'm definitely not the best CSS guy so I encourage you to try out your own stuff.
Now let's run start our development server and see how it looks 

`npx webpack serve --config webpack.dev.config.js`

We can also test our build 

`npx webpack --config webpack.prod.config.js`

Now you're all set to start building and exploring on your own, the following are just some extra bits.



## Extra Bits

### Commands

In case you want to make things a bit simpler either for yourself or for those in your team, you can create a 'shortcut' for your commands using the `scripts` section of your `package.json` ( aka npm sripts ). 

For example instead of always writing `npx webpack serve --config webpack.dev.config.js` to start our development server, we can create a 'shortcut command' for example `npm run start` by editting the "scripts" section and adding a key "start" with value `npx webpack serve --config webpack.dev.config.js`.

```json
{
	...
	"scripts":{
		"test": "echo \"Error: no test specified\" && exit 1",
		"start": "npx webpack serve --config webpack.dev.config.js"
	}
	...
}
```
So now we can run `npm run start` to start our development server
We can also do something similar for building

```json
{
	...
	"scripts":{
		"test": "echo \"Error: no test specified\" && exit 1",
		"start": "npx webpack serve --config webpack.dev.config.js",
		"build": "npx webpack --config webpack.prod.config.js"
	}
	...
}
```
Now we can run `npm run build` to build our app

### TypeScript

Always wanted to try typescript but didn't know how to transpile to javascript for development or even building, let's do it.

Let's install what we need typescript itself and a loader for webpack

`npm install --save-dev typescript ts-loader`


Now let's add a rule to load .ts ( typescript ) files and also tell webpack what types of files we need to resolve ( module resolution deals with where to find what at build time ). ( see [module resolution](https://webpack.js.org/concepts/module-resolution/) )
First `webpack.dev.config.js`

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        bundle: './src/sample_index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './sample_index.html',
            filename: 'index.html'
        })
    ],
    module: {
        rules: [
            { test: /\.css$/, use: ['style-loader','css-loader'] },
            { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }
        ]
    },
    resolve: {
        extensions: ['.ts', '.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    devtool: 'inline-source-map', // allows error messages to be mapped to the code we wrote and not compiled version
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'dist'),
        },
        port: 3000,
        host: 'localhost',
        hot: true,
        open: true
    }
}
```

then `webpack.prod.config.js`

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        bundle: './src/sample_index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './sample_index.html',
            filename: 'index.html'
        })
    ],
    module: {
        rules: [
            { test: /\.css$/, use: ['style-loader','css-loader'] },
            { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }
        ]
    },
    resolve: {
        extensions: ['.ts', '.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
}
```

Finally we need to add a `tsconfig.json` to `sample-frontend` ( [tsconfig documentation](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) ) to describe how our typescript should be compiled

```
sample-frontend
  |- package.json
  |- package-lock.json
  |- sample_index.html
  |- webpack.dev.config.js
  |- webpack.prod.config.js
  |- tsconfig.json
  |- /src
    |- sample_index.js
    |- art_helper.js
    |- style.css
  |- /dist
    |- main.js
  |- /node_modules...
```

`tsconfig.json`

```json
{
    "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "es6",
        "target": "es5",
        "allowJs": true,
        "moduleResolution": "node"
    }
}
```

Congrats now we can write write typescript, as an example create `validation_helper.ts` in `src` and add the following:

```typescript
export const validateSearchQuery = (title: string, limit: number, offset: number) => {
    if(title.length == 0)
        throw new Error("Title is required");
    if(limit < 0)
        throw new Error("Limit must be greater than 0");
    if(offset < 0)
        throw new Error("Offset must be greater than 0");
    if(limit > 10)
        throw new Error("Limit must be less than 10");
    return true;
}
```

Now you can import `validadateSearchQuery` from `validation_helper.ts` and use it to well validate your query
We can edit `sample_index.js` to following:

```javascript
import { searchArt } from './art_helper';
import { validateSearchQuery } from './validation_helper';
import './style.css';

window.onload = ev => {
    init();
}

const init = () => {
    console.log('hello I am a sample index.js v5');
    document.getElementById('title-search-button').addEventListener('click', handleSearchClick);
}

const handleSearchClick = (ev) => {
    const title = document.getElementById('title-search-input').value;
    validateSearchQuery(title, 5, 10);
    searchArt({ title }).then(renderResults);
}

const renderResults = ({ artPieces }) => {
    document.getElementById('search-results').innerHTML = artPieces.join('');
}
```

Now we can develop and build using typescript.