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.
The goal of this series is to show all the aspects of the modern, frontend JavaScript application in the simplest use case as possible:
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.
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!
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.
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.
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:
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
.
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 repositorydist
—will be constantly updated, and it can be rebuilt from the source code whenever it’s neededTo 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
> [email protected] 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:
script.js
to index.js
,index.js
and greeting.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.
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!
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.
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
> [email protected] 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.
To help with development, Webpack provides a development server.
Why should we bother? The development server:
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
> [email protected] 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
> [email protected] 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.
I have a course on webpack available at Udemywebpack available at Udemy. You will find a similar, step-by-step approach there.
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.
Also published here.