In a previous article in this series, we learned about getting input from others before beginning to code our application. After we’ve clarified all the doubts with the project stakeholders, we are then ready to turn our prototype into a JavaScript application.
What are we working on?
The goal of this series is to show all the aspects of the modern, frontend JavaScript application in the simplest use case as possible:
Inline JavaScript
First step—we need to make some part of our HTML-only prototype done with JavaScript. The easiest way of doing it is using inline JS. So, our index.html
becomes:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello World!</title>
</head>
<body>
<script type="text/javascript">
const element = document.createElement("h1");
element.innerText = "Hello World!";
document.body.appendChild(element);
</script>
</body>
</html>
If you are interested in what happened here and how it’s producing the same page as before, you can read more about DOM manipulation here.
Loading the JS file
Having all the code in the index.html
file will not scale—it will become inconvenient very quickly. Instead, let’s break our code into separate HTML and JS files:
script.js
:
const element = document.createElement("h1");
element.innerText = "Hello World!";
document.body.appendChild(element);
and updated index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello World!</title>
</head>
<body>
<script type="text/javascript" src="./script.js"></script>
</body>
</html>
That’s way better!
JavaScript module
In the previous step, we included JavaScript in the traditional way—one file at the time. Modern JS allows us to write our code in modules that define their dependencies inside—with browsers resolving the paths and loading necessary files. As of now, this feature is already available in almost 95% of browsers on the market (source).
Let’s use this in our application!
First, we will move the message to a separate file, greeting.js
:
export const greetingMessage = "Hello World!";
Note that we use export
before const greetingMessage…
. This lets JS know that this constant should be available for import from other files.
Now, we can easily import this value anywhere we need it in our project. We'll do the same thing for the updated script.js
:
import { greetingMessage } from "./greeting.js";
const element = document.createElement("h1");
element.innerText = greetingMessage;
document.body.appendChild(element);
The least necessary update is changing the type
attribute in the import in index.html
:
<title>Hello World!</title>
</head>
<body>
- <script type="text/javascript" src="./script.js"></script>
+ <script type="module" src="./script.js"></script>
</body>
</html>
You can read more about native ES modules in this article.
Turning this into an npm package
npm is a package manager that allows us to easily download community packages to be used in our application. It was started for Node, server side JavaScript, but as of a few years ago, it has become the standard for the frontend side JavaScript as well. In our case, it will allow for simple configuration of the build script and build dependencies.
To initialize the package, you can run npm init
in your package folder:
$ npm init
This utility will walk you through creating a `package.json` file.
It covers only the most common items, and it tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (hello-world)
version: (1.0.0)
…
npm provides sensible defaults, so you should be fine with picking the proposed value in most cases. After successfully running this command, you will have package.json
created in your folder.
Webpack
Using native ES modules works in most browsers, but in real-world projects, you will still see JS being bundled as part of the build process. Why? There are many things you usually want to do in the project:
- compile TypeScript or any other language that compiles to JavaScript
- reduce the number of files delivered to users
- and at the same time, have fine control over the size of the chunks that we break our code into
- include some cache busting technique—like adding the file’s cache to its name
I discuss the reasons further here.
The most popular JS bundler for JavaScript is Webpack. Let’s add it to our project! First we need to install it:
$ npm install webpack --save-dev
added 77 packages, and audited 78 packages in 7s
9 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
When successful, this command will download webpack files and add them to development dependencies in package.json
.
Git ignore
If you use git—as you should—set this value to .gitingore
:
node_modules
dist
It will keep both folders outside the repository:
-
node_modeles
—where all third party dependencies are stored. It is usually big and can be OS-specific, and each environment should get packages directly from the npm repository -
dist
—will be constantly updated, and it can be rebuilt from the source code whenever it’s needed
Build
To start using webpack, in the same package.json
file, let’s add build
to our scripts
section:
{
…
"scripts": {
"build": "webpack --mode production",
…
},
The --mode production
explicitly sets the way Webpack should build the code—so we can avoid seeing following warnings in the console:
WARNING in configuration
The 'mode' option has not been set, webpack will fall back 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/configuration/mode/
We run the build with npm run build
. The first run will install some additional dependencies:
$ npm run build
> hello-world@1.0.0 build
> webpack
CLI for webpack must be installed.
webpack-cli (https://github.com/webpack/webpack-cli)
We will use "npm" to install the CLI via "npm install -D webpack-cli".
Do you want to install 'webpack-cli' (yes/no): yes
The first build will fail because the default webpack configuration looks for code in the ./src
folder. We can fix it by:
- renaming
script.js
toindex.js
, - moving both
index.js
andgreeting.js
into new folder./src
To use our built code, let’s update index.html
with the following change:
<title>Hello World!</title>
</head>
<body>
- <script type="module" src="./script.js"></script>
+ <script src="./dist/main.js"></script>
</body>
</html>
You can find my code at this stage here.
Generating index.html
Some JS bundlers use the index files as a configuration to determine what files should be built. In Webpack, it’s usually the other way around: the configuration file is responsible for defining how the index file should be generated. It can be a bit confusing at first, but it works nicely when we get to the development server in the next step. So let’s set it up here!
Adding webpack.config.js
First, we add the configuration file webpack.config.js
:
module.exports = {
mode: "production",
};
This change lets us simplify the build script in package.json
:
"scripts": {
- "build": "webpack --mode production",
+ "build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
as the mode is already set in the configuration. At this stage, the build should work the same as before.
The example code.
Adding html-webpack-plugin
Next, we need to add another development dependency:
$ npm install --save-dev html-webpack-plugin
To use it, we need to update the webpack.config.js
to:
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "production",
plugins: [new HtmlWebpackPlugin({ title: "Hello World!" })],
};
Now, remove the old index.html
.
The final build produces two files:
$ npm run build
> hello-world@1.0.0 build
> webpack
asset index.html 215 bytes [emitted]
asset main.js 116 bytes [compared for emit] [minimized] (name: main)
orphan modules 47 bytes [orphan] 1 module
./src/index.js + 1 modules 218 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 157 ms
and its output can be found in dist
folder:
$ ls dist
index.html main.js
Take a look at the code to compare.
Development server
To help with development, Webpack provides a development server.
Why should we bother? The development server:
- watches for files changes
- rebuilds every time something is changed
- reloads the application in your browser
It easily saves you a few seconds every time you make a change to the code—which can be hundreds of times during your workday.
It’s easy to configure: just add start
script to the package.json
:
"main": "src/index.js",
"scripts": {
"build": "webpack",
+ "start": "webpack serve",
"test": "echo \"Error: no test specified\" && exit 1"
},
The first time you run this command, Webpack will propose to install the necessary dependency—webpack-dev-server
:
$ npm run start
> hello-world@1.0.0 start
> webpack serve
[webpack-cli] For using the 'serve' command you need to install: 'webpack-dev-server' package.
[webpack-cli] Would you like to install the 'webpack-dev-server' package? (That will run 'npm install -D webpack-dev-server') (Y/n) Y
Let’s see it in action:
$ npm run start
> hello-world@1.0.0 start
> webpack serve
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
…
When you start it on your machine, you can visit the URL and test whether it’s indeed reloading upon any changes you save to your files.
Check out the reference code.
Want to learn more about webpack?
I have a course on webpack available at Udemy. You will find a similar, step-by-step approach there.
Share your project
I hope the technical turn didn't scare you away and you’re still following along with your project! Share in comments your progress or struggles.
Top comments (0)