Итак, у вас очень много JavaScript-кода. А значит, вы его сжимаете перед тем, как отдавать клиенту. Наверняка даже используете YUI Compressor или Google Closure Compiler. А может, вы пишете свой client-side код на чем-нибудь модном: на Dart или CoffeeScript? В любом случае вы наверняка сталкивались с проблемой отладки и поиска ошибки в браузере. Попробуй угадать, почему в 3-й строчке на 100501 позиции значение переменной aAz
вдруг стало undefined
. А ведь до компиляции скрипта всё работало как надо! Я в таких случаях начинаю добавлять в код много инструкций console.log
, чтобы хоть как-то проследить процесс выполнения кода.
Ну что ж, у меня для вас хорошие новости. В Google знают о ваших проблемах и даже придумали решение — source map. Идея проста: компилятор должен создавать специальный файл с информацией о соответствии скомпилированного кода не скомпилированному, а браузер должен брать информацию из этого файла и показывать красоту вместо абракадабры.
Расскажу, какие шаги мне пришлось проделать, чтобы source map заработал в этом блоге. Сразу предупреждаю, что на рабочем сайте вы его не найдете, он существует только на моей локальной машине, но вы можете попробовать собрать всё из исходников сами.
- Включаем генерацию source map в Google Closure Compiler. Для этого используется опция компилятора
--create_source_map=./script.js.map
. - Файл получили. Но в нем скорее всего прописаны неправильные пути к исходным файлам, особенно если ваш корень сервера не совпадает с корнем проекта. Придется их поправить, например, вот такой unix-командой:
sed -i 's/"static\//"\//g' static/js/script.js.map
. - Чтобы браузер знал о наличии файла с картой, нужно добавить комментарий в конец скомпилированного js-фала:
echo "//@ sourceMappingURL=script.js.map" >> static/js/script.testing.js
. - Ну и последний этап, нужно включить поддержку source map в Google Chrome. Для этого открываем Developer Tools (Ctrl+Shift+I), нажимаем на кнопку настроек в правом нижнем углу и ставим галочку напротив «Enable source maps».
Вот и всё, теперь можно отлаживать свои исходники даже несмотря на то, что у нас в браузер загружен упакованный файл.
А теперь ложка дегтя. Она одна, но зато большая: поддержка. Как это обычно бывает с новыми технологиями, не все успевают или не все хотят реализовывать всякие экспериментальные поделки. Поэтому мы будем довольствоваться только одним браузером — Google Chrome. Ребята из Mozilla тоже трудятся над поддержкой source maps, но работающего результата у них пока нет. А вот запрос «ie source maps» ожидаемо выдает 0 релевантных результатов.
Та же беда с Source Maps и в языках и компиляторах. Я нашел поддержку только в Google Closure Compiler и Dart, а так же зачатки в CoffeeScript. Если знаете еще кого-нибудь, кто поддерживает — добро пожаловать в комментарии.
Скажу, что спецификация так же предусматривает source map и для CSS файлов, но я пока не тестировал эту возможность.
Ну и напоследок пара ссылок: на спецификацию и на обзор.
Развитие блога не прекращается ни на минуту (ну почти). На этот раз я сделал meta-теги в заголовке страницы для Facebook. Теперь, если добавить ссылку на мой блог, то в блок в Facebook попадет начало поста. Кстати, совершенно внезапно, вконтактик тоже правильно стал отображать описание, что не может не радовать.
В очередной раз впечатлился выразительностью Хаскеля: cколько всего можно написать одной строчкой. Вот, например, кусочек из свеженаписанного:
getDescription :: Node -> Text
getDescription = maybe emptyDescription
(until (not . T.null) (\_ -> emptyDescription) . getDescription') .
findChild (checkMainDiv . current) . fromNode
getDescription' :: Cursor -> Text
getDescription' = cutDescription . transformDescription .
T.intercalate " " . map nodeText . filter checkParagraph .
maybe [] siblings . firstChild
Можете сравнить с примерной калькой на JavaScript:
var getDescription_ = function (cursor) {
var fc = firstChild(cursor)
return cutDescription(transformDescription(
(fc ? siblings(fc) : []).filter(checkParagraph).map(nodeText).join(" ")
));
}
var getDescription = function (node) {
var mainDiv = findChild(function (cursor) {
return checkMainDiv(current(cursor))
}, fromNode(node));
return mainDiv ? getDescription_(mainDiv) || emptyDescription : emptyDescription;
}
Хотя да, и в JS есть своя прелесть.
Делюсь с вами кусочком из своей практики, который может кому-нибудь пригодиться. Лично я был бы рад найти подобный пост на просторах интернета, когда столкнулся с этой проблемой.
Итак, задача: с помощью XmlHttpRequest получать бинарные данные с сервера. Ну как-то так получилось, что нужно скачивать сжатый с помощью gzip xml-файл с сервера и выбирать из него некоторый набор данных.
Первая важная вещь, которая понадобится, — это метод overrideMimeType
у объекта XMLHttpRequest. Этот метод позволяет переопределить тип получаемых с сервера данных. И даже существует специальная кодировка, которая специально была задумана для приема бинарных данных — x-user-defined
(мы помним, что XMLHttpRequest автоматически преобразует все принятые данные в юникод). Поэтому простое добавление следующей строчки должно сделать большую часть работы.
xhr.overrideMimeType('text/plain; charset=x-user-defined');
Мозг, натренированный jQuery, выдает кусок кода практически сразу:
$.ajax({
url: url,
beforeSend: function(xhr) {
xhr.overrideMimeType(
'text/plain; charset=x-user-defined'
);
},
success: successHandler,
error: errorHandler
});
А вот с Google Closure Library не всё так просто. Там нет никаких beforeSend
, и если посмотреть в документацию, то нужная функция выглядит следующим образом:
goog.net.XhrIo.send(url, opt_callback, opt_method, opt_content, opt_headers, opt_timeoutInterval)
Только один callback, который срабатывает на complete, данные, заголовки, таймаут. Не густо. Но если посмотреть в код этой функции, то можно обнаружить, что на самом деле это прокси для объекта goog.net.XhrIo
. Но goog.net.XhrIo
тоже не конструирует XMLHttpRequest. Вместо этого использует фабрику для создания запросов, что, кстати, логично, но больше Java-way чем JS-way. Ну да ладно. Получается, чтобы вклинить вызов overrideMimeType
, нужно сделать свою фабрику с блекджеком, например, вот так:
my.net.BinaryXmlHttpFactory = function () {
goog.base(this);
};
goog.inherits(my.net.BinaryXmlHttpFactory, goog.net.DefaultXmlHttpFactory);
my.net.BinaryXmlHttpFactory.prototype.createInstance = function () {
var xhr = goog.base(this, "createInstance");
xhr.overrideMimeType('text/plain; charset=x-user-defined');
return xhr;
};
Теперь дело осталось за малым: инициализировать goog.net.XhrIo
c новой фабрикой и выполнить запрос:
var xhrIo = new goog.net.XhrIo(new my.net.BinaryXmlHttpFactory());
goog.events.listen(xhrIo, goog.net.EventType.SUCCESS, successHandler);
goog.events.listen(xhrIo, [goog.net.EventType.ERROR, goog.net.EventType.ABORT], errorHandler);
xhrIo.send(url);
Вот и всё. Сразу оговорюсь, что всё это не будет работать в Internet Explorer, т.к. там нужна своя особая магия для получения бинарных данных, в отличие от остальных, где остаток действий выглядит довольно просто:
var res = '';
for (var i = 0; i < data.length; ++i) {
var code = data.charCodeAt(i);
res += String.fromCharCode(code & 0xff);
}
Надеюсь, что кому-то этот пост поможет и направит мысли в правильную сторону.
Сегодня я обратил внимание на такую конструкцию в JavaScript: void 0
. И вдруг понял, что я не знаю, для чего на самом деле нужно слово void
. То, что void 0
используется как эквивалент undefined
, я догадывался, но не более. Так вот, раскрываю всем (и в том числе себе) глаза. Оператор void
используется для изменения результата любого выражения на undefined
. Как-то так получилось, что сейчас применений для этого слова почти не осталось, кроме сокращения записи undefined на целых 3 байта. Хотя на MDN написано, что void
может использоваться в URI-схеме 'javascript:
', чтобы избежать замены станицы результатом выполнения этого выражения. Да, так и есть. Проверил: если сделать ссылку <a href=”javascript: ‘Hello world’”>Click!</a>
, то после нажатия на нее, вся страница будет заменена на «Hello world». Но не во всех браузерах: Chrome выпилил этот пережиток прошлого из своих исходников, так что полагаться на эту возможность нельзя.
Вот и получаем в итоге два практически бесполезных куска знаний: void
и принцип работы схемы javascript:
.
Ну что я могу сказать? JsTestDriver — та еще поделка. Всё работает хорошо до тех пор, пока в браузере не возникнет исключение. В этом случае всё подвисает и не отвечает, пока не перезапустишь сервер. Например, если у тебя в коде стоит вызов a.b()
и a === undefined
, то пиши пропало. Будешь искать ошибку до скончания веков. Хотя вроде в новых IDEA и PhpStorm обещают поддержку дебаггера юнит-тестов для JsTestDriver. Посмотрим, что из этого выйдет.