We use cookies to enhance your experience on the site
CodeWorlds

ES Modules (import/export)

Welcome back to Jurassic Park! After building solid JavaScript foundations and learning asynchronous techniques, it's time to explore how to organize code into larger, scalable projects. In a world as complex as Jurassic Park, we need a well-organized structure to ensure system safety and efficiency.

Code Modularization in JavaScript

Imagine managing Jurassic Park without dividing it into teams and departments — everything would be chaotic and dangerous! The same is true for code — when everything lives in one file, it quickly becomes unreadable and hard to maintain.

ES Modules (ECMAScript Modules) is the standard JavaScript module system that allows you to:

  • Split code into smaller, more manageable pieces
  • Control which elements are accessible outside a module
  • Define dependencies between different parts of a program

Exporting Elements from Modules

Let's start by creating a few basic modules for our Jurassic Park management system:

1. Default Export

When we have one primary element to export, we use a default export:

1// dinosaur.js - Dinosaur model
2class Dinosaur {
3  constructor(name, species, diet, dangerLevel) {
4    this.name = name;
5    this.species = species;
6    this.diet = diet;
7    this.dangerLevel = dangerLevel;
8    this.health = 100;
9    this.isContained = true;
10  }
11
12  feed() {
13    this.health = Math.min(100, this.health + 20);
14    return `${this.name} has been fed. Health level: ${this.health}%`;
15  }
16
17  checkStatus() {
18    return {
19      name: this.name,
20      health: this.health,
21      isContained: this.isContained,
22      warning: this.dangerLevel > 7 ? "HIGH DANGER LEVEL" : ""
23    };
24  }
25}
26
27// Default export — only one class/function/object per file
28export default Dinosaur;

2. Named Exports

When we have multiple elements to export, we use named exports:

1// securityUtils.js - Security utility functions
2export const SECURITY_LEVELS = {
3  GREEN: "normal",
4  YELLOW: "elevated",
5  ORANGE: "high",
6  RED: "critical"
7};
8
9export function checkFenceStatus(sectorId) {
10  // Simulate checking fence status in a given sector
11  const status = Math.random() > 0.1 ? "active" : "failure";
12  return {
13    sector: sectorId,
14    status,
15    timestamp: new Date().toISOString()
16  };
17}
18
19export function triggerAlarm(sector, reason) {
20  console.log(`ALARM! Sector ${sector}: ${reason}`);
21  // Alarm activation logic
22  return {
23    success: true,
24    alarmId: "ALR-" + Math.floor(Math.random() * 10000)
25  };
26}
27
28// We can also export a default function alongside named exports
29export default function getSecurityStatus() {
30  // Returns overall park security status
31  return {
32    level: SECURITY_LEVELS.GREEN,
33    activeAlarms: 0,
34    lastChecked: new Date().toISOString()
35  };
36}

3. Exporting Previously Defined Variables

We can also define our variables, functions, or classes first and export them at the end of the file:

1// parkStats.js - Jurassic Park statistics
2const totalVisitors = 0;
3const revenue = 0;
4const incidents = [];
5
6function addVisitors(count) {
7  totalVisitors += count;
8  return totalVisitors;
9}
10
11function addRevenue(amount) {
12  revenue += amount;
13  return revenue;
14}
15
16function recordIncident(type, location, description) {
17  const incident = {
18    id: incidents.length + 1,
19    type,
20    location,
21    description,
22    timestamp: new Date().toISOString()
23  };
24  incidents.push(incident);
25  return incident;
26}
27
28function getDailyStats() {
29  return {
30    totalVisitors,
31    revenue,
32    incidentCount: incidents.length
33  };
34}
35
36// Export multiple elements at once
37export {
38  addVisitors,
39  addRevenue,
40  recordIncident,
41  getDailyStats
42};
43
44// We can also rename an exported element
45export { recordIncident as logParkIncident };

Importing Elements from Modules

After creating modules, it's time to use them in the main application:

1. Default Import

1// app.js
2import Dinosaur from './dinosaur.js';
3
4// Create new dinosaur instances
5const trex = new Dinosaur("Rex", "Tyrannosaurus", "carnivore", 9);
6const brachi = new Dinosaur("Longneck", "Brachiosaurus", "herbivore", 4);
7
8console.log(trex.checkStatus());
9console.log(brachi.feed());

2. Named Import

1// securityMonitor.js
2import { checkFenceStatus, triggerAlarm, SECURITY_LEVELS } from './securityUtils.js';
3
4// Use imported functions and constants
5function monitorSecurity() {
6  const sectors = ["A1", "B2", "C3", "D4"];
7
8  // Check each sector
9  sectors.forEach(sector => {
10    const fenceStatus = checkFenceStatus(sector);
11
12    if (fenceStatus.status === "failure") {
13      const alarmResult = triggerAlarm(
14        sector,
15        "Electric fence failure!"
16      );
17      console.log(`Alarm ${alarmResult.alarmId} triggered in sector ${sector}`);
18
19      document.querySelector("#securityStatus").textContent = SECURITY_LEVELS.RED;
20    }
21  });
22}
23
24setInterval(monitorSecurity, 60000); // Check every minute

3. Default and Named Import Together

1// dashboard.js
2import getSecurityStatus, { SECURITY_LEVELS, checkFenceStatus } from './securityUtils.js';
3
4// Use the default import
5const parkStatus = getSecurityStatus();
6console.log("Security status:", parkStatus.level);
7
8// Use the named imports
9if (parkStatus.level === SECURITY_LEVELS.RED) {
10  // Check all sectors immediately
11  ["A1", "B2", "C3", "D4"].forEach(sector => {
12    console.log(`Checking sector ${sector}: `, checkFenceStatus(sector));
13  });
14}

4. Import with Alias

1// incidentReport.js
2import { recordIncident as logIncident } from './parkStats.js';
3
4// Use the imported function under its new name
5function reportEmergency(location, details) {
6  logIncident("EMERGENCY", location, details);
7  // Additional emergency handling logic
8}

5. Import Everything as an Object

1// adminPanel.js
2import * as ParkStats from './parkStats.js';
3
4// Access all module functions through the object
5function generateDailyReport() {
6  const visitorCount = ParkStats.addVisitors(120); // Adds 120 visitors
7  const dailyRevenue = ParkStats.addRevenue(15000); // Adds $15,000 revenue
8
9  const stats = ParkStats.getDailyStats();
10
11  return `
12    Jurassic Park Daily Report:
13    - Visitor count: ${stats.totalVisitors}
14    - Revenue: $${stats.revenue}
15    - Incidents: ${stats.incidentCount}
16  `;
17}

Aggregating and Re-exporting Modules

In large projects, it's often useful to create entry points that aggregate and re-export functionality from multiple smaller modules:

1// models/index.js - Aggregate all models
2export { default as Dinosaur } from './dinosaur.js';
3export { default as Employee } from './employee.js';
4export { default as Facility } from './facility.js';
5export { default as Visitor } from './visitor.js';

This lets us import all models from a single entry point:

1// main.js
2import { Dinosaur, Employee, Facility } from './models/index.js';
3
4// Now we can use all models
5const rex = new Dinosaur("Rex", "Tyrannosaurus", "carnivore", 9);
6const lab = new Facility("InGen Labs", "B3", 12);
7const scientist = new Employee("Dr. Wu", "Chief Geneticist", "lab");

Dynamic Module Importing

ES Modules also offer a dynamic import mechanism, allowing modules to be loaded on demand:

1// emergencySystem.js
2async function handleEmergency(type) {
3  let emergencyModule;
4
5  switch(type) {
6    case "dinosaurEscape":
7      emergencyModule = await import('./emergencies/dinosaurEscape.js');
8      break;
9    case "powerOutage":
10      emergencyModule = await import('./emergencies/powerOutage.js');
11      break;
12    case "weatherAlert":
13      emergencyModule = await import('./emergencies/weatherAlert.js');
14      break;
15    default:
16      emergencyModule = await import('./emergencies/generalEmergency.js');
17  }
18
19  // Execute emergency procedure from the loaded module
20  emergencyModule.initiateProtocol();
21}
22
23// UI button to trigger the emergency procedure
24document.querySelector("#emergencyButton").addEventListener("click", () => {
25  const emergencyType = document.querySelector("#emergencyType").value;
26  handleEmergency(emergencyType);
27});

ES Modules in the Browser

To use ES Modules directly in the browser, we must add the

type="module"
attribute to the script tag:

1<!DOCTYPE html>
2<html>
3<head>
4  <title>Jurassic Park Control Panel</title>
5</head>
6<body>
7  <h1>Jurassic Park Control Panel</h1>
8  <div id="securityStatus"></div>
9  <select id="emergencyType">
10    <option value="dinosaurEscape">Dinosaur escape</option>
11    <option value="powerOutage">Power outage</option>
12    <option value="weatherAlert">Weather alert</option>
13  </select>
14  <button id="emergencyButton">Activate emergency procedure</button>
15
16  <!-- Main script using modules -->
17  <script type="module" src="./app.js"></script>
18</body>
19</html>

Benefits of a Modular Approach

Organizing applications with ES Modules offers many benefits:

  1. Encapsulation — each module has its own namespace, preventing naming conflicts
  2. Reusability — modules can be easily reused in different parts of the application
  3. Organization — code is easier to understand and maintain when split into logical parts
  4. Team collaboration — different team members can work on separate modules
  5. Lazy loading — modules can be loaded only when they are needed
  6. Better dependency management — clearly defined dependencies between modules

Practical Example: Dinosaur Monitoring System

Let's see how we might organize a larger dinosaur monitoring system for Jurassic Park using modules:

1// models/dinosaur.js
2export default class Dinosaur {
3  // ... Dinosaur class implementation (as above)
4}
5
6// services/dinoTracking.js
7import Dinosaur from '../models/dinosaur.js';
8
9export async function getAllDinosaurs() {
10  // Fetch dinosaur data from API
11  const response = await fetch('/api/dinosaurs');
12  const data = await response.json();
13
14  // Convert raw data to Dinosaur class instances
15  return data.map(dino => new Dinosaur(
16    dino.name,
17    dino.species,
18    dino.diet,
19    dino.dangerLevel
20  ));
21}
22
23export async function getDinosaursByDangerLevel(level) {
24  const allDinos = await getAllDinosaurs();
25  return allDinos.filter(dino => dino.dangerLevel >= level);
26}
27
28// services/securitySystem.js
29export async function getActiveFences() {
30  // Fetch active fence data
31  const response = await fetch('/api/fences/active');
32  return response.json();
33}
34
35export async function activateSectorLockdown(sectorId) {
36  // Activate lockdown in a given sector
37  const response = await fetch(`/api/sectors/${sectorId}/lockdown`, {
38    method: 'POST'
39  });
40  return response.json();
41}
42
43// utils/alertSystem.js
44export const ALERT_LEVELS = {
45  INFO: 'info',
46  WARNING: 'warning',
47  DANGER: 'danger',
48  CRITICAL: 'critical'
49};
50
51export function showAlert(message, level = ALERT_LEVELS.INFO) {
52  const alertElement = document.createElement('div');
53  alertElement.className = `alert alert-${level}`;
54  alertElement.textContent = message;
55
56  document.querySelector('#alertContainer').appendChild(alertElement);
57
58  // Remove alert after 5 seconds (unless it's critical)
59  if (level !== ALERT_LEVELS.CRITICAL) {
60    setTimeout(() => {
61      alertElement.remove();
62    }, 5000);
63  }
64}
65
66// controllers/monitoringDashboard.js
67import { getAllDinosaurs, getDinosaursByDangerLevel } from '../services/dinoTracking.js';
68import { getActiveFences, activateSectorLockdown } from '../services/securitySystem.js';
69import { showAlert, ALERT_LEVELS } from '../utils/alertSystem.js';
70
71export async function initializeDashboard() {
72  try {
73    // Fetch dinosaur data and fence status
74    const [allDinos, activeFences] = await Promise.all([
75      getAllDinosaurs(),
76      getActiveFences()
77    ]);
78
79    // Find dangerous dinosaurs
80    const dangerousDinos = await getDinosaursByDangerLevel(8);
81
82    // Display data on the dashboard
83    renderDinosaurTable(allDinos);
84    renderSecurityStatus(activeFences);
85
86    // Show alerts for dangerous dinosaurs
87    if (dangerousDinos.length > 0) {
88      showAlert(
89        `WARNING: ${dangerousDinos.length} dinosaurs with high danger level`,
90        ALERT_LEVELS.WARNING
91      );
92    }
93
94    // Set up emergency event listeners
95    setupEmergencyHandlers();
96
97  } catch (error) {
98    showAlert(`Dashboard initialization error: ${error.message}`, ALERT_LEVELS.DANGER);
99  }
100}
101
102function renderDinosaurTable(dinosaurs) {
103  // Dinosaur table rendering logic
104}
105
106function renderSecurityStatus(fences) {
107  // Security status rendering logic
108}
109
110function setupEmergencyHandlers() {
111  // Emergency event handler setup logic
112  document.querySelector('#emergencyLockdown').addEventListener('click', async () => {
113    const sector = document.querySelector('#sectorSelect').value;
114    try {
115      await activateSectorLockdown(sector);
116      showAlert(`Sector ${sector} lockdown activated`, ALERT_LEVELS.WARNING);
117    } catch (error) {
118      showAlert(`Failed to activate lockdown: ${error.message}`, ALERT_LEVELS.CRITICAL);
119    }
120  });
121}
122
123// app.js - Main application file
124import { initializeDashboard } from './controllers/monitoringDashboard.js';
125
126// Initialize application when DOM is ready
127document.addEventListener('DOMContentLoaded', () => {
128  initializeDashboard();
129
130  // Additional initialization logic
131  console.log('Jurassic Park monitoring system started');
132});

Browser Support and Webpack Configuration

Modern browsers support ES Modules natively, but for full compatibility with older browsers, we often use tools like Webpack or Rollup to bundle our modules into code compatible with older browsers.

Basic Webpack Configuration for ES Modules

1// webpack.config.js
2const path = require('path');
3
4module.exports = {
5  entry: './src/app.js',
6  output: {
7    filename: 'bundle.js',
8    path: path.resolve(__dirname, 'dist')
9  },
10  module: {
11    rules: [
12      {
13        test: /.js$/,
14        exclude: /node_modules/,
15        use: {
16          loader: 'babel-loader',
17          options: {
18            presets: ['@babel/preset-env']
19          }
20        }
21      }
22    ]
23  }
24};

Best Practices for Working with Modules

  1. Single responsibility — each module should have one clearly defined responsibility
  2. Small modules — keep modules small and focused on a specific task
  3. Consistent naming — use consistent naming conventions for module files
  4. Explicit dependencies — all dependencies should be clearly defined in imports
  5. Minimize state mutation — avoid modifying global state in modules
  6. Documentation — add JSDoc comments to exported functions and classes
  7. Folder structure — organize modules into a logical folder structure (e.g. models, services, utils)

Summary

ES Modules is a powerful JavaScript code organization mechanism that allows you to create modular, well-organized applications. In a world as complex as Jurassic Park, where every mistake can have catastrophic consequences, good code organization is critical to maintaining safety and efficiency.

In the next exercise we'll look at an alternative module system — CommonJS, which is widely used in the Node.js environment, ideal for building the server-side part of our Jurassic Park management system.

Go to CodeWorlds