Webpack, Parcel, Babel, blah, blah. . .why do I need a module bundler?
A module bundler is a base for many projects and frameworks. Usually, we don't pay much attention to these underlying tools. But maybe we should?
Last updated: November 7, 2022We all know three ingredients to build a website: HTML, CSS, and JavaScript. HTML is like nouns in sentences, CSS is like adjectives, and JavaScript is like verbs. HTML defines the structure, CSS styling, and JavaScript functionality. But is it enough nowadays?
The first websites in the 1990s were simple. They were static documents with hardly any styling or functionality. You could write some HTML, link it to a stylesheet and script (if any), and the website was ready. In the 90s, I was making my first steps (literally), so for my first steps in web development, some time must have passed. But even for me, this simple method of building websites is familiar. It reminds me of my first pages.
However, modern web apps are much more complex. They are highly interactive. They consist of multiple assets like images in different formats, videos, fonts, and third-party modules. Additionally, people use technologies like Typescript, React, or SASS instead of those fundamental languages. Multiple files with various extensions import each other and different assets. But it is not available natively in the browser. Modern browsers only just have started to support module functionality natively. And you can't import assets inside a js file. To resolve these problems, we need a tool.
What is a module bundler?
A module bundler solves mentioned problems. Fundamentally, the module bundler takes multiple js files (modules) and combines them into a single file (bundle) that can build your app in a browser. Apart from your local modules, it will bundle all third-party dependencies. Those dependencies can also have their dependencies. A module bundler like webpack will create a dependency graph to keep track of them. Of course, it's a simplification. To dive deeper and learn more features, let's config a webpack project.
With CLI like create-react-app
or frameworks like Gatsby, the webpack comes pre-configured. Still, I think it's good to know a little about what's under the hood.
Webpack
First, we need to initiate a new npm project.
🔴 🟡 🟢
npm init -y
Secondly, let's install webpack
🔴 🟡 🟢
npm install -D webpack webpack-cli
We need something to bundle, so let's add a simple JavaScript file.
./src/index.jsJS
1const exampleFunction = () => {2 console.log('Message')3}
Webpack will work without any configuration. When you type webpack
in the terminal, it will bundle your file with the default config. However, most often, you want to add a custom configuration. To customize webpack behavior, we need to add a webpack config file. The file needs to export an object with custom settings.
Entry
Entry is like a starting point for your application. The option takes a path to a js module. Webpack will use the module to start building its dependency graph. We'll set it to ./src/index.js
, which is also a default value. It can also take an object for multiple entry points and code splitting.
webpack.config.jsJS
1module.exports = {2 entry: './src/index.js'3}
Output
Output customizes where webpack will put our bundle. It takes an object with two parameters: path
and filename
. We'll customize the webpack to emit our bundle to ./public/main.js
.
webpack.config.jsJS
1const path = require('path')23module.exports = {4 entry: './src/index.js',5 output: {6 filename: 'main.js',7 path: path.resolve(__dirname, 'public')8 }9}
Loaders
Let's say we also want to style our app a bit. We create a style file with a variable.
./src/style.cssCSS
1:root {2 --eiffel-65: blue;3}
And import it into our app.
./src/index.jsJS
1import './style.css'
It may surprise you if you are used to frameworks, but this syntax won't work. We need a way to transform our CSS file into a valid module. And webpack on its own doesn't do much. It supports only JSON and JavaScript files out of the box. That's why we need a loader. A loader will convert different types of files into valid modules. To use CSS styling, we need to install two loaders.
🔴 🟡 🟢
npm install -D style-loader css-loader
Then we need to configure them. A loader needs two properties to work:
- The
test
property identifies files to transform. - The
use
property specifies a loader to use.
In our case, we'll use a regular expression to identify all CSS files, and we'll use installed earlier loaders.
webpack.config.jsJS
1const path = require('path')23module.exports = {4 entry: './src/index.js',5 target: ['web', 'es5'],6 output: {7 filename: 'main.js',8 path: path.resolve(__dirname, 'public')9 },10 module: {11 rules: [12 {13 test: /\.css$/,14 use: ['style-loader', 'css-loader']15 }16 ]17 }18}
Babel
We need to digress a bit. There could be another problem with our file - namely, the arrow function. Just as Yahweh confounded people's speech, the gods of browsers mixed JavaScript implementations. Even though most ES6 features work with modern browsers, the support is not 100%. Let's say we want to support Internet Explorer for whatever reason. We need a way to transform the syntax. We could use a polyfill, but with more code and more features, it would become cumbersome quickly. JavaScript is developing rapidly, and new syntax frequently emerges: async/await, the spread operator, classes, etc. Some posts ago, I even wrote about a useful feature I wasn't aware of - optional chaining. At this moment, like a gift of glossolalia from a God, comes Babel. Babel takes modern JavaScript and compiles it into a form understood by different browsers. Babel uses plugins to transform various JavaScript features like plugin-proposal-optional-chaining
. Babel plugins are small, and we don't want to list them independently. Instead, we can specify a preset with multiple features. We'll use babel-preset-env
. It allows defining the level of compatibility you need for the browser you intend to support. We'll also add a preset for React.
🔴 🟡 🟢
npm install -D @babel/core @babel/preset-env @babel/preset-react babel-loader
🔴 🟡 🟢
npm install react react-dom
You can use Babel independently, but we'll configure it with webpack. We need to add another loader inside the rules
array. This time we need to exclude node_modules
and add presets, so the use
property takes an object.
webpack.config.jsJS
1const path = require('path')23module.exports = {4 entry: './src/index.js',5 target: ['web', 'es5'],6 output: {7 filename: 'main.js',8 path: path.resolve(__dirname, 'public')9 },10 module: {11 rules: [12 {13 test: /\.css$/,14 use: ['style-loader', 'css-loader']15 },16 {17 test: /\.js$/,18 exclude: /(node_modules|bower_components)/,19 use: {20 loader: 'babel-loader',21 options: {22 presets: ['@babel/preset-env', '@babel/preset-react']23 }24 }25 }26 ]27 }28}
Now, we can create a simple React component.
./src/component.jsJSX
1import React from 'react'23const Heading = () => {4 return (5 <h1 style={{ color: 'var(--eiffel-65)' }}>6 This is heading with style variables7 </h1>8 )9}1011export default Heading
And import it alongside styles inside the index file and then render it.
./src/index.jsJSX
1import React from 'react'2import { createRoot } from 'react-dom/client'3import './style.css'4import Heading from './component'56const root = createRoot(document.getElementById('root'))7root.render(<Heading />)
If we configured everything correctly, the webpack should still work. But our bundle should contain much more code this time because we also bundle react library. We can even check what creates our bundle.
Plugins
Plugins are more powerful versions of loaders. You can use them to perform a broader range of tasks like bundle optimization or asset management. We wanted to analyze our bundle, and fortunately, there is a plugin we can use. First, let's install it.
🔴 🟡 🟢
npm install -D webpack-bundle-analyzer
Then we need to add the plugins
property to our config. It takes an array with all the plugins. We will create an instance of our plugin object there.
webpack.config.jsJS
1const path = require('path')2const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')34module.exports = {5 entry: './src/index.js',6 output: {7 filename: 'main.js',8 path: path.resolve(__dirname, 'public')9 },10 module: {11 rules: [12 {13 test: /\.css$/,14 use: ['style-loader', 'css-loader']15 },16 {17 test: /\.js$/,18 exclude: /(node_modules|bower_components)/,19 use: {20 loader: 'babel-loader',21 options: {22 presets: ['@babel/preset-env', '@babel/preset-react']23 }24 }25 }26 ]27 },28 plugins: [new BundleAnalyzerPlugin()]29}
Now, after running the webpack, a browser tab should open. You should see an interactive map with proportionally scaled rectangles. They symbolize project dependencies with additional pieces of information. In my opinion - pretty cool plugin.
Summary
Now we know why we bother to use these side technologies. A module bundler like webpack can transform our code and assets and make them compatible with various browsers. In this post, we configured webpack, but there are alternatives like parcel or rollup. They may differ in details, but the core idea is the same. A properly configured module bundler with Babel can take care of different language/browser quirks and make our life as developers less annoying.