Техноведро
Модульная система. Часть 2

Рассказывает Андрей Ерес

Модульная система

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

Модульная система

Размеров коробок бывает сколько угодно: в среднюю можно вложить две мaленьких, а в каждую из них две ещё меньших — и так до бесконечности. Когда большая коробка собрана на уровне структуры — маленьких и средних коробок внутри — автор наполняет их вещами.

Такие «коробки» — ячейки, на которые делится страница книги, в бюро называют модулями. Все книги издательства свёрстаны на основе нашей модульной системы. Я расскажу, как она работает.

Чтобы посмотреть на коробки‑модули, нажмите в книге клавиши CTRL + G

Модульность в книгах бюро

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

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

Ещё в каждой книге бюро есть нестандартные модули — виджеты. В «Информационном стиле» это упражнения на редактуру, в «Управлении проектами, людьми и собой» — игры и анимации, в «Путешествии в шахматное королевство» — партии с настоящим шахматным движком. Именно виджеты делают каждую книгу уникальной.

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

Типографика и вёрстка. Стандартные модули страниц, абзацев, картинок, подписей и примечаний
Пользовательский интерфейс. Указатель в конце книги состоит из модулей рядов и колонок с абзацами, списками и картинками
Дизайн транспортных схем. При прокрутке в модуле «свитчере» меняется картинка‑пример
Информационный стиль. Текстовые пятнашки: предложения в абзаце нужно расставить так, чтобы привести его к структуре «введение, основная часть, заключение»
Управление проектами, людьми и собой. Анимированный путь из А в Б показывает, как реальная работа над проектом отличается от плана
Путешествие в шахматное королевство. Упражнения в электронном учебнике — это полноценная игра. В книгу встроен шахматный движок, который анализирует ходы читателя в реальном времени

Жизненный цикл книжных модулей

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

Базовый модуль нельзя модифицировать, но можно расширять. Это значит, что наследуемые модули не могут переназначить стандартные методы вроде 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 уже управляет вложенными модулями.

Основные перерасчёты в модулях происходят при загрузке книг и изменении размера браузера.

Читатель открывает книгу:

init()       // Загружаем модули
calculate()  // Рассчитываем стили
render()     // Применяем стили

Читатель изменил размер окна:

reset()      // Сбрасываем стили
calculate()  // Рассчитываем стили для нового размера окна
render()     // Применяем стили

Книжные модули независимы, но неравноправны. По приоритету загрузки они делятся на три группы:

  1. affectingModules — модули, которые могут увеличить высоту разворота. Их нужно отрисовать первыми, иначе они рассчитаются неверно и повлияют на размеры остальных модулей. Если в модуле айфона из «Как написать» убрать флаг isAffectingHeight = true, разворот загрузится без учёта минимальной пропорции модуля и айфон сожмётся.

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

  3. 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 мы выделяем особые модули — реактивные (с Реактом ничего общего они не имеют). Эти модули должны непрерывно реагировать на скрол, как в анимации скруглений из «Дизайна транспортных схем».

Изгибы
Демоглава книги «Дизайн транспортных схем»

Работа реактивных модулей заслуживает отдельного техноведра, поэтому мы напишем о них позже.

TL;DR

  1. Книги бюро собраны из независимых модулей.

  2. Книжные развороты управляют вложенными модулями через единый интерфейс.

  3. Модули поддерживают иерархию расчёта и отрисовки.

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

  5. Модули на фреймворках встраиваются в общий интерфейс через модули‑обёртки.

  6. Есть ещё реактивные модули, но о них в следующий раз.

Отправить
Поделиться
Запинить