Работа с файлами в JavaScript (Node.js): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — капитан космического корабля. У тебя есть бортовой журнал, в который ты записываешь всё, что происходит в полёте: координаты, показатели топлива, встречи с астероидами. Когда полёт закончится, ты сможешь открыть этот журнал и вспомнить каждую деталь путешествия. Твой компьютер — это такой же корабль, а файлы на жёстком диске — это бесконечное множество бортовых журналов.
Когда программа запускается, все её данные живут в оперативной памяти — как мысли в голове капитана. Но стоит выключить компьютер, и эти мысли исчезают навсегда. Файлы же позволяют сохранить информацию на годы: тексты, настройки, результаты вычислений, списки пользователей. В JavaScript (Node.js) мы можем не только читать эти «бортовые журналы», но и создавать новые, дополнять их и даже удалять.
💡 Важно понимать: В браузере JavaScript не имеет доступа к файловой системе компьютера — это сделано в целях безопасности. Но Node.js, который работает на сервере, даёт программисту полный контроль над файлами. Именно его мы и будем изучать.
Чтение файлов — это как открытие книги. Ты можешь прочитать её целиком (всё сразу), можешь читать по строчкам (как стихотворение) или даже по кусочкам (как энциклопедию, где нужна только одна статья). В этой статье мы научимся всем этим способам.
Теория: как это работает изнутри
Чтобы понять, как Node.js работает с файлами, нужно разобраться в трёх ключевых понятиях: потоки, буферы и кодировки.
Потоки (Streams)
Представь, что ты пьёшь сок через трубочку. Ты не выпиваешь стакан за один глоток — ты втягиваешь жидкость постепенно. Поток в программировании работает так же: данные передаются маленькими порциями. Это особенно важно для больших файлов (видео, базы данных), потому что загружать их целиком в память — это как пытаться проглотить арбуз целиком.
В Node.js есть четыре типа потоков:
- Readable — поток для чтения (ты втягиваешь сок)
- Writable — поток для записи (ты выливаешь сок)
- Duplex — можно и читать, и писать (как раковина с двумя кранами)
- Transform — данные можно изменять на лету (как фильтр для воды)
Буферы (Buffers)
Буфер — это временное хранилище, куда складываются кусочки данных, пока ты решаешь, что с ними делать. Когда ты читаешь файл через поток, данные приходят не строками, а наборами байтов. Буфер позволяет собрать эти байты и превратить их в текст или другой формат.
Представь, что ты собираешь пазл: ты не кладёшь каждый кусочек сразу на место, а сначала складываешь их в кучку (буфер), а потом уже собираешь картинку.
Кодировки (Encodings)
Компьютер хранит всё в виде чисел (байтов). Чтобы превратить эти числа в буквы, нужна таблица соответствия — кодировка. Самая популярная — UTF-8, которая понимает и русские буквы, и иероглифы, и смайлики. Если открыть файл в неправильной кодировке, ты увидишь «кракозябры» — бессмысленные символы.
⚠️ Золотое правило: Всегда указывай кодировку 'utf-8' при работе с текстовыми файлами. Иначе Node.js будет возвращать тебе сырые байты, с которыми неудобно работать.
Синхронный и асинхронный подход
Node.js знаменит своей асинхронностью. Это как заказ в ресторане: ты можешь сидеть и ждать, пока повар приготовит еду (синхронно), или заняться своими делами, а когда блюдо будет готово, официант тебя позовёт (асинхронно).
При чтении файлов у тебя есть выбор:
- Синхронные методы (с суффиксом Sync) — программа останавливается и ждёт, пока файл не прочитается. Просто, но медленно.
- Асинхронные методы с колбэками — ты говоришь: «Прочитай файл и, когда закончишь, сделай вот это». Старый стиль.
- Асинхронные методы с промисами — современный подход. Ты используешь async/await, и код выглядит почти как синхронный, но не блокирует программу.
Синтаксис в JavaScript (Node.js): все формы и варианты
Для работы с файлами в Node.js используется встроенный модуль fs (file system). Чтобы его подключить, нужно написать в начале программы:
const fs = require('fs'); // Старый способ (CommonJS)
// или
import fs from 'fs'; // Новый способ (ES Modules) — если в package.json стоит "type": "module"
Давай рассмотрим все основные методы для чтения файлов. Я сгруппировал их в таблицу для наглядности.
Таблица методов чтения файлов
| Метод | Тип | Что делает | Когда использовать |
|---|---|---|---|
fs.readFileSync() |
Синхронный | Читает весь файл в память сразу | Для маленьких файлов в простых скриптах |
fs.readFile() |
Асинхронный (колбэк) | Читает весь файл, вызывает функцию по готовности | Когда нужно прочитать файл без остановки программы |
fs.promises.readFile() |
Асинхронный (промис) | То же самое, но возвращает Promise | Современный стандарт, удобно с async/await |
fs.createReadStream() |
Потоковый | Читает файл по кусочкам (чанкам) | Для больших файлов (видео, логи, базы данных) |
fs.openSync() + fs.readSync() |
Низкоуровневый | Открывает файл и читает определённое количество байт | Для специфических задач (чтение с определённой позиции) |
Параметры методов
Большинство методов принимают одинаковые параметры:
- path — путь к файлу (строка или URL)
- options — объект или строка с настройками:
encoding— кодировка ('utf-8', 'base64', 'hex' и др.)flag— режим открытия файла ('r' — чтение, 'w' — запись, 'a' — добавление)highWaterMark— размер кусочка в байтах для потоков (по умолчанию 64 КБ)
🔑 Секрет профессионалов: Если не указать encoding, Node.js вернёт Buffer (сырые байты). Чтобы получить строку, всегда пиши{ encoding: 'utf-8' }или просто'utf-8'вторым аргументом.
Примеры кода — от простого к сложному
Теперь перейдём к самому интересному — практике. Все примеры рабочие, ты можешь скопировать их и запустить на своём компьютере. Создай файл test.txt с любым текстом и экспериментируй.
Пример 1: Чтение файла синхронно (самый простой способ)
// Подключаем модуль для работы с файлами
const fs = require('fs');
// Путь к файлу (можешь изменить на свой)
const filePath = './test.txt';
// Читаем файл синхронно — программа подождёт, пока файл не прочитается
// Второй аргумент 'utf-8' говорит Node.js вернуть строку, а не байты
const content = fs.readFileSync(filePath, 'utf-8');
// Выводим содержимое в консоль
console.log('Содержимое файла:');
console.log(content);
// Этот код выполнится только после того, как файл будет прочитан
console.log('Файл прочитан!');
Объяснение: Синхронное чтение — это как открыть дверь и ждать, пока она откроется полностью, прежде чем войти. Просто и понятно, но если файл большой (100 МБ), программа «зависнет» на время чтения.
Пример 2: Асинхронное чтение с колбэком (классический стиль)
const fs = require('fs');
console.log('Начинаем чтение файла...');
// Асинхронное чтение — программа не ждёт, а продолжает работать
// Третий аргумент — функция, которая вызовется, когда файл будет прочитан
fs.readFile('./test.txt', 'utf-8', (error, data) => {
// Если произошла ошибка (файл не найден, нет прав доступа)
if (error) {
console.error('Ошибка при чтении:', error.message);
return; // Выходим из функции, чтобы не обрабатывать пустые данные
}
// Если ошибки нет — выводим содержимое
console.log('Содержимое файла:');
console.log(data);
});
// Эта строка выполнится ДО того, как файл будет прочитан!
console.log('Программа продолжает работать, пока файл читается...');
Объяснение: Обрати внимание на порядок вывода. Сначала увидишь «Начинаем чтение...» и «Программа продолжает...», а потом уже содержимое файла. Это и есть асинхронность — Node.js не ждёт, а занимается другими делами.
Пример 3: Асинхронное чтение с промисами (современный стиль)
const fs = require('fs').promises; // Обрати внимание — подключаем promises!
// Создаём асинхронную функцию, чтобы использовать await
async function readFileAsync() {
try {
console.log('Читаем файл...');
// await заставляет программу ждать, но не блокирует весь процесс
const data = await fs.readFile('./test.txt', 'utf-8');
console.log('Содержимое файла:');
console.log(data);
} catch (error) {
// Если произошла ошибка — ловим её здесь
console.error('Ошибка:', error.message);
}
}
// Вызываем функцию
readFileAsync();
console.log('Программа не заблокирована!');
Объяснение: Это мой любимый способ. Код выглядит как синхронный (строчки идут по порядку), но на самом деле он асинхронный. await — это как сказать: «Подожди здесь, но не останавливай всю программу». Идеально для новичков и профессионалов.
Пример 4: Чтение файла построчно (для логов и списков)
const fs = require('fs');
const readline = require('readline'); // Модуль для чтения по строкам
async function readFileByLines() {
// Создаём интерфейс для чтения
// input — это поток для чтения файла
const rl = readline.createInterface({
input: fs.createReadStream('./test.txt'),
output: process.stdout, // Можно не указывать
terminal: false // Отключаем терминальный режим
});
let lineNumber = 0;
// Событие 'line' срабатывает для каждой строки
rl.on('line', (line) => {
lineNumber++;
console.log(`Строка ${lineNumber}: ${line}`);
});
// Событие 'close' — когда файл закончился
rl.on('close', () => {
console.log(`
Всего строк: ${lineNumber}`);
});
}
readFileByLines();
Объяснение: Этот способ идеален для больших текстовых файлов (логи сервера, списки пользователей). Ты не загружаешь весь файл в память, а обрабатываешь строку за строкой. Экономия памяти — колоссальная!
Пример 5: Чтение через поток (для очень больших файлов)
const fs = require('fs');
// Создаём поток для чтения
// highWaterMark — размер кусочка в байтах (16 КБ в этом примере)
const readStream = fs.createReadStream('./test.txt', {
encoding: 'utf-8',
highWaterMark: 1024 * 16 // 16 КБ
});
let totalSize = 0;
let chunkCount = 0;
// Событие 'data' — когда пришёл новый кусочек данных
readStream.on('data', (chunk) => {
chunkCount++;
totalSize += chunk.length;
console.log(`Получен кусочек №${chunkCount}, размер: ${chunk.length} байт`);
console.log('Содержимое кусочка:');
console.log(chunk);
console.log('---');
});
// Событие 'end' — файл полностью прочитан
readStream.on('end', () => {
console.log(`
Файл прочитан полностью.`);
console.log(`Всего кусочков: ${chunkCount}`);
console.log(`Общий размер: ${totalSize} байт`);
});
// Событие 'error' — если что-то пошло не так
readStream.on('error', (error) => {
console.error('Ошибка потока:', error.message);
});
Объяснение: Поток — это как водопроводная труба. Вода (данные) течёт непрерывно, а ты можешь открыть кран и набрать стакан (кусочек). Для видеофайлов размером в гигабайты это единственный разумный способ.
Пример 6: Чтение JSON-файла (самое частое применение)
const fs = require('fs').promises;
async function readJSON() {
try {
// Допустим, у нас есть файл config.json с настройками
const rawData = await fs.readFile('./config.json', 'utf-8');
// Превращаем строку JSON в JavaScript-объект
const config = JSON.parse(rawData);
console.log('Настройки приложения:');
console.log(`Название: ${config.appName}`);
console.log(`Версия: ${config.version}`);
console.log(`Порт сервера: ${config.port}`);
// Можно работать с объектом как обычно
if (config.debug) {
console.log('Режим отладки включён');
}
} catch (error) {
if (error.code === 'ENOENT') {
console.error('Файл config.json не найден!');
} else if (error instanceof SyntaxError) {
console.error('Ошибка в формате JSON:', error.message);
} else {
console.error('Неизвестная ошибка:', error);
}
}
}
readJSON();
Объяснение: JSON — это формат обмена данными, который понимают все языки программирования. Конфиги, базы данных, API — везде используется JSON. Научиться читать его так же важно, как уметь читать обычный текст.
Пример 7: Чтение с проверкой существования файла
const fs = require('fs').promises;
async function safeReadFile(filePath) {
try {
// Сначала проверяем, существует ли файл
await fs.access(filePath, fs.constants.F_OK);
// F_OK — проверка на существование
// R_OK — проверка на чтение
// W_OK — проверка на запись
console.log('Файл существует! Читаем...');
const data = await fs.readFile(filePath, 'utf-8');
return data;
} catch (error) {
if (error.code === 'ENOENT') {
console.error(`Файл ${filePath} не найден. Создаём новый...`);
// Можно создать файл с содержимым по умолчанию
await fs.writeFile(filePath, '{}', 'utf-8');
return '{}'; // Возвращаем пустой JSON
} else {
throw error; // Пробрасываем другие ошибки дальше
}
}
}
// Использование
async function main() {
const content = await safeReadFile('./data.json');
console.log('Содержимое:', content);
}
main();
Объяснение: В реальных проектах файлы могут отсутствовать, быть повреждёнными или заблокированными. Всегда проверяй, что файл существует и доступен для чтения. Этот пример показывает профессиональный подход к обработке ошибок.
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет проще. Давай сравним, как читаются файлы в двух языках.
| Операция | JavaScript (Node.js) | Python |
|---|---|---|
| Открытие файла | fs.readFile() |
open() |
| Чтение всего файла | fs.readFileSync(path, 'utf-8') |
file.read() |
| Чтение по строкам | readline.createInterface() |
for line in file: |
| Чтение через поток | fs.createReadStream() |
Нет встроенного (нужна библиотека) |
| Кодировка по умолчанию | Buffer (байты) | Зависит от системы (обычно UTF-8) |
| Асинхронность | Встроенная, основной способ | Через asyncio (дополнительно) |
| Обработка ошибок | try/catch или колбэки | try/except |
Главные отличия:
- Асинхронность по умолчанию: В Node.js асинхронное чтение — это стандарт. В Python синхронное чтение — это норма, а асинхронное нужно подключать отдельно.
- Потоки: В Node.js потоки встроены в ядро и работают «из коробки». В Python для потокового чтения больших файлов нужно использовать специальные библиотеки.
- Кодировки: В Python open() по умолчанию возвращает строку. В Node.js нужно явно указывать 'utf-8', иначе получишь Buffer.
- Менеджер контекста: В Python есть
with open() as file:, который автоматически закрывает файл. В Node.js файлы закрываются автоматически после чтения, но для потоков нужно явно вызывать.close().
🎯 Совет для изучающих оба языка: Если ты привык к Python, в Node.js тебе может показаться неудобным, что нужно постоянно писать колбэки или await. Но это даёт суперсилу — твоя программа может одновременно читать 1000 файлов, не замедляясь!
Типичные ошибки и подводные камни
Даже опытные программисты иногда спотыкаются на ровном месте. Вот список самых частых ошибок при чтении файлов в Node.js.
1. Забыл указать кодировку
// ❌ Неправильно — получишь Buffer вместо строки
const data = fs.readFileSync('./test.txt');
console.log(data); //
// ✅ Правильно
const data = fs.readFileSync('./test.txt', 'utf-8');
console.log(data); // Hello
2. Не обрабатываешь ошибки
// ❌ Опасно — если файла нет, программа упадёт
const data = fs.readFileSync('./missing.txt', 'utf-8');
// ✅ Безопасно
try {
const data = fs.readFileSync('./missing.txt', 'utf-8');
} catch (error) {
console.error('Файл не найден:', error.message);
}
3. Путаешь синхронный и асинхронный код
// ❌ Ошибка — data будет undefined, потому что файл ещё не прочитан
let data;
fs.readFile('./test.txt', 'utf-8', (err, content) => {
data = content;
});
console.log(data); // undefined
// ✅ Правильно — используй промисы или колбэки
fs.readFile('./test.txt', 'utf-8', (err, content) => {
console.log(content); // Работает!
});
4. Проблемы с путями
// ❌ Относительный путь зависит от того, откуда запущена программа
const data = fs.readFileSync('data.txt', 'utf-8'); // Может не сработать
// ✅ Используй абсолютный путь или __dirname
const path = require('path');
const filePath = path.join(__dirname, 'data.txt');
const data = fs.readFileSync(filePath, 'utf-8');
5. Чтение слишком большого файла целиком
// ❌ Плохо — файл в 1 ГБ убьёт память
const data = fs.readFileSync('./bigvideo.mp4');
// ✅ Хорошо — используй поток
const stream = fs.createReadStream('./bigvideo.mp4');
stream.on('data', (chunk) => {
// Обрабатывай по кусочкам
});
6. Игнорирование кодировки файла
// ❌ Если файл в Windows-1251, получишь кракозябры
const data = fs.readFileSync('./russian.txt', 'utf-8');
// ✅ Используй библиотеку iconv-lite для перекодировки
const iconv = require('iconv-lite');
const buffer = fs.readFileSync('./russian.txt');
const data = iconv.decode(buffer, 'win1251');
7. Забываешь закрыть поток
// ❌ Поток останется открытым, пока программа не завершится
const stream = fs.createReadStream('./test.txt');
stream.on('data', (chunk) => {
// Обработка
});
// Поток нужно закрыть, если решил прервать чтение
stream.destroy();
Лучшие практики и советы профессионалов
За годы работы с Node.js я выработал несколько правил, которые помогают писать надёжный и быстрый код. Делюсь ими с тобой.
1. Всегда используй async/await
Забудь про колбэки, если только ты не поддерживаешь древний код. Промисы с async/await делают код читаемым и понятным. Если ты используешь Node.js версии 14 и выше, у тебя есть доступ к fs.promises.
2. Обрабатывай ошибки централизованно
Создай функцию-обёртку для чтения файлов, которая будет обрабатывать все типы ошибок:
const fs = require('fs').promises;
async function readFileSafe(path) {
try {
return await fs.readFile(path, 'utf-8');
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Файл ${path} не найден`);
} else if (error.code === 'EACCES') {
throw new Error(`Нет прав на чтение ${path}`);
} else {
throw error;
}
}
}
3. Используй path.join для путей
Никогда не склеивай пути через + или шаблонные строки. На разных ОС разные разделители (Windows — , Linux — /). Модуль path решит эту проблему:
const path = require('path');
const filePath = path.join(__dirname, 'data', 'config.json');
// На Windows: C:Users...dataconfig.json
// На Linux: /home/user/.../data/config.json
4. Для больших файлов — только потоки
Если файл больше 50 МБ, не читай его целиком. Используй createReadStream или readline. Твой сервер скажет тебе спасибо.
5. Проверяй существование файла перед чтением
Используй fs.access() или просто обрабатывай ошибку ENOENT. Не используй устаревший fs.exists() — он считается deprecated.
6. Не доверяй пользовательским путям
Если путь к файлу приходит от пользователя (например, из веб-формы), всегда проверяй его. Иначе злоумышленник может прочитать любой файл на сервере:
// ❌ Опасно — можно прочитать /etc/passwd
const data = await fs.readFile(userInput, 'utf-8');
// ✅ Безопасно — разрешаем только определённую папку
const allowedDir = path.join(__dirname, 'public');
const requestedPath = path.join(allowedDir, userInput);
if (!requestedPath.startsWith(allowedDir)) {
throw new Error('Доступ запрещён');
}
7. Используй fs.constants для проверки прав
const fs = require('fs').promises;
async function checkAccess(path) {
try {
await fs.access(path, fs.constants.R_OK);
console.log('Файл доступен для чтения');
} catch {
console.log('Нет доступа к файлу');
}
}
Вопросы для самопроверки
Проверь, как ты усвоил материал. Постарайся ответить на вопросы, не подглядывая в ответы.
- Вопрос: Какой модуль нужно подключить для работы с файлами в Node.js?
Ответ: Модульfs(file system). Подключается черезrequire('fs')илиimport fs from 'fs'. - Вопрос: В чём разница между
readFileSyncиreadFile?
Ответ:readFileSync— синхронный, блокирует выполнение программы до завершения чтения.readFile— асинхронный, программа продолжает работать, а результат приходит через колбэк или промис. - Вопрос: Что произойдёт, если не указать кодировку 'utf-8' при чтении текстового файла?
Ответ: Node.js вернёт объект Buffer (сырые байты), а не строку. Чтобы получить текст, нужно явно указать кодировку. - Вопрос: Какой способ чтения файла лучше использовать для файла размером 500 МБ?
Ответ: Потоковый —fs.createReadStream(). Чтение всего файла целиком загрузит 500 МБ в оперативную память, что может привести к её нехватке. - Вопрос: Как прочитать JSON-файл и превратить его в объект JavaScript?
Ответ: Сначала прочитать файл как строку (fs.readFile), затем использоватьJSON.parse()для преобразования строки в объект. - Вопрос: Что такое ENOENT и как его обрабатывать?
Ответ: ENOENT — ошибка, которая возникает, когда файл или директория не найдены. Обрабатывается в блоке catch или в колбэке ошибки. - Вопрос: Чем отличается чтение через readline от чтения через поток?
Ответ: readline читает файл по строкам и удобен для текстовых файлов (логи, CSV). Поток (createReadStream) читает по кусочкам произвольного размера и подходит для любых типов файлов