Олег Марков
Архитектура Feature-Sliced Design в Svelte с помощью svelte-fsd
Введение
Feature-Sliced Design (FSD) за последние годы стал одним из самых обсуждаемых подходов к архитектуре фронтенда. Он помогает держать проект управляемым по мере роста, уменьшает связанность модулей и упрощает внедрение новых фич.
Но если вы работаете со Svelte или SvelteKit, то быстро замечаете, что большинство материалов по FSD ориентированы на React. Отсюда типичная ситуация: есть желание применить понятный архитектурный подход, но нет ясности, как именно «переложить» его на мир Svelte.
Здесь на сцену выходит svelte-fsd — набор принципов и соглашений, адаптирующий Feature-Sliced Design под Svelte и SvelteKit. В этой статье вы увидите, как шаг за шагом:
- организовать структуру проекта в духе FSD;
- применить слои и срезы в Svelte;
- правильно разнести сторы, компоненты и бизнес-логику;
- подключить svelte-fsd к существующему приложению;
- выстроить удобные импорты и архитектурные границы.
Смотрите, я покажу вам, как это работает на практических примерах, чтобы вы могли сразу попробовать подход в своем проекте.
Что такое Feature-Sliced Design в контексте Svelte
Основные принципы FSD
Сначала давайте коротко напомним, что такое Feature-Sliced Design, но через призму того, как вы будете применять его в Svelte.
Идея подхода:
- Делить код не по типам файлов (components, store, utils), а по доменным смыслам — фичам и бизнес-областям.
- Выстраивать уровни (слои) приложения:
- entities — базовые сущности предметной области;
- features — функциональные «фишки» для пользователя;
- widgets — составные блоки интерфейса;
- pages — страницы приложения;
- processes — сквозные сценарии (опционально);
- app — корневая инициализация и инфраструктура;
- shared — общие модули, которыми можно делиться с любым слоем.
- Ограничивать зависимости между слоями — «верхние» слои могут использовать нижние, но не наоборот.
В React-мире обычно всё завязано на компонентный подход и хуки. В Svelte у нас есть:
- компоненты .svelte;
- сценарии (script) с логикой;
- сторы;
- контексты;
- модульные скрипты (в SvelteKit);
- файлы маршрутов.
Наша задача — разложить всё это по слоям так, чтобы структура проекта была предсказуемой, а изменения в одном месте не ломали весь проект.
Что именно добавляет svelte-fsd
svelte-fsd — это не обязательно библиотека в npm (часто это «шаблон + соглашения»), а скорее способ структурировать Svelte-проект по FSD:
- рекомендуемая структура папок;
- соглашения по именованию;
- адаптация под SvelteKit routing;
- подходы к стором и контекстам в пределах слоёв;
- понятные точки входа (public API) для фич, сущностей и т.д.
Ценность здесь не в каком-то конкретном пакете, а в наборе правил, которые вы можете реализовать руками или взять из готового шаблона.
Базовая структура проекта Svelte с Feature-Sliced Design
Давайте разберёмся, как выглядит типичный SvelteKit-проект, организованный по FSD.
Представим структуру директории src:
- app
- processes
- pages
- widgets
- features
- entities
- shared
Это те же слои, которые используются в классическом FSD, адаптированные под Svelte.
Пример структуры SvelteKit-проекта
Здесь я размещаю пример, чтобы вам было проще понять общую картину:
src/
app/
index.ts // Инициализация приложения, провайдеры, глобальные сторы
providers/
theme/
ThemeProvider.svelte
api/
ApiProvider.svelte
layout/
AppLayout.svelte // Базовый layout приложения
processes/
checkout/
model/
store.ts // Сторы и логика процесса оформления заказа
ui/
CheckoutFlow.svelte
pages/
home/
ui/
HomePage.svelte
profile/
ui/
ProfilePage.svelte
model/
loader.ts // Загрузка данных страницы
widgets/
header/
ui/
Header.svelte
model/
userMenu.ts
sidebar/
ui/
Sidebar.svelte
features/
auth/
login/
ui/
LoginForm.svelte
model/
loginStore.ts
login.api.ts
cart/
add-to-cart/
ui/
AddToCartButton.svelte
model/
addToCart.ts
entities/
user/
model/
userStore.ts
user.types.ts
ui/
UserAvatar.svelte
UserName.svelte
product/
model/
product.types.ts
ui/
ProductCard.svelte
shared/
ui/
Button.svelte
Input.svelte
Modal.svelte
api/
httpClient.ts
config/
env.ts
lib/
formatPrice.ts
createFormStore.ts
styles/
global.css
variables.css
Эта структура — пример того, как svelte-fsd предлагает разложить код по слоям. Вы можете адаптировать её под свои нужды, но главное — сохранить принцип: доменные сущности и фичи в центре, технические детали — в shared/app.
Слои FSD в Svelte-проектах
Теперь давайте пройдёмся по каждому слою и разберём, что в нём обычно живёт в Svelte-проекте.
Слой app
Слой app отвечает за «оболочку» всего приложения: инициализацию, глобальные провайдеры, общие layout’ы.
В SvelteKit часть этих задач берут на себя:
- src/routes/+layout.svelte;
- src/routes/+layout.ts / +layout.server.ts;
- src/hooks.server.ts.
В подходе svelte-fsd удобно выносить всё это в слой app как отдельные модули, а в роутинге только их подключать.
Пример провайдера темы
Покажу вам, как это реализовано на практике.
Файл src/app/providers/theme/ThemeProvider.svelte:
<script lang="ts">
import { setContext } from 'svelte';
import { writable, type Writable } from 'svelte/store';
// Тип возможных тем
type Theme = 'light' | 'dark';
// Создаем стор для текущей темы
const themeStore: Writable<Theme> = writable('light');
// Устанавливаем контекст, чтобы дочерние компоненты могли получить доступ
setContext('theme', themeStore);
</script>
<!-- Оборачиваем слот, чтобы контекст был доступен во всем приложении -->
<slot />
Файл src/app/layout/AppLayout.svelte:
<script lang="ts">
import ThemeProvider from '../providers/theme/ThemeProvider.svelte';
</script>
<!-- Здесь мы оборачиваем приложение в ThemeProvider -->
<ThemeProvider>
<slot />
</ThemeProvider>
Дальше в SvelteKit layout можно использовать AppLayout как основу.
Слой shared
Слой shared хранит то, что может использоваться в любом другом слое:
- базовые UI-компоненты (Button, Input, Modal);
- общие утилиты;
- общие типы;
- http-клиент;
- конфигурации.
Главное правило: shared ничего не знает о доменной логике. Никаких зависимостей от features, entities и т.п.
Пример базового компонента Button
Теперь вы увидите, как это выглядит в коде.
Файл src/shared/ui/Button.svelte:
<script lang="ts">
// Пропсы кнопки
export let variant: 'primary' | 'secondary' = 'primary';
export let disabled: boolean = false;
export let type: 'button' | 'submit' = 'button';
// Остальные атрибуты прокидываем через ...$$restProps
export let ariaLabel: string | undefined;
</script>
<button
class={`btn btn-${variant}`}
{type}
{disabled}
aria-label={ariaLabel}
>
<!-- Слот для содержимого кнопки -->
<slot />
</button>
<style>
/* Здесь мы задаем базовые стили, не завязанные на доменную логику */
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
}
.btn-primary {
background: #2d6cdf;
color: white;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
</style>
Этот компонент можно безопасно использовать в любом слое — от entities до pages.
Entities — сущности в Svelte-проектах
Слой entities — это модель предметной области. Здесь вы описываете пользователей, товары, заказы и т.д.
Что обычно живёт в entities:
- типы и интерфейсы;
- сторы сущностей;
- базовые UI-компоненты, отображающие сущность (аватар, карточка товара);
- иногда — простые функции работы с сущностями.
Пример: сущность User
Давайте разберёмся на примере сущности пользователя.
Файл src/entities/user/model/user.types.ts:
// Здесь мы задаем тип пользователя, который будем использовать во всем проекте
export interface User {
id: string;
name: string;
avatarUrl?: string;
email: string;
}
Файл src/entities/user/model/userStore.ts:
import { writable, type Writable } from 'svelte/store';
import type { User } from './user.types';
// Стор для текущего авторизованного пользователя
const currentUser: Writable<User | null> = writable(null);
// Экспортируем стор и вспомогательные функции
export const userModel = {
currentUser,
// Устанавливаем текущего пользователя
setUser(user: User | null) {
currentUser.set(user);
},
// Очищаем данные пользователя при логауте
logout() {
currentUser.set(null);
}
};
Файл src/entities/user/ui/UserAvatar.svelte:
<script lang="ts">
import type { User } from '../model/user.types';
// Пользователь, для которого рисуем аватар
export let user: User;
export let size: number = 32;
</script>
<!-- Если есть ссылка на аватар - показываем ее, иначе - первую букву имени -->
<div
class="avatar"
style={`width: ${size}px; height: ${size}px;`}
>
{#if user.avatarUrl}
<img src={user.avatarUrl} alt={user.name} />
{:else}
<span>{user.name.charAt(0).toUpperCase()}</span>
{/if}
</div>
<style>
.avatar {
border-radius: 50%;
overflow: hidden;
background: #ccc;
display: inline-flex;
align-items: center;
justify-content: center;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
Здесь сущность user самодостаточна: у неё есть модель и базовые компоненты. Любая feature или widget, которым нужен пользователь, берут всё отсюда, а не из “глобальной папки components”.
Features — фичи как единицы функциональности
Слой features — один из ключевых в FSD. Фича — это не просто компонент, а законченный пользовательский сценарий или его часть.
Примеры фич:
- логин;
- добавление товара в корзину;
- фильтрация списка;
- выбор языка.
Каждая фича может иметь свой UI и модель.
Пример фичи LoginForm
Давайте посмотрим, что происходит в следующем примере — фича авторизации.
Структура:
features/
auth/
login/
ui/
LoginForm.svelte
model/
loginStore.ts
login.api.ts
Файл src/features/auth/login/model/login.api.ts:
// Здесь мы эмулируем API-запрос на сервер для логина
export async function loginRequest(email: string, password: string) {
// В реальном приложении вы сделаете HTTP-запрос
// Здесь обратите внимание - мы используем await для асинхронной операции
await new Promise((resolve) => setTimeout(resolve, 500));
if (email === 'test@example.com' && password === '123456') {
return {
id: '1',
name: 'Test User',
email
};
}
throw new Error('Неверный email или пароль');
}
Файл src/features/auth/login/model/loginStore.ts:
import { writable } from 'svelte/store';
import { loginRequest } from './login.api';
import { userModel } from '$entities/user/model/userStore';
// Тип стейта фичи логина
interface LoginState {
loading: boolean;
error: string | null;
}
// Начальное состояние
const initialState: LoginState = {
loading: false,
error: null
};
// Создаем стор состояния логина
const state = writable<LoginState>(initialState);
// Экспортируем модель фичи
export const loginModel = {
state,
async login(email: string, password: string) {
// Ставим флаг загрузки и очищаем ошибку
state.set({ loading: true, error: null });
try {
const user = await loginRequest(email, password);
// Если логин успешен - сохраняем пользователя в модели сущности
userModel.setUser(user);
// Сбрасываем состояние логина
state.set({ loading: false, error: null });
} catch (e) {
// При ошибке - показываем сообщение
state.set({
loading: false,
error: e instanceof Error ? e.message : 'Ошибка входа'
});
}
}
};
Здесь обратите внимание, как слой feature зависит от слоя entities:
- loginModel вызывает userModel.setUser;
- но сущность user ничего не знает о фиче логина.
Файл src/features/auth/login/ui/LoginForm.svelte:
<script lang="ts">
import { get } from 'svelte/store';
import { loginModel } from '../model/loginStore';
import Button from '$shared/ui/Button.svelte';
let email = '';
let password = '';
// Подписываемся на стор состояния логина
let state = get(loginModel.state);
// Обновляем локальную переменную при изменении стора
const unsubscribe = loginModel.state.subscribe((value) => {
state = value;
});
// Обрабатываем отправку формы
async function handleSubmit() {
// Вызываем метод модели фичи
await loginModel.login(email, password);
}
// Очищаем подписку при уничтожении компонента
// В Svelte можно вернуть функцию из onDestroy,
// но здесь для простоты мы опускаем этот момент
</script>
<form on:submit|preventDefault={handleSubmit}>
<label>
Email
<input
type="email"
bind:value={email}
required
/>
</label>
<label>
Пароль
<input
type="password"
bind:value={password}
required
/>
</label>
{#if state.error}
<p class="error">{state.error}</p>
{/if}
<Button type="submit" disabled={state.loading}>
{#if state.loading}
Входим...
{:else}
Войти
{/if}
</Button>
</form>
<style>
.error {
color: red;
}
</style>
Фича получилась изолированной: она сама знает, как отправить запрос, как обновить сущность пользователя и как отобразить результаты.
Widgets — составные интерфейсные блоки
Слой widgets — это «секции» интерфейса, которые соединяют несколько фич и сущностей в законченную визуальную часть.
Примеры:
- шапка сайта (header);
- боковая панель (sidebar);
- блок рекомендаций;
- форма поиска с фильтрами.
Пример виджета Header
Давайте разберёмся на примере header’а, который показывает логотип, имя пользователя и кнопку логина или логаута.
Структура:
widgets/
header/
ui/
Header.svelte
model/
userMenu.ts
Файл src/widgets/header/ui/Header.svelte:
<script lang="ts">
import { userModel } from '$entities/user/model/userStore';
import UserAvatar from '$entities/user/ui/UserAvatar.svelte';
import Button from '$shared/ui/Button.svelte';
import LoginForm from '$features/auth/login/ui/LoginForm.svelte';
import { derived } from 'svelte/store';
// Создаем производный стор для текущего пользователя
const currentUser$ = derived(userModel.currentUser, ($user) => $user);
let currentUser = null;
// Подписываемся на стор пользователя
const unsubscribe = currentUser$.subscribe((value) => {
currentUser = value;
});
let showLogin = false;
// Обрабатываем клик по кнопке выхода
function handleLogout() {
userModel.logout();
}
// Переключаем показ формы логина
function toggleLogin() {
showLogin = !showLogin;
}
</script>
<header class="header">
<div class="logo">My Svelte App</div>
<div class="right">
{#if currentUser}
<!-- Показываем аватар и имя, если пользователь залогинен -->
<UserAvatar {user}={currentUser} size={32} />
<span>{currentUser.name}</span>
<Button variant="secondary" on:click={handleLogout}>
Выйти
</Button>
{:else}
<!-- Иначе показываем кнопку логина -->
<Button on:click={toggleLogin}>
Войти
</Button>
{/if}
</div>
</header>
{#if showLogin && !currentUser}
<!-- Если пользователь не залогинен и нажал "Войти" - показываем форму -->
<LoginForm />
{/if}
<style>
.header {
display: flex;
justify-content: space-between;
padding: 0.75rem 1rem;
background: #f8f8f8;
}
.right {
display: flex;
gap: 0.5rem;
align-items: center;
}
</style>
Обратите внимание, как этот фрагмент кода решает задачу:
- виджет ничего не знает о том, как работает логин внутри — он просто использует LoginForm;
- он использует сущность User (через UserAvatar и userModel);
- он комбинирует фичу и сущность в законченную часть интерфейса.
Pages — страницы в SvelteKit и FSD
Слой pages — уровень, который знает о маршрутах и собирает из widgets и features конкретные страницы.
В SvelteKit маршруты привязаны к структуре src/routes. Это значит, что нам нужно аккуратно совместить svelte-fsd со стандартным роутингом.
Один из удобных подходов:
- оставить src/routes для файлов маршрутов;
- внутри использовать компоненты и модели из слоя pages.
Организация страниц с svelte-fsd
Структура:
src/
routes/
+layout.svelte
+page.svelte // Главная страница
profile/
+page.svelte
pages/
home/
ui/
HomePage.svelte
profile/
ui/
ProfilePage.svelte
model/
loader.ts
Файл src/pages/home/ui/HomePage.svelte:
<script lang="ts">
import Header from '$widgets/header/ui/Header.svelte';
import ProductCard from '$entities/product/ui/ProductCard.svelte';
// Здесь можно подключить фичи, например, "добавить в корзину"
import AddToCartButton from '$features/cart/add-to-cart/ui/AddToCartButton.svelte';
// Массив товаров для примера
const products = [
{ id: '1', name: 'Товар 1', price: 1000 },
{ id: '2', name: 'Товар 2', price: 2000 }
];
</script>
<Header />
<main>
<h1>Главная</h1>
<div class="products">
{#each products as product}
<div class="product">
<ProductCard {product} />
<AddToCartButton {product} />
</div>
{/each}
</div>
</main>
<style>
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.product {
border: 1px solid #ddd;
padding: 0.75rem;
}
</style>
Файл src/routes/+page.svelte:
<script lang="ts">
import HomePage from '$pages/home/ui/HomePage.svelte';
</script>
<!-- Здесь мы просто рендерим страницу из слоя pages -->
<HomePage />
Таким образом, слой pages становится мостом между роутингом и архитектурой FSD.
Processes — сквозные процессы
Слой processes используется не во всех проектах. Он нужен, когда у вас есть длинные сквозные сценарии, которые затрагивают несколько страниц и фич.
Например:
- процесс оформления заказа;
- онбординг пользователя;
- мастера (wizard’ы).
Svelte здесь ничем особо не отличается от других фреймворков — в processes вы заворачиваете координацию нескольких фич.
Короткий пример: процесс checkout.
processes/
checkout/
model/
store.ts
ui/
CheckoutFlow.svelte
Файл src/processes/checkout/model/store.ts:
import { writable } from 'svelte/store';
// Состояние процесса оформления заказа
interface CheckoutState {
step: 1 | 2 | 3;
completed: boolean;
}
const initialState: CheckoutState = {
step: 1,
completed: false
};
const state = writable<CheckoutState>(initialState);
export const checkoutProcess = {
state,
// Переходим к следующему шагу
next() {
state.update((s) => {
if (s.step < 3) {
return { ...s, step: (s.step + 1) as CheckoutState['step'] };
}
return { ...s, completed: true };
});
},
// Сбрасываем процесс
reset() {
state.set(initialState);
}
};
Файл src/processes/checkout/ui/CheckoutFlow.svelte:
<script lang="ts">
import { get } from 'svelte/store';
import { checkoutProcess } from '../model/store';
import CartWidget from '$widgets/cart/ui/CartWidget.svelte';
import DeliveryForm from '$features/checkout/delivery/ui/DeliveryForm.svelte';
import PaymentForm from '$features/checkout/payment/ui/PaymentForm.svelte';
let state = get(checkoutProcess.state);
// Подписываемся на стор процесса
const unsubscribe = checkoutProcess.state.subscribe((value) => {
state = value;
});
// Переход к следующему шагу
function handleNext() {
checkoutProcess.next();
}
</script>
{#if state.completed}
<p>Заказ оформлен</p>
{:else}
{#if state.step === 1}
<CartWidget />
{:else if state.step === 2}
<DeliveryForm />
{:else if state.step === 3}
<PaymentForm />
{/if}
<button on:click={handleNext}>
Далее
</button>
{/if}
Такой процесс можно использовать на странице оформления заказа и не смешивать логику процесса с конкретными фичами.
Импорт и публичные API слоёв
Чтобы архитектура не превратилась в хаос, svelte-fsd рекомендует определять «точки входа» (public API) для каждого среза (slice).
Например:
- для сущности user — index.ts в src/entities/user;
- для фичи auth/login — index.ts в src/features/auth/login;
- для виджета header — index.ts в src/widgets/header.
Пример публичного API сущности user
Давайте посмотрим пример.
Структура:
entities/
user/
index.ts
model/
userStore.ts
user.types.ts
ui/
UserAvatar.svelte
UserName.svelte
Файл src/entities/user/index.ts:
// Реэкспортируем всё, что хотим сделать публичным
export * as userModel from './model/userStore';
export * from './model/user.types';
export { default as UserAvatar } from './ui/UserAvatar.svelte';
export { default as UserName } from './ui/UserName.svelte';
Теперь в любом месте проекта вы можете писать:
// Импортируем сущность через ее public API
import { userModel, UserAvatar } from '$entities/user';
Это даёт вам два преимущества:
- Внешний код не лезет внутрь структуры сущности (в model/ui-папки).
- Вы можете менять внутреннюю структуру user, не затрагивая всех потребителей.
Aliases для слоёв
Чтобы такие импорты выглядели аккуратно, в SvelteKit удобно настроить alias’ы.
В файле svelte.config.js (или vite.config.js, в зависимости от версии):
// Здесь мы настраиваем алиасы для слоёв FSD
import path from 'path';
import { sveltekit } from '@sveltejs/kit/vite';
const config = {
plugins: [sveltekit()],
resolve: {
alias: {
$app: path.resolve('src/app'),
$processes: path.resolve('src/processes'),
$pages: path.resolve('src/pages'),
$widgets: path.resolve('src/widgets'),
$features: path.resolve('src/features'),
$entities: path.resolve('src/entities'),
$shared: path.resolve('src/shared')
}
}
};
export default config;
Теперь вы можете писать:
import Header from '$widgets/header';
import { loginModel } from '$features/auth/login';
import { userModel } from '$entities/user';
import Button from '$shared/ui/Button.svelte';
и явно видеть, к какому слою относится импортируемый модуль.
Организация store’ов и бизнес-логики в Svelte при FSD
Svelte даёт довольно простой механизм сто́ров, но именно из-за этой простоты часто возникает соблазн создать «один главный store» и раздавать его по всему приложению. В svelte-fsd и в Feature-Sliced подходе в целом это считается антипаттерном.
Как разносить сторы по слоям
Общий принцип:
- сторы, описывающие сущность, живут в entities;
- сторы, описывающие состояние фичи, живут в features;
- сторы, описывающие сквозной процесс, — в processes;
- глобальные технические сторы (например, тема, локаль) — в app или shared (в зависимости от задачи).
Давайте посмотрим на пример общей фабрики сто́ров в shared, чтобы не дублировать код.
Файл src/shared/lib/createFormStore.ts:
import { writable } from 'svelte/store';
// Универсальная фабрика стора формы
export function createFormStore<T extends object>(initialValues: T) {
const values = writable<T>(initialValues);
const errors = writable<Record<keyof T, string | null>>({} as any);
const touched = writable<Record<keyof T, boolean>>({} as any);
return {
values,
errors,
touched,
// Устанавливаем значение поля
setField<K extends keyof T>(field: K, value: T[K]) {
values.update((v) => ({ ...v, [field]: value }));
},
// Устанавливаем ошибку поля
setError<K extends keyof T>(field: K, message: string | null) {
errors.update((e) => ({ ...e, [field]: message }));
},
// Отмечаем поле как посещенное
setTouched<K extends keyof T>(field: K, isTouched: boolean) {
touched.update((t) => ({ ...t, [field]: isTouched }));
}
};
}
Теперь любая фича с формой (логин, доставка, фильтры) может использовать эту фабрику, оставаясь в рамках своего слоя.
Интеграция svelte-fsd в существующий проект
Если у вас уже есть Svelte- или SvelteKit-проект и вы хотите постепенно перейти к Feature-Sliced Design, удобно двигаться по шагам.
Шаг 1. Ввод слоёв без жёсткой реструктуризации
- Добавьте в src директории entities, features, widgets, shared, pages, app.
- Настройте alias’ы для этих слоёв.
- Новые модули создавайте уже в новой структуре.
Старый код можно пока оставить как есть (например, в папке components) и постепенно переносить.
Шаг 2. Выделите сущности
- Сгруппируйте типы и логические сущности: User, Product, Order.
- Вынесите их модели и UI-компоненты в src/entities.
- Настройте публичные API (index.ts) для первых сущностей.
Здесь полезно начинать именно с entities — они составляют «язык» вашего приложения.
Шаг 3. Перенесите крупные фичи
- Найдите фрагменты логики, которые повторяются или логически цельны: логин, регистрация, поиск, корзина.
- Вынесите их в features, создавая отдельные папки.
- Оставьте старые вызовы, но меняйте импорты на новые пути.
Шаг 4. Постепенно собирайте widgets и pages
- Формально выделяйте виджеты (header, footer, sidebar).
- Создавайте слой pages и переносите в него логику страниц.
- В роутинге SvelteKit начинайте рендерить страницы из слоя pages, как в показанном выше примере.
Так вы сможете плавно перейти на svelte-fsd без «большого переписывания».
Заключение
Подход Feature-Sliced Design в связке с Svelte и SvelteKit помогает выстроить архитектуру проекта вокруг доменной логики, а не вокруг технических деталей.
svelte-fsd в этом контексте — это адаптация FSD под особенности Svelte:
- работа со слоями и срезами (entities, features, widgets, pages, processes, app, shared);
- ясные архитектурные границы между слоями;
- понятные публичные API для сущностей и фич;
- использование Svelte-специфики (stores, context, routing) без нарушения принципов FSD.
Главная идея — каждая часть приложения отвечает за свою область:
- сущности описывают предметный мир;
- фичи реализуют конкретные пользовательские сценарии;
- виджеты собирают фичи и сущности в интерфейсные блоки;
- страницы и процессы координируют работу нескольких частей, но не тянут в себя лишнюю логику.
Двигаясь по этому пути шаг за шагом, вы снижаете связанность кода, упрощаете поддержку и облегчаете добавление новых возможностей в Svelte-приложение.
Частозадаваемые технические вопросы по svelte-fsd
Как сочетать svelte-fsd и file-based routing SvelteKit чтобы не дублировать структуру
Обычно src/routes остаётся только точкой входа. Внутри +page.svelte вы просто импортируете компонент из слоя pages. Структуру роутов повторять в pages не обязательно, но удобно: pages/home, pages/profile и т.п. Маршруты не «знают» о слоях ниже pages, взаимодействие идёт только через компоненты страниц.
Как хранить типы которые относятся сразу к нескольким сущностям
Если тип относится к конкретной сущности — он в entities/
Как правильно использовать контексты Svelte в рамках FSD
Контекст — технический механизм. Его инициаторы обычно живут в app (глобальные провайдеры) или в widgets (локальные провайдеры для части интерфейса). Внутри features и entities лучше не создавать новые контексты, а только потреблять уже заданные, чтобы не размывать ответственность слоёв.
Как поступать с глобальными сторми авторизации или темы приложения
Если стор описывает техническое глобальное состояние (тема, локаль) — он уместен в app или shared. Если это доменное состояние (текущий пользователь) — лучше разместить его в entities/user. При этом слой app может использовать этот стор (например, чтобы регулировать доступ к страницам), но не владеть им.
Что делать с очень мелкими фичами вроде простого toggle или show/hide
Если поведение действительно примитивное и не несёт бизнес-смысла, его можно реализовать как локальное состояние компонента или как общую утилиту/хук в shared/lib. Выносить в полноценную feature есть смысл только тогда, когда сценарий отражает заметный пользовательский функционал или переиспользуется в нескольких местах.