Всем привет! Тема этого выпуска: Домены.
Домены – это возможность Node.js, который отсутствует как в обычном JavaScript, так и в JavaScript в браузерных реализациях. Домены предназначены для того, чтобы перехватывать любые асинхронные ошибки, например, если мы взглянем на сервер ниже, который мы разбирали в одном из предыдущих статей (загрузите себе код урока по ссылке для удобства), то увидим, что пока он работает – все нормально.
var http = require('http'); var fs = require('fs'); function handler(req, res) { if (req.url == '/') { fs.readFile('index.html', function(err, content) { if (err) { console.error(err); res.statusCode = 500; res.end('There is an error on the server'); return } res.end(content); }); } else { res.statusCode = 404; res.end("Not Found"); } } var server = new http.createServer(handler); server.listen(3000);
Но если где-нибудь происходит программная ошибка, скажем, вызов неизвестной функции :
fs.readFile('index.html', function(err, content) { if (err) { blabla(); console.error(err); res.statusCode = 500; res.end('There is an error on the server'); return } res.end(content);
, или человек вставил throw
, вместо того чтобы обрабатывать ошибку:
fs.readFile('index.html', function(err, content) { if (err) throw err; res.end(content); });
Или программная ошибка к примеру вызов JSON.parse("invalid!")
c invalid JSON
которое в конечном итоге валит весь процесс. Получается, что у нас есть сервер, к нему подключено, скажем, 1000 клиентов, во время обработки запроса одного из них возникла программная ошибка:
if (err) throw err;//JSON.parse("invalid!")
В текущей реализации это приводит к тому, что падает весь процесс. Это, конечно, нехорошо, потому что падение процесса – это обрыв соединения у всех подключенных клиентов. Если сервер падает, то давайте хотя бы нормально обработаем это, выведем сообщение, что произошла ошибка, и тогда уже, если ошибка критичная, то можно прервать процесс. Эта задача, несмотря на то, что встает очень естественно и должна иметь очевидное решение, не так то проста.
Из-за того, что callbacks
function(err, content)
вызываются асинхронно, обычно обертывание в try... catch
здесь совершенно бессильно, добавим такую запись к нашему коду вместо существующей:
var server = new http.createServer(function (req, res) { try { handler(req, res); } catch(err) { //error! } }); server.listen(3000);
Даже если мы вызовем обработчик внутри try... catch
, он сможет поймать лишь те ошибки, которые будут во время текущей работы функцией handler
. А если какие-то асинхронные callbacks
, то они уже сами по себе.
Теперь, когда мы уже познакомились с проблемой, поговорим о том, как она решается в Node.js. Для этого используется модуль, который называется domain
. На данный момент этот модуль устарел и использовать его в текущих версиях Node.js (7.1)не рекомендуется. Наши статьи посещенные доменам будут полезны для понимания и работы с Legacy Code к примеру. Этот модуль позволяет создавать специальный объект, который как раз и называют доменом, например, serverDomain
, добавим в наш server.js
:
var domain = require('domain'); var serverDomain = domain.create();
В контексте домена можно запускать функции, и он перехватит любые ошибки, включая асинхронные, которые в этой функции или запущенной из нее произойдут. Для примера сделаем небольшойrefactoring
текущего кода файла server.js
, сделав export
сервера и запустим его из другого файла в контексте домена:
var http = require('http'); var fs = require('fs'); function handler(req, res) { if (req.url == '/') { fs.readFile('no-such-file', function(err, content) { if (err) throw err; // JSON.parse("invalid!") res.end(content); }); } else { res.statusCode = 404; res.end("Not Found"); } } var server = new http.createServer(handler); module.exports = server;
Новый запускаемый файл будет называться app.js
добавим его в корневую директорию. Он будет создавать объект домена
и подключать сервер:
var domain = require('domain'); var serverDomain = domain.create(); var server = require('./server'); serverDomain.on('error', function(err) { console.error("Domain has caught %s", err); }); serverDomain.run(function() { server.listen(3000); });
Сервер просто экспортируется из файла server.js
. То есть, сейчас он не запускает сервер, а просто его создает.
Дальше мы ставим domain
-обработчик в app.js
serverDomain.on('error', function(err) { console.error("Domain has caught %s", err); });
Любые ошибки, которые произойдут внутри домена, будут перехвачены этой функцией. И, наконец, самое главное, запускаем сервер внутри домена специальным вызовом run
:
serverDomain.run(function() { server.listen(3000); });
Теперь любая ошибка, которая произойдет в результате работы функции выше или функции запущенной из нее, включая обработчики, будет перехвачена. Например, запустим проект, а затем запустим его еще раз. Так как адрес уже занят, это привело к ошибке, то есть, сервер бросил событие error:
Domain has caught Error: listen EADDRINUSE :::3000
Раньше это событие, так как нет обработчиков на error
, привело бы к исключению, которое бы повалило процесс, а теперь оно успешно перехвачено доменом.
Попробуем осуществить доступ к неизвестному файлу:
fs.readFile('no-such-file', function(err, content) {
Запускаем app.js
, переходим по этому адресу в Chrome:
http://127.0.0.1:3000/
Заходим.Ошибка. Вот что произошло: почему-то выпало исключение, domain
почему-то не сработал. В чем дело? Здесь проявился один важный подводный камень доменов. Чтобы его понять и все поправить, посмотрим, как вообще домены работают, и почему именно в этой ситуации domain
не справился. Разобраться с доменами будет гораздо проще, если мы вместо одного сложного примера рассмотрим несколько простых, последовательно усложняющихся. Например создадим domain.js
с таким кодом (остальные файлы можно удалить):
var domain = require('domain'); var d = domain.create(), server; d.on('error', function(err) { console.error("Domain has caught %s", err); }); d.run(function() { ERROR(); });
Здесь в domain
запускается функция, в которой вызывается непонятно что. Запускаем. domain
перехватил ошибку:
Domain has caught ReferenceError: ERROR is not defined
Как он это сделал? Очень просто. Когда функция вызывается в d.run
, вокруг нее ставится неявный try catch
. Соответственно, любое исключение, которое выпадает из этой функции, тут же переходит в ошибку:
d.on('error', function(err) { console.error("Domain has caught %s", err); });
Это самый тривиальный случай. Рассмотрим посложнее изменив код в domain.js
следующим образом:
var domain = require('domain'); var d = domain.create(), server; d.on('error', function(err) { console.error("Domain has caught %s", err); }); d.run(function() { setTimeout(function() { ERROR(); }, 1000); });
Здесь эта же функция с ошибкой вызывается внутри setTimeout
. Проверим, работает ли. Работает! Как эта функция, которая внутри setTimeout
, узнает вообще о том, что она запущена внутри домена? Ответ на этот вопрос можно дать очень просто, ели заглянуть внутрь Node.js. Дело в том, что когда функция запускается в контексте domain
, то перед его запуском внутри модуля domain
происходит специальный вызов:
d.enter();
а в конце – вызов,
//d.exit();
то есть, вход в domain
и выход из него:
.run(function() { // d.enter(); setTimeout(function() { ERROR(); }, 1000); // d.exit(); });
А это:
setTimeout(function() { ERROR(); }, 1000);
сработает асинхронно когда-нибудь. Она получит domain
за счет того, что когда мы входим в domain
, то текущий объект домена становится глобальным. Выглядит это так:
d.run(function() { // d.enter(); -> process.domain
А модуль setTimeout
, как и множество других модулей Node.js, которые занимаются всякими асинхронными вещами, интегрирован с доменами. То есть, внутренняя реализация setTimeout
смотрит, если есть текущий активный domain
, тогда она при вызове данной функции гарантирует, что этот же domain
будет активен. Соответственно, если внутри функции сделать консоль:
setTimeout(function() { console.error(process.domain); ERROR(); }); }, 1000);
То мы его и получим, запустив domain.js
:
Domain { domain: null, _events: { error: [Function] }, _eventsCount: 1, _maxListeners: undefined, members: [] } Domain has caught ReferenceError: ERROR is not defined
Обратим внимание, что это обычный объект, который наследует EventEmitter
.
Продолжение следует!
Код урока вы сможете найти здесь.
Материалы для статьи взяты из следующего скринкаста.
We are looking forward to meeting you on our website soshace.com