Как использовать файловую систему в Node.js
Чтение и запись файлов из кода необязательно сложны, но ваше приложение будет более надёжным, если вы сделаете следующее:
Убедитесь в кроссплатформенности
Windows, macOS и Linux работают с файлами по-разному. Например, для разделения каталогов в macOS и Linux используется прямой слэш
/, а в Windows — обратный слэш\и запрещены некоторые символы имён файлов, такие как:и?.Проверите все дважды!
Пользователи или другие приложения могут удалить файл или изменить разрешения доступа. Всегда проверяйте наличие таких проблем, и эффективно устраняйте ошибки.
Модуль Node.js fs
Модуль Node.js fs предоставляет методы для управления файлами и каталогами. Если вы используете другие среды исполнения JavaScript:
- Deno предоставляет собственные API файловой системы, а также поддерживает API node:fs.
- Bun предоставляет оптимизированные API файлового ввода/вывода, а также API node:fs.
- Браузеры работают в "песочнице" и не могут напрямую взаимодействовать с ОС или базовой файловой системой. Тем не менее вы можете загружать файлы и разрешать ограниченный доступ к ним через API файловой системы. Это концептуально отличается и выходит за рамки данного руководства.
Все программы JavaScript выполняются в одном потоке обработки. Базовая операционная система обрабатывает такие операции, как чтение и запись файлов, поэтому программа JavaScript продолжает выполняться параллельно. ОС оповещает среду выполнения о завершении операции с файлом.
В документации по fs приводится длинный список функций, но есть три общих типа с похожими функциями, которые мы рассмотрим далее.
1. Функции обратного вызова
Эти функции принимают в качестве аргумента функцию обратного вызова. В следующем примере передаётся встроенная функция, которая выводит содержимое файла myfile.txt. При условии отсутствия ошибок его содержимое отображается в консоли после end of program:
import { readFile } from 'node:fs';
readFile('myfile.txt', { encoding: 'utf8' }, (err, content) => {
if (!err) {
console.log(content);
}
});
console.log('end of program');Примечание: параметр { encoding: 'utf8' } гаран_тирует, что Node.js вернёт строку текстового содержимого, а не объект Buffer с двоичными данными.
Это становится сложным, когда вам нужно запускать одну функцию за другой и попадать в ад вложенных обратных вызовов! Также легко написать функции обратного вызова, выглядящие корректно, но вызывающие утечки памяти, которые трудно отладить.
В большинстве случаев использование обратных вызовов не имеет смысла. Лишь немногие из приведённых ниже примеров используют их.
2. Синхронные функции
Функции "Sync" эффективно игнорируют неблокирующий ввод/вывод Node и предоставляют синхронные API, как в других языках программирования. Следующий пример выводит содержимое файла myfile.txt до того, как в консоли появится сообщение end of program:
import { readFileSync } from 'node:fs';
try {
const content = readFileSync('myfile.txt', { encoding: 'utf8' });
console.log(content);
}
catch {}
console.log('end of program');Это выглядит проще, и я никогда бы не сказал, что не стоит использовать Sync…. но, эм… не используйте Sync! Она останавливает цикл событий и приостанавливает работу приложения. Это может быть нормально в CLI-программе при загрузке небольшого файла инициализации, но подумайте о веб-приложении Node.js со 100 одновременными пользователями. Если один пользователь запросит файл, загрузка которого займёт одну секунду, он будет ждать ответа одну секунду — как и все остальные 99 пользователей!
Нет причин использовать синхронные методы, когда у нас есть promise функции.
3. Promise функции
В ES6/2015 были представлены promise. Они представляют собой синтаксический сахар для обратных вызовов, обеспечивающий более сладкий и простой синтаксис, особенно при использовании с async/await. В Node.js также появился API fs/promises, который выглядит и ведёт себя аналогично синтаксису синхронных функций, но остаётся асинхронным:
import { readFile } from 'node:fs/promises';
try {
const content = await readFile('myfile.txt', { encoding: 'utf8' });
console.log(content);
}
catch {}
console.log('end of program');Обратите внимание на использование модуля node:fs/promises и await перед readFile().
В большинстве примеров ниже используется синтаксис, основанный на промисах. Большинство из них не включают try и catch для краткости, но вы должны добавить эти блоки для обработки ошибок.
Синтаксис модуля ES
В примерах этого руководства также используется import ES Modules (ESM), а не CommonJS require. ESM — это стандартный синтаксис модулей, поддерживаемый в Deno, Bun и браузерных runtimes.
Чтобы использовать ESM в Node.js, либо:
- называйте свои файлы JavaScript с расширением
.mjs - используйте параметр
--import=moduleв командной строке — например,node --import=module index.js, или - если у вас есть файл проекта
package.json, добавьте новый параметр"type": "module".
Вы все ещё можете использовать CommonJS require, если это необходимо.
Чтение файлов
Существует несколько функций для чтения файлов, но самая простая — это чтение всего файла в память с помощью readFile, как мы видели в примере выше:
import { readFile } from 'node:fs/promises';
const content = await readFile('myfile.txt', { encoding: 'utf8' });Второй объект options также может быть строкой. Он задаёт кодировку (encoding): установите utf8 или другой текстовый формат для чтения содержимого файла в строку.
В качестве альтернативы можно читать строки по одной, используя метод readLines() объекта filehandle:
import { open } from 'node:fs/promises';
const file = await open('myfile.txt');
for await (const line of file.readLines()) {
console.log(line);
}Есть также более продвинутые опции для чтения потоков или любого количества байт из файла.
Работа с файлами и путями каталогов
Вы часто хотите получить доступ к файлам по определённым абсолютным путям или путям относительно рабочей директории Node приложения. Модуль node:path предоставляет кроссплатформенные методы для разрешения путей во всех операционных системах.
Свойство path.sep возвращает символ разделителя каталогов — \ в Windows или / в Linux или macOS:
import * as path from 'node:path';
console.log( path.sep );Но есть и более полезные свойства и функции. join([...paths]) объединяет все сегменты пути и нормализует для ОС:
console.log( path.join('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt на macOS/Linux
\project\node\example2\myfile.txt на Windows
*/resolve([...paths]) аналогичен, но возвращает полный абсолютный путь:
console.log( path.resolve('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt на macOS/Linux
C:\project\node\example2\myfile.txt на Windows
*/normalize(path) разрешает все каталоги .. и . ссылок:
console.log( path.normalize('/project/node/example1/../example2/myfile.txt') );
/*
/project/node/example2/myfile.txt на macOS/Linux
\project\node\example2\myfile.txt на Windows
*/relative(from, to) вычисляет относительный путь между двумя абсолютными или относительными путями (на основе рабочего каталога Node):
console.log( path.relative('/project/node/example1', '/project/node/example2') );
/*
../example2 на macOS/Linux
..\example2 на Windows
*/'format(object)' строит полный путь из объекта, состоящего из составных частей:
console.log(
path.format({
dir: '/project/node/example2',
name: 'myfile',
ext: 'txt'
})
);
/*
/project/node/example2/myfile.txt
*/parse(path) делает обратное и возвращает объект, описывающий путь:
console.log( path.parse('/project/node/example2/myfile.txt') );
/*
{
root: '/',
dir: '/project/node/example2',
base: 'myfile.txt',
ext: '.txt',
name: 'myfile'
}
*/Получение информации о файле и директории
Часто требуется получить информацию о пути. Является ли он файлом? Является ли он каталогом? Когда он был создан? Когда он был последний раз изменён? Можете ли вы прочитать его? Можете ли вы добавлять в него данные?
Функция stat(path) возвращает объект Stats, содержащий информацию об объекте файла или каталога:
import { stat } from 'node:fs/promises';
const info = await stat('myfile.txt');
console.log(info);
/*
Stats {
dev: 4238105234,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
ino: 3377699720670299,
size: 21,
blocks: 0,
atimeMs: 1700836734386.4246,
mtimeMs: 1700836709109.3108,
ctimeMs: 1700836709109.3108,
birthtimeMs: 1700836699277.3362,
atime: 2023-11-24T14:38:54.386Z,
mtime: 2023-11-24T14:38:29.109Z,
ctime: 2023-11-24T14:38:29.109Z,
birthtime: 2023-11-24T14:38:19.277Z
}
*/Кроме того, в нем представлены полезные методы, в том числе:
const isFile = info.isFile(); // true
const isDirectory = info.isDirectory(); // falseФункция access(path) проверяет, можно ли получить доступ к файлу, используя определённый режим, заданный через constants. Если проверка доступности прошла успешно, промис выполняется без значения. При неудаче обещание отклоняется. Например:
import { access, constants } from 'node:fs/promises';
const info = {
canRead: false,
canWrite: false,
canExec: false
};
// is readable?
try {
await access('myfile.txt', constants.R_OK);
info.canRead = true;
}
catch {}
// is writeable
try {
await access('myfile.txt', constants.W_OK);
info.canWrite = true;
}
catch {}
console.log(info);
/*
{
canRead: true,
canWrite: true
}
*/Вы можете проверить более одного режима, например, проверить, доступен ли файл для чтения и записи:
await access('myfile.txt', constants.R_OK | constants.W_OK);Запись файлов
writeFile() — простейшая функция для асинхронной записи целого файла с заменой его содержимого, если он уже существует:
import { writeFile } from 'node:fs/promises';
await writeFile('myfile.txt', 'new file contents');Передайте следующие аргументы:
- путь к файлу
- содержимое файла — может быть
String,Buffer,TypedArray,DataView,IterableилиStream - опциональный третий аргумент может быть строкой, представляющей кодировку (например,
utf8), или объектом со свойствами, такими какencodingиsignal, чтобы прервать выполнение promise.
Аналогичная функция appendFile() добавляет новое содержимое в конец текущего файла, создавая его, если он не существует.
Для самых смелых есть метод filehandler.write(), позволяющий заменить содержимое файла в определённой точке и с определённой длиной.
Создание директорий
Функция mkdir() может создавать полные структуры каталогов, получая абсолютный или относительный путь:
import { mkdir } from 'node:fs/promises';
await mkdir('./subdir/temp', { recursive: true });Вы можете передать два аргумента:
- путь к каталогу, и
- необязательный объект с рекурсивным
Booleanзначением иmodeстрокой или целым числом.
Установка значения recursive в true создаёт всю структуру каталогов. В приведённом примере subdir создаётся в текущем рабочем каталоге, а temp — как подкаталог этого каталога. Если бы значение recursive было false (по умолчанию), promise было бы отклонён, если бы subdir не был уже определён.
mode — это разрешение для пользователей, групп и остальных в macOS/Linux со значением по умолчанию 0x777. В Windows это не поддерживается и игнорируется.
Аналогичная функция .mkdtemp() создаёт уникальный каталог, обычно предназначенный для временного хранения данных.
Чтение содержимого каталога
.readdir() считывает содержимое каталога. Promise выполняется с массивом, содержащим все имена файлов и каталогов (кроме . и ..). Имя указывается относительно каталога и не включает полный путь:
import { readdir } from 'node:fs/promises';
const files = await readdir('./'); // current working directory
for (const file of files) {
console.log(file);
}
/*
file1.txt
file2.txt
file3.txt
index.mjs
*/Вы можете передать опциональный второй параметр-объект со следующими свойствами:
encoding— по умолчанию это массив строкutf8recursive— установитеtrueдля рекурсивной выборки всех файлов из всех подкаталогов. Имя файла будет включать имя(а) подкаталога(ов). Старые версии Node.js могут не предоставлять эту опцию.withFileType— установитеtrue, чтобы вернуть массив объектовfs.Dirent, включающий свойства и методы, такие как.name,.path,.isFile(), .isDirectory()и другие.
Альтернативная функция .opendir() позволяет асинхронно открыть каталог для итеративного сканирования:
import { opendir } from 'node:fs/promises';
const dir = await opendir('./');
for await (const entry of dir) {
console.log(entry.name);
}Удаление файлов и директорий
Функция .rm() удаляет файл или директорию по указанному пути:
import { rm } from 'node:fs/promises';
await rm('./oldfile.txt');Вы можете передать в качестве необязательного второго параметра объект со следующими свойствами:
force— установитеtrue, чтобы не выдавать ошибку, если путь не существует,recursive— установитеtrue, для рекурсивного удаления каталога и его содержимого,maxRetries— количество повторных попыток, если другой процесс заблокировал файл,retryDelay— количество миллисекунд между повторными попытками.
Похожая функция .rmdir() удаляет только каталоги (нельзя передавать путь к файлу). Подобным образом .unlink() удаляет только файлы или символические ссылки (нельзя передавать путь к каталогу).
Прочие функции файловой системы
Приведённые выше примеры иллюстрируют наиболее часто используемые опции для чтения, записи, обновления и удаления файлов и каталогов. Node.js также предоставляет дополнительные, менее используемые опции, такие как копирование, переименование, изменение владельца, изменение разрешений, изменение свойств даты, создание символических ссылок и наблюдение за изменениями файлов.
Возможно, при отслеживании изменений файлов предпочтительнее использовать API на основе обратных вызовов, поскольку в нем меньше кода, он проще в использовании и не может останавливать другие процессы:
import { watch } from 'node:fs';
// запуск обратного вызова, когда в каталоге что-то изменяется
watch('./mydir', { recursive: true }, (event, file) => {
console.log(`event: ${ event }`);
if (file) {
console.log(`file changed: ${ file }`);
}
});
// сделать что-то еще...Параметр event, получаемый обратным вызовом, — это либо change, либо rename.
Подведение итогов
Node.js предоставляет гибкий и кроссплатформенный API для управления файлами и каталогами в любой операционной системе, где можно использовать среду выполнения. Немного усилий, и вы сможете писать надёжный и переносимый JavaScript-код, способный взаимодействовать с любой файловой системой.
Для получения дополнительной информации обратитесь к документации по Node.js fs и path. Другие полезные библиотеки включают:
OSдля запроса информации об операционной системеURLдля разбора URL, возможно, при сопоставлении путей к файловой системе и от неё.Streamдля работы с большими файлами- Объекты
BufferиTypedArrayдля работы с двоичными данными - Дочерние процессы для порождения подпроцессов для обработки длительных или сложных функций работы с файлами.
Вы также можете найти модули файловой системы более высокого уровня на npm, но нет лучшего опыта, чем написать свой собственный модуль.
Часто задаваемые вопросы о доступе к файловой системе в Node.js
Что такое модуль файловой системы в Node.js
Модуль файловой системы, часто называемый 'fs' — это основной модуль в Node.js, предоставляющий методы и функциональность для взаимодействия с файловой системой, включая чтение и запись файлов.
Как включить модуль fs в сценарий Node.js
Вы можете включить модуль fs с помощью выражения require, например, так: const fs = require('fs');. Это сделает все методы fs доступными в вашем скрипте.
В чем разница между синхронными и асинхронными файловыми операциями в Node.js
Синхронные файловые операции блокируют цикл событий Node.js до завершения операции, в то время как асинхронные операции не блокируют цикл событий, позволяя вашему приложению оставаться отзывчивым. Асинхронные операции обычно рекомендуются для задач ввода/вывода.
Как прочитать содержимое файла в Node.js с помощью модуля fs
Вы можете использовать метод fs.readFile() для чтения содержимого файла. Укажите путь к файлу и функцию обратного вызова для обработки данных после их считывания.
Каково назначение функции обратного вызова при работе с модулем fs в Node.js
Функции обратного вызова в операциях fs используются для обработки результатов асинхронных файловых операций. Они вызываются по завершении операции, передавая в качестве аргументов любые ошибки и данные.
Как проверить, существует ли файл в Node.js, используя модуль fs
Вы можете использовать метод fs.existsSync(), чтобы проверить, существует ли файл по указанному пути. Он возвращает true, если файл существует, и false, если не существует.
Что такое метод fs.createReadStream() и чем он полезен
fs.createReadStream() используется для эффективного чтения больших файлов. Она создаёт читаемый поток для указанного файла, позволяя читать и обрабатывать данные небольшими, управляемыми кусками.
Можно ли использовать модуль fs для создания и записи в новый файл в Node.js
Да, вы можете использовать методы fs.writeFile() или fs.createWriteStream() для создания и записи в новый файл. Эти методы позволяют указать путь к файлу, содержимое и параметры записи.
Как обрабатывать ошибки при работе с модулем fs в Node.js
Вы всегда должны обрабатывать ошибки, проверяя параметр ошибки в функции обратного вызова, предоставляемой асинхронным методам fs, или используя блоки try/catch для синхронных операций.
Можно ли удалить файл с помощью модуля fs в Node.js
Да, вы можете использовать метод fs.unlink() для удаления файла. Укажите путь к файлу и функцию обратного вызова для обработки результата.
Можно ли использовать модуль fs для работы с каталогами и структурами папок в Node.js
Да, модуль fs предоставляет методы для создания, чтения и работы с каталогами, включая создание каталогов, просмотр их содержимого и удаление каталогов.