Введение: почему работа с файлами в JavaScript — это мастхэв
Представьте, что вы — капитан космического корабля. Ваша задача — управлять грузовыми отсеками, запасами кислорода и бортовым журналом. В мире веб-разработки файловая система — это те самые отсеки, а JavaScript — ваш пульт управления. Без умения работать с файлами вы застрянете на орбите: не сможете сохранить настройки пользователя, загрузить конфигурацию сервера или обработать лог-файлы.
Многие новички думают, что JavaScript живёт только в браузере и не умеет трогать файлы на диске. Это миф! С появлением Node.js (и даже в современных браузерах через File API) JavaScript стал полноценным инструментом для работы с файловой системой. Сегодня мы разберём, как читать, писать, удалять и перемещать файлы, а также как не попасть в ловушку асинхронности.
Кстати, если вы только начинаете свой путь в программировании, советую сначала изучить основы JavaScript — это поможет быстрее разобраться с примерами ниже.
Где JavaScript работает с файлами? Два мира — два подхода
Прежде чем нырять в код, давайте проясним: файловые операции в JS делятся на два лагеря.
1. Серверный JavaScript (Node.js)
Здесь у нас полный доступ к файловой системе: создание, удаление, переименование, чтение потоков — всё, что душе угодно. Работаем через встроенный модуль fs (File System). Это как швейцарский нож для работы с файлами.
2. Клиентский JavaScript (браузер)
Браузер — это «песочница» с ограничениями. Вы не можете просто так взять и прочитать любой файл с компьютера пользователя (иначе бы хакеры праздновали). Но можно работать с файлами, которые пользователь сам выбрал через <input type="file">, или через API FileReader и Blob.
В этой статье мы сфокусируемся на Node.js, но в конце дадим бонус — пример для браузера.
Модуль fs: ваш главный инструмент
В Node.js работа с файлами начинается с подключения модуля fs:
const fs = require('fs'); // старый стиль
// или
import fs from 'fs'; // если используете ES-модули
У fs есть три режима работы: синхронный, асинхронный на колбэках и асинхронный на промисах. Мы будем использовать последний — с async/await, так как это современный и читаемый подход.
Чтение файла: от простого к сложному
Допустим, у нас есть файл data.txt с текстом. Самый простой способ прочитать его:
import { readFile } from 'fs/promises';
async function readMyFile() {
try {
const data = await readFile('data.txt', 'utf-8');
console.log(data);
} catch (err) {
console.error('Ошибка чтения:', err.message);
}
}
readMyFile();
Что здесь важно? 'utf-8' — кодировка. Если её не указать, вы получите сырой буфер (Buffer), который нужно будет преобразовать в строку. Для текстовых файлов всегда указывайте кодировку.
Если файл большой (например, лог-файл на 500 МБ), читать его целиком — плохая идея. Тут на помощь приходят потоки (streams):
import { createReadStream } from 'fs';
import { createInterface } from 'readline';
async function readLargeFile() {
const stream = createReadStream('bigfile.log', 'utf-8');
const rl = createInterface({ input: stream });
for await (const line of rl) {
console.log('Строка:', line);
// Обрабатываем каждую строку по мере чтения
}
}
readLargeFile();
Запись в файл: создаём и дописываем
Запись бывает двух типов: перезапись (write) и добавление (append). Разница очевидна: write сотрёт старый файл, append добавит в конец.
import { writeFile, appendFile } from 'fs/promises';
// Перезапись файла
await writeFile('config.json', JSON.stringify({ theme: 'dark' }));
// Добавление строки в лог
await appendFile('app.log', `[${new Date().toISOString()}] Сервер запущен\n`);
Обратите внимание: если файла не существует, writeFile создаст его, а appendFile — тоже создаст, если его нет. Удобно, правда?
Управление файловой системой: создаём, перемещаем, удаляем
Файлы — это не только чтение и запись. Иногда нужно создать папку, переименовать файл или проверить, существует ли он.
Проверка существования файла
Многие новички делают так:
if (fs.existsSync('file.txt')) { ... }
Это работает, но синхронно — блокирует поток. Лучше использовать асинхронный вариант:
import { access } from 'fs/promises';
import { constants } from 'fs';
async function fileExists(path) {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
Создание и удаление директорий
import { mkdir, rmdir, unlink } from 'fs/promises';
// Создать папку (рекурсивно — создаст все вложенные)
await mkdir('logs/2024/01', { recursive: true });
// Удалить пустую папку
await rmdir('old_data');
// Удалить файл
await unlink('temp.txt');
Важно: rmdir не удалит непустую папку. Для рекурсивного удаления используйте rm (доступен с Node.js 14.14):
import { rm } from 'fs/promises';
await rm('old_project', { recursive: true, force: true });
Перемещение и переименование
import { rename } from 'fs/promises';
// Переименовать файл
await rename('old_name.txt', 'new_name.txt');
// Переместить в другую папку
await rename('source/file.txt', 'backup/file_backup.txt');
Работа с путями: чтобы не было «сломанных» ссылок
Одна из самых частых ошибок — жёстко закодированные пути. На Windows пути с обратной косой чертой (\), на Linux — с прямой (/). Используйте модуль path:
import { join, resolve, basename, extname } from 'path';
const configPath = join(__dirname, 'config', 'app.json');
// Результат: /home/user/project/config/app.json (в Linux)
const fullPath = resolve('data', '..', 'config', 'settings.json');
// resolve нормализует путь: ../ означает подняться на уровень выше