Site Color

Text Color

Ad Color

Text Color

Evergreen

Duotone

Mysterious

Classic

Sign Up to Save Your Colors

or

Let’s Get Started with Webpack 4 by@vikasharry03

Let’s Get Started with Webpack 4

image
Vikas Yadav Hacker Noon profile picture

Vikas Yadav

Data Scientist

Few days ago, I wanted to build a static website with simple SCSS and ES6 which I can host somewhere. So I decided to setup a simple project which can convert my SCSS to CSS and ES6+ code to ES5. I decided to use Webpack 4. This article is based on this learning. Code base for this article can be found in my Github.

Agenda

  1. Introduction to Webpack 4
  2. Core terminology of Webpack
  3. Prerequisite
  4. Getting ready for Webpack
  5. Setting up config file from scratch
  6. Working with HTML
  7. Webpack dev server
  8. Working with ES6+ and Typescript
  9. Working with SCSS and CSS
  10. Loading static resources
  11. Working with different environments

Introduction to Webpack 4

As per the official website

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

Our modern JavaScript application can have different modules and these modules can be dependent on each other. Webpack take these modules with dependency and generate bundles out of it.

Webpack 4 by default is zero config(We will see about it when we start with project). Being zero config does not mean you can not configure anything in Webpack. It is highly configurable to suit your need.

Core terminology of Webpack 4

Before we start with actual installation, we need to know some basic terminology which is involved in Webpack.

  1. Entry

For building dependency graph Webpack need to know where it should start. From there onward it starts looking for other modules on which your root module depends. Default entry is ./src/index.js But you can set it to different module also:

module.exports = {
entry: './path/to/my/entry/file.js'
};

Multiple entry points is also possible:

module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
}
};

2. Output

Output config tell webpack where to write final files. Usually this is dist/ or builds/ folder. This can be named whatever you want.

const path = require('path');

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

In case of multiple entry point we need to we to slightly different config:

const path = require('path');
module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
},
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist'
}
};

Above configuration for output will result in two files in dist folder named as main.bundle.js and vendor.bundle.js

3. Loaders

Loaders allows us to transform our files for example: Transpile .ts files to .js or .scss to css. There is verity of loaders available in Webpack ecosystem like file-loader, url-loader etc.

4. Plugins

Plugins play very important role in Webpack configuration. Loaders are limited to converting modules from one form to another whereas plugins are responsible for Build optimization and asset management. We will look into it in more detail later in the article.

So this is just some basic terminology before we start with actual configuration. If you want to read more about core concepts you can refer to official page of Webpack.

3. Prerequisite

Before we start we need to have node installed in our machine. To check whether it is installed in your machine. Run

node -v 

If it does not show any version it means node is not installed.
To install node go to Node JS and download and install.

4. Getting ready for Webpack

Let’s create a folder called webpack-starter

mkdir webpack-starter
cd webpack-starter

Now initialize npm

npm init

After successful run you can see a package.json file in your webpack-starter root folder.

Now we will download webpack and webpack-cli from npm repository to run it. For this run

npm install webpack webpack-cli -D

It will add these two dependencies in package.json file.

Now we will create a srcdirectory and index.js file inside it.

mkdir src

Let’s modify npmscript in package.json

"scripts": {
"dev": "webpack"
}

Now run following command form your root folder

npm run dev

You will see a dist folder created with main.js file in your root folder.

You will see a dist folder created with main.js file in your root folder.

But there is a warning in console.

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

This warning is coming because we need to set a mode. Lets do that and hit npm run dev

"scripts": {
"dev": "webpack --mode development"
}

Now you can see that there is no warning and chunk is generated.

But wait !!! How is webpack doing it? I have not configured anything. 
Remember in the beginning I told you that Webpack 4 is zero configuration. Actually it is comes with default configuration which search for index.js file inside src folder and assumes that it is the entry point for building dependency graph. It also assumes that output folder is dist and emits converted files there.

5. Setting up config file from scratch

We will now look into how to write our own configurations so that we can have greater control on what webpack should do.

Let’s create webpack.config.js file in root directory. I have also created src/app/ directory and src/index.html .

Now open webpack.config.js and put following code for entry and output.

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

Delete the dist folder from root and hit npm run dev command. You will find same result.

6. Working with HTML

You have noticed one thing that when we ran npm run dev it is only putting main.js file into dist folder. Wepack is not copying our index.html file. For that we need to use a plugin html-webpack-plugin
So install html-webpack-plugin

npm i html-webpack-plugin -D

Now plugins section in you webpack.config.js file inside module.exports (Do not delete entry and output :P):

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
....
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack 4 Starter',
template: './src/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: false
}
})
]
}

Change index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title> <%= htmlWebpackPlugin.options.title %> </title>
<meta name="viewport" content="width=device-width,initial- scale=1">
</head>
<body>
<h3>Welcome to Webpack 4 starter </h3>
</html>

We are using htmlWebpackPlugin.options.title for adding title, which we have declared in our config file. Also put a log in your index.jsfile

console.log('This is index JS')

Now delete your dist folder and hit npm run dev command.

You will see index.html file inside dist and title is replaced by our title of config file. You will also notice that webpack has already added a script tag containing generated script main.js. You do not need to add script file manually. Isn’t that cool!!!

If you open dist/index.html file in browser and open inspector, you will see following result.

6. Webpack dev server

During the development you need a mechanism to serve your files from a local server and auto reload changes to avoid refreshing browser again and again after you make any changes.

To address this Webpack has webpack-dev-server. So let’s setup our dev server.

npm i webpack-dev-server -D

Go to your package.jsonand add following to your scripts :

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

After this go to root directory in terminal and run npm start.You can see this in your terminal

And if you go to http://localhost:8080/. You can see your welcome page. Now try changing something in /src/index.html and hit save, you can see your changes in browser without hitting refresh. :) For more information refer to webpack-dev-server .

Resolving common extensions

Sometimes we do not like to write extension while we import the modules. We can actually tell webpack to resolve some of the extensions if we do not write them. For that add following code in your webpack.config.js

module.exports {
.....
resolve: {
extensions: ['.js', '.ts']
},
.....
}

7. Working with ES6+ and Typescript

So with our dev server running now we will move to transpile our ES6+ code and Typescript to ES5 because majority of browsers can only understand ES5.

a. Transpile ES6+ to ES5

For this we need a loader. Yes! finally !!! some loader. So we need 
babel-loader @babel/core @babel/preset-env

npm i babel-loader @babel/core @babel/preset-env -D

So update your config file as follows

...
module.exports = {
......
module: {
rules: [
{
test: [/.js$/],
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
}
]
},
......
}

So what does this basically mean? We can write different rules to load modules inside module . test is basically a regex. test: [/.js$/] here we are saying that match all the files which ends with .js and exclude node_modules. babel-loader will actually transpile these modules/files to ES 5 using preset-env.

So it is time to test that this rule is actually doing something. For this we will add header.js in src/app folder and add a ES 6 code.

export class Header {
  constructor() {
    console.log(`This is header constructor`);
  }
  getFirstHeading() {
return `Webpack Starter page`;
}
}

Now we need to import this to our index.js file.

import { Header } from './app/header';
let header = new Header();
let firstHeading = header.getFirstHeading();
console.log(firstHeading);

We are simply importing the header and making and object and calling its getFistHeading method.

Run npm start and open http://localhost:8080 in browser. You can see following result:

b. Compile Typescript to ES 5

So everything is working as expected. Let’s move to compiling our typescript to ES5 . For this we just need to install dependency and change the existing rule. First thing first, install the dependency

npm i @babel/preset-typescript typescript -D

We change the rule we have just written for ES 6 conversion:

module: {
rules: [
 {
   test: [/.js$|.ts$/],
   exclude: /(node_modules)/,
   use: {
     loader: 'babel-loader',
     options: {
      presets: [
       '@babel/preset-env',
       '@babel/typescript'
      ]
}
}
}
]
}

We also need to add a tsconfig.json file in root directory. Read more about tsconfig.json here.

{
"compilerOptions": {
  "target": "esnext",
  "moduleResolution": "node",
  "allowJs": true,
  "noEmit": true,
  "strict": true,
  "isolatedModules": true,
  "esModuleInterop": true,
  "baseUrl": ".",
 },
"include": [
  "src/**/*"
 ],
"exclude": [
  "node_modules",
  "**/*.spec.ts"
 ]
}

And that’s it. It is ready to test. We will test it same way we tested Header.

We will create a file footer.ts in src/app/ and add following content.

export class Footer {
footertext: string;
  constructor() {
console.log(`This is Footer constructor`);
this.footertext = `Demo for webpack 4 set up`;
}
  getFooterText(): string {
return this.footertext
}
}

We need to import it inindex.js as we did with header.js module.

import { Footer } from './app/footer';
......
let footer = new Footer();
let footerText = footer.getFooterText();
console.log(footerText);
.......

Let’s run it. npm start and navigate to http://localhost:8080.

8. Working with CSS and SCSS

For loading the CSS styles we need two loaders css-loader and style-loader.

css-loader take all the styles referenced in our application and convert it into string. style-loader take this string as input and put them inside style tag in index.html . To install dependencies :

npm i css-loader style-loader -D 

We need to write a rule that match all .css extensions and use these loaders to load them in index.html file.

module: {
......
rules: [
.....
{
test: [/.css$/],
use:[
'style-loader',
'css-loader'
]
}
]
......
}

Let’s create a style.css file inside src folder and and put some styles in it.

Now we need to import style.css into index.js. Put following code in index.js

import '../src/style.css';

Now stop the dev server and run it again npm start. Navigate to http://localhost:8080.You can see your h3 to be red and if you inspect the DOM you will find a style tag added there with your styles in it. :)

Convert SCSS to CSS

For this we need two dependencies to be added that is node-sass and sass-loader. sass-loader simply conver your .scss files to .css using node-sass bindings. If you want you can read about node-sass more in depth here.

npm i node-sass sass-loader -D

We need to change our rule to match a .scss file.

module: {
......
rules: [
.....
{
test: [/.css$|.scss$/],
use:[
'style-loader',
'css-loader',
'sass-loader'
]
}
]
......
}

Always remember order in which loaders are being loaded matters. So do not change the order. Let’s create folder structure for our scss files. So create src/styles/scss directory and main.scss file in it with code in screenshot.

Now import in into index.js

import './styles/scss/main.scss';

Now its time to run the code. npm start and go to http://localhost:8080. You can see the results.

Extract all styles into a single file

Sometimes we do not want to add the styles in inline style tag rather we want it into a separate style file. For this we have a plugin called mini-css-extract-plugin .

npm i mini-css-extract-plugin -D

We need to update our webpack.config.js file as usual.

....
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
....
module.exports = {
.......
module: {
rules: [
......
 {
   test: [/.css$|.scss$/],
   use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
   'sass-loader'
  ]
 }
]
},
plugins: [
.........
new MiniCssExtractPlugin({
filename: 'style.css'
})
]
}

Now if you run npm start and navigate to http://localhost:8080. You can see following results:

As you can see in inspector window that a link to external css file has been added instead of inline style tags.

Adding post-css

Post css is help us in transforming css with js plugin. Auto prefixer is popular post css plugin which help us to add browser prefixes. To install dependencies of both :

npm i postcss-loader autoprefixer -D

Create a postcss.config.js inside the root directory and following code.

module.exports = {  
plugins: [
require('autoprefixer')
]
}

Now go to src/styles/scss/main.scss and update the h3 styles.

h3 {
background-color: $h3-bg-color;
user-select: none;
}

user-select prevents text selection and this property needs browser prefix. Now if you go to the terminal and run npm run dev and go to generated dist/main.css you will see user-select is auto prefixed.

10. Loading static resources

For loading static content we need a loader called file-loader.

npm i file-loader -D

Now we need to modify our rule in config file.

rules: {
....
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'assets/images'
}
}
]
}
.....
}

For testing create a directory src/assets/images and put some image. I am putting a .gif of webpack. You can download it from here .

Now we need to import it inside our index.js .

import webpackgif from './assets/images/webpack.gif';

Let’s create a img tag in index.html . We will set src attribute from javascript file. Add following code into index.html body tag.

<img id="webpack-gif" alt="webpack-gif">

Add following code to index.js .

.....
import webpackgif from './assets/images/webpack.gif';
.......
// setting the source of img
document.getElementById('webpack-gif').setAttribute('src', webpackgif);

Now if you run npm start and navigate to http://localhost:8080.You can see following results:

You might be wondering why I did not add src of img directly into the html? Why did I add it through JavaSript? Actually file-loader only load those assets which are referenced into our modules (in JavaScript or Typescript files).

If I directly add it to html and delete image import. We will get following result:

So to overcome this issue we need plugin called copy-webpack-plugin. This will copy our static assets to the folder we specify.

npm i copy-webpack-plugin -D

Update the config file:

....
const CopyWebpackPlugin = require('copy-webpack-plugin');
......
module.exports = {
.....
plugins:[
{
new CopyWebpackPlugin([{
     from:'./src/assets/images',
      to:'assets/images'
   }])
}
.....
}

Now if you run npm start and navigate to http://localhost:8080. You can see following results:

11. Working with different environments

When we prepare our assets and files for final deployment we need to keep our bundle size as less as possible. For this reason we need to minify our css and Javascript files. We can do that with one single config but it is good practice to divide config file into different files so that there will be clear separation of responsibilities. Due to this is reason its is common practice to have following files :

a. webpack.common.config.js

b. webpack.dev.config.js

c. webpack.prod.config.js

As the name suggest, common file will have all the common configs. We will use webpack-merge library to merge common config to dev and prod configuarations. So let’s install webpack-merge

npm i webpack-merge -D

Now we will create a config directory in our root folder and create above mentioned files inside it.

We will copy all content of webpack.config.js file to webpack.common.config.js file.

Update the output of webpack.common.config.js file as follows: (dist is changed to ../dist)

output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].js'
}

Now we will put following code to webpack.dev.config.js

const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.common.config.js')
module.exports = merge(webpackBaseConfig, {})

Delete content of webpack.config.js and put following code

const environment = (process.env.NODE_ENV || 'development').trim();
if (environment === 'development') {
module.exports = require('./config/webpack.dev.config.js');
} else {
module.exports = require('./config/webpack.prod.config.js');
}

This code is basically saying that if the NODE_ENV is development then use webpack.dev.config.js else webpack.prod.config.js By default it will take development.

Set environment variable

To set the environment variable for dev and prod we need a npm package called cross-env. This npm package insures that environment variable is set properly in every platform. You can read more on this here.

npm i cross-env -D

Now change the npm scripts :

"scripts": {
"build:dev": "cross-env NODE_ENV=development webpack --mode development",
"build:prod": "cross-env NODE_ENV=production webpack --mode production",
"start": "webpack-dev-server --mode development"
}

Clearing Dist folder via plugin

You might have noticed that I ask you to manually delete the dist folder whenever I run build commands. We can overcome this by using another plugin called clean-webpack-plugin.

npm i clean-webpack-plugin -D

We need to modify our common config:

const CleanWebpackPlugin   = require('clean-webpack-plugin');
module.exports = {
....
plugins: [
....
new CleanWebpackPlugin()
.....
]
}

We will proceed to prod configuration in a bit. Let’s test dev configuration first.

Now run npm run build:dev.You can see dist folder is generated with all the assets required.

Prepare resources for Production

For minifying the Javascript we need a plugin called uglifyjs-webpack-plugin

npm i uglifyjs-webpack-plugin -D

For css optimization we need another plugin called optimize-css-assets-webpack-plugin

npm i optimize-css-assets-webpack-plugin -D

After installing this update the webpack.prod.config.js :

const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const webpackBaseConfig = require('./webpack.common.config.js');
module.exports = merge(webpackBaseConfig, {
 optimization: {
   minimizer: [
     new UglifyJsPlugin(),
     new OptimizeCSSAssetsPlugin()
   ]
 }
});

Now everything is done you can generate the build for prod via npm run build:prod

You can see the difference between size of files generated in prod and dev build

Size difference is very less as we do not have larger code base.

We can do one final thing that we can implement hashing while generating the build. It is very simple we just need to add [chunkhash] in output files and mini-css-extract plugin.

Update your common config as

....
module.exports = {
...
output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].[chunkhash].js'
 }
.....
plugins: [
....
new MiniCssExtractPlugin({
   filename: 'style.[chunkhash].css'
 }),
...
]
}

Now if you generate the build via npm run build:prod you will see a hash appended at the end of files.

Kindly suggest any improvements and do share, subscribe and clap. Thanks :)

Tags