MAM - модульная система, в которой переосмыслена работа с кодом и его организация. Она проектировалась для единообразной работы с произвольными объемами кода, облегчая переиспользование и минимизируя рутину.
MAM расшифровывается как Mam owns Abstract Modules.
Идеи и концепции
Модуль
Модуль - директория, внутри которой находятся файлы реализующие его. Разные части модуля находятся в разных файлах. Абстрактный - значит, что модуль не привязан к какому-то конкретному языку, а может быть реализован на нескольких. Модуль первичен, а технологии на которых он реализован - вторичны.
Особенности:
Один модуль - одна директория
Модули произвольно вкладываются друг в друга, все есть модуль
Имя модуля - путь до него в файловой системе
Модуль может выступать пространством имен - содержать только директории
Зависимости между модулями отслеживаются автоматически
Разные типы исходников модуля, попадают в разные бандлы
Можно провести аналогию с БЭМ методологией, там используется термин "блок", первичен блок, его реализация вторична.
Соглашения вместо конфигурации
В MAM нет пользовательского конфига, вместо него используются соглашения. Соглашение можно рассматривать как некое условие, которое нужно выполнить, чтобы получить требуемый результат.
С одной стороны это позволяет просто установить MAM и стартовать проект без дополнительных действий, т.к. уже все настроено. С другой стороны, отсутствие конфигов, позволяет независимым разработчикам писать единообразные модули, которые будут работать друг с другом без дополнительных усилий, на уровне модульной системы.
Отделение прикладного кода от инфраструктурного
Компания может разрабатывать больше одного приложения. Нередко инфраструктура сборки, разработки, деплоя разворачивается для каждого из них. Инфраструктура развивается эволюционно от приложения к приложению, путем копирования и доработки. Перенос доработок в старые приложения оказывается трудоемким.
MAM поставляется в отдельном репозитории, в котором настроено рабочее окружение. Работа со множеством проектов осуществляется в одном окружении. Вы можете централизовано получать обновления для окружения и централизовано вносить изменения во все приложения.
Фрактальные моно-поли-репозитории
В начале у нас один репозиторий с проектом. Когда он разрастется, часть можно вынести в отдельный репозиторий. Репозитории могут образовывать дерево, вкладываясь друг в друга. При разделении на несколько репозиториев, код остается неизменным, добавляется только ссылка на удаленный репозиторий. MAM автоматически клонирует нужные для проекта репозитории. Локально код всех приложений выглядит как один моно-репозиторий.
Версионирование
Подход к версионированию в MAM называется "verless" - безверсионность. Он работает по принципу открытости/закрытости.
Модуль всегда имеет одну версию - последнюю.
Версии которые сохраняют обратную совместимость API, публикуются под одним именем - рефакторинг, фиксы, расширение.
Не совместимые, под разными именами - $mol_atom -> $mol_atom2.
Реализация старого интерфейса, может использовать новую реализацию (или наоборот), что предотвращает дублирование.
Что это дает:
Мейнтейнер и пользователи модуля фокусируются на одной "версии", вместо распыления внимания на несколько.
Несколько "версий" одного модуля могут сосуществовать рядом. Возможна плавная миграция.
При использовании двух "версий" одного модуля, размер бандла увеличится только на размер адаптера.
Важную функциональность необходимо покрывать тестами.
В случае, если обновление что-то ломает, фиксация ревизии обеспечивается системой контроля версий.
Сборка
Любой модуль можно собрать независимо, без предварительной подготовки. Сборщик автоматически установит недостающие зависимости и скачает удаленные репозитории, от которых зависит собираемый модуль. Артефакты помещаются в директорию - (минус) , которая создается в папке с модулем.
Далее будем называть директорию, в которую помещаются артефакты сборки - дистрибутив. А отдельный артефакт в ней - бандл.
Понятные имена
MAM накладывает ограничение на имена глобальных сущностей. Чтобы сущность можно было использовать в других модулях, ее имя должно:
Через знак подчеркивания, повторять путь до этого модуля в файловой системе
Начинаться с $ (не во всех языках такое возможно, в ts/js - да, в css - нет)
Такое именование называется 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
Можно использовать , окружение установится автоматически, согласитесь установить плагины.
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 - знак $ не ставится в начале.
.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<View | Node | string | number | boolean> {
return []
}
}
Класс View - обертка для DOM элемента, предоставляющая интерфейс для упрощения работы с ним.
Функция $mol_dom_render_children рендерит дочерние элементы, без лишних вставок и удалений в DOM-дереве. Сейчас нам нет смысла ее реализовывать, поэтому воспользуемся готовой из $mol.
Теперь создадим несколько компонентов на базе класса View. Мы наследуемся от него, переопределяем нужные методы.
class Counter extends View {
// Синхронизайия с localStorage,
// все вкладки приложения будут синхронизироваться
storage<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
}
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
Выше мы уже говорили, что бандлы помещаются в директорию -, она создается в директории модуля. Также вы можете увидеть еще несколько директорий название которых начинается со знака минус -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.*.map source map
web.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 - <script src="web.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 и перенесите туда код этого класса.
В бандлы включаются все модули от которых зависит собираемый модуль. Анализируется по 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<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. Она не будет использоваться, нужна только для демонстрации.
После сборки откройте бандл 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<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
// - }
count( next?: number ) {
// - return this.storage( 'count' , next ) ?? 0
return $my_counter_storage.value( 'count' , next ) ?? 0 // +
}
После сборки вы найдете модуль $my_counter_storage в бандле.
Откройте приложение, вы увидите что цвет фона страницы, меняется каждые 30 секунд.
Сборщик находит имя модуля $my_theme в файле counter.css и добавляет все файлы модуля $my_theme в бандлы. Найдите его код в бандле web.js.
После добавления css файла, вы найдете несколько новых модулей в js бандле. Сейчас стили добавляются в web.js бандл с помощью функции $mol_style_attach. Она и ее зависимости добавлены в бандл.
Как перенести модуль в другое место?
Во время рефакторинга, может понадобится перенести модуль в другое место. Поскольку имена зависят от месторасположения, их тоже нужно менять. Вам нужно выполнить команду редактора find and replace. Например, ищем все имена с именем my_module_name и заменяем на my_module_new_name, после перемещения файлов.
В meta.tree вместо FQN используются пути через /, их придется менять отдельно
Вынесем в отдельный модуль, модули с библиотечным кодом $my_counter_view, $my_counter_button и т.д., а в модуле $my_counter останется только код приложения.
Создайте директорию для модуля $my_lom
cd mam/my
mkdir lom
Переместите директорию $my_counter_view в $my_lom_view, и используя find and replace переименуйте все строки my_counter_view в my_lom_view. Поиск и замена производится на уровне корневой директории mam.
Повторите проделанное выше с остальными модулями: $my_counter_button, $my_counter_input, $my_counter_storage, $my_theme.
Сейчас весь библиотечный код находится в модуле $my_lom, а в модуле $my_counter остался только прикладной код. Убедитесь что приложение работает.
Монорепозитории и полирепозитории
MAM поддерживает работу с обоими типами репозиториев одновременно. Их можно вкладывать друг в друга, также как и модули. Сборщик автоматически клонирует отсутствующие репозитории. Можно произвольный модуль вынести в отдельный репозиторий, без изменения его исходного кода. Разработчик использует одно дев-окружение, в котором находятся все проекты рядом, каждый в своем неймспейсе/модуле, в том числе неймспейс mol. Т.е. при разработке это выглядит как один большой монорепозиторий, можно внести изменения в любой модуль, но при этом этот код может хранится во множестве удаленных репозиториях на github, gitlab, bitbucket и т.д.
Для того чтобы сборщик мог загрузить репозиторий, он должен знать его адрес. Ссылки на удаленные репозитории находятся в файле *.meta.tree, который размещается в родительском модуле, по отношению к модулю который выносится в удаленный репозиторий. Пример файла meta.tree
Команда выглядит так:
pack apps git \https://github.com/hyoo-ru/apps.hyoo.ru.git
pack - инструкция, говорит сборщику что перед нами удаленный репозиторий
apps - имя директории подмодуля, в которую будет загружен репозиторий
git - система контроля версия, сейчас поддерживается только одна.
после \ размещается ссылка на репозиторий.
Разберем на примере как это работает:
Запускаем команду yarn start hyoo/draw.
Сборщик не найдя директорию mam/hyoo, ищет meta.tree в директории mam
В meta.tree находит строку в которой директория подмодуля называется hyoo - pack hyoo git \https://github.com/hyoo-ru/mam_hyoo.git
Клонирует этот репозиторий в mam/hyoo
Ищет директорию mam/hyoo/draw, не найдя заглядывает в mam/hyoo/hyoo.meta.tree, теперь его интересует строка начинающаяся с pack draw. Найдя ее, клонирует репозиторий в mam/hyoo/draw.
Приступает к сборке $hyoo_draw
Для того, чтобы VSCode корректно работал с несколькими репозиториями, создайте файл mam/.gitmodules с подобным содержимым:
На вкладке контроля версий, у вас появится несколько репозиториев.
VSCode не всегда сразу отображает репозитории перечисленные в .gitmodules на вкладке Source Control. Чаще всего это решается перезапуском VSCode. Если перезапуск не помогает, то нужно изменить порядок модулей в .gitmodules
Сейчас мы вынесем неймспейс $my и модули $my_lom, $my_counter в отдельные репозитории.
Создайте репозиторий с именем mam_my на github.com
Создайте git-репозиторий в модуле $my и свяжите его с репозиторием созданным на github.
В листингах ниже **YOUR_NAME** замените на свое значение
cd mam/my
echo "# mam_my - namespace for MAM-based projects" > readme.md
echo "-*" > .gitignore
git init
git add readme.md
git commit -m "Init"
git branch -M main
git remote add origin https://github.com/**YOUR_NAME**/mam_my.git
git push -u origin main
Повторяем шаги 1 и 2 для модулей $my_lom и $my_counter. Репозитории назовите my_lom и my_counter соответственно.
Удалите директории модулей $my_lom и $my_counter, и запустите сборку модуля $my_counter - npm start my/counter. Сборщик загрузит репозитории и соберет модуль. В логе вы увидите следующее:
come
time \2022-03-25T19:42:27.723Z
place \$mol_exec
dir \my/lom
message \Run
command \git init
come
time \2022-03-25T19:42:27.738Z
place \$mol_exec
dir \my/lom
message \Run
command \git remote show https://github.com/**YOUR_NAME**/my_lom.git
come
time \2022-03-25T19:42:28.475Z
place \$mol_exec
dir \my/lom
message \Run
command \git remote add --track main origin https://github.com/**YOUR_NAME**/my_lom.git
come
time \2022-03-25T19:42:28.488Z
place \$mol_exec
dir \my/lom
message \Run
command \git pull
Разделение кода на платформы
MAM поддерживает две платформы - браузер и NodeJS. Файлы кода с тегом web будут включены только в web.js бандл, а с тегом node только в node.js. Код без тега платформы попадет в оба бандла.
Ранее, мы добавили костыль if (typeof document === undefined) return, для того чтобы не падали тесты под NodeJS. Теперь исправим его.
Но сначала займемся небольшим рефакторингом, добавьте два статических метода в класс $my_lom_view.
Мы переместили метод mound и добавили метод root, который поможет указать какой компонент является корневым.
Удалите метод mount и его вызов из класса $my_counter и добавьте присвоение компонента методу root.
Этот код будет добавлен только в web бандл. Метод mount вызывается асинхронно, потому что в бандле $my_lom_view находится выше, чем $my_counter и установка значения происходит после запуска этого кода. Соберите модуль $my_counter вручную и проверьте что этот код присутствует в web.js бандле и отсутcтвует в бандле node.js.
Как использовать NPM-пакеты в node?
Для использования NPM пакетов и модулей с которыми поставляется NodeJS создан специальный модуль $node. Вы просто пишите $node['is-odd'] или $node.express. Сборщик MAM автоматически установит эти пакеты их typescript типы, добавит их в бандл package.json.
Сейчас мы добавим изоморфности в наше приложение. Создайте директорию для модуля $my_lom_dom_context и файл context.ts.
// mam/my/lom/dom/context/context.ts
let $my_lom_dom_context: typeof globalThis
В общем коде для node и web, мы просто объявили переменную, которой будет присвоено различное значение в зависимости от платформы. Создайте файл context.web.ts.
В web мы присваиваем этой переменной объект window. Создайте файл context.node.ts.
// mam/my/lom/context/context.node.ts
$my_lom_dom_context = new $node.jsdom.JSDOM( '' , { url : 'https://localhost/' } ).window as any
В node мы присваиваем этой переменной экземпляр класса JSDOM, с помощью него наш код сможет работать под NodeJS. Для ssr такого решения не достаточно, но для тестов вполне.
as any потому что jsdom не реализует все браузерное API и его типы с typeof globalThis не сходятся.
Замените прямые обращения к document, на использование $my_lom_dom_context.document:
Запустите сборку вручную npm start my/counter. Если директория mam/node_modules не содержит jsdom, вы увидите в терминале подобные сообщения:
> start
> node ./mol/build/-/node.js "my/counter"
come
time \2022-03-25T19:21:46.435Z
place \$mol_exec
dir \
message \Run
command \npm install jsdom
come
time \2022-03-25T19:21:50.155Z
place \$mol_exec
dir \
message \Run
command \npm install @types/jsdom
В файл mam/my/counter/-/package.json добавятся зависимости:
{
"jsdom": "*",
"colorette": "*"
}
Модуль $node имеет зависимость от модуля, который использует NPM-пакет colorlette, поэтому он тоже добавился в package.json.
* используется, потому что MAM использует модель verless с NPM тоже. В экстренном случае, можно в директории модуля создать package.json и зафиксировать версию пакета в нем.
Файл mam/my/counter/-node/deps.d.ts содержит typescript типы для всех модулей NPM и NodeJS.
Для этого в *.meta.tree предусмотрена команда deploy. Нужно его создать и добавить строку deploy \path/to/file/image.png. При сборке файл будет скопирован в mam/my/module/-/path/to/file/image.png. Пример meta.tree файла.
Давайте добавим fav-иконку к нашему приложению. Создайте модуль $my_counter_logo и скачайте туда эту иконку.
В модуле $my_counter, создайте файл counter.meta.tree:
deploy \/my/counter/logo/logo.svg
Соберите модуль npm start my/counter, после сборки, вы найдете иконку по пути mam/my/counter/-/my/counter/logo/logo.svg.
В index.html добавьте строку <link href="my/counter/logo/logo.svg" rel="icon"> внутри тега head.
Как включить модуль в проект, на который никто не ссылается?
Сборщик автоматически включает в дистрибутив модули от которых зависит собираемый модуль. Иногда вам нужно добавить модули, от которых ваш код не зависит. Например, когда вы создаете приложение с каталогом компонентов. Для этого используются две команды require и include, которые добавляются в файл meta.tree. Если необходимо подключить код указанного в команде модуля, раньше кода модуля в котором находится meta.tree, то используйте require иначе используйте include.
Создайте директорию для модуля $my_lom_lib и внутри файл lib.meta.tree, с таким содержимым:
include \/my/lom/button
include \/my/lom/dom
include \/my/lom/input
include \/my/lom/lib
include \/my/lom/storage
include \/my/lom/theme
include \/my/lom/view
Соберите модуль $my_lom_lib и проверьте js бандл. Все подключенные модули будут добавлены в него.
Развертывание модуля в NPM
Теперь настроим автопубликацию модуля $my_lom_lib в NPM. Для начала создайте аккаунт или войдите.
npm adduser
# or
npm login
Откройте бандл mam/my/lom/lib/-/package.json и проверьте поле name. Имя генерируется автоматически, несколько человек не смогут опубликовать модуль, если проходя это руководство используют имя my для неймспейса. Создайте файл package.json в модуле $my_lom_lib с другим именем.
Поля из добавленного package.json будут объедены с полями сгенерированного package.json. Если вы сейчас соберете модуль, то увидите что бандле имя изменилось.
Убедимся что имя свободно.
npm search my_lom_lib_test
Если занято, измените его. Создайте персональный токен доступа для NPM.
npm token create
Создайте секрет в репозитории my_lom с именем NPM_AUTH_TOKEN и созданным токеном в качестве значения.
Отправьте изменения на гитхаб и через несколько минут, пакет будет опубликован в NPM.
При использовании NPM-пакетов, мы просто экспортируем их имена из нужного бандла web или node.
import {
$my_lom_view as View,
} from "my_lom_lib_test/web";
Как использовать NPM в web?
Чтобы NPM-пакет работал в браузере, его код необходимо добавить в бандл web.js.
На данный момент MAM сборщик самостоятельно обрабатывает код NPM-пакетов, т.к. он не заточен под NPM, это приводит к некоторым проблемам. Например не умеет удалять неиспользуемые ветки кода - if (process.env.NODE_ENV === 'development') {}. Так же у MAM нет необходимости в treeshaking, т.к. он сразу подключает только используемые модули, но NPM-пакетам он нужен. В будущем код NPM-пакетов, будет обрабатываться отдельным бандлером, таким как webpack и т.п.
В общем виде процесс подключения NPM-пакета выглядит так:
создать модуль для этого пакета
в модуле импортировать пакеты и типы через require
положить все сущности которые надо экспортировать из пакета в переменные с FQN-именами
Простой пример подключения ramda. Код модуля может быть сложнее простого реэкспорта. Например там может быть какой-нибудь адаптер, или демо с примером использования модуля.
Для NPM-пакетов создан отдельный модуль $lib, ссылка на репозиторий. При добавлении какого-то пакета, лучше добавлять его сразу туда.
При сборки модуля, NPM-пакеты установятся автоматически. При необходимости версию можно зафиксировать в корневом package.json
Сейчас мы не будем будем использовать написанное ранее приложение, а добавим модуль подключающий ReactJS в MAM. Он уже добавлен сюда. Для практики, вы можете самостоятельно добавить другой пакет по примеру ниже.
Для начала нужно загрузить репозиторий модуля $lib, запустите npm start lib. Загрузить его можно и через git clone, но ссылка на репозиторий уже находится в mam/.meta.tree, сборщик сам загрузит его по ней.
Создайте директорию для модуля $lib_react и ts файл в ней.
Тут используется namespace $ {} - они используются в системе инверсии контроля $mol, как средство авторегистрации сущностей в контейнере, об этом мы поговорим в другой главе.
Мы просто импортировали ReactJS и положили его в $lib_react. В $lib_react_jsx вынесена функция createElement, она будет использоваться как короткая ссылка для указания какую функцию использовать при транспиляции tsx.
Попробуйте открыть демо-приложение. Запустите дев-сервер и откройте в файловом менеджере lib/react/demo. Вы увидите ошибку.
// Uncaught ReferenceError: process is not defined
// at Object.<anonymous> (react.development.js:12:1)
// at -:2:1
if (process.env.NODE_ENV !== "production") {/*...*/}
Сборщик не умеет вырезать такие куски кода, пока что обойдемся заглушкой. Создадим для этого модуль $lib_react_env.
// mam/lib/react/env/env.ts
var process = process || { env: { NODE_ENV: 'development' } } as any
Используем var, чтобы без дополнительного кода объявить переменную на глобальном объекте. namespace тут не нужен, т.к. тогда typescript завернет этот код в вызов функции.
Нам нужно, чтобы этот код был включен в бандл и выполнен раньше, чем код ReactJS. Создадим файл mam/lib/react/react.meta.tree
require \/lib/react/env
Код из модуля $lib_react_env, будет подключен в начало банла, перед кодом ReactJS.
Обновите страницу, приложение должно заработать. Этот адаптер нуждается в доработке, но для начала годится.
Развертывание приложения на Github Pages
Для этого нужно создайте файл .github/workflows/deploy.yml в модуле, который вынесен в отдельный репозиторий.
После отправки коммита, приложение будет задеплоено. Исходники github action тут.
Сейчас настроим деплой нашего приложения на github. Сначала создайте персональный токен доступа тут. Создайте секрет в настройках репозитория my_counter, с созданным токеном, назовите его GH_PAT.
Создайте файл deploy.yml, не забудьте заменить \*\*YOUR_NAME\*\* в ссылке на репозиторий mam_my.
Параметры, которые принимает github action описаны в readme. Параметр meta содержит ссылку на репозиторий неймспейса mam_my, т.к. у него в файле my.meta.tree указана ссылка на репозиторий модуля $my_lom, который нужен для сборки приложения. Если его не указывать, то сборка завершится ошибкой - не найден модуль `$my_lom.
После отправки изменений на github, начнется сборка, после ее окончания, вы найдете ссылку на приложение тут: https://github.com/**YOUR_NAME**/my_counter/settings/pages.
Скопируйте ее в описание репозитория.
Циклические зависимости
Сборщик оценивает жесткость зависимостей, чем она сильнее тем раньше модуль будет включен в бандл. Жесткость оценивается по простой эвристике - количеству отступов в строке, в которой находится зависимость.
Для демонстрации циклической зависимости, мы создадим несколько файлов с кодом вне директории MAM и запустим их с помощью NodeJS.
Создайте где-нибудь директорию cyclic и файл all.mjs
// cyclic/all.mjs
export class Foo {
get bar() {
return new Bar();
}
}
export class Bar extends Foo {}
console.log(new Foo().bar);
Запустите его node app.mjs - ошибок нет. В нем есть циклическая зависимость, но код работает корректно. Разделим его на несколько файлов.
// cyclic/foo.mjs
import { Bar } from './bar.mjs';
export class Foo {
get bar() {
return new Bar();
}
}
// cyclic/bar.mjs
import { Foo } from './foo.mjs';
export class Bar extends Foo {}
// cyclic/app.mjs
import { Foo } from './foo.mjs';
console.log(new Foo().bar);
Запустите node app.mjs, вы увидите ошибку ReferenceError: Cannot access 'Foo' before initialization. Для ее исправления, нужно добавить импорт bar.mjs перед foo.mjs.
Соберите модуль $my_app и посмотрите код в js бандле.
"use strict";
class $my_foo {
get bar() {
return new $my_bar();
}
}
//my/foo/foo.ts
;
"use strict";
class $my_bar extends $my_foo {
}
//my/bar/bar.ts
;
"use strict";
console.log(new $my_foo().bar);
//my/app/app.ts
Сборщик склеивает файлы в таком порядке, как будто мы изначально писали код в одном файле. Зависимость $my_bar от $my_foo сильнее чем $my_foo от $my_bar, поэтому модули добавляются в бандл в таком порядке: $my_foo, $my_bar, $my_app.
Рекомендации по использованию MAM
Создайте корневой нейспейс - вам нужен модуль, который будет содержать все ваши наработки. Разместите его в отдельном репозиторий.
Если планируется распространять ваш код по открытой лицензии, то добавьте ссылку на него в корневой meta.tree через pull-request.
В рамках вашего неймспейса создаются неймспейсы для публичных и приватных модулей, которые разносятся по разным репозиториям.
Модули группируются по функциональности, а не по типу. Не должно быть директорий-складов actions, components, containers, helpers, utils и т.п. Модуль должен представлять из себя домен, который хорошо решает свою задачу. Например "хелпер" для сравнения объектов, можно отправить в модуль $my_object или сразу вынести в подмодуль $my_object_equal и использовать его на всех проектах.
Вся важная функциональность должна покрываться тестами.
При замораживании разработки репозиторий дев-окружения форкается и ревизии фиксируются через git-merge-tree, а также обновляется пайплайн на использование форка.
Далее
В следующей части мы научимся работать с системой реактивности $mol
MAM - модульная система, в которой переосмыслена работа с кодом и его организация. Она проектировалась для единообразной работы с произвольными объемами кода, облегчая переиспользование и минимизируя рутину.
MAM расшифровывается как Mam owns Abstract Modules.
Идеи и концепции
Модуль
Модуль - директория, внутри которой находятся файлы реализующие его. Разные части модуля находятся в разных файлах. Абстрактный - значит, что модуль не привязан к какому-то конкретному языку, а может быть реализован на нескольких. Модуль первичен, а технологии на которых он реализован - вторичны.
Особенности:
Можно провести аналогию с БЭМ методологией, там используется термин "блок", первичен блок, его реализация вторична.
Соглашения вместо конфигурации
В MAM нет пользовательского конфига, вместо него используются соглашения. Соглашение можно рассматривать как некое условие, которое нужно выполнить, чтобы получить требуемый результат.
С одной стороны это позволяет просто установить MAM и стартовать проект без дополнительных действий, т.к. уже все настроено. С другой стороны, отсутствие конфигов, позволяет независимым разработчикам писать единообразные модули, которые будут работать друг с другом без дополнительных усилий, на уровне модульной системы.
Отделение прикладного кода от инфраструктурного
Компания может разрабатывать больше одного приложения. Нередко инфраструктура сборки, разработки, деплоя разворачивается для каждого из них. Инфраструктура развивается эволюционно от приложения к приложению, путем копирования и доработки. Перенос доработок в старые приложения оказывается трудоемким.
MAM поставляется в отдельном репозитории, в котором настроено рабочее окружение. Работа со множеством проектов осуществляется в одном окружении. Вы можете централизовано получать обновления для окружения и централизовано вносить изменения во все приложения.
Фрактальные моно-поли-репозитории
В начале у нас один репозиторий с проектом. Когда он разрастется, часть можно вынести в отдельный репозиторий. Репозитории могут образовывать дерево, вкладываясь друг в друга. При разделении на несколько репозиториев, код остается неизменным, добавляется только ссылка на удаленный репозиторий. MAM автоматически клонирует нужные для проекта репозитории. Локально код всех приложений выглядит как один моно-репозиторий.
Версионирование
Подход к версионированию в MAM называется "verless" - безверсионность. Он работает по принципу открытости/закрытости.
$mol_atom -> $mol_atom2
.Что это дает:
В случае, если обновление что-то ломает, фиксация ревизии обеспечивается системой контроля версий.
Сборка
Любой модуль можно собрать независимо, без предварительной подготовки. Сборщик автоматически установит недостающие зависимости и скачает удаленные репозитории, от которых зависит собираемый модуль. Артефакты помещаются в директорию
-
(минус) , которая создается в папке с модулем.Далее будем называть директорию, в которую помещаются артефакты сборки - дистрибутив. А отдельный артефакт в ней - бандл.
Понятные имена
MAM накладывает ограничение на имена глобальных сущностей. Чтобы сущность можно было использовать в других модулях, ее имя должно:
$
(не во всех языках такое возможно, в ts/js - да, в css - нет)Примеры:
$my_alert
,$mol_data_record
,$hyoo_crowd_doc
Такое именование называется Fully Qualified Name - оно позволяют однозначно идентифицировать сущность, независимо от контекста ее использования.
Это ограничение позволяет:
Автоматический импорт/экспорт
IDE умеют генерировать импорты автоматически, что мешает делать это сборщику? В MAM не нужно использовать импорты/экспорты, чтобы воспользоваться сущностью из другого модуля, достаточно просто написать ее имя.
Сборщик по 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
Можно использовать , окружение установится автоматически, согласитесь установить плагины.
MAM-окружение достаточно установить один раз и использовать для всех проектов
Где находятся исходники MAM?
Репозиторий с MAM-окружением зависит от NPM-пакета mam. Этот пакет является модулем
$mol_build
, который опубликован в NPM, исходники модуля тут. Вся логика MAM сейчас реализована в этом модуле. Сам модуль создан одним из первых и нуждается в рефакторинге. Есть прототип новой версии, но пока нет ресурсов для его завершения.Как создать модуль?
Условно, модули можно разделить на три типа:
В руководстве будет использоваться неймспейс
my
, он годится для примеров, но не рекомендуется использовать его для разработки, чтобы можно было делится кодом. Рекомендуется придумать свое имя.Создадим неймспейс и модуль:
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
, весь код приведенный ниже поместите в него. Позже мы его разделим на модули.Класс View - обертка для DOM элемента, предоставляющая интерфейс для упрощения работы с ним.
Функция $mol_dom_render_children рендерит дочерние элементы, без лишних вставок и удалений в DOM-дереве. Сейчас нам нет смысла ее реализовывать, поэтому воспользуемся готовой из $mol.
Теперь создадим несколько компонентов на базе класса View. Мы наследуемся от него, переопределяем нужные методы.
И добавляем класс с логикой приложения.
Как собрать модуль вручную?
Сборка запускается командой
npm start путь/до/модуля
. При ручном запуске, сборщик собирает все бандлы, которые поддерживает.Соберите приложение
После запуска, сборщик вернет ошибку
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
Запустите сборку снова.
Где искать результаты сборки?
Выше мы уже говорили, что бандлы помещаются в директорию
-
, она создается в директории модуля. Также вы можете увидеть еще несколько директорий название которых начинается со знака минус-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.*.map
source 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-пакеты, то они будут указаны в зависимостях. Если этот файл присутствует в модуле, то он буде объединен со сгенерированным файлом.Запуск дев-сервера
Давайте запустим дев-сервер, он перезапускает сборку при изменении зависимостей модуля и перезагружает страницу в браузере.
Выполните команду:
Ссылка
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
-<script src="web.js"></script>
. Браузер сделает запрос по такому адресуhttp://127.0.0.1:9081/my/counter/-/web.js
.Дев-сервер анализирует адрес запроса, получает путь до модуля, и какой файл запрошен, выполняет сборку запрошенного бандла, кэширует результат и отправляет в браузер. При изменении файла кэш сбрасывается, а браузеру отправляется команда перезагрузить вкладку, после чего файл запрашивается снова и происходит его сборка.
Дев-сервер собирает только те файлы, которые непосредственно запрашиваются из браузера, для того чтобы собрать все артефакты, необходимо запустить сборку вручную.
Сейчас дев-сервер поддерживает сборку только веб-приложений. Если вы разрабатываете NodeJS проект, то вы можете запустить дев-сервер и вручную отправлять запрос за файлом
node.test.js
, для его пересборки.Как импортировать и экспортировать модули?
MAM автоматически отслеживает зависимости между модулями, анализируя FQN-имена в исходниках. Если мы хотим "экспортировать", какую-то сущность из модуля, чтобы она была доступна в других модулях, необходимо дать ей имя в формате FQN. Чтобы использовать метод, функцию или любую сущность из другого модуля, нужно просто написать ее имя.
Например, в модуле
$my_csv
объявлено две функцииИми можно воспользоваться в любом другом модуле, просто написав имя
$my_csv_decode( 'q;w;\n1;2' )
, будто она объявлена выше в этом же файле.Обратите внимание, что совпадать должен только префикс имени
$my_csv_
с путем до файлаmam/my/csv
. В этом же файле мы можем объявить функцию с таким именем$my_csv_decode_stream
, это не значит что мы обязаны класть эту функцию вmam/my/csv/decode/stream
.Теперь давайте воспользуемся FQN-именами в нашем приложении и заодно разобьем его на несколько модулей.
Получится такая структура:
View
в$my_counter_view
, создайте файлmam/my/counter/view/view.ts
и перенесите туда код этого класса.Button
И с классом
Input
В файле
mam/my/counter/counter.ts
остался классCounter
. Измените его имя на$my_counter
и имена переименованных классов.Запустите дев-сервер, если еще не сделали этого, и убедитесь что приложение работает.
Что если модуль используется много раз и его имя слишком длинное?
Положите ссылку на него в переменную с более коротким именем.
Станет:
Какие модули включаются в дистрибутив?
Сборка работает по нескольким правилам:
include
иrequire
, а также копируются статические файлы командойdeploy
. Подробнее рассматривается ниже./a/b/c
, в бандлы будут включены модулиc
,b
,a
,/
. Код родительского модуля, будет включен раньше кода модуля. Как пример можно рассмотреть модуль mam, он находится непосредственно в репозитории с дев-окружением, и будет включен в дистрибутив при сборке любого модуля.Добавим модуль для работы с localStorage. Создайте директорию для модуля
$my_counter_storage
иts
файл.Сейчас не нужно его использовать в
$my_counter
.Добавьте в файл
mam/my/counter/button/button.ts
еще одну кнопку -$my_counter_button_minor
. Она не будет использоваться, нужна только для демонстрации.После сборки откройте бандл
web.js
. Найдите класс$my_counter_button_minor
, он включен в бандл, потому что модуль$my_counter_button
используется в приложении, а класс минорной кнопки объявлен именно в нем. Если вынести объявление кнопки в отдельный модульmam/my/counter/button/minor
, тогда она не добавится в бандл.Класс
$my_counter_storage
вы не найдете в бандле, потому что он не используется в приложении.Теперь используем модуль
$my_counter_storage
в коде.После сборки вы найдете модуль
$my_counter_storage
в бандле.Зависимость между
css
иtss
Создайте модуль
$my_theme
с файломtheme.ts
Код выше, раз в 30 секунд меняет значение атрибута
my_theme
, на элементеhtml
.Создайте
css
файл в модуле$my_counter
В
css
знак$
в FQN-именах не используетсяОткройте приложение, вы увидите что цвет фона страницы, меняется каждые 30 секунд.
Сборщик находит имя модуля
$my_theme
в файлеcounter.css
и добавляет все файлы модуля$my_theme
в бандлы. Найдите его код в бандлеweb.js
.После добавления
css
файла, вы найдете несколько новых модулей вjs
бандле. Сейчас стили добавляются вweb.js
бандл с помощью функции$mol_style_attach
. Она и ее зависимости добавлены в бандл.Как перенести модуль в другое место?
Во время рефакторинга, может понадобится перенести модуль в другое место. Поскольку имена зависят от месторасположения, их тоже нужно менять. Вам нужно выполнить команду редактора
find and replace
. Например, ищем все имена с именемmy_module_name
и заменяем наmy_module_new_name
, после перемещения файлов.В meta.tree вместо FQN используются пути через
/
, их придется менять отдельноВынесем в отдельный модуль, модули с библиотечным кодом
$my_counter_view
,$my_counter_button
и т.д., а в модуле$my_counter
останется только код приложения.Создайте директорию для модуля
$my_lom
Переместите директорию
$my_counter_view
в$my_lom_view
, и используяfind and replace
переименуйте все строкиmy_counter_view
вmy_lom_view
. Поиск и замена производится на уровне корневой директорииmam
.Повторите проделанное выше с остальными модулями:
$my_counter_button
,$my_counter_input
,$my_counter_storage
,$my_theme
.Сейчас весь библиотечный код находится в модуле
$my_lom
, а в модуле$my_counter
остался только прикладной код. Убедитесь что приложение работает.Монорепозитории и полирепозитории
MAM поддерживает работу с обоими типами репозиториев одновременно. Их можно вкладывать друг в друга, также как и модули. Сборщик автоматически клонирует отсутствующие репозитории. Можно произвольный модуль вынести в отдельный репозиторий, без изменения его исходного кода. Разработчик использует одно дев-окружение, в котором находятся все проекты рядом, каждый в своем неймспейсе/модуле, в том числе неймспейс
mol
. Т.е. при разработке это выглядит как один большой монорепозиторий, можно внести изменения в любой модуль, но при этом этот код может хранится во множестве удаленных репозиториях на github, gitlab, bitbucket и т.д.Для того чтобы сборщик мог загрузить репозиторий, он должен знать его адрес. Ссылки на удаленные репозитории находятся в файле
*.meta.tree
, который размещается в родительском модуле, по отношению к модулю который выносится в удаленный репозиторий. Пример файлаmeta.tree
Команда выглядит так:
pack apps git \https://github.com/hyoo-ru/apps.hyoo.ru.git
pack
- инструкция, говорит сборщику что перед нами удаленный репозиторийapps
- имя директории подмодуля, в которую будет загружен репозиторийgit
- система контроля версия, сейчас поддерживается только одна.\
размещается ссылка на репозиторий.Разберем на примере как это работает:
yarn start hyoo/draw
.mam/hyoo
, ищетmeta.tree
в директорииmam
meta.tree
находит строку в которой директория подмодуля называетсяhyoo
-pack hyoo git \https://github.com/hyoo-ru/mam_hyoo.git
mam/hyoo
mam/hyoo/draw
, не найдя заглядывает вmam/hyoo/hyoo.meta.tree
, теперь его интересует строка начинающаяся сpack draw
. Найдя ее, клонирует репозиторий вmam/hyoo/draw
.$hyoo_draw
Для того, чтобы VSCode корректно работал с несколькими репозиториями, создайте файл
mam/.gitmodules
с подобным содержимым:На вкладке контроля версий, у вас появится несколько репозиториев.
VSCode не всегда сразу отображает репозитории перечисленные в
.gitmodules
на вкладкеSource Control
. Чаще всего это решается перезапуском VSCode. Если перезапуск не помогает, то нужно изменить порядок модулей в.gitmodules
Сейчас мы вынесем неймспейс
$my
и модули$my_lom
,$my_counter
в отдельные репозитории.mam_my
на github.com$my
и свяжите его с репозиторием созданным на github. В листингах ниже**YOUR_NAME**
замените на свое значение$my_lom
и$my_counter
. Репозитории назовитеmy_lom
иmy_counter
соответственно.mam/my/my.meta.tree
с таким содержимым:Удалите директории модулей
$my_lom
и$my_counter
, и запустите сборку модуля$my_counter
-npm start my/counter
. Сборщик загрузит репозитории и соберет модуль. В логе вы увидите следующее:Разделение кода на платформы
MAM поддерживает две платформы - браузер и NodeJS. Файлы кода с тегом
web
будут включены только вweb.js
бандл, а с тегомnode
только вnode.js
. Код без тега платформы попадет в оба бандла.Ранее, мы добавили костыль
if (typeof document === undefined) return
, для того чтобы не падали тесты под NodeJS. Теперь исправим его.Но сначала займемся небольшим рефакторингом, добавьте два статических метода в класс
$my_lom_view
.Мы переместили метод
mound
и добавили методroot
, который поможет указать какой компонент является корневым. Удалите методmount
и его вызов из класса$my_counter
и добавьте присвоение компонента методуroot
.Создайте файла
view.web.ts
в модуле$my_lom_view
.Этот код будет добавлен только в
web
бандл. Методmount
вызывается асинхронно, потому что в бандле$my_lom_view
находится выше, чем$my_counter
и установка значения происходит после запуска этого кода. Соберите модуль$my_counter
вручную и проверьте что этот код присутствует вweb.js
бандле и отсутcтвует в бандлеnode.js
.Как использовать NPM-пакеты в
node
?Для использования NPM пакетов и модулей с которыми поставляется NodeJS создан специальный модуль
$node
. Вы просто пишите$node['is-odd']
или$node.express
. Сборщик MAM автоматически установит эти пакеты их typescript типы, добавит их в бандлpackage.json
.Сейчас мы добавим изоморфности в наше приложение. Создайте директорию для модуля
$my_lom_dom_context
и файлcontext.ts
.В общем коде для
node
иweb
, мы просто объявили переменную, которой будет присвоено различное значение в зависимости от платформы. Создайте файлcontext.web.ts
.В
web
мы присваиваем этой переменной объектwindow
. Создайте файлcontext.node.ts
.В
node
мы присваиваем этой переменной экземпляр классаJSDOM
, с помощью него наш код сможет работать под NodeJS. Дляssr
такого решения не достаточно, но для тестов вполне.as any
потому чтоjsdom
не реализует все браузерноеAPI
и его типы сtypeof globalThis
не сходятся.Замените прямые обращения к
document
, на использование$my_lom_dom_context.document
:Запустите сборку вручную
npm start my/counter
. Если директорияmam/node_modules
не содержитjsdom
, вы увидите в терминале подобные сообщения:В файл
mam/my/counter/-/package.json
добавятся зависимости:Модуль
$node
имеет зависимость от модуля, который использует NPM-пакетcolorlette
, поэтому он тоже добавился вpackage.json
.*
используется, потому что MAM использует модельverless
с NPM тоже. В экстренном случае, можно в директории модуля создатьpackage.json
и зафиксировать версию пакета в нем.Файл
mam/my/counter/-node/deps.d.ts
содержит typescript типы для всех модулей NPM и NodeJS.Как добавить статический файл в дистрибутив?
Для этого в
*.meta.tree
предусмотрена командаdeploy
. Нужно его создать и добавить строкуdeploy \path/to/file/image.png
. При сборке файл будет скопирован вmam/my/module/-/path/to/file/image.png
. Пример meta.tree файла.Давайте добавим fav-иконку к нашему приложению. Создайте модуль
$my_counter_logo
и скачайте туда эту иконку.В модуле
$my_counter
, создайте файлcounter.meta.tree
:Соберите модуль
npm start my/counter
, после сборки, вы найдете иконку по путиmam/my/counter/-/my/counter/logo/logo.svg
.В
index.html
добавьте строку<link href="my/counter/logo/logo.svg" rel="icon">
внутри тегаhead
.Как включить модуль в проект, на который никто не ссылается?
Сборщик автоматически включает в дистрибутив модули от которых зависит собираемый модуль. Иногда вам нужно добавить модули, от которых ваш код не зависит. Например, когда вы создаете приложение с каталогом компонентов. Для этого используются две команды
require
иinclude
, которые добавляются в файлmeta.tree
. Если необходимо подключить код указанного в команде модуля, раньше кода модуля в котором находитсяmeta.tree
, то используйтеrequire
иначе используйтеinclude
.Создайте директорию для модуля
$my_lom_lib
и внутри файлlib.meta.tree
, с таким содержимым:Соберите модуль
$my_lom_lib
и проверьтеjs
бандл. Все подключенные модули будут добавлены в него.Развертывание модуля в NPM
Теперь настроим автопубликацию модуля
$my_lom_lib
в NPM. Для начала создайте аккаунт или войдите.Откройте бандл
mam/my/lom/lib/-/package.json
и проверьте полеname
. Имя генерируется автоматически, несколько человек не смогут опубликовать модуль, если проходя это руководство используют имяmy
для неймспейса. Создайте файлpackage.json
в модуле$my_lom_lib
с другим именем.Поля из добавленного
package.json
будут объедены с полями сгенерированногоpackage.json
. Если вы сейчас соберете модуль, то увидите что бандле имя изменилось.Убедимся что имя свободно.
Если занято, измените его. Создайте персональный токен доступа для NPM.
Создайте секрет в репозитории
my_lom
с именемNPM_AUTH_TOKEN
и созданным токеном в качестве значения.Создайте файл
mam/my/lom/.github/workflows/my_lom_lib.yml
:Отправьте изменения на гитхаб и через несколько минут, пакет будет опубликован в NPM.
При использовании NPM-пакетов, мы просто экспортируем их имена из нужного бандла
web
илиnode
.Как использовать NPM в
web
?Чтобы NPM-пакет работал в браузере, его код необходимо добавить в бандл
web.js
.На данный момент MAM сборщик самостоятельно обрабатывает код NPM-пакетов, т.к. он не заточен под NPM, это приводит к некоторым проблемам. Например не умеет удалять неиспользуемые ветки кода -
if (process.env.NODE_ENV === 'development') {}
. Так же у MAM нет необходимости вtreeshaking
, т.к. он сразу подключает только используемые модули, но NPM-пакетам он нужен. В будущем код NPM-пакетов, будет обрабатываться отдельным бандлером, таким какwebpack
и т.п.В общем виде процесс подключения NPM-пакета выглядит так:
require
Простой пример подключения
ramda
. Код модуля может быть сложнее простого реэкспорта. Например там может быть какой-нибудь адаптер, или демо с примером использования модуля.Для NPM-пакетов создан отдельный модуль
$lib
, ссылка на репозиторий. При добавлении какого-то пакета, лучше добавлять его сразу туда.При сборки модуля, NPM-пакеты установятся автоматически. При необходимости версию можно зафиксировать в корневом
package.json
Сейчас мы не будем будем использовать написанное ранее приложение, а добавим модуль подключающий
ReactJS
в MAM. Он уже добавлен сюда. Для практики, вы можете самостоятельно добавить другой пакет по примеру ниже.Для начала нужно загрузить репозиторий модуля
$lib
, запуститеnpm start lib
. Загрузить его можно и черезgit clone
, но ссылка на репозиторий уже находится вmam/.meta.tree
, сборщик сам загрузит его по ней.Создайте директорию для модуля
$lib_react
иts
файл в ней.Тут используется
namespace $ {}
- они используются в системе инверсии контроля$mol
, как средство авторегистрации сущностей в контейнере, об этом мы поговорим в другой главе.Мы просто импортировали
ReactJS
и положили его в$lib_react
. В$lib_react_jsx
вынесена функцияcreateElement
, она будет использоваться как короткая ссылка для указания какую функцию использовать при транспиляцииtsx
.Создадим модуль для
react-dom
-$lib_react_dom
В модуль
$lib_react_demo
положим приложение счетчик, для иллюстрации работы с модулем$lib_react
.Попробуйте открыть демо-приложение. Запустите дев-сервер и откройте в файловом менеджере
lib/react/demo
. Вы увидите ошибку.Сборщик не умеет вырезать такие куски кода, пока что обойдемся заглушкой. Создадим для этого модуль
$lib_react_env
.Используем
var
, чтобы без дополнительного кода объявить переменную на глобальном объекте.namespace
тут не нужен, т.к. тогда typescript завернет этот код в вызов функции.Нам нужно, чтобы этот код был включен в бандл и выполнен раньше, чем код
ReactJS
. Создадим файлmam/lib/react/react.meta.tree
Код из модуля
$lib_react_env
, будет подключен в начало банла, перед кодом ReactJS.Обновите страницу, приложение должно заработать. Этот адаптер нуждается в доработке, но для начала годится.
Развертывание приложения на Github Pages
Для этого нужно создайте файл
.github/workflows/deploy.yml
в модуле, который вынесен в отдельный репозиторий.После отправки коммита, приложение будет задеплоено. Исходники github action тут.
Сейчас настроим деплой нашего приложения на github. Сначала создайте персональный токен доступа тут. Создайте секрет в настройках репозитория
my_counter
, с созданным токеном, назовите его GH_PAT.Создайте файл
deploy.yml
, не забудьте заменить\*\*YOUR_NAME\*\*
в ссылке на репозиторийmam_my
.Параметры, которые принимает github action описаны в readme. Параметр
meta
содержит ссылку на репозиторий неймспейсаmam_my
, т.к. у него в файлеmy.meta.tree
указана ссылка на репозиторий модуля$my_lom
, который нужен для сборки приложения. Если его не указывать, то сборка завершится ошибкой - не найден модуль`$my_lom
.После отправки изменений на github, начнется сборка, после ее окончания, вы найдете ссылку на приложение тут:
https://github.com/**YOUR_NAME**/my_counter/settings/pages
. Скопируйте ее в описание репозитория.Циклические зависимости
Сборщик оценивает жесткость зависимостей, чем она сильнее тем раньше модуль будет включен в бандл. Жесткость оценивается по простой эвристике - количеству отступов в строке, в которой находится зависимость.
Для демонстрации циклической зависимости, мы создадим несколько файлов с кодом вне директории MAM и запустим их с помощью NodeJS.
Создайте где-нибудь директорию
cyclic
и файлall.mjs
Запустите его
node app.mjs
- ошибок нет. В нем есть циклическая зависимость, но код работает корректно. Разделим его на несколько файлов.Запустите
node app.mjs
, вы увидите ошибкуReferenceError: Cannot access 'Foo' before initialization
. Для ее исправления, нужно добавить импортbar.mjs
передfoo.mjs
.Теперь перенесем этот код в MAM. Создайте модули
$my_foo
,$my_bar
,$my_app
с соответствующим содержимым.Соберите модуль
$my_app
и посмотрите код вjs
бандле.Сборщик склеивает файлы в таком порядке, как будто мы изначально писали код в одном файле. Зависимость
$my_bar
от$my_foo
сильнее чем$my_foo
от$my_bar
, поэтому модули добавляются в бандл в таком порядке:$my_foo
,$my_bar
,$my_app
.Рекомендации по использованию MAM
meta.tree
через pull-request.actions
,components
,containers
,helpers
,utils
и т.п. Модуль должен представлять из себя домен, который хорошо решает свою задачу. Например "хелпер" для сравнения объектов, можно отправить в модуль$my_object
или сразу вынести в подмодуль$my_object_equal
и использовать его на всех проектах.Далее
В следующей части мы научимся работать с системой реактивности $mol