Sometimes you don't need a framework like Angular or React to demonstrate an idea or concept in JavaScript. You just want a framework-agnostic, plain JavaScript development environment to play around with things like web workers, service workers, new JavaScript constructs, or IndexedDB, for example. In this blog post, you are going to learn how to quickly prototype plain JavaScript apps using webpack 4 to create such environment with zero config and low development overhead.
Webpack is a leading static module bundler for frontend apps. It is used by tools such as
create-react-app
to quickly scaffold frontend projects. According to the webpack documentation, since version 4.0.0
, webpack doesn't require a configuration file to bundle your projects; however, the module bundler remains highly configurable to meet the increasing complexity of your projects down the line.“With webpack 4 zero config, you can stop scratching your head on how to spin a JavaScript app quickly and avoid overengineering a quick proof of concept using a framework when JavaScript is enough.”
Tweet This
You can find the final version of this exercise on the
repo on GitHub. However, I encourage you to read on and build the webpack app prototype gradually to better understand the heavy lifting that webpack is doing for you.webpack-prototype
Setting Up Zero Config Webpack 4
Head to your terminal and make a directory where you want to store your learning project your current working directory. Then, create a folder named
webpack-prototype
and make it your current working directory. You can do this easily with the following command:mkdir webpack-prototype && cd webpack-prototype
This line of commands creates the
directory and then makes it the current working directory.webpack-prototype
Once there, create a new NPM project and install
locally along with the webpack
:webpack-cli
npm init -y npm install webpack webpack-cli --save-dev
is the tool used to run webpack on the command line.webpack-cli
Next, create a simple file structure under this directory that resembles the following:
webpack-prototype |- package.json |- /dist |- index.html |- /src |- index.js
package.json
is already provided to you when you created the NPM project.By default, webpack 4 will look for a
src/index.js
file to use as an entry point. The entry point tells webpack which module it should use to start building its internal dependency graph. From this module, webpack can infer which other modules or libraries the application depends on and include them in your bundle.Also, webpack uses
dist/index.html
as the default main HTML file for your application where the generated bundle will be automatically injected.Thus, the
src
directory holds all of your application source code (the code that you'll create from scratch, write, delete, edit, and so on). The dist
directory is the distribution directory for the application. This directory holds code that has been minimized and optimized by webpack. In essence, the dist
directory holds the webpack output that will be loaded in the browser once the application is run.You can create these files quickly by issuing the following commands:
macOS / Linux:
mkdir src dist && touch dist/index.html src/index.js
Windows:
mkdir src dist && echo.> dist/index.html && echo.> src/index.js
mkdir
is used to create directories across operating systems. However, touch
>) is only available in Unix and Unix-like operating systems. echo
is a Windows equivalent of touch
. echo.
creates a file with one empty line in it.Open the project in your preferred IDE or code editor.
You can run
orcode .
to open the current working directory if you have installed the command line tools for Visual Studio Code or WebStorm.webstorm .
Give some life to
dist/index.html
by adding the following code:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Webpack Prototype</title> </head> <body> <script src="main.js"></script> </body> </html>
Within the
<body>
tag, you load a main.js
file through a <script>
tag; however, you have not created such file:<script src="main.js"></script>
No worries.
main.js
will be created automatically for you by webpack once it creates your project bundle.As a precaution, to prevent publishing your code to NPM by accident, open
package.json
and do the following:- Add
as a property."private": true,
- Delete the
line."main": "index.js",
package.json
should look like this:{ "name": "webpack-prototype", "version": "1.0.0", "description": "", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "webpack": "^4.25.1", "webpack-cli": "^3.1.2" } }
Now, give some life to
src/index.js
. For now, add a simple message on the screen:// src/index.js const createElement = message => { const element = document.createElement("div"); element.innerHTML = message; return element; }; document.body.appendChild(createElement("Webpack lives."));
Finally, to test everything is working as intended, you need to create a bundle. This can be done by issuing the following command:
npx webpack
Using
npx
, you can emulate the same behavior of the global installation of webpack
but without the actual global installation. npx
uses the local version of webpack
you installed earlier.If you have
installed in your system, you havenpm >= 5.2
available.npx
However, running this command from the command line is not efficient or too memorable. A better approach is to create a
build
NPM script in package.json
which does the same thing as npx webpack
:{ "name": "webpack-prototype", "version": "1.0.0", "description": "", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "webpack": "^4.25.1", "webpack-cli": "^3.1.2" } }
Now, you can run
npm run build
instead which is a more familiar command for JavaScript developers.Running
webpack
does the following:- Use
as the entry point.src/index.js
- Generate
as the webpack output.dist/main.js
Notice that there is a warning when you build your app:
. You'll learn soon about this in this blog post.WARNING in configuration. The 'mode' option has not been set
Verify that the bundle was created correctly by doing the following:
- Open the
directory. Do you see thedist
file there? If yes, the output worked.main.js
- If you are curious, open
. Observe the file consists of a highly minimized one-line of JavaScript code.main.js
- Open
in the browser. You should seedist/index.html
printed on the screen.Webpack lives.
To open
, find the file through the file system and double-click it. Your default browser should then open the file.dist/index.html
Change the message string in
src/index.js
to the following:// src/index.js const createElement = message => { const element = document.createElement("div"); element.innerHTML = message; return element; }; document.body.appendChild( createElement("Webpack lives by the love of Open Source.") );
Reload the browser tab presenting
index.html
. Notice that the printed message doesn't change. For it to change, you need to update your output bundle. To do this, you'll need to execute npm run build
again to re-create the bundle and then refresh the page. Run the command and refresh the page. Webpack lives by the love of Open Source.
should now be shown on the screen.This is not optimal. What you want is Hot Module Replacement to exchange, add, or remove modules while an application is running and without requiring a full reload.
What are the benefits of enabling Hot Module Replacement for you as a developer?
- During a full reload, the state of your application is lost. HRM lets you retain your app state.
- By only updating what has changed in the app, you can save time.
- Changes in your source CSS and JavaScript files are shown in the browser instantaneously which closely resembles an update done directly through the browser's dev tools.
To enable HRM, follow this steps:
- Install
which provides you with a simple web server with the ability to live-reload your app:webpack-dev-server
npm install webpack-dev-server --save-dev
- Create a
NPM script withinstart:dev
:package.json
{ "name": "webpack-prototype", "version": "1.0.0", "description": "", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "start:dev": "webpack-dev-server --mode development --content-base dist/ --open --hot" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "webpack": "^4.25.1", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.10" } }
That
start:dev
NPM script is a mouthful. What's it doing?{ // ... "scripts": { // ... "start:dev": "webpack-dev-server --mode development --content-base dist/ --open --hot" } // ... }
webpack-dev-server
: Runs the webpack dev server.--mode development
: The mode
configuration option tells webpack to use its built-in optimizations accordingly. development
produces a much more readable output than production
. If you leave this option out, the default option is production
. You may also set it to none
which disables any default behavior.Learn more about the differences between the webpack
anddevelopment
modes here.production
--content-base dist/
: Tells the dev server from where to serve your static content. By default, webpack-dev-server
will serve the files in the current directory. However, in this case, you want the content to be served from dist/
where your index.html
file is.--open
: Opens the default app url in the system's default browser. Here, it's http://localhost:8080/
.--hot
: Enables Hot Module Replacement by adding the HotModuleReplacementPlugin
and switching the server to hot
mode.- Run the webpack dev server:
npm run start:dev
Your default browser will open up, load
http://localhost:8080/
, and present you with your app again.Do something crazy: stop the
webpack-dev-server
, delete the main.js
file that was created earlier under the dist
directory, and execute npm run start:dev
again. The default browser will open again and you will see the message printed on the screen. How is that possible if you deleted main.js
?webpack-dev-server
watches your source files and re-compiles your bundle when those file change. However, this modified bundle is served from memory at the relative path specified in publicPath
. It is not written under your dist
directory. If a bundle already exists at the same URL path, by default, the in-memory bundle takes precedence. This is all taken care of automagically by specifying this line on your index.html
:<script src="main.js"></script>
That's it! You can now add more complex code to
src/index.js
or import
other modules to it. Webpack will build its internal dependency graph and include all these in your final bundle. Try that out!Importing Modules Using Zero Config Webpack
Create another module that adds another element to the DOM. Under
src
, create banner.js
:macOS / Linux:
touch src/banner.js
Windows:
echo.> src/banner.js
Open this file and populate it with this:
// src/banner.js const createBanner = () => { const link = document.createElement("a"); link.innerHTML = "Learn Webpack with Sean."; link.href = "https://webpack.academy/"; link.target = "_blank"; return link; }; export default createBanner;
Save the changes made to
src/banner.js
.Then update
src/index.js
as follows:// src/index.js import createBanner from "./banner.js"; const createElement = message => { const element = document.createElement("div"); element.innerHTML = message; return element; }; document.body.appendChild( createElement("Webpack lives by the love of Open Source.") ); document.body.appendChild(createBanner());
Save the changes made to
src/index.js
. Look at the browser. You'll now see the message Webpack lives by the love of Open Source.
and a Learn Webpack with Sean
hyperlink under it which on click takes you to the Webpack Learning Academy, a comprehensive webpack learning resource by Sean Larkin.The same principle can be applied to any modules installed in your project through
npm
. For example, if you want to use lodash
, simply execute npm install --save lodash
and import
it in any file that needs it:npm install --save lodash
Update
src/banner.js
as follows:// src/banner.js import _ from "lodash"; const createBanner = () => { const link = document.createElement("a"); link.innerHTML = _.join(["Learn", "Webpack", "Today"], "*"); link.href = "https://webpack.academy/"; link.target = "_blank"; return link; }; export default createBanner;
Recall that when importing local modules you use
as the module path. When importing NPM modules you use./relative-path-to-module
as the module path.npm-module-name
Save the changes on
src/banner.js
and look at the hyperlink in the browser update itself to look very 90's Retro: Learn*Webpack*Today
.What about CSS?
Adding CSS Stylesheets to Zero Config Webpack
Does adding CSS files work the same as adding JavaScript modules? Find out! You can make the current page look prettier as follows:
- Under
, createsrc
:banner.css
macOS / Linux:
touch src/banner.css
Windows:
echo.> src/banner.css
- Open
and populate it with the following:src/banner.css
/* src/banner.css */ .banner { position: fixed; background: #1a6db9; color: white; padding: 25px; }
Save the changes made to
src/banner.css
.Next update
src/banner.js
to import banner.css
and add the banner
class to the <a>
banner element:// src/banner.js import _ from "lodash"; import "./banner.css"; const createBanner = () => { const link = document.createElement("a"); link.innerHTML = _.join(["Learn", "Webpack", "Today"], "*"); link.href = "https://webpack.academy/"; link.target = "_blank"; link.classList = "banner"; return link; }; export default createBanner;
Save
src/banner.js
and... you get the following error in the command line:ERROR in ./src/banner.css 3:0 Module parse failed: Unexpected token (3:0) You may need an appropriate loader to handle this file type.
A solution could be to move
banner.css
to the dist
folder and call it from index.html
using a <link>
tag:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Webpack Prototype</title> <link rel="stylesheet" type="text/css" href="banner.css"> </head> <body> <script src="main.js"></script> </body> </html>
This certainly works but you'll lose live reloading for that CSS file. Any changes made to
dist/banner.css
would require you to refresh the browser. As discussed earlier, that's not optimal. What can be done? Create a minimal configuration file to use a webpack loader to handle loading CSS files.Extending Zero Config Webpack with Minimal Configuration
Under the root directory, create a
webpack.config.js
file:macOS / Windows:
touch webpack.config.js
Windows:
echo.> webpack.config.js
In order to import a CSS file from within a JavaScript module using webpack, you need to install and add the
style-loader
and css-loader
to the module configuration that will live within webpack.config.js
. You can do that by following these steps:- Install
andstyle-loader
:css-loader
npm install --save-dev style-loader css-loader
- Once those two packages are installed, update
:webpack.config.js
// webpack.config.js module.exports = { module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"] } ] } };
The
module
use a regular expression to test which files it should look for and provide to the loaders specified underrules
. Any file that has ause
extension is served to the.css
and thestyle-loader
.css-loader
Save the changes on
.webpack.config.js
Finally, you need to tell
to usewebpack-dev-server
as the configuration file through thewebpack.config.js
option. You do that by adding the--config
option to the--config webpack.config.js
NPM script present instart:dev
:package.json
{ "name": "webpack-prototype", "version": "1.0.0", "description": "", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "start:dev": "webpack-dev-server --mode development --content-base dist/ --open --hot --config webpack.config.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "css-loader": "^1.0.1", "style-loader": "^0.23.1", "webpack": "^4.25.1", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.10" }, "dependencies": { "lodash": "^4.17.11" } }
- Save the changes made on
.package.json
Stop the
webpack-dev-server
and execute npm run start:dev
again. Observe that now the 90's Retro banner has a blue background, padding, and white text:You can use this configuration file to add any other loaders you may need to address needs such as compiling SCSS to CSS, transpiling JavaScript, loading image assets, and many more. There are lots of webpack loaders to address different project tasks. Check the full webpack loader list.
“Learn how to create a minimal configuration extension to Webpack Zero Config to handle loading CSS files easily.”
Tweet This
For good measure, let's try loading images into our files as they are oftentimes part of a prototype.
A much better approach is to use images from a CDN.
Loading Images Using Webpack 4
Start by downloading the logo of webpack available here. Save it as
webpack-logo.png
and move it to the src
directory.Update
src/index.js
to import the image as follows:// src/index.js import createBanner from "./banner.js"; import WebpackImg from "./webpack-logo.png"; const createElement = message => { const element = document.createElement("div"); element.innerHTML = message; return element; }; document.body.appendChild( createElement("Webpack lives by the love of Open Source.") ); document.body.appendChild(createBanner());
Save the file. As you may be thinking, an error is shown in the command line about this type of file, a PNG image, not being able to be loaded:
ERROR in ./src/webpack-logo.png 1:0 Module parse failed: Unexpected character '�' (1:0) You may need an appropriate loader to handle this file type.
As with CSS, you need an image loader. Follow these steps to add it to your webpack module:
- Install the
webpack loader:file-loader
npm install --save-dev file-loader
- Add a new rule to
module
withinrules
:webpack.config.js
// webpack.config.js module.exports = { module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"] }, { test: /\.(png|svg|jpg|jpeg|gif)$/, use: ["file-loader"] } ] } };
This time the regular expression in the rule
test
is looking for files that have popular image file extensions, such as .png
and .jpeg
.- Restart the webpack dev server.
The file loading error is now gone.
- Update
to make use the of the image:src/index.js
// src/index.js import createBanner from "./banner.js"; import WebpackImg from "./webpack-logo.png"; const createElement = message => { const element = document.createElement("div"); element.innerHTML = message; return element; }; const createImage = image => { const element = document.createElement("div"); const imageElement = new Image(); imageElement.src = image; element.appendChild(imageElement); return element; }; document.body.appendChild( createElement("Webpack lives by the love of Open Source.") ); document.body.appendChild(createBanner()); document.body.appendChild(createImage(WebpackImg));
Take a look at the browser. The webpack logo now loads on the screen:
Your Webpack Zero Config configuration extension now includes the ability to load images. The config file still remains minimal, lightweight, and easy to understand.
Building a JavaScript App with Webpack 4
As a final note of this process, once you want to build your app again, simply execute
npm run build
.The images imported into JavaScript modules will be processed and added to the
dist
output directory. Any image variables will have the final URL of that post-processing image, which may look like this:dist/e5e245191fd9c9812bc78bd0cea9a12c.jpeg
You can also use your images within CSS files to add them as element backgrounds, for example.
Conclusion
For a simple and quick JavaScript prototype, JavaScript, CSS, and image assets are plenty to get a lot done. You are now empowered with knowledge on how to use webpack 4 to create a development environment for JavaScript projects with zero config needed. If the project requires CSS, you can extend the zero config to use CSS and file loaders to create beautiful JavaScript apps fast. As an alternative, if you prefer to, feel free to use cloud environments that use webpack 4 such as StackBlitz.
You can find a polished version of this exercise on the
repo on GitHub. The final version uses Google Fonts and an improved structure to create a much better looking webpack banner using webpack!webpack-prototype
Aside: Auth0 Authentication with JavaScript
At Auth0, we make heavy use of full-stack JavaScript to help our customers to manage user identities, including password resets, creating, provisioning, blocking, and deleting users. Therefore, it must come as no surprise that using our identity management platform on JavaScript web apps is a piece of cake.
Auth0 offers a free tier to get started with modern authentication. Check it out, or sign up for a free Auth0 account here!
Then, go to the Applications section of the Auth0 Dashboard and click on "Create Application". On the dialog shown, set the name of your application and select Single Page Web Applications as the application type:
After the application has been created, click on "Settings" and take note of the domain and client id assigned to your application. In addition, set the Allowed Callback URLs and Allowed Logout URLs fields to the URL of the page that will handle login and logout responses from Auth0. In the current example, the URL of the page that will contain the code you are going to write (e.g.
http://localhost:8080
).Now, in your JavaScript project, install the
library like so:auth0-spa-js
npm install @auth0/auth0-spa-js
Then, implement the following in your JavaScript app:
import createAuth0Client from '@auth0/auth0-spa-js'; let auth0Client; async function createClient() { return await createAuth0Client({ domain: 'YOUR_DOMAIN', client_id: 'YOUR_CLIENT_ID', }); } async function login() { await auth0Client.loginWithRedirect(); } function logout() { auth0Client.logout(); } async function handleRedirectCallback() { const isAuthenticated = await auth0Client.isAuthenticated(); if (!isAuthenticated) { const query = window.location.search; if (query.includes('code=') && query.includes('state=')) { await auth0Client.handleRedirectCallback(); window.history.replaceState({}, document.title, '/'); } } await updateUI(); } async function updateUI() { const isAuthenticated = await auth0Client.isAuthenticated(); const btnLogin = document.getElementById('btn-login'); const btnLogout = document.getElementById('btn-logout'); btnLogin.addEventListener('click', login); btnLogout.addEventListener('click', logout); btnLogin.style.display = isAuthenticated ? 'none' : 'block'; btnLogout.style.display = isAuthenticated ? 'block' : 'none'; if (isAuthenticated) { const username = document.getElementById('username'); const user = await auth0Client.getUser(); username.innerText = user.name; } } window.addEventListener('load', async () => { auth0Client = await createClient(); await handleRedirectCallback(); });
Replace the
andYOUR_DOMAIN
placeholders with the actual values for the domain and client id you found in your Auth0 Dashboard.YOUR_CLIENT_ID
Then, create your UI with the following markup:
<p>Welcome <span id="username"></span></p> <button type="submit" id="btn-login">Sign In</button> <button type="submit" id="btn-logout" style="display:none;">Sign Out</button>
Your application is ready to authenticate with Auth0!
Check out the Auth0 SPA SDK documentation to learn more about authentication and authorization with JavaScript and Auth0.
About the author
Dan Arias
Staff Developer Advocate
The majority of my engineering work revolves around AWS, React, and Node, but my research and content development involves a wide range of topics such as Golang, performance, and cryptography. Additionally, I am one of the core maintainers of this blog. Running a blog at scale with over 600,000 unique visitors per month is quite challenging!
I was an Auth0 customer before I became an employee, and I've always loved how much easier it is to implement authentication with Auth0. Curious to try it out? Sign up for a free account ⚡️.View profile