CSCI 4513
Week 4, Lecture 7
Specialized workshops collaborated to create great artifacts, just as ES6 modules combine specialized code into powerful applications.
The Tale:
Beneath Svartalfheim's mountains, dwarves maintained countless specialized forgesβsome for metalwork, others for enchantments, gemcraft, or mechanics. Great artifacts flowed through multiple workshops, each contributing expertise while maintaining independence.
The greatest works like Mjolnir weren't created in a single forge. Ore refiners purified materials, master smiths shaped forms, enchanters wove magic, and assemblers combined everything. Each workshop kept its techniques secret, passing only finished components to the next stage.
// metalwork.js - specialized module
export function forgeBase(material, shape) {
return { material, shape, durability: 100 };
}
// enchantment.js - specialized module
export function addMagic(item, type) {
return { ...item, magic: type, power: 50 };
}
// assembly.js - combines modules
import { forgeBase } from './metalwork.js';
import { addMagic } from './enchantment.js';
export function createArtifact(specs) {
let artifact = forgeBase(specs.material, specs.shape);
artifact = addMagic(artifact, specs.enchantment);
return artifact;
}
export - Makes a function available for other files to import
...item - Spread operator copies all existing properties into the new object
import { fn } from - Pull specific named exports from another module
Pattern - Each file is a specialized module. assembly.js coordinates by importing from both.
Each module does one thing well β single responsibility principle!
<script src="one.js" defer></script>
<script src="two.js" defer></script>
// one.js
const greeting = "Hello!";
// two.js
console.log(greeting);
defer - Loads scripts after HTML is parsed, but they still share global scope
The problem - greeting defined in one.js is visible in two.js because both scripts share the window's global scope.
β οΈ Danger - Any script can overwrite another script's variables! No isolation, no protection.
Imagine 10+ scripts all sharing one scope β name collisions are inevitable!
// one.js - wrap in IIFE
const greeting = (() => {
const greetingString = "Hello, Odinite!";
const farewellString = "Bye bye, Odinite!";
return greetingString; // Expose only this
})();
// two.js
console.log(greeting); // Works!
console.log(farewellString); // Error - not exposed
(() => { ... })() - IIFE: defines and immediately calls a function, creating a private scope
return - Only what's returned is exposed to the outside
Private by default - farewellString stays hidden inside the IIFE. Only greetingString is returned.
This was the pre-ES6 workaround β ES6 modules make this pattern built-in!
// one.js - Method 1: Inline export
export const greeting = "Hello, Odinite!";
export const farewell = "Bye bye, Odinite!";
// one.js - Method 2: Separate export
const greeting = "Hello, Odinite!";
const farewell = "Bye bye, Odinite!";
export { greeting, farewell };
export const - Inline: export at declaration time
export { } - Separate: declare first, export at the bottom
Both are identical - Choose whichever style you prefer. Separate export is nice when you want all exports listed in one place.
Named exports keep their exact name β importers must use the same name!
// two.js - Import specific named exports
import { greeting, farewell } from "./one.js";
console.log(greeting); // "Hello, Odinite!"
console.log(farewell); // "Bye bye, Odinite!"
// Or import only what you need
import { greeting } from "./one.js";
console.log(greeting); // Works!
import { name } - Curly braces pull specific named exports from a module
from "./one.js" - Relative path to the module file (must include extension in browsers)
Selective imports - You only import what you need. Unused exports stay in their module.
β οΈ Not objects! { } is special import/export syntax, not destructuring!
// one.js - Method 1: Inline default export
export default "Hello, Odinite!";
// one.js - Method 2: Separate default export
const greeting = "Hello, Odinite!";
export default greeting;
export default - Marks the "main" export of a file. Only one per module.
No name needed - The default export is nameless. The importer chooses what to call it.
Use default exports when a module has one primary thing to share (a class, a function, etc.)
// two.js - Name it whatever you want!
import helloOdinite from "./one.js";
console.log(helloOdinite); // "Hello, Odinite!"
// Or name it something else
import myGreeting from "./one.js";
console.log(myGreeting); // "Hello, Odinite!"
import name - No curly braces! You choose any name for the default import.
Same value, different names - Both helloOdinite and myGreeting receive the same default export.
Convention: name the import after the module or what it represents for clarity.
// one.js - Both default and named exports
export default "Hello, Odinite!";
export const farewell = "Bye bye, Odinite!";
// two.js - Import both
import greeting, { farewell } from "./one.js";
console.log(greeting); // "Hello, Odinite!"
console.log(farewell); // "Bye bye, Odinite!"
export default + export const - A file can have one default and many named exports
import greeting, { farewell } - Default first (no braces), then named in { }, separated by comma
Order matters - Default import must come before the named imports in the statement.
<!-- Only link the entry point -->
<script src="two.js" type="module"></script>
two.js <ββ one.js
type="module" - Tells the browser this is an ES module, not a regular script
Dependency graph - Browser loads two.js, sees its imports, and automatically fetches one.js too.
Only one script tag needed β the browser follows the import chain!
<!-- MUST use type="module" -->
<script src="main.js" type="module"></script>
file://
type="module" - Required attribute that enables ES6 module behavior
Automatic benefits - No need for defer attribute or "use strict" β modules get both for free.
β οΈ CORS required - Modules use CORS, so you need a web server (Live Preview, etc.)
npm is a package manager - a gigantic repository of plugins, libraries, and tools with a CLI to install them in our projects.
{
"name": "my-project",
"version": "1.0.0",
"description": "My awesome project",
"scripts": {
"test": "jest",
"build": "webpack"
},
"devDependencies": {
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
"scripts" - Custom commands you can run with npm run <name>
"devDependencies" - Packages needed only during development (not in production)
^ - Caret: allows minor and patch updates (e.g., 5.89.0 up to <6.0.0)
Shareable config - Anyone can clone your repo and run npm install to get all dependencies!
# Initialize a new project
npm init -y
# Install a package
npm install package-name
# Install as dev dependency
npm install --save-dev webpack
# Install all dependencies from package.json
npm install
npm init -y - Creates package.json with defaults (-y skips questions)
npm install - Adds a package to node_modules/ and records it in package.json
--save-dev - Saves to devDependencies instead of dependencies (build tools, test frameworks)
Running npm install with no package name installs everything from package.json.
When you install packages, npm creates a node_modules directory containing all package code.
# .gitignore
node_modules
dist
node_modules - Can be thousands of files! Recreated by npm install.
dist - Generated output folder. Rebuilt every time you bundle.
β οΈ Never commit these! package.json tracks what to install.
Webpack takes modules with dependencies and generates static assets
# Create project and initialize npm
mkdir webpack-practice && cd webpack-practice
npm init -y
# Install Webpack
npm install --save-dev webpack webpack-cli
# Create directories
mkdir src
touch src/index.js
&& - Chains commands: second runs only if first succeeds
webpack - The bundler core. webpack-cli - Command line interface to run it.
src/index.js - The entry point where Webpack starts building the dependency graph.
Both packages are dev dependencies β they're build tools, not production code.
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
};
require("path") - Node.js built-in for resolving file paths
mode - "development" for readable output, "production" for minified
entry - Where Webpack starts building the dependency graph
path.resolve(__dirname, "dist") - Absolute path to output directory
clean: true - Empties the dist folder before each build
# Bundle your code
npx webpack
# Output:
# dist/
# main.js (bundled file)
npx - Runs a locally installed package without a global install
What happens:
src/index.js)dist/main.jsOne command turns many source files into a single optimized bundle!
# Install the plugin
npm install --save-dev html-webpack-plugin
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// ... other config
plugins: [
new HtmlWebpackPlugin({
template: "./src/template.html",
}),
],
};
plugins - Array of Webpack plugins that extend its functionality
new HtmlWebpackPlugin() - Creates an HTML file in dist with the bundled JS automatically linked
template - Your HTML skeleton. Plugin injects the <script> tag for you.
No more manually linking bundle files β the plugin handles it!
# Install CSS loaders
npm install --save-dev style-loader css-loader
// webpack.config.js
module.exports = {
// ... other config
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
module.rules - Tells Webpack how to handle different file types
test: /\.css$/i - Regex matching any file ending in .css
css-loader - Reads CSS files. style-loader - Injects CSS into the DOM.
β οΈ Order matters! Loaders run right-to-left: css-loader first, then style-loader.
/* src/styles.css */
body {
background-color: rebeccapurple;
}
// src/index.js
import "./styles.css"; // Side effect import!
console.log("CSS loaded!");
import "./styles.css" - Side effect import: no variable, just triggers the loaders
How it works - css-loader reads the file, style-loader injects a <style> tag into the HTML at runtime.
No <link> tags needed! Import CSS directly in your JavaScript files.
# For HTML images
npm install --save-dev html-loader
Why loaders? - Webpack only understands JS by default. Loaders teach it to process other file types.
html-loader - Parses HTML and resolves <img src="..."> paths for bundling
CSS images work for free because css-loader already handles url() references.
// webpack.config.js
module.exports = {
module: {
rules: [
// For HTML template images
{
test: /\.html$/i,
loader: "html-loader",
},
// For JavaScript imports
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
],
},
};
loader: "html-loader" - Single loader uses loader instead of use array
Regex breakdown: /\.(png|svg|...)$/i
/ ... / β regex delimiters\. β literal dot (escaped with \)(png|svg|jpg|...) β matches any of these extensions (| means "or")$ β must be at the end of the filenamei β case-insensitive flag (.PNG works too)
type: "asset/resource" - Built-in Webpack 5 feature: copies file to dist and returns the URL
No extra npm package needed for asset/resource β it's built into Webpack 5!
// src/index.js
import odinImage from "./odin.png";
const image = document.createElement("img");
image.src = odinImage; // Correct bundled path!
document.body.appendChild(image);
import odinImage - Returns the final URL/path to the image after bundling
image.src = odinImage - Sets the src to the Webpack-resolved path (e.g., dist/abc123.png)
Why import? - Webpack needs to know about the image to include it in the bundle and set the correct path!
# Install dev server
npm install --save-dev webpack-dev-server
// webpack.config.js
module.exports = {
devtool: "eval-source-map",
devServer: {
watchFiles: ["./src/template.html"],
},
};
# Start dev server
npx webpack serve
devtool: "eval-source-map" - Enables source maps so errors show original file/line numbers
watchFiles - Also watches HTML changes (JS/CSS watched automatically)
npx webpack serve - Starts a local server at localhost:8080 with hot reload
Save a file and the browser refreshes automatically β no manual reload needed!
devtool: "eval-source-map" creates source maps so error messages reference your original source files, not the bundled mess!
restaurant-page/
βββ src/
β βββ index.js # Entry point
β βββ home.js # Home tab module
β βββ menu.js # Menu tab module
β βββ about.js # About tab module
β βββ template.html # HTML skeleton
β βββ styles.css # Styles
βββ dist/ # Generated by Webpack
βββ webpack.config.js
βββ package.json
βββ .gitignore
βββ node_modules/
src/ - Your working code. Each tab is its own module (home.js, menu.js, about.js).
dist/ - Webpack's output. Never edit files here β they get overwritten on every build.
index.js - Entry point that imports all tab modules and handles switching between them.
One module per tab keeps code organized and easy to maintain!
// src/index.js
import loadHome from "./home.js";
import loadMenu from "./menu.js";
import loadAbout from "./about.js";
const content = document.getElementById("content");
function clearContent() {
content.textContent = "";
}
document.getElementById("home-btn")
.addEventListener("click", () => {
clearContent();
loadHome();
});
document.getElementById("menu-btn")
.addEventListener("click", () => {
clearContent();
loadMenu();
});
// Load home page initially
loadHome();
import loadHome from - Each tab is a default-exported function from its own module
clearContent() - Empties the content div before loading new tab content
addEventListener - Each button clears then loads its tab module
Initial load - loadHome() at the bottom runs on page load so the home tab shows first.
// src/home.js
export default function loadHome() {
const content = document.getElementById("content");
const heading = document.createElement("h1");
heading.textContent = "Welcome to Our Restaurant";
const description = document.createElement("p");
description.textContent = "Best food in town!";
content.appendChild(heading);
content.appendChild(description);
}
export default function - Declares and exports the function in one line
createElement + appendChild - All DOM content is created in JS, not in HTML
Pattern - Each tab module exports one function that builds and appends its section's DOM elements.
The HTML template only has a nav and an empty #content div β JS does the rest!
# First time only - create gh-pages branch
git branch gh-pages
# Every deployment:
git checkout gh-pages && git merge main --no-edit
npx webpack
git add dist -f && git commit -m "Deployment commit"
git subtree push --prefix dist origin gh-pages
git checkout main
git merge main - Brings latest code into the deploy branch
git add dist -f - Force-adds dist (normally gitignored) for deployment
subtree push --prefix dist - Pushes only the dist folder contents to gh-pages
β οΈ Remember: Set GitHub Pages source to gh-pages branch in repo settings!
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
devtool: "eval-source-map",
devServer: {
watchFiles: ["./src/template.html"],
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/template.html",
}),
],
module: {
rules: [
{ test: /\.css$/i, use: ["style-loader", "css-loader"] },
{ test: /\.html$/i, loader: "html-loader" },
{ test: /\.(png|svg|jpg|jpeg|gif)$/i, type: "asset/resource" },
],
},
};
Complete config - Everything from this lecture in one file:
mode + entry + output - Core: what, where, and how to bundle
devtool + devServer - Development experience: source maps + auto-reload
plugins - Extend Webpack's capabilities (HTML generation)
module.rules - Teach Webpack to process CSS, HTML, and images
π Next Class: JSON and OOP
π Homework: Restaurant Page