Заметки в категории «JavaScript» (страница 6)

Боремся с большой вложенностью анонимных колбеков

В Node.js почти все методы библиотек, работающих с внешними данными, выполняются асинхронно. Результат возвращается либо в виде событий, либо в виде вызова указанного колбека. Разработчик тут же не задумываясь использует анонимную функцию в качестве такого колбека, а затем ещё одну и ещё… В результате, достаточно сложный участок кода оказывается ещё менее читаем.

Вот несколько советов, как улучшить читаемость такого кода.

Декомпозиция

Самым очевидным решением будет использовать в качестве колбеков не анонимные функции, а именованные. Да, простая декомпозиция может значительно повысить читаемость кода.

Возможно, в именованные функции стоит вынести только бизнесс-логику, а обработку ошибок от предыдущей функции оставить в анонимном колбеке.

Step

Когда столкнулся с тем, что основная часть бизнесс-логики у меня оказывалась глубоко закопана в «ифах» и колбеках, то первой, что попалось мне на глаза, оказалась библиотека step.

Она оказалась чрезвычайно проста в использовании и дружелюбна к другим библиотекам, использующим принцип «первым аргументом колбека всегда идёт объект ошибки».


var step = require("step");

function findRecentPostsOfActiveUsers(callback) {
  step(
    // получаем список активных пользователей
    function () {
      var nextStep = this;
      User.find({active: true}, nextStep);
    },
    // получаем последнюю публикацию каждого из ранее найденных пользователей
    function (err, users) {
      if (err) throw err;
      var group = this.group();
      users.forEach(function (user) {
        Post.findOne({userId: user.id})
          .sort({date: -1})
          .exec(group());
      });
    },
    // на последнем шаге передаем найденые публикации в колбек
    // function (err, posts) { … }
    callback
  );
}

Все выбрасываемые исключения отлавливаются и передаются в качестве ошибки следующему «шагу».

Внутри каждого шага можно организовать очередь или параллельное исполнение других асинхронных функций.

Async

Библиотека async во многом аналогична step , в части последовательного выполнения указанных функций и передачи возвращаемого результата на следующий шаг. Однако, помимо этого, там реализована масса других полезных техник для управления порядком выполнения кода.

When

Отдельно хочу отметить библиотеку when, в которой реализован немного другой подход, чем в step и async. Управление последовательностью выполнения кода осуществляются на базе отложенных объектов.

Так как отложенный объект может только один раз изменить своё состояние, то переход с шага на шаг можно осуществлять, например, по таймауту не боясь, что колбеки потом будут будут вызваны несколько раз.

В примерах и документации подробно описаны аспекты применения этой библиотеки.

Замечание

Описанные библиотеки можно и нужно применять не только на сервере, но и в браузере.

Комментарии к заметке: 4

Тестирование promise с помощью mocha

В своём «домашнем» проекте для тестирования кода я использую фреймворк mocha . В тестах все утверждения проверяются библиотекой should.js , которая выбрасывает соответствующие исключения, отлавливаемые mocha.

При тестировании promise -объектов возникла проблема из-за того, что все исключения, выброшенные внутри резолвера, автоматически отлавливаются этим объектом. В результате тесты падают по таймауту без дополнительных разъяснений.


describe("Listener", function () {
    it("should reject data", function (done) {
        var connection = new MockStream();
        var promise = listener(connection);
        connection.write("foo bar baz");
        connection.end();

        promise.then(
            function (obj) {
                done(new Error("unexpected fulfill"));
            },
            function (obj) {
                obj.should.be.a("object").and
                  .have.property("valid", false);
                done();
            }
        );

    });
});

В данном тесте я ожидаю, что promise будет отклонён с объектом, в качестве параметра, имеющим поле valid равное значению false. Если утверждение не будет соответствовать действительности, то тест не завершится естественным образом (т.е. вызов done() , который идет за утверждением, не будет выполнен).

Благо, каждый вызов метода then() возвращает другой promise-объект. Он выполняет или отклоняется так же как и его «предок», но в параметры колбеков будут передаваться совершенно другие значения. Этим можно воспользоваться для корректного завершения теста.


describe("Listener", function () {
    it("should reject data", function (done) {
        var connection = new MockStream();
        var promise = listener(connection);
        connection.write("foo bar baz");
        connection.end();

        promise.then(
            function (obj) {
                // throw new Error("unexpected fulfill");
                return new Error("unexpected fulfill");
            },
            function (obj) {
                obj.should.be.a("object").and
                  .have.property("result", false);
            }
        ).then(done, done);

    });
});

При любом изменении состояния исходного promise-объекта будет вызываться done(). Отличаться будет только параметр, с которым он будет вызван:

  • объект ошибки в случае неверного состояния или выбрасывания исключения;
  • undefined при удачном завершении теста.

Именно такие значения параметра определяют состояние теста в mocha.

Оставте свой комментарий

Компилируемые шаблоны и способы их хранения

Я уже писал в одной из заметок о шаблонах на JS . В этот раз я рассмотрю диаметральный подход – компилируемые шаблоны. Примерами таких шаблонов могут служить mustache.js, Underscore template и многие другие библиотеки.

В их основе лежит один и тот же принцип: конструктору передается исходный шаблон в виде строки текста; на выходе получаем некую функцию. Эта функция, в свою очередь, возвращает строку, в которой вместо размеченных особым образом фрагментов будут подставляться аргументы, использованные при вызове. Так же в шаблонах может быть реализована простая логика ветвления, циклы или вызов вспомогательных функций.

Интересным аспектом применения компилируемых шаблонов является то, где и как будет храниться исходный текст шаблона и скомпилированный шаблон. На ум приходят несколько вариантов:

  • в виде строки в js-файле;
  • внутри тега <script> с произвольным типом в html;
  • в отдельном файле;
  • компилирование исходного текста шаблона при сборке проекта.

Хранение в js-файле


var templateSource = "<div class=\"welcome\">" +
    '<p class="welcome__message">Hello: <%= name %></p>' +
    '</div>';

var template = _.template(templateSource);

Плюсы:

  • исходный текст шаблонов конкретного модуля сразу доступен для компиляции в момент инициализации этого модуля;
  • с большой вероятностью файл, содержащий шаблон, будет закеширован браузером, что в общем счёте сэкономит трафик при регулярном посещении страницы.

Минусы:

  • текст шаблона перемежается с логикой;
  • содержимое строки в JS нужно правильно экранировать;
  • для разных ролей пользователей могут понадобиться разные шаблоны, а это, в свою очередь, может скомпрометировать какую-то часть функциональности пользователей с высоким уровнем доступа.

Хранение в исходном HTML-коде страницы


<script id="template-welcome" type="text/x-template">
    <div class="welcome">
        <p class="welcome__message">Hello: <%= name %></p>
    </div>
</script>

Браузер содержимое тега <script> считает простым текстом, а так как в атрибуте type у него указан неизвестный ему MIME-тип, то интерпретировать или отображать он его не станет. Зато содержимое этого тега можно получить после загрузки документа, обратившись к нему по id.

var templateSource = document.getElementById("template-welcome").innerText;

Плюсы:

  • размещается вместе с другой разметкой;
  • набор шаблонов или их содержимое может динамически меняться в зависимости от внешних факторов.

Минусы:

  • если страница не будет закеширована браузером, то эти шаблоны будут каждый раз загружаться с сервера;
  • возможно, потребуется подождать окончания загрузки страницы, прежде чем к элементу, содержащему исходный текст шаблона, можно будет получить доступ.

Хранение в отдельном файле

Шаблоны, ко всему прочему, можно хранить в виде отдельных статических или генерируемых динамически файлов и загружать при необходимости.


<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <template id="template-welcome"><![CDATA[
        <div class="welcome">
            <p class="welcome__message">Hello: <%= name %></p>
        </div>
    ]]></template>
    <template id="template-product"><![CDATA[
        …
    ]]></template>
</templates>

Текст шаблона обязательно должен быть заключен в блок CDATA, чтобы его содержимое можно было получить без изменений.


$.ajax("/static/templates/welcome.xml")
    .done(function (templates) {
        var templateEle = templates.getElementById("template-welcome");
        if (templateEle) {
            callback(_.template(templateEle.innerText));
        }
        templateEle = null;
    });

Набор шаблонов загружается с помощью XHR, а затем методом getElementById() получаем нужный объект, содержащий исходный текст шаблона.

Плюсы:

  • с большой вероятностью статический файл, подгружаемый через XHR, будет закеширован браузером, и в дальнейшем некоторое время не будет запрашиваться с сервера;
  • можно организовать дополнительный уровень кеширования шаблонов (Local Storage, например);
  • наборы шаблонов можно формировать для конкретной комбинации ролей пользователя, что позволит защитить функциональность недоступную текущему пользователю.

Минусы

  • дополнительный HTTP-запрос при первом посещении;
  • инициализация модуля будет «отложенной» из-за асинхронной загрузки требуемых шаблонов.

Компилирование при сборке

В твиттере и комментариях мне подсказали ещё один способ хранения шаблонов. Каждый шаблон хранится в виде HTML-файла, а на этапе сборки эти файлы компилируются в js-функции и сериализуются прямо на сервере. В итоге клиент будет загружать обычные статические js-файлы.

Плюсы:

  • шаблоны будут кешироваться браузером как любые другие статические ресурсы;
  • браузер не тратит время на компиляцию шаблона.

Минусы:

  • для работы нужен процесс сборки проекта;
  • для компилирования шаблонов при сборке потребуется соответствующая технология (Node.js или нативная реализация на соответствующем языке программирования), что может быть накладно.

Заключение

Метод хранения сложных шаблонов в отдельных статических файлах мне кажется достаточно перспективным по сравнению с хранением тех же данных в HTML. С другой стороны, инициализация модуля оказывается гораздо сложнее, чем в случае с хранением шаблона в JS. Компилирование шаблона при сборке может стать идеальным вариантом, если в проекте есть сборка как таковая.

Комментарии к заметке: 6

Мастер-класс по Backbone.js

Для своих коллег я устроил вводную лекцию по Backbone.js и провёл мастер-класс, написав простое интерактивное приложение.

Архив с готовым приложением

Для запуска серверной части приложения понадобиться Node.js . После распаковки архива нужно будет загрузить все требуемые зависимости с помощью менеджера пакетов:

npm install

Сервер запускается командой

node app.js

Самое приложение будет доступно по адресу http://127.0.0.1:3000/

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

Комментарии к заметке: 4

Использование setTimeout() вместо setInterval()

Функция setInterval() предназначена для вызова переданного ей колбека через равные промежутки времени. У неё есть две особенности:

  1. Сложно (а иногда попросту не возможно) заниматься отладкой такого кода из-за того, что вызовы колбека будут производиться независимо от того, в каком месте кода вы поставили точку останова. На каждый шаг отладки управление будет передаваться основному циклу, который исправно вызовет колбек о очередной раз.
  2. Если длительность выполнения колбека по каким-либо причинам превысит установленную задержку, то это автоматически приведёт к лавинообразному увеличению количества замыканий, «выполняемых одновременно». Колбек будет вызываться снова и снова через заданный промежуток времени, не дожидаясь завершения предыдущего вызова.

От этих проблем можно избавиться, если воспользоваться setTimeout() . В этом случае планирование следующего вызова будет производиться только после завершения выполнения предыдущего.


/**
 * @param {Function} callback
 * @param {Number} delay
 * @param {Object} [context]
 * @return {Function}
 */
function interval(callback, delay, context) {
    var start = new Date(), id;

    delay = delay || 0;
    context = context || this;

    (function loop() {
        var result = callback.call(context, new Date() - start);
        if (result !== false) {
            id = setTimeout(loop, delay);
        }
    }());

    return function () {
        if (id) {
            clearTimeout(id);
            id = 0;
        }
    };
}

Кроме того, мы получаем несколько способов завершить выполнение этого бесконечного цикла. interval() возвращает функцию, с помощью которой можно прерывать следующий запланированный вызов колбека. Если колбек сам вернет значение false , то выполнение цикла также прекратится.


function doStuff(time) {
    $("body").append("<p>" + time  + "</p>");
    return time < 10000;
}
var cancelFn = interval(doStuff, 1000);
$("button").on("click", cancelFn);

При очередном выполнении колбека, ему в качестве параметра передается разница между текущим временем и временем вызова функции interval() . Так легко можно синхронизироваться по времени. Ведь теперь нет гарантий того, что пятый вызов колбека будет произведен ровно через 4 секунды.

Оставте свой комментарий