Предисловие
Эта статья открывает серию публикаций по обучению фреймворку $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
с таким содержимым:
<!doctype html>
<html style="height: 100%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
</head>
<body style="width: 100%; height: 100%; margin: 0">
<div id="root"></div>
<script src="web.js"></script>
</body>
</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 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<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
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
генерируется автоматически, с таким контентом:
<!doctype html><meta charset="utf-8" /><body><script src="web.js" charset="utf-8"></script>
<script src="/mol/build/client/client.js" charset="utf-8"></script>
<script>
addEventListener( 'load', ()=> {
const test = document.createElement( 'script' )
test.src = 'web.test.js'
const audit = document.createElement( 'script' )
audit.src = 'web.audit.js'
test.onload = ()=> document.head.appendChild( audit )
document.head.appendChild( test )
} )
</script>
Тут подключается 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
и перенесите туда код этого класса.Тоже самое делаем с классом
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<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<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
в бандле.
Зависимость между css и ts
Создайте модуль $my_theme
с файлом theme.ts
// mam/my/theme/theme.ts
setInterval( ()=> {
document?.documentElement.setAttribute(
'my_theme' ,
new Date().getSeconds() < 30 ? 'light' : 'dark' ,
)
} , 1_000 )
Код выше, раз в 30 секунд меняет значение атрибута my_theme
, на элементе html
.
Создайте css
файл в модуле $my_counter
/* mam/my/counter/counter.css */
[my_theme="light"] {
background-color: white;
}
[my_theme="dark"] {
background-color: black;
}
В 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
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
с подобным содержимым:
[submodule "mam_mol"]
path = mol
url = git@github.com:hyoo-ru/mam_mol.git
[submodule "hyoo"]
path = hyoo
url = git@github.com:hyoo-ru/hyoo.git
На вкладке контроля версий, у вас появится несколько репозиториев.
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
соответственно.
cd mam/my/lom
echo "# mam_lom" > readme.md
echo "-*" > .gitignore
git init
git add --all
git commit -m "Init"
git branch -M main
git remote add origin https://github.com/**YOUR_NAME**/my_lom.git
git push -u origin main
cd mam/my/counter
echo "# mam_counter" > readme.md
echo "-*" > .gitignore
git init
git add --all
git commit -m "Init"
git branch -M main
git remote add origin https://github.com/**YOUR_NAME**/my_counter.git
git push -u origin main
Создайте файл
mam/my/my.meta.tree
с таким содержимым:
pack lom git \https://github.com/**YOUR_NAME**/my_lom.git
pack counter git \https://github.com/**YOUR_NAME**/my_counter.git
Отправьте коммит с ним в удаленный репозиторий
cd mam/my
git add my.meta.tree
git commit -m "Add meta.tree"
git push
Удалите директории модулей $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
.
// mam/my/lom/view/view.ts
static root: ()=> typeof $my_lom_view
static mount() {
const node = document.querySelector( '#root' )
if ( !node ) return
const View = this.root()
const obj = new View
node.replaceWith( obj.dom_tree() )
setInterval( ()=> obj.dom_tree() , 100 )
}
Мы переместили метод mound
и добавили метод root
, который поможет указать какой компонент является корневым.
Удалите метод mount
и его вызов из класса $my_counter
и добавьте присвоение компонента методу root
.
// mam/my/counter/counter.ts
// ...
// - static mount() {
// - if ( typeof document === 'undefined' ) return
// -
// - const node = document.querySelector( '#root' )
// - const obj = new $my_counter()
// -
// - node?.replaceWith( obj.dom_tree() )
// -
// - setInterval( ()=> obj.dom_tree() , 100 )
// - }
}
// - $my_counter.mount()
$my_lom_view.root = ()=> $my_counter // +
Создайте файла view.web.ts
в модуле $my_lom_view
.
// mam/my/lom/view/view.web.ts
Promise.resolve( ()=> $my_lom_view.mount() )
Этот код будет добавлен только в 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
.
// mam/my/lom/dom/context/context.web.ts
$my_lom_dom_context = self
В 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
:
// $my_lom_view.mount
const node = $my_lom_dom_context.document.querySelector( '#root' )
// $my_lom_view.dom_node
const node = $my_lom_dom_context.document.createElement( this.dom_name() )
// $my_lom_theme
$my_lom_dom_context.document.documentElement.setAttribute( /*...*/ )
Запустите сборку вручную 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.
interface $node {
"jsdom" : typeof import( "jsdom" )
"colorette" : typeof import( "colorette" )
"path" : typeof import( "path" )
"child_process" : typeof import( "child_process" )
}
Как добавить статический файл в дистрибутив?
Для этого в *.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
с другим именем.
// mam/my/lom/lib/package.json
{
name: "my_lom_lib_test"
}
Поля из добавленного package.json
будут объедены с полями сгенерированного package.json
. Если вы сейчас соберете модуль, то увидите что бандле имя изменилось.
Убедимся что имя свободно.
npm search my_lom_lib_test
Если занято, измените его.
Создайте персональный токен доступа для NPM.
npm token create
Создайте секрет в репозитории my_lom
с именем NPM_AUTH_TOKEN
и созданным токеном в качестве значения.
Создайте файл mam/my/lom/.github/workflows/my_lom_lib.yml
:
name: my_lom_lib
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Environment Printer
uses: managedkaos/print-env@v1.0
- name: Build apps
uses: hyoo-ru/mam_build@master2
with:
package: my/lom
modules: lib
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}
package: ./my/lom/lib/-/package.json
Отправьте изменения на гитхаб и через несколько минут, пакет будет опубликован в 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 $ {
const React = require('react') as typeof import('react')
export const $lib_react = React
export const $lib_react_jsx = React.createElement
}
Тут используется namespace $ {}
- они используются в системе инверсии контроля $mol
, как средство авторегистрации сущностей в контейнере, об этом мы поговорим в другой главе.
Мы просто импортировали ReactJS
и положили его в $lib_react
. В $lib_react_jsx
вынесена функция createElement
, она будет использоваться как короткая ссылка для указания какую функцию использовать при транспиляции tsx
.
/** @jsx $lib_react_jsx */
Создадим модуль для react-dom
- $lib_react_dom
.
// mam/lib/react/dom/dom.ts
namespace $ {
const ReactDOM = require('react-dom') as typeof import('react-dom')
const Client = require('react-dom/client.js') as typeof import('react-dom/client')
export const $lib_react_dom = ReactDOM
export const $lib_react_dom_client = Client
}
В модуль $lib_react_demo
положим приложение счетчик, для иллюстрации работы с модулем $lib_react
.
<!-- mam/lib/react/demo/index.html -->
<!doctype html>
<html style="height: 100%">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
<div id="root"></div>
<script src="web.js"></script>
// mam/lib/react/demo/demo.tsx
/** @jsx $lib_react_jsx */
namespace $ {
export function $lib_react_demo() {
const [count, count_set] = $lib_react.useState( 0 )
return <div>
<button onClick={()=> count_set(count - 1)}>-</button>
<input
type="text"
value={count}
onChange={e => count_set( Number( (e.target as HTMLInputElement).value ) )}
/>
<button onClick={()=> count_set(count + 1)}>+</button>
</div>
}
const element = document.getElementById( 'root' )
if ( !element ) throw new Error('Cannot find root element')
const root = $lib_react_dom_client.createRoot( element )
root.render( <$lib_react_demo /> )
}
Попробуйте открыть демо-приложение. Запустите дев-сервер и откройте в файловом менеджере 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
в модуле, который вынесен в отдельный репозиторий.
name: Deploy
on:
workflow_dispatch:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: hyoo-ru/mam_build@master2
with:
token: ${{ secrets.GH_PAT }}
package: 'my/wiki'
- uses: alex-page/blazing-fast-gh-pages-deploy@v1.1.0
if: github.ref == 'refs/heads/master'
with:
repo-token: ${{ secrets.GH_PAT }}
site-directory: 'my/wiki/-'
После отправки коммита, приложение будет задеплоено. Исходники github action тут.
Сейчас настроим деплой нашего приложения на github. Сначала создайте персональный токен доступа тут. Создайте секрет в настройках репозитория my_counter
, с созданным токеном, назовите его GH_PAT.
Создайте файл deploy.yml
, не забудьте заменить \*\*YOUR_NAME\*\*
в ссылке на репозиторий mam_my
.
# mam/my/counter/.github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build app
uses: hyoo-ru/mam_build@master2
with:
token: ${{ secrets.GH_PAT }}
package: 'my/counter'
meta: |
my https://github.com/**YOUR_NAME**/mam_my
- name: Deploy on GitHub Pages
if: github.ref == 'refs/heads/main'
uses: alex-page/blazing-fast-gh-pages-deploy@v1.1.0
with:
repo-token: ${{ secrets.GH_PAT }}
site-directory: 'my/counter/-'
Параметры, которые принимает 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
.
// cyclic/app_fix.mjs
import './bar.mjs';
import { Foo } from './foo.mjs';
console.log(new Foo().bar);
Теперь перенесем этот код в MAM. Создайте модули $my_foo
, $my_bar
, $my_app
с соответствующим содержимым.
// mam/my/foo/foo.ts
class $my_foo {
get bar() {
return new $my_bar();
}
}
// mam/my/bar/bar.ts
class $my_bar extends $my_foo {}
// mam/my/app/app.ts
console.log(new $my_foo().bar);
Соберите модуль $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
Контакты
https://t.me/mam_mol - с вопросами, проблемами, предложеними пишите в наш чат. Вступайте, если следите за проектом.
https://t.me/mol_news - новости об экосистеме фреймворка $mol.
Комментарии (18)
mclander
25.04.2022 01:19Привет всем домотавшим (дочитавшим) до этого места. Я сдался на 40% статьи, зато я очень уважаю теперь тех, кто использует MAM
APXEOLOG
25.04.2022 01:31+1Почему был выбран сиподобный стиль названия методов фреймворка? Смотрится дико
PavelZubkov Автор
25.04.2022 11:20+1Изначально snake_case был выбран только для написания FQN-имен, потому что знаки подчеркивания имеют наибольшую поддержку среди языков, а это нужно чтобы имена одной сущности везде писались одинаково (html, css, js/ts, ... ). Но когда к нему привыкаешь, начинает казаться, что он удобнее CamelCase, имхо. Вообщем теперь он используется везде, но кое-где еще можно найти CamelCase в коде который давно не трогали.
Finesse
25.04.2022 05:31+13Статья очень большая, еле дочитал. В ходе прочтения складывается впечатление, что MAM революционно меняет систему работы с модулями, выкидывая традиционный
import
на свалку. Новая система очень напоминает систему работы с модулями в PHP (а именно PSR-4), которая является скорее костылём, чем намеренно созданной системой. Революционная смена подхода имеет недостатки (именно смена, а не новый подход):Инструменты разработки не умеют работать с новой системой. Это, например, лишает IDE магии автокомплита и рефакторинга. Насколько я понимаю, есть плагин для VSCode, но это не единственная IDE, и в целом получается ситуация «по умолчанию не работает».
Новый подход контринтуитивен для разработчиков, что является препятствием для входа в проект. Насколько я понимаю, MAM не трудно освоить, но в некоторых проектах может быть недостаточно мотивации для этого, например, когда кто-то хочет помочь проекту с открытым исходным кодом.
Проект становится заложником MAM, потому что только он поддерживает новый подход.
Нужна очень большая причина, чтобы принять недостатки, описанные выше. Я не вижу такой в статье. Все описанные возможности можно реализовать с имеющейся системой модулей, встроенной в JavaScript. Не хватает описания юз-кейсов, в которых MAM делает жизнь проще (чем с обычным
import
и другими фреймворками). В целом статья выглядит как документация, то есть описание возможностей, а не решаемых проблем.Из статьи не понятно, почему новички в JavaScript должны изучать и использовать систему модулей MAM, а не
import
/export
, которую использует вся остальная экосистема.Не понятно, зачем упоминается $mol. Про него было бы интересно почитать. Если MAM является необходимым условием для использования $mol, то он должен быть ну очень хорош, чтобы оправдать смену корневой парадигмы.
nin-jin
25.04.2022 10:02Finesse
25.04.2022 11:52Я имею ввиду не обращение к свойствам объекта. Я имею ввиду автоматический импорт переменных по их имени. Например, если в этом примере:
Пример
"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
Что будет, если в IDE кликнуть на последний
$my_foo
, удерживая кнопку Cmd? Подсветит ли IDE ошибку TS, если я напишуnew $my_foo().baz
? Сделает ли это WebStorm? Jump to definition на GitHub, думаю, тоже не поддерживается.
PavelZubkov Автор
25.04.2022 12:02Да, это документация, точнее обучалка, которая последовательно вводит в $mol. Первое с чем надо разобраться - MAM.
Наверно, я не правильно акцентировал внимание на FQN. Все таки избавление от импортов не самоцель, возможно в будущем они будут использоваться.
Плагин в VSCode нужен для подсветки синтаксисаtree
языков. Автокомплит, рефакторинг работают с typescript кодом. (покрайней мере с VSCode проблем не должно быть, на другие редакторы пока нет ресурсов)
Я бы сказал не "контринтуитивен", а "не привычен". Не думаю, что нужно много времени чтобы разобраться и привыкнуть. Только дело в желании.
Большой проект и сwebpack
можеть быть сложно на что-нибудь другое перевести. Если подумать про переход с MAM, только нужно сгенерировать импорты/экспорты скриптом, все пути содержаться в FQN-именах.
MAM больше про стандартизацию, про искусственные ограничения, которые создают такие рамки, чтобы без усисилий получался хорошо огранизованный, переиспользуемый код. Противополжность этому - полная свобода, когда разработчик может сделать так, как ему захочется. И наверно $mol целиком этой темой пропитан, про ограничения упрощающие разработку.
Для примера, можно сравнить вынос части кода проекта в отдельный репозиторий, например с nx - система которая позволяет почти полную свободу внутри кода.
tzlom
25.04.2022 09:27+1Туториал большой, я бы даже на несколько его разбил, но где ссылки на документацию?
PavelZubkov Автор
25.04.2022 11:30Именно по MAM, документации нет, только статья MAM: сборка фронтенда без боли. Ссылаться пока некуда.
В $mol документация есть, у каждого модуля она своя, находится в файле readme.md в директории модуля. Пример. На сайте пока ее нет, и не у всех модулей она есть, которым нужна. Но в целом, все не так плохо с этим, особенно если учесть то, что у большинства модулей кода не много и он простой.
Молу не хватает не документации, а именно обучалки, которая последовательно знакомит со всем необходимым. Эта статья и есть первая часть этой обучалки, позже она будет на сайте мола.
gian_tiaga
25.04.2022 10:39+4Инструмент по идее должен облегчить работу программиста. А здесь сплошное усложеение. Скажите, в чём преимущество данного инструмента. Вот есть у меня vue к примеру. И я подобное реализую намного меньшими усилиями.
nin-jin
25.04.2022 11:28+1Так в статье собственно $mol и не используется. MAM - это скорее аналог vue-cli, только не прибит гвоздями к одному фреймворку, что в статье и продемонстрировано.
funca
25.04.2022 15:35+2Вы рассказали о преимуществах, интересно какие есть компромиссы и ограничения.
Был-ли опыт миграции с Webpack и с какими особенностями приходилось сталкиваться? Может есть цифры, было бы интересно увидеть сравнение по времени сборки (холодной и инкрементальной если поддерживаете), размеры бандлов на выходе, объём необходимого рефакторинга существующего кода, особенности интеграции с другими инструментами (статические анализаторы кода, hot-reload, поддержка в популярных IDE).
Напомнило https://github.com/enb/enb, который Яндекс пиарил на всех конференциях много лет тому назад.
PavelZubkov Автор
25.04.2022 23:31+1Цели рассказать о преимуществах не было, только немного теории в виде идей/концепций и побольше практики, чтобы тот у кого есть желание смог разобраться.
По сложностям, например MAM генерирует заранее заданные бандлы, если например разрабатывается расширение для браузера, могут быть сложности, т.к. там может быть несколько точек входа дляhtml
, скриптов и т.п. Придется либо как-то внутри кода выкручиваться, либо расширять MAM.
Также MAM хоть и вырос вместе с $mol, и заточен под него и под веб, но можно разрабатывать бекенд на js, и в теории на других языках, но нужно делать поддержку.
Еще не разлизованы некоторые возможности, например используется безверсионированная схема, но у разработчика нет автообновления, т.е. репозитории и NPM-пакеты надо обновлять вручную. Но хотябы на CI при деплои скачиваются свежие версии.
Код NPM-пакетов, лучше собирать чем-нибудь заточенным под него, что умеет в тришейкинг, вырезание всякихprocess.env.NODE_ENV
и т.п. Сейчас MAM-сборщик их собирает.
Еще не решенная проблема, с дублированием кода в NPM-пакетах. Если выложить несколько пакетов, у которых есть общий код, он будет продублирован в каждом. При использовании нескольких таких пакетов в NPM-приложении, будет увеличен его размер.
Код MAM разросся, сложно быстро въехать чтобы что-то поправить. Новая версия начата, но пока некому доделывать.
Миграцию веб-приложений с webpack врядли кто-то делал, но думаю для сколько-нибудь большого проекта это будет трудоемко. Надо удалять импорты/экспорты, имена переделывать на FQN, а после наверно еще придется рефакторить расположение файлов, т.к. имена не понравятся. Придется делать адаптеры для всех используемых в проекте NPM-пакетов, а также скорее всего придется кодовую базу обновлять под последние версии этих пакетов (если полноценно использовать verless).
Сравнений с другими сборщиками нет, думаю пока используется старая версия нет смысла делать. Инкрементальная сборка есть, там сборщик реактивный, в том плане что используется таже реактивная система из $mol. После обработки файл кешируется, при изменении файла - кеш сбрасывается и т.д.
В некоторой степени $mol/MAM - это развитие идей классического БЭМ стека.
Raspy
Наконец-то появилась поддержка тайпскрипт. Помню много раз наталкивался на $mol и каждый раз меня отталкивал доморощенный синтаксис. Похоже пришло время опробовать и сделать что-то простенькое, а то на рынке фреймвёрков натуральный голод.
PavelZubkov Автор
"синтаксис" всё-таки никуда не делся, но компоненты можно без него создавать на чистом ts. Правда тогда придется решать задачи вручную, которые решает
view.tree
. В нем нет ничего сложного, через одну статью он будет подробно разбираться. Сложность только в том, что он не похож ни на что, к чему мы уже привыкли, это как первый раз увидеть html. Он такой не просто так, т.к. проектировался под конкретные задачи.