4 posts tagged

javascript

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

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

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

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

2017   html   javascript   tl;dr   доступность

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

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

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.

2017   javascript   БЭМ   ДАМП   компоненты   работа

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

Скачивание изображений в IE10+

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

К счастью, в спецификации HTML5 появился новый атрибут “download”, указывающий браузеру на то, что ресурс по ссылке следует скачать. К несчастью, этот атрибут не поддерживается в Сафари и IE.

Для IE удалось найти относительно простое альтернативное решение, основанное на блобах:

document.querySelector('a[download]').addEventListener('click', (event) => {
  // следующий код сработает только в IE10+
  if (navigator && navigator.msSaveOrOpenBlob) {
    event.preventDefault();
    const href = event.target.href;
    const xhr = new XMLHttpRequest();

    xhr.open('GET', href, true);
    xhr.responseType = 'blob';

    xhr.onload = () => {
      const filename = href.replace(/^.*\//g, '');
      return navigator.msSaveOrOpenBlob(xhr.response, filename);
    };

    xhr.send();
  }
});
2016   javascript   работа