W poprzednim ćwiczeniu poznaliśmy ES Modules - nowoczesny system modułów w JavaScript. Teraz zagłębimy się w inny, starszy, ale wciąż bardzo ważny system modułów: CommonJS. Choć nasz Park Jurajski potrzebuje najnowszych technologii do monitorowania dinozaurów w przeglądarce, jego solidne zaplecze serwerowe opiera się na Node.js, gdzie dominuje właśnie CommonJS.
CommonJS to system modułów, który został stworzony głównie z myślą o środowisku serwerowym i stał się standardem w ekosystemie Node.js. Zapewnia sposób na organizację kodu JavaScript w odrębne moduły, podobnie jak ES Modules, ale z inną składnią i logiką działania.
W przeciwieństwie do ES Modules, które zostały dodane do języka JavaScript dopiero w ES6 (2015), CommonJS istniał wcześniej i stał się de facto standardem dla Node.js. Nawet teraz, mimo oficjalnego wsparcia dla ES Modules w Node.js, CommonJS pozostaje dominującym formatem dla modułów Node.js i w ekosystemie npm.
Główne koncepcje CommonJS to:
require() do importowania modułówmodule.exports lub exports do eksportowania elementówPrzyjrzyjmy się, jak te elementy działają w kontekście naszego systemu Parku Jurajskiego.
Najprostszy sposób eksportowania to przypisanie wartości do
module.exports:1// dinosaur.js - Model dinozaura
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} został nakarmiony. Poziom zdrowia: ${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 ? "WYSOKI POZIOM ZAGROŻENIA" : ""
23 };
24 }
25}
26
27// Eksport całej klasy
28module.exports = Dinosaur;Możemy również eksportować wiele elementów, przypisując obiekt do
module.exports:1// securityUtils.js - Narzędzia do obsługi bezpieczeństwa
2const SECURITY_LEVELS = {
3 GREEN: "normalny",
4 YELLOW: "podwyższony",
5 ORANGE: "wysoki",
6 RED: "krytyczny"
7};
8
9function checkFenceStatus(sectorId) {
10 // Symulacja sprawdzania statusu ogrodzenia w danym sektorze
11 const status = Math.random() > 0.1 ? "active" : "failure";
12 return {
13 sector: sectorId,
14 status,
15 timestamp: new Date().toISOString()
16 };
17}
18
19function triggerAlarm(sector, reason) {
20 console.log(`ALARM! Sektor ${sector}: ${reason}`);
21 // Logika uruchamiania alarmu
22 return {
23 success: true,
24 alarmId: "ALR-" + Math.floor(Math.random() * 10000)
25 };
26}
27
28function getSecurityStatus() {
29 // Zwraca ogólny status bezpieczeństwa parku
30 return {
31 level: SECURITY_LEVELS.GREEN,
32 activeAlarms: 0,
33 lastChecked: new Date().toISOString()
34 };
35}
36
37// Eksportowanie wielu elementów jako obiekt
38module.exports = {
39 SECURITY_LEVELS,
40 checkFenceStatus,
41 triggerAlarm,
42 getSecurityStatus
43};Możemy też dodawać eksporty pojedynczo, używając obiektu
exports (który jest referencją do module.exports):1// parkStats.js - Statystyki Parku Jurajskiego
2let totalVisitors = 0;
3let revenue = 0;
4const incidents = [];
5
6// Eksport pierwszej funkcji
7exports.addVisitors = function(count) {
8 totalVisitors += count;
9 return totalVisitors;
10};
11
12// Eksport drugiej funkcji
13exports.addRevenue = function(amount) {
14 revenue += amount;
15 return revenue;
16};
17
18// Eksport trzeciej funkcji
19exports.recordIncident = function(type, location, description) {
20 const incident = {
21 id: incidents.length + 1,
22 type,
23 location,
24 description,
25 timestamp: new Date().toISOString()
26 };
27 incidents.push(incident);
28 return incident;
29};
30
31// Eksport czwartej funkcji
32exports.getDailyStats = function() {
33 return {
34 totalVisitors,
35 revenue,
36 incidentCount: incidents.length
37 };
38};UWAGA: Należy pamiętać, że
exports jest tylko referencją do module.exports. Jeśli przypisujemy coś bezpośrednio do module.exports, tracimy tę referencję, dlatego nie możemy mieszać tych stylów w jednym pliku:1// To NIE zadziała
2exports.someFunction = function() { /* ... */ };
3module.exports = SomeClass; // To nadpisuje poprzedni eksport
4
5// To zadziała
6exports.someFunction = function() { /* ... */ };
7exports.SomeClass = SomeClass;
8
9// To też zadziała
10module.exports.someFunction = function() { /* ... */ };
11module.exports.SomeClass = SomeClass;
12
13// I to zadziała (przypisanie całego obiektu)
14module.exports = {
15 someFunction: function() { /* ... */ },
16 SomeClass: SomeClass
17};1// app.js
2const Dinosaur = require('./dinosaur.js');
3
4// Tworzymy nowe instancje dinozaurów
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());1// securityMonitor.js
2const security = require('./securityUtils.js');
3
4// Używamy importowanych funkcji i stałych z obiektu
5function monitorSecurity() {
6 const sectors = ["A1", "B2", "C3", "D4"];
7
8 // Sprawdzamy każdy sektor
9 sectors.forEach(sector => {
10 const fenceStatus = security.checkFenceStatus(sector);
11
12 if (fenceStatus.status === "failure") {
13 const alarmResult = security.triggerAlarm(
14 sector,
15 "Awaria ogrodzenia elektrycznego!"
16 );
17 console.log(`Alarm ${alarmResult.alarmId} uruchomiony w sektorze ${sector}`);
18
19 // Aktualizacja statusu bezpieczeństwa
20 console.log("Zmiana poziomu bezpieczeństwa na:", security.SECURITY_LEVELS.RED);
21 }
22 });
23}
24
25setInterval(monitorSecurity, 60000); // Sprawdzanie co minutęW nowszych wersjach Node.js możemy również używać destrukturyzacji bezpośrednio przy imporcie:
1// dashboard.js
2const { checkFenceStatus, triggerAlarm, SECURITY_LEVELS } = require('./securityUtils.js');
3
4// Teraz możemy używać funkcji bezpośrednio
5function checkAllSectors() {
6 ["A1", "B2", "C3", "D4"].forEach(sector => {
7 console.log(`Sprawdzanie sektora ${sector}: `, checkFenceStatus(sector));
8
9 // Używamy zaimportowanej stałej
10 if (Math.random() < 0.1) {
11 console.log(`Ustawianie poziomu bezpieczeństwa: ${SECURITY_LEVELS.ORANGE}`);
12 }
13 });
14}Warto zrozumieć kluczowe różnice między CommonJS a ES Modules:
| Cecha | CommonJS | ES Modules | |-------|----------|------------| | Składnia importu |
require() | import ... from ... |
| Składnia eksportu | module.exports = ... lub exports.x = ... | export ... lub export default ... |
| Ładowanie | Synchroniczne | Asynchroniczne (ładowane przed wykonaniem) |
| Wykonanie | Kod wykonywany przy każdym imporcie | Kod wykonywany tylko raz, następnie moduł jest buforowany |
| Statyczna analiza | Nie wspiera statycznej analizy (dynamiczne ścieżki) | Wspiera statyczną analizę (ścieżki są statyczne) |
| Wsparcie w przeglądarce | Nie wspierane natywnie | Wspierane w nowoczesnych przeglądarkach |
| Hoisting | Importy nie są hoistowane | Importy są hoistowane (można używać przed deklaracją) |
| Tree-shaking | Trudniejszy do zaimplementowania | Łatwiejszy dzięki statycznej analizie |Teraz przyjrzyjmy się, jak moglibyśmy zorganizować backend naszego systemu zarządzania Parkiem Jurajskim używając Node.js i CommonJS:
1jurassic-park-backend/
2├── config/
3│ ├── database.js # Konfiguracja bazy danych
4│ ├── environment.js # Zmienne środowiskowe
5│ └── security.js # Ustawienia bezpieczeństwa
6├── models/
7│ ├── dinosaur.js # Model dinozaura
8│ ├── employee.js # Model pracownika
9│ ├── facility.js # Model placówki
10│ └── index.js # Agregator modeli
11├── controllers/
12│ ├── dinosaurController.js # Kontroler dinozaurów
13│ ├── securityController.js # Kontroler bezpieczeństwa
14│ └── employeeController.js # Kontroler pracowników
15├── routes/
16│ ├── api.js # Główny router API
17│ ├── dinosaurs.js # Trasy związane z dinozaurami
18│ └── security.js # Trasy związane z bezpieczeństwem
19├── services/
20│ ├── monitoringService.js # Usługa monitorowania
21│ ├── notificationService.js # Usługa powiadomień
22│ └── loggingService.js # Usługa logowania
23├── utils/
24│ ├── logger.js # Narzędzie do logowania
25│ └── validators.js # Walidatory danych
26├── middleware/
27│ ├── auth.js # Middleware autentykacji
28│ └── errorHandler.js # Obsługa błędów
29├── app.js # Główna aplikacja
30└── server.js # Plik startowy serweraZobaczmy, jak wyglądałyby przykładowe implementacje tych modułów:
1// config/database.js
2module.exports = {
3 host: process.env.DB_HOST || 'localhost',
4 port: process.env.DB_PORT || 27017,
5 name: process.env.DB_NAME || 'jurassic_park',
6 user: process.env.DB_USER || 'admin',
7 password: process.env.DB_PASSWORD || 'secret',
8
9 // Funkcja pomocnicza do tworzenia URI połączenia
10 getConnectionString: function() {
11 return `mongodb://${this.user}:${this.password}@${this.host}:${this.port}/${this.name}`;
12 }
13};1// models/dinosaur.js
2const mongoose = require('mongoose');
3const Schema = mongoose.Schema;
4
5// Schemat dinozaura
6const dinosaurSchema = new Schema({
7 name: { type: String, required: true },
8 species: { type: String, required: true },
9 diet: { type: String, enum: ['carnivore', 'herbivore', 'omnivore'], required: true },
10 dangerLevel: { type: Number, min: 1, max: 10, required: true },
11 enclosureId: { type: Schema.Types.ObjectId, ref: 'Enclosure' },
12 health: { type: Number, default: 100 },
13 status: {
14 isContained: { type: Boolean, default: true },
15 lastCheck: { type: Date, default: Date.now }
16 },
17 createdAt: { type: Date, default: Date.now },
18 updatedAt: { type: Date, default: Date.now }
19});
20
21// Metody schematu
22dinosaurSchema.methods.feed = function() {
23 this.health = Math.min(100, this.health + 20);
24 this.updatedAt = Date.now();
25 return this.save();
26};
27
28dinosaurSchema.methods.checkStatus = function() {
29 return {
30 name: this.name,
31 health: this.health,
32 isContained: this.status.isContained,
33 warning: this.dangerLevel > 7 ? "WYSOKI POZIOM ZAGROŻENIA" : ""
34 };
35};
36
37// Metody statyczne
38dinosaurSchema.statics.findDangerous = function() {
39 return this.find({ dangerLevel: { $gte: 8 } });
40};
41
42// Pre-save hook
43dinosaurSchema.pre('save', function(next) {
44 this.updatedAt = Date.now();
45 next();
46});
47
48// Utworzenie modelu
49const Dinosaur = mongoose.model('Dinosaur', dinosaurSchema);
50
51// Eksport modelu
52module.exports = Dinosaur;1// controllers/dinosaurController.js
2const Dinosaur = require('../models/dinosaur');
3const logger = require('../utils/logger');
4
5// Obiekt kontrolera z wieloma metodami
6const dinosaurController = {
7 // Pobierz wszystkie dinozaury
8 getAllDinosaurs: async (req, res) => {
9 try {
10 const dinosaurs = await Dinosaur.find().populate('enclosureId');
11 res.json(dinosaurs);
12 } catch (error) {
13 logger.error('Błąd podczas pobierania dinozaurów:', error);
14 res.status(500).json({ error: 'Błąd serwera' });
15 }
16 },
17
18 // Pobierz dinozaura po ID
19 getDinosaurById: async (req, res) => {
20 try {
21 const dinosaur = await Dinosaur.findById(req.params.id).populate('enclosureId');
22
23 if (!dinosaur) {
24 return res.status(404).json({ error: 'Dinozaur nie znaleziony' });
25 }
26
27 res.json(dinosaur);
28 } catch (error) {
29 logger.error(`Błąd podczas pobierania dinozaura ID=${req.params.id}: `, error);
30 res.status(500).json({ error: 'Błąd serwera' });
31 }
32 },
33
34 // Dodaj nowego dinozaura
35 createDinosaur: async (req, res) => {
36 try {
37 const { name, species, diet, dangerLevel, enclosureId } = req.body;
38
39 const newDinosaur = new Dinosaur({
40 name,
41 species,
42 diet,
43 dangerLevel,
44 enclosureId
45 });
46
47 await newDinosaur.save();
48 logger.info(`Dodano nowego dinozaura: ${name} (${species})`);
49
50 res.status(201).json(newDinosaur);
51 } catch (error) {
52 logger.error('Błąd podczas tworzenia dinozaura:', error);
53 res.status(400).json({ error: error.message });
54 }
55 },
56
57 // Nakarm dinozaura
58 feedDinosaur: async (req, res) => {
59 try {
60 const dinosaur = await Dinosaur.findById(req.params.id);
61
62 if (!dinosaur) {
63 return res.status(404).json({ error: 'Dinozaur nie znaleziony' });
64 }
65
66 await dinosaur.feed();
67 logger.info(`Nakarmiono dinozaura: ${dinosaur.name}`);
68
69 res.json({
70 message: `${dinosaur.name} został nakarmiony. Poziom zdrowia: ${dinosaur.health}%`,
71 health: dinosaur.health
72 });
73 } catch (error) {
74 logger.error(`Błąd podczas karmienia dinozaura ID=${req.params.id}: `, error);
75 res.status(500).json({ error: 'Błąd serwera' });
76 }
77 },
78
79 // Znajdź niebezpieczne dinozaury
80 getDangerousDinosaurs: async (req, res) => {
81 try {
82 const dangerousDinos = await Dinosaur.findDangerous();
83 res.json(dangerousDinos);
84 } catch (error) {
85 logger.error('Błąd podczas pobierania niebezpiecznych dinozaurów:', error);
86 res.status(500).json({ error: 'Błąd serwera' });
87 }
88 }
89};
90
91// Eksport całego obiektu kontrolera
92module.exports = dinosaurController;1// routes/dinosaurs.js
2const express = require('express');
3const router = express.Router();
4const dinosaurController = require('../controllers/dinosaurController');
5const authMiddleware = require('../middleware/auth');
6
7// Trasy związane z dinozaurami
8router.get('/', dinosaurController.getAllDinosaurs);
9router.get('/dangerous', dinosaurController.getDangerousDinosaurs);
10router.get('/:id', dinosaurController.getDinosaurById);
11
12// Trasy wymagające uwierzytelnienia
13router.post('/', authMiddleware.verifyToken, dinosaurController.createDinosaur);
14router.post('/:id/feed', authMiddleware.verifyToken, dinosaurController.feedDinosaur);
15
16// Eksport routera
17module.exports = router;1// app.js
2const express = require('express');
3const mongoose = require('mongoose');
4const cors = require('cors');
5const morgan = require('morgan');
6const helmet = require('helmet');
7
8// Importy konfiguracji i middleware
9const dbConfig = require('./config/database');
10const errorHandler = require('./middleware/errorHandler');
11const logger = require('./utils/logger');
12
13// Importy tras
14const apiRouter = require('./routes/api');
15const dinosaurRoutes = require('./routes/dinosaurs');
16const securityRoutes = require('./routes/security');
17
18// Inicjalizacja aplikacji Express
19const app = express();
20
21// Middleware
22app.use(helmet()); // Bezpieczeństwo
23app.use(cors()); // Obsługa Cross-Origin Resource Sharing
24app.use(morgan('dev')); // Logowanie żądań HTTP
25app.use(express.json()); // Parsowanie JSON
26app.use(express.urlencoded({ extended: false })); // Parsowanie formularzy
27
28// Połączenie z bazą danych
29mongoose.connect(dbConfig.getConnectionString(), {
30 useNewUrlParser: true,
31 useUnifiedTopology: true
32})
33.then(() => {
34 logger.info('Połączono z bazą danych MongoDB');
35})
36.catch(err => {
37 logger.error('Błąd połączenia z bazą danych:', err);
38 process.exit(1);
39});
40
41// Trasy
42app.use('/api', apiRouter);
43app.use('/api/dinosaurs', dinosaurRoutes);
44app.use('/api/security', securityRoutes);
45
46// Obsługa błędów 404
47app.use((req, res, next) => {
48 res.status(404).json({ error: 'Nie znaleziono' });
49});
50
51// Centralna obsługa błędów
52app.use(errorHandler);
53
54// Eksport aplikacji
55module.exports = app;1// server.js
2const http = require('http');
3const app = require('./app');
4const logger = require('./utils/logger');
5
6// Ustawienia portu
7const port = process.env.PORT || 3000;
8app.set('port', port);
9
10// Utworzenie serwera HTTP
11const server = http.createServer(app);
12
13// Nasłuchiwanie na połączenia
14server.listen(port, () => {
15 logger.info(`Serwer Parku Jurajskiego uruchomiony na porcie ${port}`);
16});
17
18// Obsługa błędów serwera
19server.on('error', (error) => {
20 if (error.syscall !== 'listen') {
21 throw error;
22 }
23
24 logger.error(`Błąd serwera: ${error}`);
25 process.exit(1);
26});Jednym z wyzwań przy pracy z CommonJS (i modułami ogólnie) są zależności cykliczne, które występują, gdy moduł A importuje moduł B, a moduł B importuje moduł A:
1// dinosaurUtils.js
2const Dinosaur = require('./dinosaur');
3
4function isDangerous(dino) {
5 return dino.dangerLevel > 7;
6}
7
8module.exports = { isDangerous };
9
10// dinosaur.js
11const dinosaurUtils = require('./dinosaurUtils'); // Zależność cykliczna!
12
13class Dinosaur {
14 // ... implementacja klasy
15
16 isHighRisk() {
17 return dinosaurUtils.isDangerous(this);
18 }
19}
20
21module.exports = Dinosaur;CommonJS radzi sobie z zależnościami cyklicznymi, ale może to prowadzić do dziwnych zachowań, jak niekompletne obiekty. Aby uniknąć takich problemów:
W wielu projektach musimy obsługiwać zarówno CommonJS, jak i ES Modules. Na przykład frontend naszego Parku Jurajskiego może używać ES Modules, a backend CommonJS. Co zrobić, gdy chcemy współdzielić kod?
Możemy użyć Babel do transpilacji między formatami:
1// babel.config.js dla ES Modules -> CommonJS
2module.exports = {
3 presets: [
4 ['@babel/preset-env', {
5 targets: {
6 node: 'current'
7 },
8 modules: 'commonjs' // Konwertuj import/export na require/exports
9 }]
10 ]
11};Innym podejściem jest tworzenie pakietów obsługujących oba formaty modułów. W package.json możesz zdefiniować:
1{
2 "name": "jurassic-park-utils",
3 "version": "1.0.0",
4 "main": "dist/index.js", // CommonJS entry
5 "module": "dist/index.mjs", // ES Module entry
6 "exports": {
7 "import": "./dist/index.mjs",
8 "require": "./dist/index.js"
9 }
10}Nowsze wersje Node.js (od 13.2.0) oficjalnie wspierają ES Modules. Możemy włączyć to wsparcie na kilka sposobów:
.mjs dla plików z ES Modules"type": "module" w package.jsonJednak wiele pakietów npm nadal używa CommonJS, więc prawdopodobnie będziesz musiał pracować z oboma systemami.
Załóżmy, że chcemy zmigrować część naszego backendu z CommonJS do ES Modules. Oto jak mogłaby wyglądać taka migracja:
1// controllers/dinosaurController.js (CommonJS)
2const Dinosaur = require('../models/dinosaur');
3const logger = require('../utils/logger');
4
5function getAllDinosaurs(req, res) {
6 // implementacja
7}
8
9function getDinosaurById(req, res) {
10 // implementacja
11}
12
13module.exports = {
14 getAllDinosaurs,
15 getDinosaurById
16};1// controllers/dinosaurController.mjs (ES Modules)
2import Dinosaur from '../models/dinosaur.mjs';
3import logger from '../utils/logger.mjs';
4
5export function getAllDinosaurs(req, res) {
6 // implementacja
7}
8
9export function getDinosaurById(req, res) {
10 // implementacja
11}
12
13// Możemy też eksportować domyślnie obiekt z funkcjami
14export default {
15 getAllDinosaurs,
16 getDinosaurById
17};Czasami potrzebujemy utworzyć wrapper, który pozwoli na import modułu ES Module z kodu CommonJS:
1// controllers/dinosaurControllerWrapper.js (CommonJS)
2// Ten plik ładuje moduł ES Module i eksportuje go jako CommonJS
3const importESM = async () => {
4 const controller = await import('./dinosaurController.mjs');
5 return controller.default; // lub wybrane funkcje
6};
7
8// Eksportujemy funkcję, która zwraca Promise z kontrolerem
9module.exports = importESM();CommonJS to powszechnie używany system modułów w Node.js, który pozwala na organizację kodu w odrębne moduły, co jest kluczowe dla budowania skalowalnych aplikacji, takich jak backend naszego systemu zarządzania Parkiem Jurajskim.
Główne koncepcje CommonJS to:
require() do importowania modułówmodule.exports lub exports do eksportowania elementówMimo rosnącej popularności ES Modules, CommonJS pozostaje istotnym systemem modułów, szczególnie w ekosystemie Node.js. Zrozumienie różnic między CommonJS a ES Modules jest kluczowe dla efektywnego rozwoju aplikacji JavaScript, które mogą działać zarówno w przeglądarce, jak i na serwerze.
W naszym Parku Jurajskim używamy CommonJS dla backendu (monitoring, baza danych, kontrola bezpieczeństwa) i ES Modules dla frontendu (interfejs użytkownika, dashboardy, powiadomienia). Ta kombinacja zapewnia nam najlepsze z obu światów i pozwala efektywnie zarządzać systemem, który musi być niezawodny - w końcu bezpieczeństwo parku i jego gości zależy od naszego kodu!
W następnym ćwiczeniu przyjrzymy się dynamicznemu importowaniu modułów, które pozwala na jeszcze większą elastyczność w ładowaniu kodu na żądanie, co może być kluczowe w sytuacjach awaryjnych w Parku Jurajskim.