4 заметки с тегом

код

Выбирайте правильные инструменты

Разработчики, которые имели медленный сайт или не имели его, с популярностью React переводят сайты на Gatsby, а потом радостно пишут в Твиттер о 90-100 в Lighthouse и рекомендуют перенести старый WordPress-блог на новый JS-фреймворк. И даже те, кто имел статический сайт на HTML с 70 в Lighthouse, переписывают его на Gatsby, который оптимизирует сайт за них и выдает хорошие цифры в тестах.

Проблема в том, что вам скорее всего не нужен JavaScript для личного блога. Gatsby крут плагинами, но всё, что нужно от него, — генерация страниц из Markdown. Клиент не хочет видеть JS-бандл. Gatsby нужен для сложных сайтов с анимациями и интерактивом, например, для этого блога — он издает звуки, персонажи двигаются, а примеры кода запускаются на месте. React с его JSX, где интерактив легко встраивается в текст, идеально здесь подходит, а еще у Gatsby хорошая поддержка MDX, чтоб делать такое же с Markdown.

Ситуация описывается так:

@twanttobealighi: реакт делает сложное простым, а простое невозможным

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

Возьмите полную статику: Eleventy, — используйте плагины для сжатия кода и картинок или напишите свой код в десять строк и дайте клиенту минималистичный набор HTML + CSS. Это тоже даст 100 в Lighthouse, но ниже буду написаны цифры, более глубоко описывающие скорость загрузки: первая отрисовка, время до взаимодействия с сайтом, самый крупный элемент сайта, — которые будут в разы меньше, а в сложных тестах вы наберете лучший результат.

А пишу я это потому, что у меня тоже сперва была полная статика с Jekyll, а потом я поддался количеству плагинов в Gatsby и автоматической оптимизации изображений. Но недавно я переписал сайт на Eleventy за три дня. Выбирайте правильные инструменты.

Y-комбинатор

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

Рассмотрим Y-комбинатор на примере Scheme — диалекта Lisp. Тестовой функцией будет length, которая измеряет длину списка. Для начала объясню как рабоатет сама функция.

(define length
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l)))))))

С каждым блоком кода открываются скобки, а потом они закрываются. Почти как {} в JavaScript. Разберем по строкам.

  1. define lengthdefine определяет называние функции, как если бы мы написали function length в JavaScript или def length в Python.
  2. lambda (l) — определяет безымянную функцию и её аргументы. В данном случаи аргумент один — l — в функцию мы передадим список. Передавая безымянную функцию в первую строку, мы определяем для нее имя. Первые две строки — аналог function length(l) в JavaScript и def length(l) в Python.
  3. cond — начало условия. Это как if, только туда передается много строк условий и действий подряд и в конце else.
  4. В каждой следующей строке после cond идет блок кода с условием и действием. Как ((null? l) 0). Читается так: ((если это) то это). А null? — это функция, которая проверяет список, который мы ей передаём, на пустоту.
  5. add1 — функция, которая добавляет 1; её надо писать самостоятельно. cdr — функция, которая возвращает список, который мы передаём, но без первого элемента.

В общем виде функция работает так:

  1. проверяем список на пустоту,
  2. если пустой — возвращаем 0, если нет, идем дальше,
  3. добавляем 1 к рекурсивному запуску фукнции,
  4. запускаем функцию со списком, который меньше на один элемент,

Пока список не будет пуст, мы будем добавлять единицу, когда он останется пуст, вернётся 0 из ((null? l) 0) и шаг за шагом в обратном направлении добавится по единице.

Если непонятно, то лучше пропустить какой-нибудь список через фукнцию. Например (lisp is great). Функция будет запускаться рекурсивно, удаляя по одному элементу из начала: (lisp is great), (is great), (great), (). Теперь можно приступать к комбинатору неподвижной точки.

Сперва попробуем избавиться от define. Функция будет безымянной. В Lisp есть синтаксис мгновенного объявления и вызова функции, как и в JavaScript. В скобки передается безымянная лямба-функция и следом аргумент:

((lambda (length)
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l)))))))
eternity)

Две лямбды означают, что мы создаём функцию, которая возвращает функцию, готовую принять другой аргумент. eternity просто пустышка, которая подставляется вместо аргумента. Такая функция будет очень похожа на length, но рекурсивного вызова не будет, потому что у функции нет имени и мы не знаем, как ее вызвать. Но если передать туда пустой список, то сработает первое условие — (null? l) и функция вернёт 0. То есть для пустого списка всё ок.

((lambda (f)
(lambda (l)
(cond
((null? l) 0)
(else (add1 (f (cdr l))))))) ; здесь будет вторая фукнция
((lambda (g)
(lambda (l) ; здесь будет cdr l
(cond
((null? l) 0)
(else (add1 (g (cdr l))))))) ; здесь будет eternity
eternity))

Теперь передадим вместо eternity другую функцию. В первую функцию передается вторая, а в неё — eternity. Теперь мы можем передать список с одним элементом. Выполнится else, вызовется вторая функция, которая такая же, как и первая, и там мы уже остановимся. А можно так добавлять бесконечно. Но код будет не универсальным, а писать так много неудобно. Перейдем к другому определению функции.

((lambda (mk-length) ; (1)
(mk-length eternity))
(lambda (length) ; (2)
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l))))))))

Теперь у нас есть отдельная функция (первая), которая возвращает похожую на length функцию. Функция (2) попадет в лямбду (1) как аругмент mk-length. Внутри происходит опять объявление с вызовом. В функцию (2) подставляется eternity вместо length.

Теперь повторим прошлый код с новым синтаксисом подстановки, передадим в функцию (2) вместо eternity саму функцию.

((lambda (mk-length) ; (1)
(mk-length
(mk-length eternity)))
(lambda (length) ; (2)
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l))))))))

Здесь мы передаем eternity в функцию, как и в прошлый раз, и всю эту конструкцию снова передаем в функцию. Уберем eternity.

((lambda (mk-length) ; (1)
(mk-length mk-length))
(lambda (length) ; (2)
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l))))))))

Функция (2) передается в функцию (1) и получает там имя mk-length, а потом передается в себя в строке (mk-length mk-length). Вернем eternity, но теперь в другое место, положим его прямо внутрь функции.

((lambda (mk-length) ; (1)
(mk-length mk-length))
(lambda (length) ; (2)
(lambda (l)
(cond
((null? l) 0)
(else (add1 ((length eternity) (cdr l))))))))

Теперь опять заменим eternity на функцию, мы уже делали так.

((lambda (mk-length) ; (1)
(mk-length mk-length))
(lambda (length) ; (2)
(lambda (l)
(cond
((null? l) 0)
(else (add1 ((length length) (cdr l))))))))

В функциональном программировании используется прием eta-reduction. Он позволяет заменить (lambda (x) (function x)) на function, потому что в первом варианте мы передаем аргумент в функцию, которая находится в ожидании, во втором варианте функция также ждет аргумент, который можно ей передать. Если мы передадим любое число в оба варианта, то функция получит число и там, и там.

Сделаем обратную замену: подставим вместо (length length) функцию (lambda (x) ((length length) x)). Здесь (length length) просто функция.

((lambda (mk-length) ; (1)
(mk-length mk-length))
(lambda (length) ; (2)
(lambda (l)
(cond
((null? l) 0)
(else (add1 ((lambda (x) ((length length) x))
(cdr l))))))))

Вытащим новую конструкцию за функцию.

((lambda (mk-length)
(mk-length mk-length))
(lambda (mk-length) ; новая функция-обертка
((lambda (length) ; подстановка аргумента
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l)))))))
(lambda (x) ; аргумент
((mk-length mk-length) x)))))

На 4 строке появились двойные скобки: это значит, что мы в функцию передаем аргумент. Выделенный фрагмент — прошлая функция, куда мы передаем последние две строки. Если подставить (lambda (x) ((length length) x)) вместо length в четвертой строке, то получим функцию из предыдущего блока. Всю эту конструкцию мы обернули в новую фукнцию.

Вытащим выделенный фрагмент в отдельный аргумент.

((lambda (le)
((lambda (mk-length)
(mk-length mk-length))
(lambda (mk-length)
(le (lambda (x)
((mk-length mk-length) x))))))
(lambda (length)
(lambda (l)
(cond
((null? l) 0)
(else (add1 (length (cdr l))))))))

Выделенный фрагмент — функция, похожая на изначальный length. Она передается в первую функцию как le. Остальное — часть, которая выполняет рекурсию. Её можно определить с помощью define.

(define Y
(lambda (le)
((lambda (f) (f f))
(lambda (f)
(le (lambda (x) ((f f) x)))))))

Здесь le — функция, которую мы хотим вызывать рекурсивно, x — ее аргумент, а f — внутреннее имя функция для применения функции к себе. Эта часть называется Y-комбинатор. Их существует несколько видов, но этот основной. Более сложное, но не менее интересное, объяснение есть в книге The Little Schemer, а еще один вид комбинатора есть во второй части — The Seasoned Schemer. Это интересные и веселые книги по функциональному программированию, которые стоит прочитать навичкам в этом разделе.

Автоматическая темная тема для сайта

Не буду доказывать, что темная тема нужна, просто вот:

  1. Apple: темная тема в iOS
  2. Apple: темная тема в macOS
  3. Google: темная тема в Material Design
  4. Microsoft: темная тема в Windows

Темные темы пришли в веб. Теперь Safari, Chrome, Firefox и другие популярные браузеры поддерживают темную тему для сайта. Используем это. Сейчас расскажу как сделать темную тему, чтоб никому не было стыдно.

Светлый и темный экраны. Попробуйте открыть эту картинку в темноте — второй вариант будет смотреться лучше
Светлый и темный экраны. Попробуйте открыть эту картинку в темноте — второй вариант будет смотреться лучше

Отправная точка — этот сайт. Можете его полистать. Если он у белый, попробуйте включить темную тему на телефоне или компьютере. Здесь уже есть поддержка темной темы, но я вернусь назад — во время разработки.

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

Логика

Есть: светлая тема
Будет: основа для работы темной темы

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

CSS

Есть: обычный CSS-код
Будет: переменные, чтоб менять всё в одном месте

Идем в CSS и понимаем, что давно надо перейти на переменные. Прописываем правила с переменными и потом их применяем:

:root {
/* Создаю переменные с цветами для документа (root) */
--background-color: #fafafa;
--foreground-color: #121212;
--accent-color: #ff5252;
}

div {
color: var(--foreground-color);
background-color: var(--background-color);
}

a:hover {
color: var(--accent-color);
}

Гугл:

“Use dark grey – rather than black – to express elevation and space in an environment with a wider range of depth. Apply limited color…”

— уже давно рассказал, что выбирать черный (#000000) для темной темы не стоит — лучше выбрать темно-серый. Устанавливаем цвета для темной темы. Если их больше, чем три — фон, текст и акцентный, — то просто менять местами не подойдет, надо изучать поведение цвета.

@media screen and (prefers-color-scheme: dark) {
:root {
/* Перезаписываю переменные с цветами для темной темы */
--background-color: #121212;
--foreground-color: #fafafa;
}
}

Работаем с цветом

Есть: обычная смена цветов местами
Будет: замена цветов на другие

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

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

Темная и светлая темы в блоге
Темная и светлая темы в блоге

Для проверки контраста в Chrome сделали приспособление века в инструментах разработчика: открываем инструменты разработчика (Ctrl|Cmd + Shift + I), включаем наведение (Ctrl|Cmd + Shift + C), наводим на текст и видим сразу маленькое окошко, где показана контрастность. Если в стилях нажать на цвет элемента, то появится точная контрастность и линии, помогающие ее настроить.

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

Суммируя: переменные, правильные цвета, медиа-стили.

Слишком-код

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

Есть замечательная библиотека date-fns, которая заменяет Moment.js в работе с датами в Джаваскрипте. Я ее использую в блоге для отображения дат заметок. Недавно вышла вторая версия, которая в три раза больше первой. На самом деле, раньше библиотека была «слишком-сумкой», которая подгружала вместе с собой все свои функции: локализацию, форматирование, дистанцию, форматирование дистанции, проверку на нынешний день, неделю, месяц, год, будущее или прошлое и т. д. А сейчас библиотека стала маленькой, потому что можно выбирать только нужные функции — выбирать только нужные карманы.

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