JavaScript Distributed Assets
- суббота, 13 декабря 2025 г. в 00:00:07
Идея вот в чем: берем стандартные JavaScript модули (ESM) и делаем их прямыми эндпоинтами для генерации любых текстовых веб-ассетов, таких как HTML-файлы, CSS, SVG или даже JSON или Markdown, используя простое соглашение о именовании исходных файлов и дефолтный экспорт результата в виде строки (JavaScript Template Literal). Проще некуда и чем-то похоже на PHP, верно? Но, что это нам дает?
Давайте разберемся, почему JSDA (JavaScript Distributed Assets) - это то, что может сделать веб-разработку "грейт эгейн", после тысячи поворотов "не туда".
// Создаем файл index.html.js
// Импортируем метод для доступа к данным (опционально):
import { getPageData } from './project-data.js';
// Получаем необходимые данные (опционально):
const pageData = await getPageData();
// Экспортируем итоговый HTML-документ:
export default /*html*/ `
<html>
<head>
<title>${pageData.title}</title>
</head>
<body>
<h1>${pageData.heading}</h1>
</body>
</html>
`;
Это будет автоматически преобразовано в обычный HTML:
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>Page Heading</h1>
</body>
</html>
JSDA - подразумевает следование следующим соглашениям:
Для определения типа выходного файла, используется шаблон *.<type>.js, например my-page.html.js, styles.css.js, image.svg.js и т.д.
Для определения входной точки генерации статики, используется шаблон index.<type>.js, например index.html.js, index.css.js, index.svg.js и т.д.
JSDA-файл должен быть стандартным ESM-модулем, содержащим экспорт в формате строки по умолчанию (export default '...')
Структура каталогов при выводе результирующих файлов для статики отражает структуру исходников (мы получаем роутинг на уровне файловой системы):
src/
├── index.html.js → dist/index.html
├── styles/
│ └── index.css.js → dist/styles/index.css
└── assets/
└── logo/
└── index.svg.js → dist/assets/logo/index.svg
Идти по пути упрощения - сложно. Когда мы выкидываем лишнее - нам кажется, что мы можем упустить что-то важное. Но, на практике, часто бывает наоборот - лишние элементы системы делают ее менее устойчивой и более уязвимой. Упрощать - это искусство, которое в ретроспективе может показаться банальным. Но это далеко не так.
Когда CEO Netlify, Мэтт Бильманн в 2015 году предложил архитектурную концепцию JAMStack, он сделал именно это - упростил. Действительно, зачем нам CMS и база данных, со всеми их уязвимостями и потреблением ресурсов на сервере, если он (сервер), в результате должен отдавать просто статику? Зачем серверу сложная логика, если можно генерировать необходимые ассеты на этапе сборки? Почему-бы не раздавать все статические файлы максимально быстро, эффективно и безопасно через CDN, минимизируя нагрузку и существенно улучшая масштабируемость при этом? И главное, зачем усложнять, если лучшего результата мы можем достигнуть упростив? Довольно контринтуитивный ход мыслей на тот момент, но очень правильный.
Однако, по моему мнению, JAMStack - это слишком общая концепция, набор самых верхнеуровневых рекомендаций, практически, не касающихся никаких деталей реализации и не предлагающих решения конкретных задач, которые могут обладать собственной сложностью. Многие продолжают считать, что у JAMStack подхода есть существенные ограничения, хотя это не так, ведь мы можем комбинировать все это с любыми другими практиками в любых произвольных сочетаниях.
В JSDA - та-же философия, но больше технических деталей. Мы берем нативные возможности веб-платформы (Node.js + браузер) и существующие стандарты, и, не добавляя никаких новых избыточных сущностей, решаем задачи, для которых, традиционно, используются значительно более громоздкие решения, при этом, никак себя не ограничивая в диапазоне возможностей.
Если JAMStack - это, преимущественно, про SSG (генераторы статических сайтов), то JSDA - играет на обоих полях, как архитектура применимая для генерации статики, так и динамики. Помимо этого, JSDA никак не ограничивает вас в использовании любых вспомогательных технологий, если это потребуется.
Традиционно, мы выделяем следующие варианты работы веб-приложений:
SPA (Single Page Application) - все под контролем клиентского JavaScript, DOM-дерево создается полностью динамически на клиенте.
SSR (Server Side Rendering) - сервер предварительно рендерит страницу, которая, впоследствии, "оживляется" на клиенте с помощью JavaScript.
SSG (Static Site Generation) - создаем необходимые HTML-файлы на этапе сборки, которые затем раздаются как статика.
Динамическая генерация страниц - сервер генерирует HTML-файлы на этапе запроса, результат можно кэшировать.
Эти подходы не исключают друг друга. У каждого из них есть свои сильные и слабые стороны, но их можно эффективно сочетать. В сложных сценариях, например, страницы документации или промо-материалы могут быть полностью статическими, страницы с товарами - могут быть, частично, предварительно созданы на сервере а частично - содержать динамические виджеты (корзина), а личный кабинет пользователя - может быть полностью реализован как SPA.
И JSDA-стек - как раз применим для таких сложных сценариев, оставаясь простым и минималистичным сам по себе.
В соответствии со спецификациями, ESM модули асинхронны и поддерживают асинхронные вызовы верхнего уровня - Top level await. Это значит, что при генерации ассетов, мы можем делать запросы и получать данные по всей цепочке импортов и зависимостей. Мы можем обращаться к базе данных, внешним API, файловой системе и т.д. Причем, нам не нужно заботится о асинхронной сложности самостоятельно, она "маскируется" под стандартными механизмами ESM, и делая простой импорт зависимости, мы можем быть уверены, что все ее данные получены на этапе резолюции модуля. На мой взгляд, это очень мощная, но недооцененная возможность платформы.
Согласно стандарту, ESM-модули автоматически кэшируются в памяти рантайма в момент их резолюции. Это позволяет делать процесс генерации более эффективным, не совершая никаких лишних операций при повторном использовании модуля. Если же, напротив, мы хотим контролировать кэширование и получать актуальные данные из цепочки импортов, мы можем использовать уникальные идентификаторы (адреса) модулей при импорте, например так:
const data = (await import(`./data.js?${Date.now()}`)).default;
При этом, мы можем использовать параметры запроса, которые будут доступны внутри модуля через import.meta, например:
const userId = import.meta.url.split('?')[1];
const data = (await import(`./user-data.js?id=${userId}`)).default;
Еще одна интересная штука, доставшаяся нам "бесплатно" - это возможность сложной композиции результирующей строки. Этот нативный "шаблонизатор" позволяет формировать любую сложную разметку или иную структуру из компонентов, повторно использовать их и внедрять любую логику при генерации. На фоне этой возможности, модные серверные компоненты из экосистемы React и Next.js - выглядят просто переусложненной глупостью.
Но как нам оживлять разметку на клиенте? Как связать DOM-элементы с данными и обработчиками? Для этого у нас также есть все необходимое, нам ничего не нужно дополнительно изобретать. Решение - стандартная группа спецификаций, известная как веб-компоненты. Приведу упрощенный пример того, как это работает.
На сервере создаем следующую разметку:
import getUserData from './getUserData.js';
let userId = import.meta.url.split('?user-id=')[1];
const userData = await getUserData(userId);
export default /*html*/ `
<my-component user-id="${userId}">
<div id="name">${userData.name}</div>
<div id="age">${userData.age}</div>
</my-component>
`;
На клиенте регистрируем CustomElement:
// функция getUserData может быть изоморфной
import getUserData from './getUserData.js';
class MyComponent extends HTMLElement {
connectedCallback() {
this.userNameEl = this.querySelector('#name');
this.userAgeEl = this.querySelector('#age');
this.userId = this.getAttribute('user-id');
// Уточняем и привязываем данные, если необходимо
getUserData(this.userId).then((data) => {
this.userNameEl.textContent = data.name;
this.userAgeEl.textContent = data.age;
});
}
}
window.customElements.define('my-component', MyComponent);
Вот и все, и никакой ужасной __NEXT_DATA__ как в Next.js. Идентификатором "оживляемой" ноды является наш кастомный тег, который легко перехватывает контроль за своим участком DOM. За все отвечает сам браузер и жизненный цикл CustomElements. Помните, что веб-компоненты вовсе не обязательно должны содержать Shadow DOM.
Но что делать, если компонентов много и у них есть собственная иерархия вложенности? Это тоже просто, привожу очередной пример.
На этот раз, простая рекурсивная функция нарисует нам структурированный HTML:
import fs from 'fs';
/**
*
* @param {String} html исходный HTML
* @param {String} tplPathSchema схема пути к шаблонам компонентов (например, './ref/wc/{tag-name}/tpl.html')
* @param {Object<string, string>} [data] данные для рендеринга
* @returns {Promise<String>} рендеринг HTML
*/
export async function wcSsr(html, tplPathSchema, data = {}) {
Object.keys(data).forEach((key) => {
html = html.replaceAll(`{[${key}]}`, data[key]);
});
const matches = html.matchAll(/<([a-z]+-[\w-]+)(?:\s+[^>]*)?>/g);
for (const match of matches) {
const [fullMatch, tagName] = match;
let tplPath = tplPathSchema.replace('{tag-name}', tagName);
let tpl = '';
// Поддерживаем шаблоны компонентов в разных форматах:
if (tplPath.endsWith('.html')) {
tpl = fs.existsSync(tplPath) ? fs.readFileSync(tplPath, 'utf8') : '';
} else if (tplPath.endsWith('.js') || tplPath.endsWith('.mjs')) {
try {
tpl = (await import(tplPath)).default;
} catch (e) {
console.warn('Template not found for ', tagName);
}
}
if (tpl) {
if (!tpl.includes(`<${tagName}`)) {
tpl = await wcSsr(tpl, tplPathSchema, data);
} else {
tpl = '';
console.warn(`Endless loop detected for component ${tagName}`);
}
}
html = html.replace(fullMatch, fullMatch + tpl);
}
return html;
}
Для более полноценной работы с веб-компонентами вы можете использовать любую популярную библиотеку. Лично я использую Symbiote.js, так как он адаптирован для работы с независимыми от контекста исполнения HTML-шаблонами (и не только).
С преобразованием JSDA-файлов в текстовые веб-ассеты все понятно, но возникает следующий вопрос, как нам представить обычные текстовые файлы в формате JSDA?
Очень просто:
import fs from 'fs';
export default fs.readFileSync('./index.html', 'utf8');
Перед экспортом содержания файла, можно проводить любые промежуточные преобразования, например, можно добавлять нужную цветовую палитру в SVG и даже делать автоматический перевод документа через обращение к LLM.
Вернемся к стандарту ESM и его замечательным возможностям, а именно к возможности загрузки модулей напрямую из CDN через HTTPS. Это полноценно поддерживается как в Node.js, так и в браузере. На уровне CDN (или даже вашего собственного ендпоинта) может происходить автоматическая промежуточная сборка и минификация модулей. Так работает множество популярных специализированных CDN для доставки кода, таких как jsDelivr, esm.run, esm.sh, cdnjs и многие другие. Это позволяет эффективно управлять внешними зависимостями, разделять циклы деплоймента для элементов сложных систем.
Для управления такими зависимостями, крайне полезным инструментом является нативная браузерная технология importmap. Забудьте о всяких нелепых штуках типа Module Federation, платформа уже предоставляет нам все необходимое.
Также, для работы с JSDA-зависимостями (особенно в контексте сервера) отлично подходит простой советский npm, где из коробки мы получаем контроль версий.
Все вышеперечисленное в предыдущем разделе объясняет наличие слова "Distributed" в акрониме JSDA. Простые и понятные модели композиции очень важны на экосистемном уровне, где поставщики решений могут быть слабо связаны между собой.
В экосистеме JSDA - очень просто создавать интегрируемые решения, так как вы всегда можете опираться на самые базовые стандарты и спецификации, не изобретая лишние велосипеды.
Один из ключевых механизмов безопасности в JSDA-стеке - это SRI (Subresource Integrity) - проверка целостности JSDA-зависимостей через хеширование.
В самом начале, я упомянул PHP, и у вас может возникнуть вопрос: если JSDA работает почти как PHP, то почему бы просто не использовать PHP?
Во первых, PHP - это процессор гипертекста. Работа с выходными форматами отличными от HTML (XML) там может быть не такой безболезненной, как хотелось бы. В PHP просто нет полноценного аналога шаблонных литералов, как в JS.
Во вторых, JavaScript - это, нравится вам это или нет, единственный язык программирования в вебе, полноценно и нативно поддерживающийся как на сервере, так и на клиенте.
В третьих, и это самое главное, используя один язык вы можете повторно использовать одни и те-же сущности в серверном и клиентском коде, писать, так называемый, "изоморфный" код, за счет чего ЗНАЧИТЕЛЬНО экономить на всех сопутствующих разработке процессах, включая ментальные, в вашей голове. Вам не приходится переключаться между языками, не приходится писать кучу отдельных конфигов, ваш проект проще тестировать и поддерживать c меньшими ресурсами.
Благодаря изоморфизму и единому языку, JSDA, будучи, преимущественно, серверной технологией, при необходимости, может легко, и, практически бесшовно, применяться и на клиенте.
Без TS в современной разработке - никуда. Однако, сам TypeScript, будучи важным экосистемным инструментом статического анализа, содержит в себе ряд сложностей и спорных моментов, которые всплывают как только вы попытаетесь сделать нечто более заковыристое. Если вы когда-либо занимались разработкой собственных библиотек - вы сразу поймете о чем я говорю.
Автор этих строк пришел к использованию деклараций типов напрямую в JS-коде в формате JSDoc, совместно с дополнительными файлами *.d.ts, как к наиболее сбалансированной практике использования TypeScript. И в этом я не одинок, я встречаю все больше опытных разработчиков, которые делают так-же.
Применимо к JSDA, такой подход дает следующие преимущества:
нет дополнительных этапов сборки: каждый модуль может быть самостоятельным ендпоинтом
работает именно тот код, который вы написали, вы не разбираетесь с глюками sourceMap при дебаге
нет проблем со следованием стандартам (например, идентификаторы ESM содержат расширения файлов, в TS - это не обязательно)
JSDoc удобнее для комментирования + позволяет автоматизировать создание документации, для чего, собственно, он и был предназначен
Но, если вам такое не нравится - вы можете совершенно спокойно использовать синтаксис TypeScript, он вполне совместим с принципами JSDA.
При работе с AI-помощниками в разработке мы сталкиваемся с одной проблемой, о которой, почему-то, пока мало говорят. AI - склонен воспроизводить популярные подходы и паттерны, без глубокого анализа их эффективности и применимости в конкретном случае. У AI пока очень плохо с тем самым упрощением, о котором я говорил ранее. React - стал слишком жирным и тормозным? Не важно, AI предложит использовать именно его, просто потому, что в сети больше примеров кода. В этом отношении, AI ведет себя как джуниор, не способный брать на себя ответственность за архитектурные решения, предполагающие выбор. При этом, сам AI, глобально, нуждается в оптимизации потребления токенов, и это вполне может находить отражение в технологиях, которые мы используем. Простота наших решений, включающая в себя сокращение сущностей, которыми мы оперируем - должна повлиять на ситуацию позитивно. Поэтому, важно создавать новые минималистичные паттерны, такие как JSDA, которые в целом должны положительно повлиять на индустрию.
До сих пор, я говорил о JSDA как о наборе соглашений и архитектурном принципе, без привязки к конкретным инструментам и не принуждающем использовать что-то конкретное. Конечно, чтобы все это кто-то использовал нужна экосистема решений, позволяющих быстро развернуть проект и максимально быстро получить первые результаты. Разработкой такой экосистемы я и занимаюсь в последнее время. Хотелось бы привлечь к этому сообщество и двигаться дальше - вместе.
Что есть на текущий момент:
JSDA Manifest - описание концепта
JSDA-Kit - изоморфная библиотека инструментов для работы с JSDA-стеком
Symbiote.js - библиотека для эффективной работы с веб-компонентами, позволяющая очень гибко работать с HTML
Из всего перечисленного, самым взрослым (production ready) проектом является Symbiote.js, остальное - в активной разработке, но попробовать можно уже сейчас.
В перспективе, планируется создание специализированной CDN, которая, помимо предварительной сборки и минификации модулей, будет автоматически выдавать готовые файлы в итоговом формате (HTML, CSS, SVG, Markdown, JSON и т. д.).
Сайт моей собственной R&D-студии, как и некоторые наши важные проекты, сделаны полностью на JSDA-стеке, этот подход УЖЕ доказал свою надежность и эффективность.