Заметки Андрея Романова

Пишу о жизни, учёбе и работе

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

Обложка статьи

14 апреля я побывал в Екатеринбурге на конференции DUMP 2017. Больше всего мне понравился доклад Владимира Гриненко об управлении зависимостями в компонентном вебе, так что я вкратце перескажу здесь его суть.

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

/* application.js */
import Button from '../../ui/button.js';
import Link from '../../ui/link.js';
/* application.css */
@import "../../ui/button.css";
@import "../../ui/link.css";

Недостатки такого подхода:

  • многословность;
  • на каждую технологию (скрипты, стили, etc) нужно писать свои импорты;
  • пути к файлам захардкожены, меняется путь — нужно переписывать импорты.

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

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

/* было, application.js */
import Button from '../../ui/button.js';
import Link from '../../ui/link.js';

/* было, application.css */
@import "../../ui/button.css";
@import "../../ui/link.css";

/* стало, application.decl.js */
['button', 'link']

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

Алгебра деклараций

Декларации зависимостей можно легко объединять, вычитать и находить их пересечение. Например, это может быть полезно для выноса общих зависимостей в один файл:

/* login.js */
['header', 'input', 'checkbox', 'button', 'footer']

/* landing.js */
['header', 'slider', 'gallery', 'button','footer']

/* пересечение, которое можно вынести в common.js */
['header', 'button', 'footer']

Композиция

Зависимости между компонентами тоже можно объявлять в виде деклараций. Было:

/* header.js */
import Button from '../../ui/button.js';
import Link from '../../ui/link.js';
// ...

export default class Header {}
/* header.css */
@import '../../ui/button.css';
@import '../../ui/link.css';

Стало:

/* header.deps.js */
['button', 'link'/*, ... */]

/* header.js */
export default class Header {}

Декларативное множественное наследование

Здесь уже начинаются страшные и со стороны непонятные БЭМ-штуки, которые, тем не менее, оказываются очень полезными и удобными, если в них разобраться.

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

Уровни переопределения

Ещё одна мощнейшая штука, которая есть в БЭМе — уровни переопределения. Наглядная демонстрация принципа:

Иллюстрация уровней переопределения в БЭМ

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

Яндекс любит проверять разные продуктовые гипотезы и проводить эксперименты. Например, команда поиска может выдвинуть такую гипотезу: «пользователи станут чаще переходить по рекламным ссылкам, если рекламу показывать на красном фоне». Чтобы проверить эту гипотезу, нужно провести А/Б-тестирование, то есть одной половине пользователей показать рекламупо-старому, а другой половине показать рекламу на красном фоне. После этого нужно сравнить результаты и проверить, действительно ли во втором случае пользователи кликали чаще.

Скорее всего, вы бы решили эту задачу так: в коде, отвечающем за показ рекламного блока, добавили бы условие наподобие «если пользователь входит в такую-то группу, добавляем сюда красный фон». Проблема в том, что таких экспериментов может быть очень много, и при таком подходе весь ваш код будет состоять из сплошных условий, которые ещё и нужно не забывать удалять. Явно немасштабируемый подход.

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

Возвращаясь к теме декларации зависимостей, в случае с уровнями переопределения мы, опять же, выигрываем:

/* было */
@import "common/button.css";
@import "project/button.css";
@import "experiment/button.css";
/* стало */
['button']

А как это использовать?

Для Реакта есть bem-react-core, для всего остального есть bem-sdk.

Минутка рефакторинга №1

Я тут подумал, что неплохо было бы раз в неделю-две делать публичное ревью кода, дабы делиться знаниями и практиковаться в улучшении кода. Я закинул удочку в Форвеб и первым прислать код на ревью не побоялся Сергей Мелодин, за что ему спасибо. Перейдём к ревью.

Задача:

Раз в t секунд последовательно выводить в консоль порции по n значений из массива, пока не будут выведены все элементы.

Исходное решение:

var myArr = ["Lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua", "Ut", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat"];
var i = myArr.length;
var x = 0;

var step = 10; // шаг смещения

function offset(x){
var y = x;
  while(x<y+step && x<i){
    console.log(myArr[x]);
    x++;
  };
};

function start(){
  offset(x);
  console.log('pause');
  x = x+step;
  if(x>i){ clearInterval(timer) };
};

var timer = setInterval(start, 1000);

Начнём с того, что тестовые данные можно сгенерировать динамически, благо значения элементов массива нам не важны:

// было
var myArr = ["Lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua", "Ut", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat"];

// стало
var myArr = Array.from({ length: 36 }, (value, index) => index);

Причёсываем код для лучшей читаемости (исправляем отступы и добавляем недостающие пробелы). Выносим интервал в отдельную переменную для гибкости. Интервал и количество выводимых значений за одну итерацию обозначаем прописными буквами, как константы. Количество выводимых за одну итерацию значений при этом переименовываем из step в ITEMS_PER_ITERATION. Заменяем var на const или let в зависимости от того, переприсваивается ли переменная в дальнейшем. Убираем точки с запятой после объявления функций, циклов и условий, они там не нужны. Переименовываем timer в intervalId, что точнее отражает суть значения. Избавляемся от микрооптимизации в виде сохранения длины myArr в переменную i — во-первых, название переменной никак не отражает её суть, во-вторых, преждевременная оптимизация лишь портит читаемость и усложняет код:

const ITEMS_PER_ITERATION = 10;
const INTERVAL = 1000;

const intervalId = setInterval(start, INTERVAL);
const myArr = Array.from({ length: 36 }, (value, index) => index);
let x = 0;

function offset(x) {
  const y = x;

  while(x < y + ITEMS_PER_ITERATION && x < myArr.length){
    console.log(myArr[x]);
    x++;
  }
}

function start() {
  offset(x);
  console.log('pause');

  x = x + ITEMS_PER_ITERATION;

  if (x > myArr.length) {
    clearInterval(intervalId);
  }
}

Сейчас есть две функции start() и offset(), с первого взгляда непонятно, что они делают. Самое ужасное — они обе изменяют переменную x. Но не всё так просто! На первый взгляд может показаться, что они изменяют одну и ту же переменную. Однако в функции offset() x — это аргумент, и он создаёт локальную переменную x. Такие переменные называются связанными — они непосредственно связаны с функцией, в которой они используются. Если бы у offset() не было аргументов, мы бы работали с внешней переменной x. Если переменная не связана, она называется свободной.

Итак, избавимся от излишней сложности со свободными и связанными переменными. Функция offset() по сути должна выводить в консоль ITEMS_PER_ITERATION элементов массива. Изменим её сигнатуру — аргумент x заменим на array, а саму функцию переименуем в logArrayItems. Ну и исправим функцию start(), чтобы она правильно работала с logArrayItems:

const ITEMS_PER_ITERATION = 10;
const INTERVAL = 1000;

const intervalId = setInterval(start, INTERVAL);
const myArr = Array.from({ length: 36 }, (value, index) => index);
let x = 0;

function logArrayItems(array) {
  array.forEach((item) => console.log(item));
}

function start() {
  logArrayItems(myArr.slice(x, x + ITEMS_PER_ITERATION));
  console.log('pause');

  x = x + ITEMS_PER_ITERATION;

  if (x > myArr.length) {
    clearInterval(intervalId);
  }
}

Теперь выглядит попроще, но всё далеко от идеала. Наше решение не выглядит целостным. Чтобы это исправить, обернём его в функцию logArrayItemsSequentially, которая будет принимать массив, элементы которого надо вывести в консоль, и настройки — количество выводимых за итерацию элементов и интервал в миллисекундах. Хорошей практикой считается обязательные параметры передавать в функцию как есть, а все опциональные параметры передавать в виде объекта (при этом опциональным параметрам нужно задать значения по умолчанию). Это позволяет сократить код при обычном использовании функции, но не потерять гибкость и оставить возможность переопределения настроек:

function logArrayItemsSequentially(array, options = {}) {
  const {
    itemsPerIteration = 10,
    interval = 1000,
  } = options;
  const intervalId = setInterval(start, interval);
  let x = 0;

  function logArrayItems(array) {
    array.forEach((item) => console.log(item));
  }

  function start() {
    logArrayItems(array.slice(x, x + itemsPerIteration));
    console.log('pause');

    x = x + itemsPerIteration;

    if (x > array.length) {
      clearInterval(intervalId);
    }
  }
}

const sampleArray = Array.from({ length: 36 }, (value, index) => index);

logArrayItemsSequentially(sampleArray/*, здесь могли быть переопределённые настройки */);

Отлично, теперь у нас есть целостное решение — функция, которую можно даже опубликовать на NPM (но лучше не надо). Снаружи её использование выглядит просто и понятно. Наведём порядок изнутри.

Для начала избавимся от функции logArrayItems — она используется лишь в одном месте:

// было
function logArrayItems(array) {
  array.forEach((item) => console.log(item));
}

function start() {
  logArrayItems(array.slice(x, x + itemsPerIteration));
  // ...
}

// стало
function start() {
  array.slice(x, x + itemsPerIteration).forEach((item) => console.log(item));
  // ...
}

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

Давайте рассмотрим суть нашего алгоритма. Наш алгоритм работает итеративно — каждый раз он выводит в консоль порцию массива и переходит к остальным элементам, повторяя те же действия, пока все элементы массива не будут выведены на экран. Сейчас такое поведение реализовано с помощью setInterval и счётчика x, указывающего на индекс первого не выведенного в консоль элемента массива.

Такое же поведение мы можем реализовать намного проще. Нам не нужны вложенные функции, счётчики и setInterval — нам нужны рекурсия и setTimeout! Тело функции logArrayItemsSequentially будет обрабатывать только одну итерацию, а переход к следующей итерации будет делаться рекурсивным вызовом logArrayItemsSequentially. Разберём решение:

function logArrayItemsSequentially(array, options = {}) {
  const {
    itemsPerIteration = 10,
    interval = 1000,
  } = options;
  // элементы, которые нужно вывести в консоль на текущей итерации
  const currentPortion = array.slice(0, itemsPerIteration);
  // оставшиеся элементы
  const tail = array.slice(itemsPerIteration);

  // сразу делаем вывод в консоль
  currentPortion.forEach((item) => console.log(item));
  console.log('pause');

  // если остаток не пустой, передаём его в следующую итерацию,
  // выполнение которой откладываем с помощью setTimeout
  if (tail.length) {
    setTimeout(() => logArrayItemsSequentially(tail, options), interval);
  }
}

const sampleArray = Array.from({ length: 36 }, (value, index) => index);

logArrayItemsSequentially(sampleArray/*, здесь могли быть переопределённые настройки */);

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

/**
 * Исходное решение
 */

var myArr = ["Lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua", "Ut", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat"];
var i = myArr.length;
var x = 0;

var step = 10; // шаг смещения

function offset(x){
var y = x;
 while(x<y+step && x<i){
   console.log(myArr[x]);
   x++;
 };
};

function start(){
	offset(x);
 console.log('pause');
 x = x+step;
 if(x>i){ clearInterval(timer) };
};

var timer = setInterval(start, 1000);

/**
 * Отрефакторенное решение
 */

function logArrayItemsSequentially(array, options = {}) {
  const {
    itemsPerIteration = 10,
    interval = 1000,
  } = options;
  const currentPortion = array.slice(0, itemsPerIteration);
  const tail = array.slice(itemsPerIteration);

  currentPortion.forEach((item) => console.log(item));
  console.log('pause');

  if (tail.length) {
    setTimeout(() => logArrayItemsSequentially(tail, options), interval);
  }
}

const sampleArray = Array.from({ length: 36 }, (value, index) => index);

logArrayItemsSequentially(sampleArray/*, здесь могли быть переопределённые настройки */);

Мой опыт работы в Рамблере

Так вышло, что с 10 октября по 23 ноября 2016 года я работал фронтенд-разработчиком в отделе рекламных технологий Рамблера.

Впечатления от собеседования я описал в отдельной заметке.

Помимо меня в отделе было ещё семь фронтендеров: один джун, один тимлид и пять мидлов.

Я занимался поддержкой и развитием «Лета», системы для размещения баннеров на площадках RAMBLER&Co. Система была написана на ПХП, Реакте и Редаксе, ну и писалась она в довольно сжатые сроки (впрочем, так бывает всегда).

Про первый рабочий день

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

Бейдж-пропуск

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

Про работу

Нормально развернуть проект не получилось. Ну, формально получилось, но на деле локально не грузилось большинство картинок, потому что картинки хранились не в какой-нибудь общедоступной CDN, а непонятно где. Зачем-то прямо в репозитории хранились билд-файлы и композеровские пакеты, и всё это ломалось на каждый чих. С фронтендом всё тоже было не в порядке. В проекте не было Автопрефиксера! А команда, которая писала проект, никогда раньше не работала с Реактом и Редаксом, что вылилось в огромные компоненты длиной более чем в тысячу строк, в дебрях которых напрямую изменялось состояние и делались другие страшные вещи, о которых я рассказывать не буду, иначе вы сегодня не заснёте. Тестов почти не было (ну, была парочка на весь проект).

Кодревью чаще всего ограничивалось комментариями «ты здесь забыл убрать console.log». Сомневаюсь, что я писал настолько хороший код. В Иннове мой тимлид (спасибо, Антон) всегда оставлял комментарии не только про код, но и про решение в целом (архитектура, масштабируемость и все дела).

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

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

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

Про увольнение

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

Когда я сообщил о том, что собираюсь уходить, тимлид с техдиром долго уговаривали меня остаться и пытались понять, почему я ухожу. Я рассказал, что мне не понравилось и привёл в пример Яндекс и Авито, сказав, какие вещи там лучше. Кажется, я немного обидел техдира, когда он на вопрос «в Авито, по твоему мнению, специалисты опытнее наших?» получил ответ «да». Помимо прочего, мне предложили зарплату почти в полтора раза больше прежней, так что если вдруг захотите получать больше, сообщите начальнику, что собираетесь уволиться ;–).

Про условия работы в целом

Компания согласилась оплатить мой перелёт из Омска в Москву, что довольно неплохо. Правда по условиям трудового договора я был обязан вернуть деньги за билет, если проработаю в компании меньше года. В итоге выделенную на билет сумму просто вычли из моей последней зарплаты, ибо год я не проработал. Но и тут без фейлов не обошлось. Через какое-то время после увольнения мне позвонили и сказали, что вычли 10 000 ₽ (столько выделили на перелёт), а билет стоил 9 500 ₽, и, мол, мне нужно приехать в офис Рамблера и заполнить какую-то бумажку, чтобы получить обратно разницу.

В компании есть собственная библиотека, но воспользоваться ей мне не довелось.

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

Каждому разработчику выдают для работы макбук и внешний монитор.

Брендированные печеньки

«Кофе, чай, печеньки» — не ведитесь на это. Печеньки бывали только с утра и расходились они буквально за полчаса. Зато однажды печеньки были с логотипом компании! Что касается чая и кофе, на нашем этаже была кухня с двумя кофемашинами и двумя чайниками. Кофемашины периодически нужно было очищать от кофейной гущи, а ещё в них заканчивалась вода и нужно было самому доливать её туда из огромной бутыли, используемой обычно в кулерах. Здесь, конечно, можно сказать «да ты совсем зажрался», но я просто сравниваю с Яндексом или Авито, в них почему-то о таких вещах беспокоиться не приходится.

Где-то я слышал про бесплатные завтраки. Они оказались скидочной картой в соседнее кафе номиналом около 120 ₽ (точно не помню), на это можно было вроде как купить кашу и напиток. Эти карты были одноразовые, так что если вы желали завтракать каждый день, нужно было каждый день утром идти в другой корпус, чтобы получить карту.

Рабочее место Опенспейс

Рабочее пространство — опенспейс без перегородок между рабочими местами. Просто столы в ряд и разработчики с обеих сторон.

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

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

Результаты

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

Чему учиться в 20** году, если вы — фронтенд-разработчик

  1. Изучайте разные парадигмы программирования (императивное, функциональное, логическое, автоматное программирование). Не нужно следовать только одной парадигме — каждая из них может оказаться полезной в зависимости от задачи.
  2. Научитесь писать (и пишите!) тесты. Автоматическое тестирование:
    — намного дешевле ручного;
    — помогает выявлять баги ещё на этапе разработки;
    — вселяет в вас уверенность в своём коде;
    — поможет вам убедиться, что вы ничего не сломали очередным рефакторингом.
  3. Не изучайте новые фреймворки и библиотеки; изучайте подходы, лежащие в их основе.
  4. Изучайте структуры данных и связанные с ними алгоритмы: списки, графы (в частности деревья), битовые карты, хеш-таблицы. Это расширит ваш кругозор и вы станете видеть более эффективные и простые способы решения ежедневных задач.
  5. Учитесь думать о задачах в мире бизнеса, а не в мире разработки. Помните о том, что программистам платят за решение задач бизнеса, а не за количество написанных строчек кода. Думайте о бизнесе, прежде чем переписывать проект с нуля на очередном модном фреймворке. Думайте о бизнесе, когда нужно определиться со списком поддерживаемых браузеров — если клиенты бизнеса пользуются IE8, ваша работа поддержать его, а не выводить надпись «Ваш браузер устарел, обновитесь!!!».

Универсальный совет — изучайте фундаментальные, проверенные временем подходы и приёмы, а не меняющиеся каждые n месяцев инструменты.

О состоянии потока

Людвиг Быстроновский в одной из своих лекций сказал, что состояние потока — зло.

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

Возможные последствия этого — стресс, недосып, да и вообще забивание на всё, что не связано с потоком (семья и личная жизнь, правильное питание, здоровье и так далее).

Избежать этих проблем просто: каждой задаче нужно уделять небольшое количество времени, после чего обязательно делать перерыв. Людвиг уделяет каждой задаче не больше 15 минут в день (конечно этого мало, если начинать делать задачу за два дня до дедлайна, но он начинает заранее и занимается задачей каждый день не больше 15 минут).

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

Ctrl + ↓ Earlier