Модуль для маркетплейс — от идеи до старта продаж. Часть 9 — Вдыхаем жизнь в генератор

Пришла пора обучить страницу генератора взаимодействовать с пользователем.Для этого потребуется набросать функционал обработки ajax запросов в компоненте, а также набросать небольшой кусок js кода для контроля над страницей. Поехали

AJAX

Мне видится, как минимум, 5 вариантов реализации обработки ajax запросов на бекенде:

  • стандартный битриксовый способ обработки ajax запросов (это когда ты ставишь параметр AJAX_MODE в настройках компонента, и оно само заботится об обработке ajax форм)
  • добавить с помощью инсталлятора скрипт куда-нибудь, в котором будет дублироваться подключение компонента генератора в режиме без шаблона, и слать запросы на него
  • добавить отдельный компонент, который будет заниматься обработкой ajax запросов, и также положить его в отдельный скрипт
  • комбинация из первого и второго способов — слать все запросы на ту же страницу, на которой подключен компонент (как делает битрикс в стандартном варианте), но бекенд для обработки запросов реализовать своими силами
  • написать нормальное API с человеческим лицом

Конечно, можно придумать много различных вариантов, но нужно подобрать такой, который будет наиболее удобен в долгосрочной поддержке и не будет мешать в развитии. Мне что-то подсказывает, что это последний вариант, но он не жизнеспособен в битриксе (придется делать это для каждого своего модуля, копипаста). Поэтому я прибегну к четвертому способу.

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

Структура запроса и ответа

Есть на эту тему множество стандартов — JSON APIJSON Schema, OData JSON Protocol в конце концов. Однако это профессиональные стандарты, которые применяются при построении крупных API. В моем же случае подошел бы какой-то более простой вариант, вроде JSend. Мне проще всего, конечно, сделать обработку собственного формата данных, чем и займемся.

Что я хочу:

  • каждый ajax запрос отправлять с помощью POST (мы не RESTful сервис разрабатываем, поэтому тут у нас свобода выбора)
  • при получении каждого ответа знать, является ли ответ ошибочным. Для этого я буду использовать коды состояния HTTP — давно прижившаяся и проверенная практика (не без изъянов, но все равно)
  • при получении каждого ответа иметь возможность получить трактовку этого типа ответа в виде обычного человекочитаемого текста
  • при получении каждого ответа иметь возможность передавать любые данные в любом формате

Для отправки запросов буду использовать jquery (благо, он входит в поставку коробки с битриксом). Поначалу была мысль использовать встроенную js библиотеку, однако после наброска первого прототипа желание сразу же отпало. Со способом описания запросов мудрить не придется — просто пишем функцию, которая будет отправлять запрос и принимать на вход пару дополнительных параметров. В дополнение, в каждом ajax запросе я буду добавлять параметр ajax_action, который будет содержать название действия, которое планирует совершить клиент.

Формат ответа мне видится следующим:

Бекенд

Для того, чтобы было удобнее следить за форматом ответа, создадим класс JsonResponse, который возьмет на себя контроль за структурой. Тут все очень просто:

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

Чтобы отличить обычный запрос от ajax запроса, в битриксе в классе HttpRequest уже есть метод isAjaxRequest. Он полагается на заголовок X-Requested-With или Bx-ajax. Первый является сейчас стандартом де-факто, и по идее вставляется самим jquery в каждый ajax запрос (хоть этот заголовок и не является обязательным для ajax запросов), а второй добавляется в ajax-запрос в случаях, когда отправку запроса осуществляет битриксовая библиотека. Для того, чтобы заголовок X-Requested-With добавлялся стопудово, пропишем его насильно при формировании ajax запроса с помощью jquery.

В принципе, нам достаточно будет полагаться на результат работы метода HttpRequest::isAjaxRequest, однако еще стоит добавить проверку на переменную ajax_action:

Ну и придумаем обработчик для ajax запросов «в стиле битрикса». Идея его заключается в том, чтобы очистить весь буфер (если он был сформирован ранее), запустить обработку ajax запроса, и после успешной обработки вывести ответ в стандартный поток вывода, убив дальнейшую работу скрипта.

Тут видно, что после того, как буфер очищен, класс вызывает метод handleAjaxAction и передает ему экземпляр запроса, ожидая получить в ответ экземпляр JsonResponse. В случае неудачи выкидывается исключение, а в случае успеха будет отрендерен корректный json ответ.

Метод handleAjaxAction — это как раз входная точка для всех ajax-действий. При проектировании я насчитал аж 4 действия:

  • инициализация генератора (generation_start)
  • выполнение шага генератора (generation_step)
  • завершающий шаг генератора (generation_finish)
  • проверка шаблона (generation_preview)

Итого имеем:

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

Процесс генерации купонов будет сопровождаться выполнением большого количества работы. Ожидается, что администратору будет необходимо генерировать купоны десятками тысяч. В связи с этим нужно вводить искусственные ограничения на выполнение каждого шага. Я для себя пока определил, что выполнение каждого шага на бекенде не должно занимать более 5ти секунд, и что за один шаг не должно генерироваться более 1000 купонов. Этого вполне достаточно, чтобы поддерживать вменяемую скорость генерации даже на слабых машинах, а в будущем можно будет дать возможность управления этими ограничениями в настройках генератора.

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

Прогресс, настройки и отчет

На протяжении процесса генерации (который выполняется несколько хитов), нужно куда-то писать прогресс, а также на каждом хите формировать отчет. Все это основывается на изначально заполненных настройках.

Для данного функционала я заложил классы ProcessSettings, ProcessProgress и ProcessReport. Задача первого — просто сформировать заполненный объект на основании запроса (при запросе generation_start мы будем отправлять форму со всеми настройками).

Заполненный объект класса ProcessSettings передается в ProcessProgress, который содержит в себе все необходимые данные для отслеживания прогресса выполнения процесса. Он сохраняется в сессии и восстанавливается из нее при каждом ajax запросе.

ProcessReport — это простой класс, который формирует массив с данными на основании прогресса выполнения. Его потом можно будет легко преобразовать в html и вывести.

Код каждого из этих классов можно найти на гитхабе.

На каждом хите в процессе генерации будет формироваться отчет в виде обычного массива и записываться в payload. Также на каждом хите будем формировать html код с прогрессом выполнения. Для работы с прогрессбаром в битриксе есть только один более-менее удобоваримый способ —  метод \CAdminMessage::_getProgressHtml (естественно, на бекенде), поэтому html с прогрессом будем генерить там и потом передавать на фронтенд, где и будем показывать. Выглядит он так:

Фронтенд

JS в нашем случае — это часть шаблона компонента. Поэтому и размещать будем его там. Для того, чтобы подключить библиотеку используем вызов \CJSCore::Init(‘jquery’). В коде компонента вызывать его как бы нельзя, т.к. другие шаблоны этого компонента могут не использовать jquery вообще. Поэтому создадим в шаблоне файл component_epilog.php и добавим вызов туда. Попутно можно подключить библиотеку popup, я планирую использовать её для отображения уведомлений.

Для описания всей логики страницы создадим класс Generator и замкнем его, чтобы он никуда не вылез наружу.

Снабдим класс переменными, которые понадобятся ему для работы:

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

Далее нужно добавить обработчики событий нажатия на кнопки — «Превью» и «Начать генерацию». При этом нужно добавить контроль за уже идущим процессом, и не давать нажать на эти кнопки в случае, если процесс был запущен.

Для отправки ajax запросов напишем простую функцию makeActionRequest. Её задачей будет контроль за тробблером, выполнение ajax-запроса, обработка ответа и ошибок. Да, тут есть немного callback-hell, но что поделать, промисы в IE пока нельзя …

Тут есть небольшая хитрость с setTimeout. Благодаря тому, что вызов success функции оборачивается в setTimeout, нам не приходится в каждой success-функции следить за тробблером. Это небольшая плата за callback-hell, но я готов её платить в данном случае, чтобы не раздувать лишний раз код на фронтенде.

Осталось лишь создать функции success и error для каждого вида запроса, и пару вспомогательных функций для контроля над вкладками и всплывашками, и все, класс готов. Полный код класса можно найти на гитхабе.

На этом закончу.

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