Creating a fireworks project using Webpack

No-nonsense webpack project

Or: How to use Webpack to build a project from scratch?

Using webpack without fully understanding it, as if it were magic? Heard about the wonders of webpack, but got flustered by its ominous documentation? Are you from outer-space (like — a totally different galaxy) and haven’t heard of webpack at all? In any of these cases (and many more), this post is for you!

Webpack: The Final Frontier. These are the voyages of the flagship app of your enterprise (or just your pet project). Its tl;dr mission: to boldly go where many developers have gone before!

What is webpack? In one word — a bundler. In a few words, webpack is a piece of code that takes your beautifully structured JavaScript (JS) project and bundles it into static files for production, all with a single command in the command-line interpreter (CLI). Magic!

What are we going to build?

An awesome app, that’s what. It’s called “Your Phrase Fireworks” (abbreviated YOPF). YOPF’s vision: You enter a phrase into an input field and the phrase is shown surrounded by crazy-cool exploding fireworks. Can’t get much more awesome than that.

Click here for the app’s demo

Since you are a developer, I assume you are using a modern browser like Chrome or Firefox. If not, please do — we will go over browser compatibility and webpack later on.

Let’s build a Webpack project

Assumptions

  1. You know some JS, HTML and CSS (nothing fancy —The basics will do)
  2. You already have Node.js installed. If not, install it and come right back
  3. You know how to edit files (while I prefer WebStorm IDE, even Notepad will do — although you might prefer Sublime or VSCode)
  4. You know how to browse folders on your machine

Setup the app

Browse to a projects folder on your machine (either via CLI or any visual file explorer). I’ll provide the CLI instructions below, but feel free to use your own methods for file/folder creation.

Every project begins the same way — by creating your project’s folder. To do so, enter the following command line prompts:

mkdir my-project

then:

cd my-project

then:

npm init -y

Amazing! We now have an npm module. Next, let’s install webpack:

npm i webpack -D

Since this is a real project, we’ll need a few additional prerequisites:

  1. Source control (Git)
  2. A spec for our app

The first is easy; if you don’t have Git installed, install it. Now, in your project’s folder, run this:

git init -q

Great! We have Git! We won’t touch it in the tutorial, but it’s good practice.

On to the second, a spec. Since this is just a blog post, we won’t put too much into this, but the basic gist of our spec would be as follows: We want to build an app in which you type something into an input, click a submit button, and then what you entered into the input appears on a screen with fireworks exploding around it. This app will be called YOPF, short for “Your Phrase Fireworks.”

Let’s create our source-files folder:

mkdir src

You’re getting good at this :). Can you guess what’s coming next? You got it:

cd src

Now, Let’s create an app.js file:

. > app.js

If you’re copying my commands above, don’t mind the error; this is just my hacky way of creating a new file on Command Prompt (CMD), Windows’ command-line interpreter. Feel free to use different CLI commands if they suit you better.

Let’s create our CSS and HTML files:

.>app.css & .>index.html

Wow — two files with the same CLI command. Can it get any better than that? Yes it can! Let’s edit the files.

To begin with, we’ll put something into our index.html file:

<div id="phrase-form-wrapper"></div>
<div id="phrase-fireworks-wrapper"></div>

Cool. We have our wrappers. Since we know that modularity is the name of the game (we do know that, right?), let’s create some modules. I really like it when my apps look the same at every level (and you should be like me… I write blog posts!), so let’s create two more folders inside the src folder:

mkdir form & mkdir fireworks

…and in each, lets create the same files:

cd form & .>form.index.js & .>form.index.html & .>form.css 
& cd ../fireworks & .>fireworks.index.js & .>fireworks.index.html & .>fireworks.css

Now we have an app with two modules. One for the phrase form, one for the fireworks and a “main module” to bind them both to one app.

Configure webpack

We also need a webpack.config.js file, which will hold, well… our webpack configuration. What does that mean? It means that, in order for webpack’s “magic” to work, you need to tell it what to do. You can name this file abracadabra or legerdemain or anything that suits your fancy. Thing is — nothing is magical (sorry, Jeremy Messersmith…); all that “magical” bundling is a result of the configuration you set up.

In general there are three parts to webpack configuration:

General configuration: Tell webpack where the “main” file is (the webpack term is entry), where and how to output everything, how to handle source maps, how to act in Dev Mode, etc.

Loaders: The webpack core loads only JS files. Loaders enable webpack to handle different file types (CSS, HTML, images, etc.). You just install them with npm.

Plugins: These extend webpack’s abilities to handle things you would normally use automation tools like Gulp or Grunt for, like uglifying your code (the latest trend is to use npm directly).

Here’s a list (in no particular order) of things we wish to achieve with webpack while building our app:

  1. Set our main module as the entry to our app
  2. Load HTML and CSS files
  3. Uglify our code
  4. Create a dist folder with an index.html file, which will be the front-end entry point to our app

First, the low-hanging fruit — installing our HTML and CSS loaders:

npm i -D html-loader style-loader css-loader

Now we’ll install 3 plugins that will help us achieve our other goals:

npm i -D uglifyjs-webpack-plugin html-webpack-plugin clean-webpack-plugin

As their names suggest, these plugins help us with file minification, auto-generation of a working index.html, and removal of our build folders before every new build respectively.

In order to use these, let’s make our webpack.config.js file look like this:

const path = require('path');
const webpack = require('webpack');
// require our plugins
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
entry: './src/app.js', // this is our app
output: {
filename: '[name].bundle.js', // the file name would be my entry's name with a ".bundle.js" suffix
path: path.resolve(__dirname, 'dist') // put all of the build in a dist folder
},
plugins: [
new UglifyJsPlugin({
sourceMap: true
}),
new CleanWebpackPlugin(['dist']), // use the clean plugin to delete the dist folder before a build
// This plugin creates our index.html that would load the app for us in the browser
new HtmlWebpackPlugin({
title: 'Your Phrase Fireworks!'
})
],
module: {
rules: [
// use the html loader
{
test: /\.html$/,
exclude: /node_modules/,
use: {loader: 'html-loader'}
},
// use the css loaders (first load the css, then inject the style)
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
};
Our preliminary webpack.config.js file— note the entryoutput and plugins properties. The module.rules apply the loaders according to test rules.

Create a new file in the project’s root:

.>webpack.config.js

…and copy the contents of the above file inside.

How does this config file work? As mentioned above — we set the entry (our main module) and the output. We also set the plugins as an array of plugins. Finally, we set the loaders (now under module.rules).

One more step before we start: Let’s set up the build command for easy usage. Open package.json (in the app’s root). Add a new property to the “scripts” object:

“build”: “webpack”

Your package.json file should now look like this:

{
"name": "your-webpack-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"clean-webpack-plugin": "^0.1.17",
"css-loader": "^0.28.7",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"style-loader": "^0.19.1",
"uglifyjs-webpack-plugin": "^1.1.4",
"webpack": "^3.10.0"
}
}
view raw package.json hosted with ❤ by GitHub

That’s it for infrastructure (for now ;)). Now the fun really begins!

Let’s code our app!

Let’s start with the main module. We would like it to do the following:

  1. Import its HTML template and add it to the DOM
  2. Import its CSS and add it to the DOM
  3. Import the two sub modules

Here’s where webpack kicks in and makes everything super simple. Let’s start coding:

Import the HTML template and add it to the DOM

import template from './index.html';

(function() {
document.body.innerHTML = template;
})();

The html-loader parses .html files , and returns an HTML string.

Import the CSS and add it to the DOM

import template from './index.html'; 
import {} from './app.css';(function() {
document.body.innerHTML = template;
})();

I don’t even know if this was worth its own step, but it’s my blog, so you’ll have to live with it. All I did was add a line to import the CSS file. My css-loader parses the CSS and sends it to my style-loader, which injects it into the DOM.

Let’s put some CSS inside app.css, just for fun:

#phrase-form-wrapper {
min-height: 50px;
background: rgba(125, 125, 0, .25);
margin-bottom: 5px;
}
#phrase-fireworks-wrapper {
height: 450px;
outline: 1px red solid;
}
view raw app.css hosted with ❤ by GitHub

Import the two sub modules

Right now you should know what to expect — just import our two classes/modules:

import template from './index.html'; 
import {} from './app.css';import YOPFForm from './form/form.index';
import YOPFFireworks from './fireworks/fireworks.index';(function() {
document.body.innerHTML = template;
})();

Here I added two new lines that import form and fireworks from their respective files.

Building the app

We’ve done so much, and we don’t even know if it’s working. Let’s build and see how it looks.

Run the command:

npm run build

When it’s done you will have a dist folder. Inside you should see an index.html file. Open it to see the explosive results.

Here’s what you should see when checking the page elements from dev tools:

What was loaded to the DOM when running the app in the browser.

You can see that our wrappers are in place, our style was added to the DOM by webpack. You can also see our template (which we inserted to the DOM in the app.js immediately invoked function expression (IIFE)).

Run it on your computer to see how it looks:

Nice…

Recap

Wow — we did a lot! We’ve set up webpack to bundle our app — JS, CSS and HTML. We’ve set up our entry (main) module to load our template and CSS and put them in the DOM, and configured webpack to place them for us neatly inside an HTML page.

Now our mission is to create the app’s two building blocks: our form and our fireworks.

The form module

What should the form module do?

  1. Import our HTML template and inject it into the DOM (sound familiar?)
  2. Set up the form’s action to submit the phrase
  3. Create some kind of API so other modules can communicate with it

Yea, API. It’s a phrase that makes me look smart…

This module is not an entry. It’s a module that is consumed by another module. We can just export a class that could be instantiated each time it’s needed. So, the basic code for our module would be:

class YOPFForm{
constructor() {

}
}

export default YOPFForm;

This means, that when we import the module, we get a class that can be instantiated with variables. Since our module/class needs to put some HTML inside some element, we would like it to import the HTML and get an element to append the HTML to:

// get the template
import template from './form.index.html';
// get the styles
import {} from './form.index.css';
class YOPFForm{
constructor(element) {
this._element = element;
this.setTemplate();
}
setTemplate() {
this._element.innerHTML = template;
}
}
export default YOPFForm;
view raw form.index.js hosted with ❤ by GitHub
Get the template, get the styles. The class itself accepts a target element in its constructor and sets the template inside.

If you run the code above, you’ll see no change. For this, you need to edit the form.index.html. Let’s put some content inside:

<form name="phrase-form">
<input name="phrase"
class="phrase-input"
type="text">
<input type="submit"
class="phrase-input-button" value="Start Awesomeness!">
</form>
view raw form.index.html hosted with ❤ by GitHub

We also need to use the new module inside our app.js:

import template from './index.html';
import {} from './app.css';
import YOPFForm from './form/form.index';
import YOPFFireworks from './fireworks/fireworks.index';
(function() {
document.body.innerHTML = template;
const form = new YOPFForm(document.getElementById('phrase-form-wrapper'));
})();
view raw app.js hosted with ❤ by GitHub

Now, build again and load the index.html (usually a tab refresh will do). You should see this:

The app with the form

How awesome is that? One more thing is needed here: the ability to connect a response to the form submit. For this we need to add a form submit listener as well as an API to enable external modules (e.g., our app.js) to hook up to it.

Setting up the form’s API

We will add a method to the class that accepts a callback and uses it every time the form is submitted. Then, we can use this method in our app.js file. This isn’t a webpack thing, so I’ll just write the full code and get on with it:

import template from './app.html';
import './app.css';
import {YOPFForm} from "../form/form";
export class App {
constructor(element) {
this._element = element;
element.innerHTML = template;
this.form = this.setupForm(this._element.querySelector('.phrase-form-wrapper'));
}
setupForm(element) {
const form = new YOPFForm(element);
form.listen(this.onPhraseChange);
return form;
}
onPhraseChange(phrase) {
alert(phrase);
}
}
view raw app.js hosted with ❤ by GitHub
// get the template
import template from './form.html';
// get the styles
import './form.css';
export class YOPFForm{
constructor(element) {
this._element = element;
this.setTemplate();
}
setTemplate() {
this._element.innerHTML = template;
this._form = this._element.getElementsByTagName('form')[0];
this._form.addEventListener("submit", (event)=> {
event.preventDefault();
this.onSubmit(event.target);
}, false);
}
onSubmit(form) {
this._callback(form.phrase.value);
}
listen(callback) {
this._callback = callback;
}
}
view raw form.index.js hosted with ❤ by GitHub
App.js uses the API. The API stores the callback “onPhraseChange” from app.js and sets up a listener that runs the callback on every form submit and returns the phrase (input element value)

When you run this code, you’ll see that every time you submit the form, an alert with the form’s value is thrown from the main entry. What an API…

But an alert is not so impressive. We want fireworks!!!

DISCLAIMER: Full disclosure — No attempt at a fully fledged solution was made, nor is the code offered here considered best practice. When building an app, consider validating input, allowing for a safe “unlisten” event, etc.

The fireworks module

Ooooooh — this is gonna be fun!

We’re going to use npm for that. Let’s install a fireworks package:

npm i -S fireworks

Now we’re going to require fireworks (just to show off, I’ll use Node.js) and import the fireworks style. After that, it’s plain JS coding, so I won’t go into much detail. Here’s the gist of it, though: I’ll expose an API method that accepts a phrase and presents it with fireworks. Here are the relevant files to change:

import template from './index.html';
import {} from './app.css';
import YOPFForm from './form/form.index';
import YOPFFireworks from './fireworks/fireworks.index';
(function() {
function onPhraseChange(phrase) {
fireworks.doFireworks(phrase);
}
document.body.innerHTML = template;
const form = new YOPFForm(document.getElementById('phrase-form-wrapper'));
const fireworks = new YOPFFireworks(document.getElementById('phrase-fireworks-wrapper'));
form.listen(onPhraseChange);
})();
view raw app.js hosted with ❤ by GitHub
.phraseText {
position: relative;
top: 50%;
transform: translateY(-50%);
text-align: center;
}
view raw fireworks.css hosted with ❤ by GitHub
const fireworks = require('fireworks');
// get the styles
import {} from './fireworks.css';
class YOPFFireworks{
constructor(element) {
this._element = element;
}
get centerX() {
return this._centerX ? this._centerX : this._centerX = this._element.offsetLeft + this._element.offsetWidth / 2;
}
get centerY() {
return this._centerY ? this._centerY : this._centerY = this._element.offsetTop + this._element.offsetHeight / 2;
}
doFireworks(phrase) {
this._element.innerHTML = '<h1 class="phraseText">' + phrase + '</h1>';
if (this._interval) {
return;
}
this._interval = setInterval(() => {
const newX = Math.random()*100*(Math.random() < .5 ? -1 : 1);
const newY = Math.random()*100*(Math.random() < .5 ? -1 : 1);
fireworks({
x: this.centerX + newX,
y: this.centerY + newY,
colors: ["#cc3333", "#4CAF50", "#81C784"]
});
}, 500);
}
stopFireworks() {
if (!this._interval) {
return;
}
clearInterval(this._interval);
this._interval = null;
}
}
export default YOPFFireworks;
app.js uses the fireworks API to change the phrase and start the fireworks! The fireworks class follows the same pattern as our form class and has a constructor and an API.

Nothing webpack about what was done above —we’ve just created the module, imported it into our main file, and used it. We now have our awesome app!!!

Head scratching moment

So far, we’ve managed to almost magically build our app and create a working index.html file with webpack. But now you scratch your head — what? Do I need to build and refresh every time I make a change? It’s time consuming and confusing.

Here webpack has two solutions: watch mode and webpack-dev-server.

Watch mode is simple — you just add —-watch to the webpack command inside of package.json and it works — webpack will build (or try to build) your app on every code change. Just refresh your browser and, there’s that magic again!

webpack-dev-server is a much more robust solution. It requires a bit of setup (e.g., npm install and a line in the configuration file) but it does the refresh out of the box (and can do much more than that). Because we don’t want to refresh every time, we will just install and setup the webpack-dev-server:

npm i -D webpack-dev-server

And then, instead of using webpack in the CLI or inside the package.json, you can just use webpack-dev-server to start a development server:

{
"name": "webpackPost",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"clean-webpack-plugin": "^0.1.17",
"css-loader": "^0.28.7",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"style-loader": "^0.19.1",
"uglifyjs-webpack-plugin": "^1.1.4",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.9.7"
}
}
view raw package.json hosted with ❤ by GitHub
Note that a `dev` script was added…

After running this:

npm run dev

You will get something like this:

“Project is running at http://localhost:8081/

Just browse to the address the project is running at and you should be good to go (auto reload and all).

Two small tricks to make your life a bit easier: (1) source maps (for easier debugging) and (2) make webpack-dev-server to open your site when the bundling is done.

Here’s the final configuration:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
devtool: 'inline-source-map',
devServer: {
open: true,
contentBase: './dist'
},
entry: './src/app.js', // this is our app
output: {
filename: '[name].bundle.js', // the file name would be my entry's name with a ".bundle.js" suffix
path: path.resolve(__dirname, 'dist') // put all of the build in a dist folder
},
plugins: [
new UglifyJsPlugin({
sourceMap: true
}),
new CleanWebpackPlugin(['dist']), // use the clean plugin to delete the dist folder before a build
// This plugin creates our index.html that would load the app for us in the browser
new HtmlWebpackPlugin({
title: 'Your Phrase Fireworks!'
})
],
module: {
rules: [
// use the html loader
{
test: /\.html$/,
exclude: /node_modules/,
use: [{loader: 'html-loader'}]
},
// use the css loaders (first load the css, then inject the style)
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
};
Note the `devtool` and `devServer` properties

The difference is in the first two properties in the config object:

devtool: 'inline-source-map',
devServer: {
open: true,
contentBase: './dist'
}

This tells webpack to add inline source maps so you can debug your code with ease. The second property is also pretty self explanatory.

There is much more to the development server than what I’ve covered here. You can learn much more in the official docs. If you find something interesting and relevant, write about it in the comments section below, and, if there’s enough material, I’ll add a section about webpack-dev-server to this post.

Best practices— development and production

First we learned how to build for distribution. Then we learned how to use the development server. We can see there’s much in common in their configuration, and a few unique properties for each. We can now create a separate configuration for development and production with a common configuration file and use the correct file for the relevant case.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: ['./src/app.js'], // this is our app
output: {
chunkFilename: '[name].bundle.js',
filename: '[name].bundle.js', // the file name would be my entry's name with a ".bundle.js" suffix
path: path.resolve(__dirname, 'dist') // put all of the build in a dist folder
},
plugins: [
// This plugin creates our index.html that would load the app for us in the browser
new HtmlWebpackPlugin({
title: 'Your Phrase Fireworks!'
}),
new webpack.ProvidePlugin({
jQuery: 'jquery',
$: 'jquery',
jquery: 'jquery'
}),
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: function (module) {
return module.context &&
module.context.includes("node_modules");
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
})
],
module: {
rules: [
// use the url loader for font files
{
test: /\.(woff2?|ttf|eot|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
}
]
},
// use the html loader
{
test: /\.html$/,
exclude: /node_modules/,
use: [{loader: 'html-loader'}]
},
// use the css loaders (first load the css, then inject the style)
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
};
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
module.exports = merge(commonConfig, {
devtool: 'inline-source-map',
devServer: {
open: true,
contentBase: './dist'
},
});
view raw webpack.dev.js hosted with ❤ by GitHub
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const merge = require('webpack-merge'); // don't forget to install this one
const commonConfig = require('./webpack.common');
module.exports = merge(commonConfig, {
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin(['dist']),
new UglifyJsPlugin({
sourceMap: true
})
]
});
view raw webpack.prod.js hosted with ❤ by GitHub
The common, dev and production config files. Don’t forget to install webpack-merge (`npm i -D webpack-merge`).

Then we can add scripts to the package.json file:

scripts: {

dev: webpack-dev-server --config webpack.dev.js,
build: webpack --config webpack.prod.js}

From now on we run npm run dev and npm run build.

Bonus: Adding Bootstrap to the mix

Bootstrap is by far the most widely used design toolkit/component library. Let’s add it to our project to make our form look a bit better:

npm i -S jquery bootstrap

Now, you would assume that you could just use imports like before:

import 'jquery';
import 'bootstrap/dist/js/bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';

But no… This would cause an error because bootstrap.css imports font files which we didn’t tell webpack how to handle — hence, it is trying to handle them like JS. Let’s fix that with a simple loader:

npm i -D url-loader file-loader

url-loader encodes any file to base64url. If it is too long, it just serves the file’s contents (via the file-loader, hence the file-loader install above). We just add the url-loader rule to our config rules list.

// use the url-loader for font files
{
test: /\.(woff2?|ttf|eot|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
}
]
}

Now I’m going to apply Bootstrap to the form (and make some minor CSS changes to make the app more beautiful). Nothing webpack-ish here. You can see all the changes in this commit, and check out the final app here.

Summary

By now you know enough webpack to build your own webpack-powered project.

You can see the full project we’ve just built here on Github.

Let’s recap some highlights from what we’ve done in this post:

  1. Webpack installation and setup in a webpack.config.js file
  2. Using loaders to load HTML and CSS files
  3. Using plugins to make our lives easier
  4. Splitting our app to sub modules which are imported by one entry
  5. Using npm modules on the client-side the way we use them in Node.js
  6. Setting up an easy-to-use development server

With this foundational knowledge, you can build something awesome! But webpack has so much more. To continue your educational journey, read the posts below:

  • Lazy Loading (coming soon)
  • Browser compatibility (coming soon)

You can also explore webpack on your own and/or ask questions in the comments section below if you are stuck.

Enjoy the fireworks!

Sign up to my newsletter to enjoy more content:

5 1 vote
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments