Марейн Хавербеке - Выразительный JavaScript
methods.GET = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(404, "File not found");
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.readdir(path, function(error, files) {
if (error)
respond(500, error.toString());
else
respond(200, files.join("n"));
});
else
respond(200, fs.createReadStream(path),
require("mime").lookup(path));
});
};
Поскольку запросы к диску занимают время, fs.stat работает асинхронно. Когда файла не существует, fs.stat передаст объект error с кодовым свойством "ENOENT" в функцию обратного вызова. Было бы здорово, если бы Node определил разные типы ошибок для разных ошибок, но такого нет. Вместо этого он выдаёт запутанные коды в стиле Unix.
Все неожиданные ошибки мы будем выдавать с кодом 500, обозначающим, что на сервере проблема – в отличие от кодов, начинающихся на 4, говорящих о проблеме с запросом. В некоторых ситуациях это будет не совсем аккуратно, но для небольшой примерной программы этого будет достаточно.
Объект stats возвращаемый fs.stat, рассказывает нам о файле всё. Например, size – размер файла, mtime – дата модификации. Здесь нам нужно узнать, директория это или обычный файл – это нам сообщит метод isDirectory.
Для чтения списка файлов в директории мы используем fs.readdir, и через ещё один обратный вызов, возвращаем его пользователю. Для обычных файлов мы создаём читаемый поток через fs.createReadStream и передаём его в ответ, вместе с типом содержимого, который модуль "mime" выдал для этого файла.
Код обработки DELETE будет проще:
methods.DELETE = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(204);
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.rmdir(path, respondErrorOrNothing(respond));
else
fs.unlink(path, respondErrorOrNothing(respond));
});
};
Возможно, вам интересно, почему попытка удаления несуществующего файла возвращает статус 204 вместо ошибки. Можно сказать, что при попытке удалить несуществующий файл, так как файла там уже нет, то запрос уже исполнен. Стандарт HTTP поощряет людей делать идемпотентные запросы – то есть такие, при которых многократный повтор одного и того же действия не приводит к разным результатам.
function respondErrorOrNothing(respond) {
return function(error) {
if (error)
respond(500, error.toString());
else
respond(204);
};
}
Когда ответ HTTP не содержит данных, можно использовать код статуса 204 (“no content”). Так как нам нужно обеспечить функции обратного вызова, которые либо сообщают об ошибки, или возвращают ответ 204 в разных ситуациях, я написал специальную функцию respondErrorOrNothing, которая создаёт такой обратный вызов.
Вот обработчик запросов PUT:
methods.PUT = function(path, respond, request) {
var outStream = fs.createWriteStream(path);
outStream.on("error", function(error) {
respond(500, error.toString());
});
outStream.on("finish", function() {
respond(204);
});
request.pipe(outStream);
};
Здесь нам не нужно проверять существование файла – если он есть, мы его просто перезапишем. Опять мы используем pipe для передачи данных из читаемого потока в записываемый, в нашем случае – из запроса в файл. Если создать поток не удаётся, создаётся событие “error”, о чём мы сообщаем в ответе. Когда данные переданы успешно, pipe закроет оба потока, что приведёт к запуску события “finish”. А после этого мы можем отчитаться об успехе с кодом 204.
Полный скрипт сервера лежит тут: eloquentjavascript.net/code/file_server.js. Его можно скачать и запустить через Node. Конечно, его можно менять и дополнять для решения упражнений или экспериментов.
Утилита командной строки curl, общедоступная на unix-системах, может использоваться для создания HTTP запросов. Следующий фрагмент тестирует наш сервер. –X используется для задания метода запроса, а –d для включения тела запроса.
$ curl http://localhost:8000/file.txt
File not found
$ curl -X PUT -d hello http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
hello
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
File not found
Первый запрос к file.txt завершается с ошибкой, поскольку файла ещё нет. Запрос PUT создаёт файл, и глядите-ка – следующий запрос его успешно получает. После его удаления через DELETE файл снова отсутствует.
Обработка ошибок
В коде файлового сервера есть шесть мест, где мы перенаправляем исключения, когда мы не знаем, как обрабатывать ошибки. Поскольку исключения не передаются автоматически в функции обратного вызова, но передаются им как аргументы, их надо каждый раз обрабатывать персонально. Это сводит на нет преимущество обработки исключений, а именно, возможность централизованно обрабатывать ошибки.
Что будет, когда что-то реально выбросит исключение в системе? Мы не используем блоки try, потому оно будет передано на самый верх стека вызовов. В Node это приводит к прекращению выполнения программы и выводу информации об исключении (вместе с отслеживанием стека) на стандартный вывод.
Поэтому наш сервер будет падать при возникновении проблем в коде – в отличие от проблем с асинхронностью, которые будут переданы как аргументы в функции вызова. Если нам надо обрабатывать все исключения, возникающие при обработке запроса, чтобы мы точно отправили ответ, нам надо добавлять блоки try/catch в каждом обратном вызове.
Это плохо. Много программ для Node написаны так, чтобы использовать как можно меньше работы с исключениями, подразумевая что в случае возникновения исключения программа не может его обработать, и поэтому надо падать.
Ещё один подход – использование обещаний, которые были описаны в главе 17. Они ловят исключения, выброшенные функциями обратного вызова и передают их как ошибки. В Node можно загрузить библиотеку promise и использовать её для обработки асинхронных вызовов. Немногие библиотеки Node интегрируют обещания, но обычно их довольно просто обернуть. Отличный модуль “promise” с NPM содержит функцию denodeify, которая берёт асинхронную функцию вроде fs.readFile и преобразовывает её в функцию, возвращающую обещание.
var Promise = require("promise");
var fs = require("fs");
var readFile = Promise.denodeify(fs.readFile);
readFile("file.txt", "utf8").then(function(content) {
console.log("The file contained: " + content);
}, function(error) {
console.log("Failed to read file: " + error);
});
Для сравнения, я написал ещё одну версию файлового сервера с использованием обещаний, которую можно найти на eloquentjavascript.net/code/file_server_promises.js. Она почище, потому что функции теперь могут возвращать результаты, а не назначать обратные вызовы, и исключения передаются неявно.
Приведу несколько строк оттуда, чтобы продемонстрировать разницу в стилях.
Объект fsp, использующийся в коде, содержит варианты функций fs с обещаниями, обёрнутыми при помощи Promise.denodeify. Возвращаемый из обработчика метода объект, со свойствами code и body, становится окончательным результатом цепочки обещаний, и он используется для определения того, какой ответ отправить клиенту.
methods.GET = function(path) {
return inspectPath(path).then(function(stats) {
if (!stats) // Does not exist
return {code: 404, body: "File not found"};
else if (stats.isDirectory())
return fsp.readdir(path).then(function(files) {
return {code: 200, body: files.join("n")};
});
else
return {code: 200,
type: require("mime").lookup(path),
body: fs.createReadStream(path)};
});
};
function inspectPath(path) {
return fsp.stat(path).then(null, function(error) {
if (error.code == "ENOENT") return null;
else throw error;
});
}
Функция inspectPath – простая обёртка вокруг fs.stat, обрабатывающая случай, когда файл не найден. В этом случае мы заменяем ошибку на успех, возвращающий null. Все остальные ошибки можно передавать. Когда обещание, возвращаемое из этих обработчиков, обламывается, сервер отвечает кодом 500.
Итог
Node – отличная простая система, позволяющая запускать JavaScript вне браузера. Изначально она разрабатывалась для работы по сети, чтобы играть роль узла в сети. Но она позволяет делать много всего, и если вы наслаждаетесь программированием на JavaScript, автоматизация ежедневных задач с Node работает отлично.