Как использовать файловую систему в 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
— по умолчанию это массив строкutf8
recursive
— установите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
предоставляет методы для создания, чтения и работы с каталогами, включая создание каталогов, просмотр их содержимого и удаление каталогов.