- PVSM.RU - https://www.pvsm.ru -

Современные веб-приложения построены на основе богатого инструментария работы с CSS, связанного со множеством пакетов NPM и этапов сборки. Ванильное же веб-приложение может выбрать более легковесный путь, отказавшись от современных методик с предварительно обработанным CSS и выбрав нативные для браузеров стратегии.
Сброс стилей до общего для всех браузеров среднего — стандартная практика в веб-разработке, и ванильные веб-приложения в этом ничем не отличаются.
Минимальный сброс используется следующим сайтом:
reset.css
/* обобщённый минималистичный сброс CSS
источник вдохновения: https://www.digitalocean.com/community/tutorials/css-minimal-css-reset */
:root {
box-sizing: border-box;
line-height: 1.4;
/* https://kilianvalkhof.com/2022/css-html/your-css-reset-needs-text-size-adjust-probably/ */
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
*, *::before, *::after {
box-sizing: inherit;
}
body, h1, h2, h3, h4, h5, h6, p {
margin: 0;
padding: 0;
font-weight: normal;
}
img {
max-width:100%;
height:auto;
}
Вот другие варианты в порядке по возрастанию сложности:
modern-normalize [2] — более подробное решение для сброса CSS в современных браузерах. Включение из CDN [3]
Kraken [4] — начальная точка для проектов фронтенда. Включает в себя сброс CSS, типографику, сетку и другие удобные инструменты. Включение из CDN [5]
Pico CSS [6] — готовый набор начинающего для стилизации семантического HTML, в том числе и для сброса CSS. Включение из CDN [7]
Tailwind [8] — если вы всё равно будете использовать Tailwind, то можете и использовать его сброс CSS. Включение из CDN [9]
Типографика — фундамент веб-сайта или приложения. Такой легковесный подход, как ванильная веб-разработка, должен согласоваться с легковесным подходом к типографике.
В Modern Font Stacks [10] описываются разнообразные популярные шрифты и варианты отката, позволяющие не загружать пользовательские шрифты и не добавлять внешние зависимости.
На нашем сайте используется стек Geometric Humanist для обычного текста и стек Monospace Code для исходного кода.
В реальном веб-проекте в случае отсутствия правильной структуры объём CSS быстро становится огромным. Давайте рассмотрим инструментарий для создания такой структуры, который предоставляет нам CSS в современных браузерах.
@import — самая базовая техника структурирования — это разбиение CSS на несколько файлов. Мы можем добавлять все эти файлы по порядку как теги <link> в index.html, но это быстро становится неудобным, если мы работаем с несколькими HTML-страницами. Вместо этого лучше импортировать их в index.css.
Например, вот основной файл CSS нашего сайта:
index.css
@import url("./styles/reset.css");
@import url("./styles/variables.css");
@import url("./styles/global.css");
@import url("./components/code-viewer/code-viewer.css");
@import url("./components/tab-panel/tab-panel.css");
Ниже показан рекомендованный способ упорядочивания файлов CSS.
Пользовательские свойства (переменные) — переменные CSS [11] можно использовать для централизованного определения шрифта и темы сайта.
Например, вот переменные для нашего сайта:
variables.css
:root {
/* https://modernfontstacks.com/
geometric humanist font */
--font-system: Avenir, Montserrat, Corbel, source-sans-pro, sans-serif;
/* monospace code font */
--font-system-code: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
--font-system-code-size: 0.8rem;
--background-color: white;
--text-color: black;
--text-color-mute: hsl(0, 0%, 40%);
--link-color: darkblue;
--nav-separator-color: goldenrod;
--nav-background-color: hsl(50, 50%, 95%);
--border-color: black;
--code-text-color: var(--text-color);
--code-text-color-bg: inherit;
--panel-title-color: black;
--panel-title-color-bg: cornsilk;
}
Ещё более мощными переменные CSS становятся в сочетании с calc() [12].
Пользовательские элементы — область видимости стилей легко можно ограничить тегом пользовательского элемента. Например, все стили компонента аватара из предыдущей части [1] статьи имеют в качестве префикса селектор x-avatar:
avatar.css
x-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
}
x-avatar[size=lg] {
width: 3.5rem;
height: 3.5rem;
}
x-avatar img {
border-radius: 9999px;
width: 100%;
height: 100%;
vertical-align: middle;
object-fit: cover;
}
Пользовательские элементы также могут иметь произвольные атрибуты, которые могут использоваться селекторами, как в случае со стилем [size=lg] из этого примера.
Shadow DOM — добавление shadow DOM к веб-компоненту ещё сильнее изолирует его стили от остальной части страницы. Например, компонент x-header из предыдущей части [1] стилизует свой элемент h1 внутри своего CSS, не влияя на содержащую его страницу и на дочерние элементы заголовка.
Все файлы CSS, которые нужно применить к shadow DOM, должны загружаться в неё явным образом, однако переменные CSS передаются в shadow DOM [13].
Ограничение shadow DOM заключается в том, что для использования внутри них пользовательских шрифтов их сначала нужно загрузить в «светлую» DOM.
Существует множество способов упорядочивания файлов CSS в репозитории; на нашем сайте применён такой:
/index.css — корневой файл CSS, который импортирует все остальные при помощи @import.
/styles/reset.css — первым делом импортируется сброс таблицы стилей.
/styles/variables.css — все переменные CSS определены в отдельном файле, в том числе и система шрифтов.
/styles/global.css — глобальные стили, применяемые для веб-страниц сайта.
/components/example/example.css — все неглобальные стили относятся к конкретным компонентам и находятся в файле CSS, расположенном рядом с файлом JS компонента.
Чтобы избежать конфликта стилей между страницами и компонентами, по умолчанию у стилей должна быть локальная область видимости. В ванильной веб-разработке есть два основных механизма реализации этого.
В случае пользовательских элементов, не имеющих shadow DOM, можно добавлять в стили префиксы с тегом пользовательского элемента. Например, вот простой веб-компонент, использующий селекторы с префиксами для создания локальной области видимости:
index.html
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="index.css">
</head>
<body>
<x-example></x-example>
<p>This <p> is not affected, because it is outside the custom element.</p>
<script type="module" src="index.js"></script>
</html>
index.js
import { registerExampleComponent } from './components/example/example.js';
const app = () => {
registerExampleComponent();
}
document.addEventListener('DOMContentLoaded', app);
index.css
@import url("./components/example/example.css");
components/example/example.js
class ExampleComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = '<p>For example...</p>';
}
}
export const registerExampleComponent = () => {
customElements.define('x-example', ExampleComponent);
}
components/example/example.css
x-example p {
font-family: casual, cursive;
color: darkblue;
}

▍ Подсказка: вложенность CSS
Если вам нужен более чистый синтаксис и вас устраивает браузерная поддержка [14], то подумайте над использованием вложенности CSS [15].
components/example/example.cssx-example { p { font-family: casual, cursive; color: darkblue; } }
Пользовательские элементы, использующие shadow DOM, изначально не стилизованы и имеют локальную область видимости, а все стили необходимо явным образом импортировать в них. Вот переработанный пример с префиксами для использования shadow DOM.
index.html
<!doctype html>
<html lang="en">
<body>
<x-example>
<p>This <p> is not affected, even though it is slotted.</p>
</x-example>
<script type="module" src="index.js"></script>
</body>
</html>
index.js
import { registerExampleComponent } from './components/example/example.js';
const app = () => {
registerExampleComponent();
}
document.addEventListener('DOMContentLoaded', app);
components/example/example.js
class ExampleComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="${import.meta.resolve('./example.css')}">
<p>For example...</p>
<slot></slot>
`;
}
}
export const registerExampleComponent = () => {
customElements.define('x-example', ExampleComponent);
}
components/example/example.css
p {
font-family: casual, cursive;
color: darkblue;
}

Чтобы использовать стили из окружающей страницы внутри shadow DOM, можно выбрать один из вариантов:
<link> или @import.::part, чтобы раскрыть API для стилизации [16].Локальную область видимости модулей CSS можно заменить одним из описанных выше способов изменения области видимости. Для образца возьмём каноничный пример модулей CSS [17] из документации Next.JS:
app/dashboard/layout.tsx
import styles from './styles.module.css'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section className={styles.dashboard}>{children}</section>
}
app/dashboard/styles.module.css
.dashboard {
padding: 24px;
}
В качестве ванильного веб-компонента он бы выглядел так:
components/dashboard/layout.js
class Layout extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="${import.meta.resolve('styles.css')}">
<section class="dashboard"><slot></slot></section>
`;
}
}
export const registerLayoutComponent =
() => customElements.define('x-layout', Layout);
components/dashboard/styles.css
@import url("../shared.css");
.dashboard {
padding: 24px;
}
Так как shadow DOM не наследует стили страницы, styles.css должен сначала импортировать стили, общие для страницы и «теневого» веб-компонента.
Давайте рассмотрим список возможностей на главной станице PostCSS [18].
Добавление префиксов поставщика к правилам CSS с использованием значений из Can I Use — в большинстве сценариев использования префиксы поставщика больше не требуются. Показанный в примере псевдокласс :fullscreen теперь работает в браузерах без префиксов [19].
Преобразование современного CSS в то, что может понимать большинство браузеров — современный CSS, который вы хотите использовать, скорее всего, уже поддерживается. Показанное в примере правило color: oklch() теперь работает во всех популярных браузерах [20].
Модули CSS — см. альтернативы, описанные в предыдущем разделе. Организуйте согласованные форматы и избегайте ошибок в таблицах стилей при помощи stylelint. Можно добавить в Visual Studio Code расширение vscode-stylelint [21] для выполнения того же линтинга во время разработки без необходимости встраивания его в этап сборки.
Подведём итог: из-за отказа Microsoft от поддержки IE11 и постоянного совершенствования актуальных браузеров PostCSS по большей мере стал ненужным.
Аналогично PostCSS, давайте разберём список основных возможностей SASS [22]:
Переменные — заменены пользовательскими свойствами CSS [11].
Вложенность — вложенность CSS недавно стала поддерживаться [14] всеми популярными браузерами, что вполне может покрыть ваши потребности.
Модули — можно аппроксимировать сочетанием @import, переменных CSS и описанных выше способов управления областями видимости.
Примеси (mixin) — к сожалению, функция CSS-примесей [23], которая может заменить их, по-прежнему находится на этапе спецификации.
Операторы — во многих случаях могут быть заменены встроенной функцией calc() [12].
Подведём итог: SASS намного мощнее, чем PostCSS, и хотя у многих его возможностей есть ванильные альтернативы, заменить его полностью не так легко. Вам самим решать, стоит ли увеличение сложности из-за препроцессора SASS его дополнительных возможностей.
Для веб-сайтов с большим количеством контента и низкой интерактивностью предпочтительна многостраничная структура.
Отказавшись от использования фреймворков, мы должны будем писать эти HTML-страницы с нуля. При этом важно понимать, как должна выглядеть хорошая минимальная HTML-страница.
example.html
<!doctype html>
<html lang="en">
<head>
<title>Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="index.css">
</head>
<body>
<noscript><strong><font color="#000">Please enable JavaScript to view this page correctly.</font></strong></noscript>
<header>
title and navigation ...
</header>
<main>
main content ...
</main>
<footer>
byline and copyright ...
</footer>
<script type="module" src="index.js"></script>
</body>
</html>
Объяснение каждого элемента:
<!doctype html> — требуется, чтобы HTML парсился как HTML5, а не как более старая версия.
<html lang="en"> — атрибут lang рекомендован, чтобы язык страницы не определялся ошибочно.
<head><title> — используется для вкладки браузера и сохранения в закладки; то есть, по сути, он обязателен.
<head><meta charset="utf-8"> — это почти не требуется, но эту строку нужно добавить, чтобы страница точно интерпретировалась, как UTF-8. Очевидно, что в редакторе, используемом для создания этой страницы, тоже должна быть выбрана UTF-8.
<head><meta name="viewport"> — необходимо для того, чтобы структура страницы удобно просматривалась на мобильных устройствах.
<head><link rel="stylesheet" href="index.css"> — по стандарту таблица стилей загружается из <head> блокирующим образом, чтобы не возникала вспышка нестилизованного контента разметки страницы.
<body><noscript> — так как веб-компоненты не работают JavaScript, обычно рекомендуется добавлять уведомление noscript для пользователей, у которых отключен JavaScript. Это уведомление должно присутствовать только на страницах с веб-компонентами. Если вы не хотите показывать ничего, кроме уведомления, то см. показанный ниже шаблонный паттерн.
<body><header/main/footer> — разметка страницы должна быть упорядочена при помощи HTML-маркеров (landmark) [24]. При правильном использовании landmark помогают в разбиении страницы на логические блоки и обеспечении accessibility структуры страницы. Так как они созданы на основе стандартов, повышается вероятность их совместимости с уже существующими и новыми инструментами accessibility.
<body><script type="module" src="index.js"> — основной файл JavaScript находится в конце, он загружает веб-компоненты.
На страницах, где содержимое должно отображаться только при включенном JavaScript, можно использовать следующий шаблонный паттерн:
index.html
<!doctype html>
<html lang="en">
<head>
<title>Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="index.css">
</head>
<body>
<noscript><strong><font color="#000">Please enable JavaScript to view this page.</font></strong></noscript>
<template id="page">
<header>
title and navigation ...
</header>
<main>
main content ...
</main>
<footer>
byline and copyright ...
</footer>
</template>
<script type="module" src="index.js"></script>
</body>
</html>
index.js
const app = () => {
const template = document.querySelector('template#page');
if (template) document.body.appendChild(template.content, true);
}
document.addEventListener('DOMContentLoaded', app);
В разметке страницы по умолчанию должен использоваться семантический HTML для повышения accessibility и улучшения SEO. Веб-компоненты следует использовать только в тех случаях, когда сложность и степень взаимодействий превышает возможности стандартной HTML-разметки.
Освойте следующие аспекты семантического HTML:
Landmark (маркеры) [24] — как говорилось выше, landmark — это фундамент структуры страницы, по умолчанию обеспечивающие качественную структуру и accessibility.
Элементы [25] — хорошее знание множества встроенных элементов HTML сэкономит вам время благодаря отсутствию необходимости пользовательских элементов и упрощению реализации в случае необходимости пользовательских элементов. При правильном использовании HTML-элементов они по умолчанию обеспечивают accessibility.
Формы [26] — при их полнофункциональном использовании встроенные формы HTML способны реализовывать множество сценариев применения интерактивности. Изучите такие их возможности, как разнообразные типы ввода [27], валидация на стороне клиента [28] и псевдоклассы UI [29]. Если вы не можете найти подходящего для себя типа ввода, то можете использовать связанные с формами пользовательские элементы [30], но учитывайте поддержку браузерами ElementInternals [31].
Вероятно, вам захочется добавить в HTML элемент, не основанный на стандартах, а именно ссылку на фавиконку:
favicon.ico в корень сайта и добавьте на неё ссылку в свой HTML: <link rel="icon" href="favicon.ico">Рекомендуемая структура проекта для ванильного многостраничного веб-сайта такова:
/ — в корне проекта находятся файлы, которые не будут публиковаться, например, README.md, LICENSE и .gitignore.
/public — папка public публикуется в неизменном виде, без этапов сборки. В ней заключается весь веб-сайт.
/public/index.html — главная лендинг-страница веб-сайта, не особо отличающаяся от других страниц, за исключением пути.
/public/index.[js/css] — основная таблица стилей и javascript. Они содержат общие для всех страниц стили и код.
index.js загружает и регистрирует веб-компоненты, используемые на всех страницах. Если сделать его общим для нескольких HTML-страниц, то можно избежать ненужного дублирования и рассогласованности между страницами.
/public/pages/[имя].html — все прочие страницы сайта, каждая из которых включает в себя одинаковые index.js и index.css и, разумеется, содержит непосредственно контент в виде разметки HTML с использованием веб-компонентов.
public/components/[name]/ — по одной папке на каждый веб-компонент, содержащей файлы [имя].js и [имя].css. Файл .js импортируется в файл index.js для регистрации веб-компонента. Файл .css, как говорилось выше, импортируется в глобальный index.css или в shadow DOM.
/public/lib/ — для всех внешних библиотек, используемых как зависимости. Ниже мы расскажем о том, как добавлять и использовать эти зависимости.
/public/styles/ — глобальные стили, на которые ссылается index.css.
Файлы конфигурации для повышения удобства работы в редакторах программиста тоже размещаются в корне проекта. Благодаря расширениям редактора основная часть процесса разработки возможна без этапа сборки. Пример см. в статье о настройке Visual Studio Code [36].
«Олдскульный» способ маршрутизации стандартных HTML-страниц и связующих их тегов <a> обладает следующими преимуществами: простое индексирование поисковыми движками и изначальная полная поддержка функциональности истории браузера и закладок.
В процессе разработки вам могут потребоваться сторонние библиотеки. Их можно использовать без npm и бандлера.
Чтобы использовать библиотеки без бандлера, их предварительно нужно собрать в формат ESM или UMD. Такие библиотеки можно скачать с unpkg.com:
unpkg.com/[library]/ (последняя косая черта важна), например, на unpkg.com/microlight/ [37]dist, esm или umdlib/Или же библиотеку можно загрузить напрямую из CDN.
Формат модулей UMD [38] — это старый формат для библиотек, загружаемых из тега script; он обладает самой широкой поддержкой, особенно среди старых библиотек. Его можно распознать по наличию typeof define === 'function' && define.amd в JS библиотеки.
Чтобы включить его в свой проект, нужно выполнить следующие действия:
<script src="lib/microlight.js"></script>const { microlight } = window;
Формат модулей ESM [39] (также называемый модулями JavaScript) — это формат, определённый стандартом ECMAScript; у новых и популярных библиотек обычно есть ESM-сборка. Её можно распознать по использованию ключевого слова export.
Чтобы включить его в свой проект, нужно выполнить следующие действия:
import('https://unpkg.com/web-vitals@4.2.2/dist/web-vitals.js').then((webVitals) => ...)
import webVitals from 'lib/web-vitals.js'
Для удобного упорядочивания библиотек и отделения их от остальной кодовой базы их можно загружать и экспортировать из файла imports.js.
Например, вот страница, на которой используется UMD-сборка Day.js [40] и ESM-сборка web-vitals [41]:

Рендеринг текста выполнен компонентом <x-metrics>:
components/metrics.js
import { dayjs, webVitals } from '../lib/imports.js';
class MetricsComponent extends HTMLElement {
#now = dayjs();
#ttfb;
#interval;
connectedCallback() {
webVitals.onTTFB(_ => this.#ttfb = Math.round(_.value));
this.#interval = setInterval(() => this.update(), 500);
}
disconnectedCallback() {
clearInterval(this.#interval);
this.#interval = null;
}
update() {
this.innerHTML = `
<p>Page loaded ${this.#now.fromNow()}, TTFB ${this.#ttfb} milliseconds</p>
`;
}
}
export const registerMetricsComponent = () => {
customElements.define('x-metrics', MetricsComponent);
}
В папке /lib находятся следующие файлы:
Изучив глубже последний файл, мы увидим, как он выполняет загрузку сторонних зависимостей:
lib/imports.js
// UMD-версия dayjs с https://unpkg.com/dayjs/
const dayjs = window.dayjs;
const dayjsRelativeTime = window.dayjs_plugin_relativeTime;
dayjs.extend(dayjsRelativeTime);
// ESM-версия web-vitals с https://unpkg.com/web-vitals/dist/web-vitals.js
import * as webVitals from './web-vitals.js';
export { dayjs, webVitals };
Он импортирует ESM-библиотеку напрямую, но подтягивает UMD-библиотеки из объекта Window. Они загружаются в HTML.
Вот объединённый пример:
index.html
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="index.css">
</head>
<body>
<script src="./lib/dayjs/dayjs.min.js"></script>
<script src="./lib/dayjs/relativeTime.js"></script>
<script type="module" src="index.js"></script>
<x-metrics></x-metrics>
</body>
</html>
index.css
body { font-family: sans-serif; }
index.js
import { registerMetricsComponent } from './components/metrics.js';
const app = () => {
registerMetricsComponent();
};
document.addEventListener('DOMContentLoaded', app);
components/metrics.js
import { dayjs, webVitals } from '../lib/imports.js';
class MetricsComponent extends HTMLElement {
#now = dayjs();
#ttfb;
#interval;
connectedCallback() {
webVitals.onTTFB(_ => this.#ttfb = Math.round(_.value));
this.#interval = setInterval(() => this.update(), 500);
}
disconnectedCallback() {
clearInterval(this.#interval);
this.#interval = null;
}
update() {
this.innerHTML = `
<p>Page loaded ${this.#now.fromNow()}, TTFB ${this.#ttfb} milliseconds</p>
`;
}
}
export const registerMetricsComponent = () => {
customElements.define('x-metrics', MetricsComponent);
}
lib/imports.js
// UMD-версия dayjs с https://unpkg.com/dayjs/
const dayjs = window.dayjs;
const dayjsRelativeTime = window.dayjs_plugin_relativeTime;
dayjs.extend(dayjsRelativeTime);
// ESM-версия web-vitals с https://unpkg.com/web-vitals/dist/web-vitals.js
import * as webVitals from './web-vitals.js';
export { dayjs, webVitals };
К сожалению, не у всех библиотек есть UMD- или ESM-сборки, но их становится всё больше.
Альтернативой способу с imports.js могут стать import map [42]. Они определяют уникальное отображение между именем модуля, который можно импортировать, и соответствующим файлом библиотеки в особом теге script в head HTML. Они позволяют использовать в остальной части кодовой базы более традиционный синтаксис импорта на основе модулей.
Вот, как будет выглядеть предыдущий пример, адаптированный под использование import map:
index.html
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="index.css">
<script src="./lib/dayjs/dayjs.min.js"></script>
<script src="./lib/dayjs/relativeTime.js"></script>
<script type="importmap">
{
"imports": {
"dayjs": "./lib/dayjs/module.js",
"web-vitals": "./lib/web-vitals.js"
}
}
</script>
</head>
<body>
<script type="module" src="index.js"></script>
<x-metrics></x-metrics>
</body>
</html>
lib/dayjs/module.js
// UMD-версия dayjs с https://unpkg.com/dayjs/
const dayjs = window.dayjs;
const dayjsRelativeTime = window.dayjs_plugin_relativeTime;
dayjs.extend(dayjsRelativeTime);
export default dayjs;
components/metrics.js
import dayjs from 'dayjs';
import * as webVitals from 'web-vitals';
class MetricsComponent extends HTMLElement {
#now = dayjs();
#ttfb;
#interval;
connectedCallback() {
webVitals.onTTFB(_ => this.#ttfb = Math.round(_.value));
this.#interval = setInterval(() => this.update(), 500);
}
disconnectedCallback() {
clearInterval(this.#interval);
this.#interval = null;
}
update() {
this.innerHTML = `
<p>Page loaded ${this.#now.fromNow()}, TTFB ${this.#ttfb} milliseconds</p>
`;
}
}
export const registerMetricsComponent = () => {
customElements.define('x-metrics', MetricsComponent);
}
При использовании import map нужно учитывать следующие аспекты:
module.js для dayjs в этом примере.<script type="importmap" src="importmap.json"> пока поддерживаются не всеми браузерами [43]. Из-за этого import map должны дублироваться на каждой HTML-странице.index.js, предпочтительно из раздела <head>.node_modules или из CDN. Можно использовать JSPM generator [44] для быстрого создания import map для зависимостей for CDN. Однако при этом стоит иметь в виду, что из-за добавления таких внешних зависимостей ванильная кодовая база будет зависеть от постоянной доступности соответствующего сервиса.Ванильные веб-сайты поддерживаются всеми современными браузерами. Но что это значит?
Чтобы следить за новыми веб-стандартами, изучайте следующие проекты:
Для развёртывания сайта можно выбрать любого провайдера, способного хостить статические веб-сайты.
На примере GitHub Pages [69]:
path и измените его на ./public
Все популярные фреймворки тестирования предназначены для работы в конвейерах сборки.
Однако у ванильного веб-сайта нет этапа сборки. Для тестирования веб-компонентов можно применить старомодный подход: тестирование в браузере при помощи фреймворка Mocha.
Например, вот юнит-тесты для компонента <x-tab-panel>, использованного для отображения вкладок панелей исходного кода на нашем веб-сайте:

А для того, чтобы ещё глубже разобраться в коде, покажу, как выглядит тестирование исходного кода:
tests/index.html
<!doctype html>
<html lang="en">
<head>
<title>Plain Vanilla - Tests</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<link rel="icon" href="../favicon.ico">
<link rel="stylesheet" href="../index.css">
<!-- https://unpkg.com/mocha@9.2.2/mocha.css -->
<link rel="stylesheet" href="./lib/mocha/mocha.css" />
<!-- https://unpkg.com/chai@4.3.6/chai.js -->
<script src="./lib/mocha/chai.js"></script>
<!-- https://unpkg.com/mocha@9.2.2/mocha.js -->
<script src="./lib/mocha/mocha.js"></script>
<!-- https://unpkg.com/@testing-library/dom@8.17.1/dist/@testing-library/dom.umd.js -->
<script src="./lib/@testing-library/dom.umd.js"></script>
</head>
<body>
<div id="mocha"></div>
<script type="module" class="mocha-init">
mocha.setup('bdd');
mocha.checkLeaks();
</script>
<script type="module" src="./tabpanel.test.js"></script>
<!-- здесь другие тесты -->
<script type="module" class="mocha-exec" src="./index.js"></script>
</body>
</html>
tests/index.js
import { registerTabPanelComponent } from "../components/tab-panel/tab-panel.js";
const app = () => {
registerTabPanelComponent();
mocha.run();
}
document.addEventListener('DOMContentLoaded', app);
tests/tabpanel.test.js
import { render, screen, waitFor, expect, fireEvent } from './imports-test.js';
const renderTabPanel = () => {
const div = document.createElement('div');
div.innerHTML = `
<x-tab-panel>
<x-tab title="Tab 1" active>
<p>Tab 1 content</p>
</x-tab>
<x-tab title="Tab 2">
<p>Tab 2 content</p>
</x-tab>
</x-tab-panel>
`;
render(div);
}
describe('tabpanel', () => {
it("renders a tabpanel with active tab", async () => {
// ARRANGE
renderTabPanel();
// ASSERT
// выбрана активная вкладка
const activeTab = await screen.findByRole('tab', { name: 'Tab 1', selected: true });
expect(activeTab).to.not.be.undefined;
// активная tabpanel видима
const activePanel = screen.getByText(/Tab 1 content/);
expect(activePanel).to.not.be.undefined;
// контент неактивной tabpanel скрыт
const tab2 = screen.getByTitle('Tab 2');
await waitFor(() => expect(tab2.offsetParent).to.be.null);
});
it("activates a different tab on click", async () => {
// ARRANGE
renderTabPanel();
const tab2 = screen.getByTitle('Tab 2');
// ASSERT
// контент неактивной tabpanel скрыт
await waitFor(() => expect(tab2.offsetParent).to.be.null);
// находим кнопку неактивной вкладки и нажимаем на неё
const tab2Button = await screen.findByRole('tab', { name: 'Tab 2' });
expect(tab2Button).not.to.be.undefined;
fireEvent.click(tab2Button);
// делаем видимым контент неактивной tabpanel
await waitFor(() => expect(tab2.offsetParent).not.to.be.null);
});
});
tests/imports-test.js
const { expect } = window.chai;
const { getByText, queries, within, waitFor, fireEvent } = window.TestingLibraryDom;
let rootContainer;
let screen;
beforeEach(() => {
// скрытый div, в который тест может рендерить элементы
rootContainer = document.createElement("div");
rootContainer.style.position = 'absolute';
rootContainer.style.left = '-10000px';
document.body.appendChild(rootContainer);
// предварительная привязка вспомогательных @testing-library/dom к rootContainer
screen = Object.keys(queries).reduce((helpers, key) => {
const fn = queries[key]
helpers[key] = fn.bind(null, rootContainer)
return helpers
}, {});
});
afterEach(() => {
document.body.removeChild(rootContainer);
rootContainer = null;
});
function render(el) {
rootContainer.appendChild(el);
}
export {
rootContainer,
expect,
render,
getByText, screen, within, waitFor, fireEvent
};
Примечания по работе с таким подходом:
public/tests/. Поэтому тесты будут доступны интерактивно, если добавить /tests к URL развёрнутого сайта. Если вы не хотите развёртывать тесты на работающем веб-сайте, то исключите папку тестов на этапе развёртывания.imports-test.js конфигурирует её для ванильного использования.shadowRoot, а затем выполнить запрос внутри него.Примером разработки может быть наш веб-сайт [75]. Его проект есть на GitHub [76].
Автор: ru_vds
Источник [77]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/shadow-dom/420476
Ссылки в тексте:
[1] первой части: https://habr.com/ru/companies/ruvds/articles/909390/
[2] modern-normalize: https://github.com/sindresorhus/modern-normalize
[3] Включение из CDN: https://www.jsdelivr.com/package/npm/modern-normalize
[4] Kraken: https://cferdinandi.github.io/kraken/
[5] Включение из CDN: https://cdnjs.com/libraries/Kraken
[6] Pico CSS: https://picocss.com/
[7] Включение из CDN: https://picocss.com/docs#usage-from-cdn
[8] Tailwind: https://tailwindcss.com/
[9] Включение из CDN: https://kopi.dev/tailwind-css-with-cdn-html/
[10] Modern Font Stacks: https://modernfontstacks.com/
[11] переменные CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
[12] calc(): https://developer.mozilla.org/en-US/docs/Web/CSS/calc
[13] переменные CSS передаются в shadow DOM: https://javascript.info/shadow-dom-style
[14] браузерная поддержка: https://caniuse.com/css-nesting
[15] вложенности CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting
[16] раскрыть API для стилизации: https://meowni.ca/posts/part-theme-explainer/
[17] пример модулей CSS: https://nextjs.org/docs/app/building-your-application/styling/css-modules#example
[18] PostCSS: https://postcss.org/
[19] работает в браузерах без префиксов: https://caniuse.com/mdn-css_selectors_fullscreen
[20] работает во всех популярных браузерах: https://caniuse.com/?search=oklch
[21] расширение vscode-stylelint: https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint
[22] SASS: https://sass-lang.com/guide/
[23] CSS-примесей: https://github.com/w3c/csswg-drafts/issues/9350
[24] HTML-маркеров (landmark): https://developer.mozilla.org/en-US/blog/aria-accessibility-html-landmark-roles/
[25] Элементы: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
[26] Формы: https://developer.mozilla.org/en-US/docs/Learn/Forms
[27] разнообразные типы ввода: https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types
[28] валидация на стороне клиента: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation
[29] псевдоклассы UI: https://developer.mozilla.org/en-US/docs/Learn/Forms/UI_pseudo-classes
[30] связанные с формами пользовательские элементы: https://web.dev/articles/more-capable-form-controls
[31] поддержку браузерами ElementInternals: https://caniuse.com/mdn-api_elementinternals
[32] фавиконки в SVG: https://medium.com/swlh/are-you-using-svg-favicons-yet-a-guide-for-modern-browsers-836a6aace3df
[33] Встройте тёмный режим: https://css-tricks.com/svg-favicons-in-action/
[34] RealFaviconGenerator: https://realfavicongenerator.net/
[35] стандарт де-факто: https://dev.to/masakudamatsu/favicon-nightmare-how-to-maintain-sanity-3al7
[36] настройке Visual Studio Code: https://plainvanillaweb.com/blog/articles/2024-10-20-editing-plain-vanilla/
[37] unpkg.com/microlight/: https://unpkg.com/microlight/
[38] Формат модулей UMD: https://jameshfisher.com/2020/10/04/what-are-umd-modules/
[39] Формат модулей ESM: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
[40] Day.js: https://day.js.org/
[41] web-vitals: https://github.com/GoogleChrome/web-vitals
[42] import map: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
[43] пока поддерживаются не всеми браузерами: https://github.com/WICG/import-maps/issues/235
[44] JSPM generator: https://generator.jspm.io
[45] caniuse.com: https://caniuse.com
[46] Import Map: https://caniuse.com/import-maps
[47] декларативных Shadow DOM: https://caniuse.com/declarative-shadow-dom
[48] HTTP/2: https://caniuse.com/http2
[49] семантические элементы HTML5: https://caniuse.com/html5semantic
[50] пользовательские элементы: https://caniuse.com/custom-elementsv1
[51] шаблоны: https://caniuse.com/template
[52] Shadow DOM: https://caniuse.com/shadowdomv1
[53] MutationObserver: https://caniuse.com/mdn-api_mutationobserver
[54] CustomEvent: https://caniuse.com/customevent
[55] FormData: https://caniuse.com/mdn-api_formdata
[56] Element.closest: https://caniuse.com/element-closest
[57] модули JavaScript: https://caniuse.com/es6-module
[58] ECMAScript 6 / 2015: https://caniuse.com/es6
[59] ECMAScript 8 / 2017: https://caniuse.com/async-functions,object-values,object-entries,mdn-javascript_builtins_object_getownpropertydescriptors,pad-start-end,mdn-javascript_grammar_trailing_commas_trailing_commas_in_functions
[60] ECMAScript 11 / 2020: https://caniuse.com/?feats=mdn-javascript_operators_optional_chaining,mdn-javascript_operators_nullish_coalescing,mdn-javascript_builtins_globalthis,es6-module-dynamic-import,bigint,mdn-javascript_builtins_promise_allsettled,mdn-javascript_builtins_string_matchall,mdn-javascript_statements_export_namespace,mdn-javascript_operators_import_meta
[61] @import: https://caniuse.com/mdn-css_at-rules_import
[62] переменные: https://caniuse.com/css-variables
[63] calc(): https://caniuse.com/calc
[64] flexbox: https://caniuse.com/flexbox
[65] grid: https://caniuse.com/css-grid
[66] display: contents: https://caniuse.com/css-display-contents
[67] Baseline: https://web.dev/baseline
[68] Interop: https://web.dev/blog/interop-2024
[69] GitHub Pages: https://pages.github.com/
[70] Mocha: https://mochajs.org/
[71] Chai: https://www.chaijs.com/
[72] DOM Testing Library: https://testing-library.com
[73] не может выполнять запросы внутри корней shadow: https://github.com/testing-library/dom-testing-library/issues/413
[74] методы async: https://testing-library.com/docs/dom-testing-library/api-async
[75] веб-сайт: https://plainvanillaweb.com/
[76] GitHub: https://github.com/jsebrech/plainvanilla/
[77] Источник: https://habr.com/ru/companies/ruvds/articles/910734/?utm_source=habrahabr&utm_medium=rss&utm_campaign=910734
Нажмите здесь для печати.