Clean Architecture — это набор принципов организации кода, направленных на разделение ответственности, независимость бизнес-логики от фреймворков и упрощение поддержки проекта. Во фронтенде, и особенно во Vue.js, эти принципы позволяют избежать превращения компонентов в перегруженные «комбайны», где смешаны логика, состояние, запросы к API и представление.
Ключевая идея: бизнес-логика не должна зависеть от Vue, а Vue должен быть лишь способом отображения и связывания данных.
Во Vue-проекте Clean Architecture обычно адаптируется под фронтенд-реалии, сохраняя фундаментальные слои.
Содержит чистую бизнес-логику:
Этот слой:
// domain/user/User.js
export class User {
constructor({ id, email, isActive }) {
if (!email) {
throw new Error('Email is required');
}
this.id = id;
this.email = email;
this.isActive = isActive;
}
deactivate() {
this.isActive = false;
}
}
Описывает сценарии использования приложения:
Use Case — это один законченный сценарий.
// application/users/DeactivateUser.js
export class DeactivateUser {
constructor(userRepository) {
this.userRepository = userRepository;
}
async execute(userId) {
const user = await this.userRepository.getById(userId);
user.deactivate();
await this.userRepository.save(user);
return user;
}
}
Здесь отсутствует любая привязка к Vue или HTTP.
Реализует технические детали:
// infrastructure/repositories/UserApiRepository.js
import axios from 'axios';
import { User } from '@/domain/user/User';
export class UserApiRepository {
async getById(id) {
const { data } = await axios.get(`/users/${id}`);
return new User(data);
}
async save(user) {
await axios.put(`/users/${user.id}`, user);
}
}
Этот слой зависит от домена, но не наоборот.
Содержит:
Vue здесь выполняет роль «тонкого» слоя.
// presentation/composables/useDeactivateUser.js
import { DeactivateUser } from '@/application/users/DeactivateUser';
import { UserApiRepository } from '@/infrastructure/repositories/UserApiRepository';
export function useDeactivateUser() {
const repository = new UserApiRepository();
const useCase = new DeactivateUser(repository);
const deactivate = async (userId) => {
return await useCase.execute(userId);
};
return { deactivate };
}
Ключевой принцип Clean Architecture — зависимости направлены внутрь.
Используются интерфейсы и абстракции:
// domain/user/UserRepository.js
export class UserRepository {
getById(id) {
throw new Error('Not implemented');
}
save(user) {
throw new Error('Not implemented');
}
}
Use Case работает с этим интерфейсом, не зная, откуда приходят данные.
Pinia не является хранилищем бизнес-логики. В чистой архитектуре:
// presentation/stores/userStore.js
import { defineStore } from 'pinia';
import { useDeactivateUser } from '@/presentation/composables/useDeactivateUser';
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
}),
actions: {
async deactivateUser() {
const { deactivate } = useDeactivateUser();
this.user = await deactivate(this.user.id);
},
},
});
Vue-компоненты:
<script setup>
import { useUserStore } from '@/presentation/stores/userStore';
const store = useUserStore();
</script>
<template>
<button @click="store.deactivateUser">
Deactivate
</button>
</template>
Компонент отвечает только за отображение и пользовательские действия.
Типичная структура Vue-проекта с Clean Architecture:
src/
├── domain/
│ └── user/
├── application/
│ └── users/
├── infrastructure/
│ └── repositories/
├── presentation/
│ ├── components/
│ ├── composables/
│ └── stores/
Такая структура:
Use Case тестируется без Vue и браузера:
it('deactivates user', async () => {
const fakeRepo = {
getById: async () => new User({ id: 1, email: 'a@a.com', isActive: true }),
save: async () => {},
};
const useCase = new DeactivateUser(fakeRepo);
const user = await useCase.execute(1);
expect(user.isActive).toBe(false);
});
Бизнес-логика проверяется изолированно, без моков UI и HTTP.
Во Vue-проектах Clean Architecture:
Главное — разделение ответственности, а не слепое следование шаблонам.