dresscodejs
v2.0.8
Published
DressCodeJS – это методология построения архитектуры JavaScript-приложений и утилита сборки JS-кода, написанного по этой методологии.
Readme
DressCodeJS
DressCodeJS – это методология построения архитектуры JavaScript-приложений и утилита сборки JS-кода, написанного по этой методологии.
Утилита построена на Jossy – сборщике JS-файлов.
Содержание
- Идея
- Структура проекта
- Как работает сборка
- Установка и настройка проекта
- Сборка
- Директивы
- Макрос $$
- Условная сборка и слои
- Разработка через Yaxy
- Существующие библиотеки
- Ограничения
Идея
Согласно DressCodeJS, приложение состоит из компонентов – классов и статических объектов, содержащих вложенные классы, объекты или самостоятельные функции.
Главные правила:
- Компонент именован в CamelCase – имя начинается с заглавной буквы. Это важно: сборщик по имени определяет, что такое есть компонент.
- Каждый компонент лежит в своём файле с именем, совпадающим с именем компонента:
MyProject/App.jsописывает компонентMyProject.App. - Корневой объект (объект, содержащий вложенные классы) – это папка с именем, совпадающим с именем объекта, плюс файл
index.js, в котором объект объявляется: папкаMyProject/Forms/сindex.js, содержащимMyProject.Forms = {}, – это компонентMyProject.Forms. - Зависимости не объявляются: сборщик смотрит, какие имена используются в коде, и сам подключает нужные файлы.
Пример. Имеем такую структуру файлов:
+ js-libs
|-- + MyProject
|-- + Forms
| |-- index.js
| |-- Abstract.js
|-- index.js
|-- App.jsИ примерное содержимое:
js-libs/MyProject/index.js
var MyProject = {};js-libs/MyProject/App.js
MyProject.App = Bricks.inherit({
constructor: function() {
this._form = new MyProject.Forms.Abstract();
}
});js-libs/MyProject/Forms/index.js
MyProject.Forms = {};js-libs/MyProject/Forms/Abstract.js
MyProject.Forms.Abstract = Bricks.inherit();(Bricks – библиотека базовых классов, написанная в терминах DressCodeJS.)
Теперь, если мы где-либо вызовем new MyProject.App() и соберём файл, в результирующий файл попадут все перечисленные выше: сборщик увидел обращение к MyProject.Forms.Abstract и подключил нужный файл. А если вызовем new MyProject.Forms.Abstract(), то подключатся все файлы, кроме MyProject/App.js – он нигде не используется. И в обоих случаях подключится лишь необходимый кусок файла Bricks/index.js, в котором описана функция Bricks.inherit, а не весь файл.
В директории js-libs рядом с MyProject могут лежать и другие папки с исходниками – например, папка OtherProject, в которой описан корневой объект OtherProject.
Структура проекта
Типовой проект выглядит так:
project/
|-- .dresscode
|-- static/
| |-- js-dev/ # скрипты, подключаемые на страницы сайта
| | `-- index.js
| `-- js-libs/ # библиотека
| `-- MyProject/
| |-- index.js
| `-- App.js
`-- node_modules/- Библиотека (
js-libs) – каталог, который объявлен в файле.dresscode. В библиотеках нет запускаемого кода, только описания классов и функций: если собрать библиотечный файл со всеми зависимостями и подключить его на страницу, ничего не произойдёт. - Приложение (
js-dev) – скрипты, подключаемые на страницы сайта: здесь создание всех необходимых объектов, их настройка и добавление в нужные части страницы. При выкладке сайта все скрипты из этой папки собираются утилитойdresscodejsв готовые js-файлы.
Файл .dresscode
Файл .dresscode объявляет, какие каталоги являются библиотеками. Это обычный текстовый файл: одна строка – один относительный (или абсолютный) путь до каталога-библиотеки:
static/js-libs
node_modules/<имя пакета>/libДля собираемого файла утилита ищет подходящий .dresscode: сначала в директории файла, затем в директории выше, и так далее, до корня. Обычно файл один на весь проект и лежит в корне проекта.
Если библиотека выпускается отдельным пакетом, в корне библиотеки лежит её собственный .dresscode – с ним утилита найдёт компоненты пакета.
Путь, по которому каталог не найден, молча пропускается: одна и та же библиотека может быть объявлена несколькими путями, если её расположение меняется в зависимости от layout установки. Например, при разработке библиотеки Quantum её зависит Botex: в .dresscode Botex указаны и node_modules/dresscode-quantum/lib (когда Quantum разрабатывается рядом), и ../../../node_modules/dresscode-quantum/lib (когда Botex установлен в проекте, куда npm поднял и Quantum в корень). В каждом layout существует ровно один из двух путей, отсутствующий ничего не даёт и ошибку не вызывает. Если же оба пути существуют одновременно, утилита завершится с ошибкой: одно и то же имя компонента достижимо из двух мест, и сборке не за чем выбирать. Проверьте .dresscode и оставьте один путь.
Структура библиотеки
Сборщик видит как компоненты:
index.jsв папке – корневой объект с именем папки (MyProject/index.js→MyProject);- файлы
Имя.jsв подпапке – члены корневого объекта (MyProject/App.js→MyProject.App); - подпапки – вложенные объекты (
MyProject/Forms/index.js→MyProject.Forms).
Имя папки или файла – это имя компонента: начинается с заглавной буквы и состоит из букв и цифр. Файлы в корне библиотеки и файлы со строчными именами сборщик не видит – положите их в папку.
При подключении компонента вместе с ним всегда подключается и объявление его корневого объекта (содержимое соответствующего index.js). Поэтому в результирующем коде родительские объекты MyProject и MyProject.Forms объявлены, даже если код напрямую их не использует.
Как работает сборка
Если в коде написано new MyProject.App(), то файл с компонентом MyProject.App автоматически попадёт в сборку – вместе со всем, что использует этот файл, и так далее. Объявлять зависимости не нужно.
Как сборщик понимает, где брать компоненты:
| Как написано | Что это | Что попадёт в сборку |
|---|------------------------|--------------------------------------------------------------|
| Foo.bar – со строчной буквы | свойство компонента | компонент Foo; из его файла – только используемые свойства |
| Foo.Bar – в CamelCase | вложенный компонент | файл компонента Foo.Bar |
| Foo.VERSION – целиком в капсе | «константа» компонента | компонент Foo |
Два момента, которые стоит знать:
- В сборку попадает только нужное: из
index.jsкорневого объекта, содержащего много свойств, попадут лишь те свойства, которые реально используются в коде. - Подключаемые файлы выводятся в начале выходного файла, перед его собственным кодом. Если файл завернут в скоуп (например, в замыкание) и подключаемые файлы должны попасть внутрь него, переместите блок подключений директивой
//#importsв нужное место.
Все библиотеки, которые используются в коде, должны быть объявлены в .dresscode – иначе сборщик не увидит их компоненты.
Установка и настройка проекта
Установите dresscodejs в devDependencies:
npm install --save-dev dresscodejsСоздайте директорию для подключаемых на страницы js-файлов – обычно
static/js-dev.Создайте библиотечную директорию – обычно
static/js-libs.В корне проекта создайте файл
.dresscodeи запишите в нём путьstatic/js-libs.Если нужны внешние библиотеки, установите их через npm в devDependencies и добавьте пути до каталогов-библиотек в
.dresscode– например,node_modules/<имя пакета>/lib.Настройте Yaxy для сборки js-файлов на лету.
Внешние библиотеки подключаются по пути, а не по версии: в билд попадает ровно тот код, который сейчас лежит по указанному пути в node_modules. За версиями следит npm (package.json + package-lock.json): зафиксированные версии дают воспроизводимую сборку.
Сборка
Командная строка
./node_modules/.bin/dresscodejs -i static/js-dev/index.js -o static/js/index.jsПараметры:
| Параметр | Назначение |
|---|---|
| -i, --input <path> | (обязательно) файл, который нужно собрать, или папка: тогда собираются все js-файлы в ней (рекурсивно, см. Сборка папки) |
| -o, --output <path> | файл результата; если не указан – результат в stdout (только в файл-режиме; в папочном режиме – папка и обязателен) |
| -d, --debug | не переименовывать приватные имена (см. $$) + эквивалент --set debug |
| --set <flag> | (можно многократно) булевы флаги для директивы //#if |
| --layer <name> | собрать только код из слоя //#layer name; несовместимо с --layers |
| --layers <name> | (можно многократно) базовый код плюс перечисленные слои; несовместимо с --layer |
| --private-dict <path.json> | файл словаря приватных имён: читается перед сборкой, записывается после; обязателен, если код собирается в несколько выходных файлов (см. $$) |
| --fail-on-errors | при ошибке сборки завершить процесс с кодом 1 (по умолчанию ошибка вкладывается в результат как throw new Error(...) и сборка завершается успешно) |
Пример боевой сборки с условной компиляцией и стабильным словарём:
./node_modules/.bin/dresscodejs -i static/js-dev/index.js -o static/js/index.js --set ie --private-dict private-names.jsonПро ошибки: по умолчанию при ошибке сборки (неизвестная директива, компонент из #require не найден и т.п.) в результат вкладывается строка throw new Error("DresscodeError: …") – файл соберётся, но упадёт с диагностикой при выполнении. Для CI удобнее --fail-on-errors. Ошибка из-за дубликата имени компонента (см. Файл .dresscode) в любом режиме завершает сборку с кодом 1: результат такой сборки был бы некорректен.
Сборка папки
Если в -i передать папку, а не файл, собираются все js-файлы в ней (рекурсивно, включая вложенные каталоги). Каждый файл получает собственный файл результата в папке -o с сохранением структуры: js-dev/app/page.js → js/app/page.js.
./node_modules/.bin/dresscodejs -i static/js-dev -o static/js --private-dict private-names.jsonНюансы режима:
-o(папка) обязателен: несколько файлов вstdoutвывести нельзя. Если путь в-oсуществует и является файлом – ошибка.- Вся папка собирается одним экземпляром DressCode: словарь приватных имён общий, так что одно и то же имя
$$получает одно и то же приватное имя во всех выходных файлах. Если код собирается в несколько выходных файлов, один и тот же--private-dictпередавать во все сборки обязательно (см.$$) – иначе имена не будут стабильны между сборками. - Файлы собираются в детерминированном (отсортированном) порядке – индексы новых приватных имён стабильны между пересборками.
- Сам выходной каталог в сбор не попадает, если он лежит внутри входного (напр.
-i . -o build): пересборка не перебирает собственные результаты. - Семантика ошибок та же, что в файл-режиме: без
--fail-on-errorsошибка вкладывается в результат соответствующего файла, остальные файлы собираются; с--fail-on-errors– сборка прерывается на первой ошибке, словарь не пишется, код выхода 1.
Использование из Node.js
var {DressCode} = require('dresscodejs');
var dresscode = new DressCode(
false /* debug: не переименовывать приватные имена */,
false /* failOnErrors: выбрасывать исключение при ошибке сборки */
);
dresscode.compile('static/js-dev/index.js', {debug: true}).then((result) => {
fs.writeFileSync('static/js/index.js', result);
});compile(file, context, labels, layers) – context – объект булевых флагов для //#if, labels – массив областей собираемого файла, которые попадут в вывод, layers – строка (собрать только этот слой) или массив строк (базовый код плюс перечисленные слои).
compileCode(virtualFile, content) – собирает строку кода как файл по указанному пути (виртуальный файл; его директория участвует в поиске .dresscode).
Прочитанные файлы кэшируются в экземпляре класса: один экземпляр DressCode удобно использовать для сборки нескольких связанных файлов. Кэш не перепроверяется по диску – если исходные файлы изменились, вызовите dresscode.clearCache() либо создайте новый экземпляр.
Директивы
Директива пишется на отдельной строке (//#имя аргумент) или инлайном в строке (/*#имя аргумент#*/).
| Директива | Назначение |
|---|----------------------------------------------------------------------------------------------------------------------------------|
| //#require Component | явно подключить компонент библиотеки по имени – вместе со всем, что он использует |
| //#require Component.member | явно подключить компонент, но из его файла – только свойство member |
| //#require Component['member'] | то же, что и Component.member, к квадратных скобках может быть только строковый литерал |
| //#imports | вывести блок подключённых файлов в этой точке файла (по умолчанию блок выводится в начало файла) |
| //#without file.js | исключить из сборки файл вместе с тем, что подключает он; удобно, когда код разделён на два билда (общая часть и часть страницы) |
| //#if flag / //#endif | выводить код в зависимости от флага |
| //#set flag / //#unset flag | задать / снять флаг |
| //#layer name / //#endlayer | слой кода (см. ниже) |
#require в большинстве случаев не нужен, DressCode сам подключает зависимости. Явное указание #require может быть нужно, чтобы в базовый js-файл сайта попал код, который в этом файле не используется, но используется во всех остальных. Например, имеем компоненты MyProject.AbstractApp, MyProject.App1, MyProject.App2. App1 и App2 наследуют AbstractApp. Код собирается в файлы index.js, app1.js и app2.js. Имеет смысл вынести код компонента MyProject.AbstractApp в файл index.js, но он там нигде не вызывается. В этом случае в файле index.js указываем //#require MyProject.AbstractApp, а в файлах app1.js и app2.js не забываем про //#without index.js.
DressCode поддерживает и другие директивы Jossy.
Макрос $$
В компонентном файле (файле, описывающем компонент) сборщик заменяет все вхождения $$ на имя компонента. Допускается суффикс: $$, $$__suffix (буквы, цифры, _, -). Это позволяет задавать имена методов и CSS-классов, не пересекающихся с именами из других файлов.
- Debug-сборка (
-d):$$заменяется именем компонента, в котором точки заменены подчёркиваниями:MyProject_Forms_Abstract. - Production-сборка: заменяется коротким приватным именем
_0_,_1_, … Одно и то же имя получает одно и то же приватное имя в пределах сборки.
Стабильность имён между сборками – только через --private-dict: словарь читается перед сборкой и записывается после. Если код проекта собирается в несколько выходных файлов (страницы, бандлы), словарь обязателен: передавайте один и тот же файл словаря во все сборки – иначе «один и тот же» приватный член в разных файлах получит разные имена.
Сборки, работающие с одним и тем же файлом словаря, нельзя запускать параллельно: последовательность «прочитать – собрать – записать» не защищена от гонок, последний завершившийся процесс перезаписывает имена, добавленные остальными, – и в разных выходных файлах возникнут конфликты приватных имён. Запускайте такие сборки последовательно; пересборка, не добавившая новых имён, словарь не переписывает.
Подстановка происходит по всему файлу, в том числе внутри строк-литералов.
Условная сборка и слои
Флаги --set и директивы //#set///#if позволяют компилировать разные версии кода:
//#if lang_ru
alert('Русский интерфейс');
//#endif
//#if debug
console.log(...);
//#endif
//#if not debug
sendTelemetry();
//#endif-d, --debug автоматически задаёт флаг debug – удобно для отладочных строк, которые никогда не попадают в боевые сборки.
Слои (//#layer) позволяют получить только код слоя, исключив базовый:
//#layer test
MyProject.App = FakeApp;
//#endlayer
realInit();- без параметров в сборку попадает только базовый код (
realInit()); --layer test– только код слоя (MyProject.App = FakeApp);--layers test– базовый код плюс слой.
Разработка через Yaxy
Для разработки удобнее не собирать файлы вручную, а использовать прокси-сервер Yaxy: он подменяет запрашиваемые сайтом ресурсы на локальные. Правая часть правила поддерживает протокол bin:, который выполняет команду и возвращает её stdout – этим и собирается js на лету:
# .yaxy
site.my/static/js/index.js => bin: dresscodejs -i ~/static/js-dev/index.js
site.my => localhost:3000Запустите:
yaxy --config .yaxyКаждый запрос к site.my/static/js/index.js в браузере получает свежесобранный файл.
Существующие библиотеки
Библиотеки, написанные в терминах DressCodeJS:
- Bricks – базовые классы и функции (
Bricks.inherit); - dresscode-quantum –
Quantum: объекты с изменяющимся значением и синхронно обновляющиеся цепочки – удобные для синхронизации UI с состоянием; - dresscode-botex –
Botex: шаблонизатор и библиотека построения UI; - dresscode-binary –
Binary: кодирование данных в компактный бинарный формат (localStorage, трафик); - dresscode-resources – компилятор бинарных файлов в JS-код: встраивает ресурсы в сборку;
- dresscode-polyfills – коллекция полифиллов для старых браузеров.
Ограничения
- Код разбирается как ES2022 (
esprima-next):?.,??, class fields, BigInt поддерживаются; более новый синтаксис не поддерживается.
