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
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.
Before we start with actual installation, we need to know some basic terminology which is involved in Webpack.
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.
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.
Let’s create a folder called webpack-starter
mkdir webpack-startercd 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 src
directory and index.js
file inside it.
mkdir src
Let’s modify npm
script 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 configurationThe '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.
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.
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.js
file
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.
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](https://www.npmjs.com/package/webpack-dev-server)
. So let’s setup our dev server.
npm i webpack-dev-server -D
Go to your package.json
and 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']
},
.....}
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](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.
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](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.
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](http://localhost:8080.You)
.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:
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 :)