Введение в $mol. Часть 1. Модульная система

Эта статья открывает серию публикаций по обучению фреймворку $mol. Сегодня мы разберемся в модульной системе MAM. Речь пойдет об организации кода, отделении его от инфраструктуры, сборке, версионировании, нейминге, минимизации размера бандла, автоматическом выкачивании зависимостей, фрактальных моно-поли-репозиториях, разделении кода на платформы, альтернативе импортам/экспортам, автоматическом разруливании циклических зависимостей.
$mol — высокоуровневый веб-фреймворк, который переосмысливает общепринятые подходы. Одновременно с этим это набор узкоспециализированных, подогнанных друг к другу модулей. Популярные высокоуровневые фреймворки ассоциируются с неповоротливостью и сложной кастомизацией. $mol спроектирован так, что можно быстро собрать интерфейс как на высокоуровневом фреймворке, при этом гибкость кастомизации превосходит низкоуровневые фреймворки.
Для кого-то богатство экосистемы фреймворка является определяющим фактором. В $mol много модулей, они закрывают большинство потребностей. Недостающие можно собрать из существующих или использовать npm.
Как это не парадоксально звучит, но чем больше у вас опыта, тем сложнее освоить $mol. В нем используется много непривычных подходов, которые при первом знакомстве вызывают отторжение. Но стоит их переварить и становится непонятно, как жилось без этого раньше.
Модульная система MAM
MAM — это модульная система, в которой живут модули $mol. Абстрактный модуль — это директория с файлами, которые его реализуют. MAM — это набор правил, ограничений и принципов, которые превращают код в кубики LEGO.
Изучение $mol, лучше начинать с MAM. Разберитесь как использовать модули на практике, а затем приступите к знакомству с системой реактивности.
Реактивность
Если вы знаете, как работает Mobx, то уже представляете как работает система реактивности в $mol. Тут она используется на всех уровнях, а не только на уровне представления.
В отличие от архитектуры потока управления, в $mol реализована архитектура потока данных. Приложение определяется как набор классов с реактивными свойствами. Свойство представляет собой метод с реактивным декоратором. Каждое свойство определяется как некоторая функция от других свойств. Результат выполнения свойств кэшируется.
При вызове, реактивное свойство запоминают к каким свойствам оно обращалось во время выполнения, и кто к нему обращался.
Когда какое-то свойство меняет свое значение, зависимые от него свойства помечаются устаревшими. Если получить значение устаревшего свойства, произойдет актуализация помеченного поддерева и свойство вернет актуальный результат. Если обратиться к свойству в актуальном состоянии, оно вернет результат из кэша.
Всё приложение на этапе выполнения представляет собой большое дерево зависимостей. Каждое свойство знает свои зависимости, это дает простой и надежный механизм управления жизненным циклом объектов — они создаются, когда появляется зависимость, и уничтожаются, когда от них ничего не зависит. Это решает две фундаментальные проблемы: утечки памяти и инвалидацию кэша.
View-компоненты
После того как вы разберетесь с реактивностью, можно переходить к view-компонентам, из них строится пользовательский интерфейс.
В $mol компонент состоит из нескольких частей, а каждая часть, находится в отдельном файле, можно выделить 5 частей:
Декларативное описание интерфейса компонента и потоков данных (обязательное, остальное опционально)
Императивное поведение компонента
Стили
Локализация
Тесты
Есть базовый класс $mol_view с набором свойств, такими как события, атрибуты, дети и т.д. Он является оберткой над одним DOM-элементом. Когда любое из его свойств становится неактуальным, реактивная система автоматически вызывает его актуализацию, что точечно обновляет элемент в DOM-дереве.
Пользовательские компоненты, наследуются от базового класса, и настраивают его компоновку и поведение.
Для описания компонент используется язык view.tree. С виду он выглядит сложными и нечитаемым, но это только с первого взгляда, точно такое же впечатление производит html, когда видишь его впервые. Но view.tree не html, там всего порядка 10 операторов, на изучение которых необходимо несколько часов. Он не является шаблоном в общепринятом понимании, это ближе к интерфейсам в typescript.
С помощью view.tree мы говорим:
как называется наш компонент
от какого компонента он наследуется
какими компонентами он владеет
как эти компоненты компонуются в его
childrenкакие состояния имеет компонент
какими потоками данных связаны компоненты и состояния
Сборщик из view.tree описания генерирует класс, от которого можно отнаследоваться и добавить поведение.
Часть 1. Модульная система MAM
MAM — модульная система, в которой переосмыслена работа с кодом и его организация. Она проектировалась для единообразной работы с произвольными объемами кода, облегчая переиспользование и минимизируя рутину.
MAM расшифровывается как Mam owns Abstract Modules.
Идеи и концепции
Модуль
Модуль — директория, внутри которой находятся файлы реализующие его. Разные части модуля находятся в разных файлах. Абстрактный — значит, что модуль не привязан к какому-то конкретному языку, а может быть реализован на нескольких. Модуль первичен, а технологии на которых он реализован — вторичны.
Особенности:
Один модуль — одна директория
Модули произвольно вкладываются друг в друга, все есть модуль
Имя модуля — путь до него в файловой системе
Модуль может выступать пространством имен — содержать только директории
Зависимости между модулями отслеживаются автоматически
Разные типы исходников модуля, попадают в разные бандлы
Можно провести аналогию с БЭМ методологией, там используется термин «блок», первичен блок, его реализация вторична.
Соглашения вместо конфигурации
В MAM нет пользовательского конфига, вместо него используются соглашения. Соглашение можно рассматривать как некое условие, которое нужно выполнить, чтобы получить требуемый результат.
С одной стороны это позволяет просто установить MAM и стартовать проект без дополнительных действий, т.к. уже все настроено. С другой стороны, отсутствие конфигов, позволяет независимым разработчикам писать единообразные модули, которые будут работать друг с другом без дополнительных усилий, на уровне модульной системы.
Отделение прикладного кода от инфраструктурного
Компания может разрабатывать больше одного приложения. Нередко инфраструктура сборки, разработки, деплоя разворачивается для каждого из них. Инфраструктура развивается эволюционно от приложения к приложению, путем копирования и доработки. Перенос доработок в старые приложения оказывается трудоемким.
MAM поставляется в отдельном репозитории, в котором настроено рабочее окружение. Работа со множеством проектов осуществляется в одном окружении. Вы можете централизовано получать обновления для окружения и централизовано вносить изменения во все приложения.
Фрактальные моно-поли-репозитории
В начале у нас один репозиторий с проектом. Когда он разрастется, часть можно вынести в отдельный репозиторий. Репозитории могут образовывать дерево, вкладываясь друг в друга. При разделении на несколько репозиториев, код остается неизменным, добавляется только ссылка на удаленный репозиторий. MAM автоматически клонирует нужные для проекта репозитории. Локально код всех приложений выглядит как один моно-репозиторий.
Версионирование
Подход к версионированию в MAM называется «verless» — безверсионность. Он работает по принципу открытости/закрытости.
Модуль всегда имеет одну версию — последнюю.
Версии которые сохраняют обратную совместимость API, публикуются под одним именем — рефакторинг, фиксы, расширение.
Не совместимые, под разными именами —
$mol_atom -> $mol_atom2.Реализация старого интерфейса, может использовать новую реализацию (или наоборот), что предотвращает дублирование.
Что это дает:
Мейнтейнер и пользователи модуля фокусируются на одной «версии», вместо распыления внимания на несколько.
Несколько «версий» одного модуля могут сосуществовать рядом. Возможна плавная миграция.
При использовании двух «версий» одного модуля, размер бандла увеличится только на размер адаптера.
Важную функциональность необходимо покрывать тестами.
В случае, если обновление что-то ломает, фиксация ревизии обеспечивается системой контроля версий.
Сборка
Любой модуль можно собрать независимо, без предварительной подготовки. Сборщик автоматически установит недостающие зависимости и скачает удаленные репозитории, от которых зависит собираемый модуль. Артефакты помещаются в директорию - (минус) , которая создается в папке с модулем.
Далее будем называть директорию, в которую помещаются артефакты сборки — дистрибутив. А отдельный артефакт в ней — бандл.
Понятные имена
MAM накладывает ограничение на имена глобальных сущностей. Чтобы сущность можно было использовать в других модулях, ее имя должно:
Через знак подчеркивания, повторять путь до этого модуля в файловой системе
Начинаться с
$(не во всех языках такое возможно, в ts/js — да, в css — нет)
Примеры: $my_alert, $mol_data_record, $hyoo_crowd_doc
Такое именование называется Fully Qualified Name — оно позволяют однозначно идентифицировать сущность, независимо от контекста ее использования.
Это ограничение позволяет:
Лучше продумывать имена модулей и структуру приложения
Разработчик всегда знает, что где лежит
Делает имена глобально-уникальными
Упрощает анализ кода
Автоматический импорт/экспорт
IDE умеют генерировать импорты автоматически, что мешает делать это сборщику? В MAM не нужно использовать импорты/экспорты, чтобы воспользоваться сущностью из другого модуля, достаточно просто написать ее имя.
$mol_assert_ok( true )
Сборщик по FQN-именам понимает где и какой модуль используется, и автоматически их подключает при сборке.
Гранулированность
Чтобы код максимально переиспользовался, он должен быть разбит на множество маленьких, специализированных модулей.
Для этого нужно, простое создание и использование модулей. В MAM для создания модуля, достаточно создать директорию с файлом, а для использования обратится к FQN-имени.
Оптимизация размера бандла
Так как модули имеют высокую гранулированность, а в сборке участвуют только зависимые модули, то бандлы имеют минимальный размер.
Независимость от языков
Для разных вещей используются разные языки: js, css, html, svg, ts, и т.д. Например в webpack, точкой входа является скрипт, в котором подключаются файлы на остальных языках. А что если модуль состоит только из CSS?
В MAM модульная система отделена от языков, т.е. зависимости могут быть кросс-языковыми. css может зависеть от js, который зависит от ts. В исходниках обнаруживаются зависимости от модулей, а модули подключаются целиком (все файлы) и могут содержать исходники на любых языках — точнее на тех, которые сейчас поддерживает MAM, но есть возможность расширить их список.
Разные типы файлов
Каждый файл модуля специализируется на чем-то своем, предназначение файла отражено в его имени. А в дистрибутив сборщик кладет несколько разных типов бандлов.
Например:
*.node.ts— код из этого файла попадет только в бандлnode.js*.web.ts— код попадет только в бандлweb.js*.ts— попадет в оба бандлаweb.jsиnode.js
*.test.ts— попадет в бандл с тестамиweb.test.jsиnode.test.js.*.node.test.ts— также можно у платформу*.locale=ru.json,*.locale=en.json— файлы локализации для русского и английского
Подробный разбор того какие файлы модуля поддерживаются MAM и какие бандлы создаются в дистрибутиве производится ниже.
Тестовые бандлы
MAM создает дополнительные бандлы с тестами web.test.js и node.test.js. В них добавляется код приложения и код тестов (для web это не совсем так, объясняется ниже), тесты создаются в файлах *.test.ts*. При запуске тестового бандла, исполняется код приложения, после него запускаются тесты.
При падении теста, под подозрением оказываются: тест, модуль для которого написан тест и зависимости этого модуля. Сборщик MAM строит граф зависимостей модулей и перед запуском сортирует тесты по глубине, от меньшей к большей. Такой подход гарантирует, что при запуске тестов модуля, его зависимости уже протестированы — под подозрением остаются только тест и модуль.
При разработке, следует запускать тестовые бандлы. Тесты запускаются после каждой сборки, в отладчике следует поставить остановку на ошибках, чтобы раньше выявлять проблемы.
Одинаковый код на dev и prod
В NPM-пакетах можно встретить ситуацию, что код который запускается во время разработки отличается от кода, который публикуется. Ситуация когда ошибка воспроизводится только на production не исключительна. MAM специально не преобразует код в production бандлах, при разработке запускается тот же код. Отличие только в том, что в тестовые бандлы добавляется код тестов.
Погружение
Далее, на примере небольшого веб-приложения — счетчик, мы попробуем MAM на практике и разберем подробности его работы. В следующих частях цикла к этому приложению добавим реактивность и переведем его на $mol-компоненты.
Установка MAM-окружения и настройка VSCode
Обновите NodeJS до LTS версии
Загрузите репозиторий MAM
git clone https://github.com/hyoo-ru/mam.git ./mam && cd mam
Установите зависимости, вашим пакетным менеджером
npm install
Установите плагины для VSCode
Можно использовать gitpod, окружение установится автоматически, согласитесь установить плагины.
MAM-окружение достаточно установить один раз и использовать для всех проектов
Где находятся исходники MAM?
Репозиторий с MAM-окружением зависит от NPM-пакета mam. Этот пакет является модулем $mol_build, который опубликован в NPM, исходники модуля тут. Вся логика MAM сейчас реализована в этом модуле. Сам модуль создан одним из первых и нуждается в рефакторинге. Есть прототип новой версии, но пока нет ресурсов для его завершения.
Как создать модуль?
Подумать над именем
Создать директорию с файлом
Условно, модули можно разделить на три типа:
Пространство имен/namespace — модуль, который содержит только другие модули
Модуль — директория с файлами и другими модулями
Подмодуль — модуль внутри другого модуля Один и тот же модуль в разных контекстах можно назвать и тем и другим.
В руководстве будет использоваться неймспейс my, он годится для примеров, но не рекомендуется использовать его для разработки, чтобы можно было делится кодом. Рекомендуется придумать свое имя.
Создадим неймспейс и модуль:
Перейдите в директорию с MAM —
cd mamСоздайте директорию для неймспейса —
mkdir my && cd myСоздайте директорию для модуля приложения —
mkdir counter
Какие языки и форматы может содержать модуль?
MAM возник вместе с $mol и часть файлов заточена под него. Но в целом, ограничений нет, при необходимости можно добавить поддержку других, например dockerfile.
Ниже перечислены файлы, которые могут размещаться в модуле и с которыми на данный момент умеет работать MAM.
index.html
Это обычный html, который может содержать произвольную разметку. Он является точкой входа для бандла web.js, в нем определяется корневой DOM-элемент, к которому будет монтироваться приложение.
Нельзя размещать index.html в корневом неймспейсе, только в его модулях и глубже
package.json
Сборщик автоматически генерирует package.json. Он поможет при публикации модуля в NPM и при разработке под NodeJS — информация о зависимостях и некоторая другая генерируется автоматически.
В директории модуля можно разместить файл package.json с необходимым содержимым, тогда сборщик смержит его со сгенерированным package.json.
readme.md
Используется для документации модулей. Если модуль еще разрабатывается, добавьте строку unstable. При сборке модуля, этот файл копируется в дистрибутив. Если он отсутствует, то сборщик ищет файл readme.md в родительском модуле и так рекурсивно до корня. Пример.
.ts
Код на typesctipt
.jam.js
Код на javascript тоже поддерживается, но необходимо перед расширением добавлять jam (javascript abstract module). Пример.
.web.ts, .node.ts
Для разделения кода по платформам используются теги web и node. Если тег указан, то код попадет в указанный бандл, web.js или node.js. Если тег не указан, то код попадет в оба бандла.
.test.ts
Код в файле с тегом test попадет в тестовый бандл, их тоже два web.test.js и node.test.js. В месте с тегом test, можно указывать тег платформы — *.web.test.ts.
.css
Произвольный css код. В FQN-именах у css — знак $ не ставится в начале.
.css.ts
Статически типизированный, каскадный css in ts, можно использовать только с компонентами $mol.
.view.tree
Декларативное описания view-компонент, используется в $mol. Можно использовать для описания любых классов.
.locale=*.json*
Локализованные тексты на разных языках, используется в $mol. Тег locale принимает параметр — язык текстов, например *.locale=ru.json.
.meta.tree
Файл с инструкциями для сборщика, поддерживает несколько команд:
deploy— копирует указанный файл в дистрибутивrequireиinclude— включает указанный модуль в зависимости, даже если в коде он не используется.pack— указывает адрес удаленного репозитория для подмодуля
Тег view
В $mol принято файлам реализующим view-компонент, добавлять тег view — counter.view.tree, counter.view.ts, counter.view.css. У формата .view.tree, view — это не тег, а часть расширения.
Все теги — это часть составного расширения. Если читать расширение справа налево, получится конкретизация от общего к частному.
Как называть файлы модуля?
Обычно файлам дают имя модуля, например counter.view.tree, counter.view.ts, counter.view.css для файлов в модуле my/counter. Но сборщику не важны их имена, он читает все файлы модуля, которые поддерживает. Важно как называются сущности внутри них, например класс компонента my/counter в коде должен называться class $my_counter {}.
index.html, package.json, readme.md — называются всегда одинаково.
Начальная реализация модуля $my_counter
Создайте файл mam/my/counter/index.html с таким содержимым:
Создайте mam/my/counter/counter.ts, весь код приведенный ниже поместите в него. Позже мы его разделим на модули.
class View {
// Тут и ниже, такие поля используются для кеширования.
// Удалятся при добавлении реактивности, в следующей главе
_dom_node = null as unknown as Element
// Создание DOM-ноды и регистрация событий на ней
dom_node() {
if ( this._dom_node ) return this._dom_node
const node = document.createElement( this.dom_name() )
for ( const [name, fn] of Object.entries(this.event()) ) {
node.addEventListener(name ,fn)
}
// Атрибут с именем класса, для матчинга из css
node.setAttribute('view', this.constructor.name)
return this._dom_node = node
}
// Актуализация атрибутов и полей
dom_node_actual() {
const node = this.dom_node()
for ( const [name, val] of Object.entries(this.attr()) ) {
node.setAttribute(name, String(val))
}
for ( const [name, val] of Object.entries(this.field()) ) {
node[name] = val
}
return node
}
// Подготовка и рендеринг дочерних компонентов
dom_tree() {
const node = this.dom_node_actual()
const node_list = this.sub().map( node => {
if ( node === null ) return null
return node instanceof View ? node.dom_tree() : String(node)
} )
// Воспользуемся рендером из $mol
$.$mol_dom_render_children( node , node_list )
return node
}
// Методы ниже будут переопредялятся в компонентах-наследниках
// Имя DOM-элемента
dom_name() {
return 'div'
}
// Объект с атрибутами
attr(): { [key: string]: string|number|boolean|null } {
return {}
}
// Объект с событиями
event(): { [key: string]: (e: Event) => any } {
return {}
}
// Объекст с полями
field(): { [key: string]: any } {
return {}
}
// Дочерние компоненты
sub(): Array {
return []
}
}
Класс View — обертка для DOM элемента, предоставляющая интерфейс для упрощения работы с ним.
Функция $mol_dom_render_children рендерит дочерние элементы, без лишних вставок и удалений в DOM-дереве. Сейчас нам нет смысла ее реализовывать, поэтому воспользуемся готовой из $mol.
Теперь создадим несколько компонентов на базе класса View. Мы наследуемся от него, переопределяем нужные методы.
class Button extends View {
dom_name() { return 'button' }
title() { return '' }
click( e: Event ) {}
sub() {
return [ this.title() ]
}
event() {
return {
click: (e: Event) => this.click(e)
}
}
}
class Input extends View {
dom_name() { return 'input' }
type() { return 'text' }
_value = ''
value( next = this._value ) {
return this._value = next
}
change( e: Event ) {
this.value( (e.target as HTMLInputElement).value )
}
field() {
return {
value: this.value(),
}
}
attr() {
return {
type: this.type(),
}
}
event() {
return {
input: (e: Event)=> this.change(e),
}
}
}
И добавляем класс с логикой приложения.
class Counter extends View {
// Синхронизайия с localStorage,
// все вкладки приложения будут синхронизироваться
storage( key: string, next?: Value ) {
if ( next === undefined ) return JSON.parse( localStorage.getItem( key ) ?? 'null' )
if ( next === null ) localStorage.removeItem( key )
else localStorage.setItem( key, JSON.stringify( next ) )
return next
}
count( next?: number ) {
return this.storage( 'count' , next ) ?? 0
}
count_str( next?: string ) {
return this.count( next?.valueOf && Number(next) ).toString()
}
inc() {
this.count( this.count() + 1 )
}
dec() {
this.count( this.count() - 1 )
}
// Создаем инстанс Button
// Переопределяем title
// click биндим на this.inc
_Inc = null as unknown as View
Inc() {
if (this._Inc) return this._Inc
const obj = new Button
obj.title = ()=> '+'
obj.click = ()=> this.inc()
return this._Inc = obj
}
_Dec = null as unknown as View
Dec() {
if (this._Dec) return this._Dec
const obj = new Button
obj.title = ()=> '-'
obj.click = ()=> this.dec()
return this._Dec = obj
}
_Count = null as unknown as View
Count() {
if (this._Count) return this._Count
const obj = new Input
obj.value = (next?: string)=> this.count_str( next )
return this._Count = obj
}
sub() {
return [
this.Dec(),
this.Count(),
this.Inc(),
]
}
static mount() {
const node = document.querySelector( '#root' )
const obj = new Counter()
node?.replaceWith( obj.dom_tree() )
// Реактивность добавится в следующей главе, сейчас воспользуемся костылем
setInterval( ()=> obj.dom_tree() , 100 )
}
}
// Вызываем для монтирования приложения в DOM-дерево
Counter.mount()
Как собрать модуль вручную?
Сборка запускается командой npm start путь/до/модуля. При ручном запуске, сборщик собирает все бандлы, которые поддерживает.
Соберите приложение
cd mam
npm start my/counter
После запуска, сборщик вернет ошибку ReferenceError: document is not defined для строки const node = document.querySelector('#root'). Обратите внимание на имя файла node.test.js, он node.test.js запускается автоматически сразу после сборки.
У нас в коде, после объявления класса Counter, запускается статический метод Counter.mount(). Внутри него есть обращение к window.document, т.к. node.test.js запускается под NodeJS, мы получаем ошибку.
Сейчас мы добавим костыль, позже исправим. Добавьте строку в начало метода Counter.mount
static mount() {
if ( typeof document === 'undefined' ) return // +
const node = document.querySelector( '#root' )
const obj = new Counter()
node?.replaceWith( obj.dom_tree() )
setInterval( ()=> obj.dom_tree() , 100 )
}
Запустите сборку снова.
Где искать результаты сборки?
Выше мы уже говорили, что бандлы помещаются в директорию -, она создается в директории модуля. Также вы можете увидеть еще несколько директорий название которых начинается со знака минус -css, -view.tree, -node — это промежуточные результаты, они создаются по необходимости.
Когда модуль выносится в отдельный репозиторий в .gitignore достаточно добавить строку -*
Какие файлы создает сборщик?
Теперь заглянем в директорию дистрибутива mam/my/counter/-.
index.html, test.html
Это точка входа, для запуска модуля в браузере. Если файл index.html создан в модуле, то он будет просто скопирован. Автоматически сборщик не создает его.
Файл test.html создается всегда, не зависимо от наличия index.html. Он нужен для того чтобы запустить тесты в браузере. Если index.html отсутствует, то test.html генерируется автоматически, с таким контентом:
Тут подключается web.js файл, он содержит код модуля и зависимостей. Файл /mol/build/client/client.js — небольшой скрипт, открывает соединение по веб-сокетам с дев-сервером и по его команде перезагружает страницу. По событию load загружается web.test.js — тесты для браузера и web.audit.js — выводит в лог ошибки типов typescript.
Тесты запускаются при каждой перезагрузки страницы, это нужно для раннего выявления проблем.
Если есть index.html, его содержимое копируется в test.html и часть начиная с загрузки client.js добавляется в конец.
Браузерные бандлы
web.jsсодержит код собираемого модуля и код модулей от которых зависитweb.*.mapsource mapweb.esm.jsтоже самое, только в форматеesmмодуляweb.d.tsфайл с декларациями typescript типовweb.test.jsсодержит тесты модуля и тесты его зависимостей, самого кода модуля и его зависимостей в нем нет, т.к. вhtmlфайлweb.jsподгружается отдельноweb.view.treeсюда складываются деклорации из всехview.treeфайловweb.locale=en.jsonлокализация на английском, генерируется автоматически путем анализаview.tree, для других языков этот файл копируется, переводится и помещается в директорию с модулем.web.view=*.jsonлокализация для других языков, просто копируется из директории модуля в дистрибутив.web.deps.jsonинформация о графе зависимостей модулейweb.audit.jsв случае ошибок в проверке типов, тут будетconsole.logс информацией о них. Если ошибок нет, тоconsole.log("Audit passed")
Серверные бандлы
Все файлы с префиксом node предназначены для запуска под NodeJS. Список файлов, точно такой же как и для браузера. Отличие только в коде, т.е. исходный код файлов с тегом node попадает только в серверные бандлы, а с тегом web только в браузерные.
node.test.js содержит и код модуля с зависимостями и тесты к ним, в отличие от web.test.js.
readme.md
Копируется из директории с модулем, если в модуле его нет, то ищется в родительском модуле и так до корня.
package.json
Сборщик автоматически генерирует файл package.json, используется для публикации пакетов в NPM и для серверных приложений. Если приложение использует NPM-пакеты, то они будут указаны в зависимостях. Если этот файл присутствует в модуле, то он буде объединен со сгенерированным файлом.
Запуск дев-сервера
Давайте запустим дев-сервер, он перезапускает сборку при изменении зависимостей модуля и перезагружает страницу в браузере.
Выполните команду:
cd mam
npm start
Ссылка http://127.0.0.1:9080 появится в терминале. Откройте ее, вы увидите в файловом менеджере директории находящиеся в mam. Откройте модуль приложения mam/my/counter. Обнаружив файл index.html, дев-сервер начнет сборку этого модуля.
На текущий момент, поддерживается пересборка только для модулей содержащих файл index.html.
Когда вы откроете модуль c файлом index.html, в адресной строке браузера будет путь http://127.0.0.1:9081/my/counter/-/test.html. Он состоит из:
путь до модуля, который вы собираете
/my/counter,директория дистрибутива
/-/запрашиваемый бандл
test.html
После того как браузер загрузит html документ, начнется загрузка js файла, ссылка на который находится в теге script — . Браузер сделает запрос по такому адресу http://127.0.0.1:9081/my/counter/-/web.js.
Дев-сервер анализирует адрес запроса, получает путь до модуля, и какой файл запрошен, выполняет сборку запрошенного бандла, кэширует результат и отправляет в браузер. При изменении файла кэш сбрасывается, а браузеру отправляется команда перезагрузить вкладку, после чего файл запрашивается снова и происходит его сборка.
Дев-сервер собирает только те файлы, которые непосредственно запрашиваются из браузера, для того чтобы собрать все артефакты, необходимо запустить сборку вручную.
Сейчас дев-сервер поддерживает сборку только веб-приложений. Если вы разрабатываете NodeJS проект, то вы можете запустить дев-сервер и вручную отправлять запрос за файлом node.test.js, для его пересборки.
Как импортировать и экспортировать модули?
MAM автоматически отслеживает зависимости между модулями, анализируя FQN-имена в исходниках. Если мы хотим «экспортировать», какую-то сущность из модуля, чтобы она была доступна в других модулях, необходимо дать ей имя в формате FQN. Чтобы использовать метод, функцию или любую сущность из другого модуля, нужно просто написать ее имя.
Например, в модуле $my_csv объявлено две функции
// mam/my/csv/cvs.ts
function $my_csv_decode( text = 'a;b;c\n1;2;3' ) {
return $mol_csv_parse( text )
}
function $my_csv_encode( list = [['a','b','c'], [1,2,3]] ) {
return list.map(
line => line.map( cell => `"${cell.replace( /"/g, '""' )}"` ).join(';')
).join('\n')
}
Ими можно воспользоваться в любом другом модуле, просто написав имя $my_csv_decode( 'q;w;\n1;2' ), будто она объявлена выше в этом же файле.
Обратите внимание, что совпадать должен только префикс имени $my_csv_ с путем до файла mam/my/csv. В этом же файле мы можем объявить функцию с таким именем $my_csv_decode_stream, это не значит что мы обязаны класть эту функцию в mam/my/csv/decode/stream.
Теперь давайте воспользуемся FQN-именами в нашем приложении и заодно разобьем его на несколько модулей.
Получится такая структура:
mam /
my /
counter /
view /
button /
input /
Переименуйте класс
Viewв$my_counter_view, создайте файлmam/my/counter/view/view.tsи перенесите туда код этого класса.Тоже самое делаем с классом
Button
// mam/my/counter/button/button.ts
class $my_counter_button extends $my_counter_view {
dom_name() { return 'button' }
title() { return '' }
click( e: Event ) {}
sub() {
return [ this.title() ]
}
event() {
return {
click: (e: Event) => this.click(e)
}
}
}
И с классом Input
// mam/my/counter/input/input.ts
class $my_counter_input extends $my_counter_view {
dom_name() { return 'input' }
type() { return 'text' }
_value = ''
value( next = this._value ) {
return this._value = next
}
event_change( e: Event ) {
this.value( (e.target as HTMLInputElement).value )
}
field() {
return {
value: this.value(),
}
}
attr() {
return {
type: this.type(),
}
}
event() {
return {
input: (e: Event)=> this.event_change(e),
}
}
}
В файле mam/my/counter/counter.ts остался класс Counter. Измените его имя на $my_counter и имена переименованных классов.
Запустите дев-сервер, если еще не сделали этого, и убедитесь что приложение работает.
Что если модуль используется много раз и его имя слишком длинное?
Положите ссылку на него в переменную с более коротким именем.
const Response = $mol_data_record({
status: $mol_data_number,
data: $mol_data_record({
name: $mol_data_string,
surname: $mol_data_string,
age: $mol_data_number,
birth_date: $mol_data_pipe( $mol_data_string, $mol_time_moment ),
}),
})
Станет:
const Rec = $mol_data_record
const Str = $mol_data_string
const Num = $mol_data_number
const Response = Rec({
status: Num,
data: Rec({
name: Str,
surname: Str,
age: Num,
birth_data: $mol_data_pipe( Str, $mol_time_moment ),
}),
})
Какие модули включаются в дистрибутив?
Сборка работает по нескольким правилам:
В бандлы включаются все модули от которых зависит собираемый модуль. Анализируется по FQN-именам.
Включаются модули, подключенные командами
includeиrequire, а также копируются статические файлы командойdeploy. Подробнее рассматривается ниже.Включается родительский модуль, для каждого включенного модуля. Например, при сборке модуля
/a/b/c, в бандлы будут включены модулиc,b,a,/. Код родительского модуля, будет включен раньше кода модуля. Как пример можно рассмотреть модуль mam, он находится непосредственно в репозитории с дев-окружением, и будет включен в дистрибутив при сборке любого модуля.Модуль включается целиком. Если модуль включается в дистрибутив, то все его файлы, с которыми умеет работать MAM, будут включены в соответствующие бандлы. Подмодули не включаются автоматически, только если срабатывают правила выше.
Добавим модуль для работы с localStorage. Создайте директорию для модуля $my_counter_storage и ts файл.
// mam/my/counter/storage/storage.ts
class $my_counter_storage {
static value( key: string, next?: Value ) {
if ( next === undefined ) return JSON.parse( localStorage.getItem( key ) ?? 'null' )
if ( next === null ) localStorage.removeItem( key )
else localStorage.setItem( key, JSON.stringify( next ) )
return next
}
}
Сейчас не нужно его использовать в $my_counter.
Добавьте в файл mam/my/counter/button/button.ts еще одну кнопку — $my_counter_button_minor. Она не будет использоваться, нужна только для демонстрации.
// mam/my/counter/button/button.ts
class $my_counter_button extends $my_counter_view {
dom_name() { return 'button' }
title() { return '' }
click( e: Event ) {}
sub() {
return [ this.title() ]
}
event() {
return {
click: (e: Event) => this.click(e)
}
}
}
class $my_counter_button_minor extends $my_counter_button {
attr() {
return {
'my_counter_button_minor': true,
}
}
}
После сборки откройте бандл web.js. Найдите класс $my_counter_button_minor, он включен в бандл, потому что модуль $my_counter_button используется в приложении, а класс минорной кнопки объявлен именно в нем. Если вынести объявление кнопки в отдельный модуль mam/my/counter/button/minor, тогда она не добавится в бандл.
Класс $my_counter_storage вы не найдете в бандле, потому что он не используется в приложении.
Теперь используем модуль $my_counter_storage в коде.
// mam/my/counter/counter.ts
class $my_counter extends $my_counter_view {
// delete
// - storage( key: string, next?: Value ) {
// - if ( next === undefined ) return JSON.parse( localStorage.getItem( key ) ?? 'null' )
// -
// - if ( next === null ) localStorage.removeItem( key )
// - else localStorage.setItem( key, JSON.stringify( next ) )
// -
// - return next
// - }
count( next?: number ) {
// - return this.storage( 'count' , next ) ?? 0
return $my_counter_storage.value( 'count' , next ) ?? 0 // +
}
После сборки вы
