Около года назад Instawork начал экспериментировать с новым продуктом под названием Концерты. Gigs позволяет ресторанам и поставщикам общественного питания заполнять смены по требованию квалифицированными посудомоечными машинами, поварами и серверами из сети профессионалов Instawork. Наш MVP был не более чем формой Google для запроса смены. Поначалу это работало нормально, но когда продукт стал популярным, стало ясно, что нам нужна веб-панель, чтобы менеджеры могли просматривать и редактировать свои выступления.

Наша существующая веб-панель (приложение для найма для работы на полную ставку) была построена как одностраничное приложение (SPA) с использованием React и Redux. SPA получал данные через RESTful JSON API, написанный на Django. Эта архитектура позволила нам создать динамическое приложение с широкими возможностями взаимодействия с пользователем. Это также сэкономило время, предоставив нашим мобильным приложениям доступ к API. Граница абстракции между клиентом и сервером помогла нам написать модульный код с четким разделением бизнес-логики и логики представления. В общем, создание SPA с React / Redux на JSON API было правильным выбором для зрелого, четко определенного проекта.

Однако мы обнаружили, что ставим под сомнение подход SPA, когда пришло время создавать информационную панель для концертов. Продукт быстро развивался по мере того, как мы узнавали, что работает для нашего бизнеса и профессионалов, поэтому быстрое развитие было требованием для проекта. React / Redux SPA с его многочисленными уровнями абстракций не подходил для быстрой итерации.

  • Каждая новая функция требовала отдельных изменений кода в базах кода серверной части (Python) и внешнего интерфейса (JS).
  • Абстракция требовала повторения кода, поскольку данные API должны были быть представлены как на стороне сервера, так и в браузере.
  • Нам нужно было проделать дополнительную работу, чтобы наш API оставался обратно совместимым.

Поскольку наша архитектура SPA выглядела как неправильный подход для панели инструментов Gigs, мы обратились к тому, что Django предлагал из коробки.

Назад к основам с Django

Полнофункциональные фреймворки, такие как Django или Ruby on Rails, уже давно дают разработчикам возможность создавать веб-приложения с минимальными усилиями. С помощью зрелого, проверенного в боях инструмента, такого как Django, мы могли бы создать базовую панель инструментов (отображающую список концертов) менее чем за день. Этот процесс должен показаться знакомым любому, кто раньше работал с Django или Rails:

Мы начали с простой модели Django для представления концертов в нашей базе данных.

# models.py
class Gig(models.Model):
    id = models.IntegerField(primary_key=True)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)
    position = models.CharField(max_length=255)
    address = models.CharField(max_length=255)
    starts_at = models.DateTimeField()
    ends_at = models.DateTimeField()

Затем мы создали представление на основе классов, расширив ListView в Django. Этот класс предоставляет общие функции, такие как отрисовка шаблонов, помощники по разбивке на страницы, фильтрация набора запросов и многое другое.

# views.py
class GigsList(ListView):
    paginate_by = 20
    template_name = 'gigs.html'
    def get_queryset(self):
        return Gig.objects.filter(created_by=self.request.user)

Затем мы соединили представление с шаблоном пути URL.

# urls.py
urlpatterns = [
  url(r'^dashboard/gigs/?$', GigsList.as_view(name='gigs'))
]

Наконец, мы определили шаблон для рендеринга страницы (gigs.html). Обратите внимание, что ListView предоставляет контекст шаблона для page_obj и object_list. ListView также автоматически обрабатывает разбиение на страницы с помощью параметра запроса page.

{# gigs.html #}
<h2>My Gigs</h2>
<div id="gigs">
  {% include '_gigs_items.html' %}
</div>
<div class="pagination">
  {% if page_obj.has_previous %}
    <a href="{% url 'gigs' %}?page={{ page_obj.previous_page_number }}">Previous</a>
  {% endif %}
  {% if page_obj.has_next %}
    <a href="{% url 'gigs' %}?page={{ page_obj.next_page_number }}">Next</a>
  {% endif %}
</div>
{# _gigs_items.html #}
{% for gig in object_list %}
  <div class="gig">
    <div class="gig__title">{{ gig.title }}</div>
    <div class="gig__location">{{ gig.location }}</div>
    <div class="gig__time">
      {{ gig.starts_at|date:"D, b d g:i A" }} -
      {{ gig.ends_at|date:"g:i A" }}
    </div>
  </div>
{% endfor %}

Помимо стилей CSS и модульных тестов, это весь код, необходимый для доставки базовой панели Django нашим пользователям! Используя Django для создания чисто серверного веб-приложения, мы полностью устранили все клиентские уровни в нашей архитектуре:

Это не только позволило нам быстрее выпустить первую версию, но и позволило быстро выполнять итерацию. Новые функции могут быть реализованы в единой кодовой базе. Инженеры могут сосредоточиться на разработке с использованием единого фреймворка вместо того, чтобы переключаться между Python и JS. И нам больше не приходилось беспокоиться о совместимости API, поскольку код Django был написан для конкретной страницы и не использовался в других контекстах.

… Но неужели это слишком просто?

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

  • Добавьте немного JS на наши страницы, обрабатываемые сервером. Нам нужно будет добавить вызовы AJAX к пользовательским API и отобразить элементы пользовательского интерфейса на клиенте. Без полноценного JS-фреймворка этот тип кода становится сложным в обслуживании спагетти, подверженным ошибкам и тесно связанным с шаблонами, отображаемыми на сервере.
  • Перепишите приложение, используя архитектуру SPA / API. Такой фреймворк, как React / Redux, поможет нам написать поддерживаемый код, но нам нужно будет выбросить текущую версию и начать с нуля. И, конечно же, мы теряем скорость, простоту и мощность Django для будущих итераций.

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

Intercooler.js: взаимодействие без JS

Intercooler - это клиентская библиотека, которая упрощает определение запросов AJAX с помощью атрибутов HTML. Такие платформы, как React, Vue или Angular, ожидают, что запросы AJAX будут отвечать JSON. Интеркулер другой: он ожидает, что ответ будет HTML, который будет вставлен на страницу. Вот простой пример:

<a ic-get-from="/hello">Hello World!</a>

Установив атрибут ic-get-from, Intercooler отправит запрос GET на /hello и вставит содержимое ответа в элемент a. Интеркулер предоставляет гораздо больше атрибутов, которые расширяют базовое поведение несколькими способами:

  • Добавление или добавление контента к другим элементам на странице
  • Добавление / удаление классов CSS к новому контенту, чтобы обеспечить плавные переходы
  • отображение индикатора загрузки во время выполнения запроса AJAX
  • указание зависимостей между компонентами, чтобы обновить сразу несколько частей пользовательского интерфейса
  • Запуск запроса AJAX, когда элемент становится видимым.

Intercooler позволяет нам постепенно улучшать наши страницы Django, делая их удобнее в обслуживании, без необходимости полной перезаписи на JavaScript. Чтобы продемонстрировать это в действии, мы добавим Intercooler в приведенный выше код Django, чтобы заменить следующую / предыдущую разбивку на страницы бесконечной прокруткой.

Начнем с шаблона (новый код выделен жирным шрифтом):

{# gigs.html #}
<script src="{% static 'js/vendor/intercooler-1.2.1.min.js' %}"></script>
<h2>My Gigs</h2>
<div id="gigs">
  {% include '_gigs_items.html' %}
</div>
{# _gigs_items.html #}
{% for gig in object_list %}
  <div class="gig"
    {% if forloop.last and page_obj.has_next %}
      ic-trigger-on="scrolled-into-view"
      ic-append-from="{% url 'gigs' %}?page={{ page_obj.next_page_number }}"
      ic-target="#gigs"
    {% endif %}
  >
    <div class="gig__title">{{ gig.title }}</div>
    <div class="gig__location">{{ gig.location }}</div>
    <div class="gig__time">
      {{ gig.starts_at|date:"D, b d g:i A" }} -
      {{ gig.ends_at|date:"g:i A" }}
    </div>
  </div>
{% endfor %}

В gigs.html мы удалили код, который отображал предыдущие и следующие кнопки. В _gigs_items.html мы добавили несколько атрибутов Intercooler к последнему элементу в списке:

  • ic-trigger-on сообщает Intercooler сделать запрос AJAX, когда последний элемент прокручивается в поле зрения.
  • ic-append-from сообщает Intercooler сделать запрос AJAX для следующей страницы результатов и добавить результаты в целевой контейнер.
  • ic-target указывает контейнер, в который будет добавлен ответ AJAX.
{# view.html #}
class GigsList(ListView):
    paginate_by = 20
    def get_template_names(self):
        if int(self.request.GET.get('page')) > 1:
            return ['_gigs_items.html']
        return ['gigs.html']
    def get_queryset(self):
        return Gig.objects.filter(created_by=self.request.user)

В представлении мы заменяем статический шаблон (gigs.html) методом get_template_names . На первой странице концертов мы хотим отобразить всю страницу. Для последующих страниц мы хотим отобразить только контейнеры div для добавления в бесконечную прокрутку.

Это все, что нужно! Давайте посмотрим, что происходит, когда пользователь загружает и прокручивает страницу:

  • Когда пользователь посещает /gigs, мы загружаем всю страницу с первыми 20 гигами.
  • Когда пользователь прокручивает страницу вниз, Intercooler инициирует запрос AJAX на /gigs?page=2.
  • Сервер ответит отображением _gigs_items.html: только элементы div без остального содержимого страницы.
  • Интеркулер добавит элементы div в контейнер #gigs.

Одно простое улучшение, которое мы можем сделать, - это показать счетчик, когда пользователь достигает конца списка, до загрузки новых элементов. Это изменение можно сделать только в шаблоне:

{# gigs.html #}
<script src="{% static 'js/vendor/intercooler-1.2.1.min.js' %}"></script>
<h2>My Gigs</h2>
<div id="gigs">
  {% include '_gigs_items.html' %}
</div>
<img id="spinner" src="/static/spinner.gif" style="display:none">
{# _gigs_items.html #}
{% for gig in object_list %}
  <div class="gig"
    {% if forloop.last and page_obj.has_next %}
      ic-trigger-on="scrolled-into-view"
      ic-append-from="{% url 'gigs' %}?page={{ page_obj.next_page_number }}"
      ic-target="#gigs"
      ic-indicator="#spinner"
    {% endif %}
  >
    <div class="gig__title">{{ gig.title }}</div>
    <div class="gig__location">{{ gig.location }}</div>
    <div class="gig__time">
      {{ gig.starts_at|date:"D, b d g:i A" }} -
      {{ gig.ends_at|date:"g:i A" }}
    </div>
  </div>
{% endfor %}

Все, что нам нужно сделать, это добавить скрытый элемент счетчика к основному шаблону, а затем добавить атрибут ic-indicator к последнему элементу в списке. Это указывает Intercooler показывать #spinner во время ожидания ответа от запроса AJAX и скрывать его, когда запрос завершается.

Итерация с простотой

Из этого опыта разработки мы пришли к некоторым идеям, которые оказали глубокое влияние на то, как мы создаем программное обеспечение в Instawork:

  • Важно использовать стек технологий, который соответствует потребностям проекта. Подход SPA / API может хорошо работать для зрелых проектов, но он не подходит для незрелого, быстро развивающегося продукта.
  • Отрисованные на сервере страницы хорошо подходят для быстрой итерации из-за мощи таких фреймворков, как Django, и ограниченного набора технологий и абстракций.
  • Intercooler.js позволил нам развить наши страницы, отображаемые на сервере, чтобы они имели богатые, похожие на SPA функции, без необходимости адаптировать новую архитектуру и полностью переписывать.

Основной принцип команды инженеров Instawork - «итерация с простотой». Мы стремимся быстро создать хороший продукт, учиться на отзывах пользователей и вносить небольшие постоянные улучшения, пока не получим что-то отличное. Комбинация Django и Intercooler позволила нам удвоить этот процесс на благо наших пользователей и компании.

Что дальше?

Мы были настолько впечатлены уровнем производительности Django + Intercooler, что приняли его в качестве основного инструмента для веб-разработки. Но поскольку наша команда инженеров работает как в Интернете, так и в мобильной версии, мы начали замечать резкий контраст между нашей производительностью на двух платформах. Это заставило нас задуматься: почему мобильная разработка не может быть такой же быстрой и простой, как веб-разработка с Django и Intercooler?

Мы сделали больше, чем просто думали об этом, мы фактически создали структуру, позволяющую «итерацию с простотой» в наших мобильных приложениях. Следите за новостями о нашем решении в ближайшее время!

Обновление. Мы рады объявить об открытии исходного кода Hyperview, нашей серверной платформы для мобильных приложений. Hyperview черпает вдохновение в Intercooler и позволяет нам писать собственные мобильные приложения так же, как мы пишем веб-приложения. Ознакомьтесь с объявлением в блоге или на сайте Hyperview: https://hyperview.org