This post is taken from my blog so be sure to check it out for more up-to-date content π
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-loader and 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 scripts to 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. π
Top comments (4)
Thanks, it's a very usefulf guide. However, I would recommend Terser instead of Uglify for ES6+ support.
I haven't used Laravel Mix outside of Laravel yet but it should work on any type of application, it is a Webpack wrapper that makes it a lot easier to use: laravel-mix.com/
If we're into wrappers, then my personal best is PoiJS which has really good support for JS frameworks - Vue especially. π But Mix looks cool.
I will give it a try, thanks for the suggestion.