Устройство модульной системы
Модульная вёрстка похожа на раскладку вещей по коробкам. Если представить, что разворот книги — это большая коробка, то авторы сначала наполняют её маленькими коробками. В одной коробке будет текст, в другой — картинка, в третьей — подпись. Если часть коробок нужно отделить от остальных, автор складывает их в общую среднюю коробку и затем уже вкладывает в большую.
Устройство модульной системы
Размеров коробок бывает сколько угодно: в среднюю можно вложить две мaленьких, а в каждую из них две ещё меньших — и так до бесконечности. Когда большая коробка собрана на уровне структуры — маленьких и средних коробок внутри — автор наполняет их вещами.
Такие «коробки» — ячейки, на которые делится страница книги, в бюро называют модулями. Все книги издательства свёрстаны на основе нашей модульной системы. Я расскажу, как она работает.
Артём Горбунов
Что такое модульная вёрстка
Йозеф Мюллер‑Брокманн
Модульные системы в графическом дизайне
Реакт
Components and Props
Модульность — это одновременно принцип и вёрстки, и разработки книг бюро. В вёрстке прямоугольник делится на любое число прямоугольников любых пропорций. Вложенные прямоугольники делятся снова, и так до бесконечности. В разработке любой модуль может состоять из любого числа других модулей. Это и есть принцип модульности — организация целого из частей.
Артём Горбунов
Что такое модульная вёрстка
Йозеф Мюллер‑Брокманн
Модульные системы в графическом дизайне
Реакт
Components and Props
Книги бюро собраны из независимых модулей, таких как абзацы, колонки, примечания, картинки и подпиcи. Все модули могут вкладываться друг в друга. В развороты вложены страницы, в страницы — абзацы, картинки или колонки, а в колонках — снова абзацы или картинки. Внутрь абзаца можно вложить примечание, и оно встанет на поля книги. Всё это — стандартные модули, которые одинаково работают во всех книгах.
Ещё в каждой книге бюро есть нестандартные модули — виджеты. В «Информационном стиле» это упражнения на редактуру, в «Управлении проектами, людьми и собой» — игры и анимации, в «Путешествии в шахматное королевство» — партии с настоящим шахматным движком. Именно виджеты делают каждую книгу уникальной.
Как и стандартные модули, виджеты тоже во всех книгах работают одинаково. Если в следующей книге мы захотим совместить шахматные партии с заданиями на редактуру, нам это ничего не будет стоить. Мы просто вызовем нужный виджет в коде книжного разворота.
Чтобы обеспечить общий интерфейс для всех книжных модулей, мы наследуем их от базового модуля. В базовом модуле описан жизненный цикл модулей от инициализации до отрисовки:
init()
— инициализация, поиск ДОМ‑нод, начальные расчёты без измерений макета.
reset()
— сброс стилей после изменения размера браузера.
calculate()
— измерение макета и расчёты.
render()
— запись в ДОМ.
Базовый модуль нельзя модифицировать, но можно расширять. Это значит, что наследуемые модули не могут переназначить стандартные методы вроде render()
. Вместо них мы пишем для каждого модуля соответствующие методы с префиксами pre
и post
: preRender()
и postRender()
.
В книге «Как написать» мы показываем письма внутри айфонов. Чтобы айфоны с небольшими письмами не превращались в Эпл‑вотч, мы написали небольшой модуль для контроля минимальной пропорции:
{ const BaseModule = require('_base/base') // Задаём минимальную пропорцию айфона const IPHONE_MIN_RATIO = 2.04 class Iphone extends BaseModule { preInit() { // Указываем, что модуль влияет // на высоту разворота this.isAffectingHeight = true } preReset() { // Сбрасываем высоту айфона // при изменении размера разворота this.$el.css('min-height', 'auto') } preCalculate() { // Измеряем ширину айфона и рассчитываем // соответствующую минимальную высоту this.width = this.$el.outerWidth() this.minHeight = this.width * IPHONE_MIN_RATIO } preRender() { // Применяем стиль к айфону this.$el.css('min-height', this.minHeight) } } // Проходимся по ДОМ-нодам // и создаём виджеты с айфонами $('.js__iphone').each(function() { new Iphone($(this)) }) }
За работу и координацию модулей отвечает движок: модули Book
и Spread
. Book
запускает книгу и распределяет развороты — модули Spread
. А Spread
уже управляет вложенными модулями.
Основные перерасчёты в модулях происходят при загрузке книг и изменении размера браузера.
Читатель открывает книгу:
Book
устанавливает минимальную высоту разворотов, равную окну браузера.
Spread
инициализирует свои модули и отдаёт команду рассчитать и применить стили.
init() // Загружаем модули calculate() // Рассчитываем стили render() // Применяем стили
Читатель изменил размер окна:
Book
устанавливает новый размер разворотов.
Spread
отдаёт команду сбросить предыдущие стили, заново рассчитать и применить стили для нового размера окна.
reset() // Сбрасываем стили calculate() // Рассчитываем стили для нового размера окна render() // Применяем стили
Книжные модули независимы, но неравноправны. По приоритету загрузки они делятся на три группы:
affectingModules
— модули, которые могут увеличить высоту разворота. Их нужно отрисовать первыми, иначе они рассчитаются неверно и повлияют на размеры остальных модулей. Если в модуле айфона из «Как написать» убрать флаг isAffectingHeight = true
, разворот загрузится без учёта минимальной пропорции модуля и айфон сожмётся.
finalModules
— модули, которые зависят от позиции и размеров других модулей. Поэтому их нужно рассчитывать и отрисовывать в последнюю очередь. Пока у нас только один такой модуль — заметка на полях.
dependentModules
— все остальные модули.
Приоритет загрузки учитывается в командах, которые отдаёт модулям Spread
:
Spread.prototype.reset() { // Сбрасываем все стили this.resetModules(this.affectingModules) this.resetModules(this.dependentModules) this.resetModules(this.finalModules) } Spread.prototype.calculate() { // Сначала рассчитываем и отрисовываем модули, // влияющие на высоту разворота this.calculateModules(this.affectingModules) this.renderModules(this.affectingModules) // Потом рассчитываем остальные модули this.calculateModules(this.dependentModules) } Spread.prototype.render() { // Отрисовываем остальные модули this.renderModules(this.dependentModules) // Когда всё отрисовано, рассчитываем // и отрисовываем финальные модули this.calculateModules(this.finalModules) this.renderModules(this.finalModules) }
Некоторые модули для правильной отрисовки должны учитывать размеры вложенных модулей:
.module // Если module расcчитается .children // перед любым из children, .children // разворот отрисуется неправильно
Такие модули мы называем родительскими. Чтобы обеспечить правильный порядок отрисовки, родительские модули перехватывают у Spread
контроль над вложенными модулями — сабмодулями:
// При инициализации вложенного модуля // не даём событию подняться до разворота // и регистрируем модуль в subModules if (isMother) { this.$el.on('moduleInit', (e, subModule) => { e.stopPropagation() this.subModules.push(subModule) }) }
После этого сабмодули работают не вразнобой, а как решит родительский модуль. Например, EvenwidthImagesModule
устанавливает вложенным картинкам одинаковую ширину:
class EvenwidthImagesModule extends BaseModule { preInit() { // Обозначаем модуль как родительский this.isMother = true } postInit() { // Подготавливаем массив для модулей // с одинаковой шириной this.evenModules = [] } preCalculate() { // Собираем evenModules this.findEvenSubModules(this) // ... } findEvenSubModules(rootModule) { // Выделяем из сабмодулей // модули с одинаковой шириной rootModule.subModules.forEach(subModule => { if (subModule.$el.is('.is__evenwidth')) { this.evenModules.push(subModule) } // Обрабатываем вложенные сабмодули this.findEvenSubModules(subModule) }) } postCalculate() { this.evenModules.forEach(subModule => { // Рассчитываем ширину evenModules // с учётом ограничений subModule.calculateWithConstraints() } } // ... }
Модули, которые я описывал выше, влияют на поведение и вид разворотов. Они написаны на чистом Яваскрипте (с примесью Джейквери) и работают со стилями на странице. Но ещё мы используем интерактивные виджеты, написанные на фреймворке Преакт.
Преактивные модули встраиваются в общий интерфейс через модуль‑обёртку. Обёртка слушает команды от разворота, рассчитывает данные и монтирует преактивный компонент.
Так работают шахматные доски в «Путешествии в шахматное королевство»:
// chessExampleWrapper.js class ChessExampleWrapper extends BaseModule { preInit() { // Расcчитываем и собираем данные this.data = {} } postInit() { // Отдаём команду за загрузку if (!this.unloadable) this.load() } load() { // Монтируем компонент ChessExample this.component = reactify( this.$el, ChessExample, { ...this.data } ) } // ... }
// chessExample.js import { h, Component } from 'preact' // ChessExample — обычный компонент на Преакте class ChessExample extends Component { // ... }
Среди dependentModules
мы выделяем особые модули — реактивные (с Реактом ничего общего они не имеют). Эти модули должны непрерывно реагировать на скрол, как в анимации скруглений из «Дизайна транспортных схем».
Работа реактивных модулей заслуживает отдельного техноведра, поэтому мы напишем о них позже.
Книги бюро собраны из независимых модулей.
Книжные развороты управляют вложенными модулями через единый интерфейс.
Модули поддерживают иерархию расчёта и отрисовки.
Некоторые модули для правильной отрисовки перехватывают у разворотов контроль над сабмодулями.
Модули на фреймворках встраиваются в общий интерфейс через модули‑обёртки.
Есть ещё реактивные модули, но о них в следующий раз.