Getting started with webpack - Part 6: Working with images
You will need Node 6.11.5+ installed on your machine.
In this part of the series, we will dig deeper into webpack to see what else is possible. We will specifically try to use other webpack plugins in our application to work with images.
In the previous part of the series, we have learned how to use plugins in webpack. We also learned about optimizations in webpack and how we can use some plugins as minimizers. We can use plugins to do many things while developing. In this article, we will consider a few uses specific to our application.
Let’s get started.
Source code of the application is available on GitHub.
Prerequisites
To follow along in this series, you need the following requirements:
- Completed all previous parts of the series.
- Basic knowledge of JavaScript.
- Basic knowledge of the CLI.
- A text editor. VS Code is recommended.
- Node.js (>=6.11.5) and npm installed locally.
Let’s continue with the series.
Minifying images using the imagemin plugin
When developing applications, we usually want to make our asset files smaller. Doing this will lead to smaller assets and thus a faster website. To achieve image compression, we will be using a webpack plugin called imagemin-webpack-plugin.
Before we add it to the application, let’s see how this will be implemented in our config file.
To add it to our webpack configuration, you would typically need to do something like this:
var ImageminPlugin = require('imagemin-webpack-plugin').default
module.exports = {
plugins: [
// Make sure that the plugin is after any plugins that add images
new ImageminPlugin({
disable: false,
pngquant: {
quality: [0.3, 0.5]
},
})
]
}
Above, we imported the plugin and then we stored it to the ImageminPlugin
variable. Next, in the actual webpack plugin, we instantiate the plugin and pass the following options to it:
disable
: this accepts a boolean value. Iftrue
, the plugin will be disabled. We would typically disable the plugin during development.pngquant
: this accepts an object which will be the options for the imagemin pngquant plugin. To see the available options, check here.
There are other options we can specify, you can see all the options here.
One thing to remember though is, when you are adding other webpack plugins that work with images, you should always add them before the imagemin plugin.
Adding the imagemin plugin to our project
In this part, we will be building off the code in the previous part. If you don’t have it already, you can download the project code from GitHub. We will be using the code there as a base for the modifications we are going to make going forward. When you have downloaded the project, open Part-5
in your code editor and follow along.
Before we get started, run the following command in the root of the project to install the npm dependencies:
$ npm install
To get started, we need to decide the plugins we want to use and then install them using npm. After installing them, we will activate them in the webpack configuration file.
The plugins we will use are as follows:
- imagemin-webpack-plugin - to compress images
- copy-webpack-plugin - to copy compressed images around
- url-loader - a loader for webpack which transforms files into base64 URIs
- file-loader - resolves
import
/require()
on a file into a URL and emits the file into the output directory.
Let’s start adding them one after the other.
Loading images in our project
First, we will start with loading images in our project. For this, we need both file-loader
and url-loader
. To install them, run the following command:
$ npm install url-loader file-loader --save-dev
When the installation is complete, open the webpack configuration file and replace the contents with the following:
// File: ./webpack.config.js
const webpack = require('webpack');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const env = process.env.NODE_ENV;
module.exports = {
mode: env == 'production' || env == 'none' ? env : 'development',
entry: path.resolve(__dirname + '/src/index.js'),
output: {
path: path.resolve(__dirname + '/dist/assets'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.(png|jp(e*)g|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8000,
name: 'images/[hash]-[name].[ext]',
publicPath: 'assets',
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css'
}),
new OptimizeCssAssetsPlugin({
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }]
}
})
]
};
Above, we just added a new rule to the list of rules. We added the rule to look for images and pass them through the url-loader
. The test we run for images is /\.(png|jp(e*)g|svg)``*$*``/
which will match images with the following extensions: png, jpg, jpeg, svg.
We also specified some options
for the url-loader
:
limit
- when the image file size is smaller than 8000 bytes (8kb), the image is converted to base64 format and passed as thesrc
of the image. This helps save a DNS request and thus make your application faster. If the size is greater than 8000 bytes, the image is passed to thefile-loader
which will load the image normally.name
- this is passed to the file loader in the situation where the file size is greater than 8000 bytes.
Now that we have configured that, let’s download this icon and this icon from font-awesome. After downloading them, place them in the src/images
directory. The icons we downloaded are both below 8000 bytes so we will use this to demonstrate the base64 URL that the url-loader
generates.
Open the index.js
file and import both images and add them to the HTML as seen below:
// File: ./src/index.js
// [...]
import passwordIcon from './images/lock-solid.svg';
import copyIcon from './images/copy-solid.svg';
document.getElementById('copy_icon').src = copyIcon;
document.getElementById('password_icon').src = passwordIcon;
Next, open the index.html
file and replace the contents with the following:
<!-- File: ./dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Webpack Sample</title>
<link rel="stylesheet" href="/assets/main.css" />
</head>
<body>
<div class="container">
<div class="text-center">
<img id="password_icon" />
<h1 id="random_password"></h1>
<img id="copy_icon" />
</div>
</div>
<script src="./assets/bundle.js"></script>
</body>
</html>
Next, open the style.scss
file and append the following code:
/* File: ./src/style.scss */
.text-center {
text-align: center;
}
#password_icon,
#copy_icon {
width: 20px;
}
#random_password {
display: inline-block;
margin: 0 10px;
}
Next, run the npm command to build the application:
$ npm run build
Now you can run the server to see the changes:
$ node dist/server.js
If all went well, you should see both images and if you Inspect Element and view the image source, you’ll notice it’ll be the Base64 representation.
Before we demonstrate the other way url-loader
handles images, let’s implement the copy to clipboard feature.
Open the src/index.js
file and replace:
document.getElementById('copy_icon').src = copyIcon;
With
const copyIconElem = document.getElementById('copy_icon');
copyIconElem.src = copyIcon;
copyIconElem.onclick = () => {
copyToClipboard(document.getElementById('actual_password').innerText);
alert('Copied to clipboard');
};
Next, create a new file ./src/utilities/copyToClipboard.js
and paste the following into it:
// File: ./src/utilities/copyToClipboard.js
const copyToClipboard = str => {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
};
export default copyToClipboard;
The code above is just a function that copies the passed parameter to the clipboard.
In your src/index.js
file, import the module you just created at the top:
// File: ./src/index.js
// [...]
import copyToClipboard from './utilities/copyToClipboard'
// [...]
Next, in the same file, replace:
document.addEventListener('DOMContentLoaded', () => {
const randomStringGenerator = new RandomStringGenerator();
const randomStr = `Random String: <span>${randomStringGenerator.generate()}</span>`;
window.setTimeout(
() => (document.getElementsByTagName('h1')[0].innerHTML = randomStr),
0
);
});
With
document.addEventListener('DOMContentLoaded', () => {
const randomStringGenerator = new RandomStringGenerator();
const randomString = `Random String: <span id="actual_password">${randomStringGenerator.generate()}</span>`;
document.getElementById('random_password').innerHTML = randomString;
});
Now you can build the app and start the server if not already running. The copy to clipboard function should work now.
Loading images with full URLs
Now that we have demonstrated Base64 URLs for images, let’s demonstrate how larger images will be handled. Download an illustration from here and save it to your src/images
directory. We are saving ours as security.svg
.
To get started, open the src/index.js
, import the image:
// File: ./src/index.js
// [...]
import securityIllustration from './images/security.svg';
document.getElementById('header_image').src = securityIllustration;
// [...]
Next, open the dist/index.html
file and update as seen below:
<!-- File: ./dist/index.html -->
<!-- [...] -->
<div class="container">
<div class="text-center">
<img id="header_image" />
<!-- [...] -->
</div>
</div>
Now, open the ./src/style.scss
and append this:
#header_image {
max-width: 500px;
margin-bottom: 100px;
}
Finally, open the dist/server.js
and replace the content with the following:
// File: ./dist/server.js
const express = require('express');
const app = express();
const port = 3000;
const path = require('path');
app.use('/assets', express.static(path.join(__dirname, 'assets')));
app.get('/', (req, res) => res.sendFile(path.join(__dirname + '/index.html')));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
Above, we are using express to serve all the static files.
Now that we have completed that, let’s build the application and start the server. To do this, run the following commands:
$ npm run build
$ node dist/server.js
If you had the server running before, you need to stop it and restart it.
That’s all, we have our images compiled, compressed, and presented using our webpack plugins and loaders.
Conclusion
In this part of the series, we have learned how to work with images in webpack. We learned how to compress images and copy them to our public directory using webpack. However, Webpack is a lot more powerful than this. We will dive a little deeper in the next part.
The source code to this application is available on GitHub.
19 February 2019
by Neo Ighodaro