Фреймворк-независимые дизайн-системы: практический подход к веб-компонентам
Практическое руководство по созданию фреймворк-независимой дизайн-системы на веб-компонентах. Разбираем, как объединить компонентную библиотеку и документацию в едином репозитории с помощью Elena и VitePress.
Перевод статьи Scott Riley (Piccalilli), оригинал: https://piccalil.li/blog/framework-agnostic-design-systems-part-1/
Прежде чем мы начнём, небольшое примечание: это практическое руководство, которое охватывает управление, создание и упаковку компонентов дизайн-системы. Невозможно углубляться в каждый шаг до мельчайших деталей, не превратив материал в полноценный курс. Предполагается наличие некоторых базовых знаний:
- Базовые знания HTML и CSS
- Базовое понимание веб-компонентов
- Установленные Node.js и npm
- Умение работать в терминале на уровне, достаточном для установки пакетов
- Базовые знания конфигурационных файлов и JSON
- Понимание революционной идеи о том, что
<button>— это не<div>
Наконец, это довольно длинный пост. Считайте каждый h2 приглашением сделать перерыв на чай и подышать свежим воздухом.
Фреймворк-независимые компоненты
Из всех недавних хайпов/пузырей/назовите их как хотите в мире технологий тот, что одновременно волновал и ставил в тупик меня в равной степени, — это бум дизайн-систем. Общая концепция определённо фантастическая, и почти любая команда или проект могут извлечь пользу из какой-либо формы централизованного хранилища дизайнерских решений. Но, как и любой другой бум, он породил много странностей. Люди сошлись на определённых способах восприятия дизайна в «эпоху дизайн-систем», слайды Atomic Design в каждой конференц-презентации стали мемом, а дизайн-токены стали целой личностью для некоторых.
Этот пост — не обо всех странностях, но нам нужно опереться на что-то более конкретное, чем крутая технология — это круто. И одна из моих наименее любимых странностей из курса «Дизайн-системы: странности 101» довольно специфична, но при этом является источником настоящей физической боли для меня: библиотеки компонентов, привязанные к конкретному фреймворку.
Идея о том, что наши дизайн-системы могут, и даже должны, работать на компонентах, написанных под конкретный фреймворк, кажется мне дикой. Дизайн-системы, по крайней мере частично, должны быть про универсальность, компонуемость и переносимость. Встраивание привязки к фреймворку в уравнение с самого первого дня абсолютно нелепо.
Я понимаю, что веб-стандарты немного отставали на заре дизайн-систем, а веб-компоненты и кастомные элементы отставали от всех тех приятных возможностей, которые предлагают реактивные фреймворки с управлением состоянием. К счастью для нас, это больше не так, и существует ряд замечательных инструментов, построенных вокруг создания и потребления стандартных веб-компонентов.
На самом деле, я бы даже сказал, что на момент написания этого поста веб-компоненты — единственно лучший подход к созданию библиотеки компонентов. Они переносимы, используют веб-стандарты, и любой фреймворк, который не является полным бардаком (и многие, которые являются, смотрю на тебя, React), будет поддерживать их либо напрямую, либо с минимальной конфигурацией.
Отвлечения в сторону, этот пост будет максимально практичным введением в создание веб-компонентов с использованием веб-стандартов, современного CSS и некоторых удобных инструментов, которые помогут нам ускориться. Он также будет весьма субъективным и сильно опираться на идею, что мы должны поставлять нашу библиотеку вместе с документацией в одном репозитории. Вы не обязаны заниматься всеми этими штуками с документацией, если не хотите, но я настоятельно рекомендую попробовать. Весь код здесь для вас, так почему бы и нет!
Принципы
Сначала рассмотрим несколько принципов. Если они вам близки — читайте дальше; если нет — можете закрыть вкладку, заварить чай и заняться своими делами.
Минимально возможный уровень
Хотя существует масса инструментов, которые превращают компоненты для конкретного фреймворка (давайте будем честны — почти всегда это React) в веб-компоненты, я не думаю, что такой подход соответствует тому, что мы говорим, что хотим от наших библиотек компонентов и паттернов по духу.
Когда мы создаём компоненты и проектируем API компонентов, мы разрабатываем некоторые из самых атомарных элементов дизайн-системы. В таком сценарии, на мой взгляд, есть явная, ощутимая польза от работы максимально близко к платформе доставки. Для веб-продуктов это означает работу непосредственно с веб-стандартами.
На уровне компонентов я гораздо более настроен на удаление слоёв абстракции и работу ближе к веб-стандартам. Вместо того чтобы сразу прыгать в модный фреймворк, я гораздо больше предпочитаю работать с инструментами сборки и лёгкими обёртками. Это означает, что вы всегда находитесь в «режиме веб-стандартов», идёте прямым путём. Горжусь вами.
Максимально простые компоненты
Компоненты должны быть максимально примитивными. Даже самый, казалось бы, сложный компонент можно представить как очень простую конечную машину состояний. Мне ещё не встречался компонент, который нельзя было бы выразить таким образом, и вам не нужно по умолчанию обращаться к раздутому фреймворку для простых вариантов компонентов и изолированного состояния.
Я бы даже сказал, что многие реактивные компоненты — это антипаттерн. Реактивность обычно означает логику, и очень часто это приводит нас в область «бизнес-логики» и общего состояния на уровне контейнера или приложения. Простая реактивность на уровне компонента часто необходима — представьте кнопку, которая показывает спиннер загрузки, пока что-то обрабатывается, и возвращается в исходное состояние, когда всё готово, — но добавление тонн состояний и реактивности в изолированном коде компонента всегда кажется мне красным флагом.
По моему опыту, компоненты наиболее полезны, когда им явно сообщают, какое состояние они должны отражать и какой контент содержать. Они намеренно ограничены и явно декларативны. Обработка сложного состояния и реактивности в вашем приложении, даже если это означает комбинирование нескольких примитивов в паттерн, специфичный для приложения, гораздо более разумна, чем попытка централизовать сложный громадный компонент, который пытается слишком многое обрабатывать.
Максимально устойчиво к будущему
Устоявшиеся фреймворки со временем становятся устаревшими технологиями. Учитывая всю работу по «переписыванию Angular-проектов на React», на которой некоторые из нас спокойно прожили целых два года, мы должны это понимать. Сам React становится (можно спорить, уже стал) устаревшей технологией, а «переписывание нашего React-приложения на Solid/Svelte» — теперь обычное дело. Я вполне ожидаю, что это будет повторяться до тошноты.
Веб-стандарты — хотя, признаюсь, они развиваются медленнее и поддерживаются утомительными, своеобразными процессами выпуска — всегда будут с нами. Веб-стандарты выдержали проверку времени и последовательно доказывают, что они заметно более надёжны, чем ваш проблемный любимый, переусложнённый фреймворк.
Фреймворки тоже по-настоящему замечательны, когда используются правильно. Говоря по опыту, попытка написать состоятельные, реактивные приложения на ванильном HTML, CSS и JS — закаляющая, но в конечном счёте неразумная задача. Однако примитивные компоненты — это не сложные, состоятельные, реактивные веб-приложения. Это маленькие куски атомарного веб-кода, и создавать их со всеми накладными расходами и своеобразиями полноценного фреймворка — это просто приглашение к будущему устареванию.
Создавая непосредственно с помощью веб-стандартов, мы получаем более низкоуровневое понимание того, как работают наши компоненты, встроенную защиту от будущего, избегая модного фреймворка, и по сути более прогрессивную, доступную (или, по крайней мере, более легко делаемую доступной) и нативную для веба библиотеку компонентов.
Разделяя ваши атомарные компоненты дизайн-системы от ваших компонентов приложения, вы получаете лучшее из обоих миров: переносимые, примитивные компоненты на уровне системы; сложные и реактивные компоненты и обёртки на уровне приложения.
Таким образом, когда вам действительно понадобится переписать приложение на Solid/Svelte, вам хотя бы не придётся переписывать всю библиотеку компонентов вместе с ним.
Принимать решения в коде
Я абсолютно готов стоять насмерт на этой позиции. Инструменты для дизайна — ужасные места для принятия решений по дизайн-системе. Это отчасти потому, насколько оторваны такие инструменты, как Figma, от того, как на самом деле работают дизайн-системы, вплоть до откровенно катастрофического несоответствия словаря и концепций.
Как человек, который по сути больше дизайнер, чем разработчик, я создал и работал с более чем дюжиной «дизайн-систем» в Figma. Как человек, который также проводит гораздо больше времени в коде, чем в инструментах дизайна, я считаю себя вправе сказать, что ни одна из них не отражала того, чем должна быть хорошая системная основа. Это не укол в сторону дизайнеров, которые не пишут код, скорее просто показывает, насколько сложной сами инструменты делают эту часть нашей работы.
Инструменты для дизайна — это места для быстрого тестирования разных идей и экспериментов со стилем и компоновкой. Они абсолютно ужасны для кодификации системных решений, отчасти из-за своей самой природы: они предоставляют очень маленькое, проприетарное подмножество возможностей нашего реального носителя — браузера.
Так же и браузер, но я рискую укрепиться на том самом холме.
Окончательные системные дизайнерские решения должны приниматься в браузере. Токены цвета могут использовать современные цветовые пространства. Токены размеров и отступов должны выражаться в относительных единицах, где это возможно. Почти каждый тип токена может выиграть от какой-либо математики, включая логарифмические шкалы для типографики или программные сдвиги оттенка и светлоты для цветов. API компонентов также следует строить с помощью надёжных, хорошо типизированных определений. Инструменты дизайна могут предложить лишь подобие этих концепций.
Если вы начинаете с Figma — это вполне нормально, но это ужасный источник истины. Воспринимайте свой инструмент дизайна как точный инструмент прототипирования, а не как конечную цель для дизайнерских решений.
Документировать по ходу разработки
Опираясь на последний принцип, если лучшее место для принятия решений — код, то лучшее время для документирования этих решений — фаза сборки, пока они свежи в вашей голове. Разрабатываете props? Ну посмотрите-ка, у вас уже есть прекрасный набор определений типов для этих props, неплохо было бы добавить туда маленький комментарий JSDoc и заняться своими делами.
Мне нравится идти дальше и разворачивать библиотеку компонентов прямо в документирующем фреймворке вроде VitePress, активно создавая человекочитаемую документацию параллельно с разработкой самих компонентов. Это не только в итоге станет «официальной» документацией дизайн-системы, но и позволит проверить, насколько переносимы ваши компоненты.
Это делает мой мозг счастливым, потому что полное кодовое представление моих дизайн-систем (что абсолютно, всегда включает фактическую документацию) живёт в одном репозитории. Всю систему можно развернуть, не жонглируя зависимостями, и это заставляет меня относиться к документации как к необходимому шагу к релизу.
Давайте создадим (и задокументируем)
Ладно, хватит болтать, давайте на самом деле создадим что-то практичное. Мы соберём основы для централизованной, независимой от фреймворка библиотеки компонентов. Мы будем прорабатывать документацию по мере создания компонентов, что даст нам действительно чистый тестовый стенд для самих компонентов. Настоящий порочный круг, если таковой вообще был.
Вот что у нас будет в конце этой статьи:
- Основа для нашей гибридной библиотеки компонентов/документации «всё-в-одном» дизайн-системы
- Стильная, хорошо задокументированная кнопка как веб-компонент
- Развёртываемый сайт документации, который показывает, как замечательна наша кнопка
- Готовая к продакшену библиотека компонентов, которую можно опубликовать в вашем любимом пакетном менеджере
Нам действительно нужно беспокоиться только о двух инструментах: Elena для сборки и распространения нашей библиотеки компонентов и VitePress для создания нашей документации.
Elena
Клей для всего этого проекта — Elena. Фантастическая библиотека от непобедимого Ariel Salminen для создания прогрессивных веб-компонентов. Я не буду углубляться в философию того, что означает «прогрессивный» в этом контексте, потому что это уже исключительно хорошо задокументировано самим Ariel.
Elena — это крошечная библиотека, которая делает Just Enough Abstraction™ поверх стандартных веб-компонентов. Мы получаем такие вещи, как props (отражаемые как пользовательские атрибуты), изолированную реактивность, методы жизненного цикла и даже классные штуки вроде миксинов для компонуемости. Мы не будем углубляться слишком сильно в Elena, но следите за ходом мысли и, если вам понравятся основы, я очень рекомендую погрузиться во всё, что она предлагает. Это круто.
Цитата из документации Elena:
[Elena] берёт на себя межфреймворковую сложность (синхронизация prop/атрибута, делегирование событий, совместимость с фреймворками), чтобы вы могли сосредоточиться на создании компонентов, а не на инфраструктуре.
Именно этого я и хочу от такого инструмента: позвольте мне писать код, не абстрагируйте веб-стандарты, разберитесь со всей странной ерундой, которую я не хочу трогать.
VitePress
Я не буду тратить много времени на VitePress, потому что, честно говоря, это просто самое удобное готовое решение для документации, которое не называется Storybook. Пара npm install — и у нас есть надёжное, основанное на Markdown решение для документации, готовое к работе.
Позже мы сделаем несколько классных штук с VitePress, JSDoc и нашим сгенерированным Elena манифестом пользовательских элементов, что поможет ускорить процесс документирования, но, честно говоря, иметь где-то документировать гораздо важнее, чем то, во что мы документируем.
Структура проекта
Здесь всё может изначально показаться немного странным. Хотя нам нужно собирать и распространять наши компоненты как отдельную библиотеку, нам также нужно их видеть и тестировать. Самый простой способ — это слепить статический сайт и просто свалить все компоненты на одну страницу. Это вполне нормально, и, по сути, я бы поощрил это, если вы просто экспериментируете с Elena, но по причинам, изложенным выше, я считаю, что имеет большой смысл создавать внутри нашей документации.
У нас по сути будет что-то вроде монорепозитория: Elena будет делать своё дело на уровне компонентов, а сам сайт документации будет статически генерироваться с помощью VitePress.
Создание каркаса проекта
Здесь довольно много движущихся частей, поэтому вместо того, чтобы просто кидать вам дикую цепочку склеенных npm install, давайте разберём настройку шаг за шагом.
Для начала создадим папку проекта:
Замените my-ds на любое название, которое хотите дать своему проекту.
Затем инициализируем npm:
Команда npm init -y создаст файл package.json в корне проекта. Эта корневая папка напрямую ничего не будет делать — она просто склеивает наши компоненты и документацию вместе в монорепозитории.
Приведём в порядок наш package.json:
Здесь происходит кое-что, что пока не имеет особого смысла (и даже не будет работать) — это станет понятно чуть позже. Настройка workspaces позволит нам обращаться со сборкой библиотеки компонентов как с пакетом, не публикуя его, а скрипты dev, а также различные watch и build позволят нам отслеживать и собирать либо документацию, либо компоненты (либо оба варианта одновременно с помощью команды dev).
Кстати, давайте установим пару штук:
Это установит concurrently, который позволит нам запускать команду watch Elena и команду dev VitePress одновременно. Мы будем держать её запущенной, пока работаем. Также установится VitePress и его тема по умолчанию. Если хотите заморочиться — можете использовать другую тему.
Настройка VitePress
Настроим документацию. Для начала создадим папку docs:
Затем создайте docs/.vitepress/config.mjs:
Проверьте расширение!Обратите внимание: мы используем .mjs, а не .js — это заставляет Node трактовать файл как ESM. Это необходимо, потому что мы импортируем из vitepress. Вам не обязательно знать, что это значит. Честно говоря, я не уверен, что сам это понимаю. Просто убедитесь, что используете .mjs. Ладно, спасибо.
Здесь мы используем postIsolateStyles, чтобы ограничить область действия встроенных стилей .vp-doc VitePress и не дать им просочиться в наши примеры компонентов. Без этого встроенные стили VitePress могут переопределять стили ваших компонентов (включая инкапсулированные сбросы) из-за того, как Vite внедряет таблицы стилей во время выполнения.
Далее создайте минимальный docs/index.md, чтобы у VitePress была домашняя страница:
Сейчас хороший момент, чтобы проверить, всё ли работает:
Вы должны увидеть очень простой локальный сайт на VitePress! По умолчанию он будет доступен по адресу http://localhost:5173/.
Настройка Elena
Теперь, для MVP нашей дизайн-системы, давайте установим Elena. Мы будем использовать Elena для создания компонентов и в конечном итоге распространять их в виде пакета. Именно здесь некоторые вещи из корневого package.json начинают обретать смысл.
Начнём с создания папки компонентов и инициализации npm для нашего пакета компонентов:
Далее отредактируйте packages/components/package.json:
Затем установите Elena:
Это установит Elena, её бандлер и CLI-инструмент.
Мы будем использовать CLI-инструмент Elena для создания каркаса наших компонентов. По сути, он проведёт нас через создание компонента, позволяя запустить команду, которая генерирует нужную папку и создаёт js- и css-файлы для любого компонента, который мы захотим создать. Подробнее об этом позже!
Пока что нам нужно настроить Elena. Во многих случаях можно пропустить этот шаг и просто использовать настройки по умолчанию. Однако мы ведём себя как глупые гуси и совмещаем документацию и компоненты в одном проекте, так что, возможно, придётся кое-что подкрутить. Плюс я люблю, когда конфиги явные, а не невидимые.
Создайте конфиг Elena по пути packages/components/elena.config.mjs:
Это говорит Elena, где искать наши компоненты (src), куда выводить собранные компоненты (dist) и где искать точку входа нашей библиотеки (src/index.js). Обратите внимание: все эти пути относительны директории packages/components, а не корня проекта. Наши инструменты Elena и библиотека компонентов самодостаточны.
Эта точка входа важна, если мы хотим импортировать все наши веб-компоненты через bundle.js, который генерирует Elena. Пока что создадим пустую.
Создайте packages/components/src/index.js. Пока что он может быть просто пустым или содержать комментарий-заглушку:
По мере создания компонентов мы сможем добавлять соответствующие export в этот файл, чтобы они попадали в наш production-бандл.
Убедитесь, что Elena работает: перейдите в корень проекта и выполните:
К сведениюЕсли вы запускаете это на Mac с Apple Silicon, то, скорее всего, столкнётесь с ошибкой. По моему опыту, это из-за зависимости Elena (lightningcss), которая немного кривая (простите за такую техническую терминологию). Если при попытке сборки вы получаете ошибку 'MODULE_NOT_FOUND', выполните следующее из корня проекта: Code languagebashCopy to clipboard rm -rf node_modules packages/components/node_modules package-lock.json && npm install Это уничтожит папку node_modules и переустановит зависимости, разложив всё по своим местам. Если эта ошибка случилась однажды, то, скорее всего, придётся запускать это каждый раз при установке новой зависимости. Мне жаль. Управление пакетами — как всегда, Очень Приятное Занятие.
И выдохнем…
Отойдём на шаг назад, поставим чайник и посмотрим, что у нас есть. Ваша структура проекта должна выглядеть так:
Наша корневая папка по сути просто контейнер, так что особо беспокоиться о ней не стоит.
Наша папка docs — это место, где будет жить всё, связанное с VitePress. В конечном итоге это станет полноценной документацией дизайн-системы, и мы будем использовать её для предпросмотра и документирования наших компонентов по мере их создания.
Наша папка packages/components — это место, где мы будем работать со всем, связанным с компонентами. Папка packages/components/src — это место, где мы будем создавать компоненты, а index.js в ней — точка входа нашей библиотеки, где мы просто будем export'ировать любые компоненты, которые хотим включить в бандл.
Наша папка dist в packages/components — это место, где будут храниться собранная библиотека компонентов и манифест кастомных элементов. Затем мы сможем импортировать отсюда в наш проект VitePress так, будто это установленный пакет.
Однако чтобы дойти до этого, нам нужны какие-то реальные компоненты для распространения.
Создаём наш первый компонент
Теперь, когда всё настроено, мы наконец-то можем начать создавать компоненты! В этой статье мы будем держать всё просто и сосредоточимся на, возможно, самом распространённом компоненте: прекрасной кнопке.
По невероятному стечению обстоятельств ваш собственный веб-мастер Piccalilli, Andy Bell, уже написал великолепную статью о создании компонентов кнопок на стандартном HTML и CSS. Мы будем опираться на эти принципы здесь, с небольшими изменениями, чтобы получить максимум от нашей настройки веб-компонентов.
Статья Andy отлично объясняет почему стоят за многими семантическими и структурными решениями, касающимися самих кнопок, поэтому я не буду углубляться в это слишком сильно. Andy прошёл путь, чтобы мы могли бежать. Какой человек.
Композитные, примитивные и декларативные компоненты
В основе концепции Elena «прогрессивные веб-компоненты» лежит разделение компонентов на три основные категории: композитные, примитивные и декларативные. Документация Elena прекрасно объясняет различия подробно, но важно помнить, что все это всё ещё просто веб-компоненты. Нас не заставляют принимать нестандартные концепции или методы, скорее нас поощряют думать о наших компонентах в этих терминах.
Я позволю документации Elena сделать основную работу с этими определениями, но вот основные моменты:
- Композитные компоненты оборачивают и расширяют свой внутренний HTML. Они отлично подходят для таких вещей, как слайдеры, аккордеоны, карточки и многослойные макеты — там, где вы чаще всего позволяете HTML и CSS делать основную работу и расширяете возможности с помощью JS в нужной области. Композитные компоненты также отлично подходят для паттернов, где мы можем захотеть объединить примитивные компоненты и HTML в переиспользуемые «макро» компоненты с определённым поведением. Подумайте о таких вещах, как диалоги, fieldsets и баннеры уведомлений — там, где структура и поведение фиксированы, но содержимое внутри остаётся гибким и определяется потребителем.
- Примитивные компоненты объявляют и рендерят свой собственный HTML и поставляются с собственной функцией
render(). Это, вероятно, самые распространённые компоненты, которые мы будем использовать в дизайн-системе — подумайте о таких вещах, как кнопки, поля ввода, индикаторы загрузки, бейджи и т.д. - Декларативные компоненты — это комбинация обоих типов и могут объединять Light DOM и декларативный Shadow DOM. Если мы не знаем, что нам действительно нужна инкапсуляция с Shadow DOM, мы можем практически игнорировать его для библиотек компонентов. Я не углублялся слишком сильно в это, но мои первые мысли таковы, что декларативные компоненты были бы отличны для полностью инкапсулированных компонентов, таких как веб-редакторы контента или блоки кода с подсветкой синтаксиса/редакторы, где инкапсуляция и изоляция часто критичны.
На этот раз мы строим примитивный компонент. Наша кнопка будет объявлять и рендерить свой собственный HTML, и мы будем стилизовать её с помощью CSS в ограниченной области.
Создаём каркас компонента
Мы будем использовать CLI-помощник Elena для генерации папки и файлов, которые нам нужны для нашего компонента.
Пространства имён компонентов и пользовательские элементыМы используем сугубо учебный префикс my- для нашей кнопки, но зачем вообще нужен префикс? Это возвращает нас к тому, как пользовательские элементы требуют наличия - в имени тега, чтобы избежать конфликтов с нативными HTML-элементами. Если бы у нас был полный контроль, и мы создали компонент <button>, мы бы конфликтовали с настоящим HTML-элементом <button>, и у нас было бы Очень Плохое Время. Поэтому мы просто не можем этого делать — все наши пользовательские элементы должны быть в формате <{prefix}-{component}>.Жёсткого требования, чтобы наши файлы тоже были с дефисом, нет, но лично мне нравится аккуратность, когда имена файлов и папок совпадают с нашим фактическим элементом. Так что выберите префикс и придерживайтесь его. Для моей дизайн-системы Mindful Design я использую префикс md- — так что все мои пользовательские элементы выглядят примерно как <md-button>, <md-card> и т.д. Web Awesome использует wa-. Вы можете использовать всё, что пожелает ваше сердце. Главное — будьте последовательны.
Из папки packages/components выполните:
Затем вам будет предложено выбрать, какие функции и язык вы хотите. Для нашей кнопки нужно выбрать:
- Props
- CSS-переменные
- CSS-инкапсуляция
- Комментарии в коде
Нажмите Enter, затем выберите JavaScript в качестве языка.
Установите выходную директорию в src/. По умолчанию Elena использует src/components/, но нам не нужен такой уровень вложенности.
Это создаст каркас наших файлов с несколькими примерными значениями и комментариями, так что мы не будем смотреть на пустые файлы. Нажмите Enter после выбора функций, языка и директории, и Elena сгенерирует папку my-button с соответствующими JS и CSS файлами. О стилизации мы позаботимся позже, сейчас мы хотим спроектировать API нашего компонента.
Откройте следующий файл:
Здесь происходит много всего для простого boilerplate, но мы разберём каждый раздел по мере продвижения!
Добавление props
Компонентные props позволят нам управлять стилизацией и поведением компонента декларативным образом. Затем мы можем использовать эти props для создания вариантов наших компонентов. Для нашей кнопки давайте упростим и используем следующие props:
variant: стилевой вариант нашей кнопки, например «primary», «danger»disabled: отключена ли кнопка или нетhref: куда должна вести кнопка, также определяет, будет ли кнопка рендериться как ссылка или как кнопка
Это небольшое подмножество props, которые потребуются кнопке в продакшене, но этого достаточно, чтобы двигаться дальше. Если после этого вы почувствуете себя уверенно, можете вернуться и добавить больше props — size или icon prop были бы отличной отправной точкой!
Давайте добавим эти props в наш компонент кнопки:
Объявление static props позволяет нам определить конечный массив props, которые будет принимать наш компонент. По умолчанию все эти props будут отражаться на нашем отрендеренном компоненте как HTML-атрибуты. Вам почти всегда нужно, чтобы это было так, особенно если вы используете нестандартные атрибуты вроде disabled, download и т.д.
Прямо под этим массивом вы найдёте заготовленные значения props по умолчанию, каждое с небольшим комментарием сверху. Давайте последуем примеру Elena и установим значения по умолчанию для добавленных нами props:
Комментарии над каждым определением — это JSDoc-комментарии. Они могут выглядеть немного непривычно, но позволяют документировать наши компоненты и props и могут служить источником истины для документирования API наших компонентов. Это также даёт нам немного «мягкой типизации» без необходимости использовать TypeScript. Большинство IDE будут подсвечивать или предупреждать вас, если вы установите prop в значение/тип, не указанный в синтаксисе JSDoc.
В приведённом выше примере мы определяем наш prop variant, задаём ему значение по умолчанию «default» и мягко типизируем его с помощью определения @type. В данном случае мы принимаем только одно из четырёх перечисленных значений.
На этом этапе это может показаться немного бессмысленным, но следите за своими JSDoc-комментариями по мере создания компонентов. Мы будем использовать их позже. Пока мы на этом, мы могли бы также задать более точное описание для нашего компонента.
Измените верхний комментарий в следующем файле:
Мы также удалили определения @cssprop из этого комментария. Они были сгенерированы, потому что мы выбрали «CSS Variables» при создании нашего компонента, и позволяют нам раскрыть кастомные свойства, используемые для стилизации наших компонентов. Если вы работаете над темизируемой или headless библиотекой компонентов, вы, возможно, захотите оставить их, в противном случае я предпочитаю пропускать определение этих свойств и не раскрывать их в своей документации.
Давайте взглянем на нашу функцию render():
Если у вас нет тяжёлого случая React Brain, вы, возможно, заметите хотя бы одну из пары проблем: во-первых, button — это не div. Дико, правда? Это не вина Elena, мы просто создали базовый компонент, и div — это, безусловно, самый распространённый HTML-элемент. Нам нужно самим отрендерить правильную, семантическую, доступную разметку.
Во-вторых, мы только что добавили href как prop, а это атрибут a, а не button. Нам нужно условно рендерить либо a, либо button в зависимости от того, установлен ли href.
Условный рендеринг
Дискуссия «должны ли ссылки когда-либо стилизоваться как кнопки?» старше, чем бородка вашего отчима, и точно так же как-то ещё сохраняется сквозь века. Я слишком стар и устал, чтобы беспокоиться об этом, а реальность такова, что кнопки-ссылки CTA — одна из самых распространённых вещей, которые вы увидите на сайте, в конкуренции только с баннерами cookie и плохой доступностью в своей повсеместности.
Так что вы будете делать это, нравится нам это или нет, и вам лучше делать это правильно.
Самый чистый подход к этому — абстрагировать наш рендеринг, добавив две новые функции:
Затем мы можем заменить функцию render() нашего компонента:
Супер просто: если href присутствует, это ссылка, если нет — это кнопка. Нам не нужно добавлять новые props, просто используем тот, что у нас уже есть.
Мы используем nothing в этой функции, и если вы попробуете собрать/запустить watch прямо сейчас, вы получите ошибку. Это потому, что nothing — это помощник Elena для безопасного рендеринга, ну, ничего.
Давайте импортируем его в начало нашей кнопки. Отредактируйте первую строку следующего файла:
Теперь давайте соберём нашу библиотеку компонентов, чтобы включить нашу новую кнопку в продакшенный bundle.js. Отредактируйте packages/components/src/index.js:
Затем из корня проекта выполните:
Если повезёт, сборка пройдёт без проблем, и мы наконец-то сможем встроить нашу кнопку в документацию.
Предпросмотр нашей кнопки
На данный момент у нас есть всё необходимое, чтобы отрендерить нашу кнопку и увидеть её на странице. Потребовалось немного настроек, чтобы дойти до этого, но мы сделали это!
Благодаря тому, как наш проект настроен, мы теперь можем подключать наши собранные компоненты так, как будто они являются отдельным пакетом. Нам просто нужно настроить VitePress для импорта бандла, который генерирует Elena, и сказать ему обрабатывать наши импортированные компоненты как веб-компоненты (по умолчанию VitePress ожидает Vue-компоненты).
Создайте docs/.vitepress/theme/index.js:
Это говорит нашей теме VitePress импортировать файлы бандла, которые сгенерировала Elena. @my-ds/components загружается асинхронно, так как это клиентская часть, а VitePress по умолчанию использует серверный рендеринг. @my-ds/components/dist/bundle.css импортируется напрямую в начале нашего файла, потому что это просто старый добрый CSS.
Примечание о серверном рендеринге (SSR)Если вы знаете, что вам нужен SSR, есть несколько способов включить его с Elena. В зависимости от вашего фреймворка/генератора сайта на выбор, вам, возможно, потребуется выполнить несколько дополнительных шагов конфигурации. Обратитесь к документации Elena за советами по SSR и на страницу интеграций с фреймворками для более продвинутых интеграций.
Далее обновите docs/.vitepress/config.mjs:
Это немного хакерский способ, но по сути он говорит VitePress рассматривать любой тег с - как пользовательский элемент вместо Vue-компонента. Учитывая, что пользовательские элементы требуют -, чтобы избежать конфликтов с нативными HTML-элементами, этого достаточно для наших целей.
Теперь давайте соберём нашу фактическую документацию по кнопке. Создайте docs/components/button.md:
Это должно дать нам всё необходимое для предпросмотра нашей кнопки! Запустите процесс разработки, если вы ещё этого не сделали:
Это запустит документацию VitePress в режиме разработки и одновременно запустит скрипт watch Elena, давая нам довольно удобный опыт live-reload. Перейдите на http://localhost:5173/components/button.html, и вы должны увидеть свою прекрасную кнопку!
Держите сервер разработки запущеннымКоманда npm run dev запустит ваш dev-сервер документации и будет держать Elena в фоне, отслеживая изменения компонентов. Вы будете получать live reloads всякий раз, когда вносите изменения, и теперь вы можете плавно вносить и тестировать изменения компонентов и документации.
Если вы откроете инспектор браузера и посмотрите на отрендеренную кнопку, вы увидите что-то вроде этого:
Наш хост-элемент <my-button> оборачивает разметку из своей функции render(), и мы имеем наш первый веб-компонент, отрендеренный в браузере! Я знаю, я знаю. Это выглядит совсем не круто, но оно там есть! Давайте придадим ему стиль.
Стилизация нашей кнопки
Если вы дошли до этого места, то, возможно, заметили явное отсутствие дискуссий о дизайн-токенах. Не потому, что они неважны, а потому что управление токенами и их распространение — это крайне объёмная тема, а эта статья и без того получается очень уж длинной. Скорее всего, мы разберём рабочие процессы с токенами в отдельном посте!
Пока что мы будем придерживаться простого подхода и использовать стили с ограниченной областью видимости Elena. Это позволяет нам частично нейтрализовать каскад в CSS и гарантировать, что стили не выйдут за пределы оформления отдельного компонента.
Мы также сделаем кое-что, чего я не рекомендую для production-компонентов, особенно если вы хотите строить темизируемые дизайн-системы, наследующие разумные глобальные стили (спойлер: вы хотите), — а именно, сбросим стили компонента перед применением собственных. В итоге мы получаем компонент, который не пропускает стили наружу (благодаря ограниченной области видимости) и не позволяет глобальным стилям проникать внутрь (благодаря сбросу на уровне компонента).
Стили с ограниченной областью видимости
Откройте следующий файл:
По умолчанию Elena генерирует at-правило @scope для любого создаваемого компонента. Вы не обязаны использовать стили с ограниченной областью видимости. Важно помнить: это всё обычный CSS. Мы не делаем ничего дикого или хрупкого вроде CSS-in-JS, мы просто используем конкретную стандартную возможность CSS. Вы с таким же успехом можете писать неограниченный CSS с пространствами имён и получить в целом те же результаты.
Более того, если вам нужно поддерживать браузеры, которые не поддерживают @scope, то использование пространств имён может быть тем самым подходом, который вам нужен. Для этого примера (и для моей собственной production-работы) меня вполне устраивает @scope — я считаю его гораздо более чистым способом стилизации компонентов.
Поскольку при создании компонента мы выбрали «CSS Encapsulation», вы видите, что Elena сгенерировала следующее:
По сути это означает, что наш компонент не будет наследовать стили из более высоких уровней каскада. В зависимости от вашего подхода к стилизации в дизайн-системе, это может быть как тем, что нужно, так и нет. Для этого конкретного сценария это самый простой способ гарантировать полный контроль над каждым компонентом. Однако во многих реальных сценариях вам действительно стоит хотеть некоторой степени наследования или стилей по умолчанию в компонентах.
Примечание об инкапсулированном сбросеИспользование all: unset и display: revert сбросит любые стили, определённые до тех пор, пока эти правила не встретятся. Это не предотвратит применение дальнейших неограниченных стилей в файлах или тегах