fs.watch: наблюдение за ФС и live reload
fs.watchподписывает listener на изменения файла или каталога. Используется для кэширования, hot reload модулей, live deploy без остановки сервиса.
Что это
Не callback, а listener — навешивается один раз, срабатывает многократно при каждом изменении. Знает только два типа событий: 'rename' и 'change'. Об удалении напрямую не сообщает — приходит как 'rename', нужно дополнительно проверять fs.stat/fs.access.
API
const fs = require('node:fs');
const watcher = fs.watch('config.json', (event, filename) => {
// event: 'rename' | 'change'
console.log(event, filename);
});
// Каталог (recursive на macOS/Windows)
fs.watch('src', { recursive: true }, (event, filename) => { /* ... */ });
watcher.close();
// fs/promises вариант (Node 19+)
const { watch } = require('node:fs/promises');
for await (const e of watch('src', { recursive: true })) {
console.log(e.eventType, e.filename);
}
Подводные камни
- Дублирование событий: текстовые редакторы сохраняют atomic-save (пишут во временный файл →
rename), приходит 2-3 события вместо одного. Решение: throttle/debounce обёртка - Удаление файла: приходит как
'rename'. Чтобы отличить — после события вызватьfs.accessи проверитьENOENT - watch не рекурсивен на Linux по умолчанию — нужно ставить watcher на каждый подкаталог вручную или библиотеку
chokidar - Дескрипторы: каждый
watchдержит файловый дескриптор, при много-watch это ограничение ОС
Live reload через watch + require.cache
Паттерн: меняем серверный код на лету, сервис не перезапускается.
function reloadModule(modulePath) {
// 1. Получить абсолютный путь — это ключ в require.cache
const fullPath = require.resolve(modulePath);
// 2. Удалить из кэша
delete require.cache[fullPath];
// 3. require снова — подгрузится свежая версия
try {
return require(fullPath);
} catch (err) {
// syntax error в новом файле — не уронить процесс
console.error('Reload failed:', err);
}
}
fs.watch('./lib', { recursive: true }, (event, filename) => {
const mod = reloadModule(`./lib/${filename}`);
if (mod) routes = mod.routes; // подменили коллекцию роутов разом
});
Опасности live reload в production
- Файлы приходят не разом: при деплое через rsync/git/npm файлы появляются по одному. Если подгружать каждое изменение — поймаешь промежуточное состояние
- Решение: накапливать изменения в буфере, применять разом по команде (атомарное переключение версии)
- Сосуществование версий в памяти: старая лямбда не вызвала callback → ещё живёт; новая уже загружена. Обе обращаются к общим структурам — могут конфликтовать
require.cacheдля ESM не работает: сimport/exportнет прямого аналога, нужны loaders
🎓 Источники
-
🎓 Наблюдение за файловой системой в Node.js · 2018-11-28
- Тезисы:
watch— это listener, не callback: ставится один раз, срабатывает многократно- Редактор даёт 2 события на одно сохранение (atomic save через rename во временный файл)
- watch знает только
renameиchange, удаление приходит как rename - Кеш файлов в
Map: ключ — относительный путь, значение — Buffer; cacheFolder через readdir + cacheFile - Можно держать в кэше уже сжатые данные (gzip заранее) и писать буфер прямо в сокет
- Live reload без остановки сервиса: для деплоя нужно атомарное переключение, иначе ловишь промежуточное состояние
- Цитата:
«Если будет live reload, то мониторинг ФС при деплое опасен. Кто-то деплоит rsync, FTP, git clone — не все файлы за один раз приходят на сервер.»
- Тезисы:
-
🎓 Архив 2018 — Часть 10 Наблюдение за файловой системой и динамическая подгрузка · 2020-01-08
- Тезисы:
require.resolve(path)даёт абсолютный путь — это ключ вrequire.cache. Без удаления второй require берёт из кэшаrequire.cache[fullPath]хранит структуру модуля:loaded,children,parent,exportsrequire.extensions['.js'/'.node']— обработчики расширений (.nodeдля нативных плагинов)- Альтернатива require —
vm.createScript+vm.createContext: безопасная замена eval с песочницей - Каждый файл Node оборачивается в функцию, аргументами идут
module, exports, require, поэтомуrequire— не глобальный
- Цитата:
«У require есть resolve, которая нам из имени файла дает по какому он ключу лежит в кэше Require. Мы его можем из кэша Require удалить.»
- Тезисы: