Компонентно ориентираната разработка обръща традиционния подход отгоре надолу при изграждането на потребителски интерфейси. Вместо да изграждате страници и после да извличате части за многократна употреба, започвате с отделни компоненти и ги сглобявате в изгледи.
Ползата не е само повторната употреба. Когато даден компонент се изгражда и изпробва самостоятелно, граничните му случаи излизат наяве рано, неговият API трябва да бъде ясно дефиниран, а страницата, която в крайна сметка го използва, е най-вече сглобяване. Цената е малко повече предварително мислене за границите, което тази статия се опитва да улесни.
Йерархията на компонентите
Организирайте компонентите в ясна йерархия: атоми (бутони, полета за въвеждане, етикети), молекули (ленти за търсене, групи от полета във форма), организми (навигация, hero секции), шаблони (оформления на страници) и страници (конкретни екземпляри). Тази методология на атомния дизайн, популяризирана от Брад Фрост, дава общ речник за обсъждане на архитектурата на интерфейса.
Приемайте йерархията като речник, а не като структура от папки, на която трябва да се подчинявате. Екипите често затъват в спорове дали една карта е молекула или организъм. По-полезно разделение за кода е:
- Примитиви, които не знаят нищо за вашата предметна област:
Button,Input,Badge,Dialog. - Композиции, които подреждат примитиви в разпознаваем модел:
SearchField,StatCard,EmptyState. - Функционалности, които свързват композициите с реални данни и поведение:
InvoiceTable,CheckoutSummary.
Правилото, което поддържа тази структура здрава, е зависимостите да сочат само надолу. Примитивът никога не импортира функционалност, а композицията никога не извлича данни.
Разработка в изолация
Изграждайте и тествайте компонентите извън контекста на цяла страница. Инструменти като Storybook или Container API на Astro за самостоятелно рендериране на компонент ви позволяват да рендерирате компоненти с различни props, състояния и варианти на данните. Така граничните случаи се улавят рано, а компонентите стават по-устойчиви.
Изолацията ви принуждава да отговорите на въпроси, които цялата страница ви позволява да пренебрегнете. Как изглежда таблицата с нула реда? С един ред, чието име е дълго 80 знака? Докато се зарежда? След като заявката се провали? Запишете всеки от тези случаи като именувано състояние и го рендерирайте:
export const states = {
loading: { status: 'loading' },
empty: { status: 'ready', rows: [] },
error: { status: 'error', message: 'Could not load invoices.' },
long: { status: 'ready', rows: [longNameRow, ...sampleRows] },
};
Разработката в изолация прави практични и визуалните регресионни тестове. Правете моментни снимки на състоянията на компонентите си и автоматично откривайте нежелани визуални промени между изданията. Поддържайте снимките фокусирани — по едно състояние на компонент на изображение, — така че неуспешното сравнение да сочи директно причината, а не цяла страница.
Props и поток на данните
Проектирайте ясни API на компонентите с добре дефинирани props. Използвайте TypeScript интерфейси, за да документирате очакваните входни данни, и задавайте разумни стойности по подразбиране. За повечето компоненти предпочитайте изричното подаване на props пред контекст или глобално състояние — така потокът на данните може да се проследи, а компонентите се тестват по-лесно.
Няколко навика водят до API, които остаряват добре:
- Моделирайте състояния, а не булеви стойности.
isLoading,isErrorиisEmptyкато отделни флагове допускат невъзможни комбинации. Едно обединениеstatus— не. - Именувайте вариантите според намерението.
variant="danger"оцелява след ребрандиране;color="red"— не. - Приемайте children или слотове за съдържание. Компонент, който приема props
title,subtitle,iconиfooterText, ще продължава да расте; компонент, който приема сглобено съдържание, няма. - Предавайте нататък естествените атрибути.
Buttonтрябва да приемаtype,disabledиaria-*, без всеки от тях да се декларира наново.
type Status = 'loading' | 'empty' | 'error' | 'ready';
interface StatCardProps {
/** Кратък етикет, показан над стойността. */
label: string;
/** Форматирана стойност; пренебрегва се, освен ако status не е 'ready'. */
value?: string;
status: Status;
}
Контекстът и глобалните хранилища все още имат своето място: тема, локал, текущият потребител. Те наистина са част от средата. Повечето други данни трябва да пристигат чрез props, защото компонент, чиито входни данни са видими в сигнатурата му, може да се рендерира навсякъде, включително в тест.
Тестване на компоненти
Модулните тестове проверяват дали компонентите се рендерират правилно с дадени props и дали обработват взаимодействията на потребителя според очакванията. Интеграционните тестове потвърждават, че сглобените компоненти работят правилно заедно. Следвайте водещия принцип на Testing Library: тествайте поведение, а не детайли на имплементацията.
На практика това означава да търсите елементи по това, което потребителят възприема — роля, достъпно име, видим текст, — а не по имена на класове или вътрешно състояние:
test('shows the error message and a retry button', async () => {
render(InvoiceTable, { status: 'error', message: 'Could not load invoices.' });
expect(screen.getByText('Could not load invoices.')).toBeVisible();
expect(screen.getByRole('button', { name: 'Retry' })).toBeEnabled();
});
Тест, написан по този начин, оцелява при преработка на маркирането и същевременно служи като лека проверка на достъпността: ако не можете да намерите бутона по ролята и името му, не може и екранният четец. Чистата логика, като функцията, която съпоставя вариант с имена на класове, се тества най-лесно директно като обикновена функция, без никакво рендериране.
Документиране в хода на изграждането
Изолацията ви дава документация почти безплатно, стига да я запишете, докато решенията са още пресни. За всеки компонент запишете:
- За какво служи и, също толкова важно, за какво не служи.
- Всеки prop с неговия тип, стойност по подразбиране и описание на един ред.
- Състоянията, които поддържа, и как изглежда всяко от тях.
- Поведението при работа с клавиатура и всички ARIA атрибути, които управлява вместо потребителя на компонента.
Поставяйте описанията в самите дефиниции на типовете, както в примера StatCardProps по-горе, за да ги показват редакторите при посочване с мишката и да не могат да се разминат с кода.
Къде нещата се объркват
Компонентно ориентираната разработка има слабости, които си струва да назовем:
- Преждевременна абстракция. Извличайте общ компонент, когато имате две или три реални употреби, а не една въображаема бъдеща.
- Експлозия на props. Когато един компонент има повече булеви props, отколкото редове маркиране, разделете го на по-малки части.
- Изолация без интеграция. Всеки компонент може да изглежда перфектно в собствената си история, докато страницата все пак се чупи. Поддържайте и малък брой тестове на ниво страница.
Заключение
Компонентно ориентираната разработка създава интерфейси, които са по-последователни, по-лесни за поддръжка и по-бързи за изграждане. Започнете с най-малките градивни елементи, дайте на всеки ясно дефиниран API и пълен набор от състояния и сглобявайте нагоре.