Протягом останнього десятиліття архітектура наших PHP-застосунків на Laravel залишалася стабільною. Водночас frontend-стек постійно змінювався: від jQuery до React і Vue, а потім назад до React у поєднанні з Inertia та TypeScript.
Зараз, коли ми маємо великі та довготривалі проекти на Inertia у продакшені, ми чітко розуміємо, що працює, а що — ні. Настав час формалізувати наші рішення, оскільки цього року ми запускаємо нові проекти та наймаємо розробників.
Структура frontend-коду залежить від типу застосунку. Це може бути як величезний проект на Inertia з сотнями сторінок та спільними компонентами, так і складний Single Page App на кшталт клону Figma. У цьому дописі ми розберемо наш базовий підхід до структурування, який може адаптуватися під потреби проекту.
Для початку поглянемо на типову структуру Inertia React застосунку:
resources
├── css
│ └── app.css
└── js
├── common
│ ├── button
│ │ └── Button.tsx
│ └── card
│ ├── Card.tsx
│ ├── CardHeader.tsx
│ └── CardContent.tsx
├── modules
│ ├── auth
│ │ ├── Avatar.ts
│ │ └── useCurrentUser.ts
│ └── categories
│ └── CategoryBadge.tsx
├── pages
│ ├── layouts
│ │ └── Layout.tsx
│ ├── profile
│ │ ├── layouts
│ │ │ └── ProfileLayout.tsx
│ │ └── ProfilePage.tsx
│ ├── posts
│ │ ├── components
│ │ │ └── PublishStatus.tsx
│ │ ├── helpers
│ │ │ └── generateSlug.ts
│ │ ├── CreatePostPage.tsx
│ │ ├── EditPostPage.tsx
│ │ └── PostsIndexPage.tsx
│ └── DashboardPage.tsx
├── shadcn
│ ├── dialog.tsx
│ ├── input.tsx
│ └── select.tsx
└── app.tsx
Розглянемо деталі.
Структура JavaScript-застосунку
Ми використовуємо чотири основні директорії в resources/js:
common: Універсальні модулі та компоненти. Теоретично їх можна переносити між проектами, хоча стилі зазвичай залишаються специфічними для кожного сайту.modules: Специфічний для проекту код. Це компоненти, що використовуються на багатьох сторінках, або логіка, яку варто винести за межі папкиpages.pages: Компоненти сторінок для Inertia. Вони можуть містити власний локальний код або допоміжні компоненти.shadcn: Автоматично згенеровані компоненти з shadcn/ui.
Common та modules
Обидві директорії мають однорівневу структуру папок для визначення контексту. Якщо вміст папки розростається, ми рекомендуємо групувати файли за типом.
Простий модуль у common, наприклад button або card, може містити лише кілька компонентів:
resources
└── js
└── common
├── button
│ └── Button.tsx
└── card
├── CardContents.tsx
├── CardHeader.tsx
└── Card.tsx
Складніший модуль, наприклад agenda, варто структурувати за типами файлів:
resources
└── js
└── modules
└── agenda
├── components
│ ├── Agenda.tsx
│ ├── ListView.tsx
│ └── GridView.tsx
├── contexts
│ └── AgendaContext.tsx
├── helpers
│ └── parseDate.ts
├── hooks
│ └── useAgenda.ts
└── types.ts
Піддиректорії за типами
Для організації модуля зазвичай використовуються такі папки:
components
contexts
constants
helpers
hooks
stores
Типи ми зазвичай збираємо в одному файлі types.ts, але якщо він стає занадто великим, створюємо окрему папку types. Глобальні утиліти (наприклад, helpers або hooks) можуть розміщуватися безпосередньо в корені common.
resources
└── js
└── common
├── helpers
│ └── parseDateFromServer.ts
└── hooks
└── useIntersectionObserver.ts
Правила найменування (Casing)
Ми дотримуємося чітких стандартів:
- Файли, що експортують компоненти або React-контексти —
PascalCase. - Усі інші файли —
camelCase. - Директорії —
kebab-case(як назви npm-модулів).
Common проти modules
Якщо ви не впевнені, куди покласти код, запитайте себе: «Чи стосується це конкретної бізнес-логіки або фічі застосунку?». Якщо так — це modules.
Pages
Тут живуть компоненти сторінок, які викликає Inertia. Структура цієї папки зазвичай повторює структуру URL. Назви файлів мають суфікс Page.
Також ми маємо папку layouts для глобальних макетів. Якщо сторінку потрібно розбити на частини або додати специфічні helpers чи hooks, ми використовуємо ту саму структуру папок за типами, щоб у корені pages залишалися лише чисті компоненти сторінок.
resources
└── js
└── pages
├── layouts
│ └── Layout.tsx
├── profile
│ ├── layouts
│ │ └── ProfileLayout.tsx
│ └── ProfilePage.tsx
├── posts
│ ├── components
│ │ └── PublishStatus.tsx
│ ├── helpers
│ │ └── generateSlug.ts
│ ├── CreatePostPage.tsx
│ ├── EditPostPage.tsx
│ └── PostsIndexPage.tsx
└── DashboardPage.tsx
Shadcn
Ми використовуємо shadcn/ui для швидкого старту проекту, щоб не витрачати тижні на створення власної бібліотеки компонентів. З часом ми переносимо та адаптуємо ці компоненти до папки common.
Ми уникаємо значних змін у самих компонентах shadcn, оскільки API, яке вони використовують разом із Radix, часто є занадто низькорівневим для повсякденної розробки.
export function FruitSelect() {
return (
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="blueberry">Blueberry</SelectItem>
<SelectItem value="grapes">Grapes</SelectItem>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
)
}
Такий підхід зручний для створення UI-бібліотеки, але у прикладному коді ми часто створюємо простіші абстракції:
export function FruitSelect() {
return (
<Select
placeholder="Select a fruit"
options={[
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'blueberry', label: 'Blueberry' },
{ value: 'grapes', label: 'Grapes' },
{ value: 'pineapple', label: 'Pineapple' },
]}
/>
)
}
Стилі
Для стилізації ми обираємо Tailwind CSS. Завдяки його компонентному підходу, для більшості проектів достатньо одного файлу app.css.
resources
└── css
└── app.css
Для складніших проектів ми розділяємо стилі за папками base, components та utilities.
resources
└── css
├── base
│ ├── a.css
│ └── h1.css
├── components
│ ├── button.css
│ └── card.css
├── utilities
│ └── typography.css
└── app.css
Багатозонні застосунки
Якщо застосунок має чітко розділені частини (наприклад, адмін-панель та клієнтську частину), ми вводимо папку apps. Кожна «зона» може мати власні modules та pages, а спільний дизайн-системний код залишається у common у корені resources/js.
resources
├── css
│ ├── admin
│ │ └── app.css
│ └── client
│ └── app.css
└── js
├── apps
│ ├── admin
│ │ ├── modules
│ │ ├── pages
│ │ └── app.tsx
│ └── client
│ ├── modules
│ ├── pages
│ └── app.tsx
├── common
└── modules
Нотатки щодо React-компонентів
Ось як виглядає типовий компонент у наших проектах:
import { PropsWithChildren } from 'react';
import { cn } from '@/common/helpers/cn';
import { PropsWithClassName } from '@/common/types/props';
type Props = PropsWithClassName<PropsWithChildren<{
onClick?: () => void;
type?: 'button' | 'submit';
}>>;
export function Button({ onClick, type, className, children }: Props) {
return (
<button type={type} onClick={onClick} className={cn(className)}>
{children}
</button>
);
}
- Ми віддаємо перевагу оголошенню компонентів через
function, а не черезconst, щоб візуально відрізняти їх від змінних. Анонімні функції (колбеки) пишемо як стрілкові. - Використовуємо лише іменовані експорти для консистентності назв. Один файл — один компонент. Виняток —
export defaultдля сторінок Inertia, оскільки вона завантажує їх за назвою файлу. - Ми не використовуємо
barrel files(index.ts). Хоча це здається зручним, на практиці це лише ускладнює навігацію та підтримку коду. - Імпорти групуються за допомогою
prettier-plugin-sort-imports: спочатку бібліотеки, потім внутрішні модулі через абсолютні шляхи з аліасами. - Props сортуємо за алфавітом, залишаючи
classNameтаchildrenнаприкінці.
Оптимізація для backend та fullstack-розробки
У нашій компанії на одного frontend-розробника припадає чотири backend-фахівці. Таке співвідношення навчило нас будувати процеси так, щоб backend-розробники могли створювати інтерфейси самостійно.
Саме тому поділ на common/modules та pages є надзвичайно ефективним. Колеги з backend використовують готові абстракції в pages, не заглиблюючись у тонкощі React. Якщо ж фіча потребує складної логіки чи управління станом, підключається фахівець із frontend. Такий підхід дозволяє backend-розробникам закривати до 75% завдань зі створення форм та таблиць, використовуючи готові патерни.