Фронт-энд Островка изнутри

в 13:19, , рубрики: css, google closure, html, javascript, jsdoc, scss, архитектура, БЭМ, Веб-разработка, верстка, фронтенд, метки: , , , , , , , , ,

Привет, меня зовут Игорь (iamo0), я старший фронт-энд разработчик в Островке. Я занимаюсь нашим основным продуктом: сайтом Ostrovok.ru. С помощью нашего сайта ежедневно бронируют отели тысячи человек, поэтому для нас очень важно, чтобы качество нашего продукта было на высоте. А для этого нужно не отвлекаться на разного рода мелочи и уметь эффективно решать поставленные задачи.

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

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

Командная работа

Автобусное число

В команде, занимающейся фронт-эндом Островка, работает девять человек. Как правило, даже двум разработчикам нелегко прийти к консенсусу, как правильно писать код (привет, Экстранет ;-), что уж говорить о девятерых. С другой стороны, нужно думать о том, чтобы архитектура проекта была ясной и четкой, код — легко читаемым и поддерживаемым. Важно, чтобы новичок, недавно пришедший в команду или человек, которому приходится переключиться на другой проект, смог быстро разобраться в коде.

У менеджеров и разработчиков есть термин «автобусное число». Это число разработчиков, котрых должен сбить автобус, чтобы проект стало невозможно поддерживать. В самом худшем случае автбусное число равно единице. Наша задача — максимально повысить автобусное число, в идеале, оно должно быть равным количеству разработчиков.

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

Гайдлайны

Чтобы разработчики писали код в одинаковом стиле существуют гайдлайны. На одном из наших внутренних ресурсов любой разработчик может найти подробнейшие гайдланы по JavaScript, HTML и CSS, которые включают в себя не только правила форматирования кода, но и описывают особенности экосистемы нашего проекта: где и как мы храним файлы, каким образом организуем сборку проекта и некоторые правила написания кода.

Одним из важнейших правил, используемых в нашем проекте, является обязательное документирование кода с помощью jsDoc (http://code.google.com/p/jsdoc-toolkit/, https://developers.google.com/closure/compiler/docs/js-for-compiler). Использование jsDoc дает большие преимущества. Во-первых, возрастает читаемость кода. Во-вторых, во многих IDE невероятно удобно ориентироваться в проекте, в котором используется jsDoc. Те же, кто любит стрелять себе в ноги использует простые текстовые редакторы, вроде vim, могут скомпилировать документацию в HTML-файлы с помощью консольной утилиты идущей в jsDoc-toolkit’e. Ну и, наконец, чтобы правильно написать jsDoc, разработчик должен разложить все по полочкам у себя в голове, а это никогда не навредит.

Код-ревью

Второе условие увеличения автобусного числа — знание разработчиков о деталях кода других частей проекта — достигается с помощью системы код-ревью, которая в Островке называется WTF (первая реакция любого разработчика, который смотрит не на свой код).

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

Особенности разработки

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

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

Попробуйте поменять фильтры на нашей поисковой выдаче, например количество звезд или стоимость номеров — список отелей обновится мгновенно, потому что все вычисления происходят на клиенте. Несмотря на то, что мы работаем с большими объемами данных — например, в Лондоне у нас больше двух тысяч отелей — сайт реагирует на запросы пользователя очень быстро.

JavaScript

Такой большой упор на клиентскую часть требует того, чтобы код был тщательно организован и структурирован. Мы избрали модульный подход к разработке, основанный на ООП. Это значит, что мы активно используем классы, наследования, слабое связывание и прочие прелести ООП, которые предоставляет JavaScript.

Для каждой страницы у нас есть отдельный JavaScript-класс, содержащий в себе какие-то вложенные модули, которые, в свою очередь, так же могут дробиться до бесконечности.

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

Чтобы проиллюстрировать подход, приведу достаточно грубый пример кода. Представим, что у нас есть страница с формой ввода электронной почты для подписки.

Мы организуем этот код так:

//base.js
/** 
 * @fileoverview Sperical code in vacuum for Habrahabr.ru
 * @author Igor Alexeenko (habrahabr.ru/users/iamo0)
 */

/** Project namespace */
var ota = {};

/**
 * Inheritance interface. Uses temporary constructor.
 * @param {Function} childCtor
 * @param {Function} parentCtor
 */
ota.inherit = function(childCtor, parentCtor) {
  var temporaryCtor = function() {};
  temporaryCtor.prototype = parentCtor.prototype;

  childCtor.prototype = new temporaryCtor;
  childCtor.prototype.constructor = childCtor;

  // Link to parent's prototype.
  childCtor._super = parentCtor.prototype;
};

/**
 * Base module. It's not necessary, to implement full base class in simple
 * example, so let it just be an empty class, inherited from Object.
 * @constructor
 * @extends {Object}
 */
ota.BaseModule = function() {};
ota.inherit(ota.BaseModule, Object);

//page.js
/** 
 * @fileoverview Page controller.
 * @author Igor Alexeenko (o0@ostrovok.ru)
 */

/**
 * @constructor
 * @extends {ota.BaseModule}
 */
ota.Page = function() {
  this._form = new ota.EmailForm;
};
ota.inherit(ota.Page, ota.BaseModule);

//searchform.js
/** 
 * @fileoverview Email form.
 * @author Igor Alexeenko (alexeenko.igor@gmail.com)
 */

/**
 * @constructor
 * @extends {ota.BaseModule}
 */
ota.EmailForm = function() {
  this._formElement = document.querySelector('.' + ota.EmailForm.CLASS_NAME);
  this._formElement.addEventListener(
      'submit', 
      ota.EmailForm.getEventHandler(this, this._onSubmit)
  );
};
ota.inherit(ota.EmailForm, ota.BaseModule);

/**
 * Class name, which uses for match DOM-element.
 * @const
 * @type {string}
 */
ota.EmailForm.CLASS_NAME = 'form-element';

/**
 * Error message, which displays if user, entered wrong email address.
 * @const
 * @type {string}
 */
ota.EmailForm.EMAIL_ERROR_MESSAGE = (
    'Введите, пожалуйста, правильный адрес электронной почты.'
);

/**
 * @const
 * @type {RegExp}
 */
ota.EmailForm.EMAIL_REGEXP = (
    /^[a-zA-Z0-9-_.+]{1,}@[a-zA-Z0-9-_.]{2,}.[a-zA-Z]{2,}$/
);

/**
 * Static method. Returns event handler with certain context.
 * @param {Object} context
 * @param {Function} handler
 * @return {Function}
 */
ota.EmailForm.getEventHandler = function(context, handler) {
  return function(event) {
    handler.call(context, event);
  };
};

/**
 * Form submit handler. Alerts an error if email field is empty or its value
 * is invalid. Otherwise, submits the form.
 * @param {Event} event
 * @private
 */
ota.EmailForm.prototype._onSubmit = function(event) {
  event.preventDefault();

  var formIsValid = this.validateForm();
  var emailField = this._formElement.elements[0];

  if (!formIsValid) {
    alert(ota.EmailForm.EMAIL_ERROR_MESSAGE);
    emailField.focus();
    return;
  }
 
  this._formElement.submit();
};

/**
 * Form validator. Returns true if email field value is valid.
 * @return {boolean}
 */
ota.EmailForm.prototype.validateForm = function() {
  var emailField = this._formElement.elements[0];
  var emailValue = emailField.value;

  return (emailValue !== '' && ota.EmailForm.EMAIL_REGEXP.test(emailValue));
};

Теперь, чтобы заставить это работать, мне достаточно в HTML страницы поставить следующий код, разумеется, сначала подключив все необходимые файлы:

<script>
  new ota.Page;
</script>

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

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

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

Фронт энд Островка изнутри
Кастомизированная и обычная поисковая выдача для Москвы (хайрез).

Разметка и стили

Тщательная организация кода, разумеется, касается не только JavaScript, но и верстки: серверных и клиентских шаблонов и CSS. Требования к верстке те же самые, что и к JavaScript-приложению: модульность, гибкость, настраиваемость, безболезненное наследование. Нам повезло с тем, что существует достаточно хорошо устоявшаяся и зарекомендовавшая себя система организации кода, подходящая под эти требования. Это БЭМ (http://ru.bem.info/method/, http://clubs.ya.ru/bem/).

Для разметки и стилей используем БЭМ-методологию, правда с некоторыми особенностями. Во-первых, БЭМ не слишком хорошо подходит к нашей системе работы с JavaScript, поэтому js файлы храним отдельно и организуем в своем собственном порядке, а во-вторых для стилей используем SCSS(http://sass-lang.com/) вместо простого CSS.

Клиентские шаблоны

Из-за того, что большая часть логики реализована на JavaScript, мы активно пользуемся клиентскими шаблонами. Поначалу, мы использовали шаблонизатор, встроенный в библиотеку underscore.js (http://underscorejs.org/), но быстро поняли что это неудобно. Во-первых, хотелось хранить шаблоны в отдельных файлах, а во-вторых, хотелось, чтобы шаблоны были не строковыми переменными в JavaScript, в которых писать разметку в текстовых редакторах вроде vim подобно казни.

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

К нам на помощь пришел наш отдел инфраструктуры, сделав для нас шаблонизатор. Мы назвали его JST — JavaScript Templates. Основной синтаксис оставили из underscore.js, но сделали так, чтобы шаблоны хранились во внешних файлах с расширением jst и автоматически компилировались в JavaScript-функции еще на этапе сборки проекта. Скомпилированные шаблоны подключаются к проекту как обычные js-файлы.

<!-- path/to/template.jst -->
<ul>
  <% elements.forEach(element, function(el, index) { %>
  <li><%= element.name %></li>
  <% }) %>
</ul>

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

// some_file.js
var template = templateNs['path/to/template.jst'];
var list = [
  { name: 'Первый' },
  { name: 'Второй' },
  { name: 'Третий' }
];

var renderedHtml = template.render({
  elements: list
});

document.body.innerHTML = renderedHtml;

Работа с клиентскими шаблонами стала невероятно удобной как для тех, кто занимается версткой, так и для тех, кто пишет JavaScript.

Зависимости и сборка проекта

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

Сервис, который занимается сборкой проекта называется mediagenerator, его написал один из наших разработчиков. Этот сервис собирает зависимости внутри проекта на основе использования специальных тегов.

Вернемся к примеру. Чтобы подключить в основном для страницы файле page.js, файлы base.js и searchform.js, нужно прописать зависимости прямо внутри page.js. Прописываются они в jsDoc, в теге @require. Получается, чтобы подключить нужные файлы, достаточно в файле page.js, в любом месте, например в блоке @fileoverview, добавить два тега:

/**
 * @fileoverview Page controller
 * @author Igor Alexeenko (twitter.com/iamo0)
 * @require ‘base.js’
 * @require ‘searchform.js’
 */

Все. Теперь в режиме отладки на страницу добавятся два тега <script>, которые подключат нужные файлы. А в продакшн режиме, медиагенератор склеит все эти три файла в один и пропустит через минификатор. Разумеется, он умеет следить за сложными зависимостями и никогда не подключит один файл несколько раз.

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

К чему мы идем

Есть старая программистская шутка: любая достаточно сложная программа на Си или Фортране содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины языка Common Lisp.

Для фронт-энд разработчиков эту шутку можно перефразировать так: любой достаточно сложный проект, написанный на jQuery, Backbone или Knockout.js (http://jquery.com/, http://backbonejs.org/ или http://knockoutjs.com/), содержит заново написанную, глючную и медленную реализацию половины Google Closure Tools (https://developers.google.com/closure/).

Действительно, все о чем я здесь рассказал: модульность, ООП-подход, наличие базового набора модулей от которых можно наследоваться, работа со стилями, прекомпилируемые шаблоны — есть в экосистеме Google Closure. Помимо JavaScript-библиотеки Google Closure Library, в эту систему входит JS-минификатор Google Closure Compiler; шаблонизатор Soy, компилирующийся как в серверные, так и в клиентские шаблоны; и язык стилей, интегрированный с Google Closure Compiler и поэтому позволяющий минифицировать даже имена css-классов (голубая мечта любого разработчика, использующего БЭМ).

Closure Tools разрабатывается Google для их собственных проектов с 2005 года. Сначала библиотека писалась для нужд почтовика GMail, а потом с ее использованием были переписаны все остальные проекты Google. База кода одной только JavaScript-библиотеки Closure Library весит около 8Мб и содержит все, что нужно для разработки сложных проектов.

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

Все в этом мире уже написано каким-нибудь Пушкиным: большая часть задач, с которыми сталкиваются многие разработчики средних/больших проектов при разработке их с нуля, была уже решена инженерами Google в процессе написания их бесконечного количества сервисов.

Этой осенью готовится к выходу новая версия Островка, которая сейчас разрабатывается именно с использованием Google Closure. Основная особенность нового Островка в том, что это синглпейдж-приложение. Это значит, что вся логика по переходу между страницами, работе с адресной строкой, загрузкой данных и рендерингом страниц переместится на клиентскую часть. Сложность разработки проекта с лихвой компенсируется скоростью работы сайта и более удобным пользовательским интерфейсом.

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

Автор: Ostrovok

Поделиться

* - обязательные к заполнению поля