Как и зачем mkdev перешёл на Vue.js

Illustration of a perplexed person wearing a headband and apron attempting to pull apart sticky noodles over a bowl, with a sack of ingredients and garlic cloves spilled on the table. Illustration of a perplexed person wearing a headband and apron attempting to pull apart sticky noodles over a bowl, with a sack of ingredients and garlic cloves spilled on the table.

Disclaimer: в этой статье человек, который терпеть не может и не умеет работать с фронтендом, пишет о том, как работать с фронтендом и выбирать JavaScript фреймворк, и почему Vue.js - это отличный выбор. Я вас предупредил.

Я не люблю и не умею работать с фронтендом. Точно так же не любят с ним работать остальные разработчики mkdev. Мы предпочитаем фокусироваться на бакенде, а фронтенд мы либо отдаём на аутсорс, либо из рук вон плохо делаем сами. А затем через боль и слёзы переделываем, и переделываем, и переделываем - потому что Леонид Сущев, со-основатель проекта, не позволяет нам издеваться над его дизайнами.

В первой половине 2017 года мы почти решили проблему с вёрсткой - наняли эксперта, который переверстал весь сайт с использованием сетки и БЭМ. За один присест БЭМ для проекта с трёхлетней историей, конечно, не выкатить - мы ещё в процессе, и как закончим, так обязательно расскажем о результате.

А вот проблему с нашим JavaScript кодом мы даже и не пытались решить. mkdev - не single page application, у нас нет огромного числа интерактивных элементов и сложных визуальных редакторов. В паре мест мы используем datepicker, иногда встречается сортировка при помощи drag & drop, поверх статей есть простецкий markdown редактор. Всё настолько банально, что мы вот уже три года как используем готовые jQuery плагины и в ус не дуем. Не дули.

99 ошибок при написании кода с использованием jQuery

Есть у нас форма платежа, которую можно подёргать в профиле любого ментора. В форме есть слайдер для выбора количества недель. В зависимости от слайдера, туда-сюда динамически обновляются кнопочки. Прервись от чтения и иди подёргай этот слайдер, чтобы понять, о чём речь.

Подёргал? Видишь, как меняется форма в зависимости от слайдера и нажатия на разные кнопочки, и как цена динамически выводится в кнопке? Так вот, за годы фронтенд код этой формы превратился в самое ужасное, что человек может сделать при помощи jQuery.

Посмотрим на несколько примеров этого ужаса.

Огромное число условных операторов

if (window.location.pathname == "/dashboard/account") {
  $("#button-payment-block, .link-cancel").hide();
} else {
  $("#payment-block, #mentor-payment__login-form-container").hide();
  var value = $("#button-payment-block").text().split(" ").splice(0,3).join(" ");
  $("#button-payment-block").html(value);
}

Феерический парсинг строк

if ( $('.payment-form__weekly-price').html() ) {
  var arr = $(".payment-form__weekly-price").html().split(" ");
  var current_price_for_week = (options.total / (number_of_days / 7)).toFixed(1);
  arr.splice(3, 1, '' + current_price_for_week);
  $('.payment-form__weekly-price').html(arr.join(" "));
}

Постоянные манипуляции с DOM

$cardForm.show();
$cardForm.find('input[type=submit]').prop("disabled", false);
$('.upgrade-block .upgrade__payment-block, .upgrade-block .upgrade__title, .upgrade-block .upgrade__info').hide();
$('#upgrade__payment-block--payment-button').hide();
$('.upgrade-block').css({'border': 'none'});


И в таком духе целых 284 строчки невыносимой мешанины, в которой невозможно разобраться вообще никому. Усложнялось дело тем, что id элементов совершенно не сооветствовали сути этого элемента: #upgrade__payment-block--payment-button мог оказаться как кнопкой для совершения апгрейда подписки, так и простой кнопкой для платежа. Поди разберись.

Надеюсь, к этому моменту дорогой читатель оценил масштабы трагедии.

Какой JavaScript фреймворк выбрать

Мы рассматривали два варианта решения этой проблемы:

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

Мы боялись, что выбрав первый вариант, мы снова через год не будем понимать наш собственный код. Поэтому мы выбрали второй вариант. Нам нужна была библиотека или фреймворк, который:

  1. Позволит внедрять себя итеративно, лишь для небольших кусков фронтенда;
  2. Будет достаточно мощным из коробки, чтобы решить все наши проблемы без лишней возни;
  3. При этом будет достаточно простым, чтобы освоить его на нужном уровне за пару часов выходного дня.

Выбирали мы недолго и не тщательно. Честно говоря, выбор был сделан сразу после перепрочтения замечательной статьи Ивана Шаматова Rails 5 и Vue.js: как перестать мучаться с фронтендом и начать жить. Полистав документацию Vue.js, я решил, что надо пробовать.

webpacker, postcss, babel, yarn - отвалите.

Последовав совету Ивана Шаматова, я запустил рельсовый генератор для модных фронтендовых штук, надеясь быстро прикрутить webpack. К сожалению, генератор нагенерировал мне сверху babel и postcss, которые я использовать не планировал. Попутно оказалось, что мне ещё нужно будет использовать yarn. На этом этапе моё терпение закончилось, и я удалил весь сгенерированный рельсами мусор.

Но как же так, Кирилл? Это ведь не мусор, это же важнейшие инструменты современной фронтенд разработки!!11

Согласен. Думаю, время "современной" фронтенд разработки для кода mkdev ещё не пришло. Стандартный assets pipeline и подключение сторонних js-библиотек через гемы или путём бросаниях их vendor/assets пока что отлично работают в нашем случае. Внедрять сразу полдюжины новых технологий, только потому что это модно и круто, мы не любим.

Я взял vuejs-rails и подцепил ещё пару библиотек (для слайдера, например) прямиком в vendor/assets. У этого подхода, конечно, есть недостатки, которые на данном этапе совершенно не существенны.

Влюбляясь в Vue.js

Я не буду учить тебя как использовать Vue.js - у него есть шикарнейшая документация. А если хочешь по-настоящему стать мастером этой технологии (особенно в комбинации с Ruby on Rails), то найми Ивана Шаматова своим ментором. Поэтому дальше - опыт внедрения Vue.js в mkdev.me и мои впечатления.

Порог вхождения Vue.js

Проведя с Vue.js в сумме около 6-7 часов, у меня получилось полностью переписать всю фронтенд-часть формы для платежей на Vue.js - и выкатить это в production. Не самый плохой результат для совершенно новой для меня технологии.

Код на Vue.js в разы лаконичней и понятней

Если оценивать количество строчек кода, то payments.js сократился до 166 строчек. Из них 30+ уходит на внешний вид компонента для слайдера - компонент хороший, но по непонятным причинам вынуждает описывать его внешний вид прямо в js коде.

Сам компонент Vue.js всего лишь определяет свои свойства и методы. Не углубляясь в детали, вот как выглядит новый код платёжной формы:

 Vue.component('payment-form', {
      template: '#payment-form-template',
      props: [
        'initial_upgrade',
        'mentor_id',
        'initial_number_of_weeks',
        'current_user_is_present',
        'has_mentor'
      ],
      data: function() {
        return {
          coupon: "",
          error_message: "",
          final_form_visible: this.mentor_id == null, // Either login or card details form
          loading: false,
          number_of_weeks: this.initial_number_of_weeks,
          price: 0,
          upgrade: this.initial_upgrade,
          upgrade_notice_visible: this.initial_upgrade,
          weekly_price: 0,
          card: {
            // ...
          },
          slider: {
            // ...
          }
        }
      },
      components: {
        'vueSlider': window['vue-slider-component'],
        'vueMaskedInput': window['VueTheMask']
      },
      computed: {
        can_proceed_to_final_form: function() {
          return !(this.number_of_weeks == 0 || (this.has_mentor && this.mentor_id != null));
        }
      },
      watch: {
        number_of_weeks: function(val) { this.setPrice(); },
        coupon: function(val) { this.setPrice(); },
        price: function(val) {
          this.weekly_price = (this.price / this.number_of_weeks).toFixed(1);
        }
      },
      methods: {
        setPrice: function() {
          // ...
        },
        getStripeToken: function(event) {
          // ...
        },
        showFinalForm: function(event) {
          // ...
        },
        toggleViews: function(event) {
          // ...
        }
      },
      created: function () {
        this.setPrice();
      }
    })

В шаблоне мы указываем, где выводить эти свойства и при каком действии вызывать какой метод, например:

<a href="#"
   class="payment__button button button--centered"
   v-bind:class="[can_proceed_to_final_form ? '' : 'button--disabled']"
   v-if="!final_form_visible"
   v-on:click.prevent="showFinalForm">
  <%= t ".enroll_for", price: "{{price}}" %>
</a>
<! --- >
<template v-if="final_form_visible">
  <p class="payment__notice payment__notice--centered">
    <%= raw t '.to_continue' %>
  </p>
  <%= render "login_form" %>
  <%= link_to t('.cancel'), "#",
              class: "payment__cancel-link",
              "v-if" => "final_form_visible",
              "v-on:click.prevent" => "final_form_visible = false" %>
</template>

Элементы сами прячутся и выводятся в зависимости от значений свойств компонента, а привязка событий к элеметам (клик по кнопке) находится прямо в шаблоне.

Без webpacker указывать компоненты не так удобно - но это не страшно

В идеале, компоненты в Vue.js хранятся каждый в отдельном файлике с расширением .vue. В этом файлике хранится и шаблон, и код, и даже CSS. Отказавшись от использования webpacker, мы (наверное) потеряли возможность так делать. К счастью, Vue.js даёт несколько альтернативных способов определения компонент. Способ с X-Templates отлично лёг в нашу систему с assets pipeline:

<script type="text/x-template" id="payment-form-template">
  <div class="payment"
       v-bind:class="[upgrade_notice_visible ? 'payment--large' : '']">
<! -- >
</script>

Vue.js очень удобно встраивать кусками

Я уже частично рассказал об этом, но стоит лишний раз заметить: Vue.js не заставляет никого полностью переписывать весь код. Мы взяли один конкретный компонент проекта - платёжную форму - и применили для него Vue.js. Весь остальной фронтенд не поменялся.

Для Vue.js миллион плагинов

Я боялся, что для Vue.js будет не такой большой выбор готовых решений, как для jQuery. Я боялся зря. Сразу же нашлись готовые компоненты для слайдера, для masked input. Погуляв по Интернету, обнаружились компоненты для datepicker, для табличек и для чего угодно ещё.

Светлое будущее фронтенда mkdev.me

Подведём итоги.

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

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

Разумеется, мы ещё не научились использовать Vue.js как супер-профи. У нас в планах пара десятков мелких фиксов и улучшений для кода платёжной формы. А затем мы будем постепенно выбрасывать jQuery и заменять всё на Vue.js компоненты. Возможно, в итоге мы даже внедрим Webpack.

Ещё один бонус от внедрения Vue.js - разработчикам mkdev интересно его смотреть, трогать и внедрять. А такого в истории фронтенд-разработки mkdev.me ещё не было.