⚡ JavaScript

Итоговый практический проект в JavaScript (Node.js): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты учишься готовить. Сначала ты учишься резать овощи (переменные), потом жарить яйца (условия), потом варить суп (циклы). Ты знаешь много отдельных рецептов. Но настоящее мастерство приходит, когда ты можешь приготовить полноценный обед из трёх блюд, где всё готовится одновременно, не пригорает и подаётся горячим.

Итоговый практический проект — это и есть такой «полноценный обед» в программировании. Это программа, которая объединяет всё, что ты выучил: переменные, функции, массивы, объекты, работу с файлами, обработку ошибок. Она решает реальную задачу, а не просто показывает, как работает какая-то одна команда.

«Проект — это не экзамен. Это твоё портфолио. Вещь, которую можно показать друзьям и сказать: "Смотри, я это сделал!"»

Зачем это нужно? Во-первых, чтобы закрепить знания. Когда ты пишешь код для проекта, ты не просто повторяешь — ты думаешь, как соединить кусочки. Во-вторых, чтобы понять, как работают настоящие программы. В реальной жизни никто не пишет «задачу про котиков» — пишут приложения, сайты, ботов. В-третьих, это твой первый шаг в портфолио. Даже простой проект, сделанный аккуратно, показывает работодателю: «Этот человек умеет доводить дело до конца».

В этой статье мы разберём, как спроектировать и написать итоговый проект на JavaScript в среде Node.js. Мы не будем учить новым командам — мы научимся собирать из них работающую систему.

Теория: как это работает изнутри

Любой проект, даже самый сложный, состоит из трёх уровней:

  1. Входные данные — то, с чем программа работает (файлы, нажатия клавиш, запросы из интернета).
  2. Логика обработки — то, как программа эти данные превращает в результат (расчёты, поиск, сортировка).
  3. Выходные данные — то, что программа выдаёт пользователю (сохранённый файл, ответ в консоли, график).

В Node.js (это JavaScript, который работает не в браузере, а на компьютере) у нас есть суперспособности: мы можем читать и писать файлы, слушать порты, запускать другие программы. Но главное — у нас есть модульная система. Это как конструктор Lego: каждый кубик (модуль) делает что-то своё, а мы собираем из них общую фигуру.

Внутри проекта код обычно делится на файлы:

  • index.js или main.js — точка входа, с которой всё начинается.
  • utils.js — вспомогательные функции (например, форматирование даты).
  • data/ — папка с файлами данных (если они есть).
  • package.json — файл с описанием проекта (как паспорт).

Когда ты запускаешь проект командой node index.js, происходит вот что:

  1. Node.js читает файл index.js.
  2. Видит команды require() — подключает другие файлы и библиотеки.
  3. Выполняет код сверху вниз.
  4. Если программа должна работать долго (например, сервер), она не завершается — ждёт событий.

Самое важное в проекте — архитектура. Это не страшное слово. Это просто ответ на вопрос: «Как у меня всё устроено?» Хорошая архитектура — когда можно легко добавить новую функцию, не сломав старую. Плохая — когда всё в одном файле и ты боишься что-то менять.

«Хороший код — это код, который легко читать. Отличный код — это код, который легко менять.»

Синтаксис в JavaScript (Node.js): все формы и варианты

В проекте мы используем не какой-то особый синтаксис, а комбинацию уже знакомых вещей. Давай вспомним главные инструменты, которые нам понадобятся.

1. Модули (подключение файлов)

В Node.js есть два способа подключать модули: старый (CommonJS) и новый (ES Modules). Мы будем использовать старый — он проще и работает везде.

// Подключаем встроенный модуль для работы с файловой системой
const fs = require('fs');

// Подключаем свой собственный файл (например, helpers.js)
const helpers = require('./helpers.js');

// Подключаем библиотеку, установленную через npm (например, chalk для цветов)
const chalk = require('chalk');

2. Чтение и запись файлов

Это основа любого проекта, который хранит данные.

// Чтение файла (синхронное — программа ждёт, пока файл прочитается)
const data = fs.readFileSync('data.txt', 'utf8');

// Запись в файл (создаёт файл, если его нет, или перезаписывает)
fs.writeFileSync('output.txt', 'Привет, мир!');

// Добавление в конец файла (не перезаписывает, а дописывает)
fs.appendFileSync('log.txt', 'Новая запись
');

3. Работа с JSON

JSON — это формат данных, который понимают и люди, и компьютеры. В проектах мы часто храним настройки или данные в JSON.

// Превращаем объект в строку JSON
const obj = { name: 'Анна', age: 12 };
const jsonString = JSON.stringify(obj, null, 2); // null, 2 — для красивого форматирования

// Превращаем строку JSON обратно в объект
const parsed = JSON.parse(jsonString);
console.log(parsed.name); // Анна

4. Обработка ошибок (try/catch)

Программа не должна падать, если что-то пошло не так. Ошибки нужно ловить.

try {
  // Пытаемся сделать что-то опасное
  const data = fs.readFileSync('несуществующий_файл.txt', 'utf8');
} catch (error) {
  // Если произошла ошибка — не падаем, а сообщаем
  console.log('Ошибка: файл не найден. Будет использован пустой список.');
  const data = '[]'; // Значение по умолчанию
}

5. Аргументы командной строки

Когда мы запускаем программу, мы можем передать ей параметры.

// Получаем аргументы (первый — путь к node, второй — путь к файлу, остальные — наши)
const args = process.argv.slice(2); // Отрезаем первые два

// Пример: node index.js --name=Анна
// args[0] будет '--name=Анна'

// Разбираем аргументы вручную
const params = {};
args.forEach(arg => {
  const [key, value] = arg.split('=');
  params[key.replace('--', '')] = value;
});
console.log(params.name); // Анна

6. Функции обратного вызова (колбэки) и промисы

В Node.js много асинхронных операций. Мы можем работать с ними через колбэки или промисы.

// Асинхронное чтение файла через колбэк
fs.readFile('data.txt', 'utf8', (err, data) => {
  if (err) {
    console.log('Ошибка:', err);
    return;
  }
  console.log('Данные:', data);
});

// Асинхронное чтение через промис (современный способ)
const fsPromises = require('fs').promises;
async function readData() {
  try {
    const data = await fsPromises.readFile('data.txt', 'utf8');
    console.log('Данные:', data);
  } catch (err) {
    console.log('Ошибка:', err);
  }
}

Примеры кода — от простого к сложному

Теперь самое интересное: мы напишем семь примеров, от простой программы до полноценного проекта. Каждый пример — рабочий, его можно скопировать и запустить.

Пример 1: Приветствие с именем

Самая простая программа, которая берёт имя из аргументов и приветствует пользователя.

// Файл: hello.js
// Запуск: node hello.js --name=Маша

// Получаем аргументы командной строки
const args = process.argv.slice(2);

// Проверяем, есть ли аргументы
if (args.length === 0) {
  console.log('Привет, незнакомец!');
  console.log('Использование: node hello.js --name=ТвоёИмя');
  process.exit(0); // Завершаем программу
}

// Извлекаем имя из аргумента --name=...
let userName = 'Гость'; // Значение по умолчанию
args.forEach(arg => {
  if (arg.startsWith('--name=')) {
    userName = arg.split('=')[1];
  }
});

// Выводим приветствие
console.log(`Привет, ${userName}! Рад познакомиться.`);
console.log(`Сегодня ${new Date().toLocaleDateString('ru-RU')}.`);

Объяснение: Мы учим программу принимать параметры. Если параметра нет — используем значение по умолчанию. Это основа для любого проекта с настройками.

Пример 2: Записная книжка (одна запись)

Программа сохраняет одну заметку в файл.

// Файл: note.js
// Запуск: node note.js "Купить молоко"

const fs = require('fs');
const args = process.argv.slice(2);

// Проверяем, передали ли текст заметки
if (args.length === 0) {
  console.log('Ошибка: не указан текст заметки.');
  console.log('Использование: node note.js "Текст заметки"');
  process.exit(1); // 1 — код ошибки
}

const noteText = args.join(' '); // Объединяем все аргументы в строку
const fileName = 'notes.txt';

try {
  // Добавляем заметку в файл (с новой строки и датой)
  const timestamp = new Date().toLocaleString('ru-RU');
  const entry = `[${timestamp}] ${noteText}
`;
  fs.appendFileSync(fileName, entry, 'utf8');
  console.log(`Заметка сохранена в файл ${fileName}`);
} catch (error) {
  console.log('Ошибка при сохранении заметки:', error.message);
}

Объяснение: Мы используем appendFileSync — это безопасно, потому что мы не перезаписываем старые заметки, а добавляем новые. Обработка ошибок через try/catch защищает программу от падения.

Пример 3: Чтение всех заметок

Программа показывает все сохранённые заметки.

// Файл: read-notes.js
// Запуск: node read-notes.js

const fs = require('fs');
const fileName = 'notes.txt';

// Проверяем, существует ли файл
if (!fs.existsSync(fileName)) {
  console.log('Заметок пока нет. Создай первую: node note.js "Твоя заметка"');
  process.exit(0);
}

try {
  // Читаем весь файл
  const content = fs.readFileSync(fileName, 'utf8');
  
  // Разбиваем на строки и выводим каждую с номером
  const lines = content.split('
').filter(line => line.trim() !== '');
  
  if (lines.length === 0) {
    console.log('Файл пуст.');
  } else {
    console.log('=== Твои заметки ===');
    lines.forEach((line, index) => {
      console.log(`${index + 1}. ${line}`);
    });
    console.log(`Всего заметок: ${lines.length}`);
  }
} catch (error) {
  console.log('Ошибка при чтении файла:', error.message);
}

Объяснение: Мы проверяем, существует ли файл, чтобы не получить ошибку. Фильтруем пустые строки. Нумеруем заметки для удобства.

Пример 4: Менеджер задач (Todo List) — основа

Настоящий проект: добавляем, смотрим и отмечаем задачи.

// Файл: todo.js
// Запуск: node todo.js add "Купить хлеб"
//         node todo.js list
//         node todo.js done 1

const fs = require('fs');
const args = process.argv.slice(2);
const command = args[0]; // Первый аргумент — команда
const DATA_FILE = 'tasks.json';

// Функция загрузки задач из файла
function loadTasks() {
  try {
    if (!fs.existsSync(DATA_FILE)) {
      return []; // Если файла нет — возвращаем пустой массив
    }
    const data = fs.readFileSync(DATA_FILE, 'utf8');
    return JSON.parse(data);
  } catch (error) {
    console.log('Ошибка загрузки задач. Создаём новый список.');
    return [];
  }
}

// Функция сохранения задач в файл
function saveTasks(tasks) {
  try {
    fs.writeFileSync(DATA_FILE, JSON.stringify(tasks, null, 2), 'utf8');
  } catch (error) {
    console.log('Ошибка сохранения:', error.message);
  }
}

// Команда: добавить задачу
function addTask(text) {
  const tasks = loadTasks();
  const newTask = {
    id: tasks.length + 1,
    text: text,
    done: false,
    createdAt: new Date().toISOString()
  };
  tasks.push(newTask);
  saveTasks(tasks);
  console.log(`✅ Задача добавлена: "${text}" (ID: ${newTask.id})`);
}

// Команда: показать все задачи
function listTasks() {
  const tasks = loadTasks();
  if (tasks.length === 0) {
    console.log('📋 Список задач пуст. Добавь первую: node todo.js add "Твоя задача"');
    return;
  }
  
  console.log('=== Список задач ===');
  tasks.forEach(task => {
    const status = task.done ? '✅' : '⬜';
    console.log(`${status} [${task.id}] ${task.text}`);
  });
  console.log(`Всего задач: ${tasks.length}`);
}

// Команда: отметить задачу как выполненную
function doneTask(id) {
  const tasks = loadTasks();
  const task = tasks.find(t => t.id === id);
  
  if (!task) {
    console.log(`❌ Задача с ID ${id} не найдена.`);
    return;
  }
  
  if (task.done) {
    console.log(`⚠️ Задача "${task.text}" уже выполнена.`);
    return;
  }
  
  task.done = true;
  saveTasks(tasks);
  console.log(`✅ Задача "${task.text}" отмечена как выполненная!`);
}

// Главная логика: выбираем команду
switch (command) {
  case 'add':
    const text = args.slice(1).join(' ');
    if (!text) {
      console.log('Ошибка: не указан текст задачи.');
      console.log('Использование: node todo.js add "Текст задачи"');
    } else {
      addTask(text);
    }
    break;
    
  case 'list':
    listTasks();
    break;
    
  case 'done':
    const id = parseInt(args[1]);
    if (isNaN(id)) {
      console.log('Ошибка: укажите ID задачи (число).');
      console.log('Использование: node todo.js done 1');
    } else {
      doneTask(id);
    }
    break;
    
  default:
    console.log('Неизвестная команда. Доступные команды:');
    console.log('  add "текст" — добавить задачу');
    console.log('  list — показать все задачи');
    console.log('  done ID — отметить задачу выполненной');
}

Объяснение: Это уже полноценное приложение. Мы разделили код на функции, каждая делает своё дело. Данные хранятся в JSON — это удобно и читаемо. Обработка ошибок есть везде. Программа не падает, если файл повреждён или команда введена неверно.

Пример 5: Генератор паролей

Программа создаёт случайные пароли с настройками.

// Файл: password-generator.js
// Запуск: node password-generator.js --length=12 --numbers --symbols

const args = process.argv.slice(2);

// Парсим аргументы
const settings = {
  length: 8,        // Длина пароля по умолчанию
  useNumbers: false, // Использовать цифры
  useSymbols: false, // Использовать символы
  count: 1          // Сколько паролей сгенерировать
};

args.forEach(arg => {
  if (arg.startsWith('--length=')) {
    settings.length = parseInt(arg.split('=')[1]);
  } else if (arg === '--numbers') {
    settings.useNumbers = true;
  } else if (arg === '--symbols') {
    settings.useSymbols = true;
  } else if (arg.startsWith('--count=')) {
    settings.count = parseInt(arg.split('=')[1]);
  }
});

// Наборы символов
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?';

// Функция генерации одного пароля
function generatePassword() {
  let chars = letters;
  if (settings.useNumbers) chars += numbers;
  if (settings.useSymbols) chars += symbols;
  
  let password = '';
  for (let i = 0; i < settings.length; i++) {
    const randomIndex = Math.floor(Math.random() * chars.length);
    password += chars[randomIndex];
  }
  return password;
}

// Генерируем нужное количество паролей
console.log('=== Сгенерированные пароли ===');
for (let i = 0; i < settings.count; i++) {
  console.log(`${i + 1}. ${generatePassword()}`);
}
console.log(`Параметры: длина ${settings.length}, цифры: ${settings.useNumbers}, символы: ${settings.useSymbols}`);

Объяснение: Программа использует случайные числа для выбора символов. Настройки передаются через аргументы. Можно генерировать сразу несколько паролей.

Пример 6: Простой HTTP-сервер

Сервер, который отвечает на запросы из браузера.

// Файл: server.js
// Запуск: node server.js
// Открой браузер: http://localhost:3000

const http = require('http');

// Создаём сервер
const server = http.createServer((request, response) => {
  // request — что прислал браузер
  // response — что мы отправляем обратно
  
  console.log(`Запрос: ${request.method} ${request.url}`);
  
  // Устанавливаем заголовки ответа
  response.writeHead(200, {
    'Content-Type': 'text/html; charset=utf-8'
  });
  
  // Формируем HTML-страницу
  const html = `
    
    
    
      Мой первый сервер
      
    
    
      

Привет с сервера!

Текущее время: ${new Date().toLocaleString('ru-RU')}

Ты запросил: ${request.url}

`; // Отправляем ответ response.end(html); }); // Запускаем сервер на порту 3000 const PORT = 3000; server.listen(PORT, () => { console.log(`Сервер запущен на http://localhost:${PORT}`); console.log('Нажми Ctrl+C, чтобы остановить сервер.'); });

Объяснение: Сервер — это программа, которая работает постоянно и ждёт запросы. Когда приходит запрос, мы формируем HTML-страницу и отправляем её. Это основа для веб-приложений.

Пример 7: Итоговый проект — Персональный дневник

Объединяем всё: файлы, JSON, аргументы, сервер. Это уже серьёзное приложение.

// Файл: diary.js
// Запуск: node diary.js --action=add --text="Сегодня был хороший день"
//         node diary.js --action=list
//         node diary.js --action=server (запускает веб-сервер)

const fs = require('fs');
const http = require('http');
const args = process.argv.slice(2);
const DATA_FILE = 'diary.json';

// Парсим аргументы
const params = {};
args.forEach(arg => {
  const [key, value] = arg.replace('--', '').split('=');
  params[key] = value || true;
});

// Загружаем записи
function loadEntries() {
  try {
    if (!fs.existsSync(DATA_FILE)) return [];
    return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
  } catch {
    return [];
  }
}

// Сохраняем записи
function saveEntries(entries) {
  fs.writeFileSync(DATA_FILE, JSON.stringify(entries, null, 2), 'utf8');
}

// Добавление записи
function addEntry(text) {
  const entries = loadEntries();
  const entry = {
    id: entries.length + 1,
    text: text,
    date: new Date().toISOString(),
    mood: params.mood || 'neutral'
  };
  entries.push(entry);
  saveEntries(entries);
  console.log(`📝 Запись добавлена: "${text}"`);
}

// Просмотр записей
function listEntries() {
  const entries = loadEntries();
  if (entries.length === 0) {
    console.log('Дневник пуст. Добавь первую запись!');
    return;
  }
  
  console.log('=== Мой дневник ===');
  entries.forEach(entry => {
    const date = new Date(entry.date).toLocaleString('ru-RU');
    const moodEmoji = { happy: '😊', sad: '😢', neutral: '😐', angry: '😠' };
    const emoji = moodEmoji[entry.mood] || '😐';
    console.log(`[${entry.id}] ${date} ${emoji}`);
    console.log(`     ${entry.text}`);
    console.log('---');
  });
}

// Запуск веб-сервера для просмотра дневника
function startServer() {
  const server = http.createServer((req, res) => {
    const entries = loadEntries();
    
    let html = `
      
      
      
        Мой дневник
        
      
      
        

📖 Мой дневник

`; if (entries.length === 0) { html += '

Записей пока нет.

'; } else { entries.reverse().forEach(entry => { const date = new Date(entry.date).toLocaleString('ru-RU'); html += `
${entry.mood === 'happy' ? '😊' : entry.mood === 'sad' ? '😢' : '😐'}
${date}

${entry.text}

`; }); } html += ' '; res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); }); server.listen(3000, () => { console.log('🌐 Сервер дневника запущен на http://localhost:3000'); }); } // Главное меню switch (params.action) { case 'add': if (!params.text) { console.log('Ошибка: укажи текст записи через --text="..."'); } else { addEntry(params.text); } break; case 'list': listEntries(); break; case 'server': startServer(); break; default: console.log('Доступные действия:'); console.log(' --action=add --text="Текст" — добавить запись'); console.log(' --action=list — показать все записи'); console.log(' --action=server — запустить веб-сервер'); console.log(' --mood=happy|sad|neutral|angry — настроение (для add)'); }

Объяснение: Это полноценное приложение с тремя режимами работы. Данные хранятся в JSON. Можно добавлять записи с указанием настроения, просматривать их в консоли или через веб-интерфейс. Код разбит на функции, есть обработка ошибок, понятные сообщения пользователю.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаешь Python или планируешь его учить, вот сравнение подходов к проектам.

Аспект JavaScript (Node.js) Python
Запуск проекта node index.js python main.py
Подключение модулей const fs = require('fs') import os или from pathlib import Path
Чтение файла fs.readFileSync('file.txt', 'utf8') open('file.txt').read()
Работа с JSON JSON.parse() и JSON.stringify() json.loads() и json.dumps()
Аргументы командной строки process.argv sys.argv
Асинхронность Колбэки, промисы, async/await async/await (похоже)
Установка библиотек npm install имя pip install имя
Веб-сервер (простой) Встроенный модуль http Встроенный http.server
Типизация Динамическая, слабая Динамическая, сильная

Главное отличие: JavaScript изначально создавался для браузера, поэтому в нём много асинхронности «из коробки». Python — более «академичный», код часто читается как псевдокод. Но для проектов оба языка подходят отлично. Выбирай тот, который тебе ближе.

«Хороший программист не воюет за язык. Он выбирает инструмент под задачу.»

Типичные ошибки и подводные камни

Даже опытные программисты иногда наступают на эти грабли. Вот список самых частых ошибок в проектах на Node.js.

1. Забыли обработать ошибку

Самая частая ошибка новичков — не использовать try/catch при работе с файлами.

// ❌ Плохо: программа упадёт, если файла нет
const data = fs.readFileSync('file.txt', 'utf8');

// ✅ Хорошо: ошибка обработана
try {
  const data = fs.readFileSync('file.txt', 'utf8');
} catch (err) {
  console.log('Файл не найден, используем пустой массив');
  const data = '[]';
}

2. Не проверяем существование файла

Перед чтением файла стоит проверить, существует ли он.

// ❌ Плохо: может выдать ошибку
const data = fs.readFileSync('data.json');

// ✅ Хорошо: проверяем
if (fs.existsSync('data.json')) {
  const data = fs.readFileSync('data.json');
} else {
  // Создаём файл с начальными данными
  fs.writeFileSync('data.json', '[]');
}

3. Путаница с синхронным и асинхронным кодом

Если используешь readFileSync — программа ждёт. Если readFile — не ждёт, и данные будут позже.

// ❌ Ошибка: данные ещё не загружены
let data;
fs.readFile('file.txt', 'utf8', (err, content) => {
  data = content;
});
console.log(data); // undefined!

// ✅ Правильно: используем колбэк
fs.readFile('file.txt', 'utf8', (err, data) => {
  console.log(data); // Теперь данные есть
});

4. Забыли про кодировку

Если не указать 'utf8', Node.js вернёт буфер (набор байтов), а не текст.

// ❌ Плохо: получим буфер
const data = fs.readFileSync('file.txt');
console.log(data); // <Buffer 48 65 6c 6c 6f>

// ✅ Хорошо: указываем кодировку
const data =