Source map

Итак, у вас очень много JavaScript-кода. А значит, вы его сжимаете перед тем, как отдавать клиенту. Наверняка даже используете YUI Compressor или Google Closure Compiler. А может, вы пишете свой client-side код на чем-нибудь модном: на Dart или CoffeeScript? В любом случае вы наверняка сталкивались с проблемой отладки и поиска ошибки в браузере. Попробуй угадать, почему в 3-й строчке на 100501 позиции значение переменной aAz вдруг стало undefined. А ведь до компиляции скрипта всё работало как надо! Я в таких случаях начинаю добавлять в код много инструкций console.log, чтобы хоть как-то проследить процесс выполнения кода.

Ну что ж, у меня для вас хорошие новости. В Google знают о ваших проблемах и даже придумали решение — source map. Идея проста: компилятор должен создавать специальный файл с информацией о соответствии скомпилированного кода не скомпилированному, а браузер должен брать информацию из этого файла и показывать красоту вместо абракадабры.

Расскажу, какие шаги мне пришлось проделать, чтобы source map заработал в этом блоге. Сразу предупреждаю, что на рабочем сайте вы его не найдете, он существует только на моей локальной машине, но вы можете попробовать собрать всё из исходников сами.

  1. Включаем генерацию source map в Google Closure Compiler. Для этого используется опция компилятора --create_source_map=./script.js.map.
  2. Файл получили. Но в нем скорее всего прописаны неправильные пути к исходным файлам, особенно если ваш корень сервера не совпадает с корнем проекта. Придется их поправить, например, вот такой unix-командой: sed -i 's/"static\//"\//g' static/js/script.js.map.
  3. Чтобы браузер знал о наличии файла с картой, нужно добавить комментарий в конец скомпилированного js-фала: echo "//@ sourceMappingURL=script.js.map" >> static/js/script.testing.js.
  4. Ну и последний этап, нужно включить поддержку 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 есть своя прелесть.

Получение бинарных данных в Google Closure Library

Делюсь с вами кусочком из своей практики, который может кому-нибудь пригодиться. Лично я был бы рад найти подобный пост на просторах интернета, когда столкнулся с этой проблемой.

Итак, задача: с помощью 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

Сегодня я обратил внимание на такую конструкцию в 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

Ну что я могу сказать? JsTestDriver — та еще поделка. Всё работает хорошо до тех пор, пока в браузере не возникнет исключение. В этом случае всё подвисает и не отвечает, пока не перезапустишь сервер. Например, если у тебя в коде стоит вызов a.b() и a === undefined, то пиши пропало. Будешь искать ошибку до скончания веков. Хотя вроде в новых IDEA и PhpStorm обещают поддержку дебаггера юнит-тестов для JsTestDriver. Посмотрим, что из этого выйдет.

← СтаршеМоложе →