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

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

Какие каналы читать в Телеграме

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

For Web — полезности для разработчиков интерфейсов: фронтенд, дизайн и программирование. Признаюсь, слукавил: этот канал я не читаю, я в него пишу.

Веб-стандарты — ежедневные новости и события фронтенда.

Breakfast.js — ежедневная утренняя порция фронтенд-новостей от Дмитрия Мананникова.

devSchachtChannel — анонсы новых статей и подкастов от devSchacht.

Иван Акулов про фронтенд — заметки про фронтенд, UX и смежные темы.

webo_ru — Виталий Харисов из Яндекса об оптимизации сайтов. Есть английская версия.

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

Про руководство разработчиками — руководитель службы разработки интерфейсов Олег Мохов из екатеринбуржского Яндекса о своей работе и её особенностях.

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

Всё о программировании (с Козулей) — Владислав Козуля о карьере, стартапах и своём опыте разработки.

isqualog — Софья Ильинова о фронтенде, дизайне, работе и жизни.

Internet 9000 — Сергей Сурганов о дизайне и технологиях.

TechSparks — Андрей Себрант с новостями хайтека.

Записки Коха — Артур Кох о email-разработке и смежных темах.

Evil Martians — Злые марсиане о стартапах, веб-разработке, интернет-бизнесе, бэкенде, фронтенде, мобильной разработке, devops и data science.

запуск завтра — техдир «Медузы» Самат Галимов о своих буднях и технологиях.

Про Сидней/Atlassian — Дмитрий Сорин об опыте переезда в Австралию и работе в Atlassian.

Интерфейсы без шелухи — Антон Жиянов о продуктоводстве, интерфейсах, здравом смысле и разработке софта.

Чёт приуныл — Ян Хацкевич с анонсами новых статей из своего личного блога о дизайне.

Design without cats — Андрей Болонев о дизайне без котиков. Мысли, цитаты из статей, реальные кейсы.

Design & Productivity — Костя Горский про дизайн, продуктивность и жизнь. Мало ссылок и много мыслей.

GOV — о дизайне государства.

Канал Ильи Бирмана — заметки, находки и советы о дизайне.

Л — Людвиг Быстроновский о дизайне, искусстве, продуктивности.

Бюро Горбунова — дизайн-советы, наблюдения и книги.

No Flame No Game — Аня Булдакова о продуктовом менеджменте.

Главред — Максим Ильяхов с советами и статьями о тексте, редактуре, информационном стиле и рекламе.

ГЗОМ — о тексте, тонкостях и пунктирностях русского языка, грамматике и стилистике, работе редактора, работе мозга — затейливо и с внятной аргументацией.

Мильчин-канал — инъекции «Справочника издателя и автора» Аркадия Мильчина и Людмилы Чельцовой. Канал для редакторов, дизайнеров и зануд.

Исправил — Никита Юкович о русском языке и о том, как не ошибаться.

Angry Translator — Даниил Орловский о тонкостях английской грамматики, типичных и нетипичных ошибках, лексике.

Сразу нет — Иван Колпаков из «Медузы» о журналистике.

Разумная внимательность — Рахим Давлеткалиев об осознанности, медитации, разуме и реальности.

За бугром — размышления иммигранта о жизни в США.

Планктон челлендж — вся Европа за год, будучи офисным планктоном. Сергей Плащинский с фотографиями, видео и отчётами о поездках.

Че Ченел — Илья и Стася Чекальские о путешествиях, переездах и жизни в Польше.

Тинькофф-журнал — новые cтатьи об управлении деньгами. Как экономить, вкладывать, защищать свои права и общаться с банками.

А что читаете вы?

Визуальная семантика, или чем разметка отличается от стилей

Иногда новички во фронтенде не понимают истинного предназначения разметки (HTML) и стилей (CSS). Однажды в комментариях к одной из публикаций Форвеба возник вопрос:

Что в 2017 лучше использовать для вёрстки таблиц, HTML-таблицы или CSS-гриды?

Что не так с этим вопросом? Давайте разберёмся.

Разметка — про семантику

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

Стили — про внешний вид

Голый текст это хорошо, но ещё лучше, когда его можно оформить — задать размер шрифта и интерлиньяж, ограничить ширину, выделить заголовки, покрасить ссылки в нужный цвет. За всю эту внешнюю красоту отвечают стили (CSS), и они чаще всего никак не влияют на семантику, то есть на смысл текста. Они лишь дают возможность изменить его внешний вид.
Хороший пример — CSS Zen Garden. Один и тот же HTML-документ, смысл которого не меняется, оформлен более чем двумястами разными способами.

Если разметка не отвечает за визуальное представление, что в HTML делают элементы вроде <i> или <strong>?

Дело в том, что эти элементы тоже несут смысл. Например, так определяется элемент <strong> в спецификации HTML:

Элемент <strong> означает важность, серьёзность или срочность его содержимого.

Заметьте, ни слова не сказано о выделении содержимого жирным начертанием шрифта. Жирное начертание для <b> и <strong>, фоновое выделение для <mark> и другое подобное оформление задаётся стандартыми стилями браузера (user-agent stylesheet). Эти стили никак не относятся к семантике разметки. Более того, в разных условиях одни и те же HTML-элементы могут быть визуально представлены по-разному (например, элементы форм на Windows и macOS). Несмотря на разный внешний вид, кнопки на Windows и кнопки на macOS несут один и тот же смысл.

Разделяйте семантику и представление

Вернёмся к вопросу из начала статьи:

Что в 2017 лучше использовать для вёрстки таблиц, HTML-таблицы или CSS-гриды?

Таблицы нужно размечать как таблицы, то есть тегами <table>, <td> и так далее. Как они будут выглядеть на экране у пользователя — дело ваше, вы полностью контролируете их внешний вид через стили. Ничего не мешает разметить HTML-таблицу и стилизовать её CSS-гридами или флексами.

Запомните: HTML — про семантику, CSS — про внешний вид.

Что за атрибут inert и зачем он нужен?

Как можно догадаться из названия, этот атрибут помечает элемент как инертный (или неактивный, но не путайте с disabled). Для таких элементов (и всего дерева их потомков) отключается срабатывание пользовательских событий (например, фокус по нажатию tab, выделение текста, клики). Ассистивные технологии вроде экранных читалок просто игнорируют такие элементы. Также спецификация рекомендует разработчикам браузеров игнорировать инертные элементы при поиске по содержимому страницы.

По сути, этот атрибут сочетает в себе поведение tabindex=”-1”, aria-hidden и pointer-events: none. Им следует помечать скрытые модальные окна, выпадающие меню, невидимые слайды карусели и другие подобные элементы интерфейса. Это улучшит доступность ваших интерфейсов: при навигации с клавиатуры или при использовании экранных читалок инертные элементы просто будут игнорироваться.

Полезные ссылки:
— атрибут inert в спецификации whatwg;
история появления, способы применения, описание пробелов в спецификации и полифил;
выпуск A11ycasts с Робом Додсоном, посвящённый атрибуту inert.

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

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

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/*, здесь могли быть переопределённые настройки */);
Ctrl + ↓ Earlier