Node.js имеет свою специфику, и некоторые подходы, которые могут работать в других средах, здесь становятся антипаттернами. Рассмотрим наиболее распространенные.
// Плохой пример
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) throw err;
fs.writeFile('result.txt', data1 + data2, (err) => {
if (err) throw err;
console.log('Done!');
});
});
});
Решение: Использовать Promises/async-await или библиотеки типа async.js
// Хороший пример
async function processFiles() {
try {
const [data1, data2] = await Promise.all([
fs.promises.readFile('file1.txt', 'utf8'),
fs.promises.readFile('file2.txt', 'utf8')
]);
await fs.promises.writeFile('result.txt', data1 + data2);
console.log('Done!');
} catch (err) {
console.error(err);
}
}
// Плохой пример - синхронные операции в обработчике запроса
app.get('/data', (req, res) => {
const data = fs.readFileSync('huge-file.json'); // Блокирует event loop
res.send(data);
});
Решение: Всегда использовать асинхронные API
// Плохой пример 1 - игнорирование ошибок
fs.readFile('file.txt', (err, data) => {
console.log(data); // Что если err?
});
// Плохой пример 2 - неправильное использование rejections
new Promise((resolve, reject) => {
someAsyncOperation((err) => {
if (err) throw err; // Не попадет в catch!
});
});
Решение: Всегда обрабатывать ошибки явно
// Плохой пример - использование глобальных переменных
let cache = {};
app.get('/data', (req, res) => {
if (!cache.data) {
cache.data = fetchData(); // Проблемы при кластеризации
}
res.send(cache.data);
});
Решение: Использовать специализированные решения (Redis, Memcached)
// Плохой пример - отсутствие обработки backpressure
readable.pipe(writable); // Без обработки переполнения
Решение: Всегда учитывать обратное давление
readable.on('data', (chunk) => {
if (!writable.write(chunk)) {
readable.pause();
writable.once('drain', () => readable.resume());
}
});
// Плохой пример - middleware с множеством ответственностей
app.use((req, res, next) => {
authenticate(req);
logRequest(req);
validateParams(req);
checkPermissions(req);
transformData(req);
next(); // Слишком сложно для тестирования
});
Решение: Разделять middleware на специализированные
// Плохой пример - загрузка всего файла в память
app.post('/upload', (req, res) => {
let data = '';
req.on('data', chunk => data += chunk); // Проблема с большими файлами
req.on('end', () => processData(data));
});
Решение: Использовать потоки
app.post('/upload', (req, res) => {
const transform = new TransformStream({ /* ... */ });
req.pipe(transform).pipe(res);
});
// Плохой пример - жесткие зависимости от окружения
if (process.env.NODE_ENV === 'production') {
db.connect('prod:password@localhost'); // Креды в коде
}
Решение: Использовать config-файлы и переменные окружения правильно
// Плохой пример
setTimeout(() => {}, 86400000); // Что это за число?
if (status === 4) { ... } // Что означает 4?
Решение: Использовать константы и enums
// Плохой пример - require внутри функций
function getConfig() {
return require('./config.json'); // Кэшируется только при первом вызове
}
// Плохой пример - циклические зависимости
// a.js
require('./b');
// b.js
require('./a');
Решение: require только на верхнем уровне модуля
Node.js-разработка требует особого внимания к асинхронности, управлению памятью и потоками данных. Избегая этих антипаттернов, вы создадите более надежные, производительные и поддерживаемые приложения. Ключевые принципы: асинхронность везде где возможно, правильная обработка ошибок, избегание глобального состояния и грамотная работа с ресурсами.