Lit: свои веб-компоненты — мастерская и 8 примеров интерфейсов
Создаём собственные веб-компоненты для PHP-сайтов и сервисов: восемь живых примеров, события, формы, HTTP-каталог, тема и готовые исходники.

Lit на практике: создаём свои веб-компоненты для сайтов и сервисов. Восемь живых интерфейсов, настоящие события и HTTP-загрузка, полный проект и запуск на Windows, Linux и VPS.
Что такое Lit и зачем нужны свои веб-компоненты
В одном проекте карточка услуги размещена на странице PHP, в другом похожая карточка живёт в административной панели, а через полгода её нужно вставить в личный кабинет. Если копировать разметку и обработчики, исправлять поведение придётся в нескольких местах. Lit помогает оформить такой интерфейс как собственный HTML-элемент с понятными входными свойствами, внутренним состоянием и событиями наружу.
Библиотека добавляет удобное описание шаблона и реактивные обновления поверх стандартов браузера. Вы пишете класс, регистрируете тег вроде aen-product, а затем используете этот тег рядом с обычными формами, ссылками и PHP-разметкой. Для посетителя это привычная карточка или форма. Для разработчика — отдельная деталь, которую можно проверить и переносить между страницами.
В этой статье используется Lit 3.3.3. Мы собрали восемь работающих интерфейсов и показываем не только код класса, но и поведение страницы: как передать цену, получить результат действия, обработать неудачную загрузку и сохранить управление с клавиатуры. Исходники собственной мастерской доступны под MIT, лицензии Lit и других зависимостей вложены отдельно.
Где Lit полезен: CMS, кабинеты, виджеты и дизайн-системы
Хороший первый кандидат — автономный виджет с ограниченной ответственностью. Например, калькулятор стоимости услуги, выбор периода отчёта, карточка объекта, панель предпочтений или форма подписки. У него есть несколько входных параметров, небольшой набор событий и чёткое место на странице. Такой компонент проще внедрить постепенно: сначала на одной странице, затем в остальных разделах.
Для сайта на PHP особенно удобно разделить задачи. Сервер проверяет права, выбирает данные и формирует документ. Lit отвечает за интерактивность определённого участка: пересчёт суммы, раскрытие подробностей, фильтрацию уже полученного набора. Встраивание не обязывает переводить весь сайт на клиентскую маршрутизацию или переносить шаблоны в новое приложение.
В дизайн-системе собственный элемент становится договорённостью команды: какие свойства допустимы, какие состояния нужно показать, как называются события и какие CSS-переменные разрешено менять. Начинать следует с реального повторения, а не с попытки сразу создать универсальный компонент на десятки случаев. Две понятные специализированные детали часто удобнее одной перегруженной формы с множеством флагов.
Lit, готовые UI-компоненты и большие фреймворки
Lit не поставляет готовый каталог таблиц, календарей, диалогов и форм. В мастерской мы проектируем эти детали самостоятельно. Если требуется быстро собрать стандартную панель, готовый набор компонентов может сэкономить больше времени. Lit интересен, когда важны собственное поведение и оформление, а результат должен оставаться обычным пользовательским элементом браузера.
Сравнивать его с React, Vue или Angular лучше на уровне задачи. Большому приложению могут понадобиться маршруты, управление состоянием, серверный рендеринг, формы и принятые командой инструменты. Наличие компактного компонента не решает эти вопросы автоматически. При этом веб-компонент способен существовать внутри страницы другого стека, если интеграция корректно передаёт свойства и подписывается на DOM-события.
На сайте уже есть мастерская Web Awesome: там мы используем готовые элементы. Здесь следующий уровень контроля — сами определяем внутреннюю разметку, состояния и контракт. Полезно пройти обе публикации последовательно: сначала оценить поведение законченных компонентов, затем понять, как организовать собственный.
Мини-мастерская: восемь интерфейсов прямо на странице
Начните с карточки услуги: увеличьте количество и добавьте пакеты в учебную корзину. Под примером появится настоящее событие с данными. Затем откройте каталог, воспроизведите ошибку 404 и повторите загрузку. Так сразу видна разница между изменением состояния в браузере и обращением за JSON к серверу.
В списке доступны калькулятор тарифа, профиль, задачи, вкладки кабинета, темизация и пошаговая запись. Каждый пример решает отдельную интерфейсную задачу. Переключатель начинает выбранный сценарий заново; кнопка сброса делает то же самое для текущего. Введённый профиль и заявка остаются в памяти вкладки, никуда не отправляются и после переключения не сохраняются.
Если встроенная область тесная, откройте мастерскую на всю страницу. На телефоне это особенно удобно: элементы управления располагаются над примером. Кнопка исходников показывает настоящий модуль компонентов, а полный ZIP содержит готовую сборку, зависимости с фиксированными версиями, тесты, учебный JSON и инструкции. Отдельный исходный JavaScript не является автономной веб-страницей.
Установка и устройство проекта
Для изменения исходников понадобится Node.js 24 и npm. В архиве версии закреплены в package-lock.json, поэтому используем npm ci. Эта команда восстанавливает согласованный набор зависимостей. В браузер попадает локальная сборка app.js; во время просмотра не нужно обращаться к публичному CDN за Lit или стилями Bootstrap.
Папка src содержит модель данных, классы компонентов и небольшую оболочку мастерской. Каталог public уже готов к размещению на веб-сервере: там HTML, CSS, JavaScript, JSON и лицензии. Сценарий сборки копирует исходные модули в public/source, чтобы кнопка просмотра кода показывала ровно ту реализацию, из которой собрана страница.
Разделение удобно для собственного проекта. Сначала измените один класс в src/components.js, затем пересоберите страницу и проверьте результат. Не редактируйте одновременно исходный и собранный app.js: следующая сборка перезапишет выходной файл. Если нужен только просмотр примеров, установленный PHP может раздать готовую public без установки npm-зависимостей.
npm ci --ignore-scripts
npm run build
npm test
npm start
Первый компонент: класс, шаблон и регистрация
Минимальный компонент состоит из класса, метода render и регистрации имени. В имени пользовательского элемента обязателен дефис. Префикс проекта помогает избежать столкновений: два разных скрипта не должны регистрировать несовместимые классы под одним тегом. В мастерской используется префикс aen, а в своём продукте лучше выбрать собственный.
В примере ниже отдельный счётчик подходит для проверки всей цепочки: импорт библиотеки, создание экземпляра, реактивное изменение и обновление текста. Сохраните модуль в проекте, соберите его и добавьте тег на страницу. Код с импортом из lit рассчитан на сборщик; браузер не разрешит такое имя пакета сам без дополнительных настроек.
Метод render описывает желаемый вид компонента для текущего состояния. Не стоит внутри него отправлять HTTP-запросы, сохранять данные или назначать глобальные обработчики: обновление шаблона может происходить многократно. Побочные действия лучше запускать из явного обработчика или подходящего метода жизненного цикла, чтобы пользовательское действие имело предсказуемый результат.
import {LitElement, html} from 'lit';
class DemoCounter extends LitElement {
static properties = {count: {state: true}};
constructor() { super(); this.count = 0; }
render() {
return html`<button @click=${() => this.count++}>
Нажатий: ${this.count}
</button>`;
}
}
customElements.define('demo-counter', DemoCounter);
Свойства, атрибуты и внутреннее состояние
В карточке услуги имя и цена относятся к внешнему контракту: их задаёт страница. Количество и сообщение об учебном добавлении — внутреннее состояние конкретного экземпляра. Такое разделение помогает не превращать каждую техническую переменную в публичный атрибут. Передача сложного массива через свойство также понятнее, чем длинная JSON-строка внутри HTML.
Атрибуты в разметке имеют текстовое представление. Lit может преобразовать объявленные простые типы, но проверку бизнес-ограничений нужно написать отдельно. Тип Number не означает, что цена автоматически неотрицательна, а число сотрудников укладывается в тариф. В калькуляторе мастерской значение нормализуется и ограничивается диапазоном от одного до пятидесяти мест.
При встраивании в Lit-шаблон точка перед именем задаёт JavaScript-свойство, вопросительный знак управляет наличием булевого атрибута, а символ @ связывает обработчик события. Эти формы решают разные задачи. Например, disabled=”false” в обычном HTML всё равно означает наличие атрибута; для включения и выключения нужно менять именно его присутствие.
html`<aen-product .price=${12000} name="Аудит"></aen-product>`;
html`<button ?disabled=${saving} @click=${save}>Сохранить</button>`;
html`<input .value=${query} @input=${e => query = e.target.value}>`;
Пример 1. Карточка услуги и событие корзины
В карточке посетитель видит состав услуги, цену и количество. Итог обновляется сразу, поэтому до действия понятно, сколько пакетов выбрано. Кнопки ограничены диапазоном от одного до десяти. Это маленькое удобство важнее эффектной анимации: интерфейс не предлагает заведомо неподдерживаемое действие и не заставляет узнавать ограничение после отправки формы.
По нажатию компонент создаёт событие cart-add. В detail попадают идентификатор услуги, количество и рассчитанная сумма. Оболочка мастерской подписана на это событие и показывает полученный объект под примером. В настоящем магазине родительская страница могла бы обновить корзину, а затем обратиться к серверу. Сервер должен самостоятельно проверить товар, доступное количество и действительную цену.
Не следует превращать отправленную клиентом сумму в основание для оплаты. Событие удобно для координации интерфейса, но оно не подтверждает коммерческую операцию. В мастерской прямо написано, что заказ не отправлен. Такой же честный текст нужен на промежуточных прототипах, которыми пользуются заказчики или сотрудники.
this.dispatchEvent(new CustomEvent('cart-add', {
detail: {sku: 'audit', quantity: this.quantity},
bubbles: true,
composed: true
}));
document.querySelector('aen-product').addEventListener('cart-add', e => {
console.log('Выбрано пакетов:', e.detail.quantity);
});
Пример 2. Тариф команды и расчёт стоимости
Калькулятор показывает месячную стоимость и размер фактического платежа. Эти значения важно разделять: при годовой оплате пользователь видит эквивалент в месяц, но списывается стоимость двенадцати месяцев. В нашем учебном тарифе место стоит 900 рублей, а годовая скидка составляет двадцать процентов. Для пяти мест получается 3600 рублей в месяц и 43200 рублей за год.
Попробуйте изменить число сотрудников и включить годовую оплату. Расчёт происходит из текущих параметров, без хранения второй независимой копии итоговой суммы. Если одновременно поддерживать seats, annual, monthly и total как изменяемые состояния, легко забыть обновить одно из них. Вычисляемое значение лучше получать из небольшого числа исходных параметров.
В реальном тарифе добавятся налоги, округление, ограничения плана и возможная смена цены. Держите расчёт в отдельной функции и проверяйте крайние значения. Для денежных вычислений с дробными единицами используйте целые минимальные единицы валюты или подходящую десятичную арифметику. Учебная формула демонстрирует интерфейс, а не заменяет биллинг.
const seats = 5;
const annual = true;
const monthly = seats * 900 * (annual ? 0.8 : 1);
const payment = monthly * (annual ? 12 : 1);
const label = new Intl.NumberFormat('ru-RU', {
style: 'currency', currency: 'RUB', maximumFractionDigits: 0
}).format(payment);
Пример 3. Каталог с HTTP, поиском и восстановлением
Каталог показывает настоящий сетевой цикл. Кнопка запрашивает JSON, проверяет статус ответа, читает массив и отображает карточки. Отдельная кнопка обращается к отсутствующему файлу и получает 404. Это позволяет увидеть ошибку без отключения интернета. Повторная нормальная загрузка восстанавливает список, а поиск и направление совместно ограничивают выдачу.
Пустой результат поиска и неудачная загрузка — разные состояния. В первом случае данные получены, но не подходят под условия. Во втором ещё нельзя утверждать, что услуг нет. Мастерская показывает разные сообщения и оставляет понятный путь к повтору. В сервисе с оплатой или заявками такая разница непосредственно влияет на доверие: посетитель должен понимать, что произошло и что можно сделать.
Не забывайте о гонке запросов. Медленный старый ответ способен перезаписать более новый результат. В исходниках используются AbortController и номер запроса; при удалении компонента запрос отменяется. Проверяется и формат данных. Для больших каталогов фильтрацию и пагинацию обычно переносит на сервер команда разработки, а компонент показывает состояние загрузки и полученную страницу.
const controller = new AbortController();
const response = await fetch('./data/services.json', {
signal: controller.signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const rows = await response.json();
if (!Array.isArray(rows)) throw new Error('Ожидался массив');
// Полная мастерская проверяет также поля каждой записи.
Пример 4. Форма профиля и нативная проверка
Профиль содержит имя, email и предпочтение рассылки. Внутри компонента находится обычная HTML-форма с подписанными полями. Обязательность и формат email проверяет браузер. Мы не рисуем собственную имитацию каждого поля, поэтому сохраняем знакомые способы ввода, перехода клавишей Tab и отправки формы.
После успешной проверки FormData собирает значения, а profile-save передаёт их родительской странице. В мастерской это локальное сохранение в памяти: нет базы, отправки письма или подписки. Используйте вымышленные значения. В рабочем сервисе результат запроса нужно показать отдельно: «Отправляем», «Сохранено» или сообщение о конкретной ошибке с сохранением введённых данных.
Форма внутри Shadow DOM и пользовательский элемент как участник внешней формы — разные архитектуры. Наш пример использует первый вариант. Если вы хотите, чтобы собственный тег автоматически попадал в FormData внешней формы, понадобится отдельная интеграция через возможности form-associated custom elements. Не стоит обещать это поведение компоненту только потому, что внутри него есть input.
const form = this.renderRoot.querySelector('form');
if (!form.reportValidity()) return;
const data = new FormData(form);
const profile = {
name: String(data.get('name')).trim(),
email: String(data.get('email')).trim(),
digest: data.has('digest')
};
Пример 5. Задачи, ключи и сохранение идентичности
В списке можно добавить задачу, изменить отметку выполнения, удалить запись и развернуть порядок. Последнее действие намеренно сделано заметным: оно помогает проверить, не переехало ли внутреннее состояние одного поля в другую строку. Каждая задача получает устойчивый ID, а repeat использует его как ключ.
Для списка с интерактивными элементами идентичность записи имеет практический смысл. Поле ввода, отметка и фокус должны относиться к той же задаче после сортировки. Индекс массива ненадёжен: после вставки в начало все индексы меняются. В рабочей системе используйте идентификаторы из базы или стабильные локальные ID до первого сохранения.
Массив обновляется новым значением через map, filter или spread. Это делает изменение заметным реактивной системе и упрощает чтение кода. Само по себе push в существующий массив не обязано вызвать нужное обновление. Для большой доски задач добавьте серверные операции, обработку конфликтов и восстановление неотправленных правок; в учебной сцене всё остаётся в памяти.
import {repeat} from 'lit/directives/repeat.js';
const view = html`${repeat(tasks, task => task.id, task => html`
<label><input type="checkbox" .checked=${task.done}>
${task.title}
</label>
`)}`;
this.tasks = this.tasks.map(task =>
task.id === id ? {...task, done: true} : task
);
Пример 6. Вкладки с клавиатурой и понятным фокусом
Вкладки кабинета показывают обзор, счета и поддержку. Это локальные панели, а не переходы по разным адресам сайта. Для такого поведения подходят роли tablist, tab и tabpanel, связь через aria-controls и указание выбранной вкладки. Если же раздел должен иметь собственный URL, обычные ссылки могут оказаться правильнее.
Проверьте клавиатуру: Tab попадает на активную вкладку, стрелки переключают соседние, Home и End переходят к первой и последней. Неактивные вкладки исключены из последовательности Tab, но остаются достижимыми стрелками. После изменения активной панели код дожидается updateComplete и только затем переводит фокус на нужную кнопку.
Наличие ARIA-атрибутов не является автоматическим доказательством полной доступности. Нужны согласованные клавиатурные действия, видимый фокус, читаемые названия и проверки с реальными вспомогательными средствами. Здесь реализован базовый шаблон с автоматической активацией для мгновенных локальных панелей. Если переключение запускает медленную сеть, подумайте о ручном подтверждении выбора.
this.active = nextIndex;
await this.updateComplete;
this.renderRoot.querySelector(`#tab-${nextIndex}`).focus();
// В шаблоне:
html`<button role="tab"
aria-selected=${active ? 'true' : 'false'}
tabindex=${active ? 0 : -1}>Обзор</button>`;
Пример 7. Shadow DOM, слоты и оформление
В сцене оформления карточка принимает текст через обычный слот, а ссылку действия — через именованный. Родитель определяет содержание, компонент задаёт расположение. Это полезно для уведомлений, карточек, панелей и заголовков: не нужно заранее превращать каждый возможный абзац в строковое свойство.
Тема меняется через CSS-переменные, а рамка доступна снаружи через объявленную часть surface. Так компонент предоставляет ограниченный и понятный интерфейс оформления. Bootstrap используется для оболочки мастерской, но его глобальные правила не проходят внутрь Shadow DOM. Попытка добавить обычный класс btn внутренней кнопке не подключит стили автоматически.
Shadow DOM помогает изолировать устройство и CSS компонента, но не является защитой секретов. Данные в браузере остаются доступны пользователю и скриптам с соответствующим доступом. Используйте изоляцию для устойчивой верстки, а права доступа проверяйте на сервере. При выборе тёмной темы отдельно оценивайте контраст текста, границы полей и видимость фокуса.
// Внутренний шаблон:
html`<section part="surface">
<slot></slot>
<slot name="actions"></slot>
</section>`;
// Внешний CSS:
// aen-notice { --aen-accent: #4c44a7; }
// aen-notice::part(surface) { border-width: 2px; }
Пример 8. Пошаговая запись на услугу
Мастер записи разделяет выбор услуги, контактные данные и проверку результата. До подтверждения посетитель видит собранную заявку и может вернуться назад. Пустое имя блокирует переход с понятным сообщением, а введённые значения сохраняются между шагами. После подтверждения создаётся событие booking-create и выводится итог.
Это полезный каркас для заявки на консультацию, настройки интеграции или оформления услуги. Но количество шагов должно соответствовать реальной сложности. Короткую форму из двух полей обычно удобнее показать сразу. Деление оправдано, когда выбор на раннем шаге влияет на последующие параметры и пользователю действительно помогает последовательность.
Учебные дни не являются доступными слотами календаря. Для настоящей записи сервер должен проверить актуальность времени, предотвращать одновременное бронирование и возвращать подтверждённый идентификатор. Клиентский шаг «Готово» показывают только после такого ответа. У нас текст специально сообщает о формировании локальной заявки, чтобы демонстрацию нельзя было принять за работающую запись.
if (!this.name.trim()) {
this.error = 'Укажите имя для учебной заявки.';
return;
}
this.step = 2;
await this.updateComplete;
this.renderRoot.querySelector('h3').focus();
Как страница общается с компонентом
Удобный контракт строится в двух направлениях. Страница передаёт данные через свойства, компонент сообщает о намерениях пользователя через события. Не заставляйте оболочку искать внутренние input и читать их по случайным селекторам: такая связь ломается после обычного изменения разметки.
В detail события следует передавать небольшой объект с понятными полями. Для покупки достаточно идентификатора и количества; для выбора записи — ID, а не вся внутренняя структура библиотеки. Названия событий стоит документировать вместе с версией компонента. Изменение смысла поля может сломать интеграцию даже тогда, когда внешний вид остался прежним.
Флаги bubbles и composed решают разные задачи: распространение вверх по DOM и переход через границу Shadow DOM. В мастерской они позволяют слушателю оболочки получить события вложенных элементов. Не включайте их механически у всех внутренних уведомлений: наружу должны выходить только события, которые относятся к публичному контракту.
const element = document.querySelector('aen-product');
element.price = 15000;
element.addEventListener('cart-add', event => {
const {sku, quantity} = event.detail;
// Дальше приложение решает, как сохранить выбор.
console.log({sku, quantity});
});
Жизненный цикл, очистка и асинхронные действия
Компонент может появляться и исчезать много раз: при переключении вкладок, фильтрации блоков или переходе внутри приложения. Поэтому внешний обработчик, таймер и сетевой запрос должны иметь понятное время жизни. Накопленные подписки вызывают повторные действия, а незавершённый запрос способен обновить уже неактуальный экран.
В каталоге отключение элемента отменяет текущий запрос. Номер операции дополнительно защищает от старого ответа, а проверка isConnected исключает применение результата к удалённому экземпляру. Если компонент должен сам загружать данные при повторном подключении, это поведение следует отдельно реализовать и проверить. Наш каталог загружается по явной кнопке и не обещает автоматическую синхронизацию.
При переопределении connectedCallback и disconnectedCallback вызывайте методы базового класса. Для изменения фокуса после рендера используйте updateComplete, а не случайную задержку на несколько миллисекунд. Таймер не гарантирует, что нужная разметка появилась, особенно на медленном телефоне или при нескольких последовательных обновлениях.
disconnectedCallback() {
super.disconnectedCallback();
this.controller?.abort();
this.request++;
}
Безопасный вывод и серверная проверка
Строка пользователя должна оставаться строкой. В обычной текстовой подстановке Lit отображает её как данные, поэтому название задачи с угловыми скобками не превращается в HTML. Это полезное базовое поведение. В мастерской нет вставки произвольной разметки через unsafeHTML, выполнения введённого JavaScript или внешних HTML-шаблонов.
Однако безопасная текстовая подстановка не делает любое применение значения безопасным. Отдельного контроля требуют адрес ссылки, CSS, параметры запроса и содержимое, передаваемое другим библиотекам. Разрешайте ожидаемые форматы и протоколы, ограничивайте размер данных, не публикуйте токены доступа внутри атрибутов или клиентского bundle.
На сервере повторно проверяйте обязательные поля, диапазоны, принадлежность записи пользователю и право на действие. Клиентскую валидацию можно обойти, а CustomEvent можно создать вручную. Назначение интерфейса — помочь человеку заполнить данные и исправить ошибки. Решение о сохранении, цене, оплате и доступе остаётся ответственностью серверной части.
const title = '<img src=x onerror=alert(1)>';
const safeText = html`<p>${title}</p>`;
// Пользователь увидит текст; элемент img здесь не создаётся.
Встраивание в PHP-страницу
Серверный шаблон может вывести тег с первоначальными данными и подключить собранный модуль. Для текста атрибута используйте экранирование HTML, а число сначала преобразуйте и проверьте. Это разные операции: экранирование защищает синтаксис разметки, но не подтверждает корректность бизнес-значения.
Для полноценного приложения полезно собрать отдельный entry-модуль, который только регистрирует компоненты. Файл app.js мастерской дополнительно управляет переключателем и служебными блоками, поэтому переносить его на произвольную страницу целиком не нужно. Возьмите src/components.js, создайте отдельный импорт и соберите bundle для своей разметки.
Обработчик сохранения через PHP в этот статический проект не включён. При добавлении API используйте обычный серверный подход: авторизованная сессия, проверка CSRF для соответствующего способа аутентификации, валидация, контроль прав и структурированный JSON-ответ. Компонент должен показывать серверную ошибку рядом с действием и сохранять введённый пользователем текст.
<?php
$name = 'Аудит интерфейса';
$price = 12000;
?>
<aen-product name="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
price="<?= (int) $price ?>"></aen-product>
<script type="module" src="/assets/components.bundle.js"></script>
Windows: распаковать, запустить и изменить
Установите Node.js 24 из официального источника, распакуйте архив в отдельную папку и откройте PowerShell в каталоге проекта. Команды ниже используют npm.cmd: это позволяет запускать npm без изменения политики выполнения PowerShell ради файла npm.ps1. После сборки откройте локальный адрес в обычном браузере.
Если порт 8154 занят другим приложением, задайте переменную PORT перед запуском. Не завершайте чужой процесс только ради освобождения привычного номера. Локальный сервер слушает 127.0.0.1, поэтому предназначен для просмотра на этом компьютере. Завершение выполняется сочетанием Ctrl+C в терминале, где запущен npm start.
Для просмотра готовой папки public подойдёт также команда php -S 127.0.0.1:8154 -t public, если PHP уже установлен. Такой запуск не меняет архитектуру мастерской: PHP только отдаёт файлы. Открывать index.html двойным щелчком через file:// не стоит, поскольку модульные скрипты и fetch рассчитаны на HTTP.
Expand-Archive -LiteralPath .\lit-project.zip -DestinationPath .\lit-demo
Set-Location .\lit-demo\lit-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd start
# Если нужен другой порт:
# $env:PORT = '8159'; npm.cmd start
Linux: рабочий запуск и проверка результата
На Linux используйте обычную учётную запись, установленный Node.js 24 и npm. Команды установки зависимостей не требуют sudo. Архив содержит ту же структуру, что и версия для Windows: отдельной ветки исходников нет. После распаковки сборка создаёт локальные ресурсы в public, а тесты проверяют расчёт, события, списки, запросы и шаги формы.
Локальный сервер также слушает только loopback. На удалённом сервере его удобно просмотреть через SSH-туннель, не открывая тестовый порт всему интернету. Для постоянного публичного размещения лучше раздавать готовую public через Caddy или Nginx. Держать npm start под менеджером процессов ради статических файлов не требуется.
Проверьте не только главную страницу, но и JavaScript, JSON и лицензии. Неверный MIME для модулей или отсутствие файла данных проявятся уже после загрузки HTML. В этой статье Linux-проверка означает сборку и тестирование чистого архива в контейнере; отдельный внешний VPS и выпуск сертификата не выдаются за выполненный нами этап.
mkdir -p "$HOME/lit-demo"
unzip lit-project.zip -d "$HOME/lit-demo"
cd "$HOME/lit-demo/lit-project"
node --version
npm ci --ignore-scripts
npm run build
npm test
npm start
# Во втором терминале:
curl -I http://127.0.0.1:8154/app.js
curl http://127.0.0.1:8154/data/services.json
Размещение на VPS: статика, HTTPS и откат
Для публикации достаточно готовой папки public. На сервере создайте отдельный каталог выпуска, скопируйте туда файлы и укажите его в конфигурации веб-сервера. Пользователь Caddy должен иметь право читать файлы и проходить по каталогам. Не переносите в публичный корень node_modules, приватные конфигурации и содержимое всего рабочего проекта.
В примере домен заменяется вашим настоящим именем, DNS должен указывать на сервер, а необходимые порты доступны снаружи. Автоматический HTTPS Caddy зависит от корректной сетевой настройки. Перед перезагрузкой проверьте конфигурацию. После публикации откройте страницу, переключите все примеры, вызовите загрузку JSON и проверьте ответ модуля с корректным Content-Type.
Обновление удобно делать отдельным каталогом release-02, оставляя предыдущий выпуск для отката. Сначала проверяют новые файлы, затем меняют root и перезагружают конфигурацию. Ссылка ZIP в мастерской относительная. В WordPress архив лежит на уровень выше public и доступен по этому адресу. При отдельном Caddy root, направленном только на public, файл рядом с public автоматически не раздаётся. Замените href на /downloads/lit-project.zip и настройте отдельный маршрут выдачи архива либо укажите существующий раздел загрузок; проверьте скачивание после публикации. Если распространяете исходники, сохраняйте уведомления лицензий.
demo.example.com {
root * /srv/lit/releases/release-01/public
encode zstd gzip
file_server
header X-Content-Type-Options nosniff
}
# После сохранения в /etc/caddy/Caddyfile:
# sudo caddy validate --config /etc/caddy/Caddyfile
# sudo systemctl reload caddy
Мобильный интерфейс, производительность и SEO
На узком экране важнее сохранить читаемое действие, чем уместить все элементы рядом. В мастерской выбор примера перемещается выше содержимого, поля занимают доступную ширину, кнопки допускают перенос текста. В исходниках предусмотрен видимый фокус, а необязательные переходы учитывают предпочтение уменьшенного движения. Проверяйте также длинные названия, ошибки и пустые состояния.
Один небольшой компонент не гарантирует быструю страницу. Итог зависит от объёма всей сборки, числа экземпляров, размера JSON и частоты обновлений. Не перерисовывайте крупный список на каждое несущественное изменение и не загружайте данные, которые экран не использует. Для тяжёлого каталога лучше отдельная стратегия серверных запросов и ограничения объёма.
Для поисковых страниц основные объяснения, заголовки и полезные ссылки разумно оставить в обычном серверном HTML. Мастерская дополняет материал интерактивностью. Не прячьте всё содержание статьи внутрь компонента, рассчитывая, что любой поисковик и любой режим браузера одинаково обработают клиентский JavaScript. Назначение примеров здесь — помочь читателю проверить поведение и забрать работающий код.
Частые ошибки и проверка перед использованием
Если тег виден в DOM, но внутри пусто, проверьте загрузку модуля, имя customElements.define и сообщения консоли. Если изменение массива не обновляет экран, посмотрите, присваивается ли новое значение. Если глобальный CSS не действует, вспомните границу Shadow DOM и используйте предусмотренные переменные или части.
Когда событие не приходит родителю, проверьте название, место подписки и необходимость bubbles/composed. Если фокус остаётся на исчезнувшем элементе, дождитесь завершения обновления и выберите реальную доступную цель. Если каталог внезапно показывает старый поиск, исследуйте порядок завершения запросов, а не добавляйте произвольную задержку перед рендером.
Перед переносом в продукт пройдите каждую ветку: успешное действие, пустой ввод, ошибка сервера, повтор, быстрые переключения и удаление компонента. Отдельно оцените клавиатуру и мобильную ширину. В архиве есть автоматические проверки важных состояний, но они не заменяют визуальный осмотр, тест на физическом телефоне и проверку с используемыми вашей аудиторией вспомогательными технологиями.
npm test
npm audit --audit-level=low
npm run build
# Сборку проверяют до переноса public на сервер.
# Обновление версий выполняйте отдельно, с повторной проверкой.
Исходники и официальная документация
В полном архиве находятся восемь сценариев, общий класс с оформлением, модель учебных услуг, оболочка, локальные ресурсы, README и тесты. Возьмите одну понятную деталь, переименуйте тег под свой проект и сначала встроите её на отдельную страницу. Затем добавляйте серверное сохранение и реальные данные, сохраняя явные состояния загрузки и ошибки.
Встроенная кнопка показа кода читает тот же исходный модуль, из которого сделана сборка. Для отдельного переиспользования зарегистрируйте компоненты через свой entry-файл и соберите зависимости. Это помогает отделить библиотечную часть от демонстрационной оболочки: в продукте не нужны переключатель сцен и журнал учебных событий.
Следующие ссылки ведут к первичным источникам. Документация описывает базовый механизм, а практические решения в статье и архиве показывают один из вариантов применения. При обновлении версии сравнивайте не только успешную сборку, но и договорённости компонента: свойства, события, CSS-переменные, фокус и формат данных.


