Отход от React и Vue.js во внешнем интерфейсе с использованием чистой архитектуры

Эта статья является английским переводом оригинала в моем блоге: ReactJs y VueJs en el front end usando Clean Architecture.

Одним из преимуществ использования чистой архитектуры, среди прочего, является возможность отсоединить наше приложение от механизма доставки пользователю, то есть от инфраструктуры пользовательского интерфейса или библиотеки.

Это преимущество в долгосрочных приложениях позволяет нам адаптироваться в будущем к изменениям, которые обязательно произойдут в библиотеках и фреймворках.

В этой статье мы доведем чистую архитектуру до крайности во внешнем интерфейсе, применив два механизма доставки: React и Vue.js.

У нас будет как можно больше кода, повторно используемого между двумя реализациями.

Это станет возможным за счет создания логики домена, данных и удаленного отображения React и Vue.js.

Зачем отходить от фреймворка?

Я разработал различные технологии с применением чистой архитектуры, такие как .Net, Android, iOS и Flutter. Долгое время я тоже занимаюсь программированием во front-end и пишу об этом.

Одна из самых больших проблем, когда дело доходит до развития приложения, - это связь с UI-фреймворком.

На интерфейсе постепенно из-за ответственности, которую со временем приобрели приложения этого типа, становится все больше и больше смысла разрабатывать более структурированным образом, и проблемы, которые необходимо решить, очень похожи на те, которые существуют на другие направления, такие как backend или мобильная разработка.

Существуют такие фреймворки, как React и Vue.js, которые облегчают нам жизнь, чтобы справиться с этими проблемами во внешнем интерфейсе.

Сегодня интерфейсное приложение во многих случаях является независимым от серверного приложения и поэтому должно иметь собственную архитектуру.

Кроме того, эта архитектура должна помочь нам в следующих моментах:

  • Независимо от пользовательского интерфейса, фреймворков, оставшихся и постоянных API, баз данных или сторонних сервисов.
  • Эскалация.
  • Тестируемость.

Это означает, что если мы изменим представление о приложении ReactJs или VueJs, чтобы иметь интерфейсное приложение, которое использует для рендеринга ReactJs или VueJs, это значительно упростит нашу жизнь в будущем.

Так, например, преобразование вашего приложения ReactJS от использования классов, как раньше, к использованию функций и хуков, как это делается сейчас, является гораздо более тривиальным. То же самое произойдет, если вы переключитесь в VueJS с использования API параметров на API композиции.

Это более тривиально, потому что вы используете фреймворк только для строго необходимых, таких как рендеринг, и, таким образом, вы не злоупотребляете им, удерживая его подальше от любого типа логики, будь то предметная область, данные или логика представления.

Фреймворки развиваются, и вы не можете это контролировать, но вы можете контролировать их связь с ними и то, как их изменения влияют на вас.

Но в этом случае мы собираемся выйти за рамки того, как адаптироваться к изменениям, которые могут произойти во фреймворке, и мы увидим объем кода, который не может измениться, когда мы изменим ReactJS с помощью VueJS, если мы будем использовать чистую архитектуру и разделить обязанности. .

Это картина, о которой вы должны помнить, если разрабатываете с использованием чистой архитектуры.

Если вы не понимаете концепции чистой архитектуры, я рекомендую вам прочитать эту статью.

Самая важная часть - это правило зависимости, поэтому, если вы не понимаете, о чем я говорю, рекомендую вам прочитать эту статью.

Пример, который мы собираемся увидеть, основан на том, что мы видели в этой статье.

Наш сценарий

Это тележка для покупок с достаточной функциональностью, чтобы выглядеть как настоящий пример. У нас будет глобальное состояние, неглобальное состояние, и мы будем имитировать вызов удаленной службы.

Архитектура

На уровне структуры проекта мы будем использовать монорепозицию с использованием пряжи рабочих пространств, таким образом мы можем разделить проект на модули или пакеты, разделяющие код между ними.

У нас есть несколько пакетов:

  • Ядро: в этом пакете у нас будет весь общий код между приложением, отображаемым ReactJS, и приложением, отображаемым VueJs.
  • React: в этом пакете находится версия приложения React.
  • Vue: в этом пакете находится версия приложения Vue.

¿Какой код используется повторно?

Мы собираемся повторно использовать весь код, который мы должны были отсоединить от UI Framework, поскольку, будучи разными версиями одного и того же приложения, имеет смысл, чтобы этот код был общим и не записывался дважды.

Это демонстрационное упражнение, демонстрирующее потенциал чистой архитектуры, но такое разделение инфраструктуры пользовательского интерфейса необходимо даже при разработке настоящего приложения.

Использование UI-фреймворка для того, что строго необходимо, позволяет нам лучше адаптироваться к изменениям в будущих версиях фреймворка.

Это связано с тем, что код, который содержит логику приложения, которая является наиболее важной частью, которая со временем меняется меньше, и является кодом, который потенциально может использоваться двумя версиями одного и того же приложения, как в этом примере, он отсоединяется независимо от Фреймворк пользовательского интерфейса.

В чистой архитектуре на уровне домена находится бизнес-логика предприятия и приложения.

Уровень данных - это то место, где мы общаемся с постоянством.

Логика представления - это та, которая решает, какие данные показывать, должно ли что-то быть видимым или нет, должно ли быть показано пользователю, что мы загружаем данные, или должна ли отображаться ошибка. Здесь осуществляется управление состоянием компонентов.

Каждая из этих трех частей содержит логику, которую мы должны отсоединить, и находится в основном пакете.

Уровень домена

Уровень домена - это место, где находится бизнес-логика предприятия и приложения.

Случаи применения

Варианты использования - это намерения, они содержат бизнес-логику приложения, это действия, и в этом примере у нас есть следующее:

  • GetProductsUseCase
  • GetCartUseCase
  • AddProductToCartUseCase
  • EditQuantityOfCartItemUseCase
  • RemoveItemFromCartUseCase

Давайте посмотрим на пример GetProductsUseCase:

export class GetProductsUseCase {
    private productRepository: ProductRepository;
constructor(productRepository: ProductRepository) {
        this.productRepository = productRepository;
    }
execute(filter: string): Promise<Either<DataError, Product[]>> {
        return this.productRepository.get(filter);
    }
}

Этот вариант использования прост, потому что он состоит из простого вызова уровня данных, в других контекстах, где, например, при создании продукта мы должны подтвердить, что больше нет продукта с тем же SKU, было бы больше логики .

Варианты использования возвращают тип Either, если вы не уверены, что это такое, я рекомендую вам прочитать эту статью и эту статью.

Таким образом, обработка ошибок не выполняется с помощью перехвата обещаний, но объект результата самого обещания сообщает вам, успешен ли результат или нет.

Использование Either по сравнению с классическим try-catch имеет несколько преимуществ:

  • За процессом выполнения проще следить без переходов между вызывающими абонентами при возникновении ошибки.
  • Это явно указано на то, что что-то может пойти не так.
  • Ошибки, которые могут возникнуть, явно указаны.
  • При использовании исчерпывающего переключателя, если вы добавите больше ошибок в будущем, TypeScript предупредит вас, где вы не учли эту новую ошибку.

Тип ошибок следующий:

export interface UnexpectedError {
    kind: "UnexpectedError";
    message: Error;
}
export type DataError = UnexpectedError;

Потенциально в будущем это может развиться примерно так:

export interface ApiError {
    kind: "ApiError";
    error: string;
    statusCode: number;
    message: string;
}
export interface UnexpectedError {
    kind: "UnexpectedError";
    message: Error;
}
export interface Unauthorized {
    kind: "Unauthorized";
}
export interface NotFound {
    kind: "NotFound";
}
export type DataError = ApiError | UnexpectedError | Unauthorized;

А на уровне представления, если я использую исчерпывающий переключатель, Typescript предупредит меня, что я должен добавлять больше случаев для каждой новой ошибки.

Сущности

Сущности содержат бизнес-логику предприятия.

Давайте посмотрим на пример тележки:

type TotalPrice = number;
type TotalItems = number;
export class Cart {
    items: readonly CartItem[];
    readonly totalPrice: TotalPrice;
    readonly totalItems: TotalItems;
constructor(items: CartItem[]) {
        this.items = items;
        this.totalPrice = this.calculateTotalPrice(items);
        this.totalItems = this.calculateTotalItems(items);
    }
static createEmpty(): Cart {
        return new Cart([]);
    }
addItem(item: CartItem): Cart {
        const existedItem = this.items.find(i => i.id === item.id);
if (existedItem) {
            const newItems = this.items.map(oldItem => {
                if (oldItem.id === item.id) {
                    return { ...oldItem, quantity: oldItem.quantity + item.quantity };
                } else {
                    return oldItem;
                }
            });
return new Cart(newItems);
        } else {
            const newItems = [...this.items, item];
return new Cart(newItems);
        }
    }
removeItem(itemId: string): Cart {
        const newItems = this.items.filter(i => i.id !== itemId);
return new Cart(newItems);
    }
editItem(itemId: string, quantity: number): Cart {
        const newItems = this.items.map(oldItem => {
            if (oldItem.id === itemId) {
                return { ...oldItem, quantity: quantity };
            } else {
                return oldItem;
            }
        });
return new Cart(newItems);
    }
private calculateTotalPrice(items: CartItem[]): TotalPrice {
        return +items
            .reduce((accumulator, item) => accumulator + item.quantity * item.price, 0)
            .toFixed(2);
    }
private calculateTotalItems(items: CartItem[]): TotalItems {
        return +items.reduce((accumulator, item) => accumulator + item.quantity, 0);
    }
}

В этом примере сущности простые, со свойствами примитивных типов, но реальный пример, когда были проверки, мы могли бы иметь Entities и V alue Objects, определенные как классы и с фабричными методами, где выполняется проверка. Мы используем Either, чтобы вернуть ошибки или результат.

Границы

Границы - это абстракции адаптеров, например, в гексагональной архитектуре они называются портами. Они определены на уровне вариантов использования в домене и указывают, как мы собираемся взаимодействовать с адаптерами.

Например, для связи с уровнем данных мы используем шаблон репозитория.

export interface ProductRepository {
    get(filter: string): Promise<Either<DataError, Product[]>>;
}

Уровень данных

На уровне данных находятся адаптеры, и адаптер отвечает за преобразование информации между доменом и внешними системами.

Внешними системами могут быть веб-сервис, база данных и т. Д.

В этом простом примере я использую те же сущности, которые представляют продукт, корзину покупок и элементы корзины между уровнями презентации, домена и данных.

В реальных приложениях обычно используются разные структуры данных для каждого уровня или даже объекты передачи данных (DTO) для передачи данных между уровнями.

В этом примере у нас есть репозитории, которые возвращают данные, хранящиеся в памяти.

const products = [
  ...
];
export class ProductInMemoryRepository implements ProductRepository {
    get(filter: string): Promise<Either<DataError, Product[]>> {
        return new Promise((resolve, _reject) => {
            setTimeout(() => {
                try {
                    if (filter) {
                        const filteredProducts = products.filter((p: Product) => {
                            return p.title.toLowerCase().includes(filter.toLowerCase());
                        });
resolve(Either.right(filteredProducts));
                    } else {
                        resolve(Either.right(products));
                    }
                } catch (error) {
                    resolve(Either.left(error));
                }
            }, 100);
        });
    }
}

Важно понимать, что репозиторий является адаптером и что его абстракция или порт определены в домене, поэтому традиционное направление зависимости меняется на противоположное.

Это самая важная часть чистой архитектуры, домен не должен иметь никакой зависимости от внешних слоев, таким образом он отсоединяется, и в будущем будет проще заменить адаптер другим или даже в целях тестирования.

Таким образом, если мы заменим реализацию адаптера той, которая вызывает веб-службу, домен не будет затронут, и поэтому мы скрываем детали реализации.

Уровень презентации - Адаптеры

Адаптеры уровня представления являются последней частью нашего основного пакета для повторного использования, и именно здесь мы подключаем уровни UI React или Vue.

Эти адаптеры также можно повторно использовать между двумя версиями приложения, они являются посредниками между компонентами пользовательского интерфейса и уровнем домена.

Они содержат логику представления, определяющую, какая информация отображается, что должно быть видно и т. Д.

Управление состоянием выполняется на этом уровне и не зависит от React или Vue.

Мы можем использовать разные шаблоны представления. В этом случае я использую шаблон BLoC, потому что он очень хорошо сочетается с декларативными фреймворками, такими как React и Vue.

Если вы хотите вникнуть в паттерн BLoC, рекомендую прочитать эту статью.

Как я уже говорил в этой статье, когда вы используете BLoC с чистой архитектурой, имеет смысл называть их PLoC, Компонент логики представления. В этом примере они названы так.

Давайте посмотрим на пример корзины для покупок:

export class CartPloc extends Ploc<CartState> {
    constructor(
        private getCartUseCase: GetCartUseCase,
        private addProductToCartUseCase: AddProductToCartUseCase,
        private removeItemFromCartUseCase: RemoveItemFromCartUseCase,
        private editQuantityOfCartItemUseCase: EditQuantityOfCartItemUseCase
    ) {
        super(cartInitialState);
        this.loadCart();
    }
closeCart() {
        this.changeState({ ...this.state, open: false });
    }
openCart() {
        this.changeState({ ...this.state, open: true });
    }
removeCartItem(item: CartItemState) {
        this.removeItemFromCartUseCase
            .execute(item.id)
            .then(cart => this.changeState(this.mapToUpdatedState(cart)));
    }
editQuantityCartItem(item: CartItemState, quantity: number) {
        this.editQuantityOfCartItemUseCase
            .execute(item.id, quantity)
            .then(cart => this.changeState(this.mapToUpdatedState(cart)));
    }
addProductToCart(product: Product) {
        this.addProductToCartUseCase
            .execute(product)
            .then(cart => this.changeState(this.mapToUpdatedState(cart)));
    }
private loadCart() {
        this.getCartUseCase
            .execute()
            .then(cart => this.changeState(this.mapToUpdatedState(cart)))
            .catch(() =>
                this.changeState({
                    kind: "ErrorCartState",
                    error: "An error has ocurred loading products",
                    open: this.state.open,
                })
            );
    }
mapToUpdatedState(cart: Cart): CartState {
        const formatOptions = { style: "currency", currency: "EUR" };
return {
            kind: "UpdatedCartState",
            open: this.state.open,
            totalItems: cart.totalItems,
            totalPrice: cart.totalPrice.toLocaleString("es-ES", formatOptions),
            items: cart.items.map(cartItem => {
                return {
                    id: cartItem.id,
                    image: cartItem.image,
                    title: cartItem.title,
                    price: cartItem.price.toLocaleString("es-ES", formatOptions),
                    quantity: cartItem.quantity,
                };
            }),
        };
    }
}

Базовый класс всех PLoC отвечает за сохранение состояния и уведомление при его изменении.

type Subscription<S> = (state: S) => void;
export abstract class Ploc<S> {
    private internalState: S;
    private listeners: Subscription<S>[] = [];
constructor(initalState: S) {
        this.internalState = initalState;
    }
public get state(): S {
        return this.internalState;
    }
changeState(state: S) {
        this.internalState = state;
if (this.listeners.length > 0) {
            this.listeners.forEach(listener => listener(this.state));
        }
    }
subscribe(listener: Subscription<S>) {
        this.listeners.push(listener);
    }
unsubscribe(listener: Subscription<S>) {
        const index = this.listeners.indexOf(listener);
        if (index > -1) {
            this.listeners.splice(index, 1);
        }
    }
}

Вся информация, которая требуется компоненту пользовательского интерфейса, должна интерпретироваться из состояния, элементов для отображения в таблице или списке, а также должна ли что-то быть видимым или нет, например, корзина для покупок, загрузка или отображаемая ошибка.

export interface CommonCartState {
    open: boolean;
}
export interface LoadingCartState {
    kind: "LoadingCartState";
}
export interface UpdatedCartState {
    kind: "UpdatedCartState";
    items: Array<CartItemState>;
    totalPrice: string;
    totalItems: number;
}
export interface ErrorCartState {
    kind: "ErrorCartState";
    error: string;
}
export type CartState = (LoadingCartState | UpdatedCartState | ErrorCartState) & CommonCartState;
export interface CartItemState {
    id: string;
    image: string;
    title: string;
    price: string;
    quantity: number;
}
export const cartInitialState: CartState = {
    kind: "LoadingCartState",
    open: false,
};

В этом случае с помощью типов объединения машинописных текстов мы можем более безопасно и функционально смоделировать наше состояние, используя сумма алгебраических типов данных.

Этот способ моделирования менее подвержен ошибкам, потому что вы указываете очень четкую форму, что состояние имеет 3 основных возможности:

  • Информация о загрузке
  • произошла ошибка
  • Обновленные данные

Уровень презентации - пользовательский интерфейс

На этом уровне находятся компоненты и все, что связано с React или Vue, например компоненты, хуки, приложения и т. Д.

Компоненты очень простые и легкие, потому что они могут свободно управлять любым типом логики или управления состоянием, это ответственность каждого из уровней в основном пакете.

Приложение React

В ответ у нас будут компоненты, которые отображают наш список продуктов, панель приложения с количеством продуктов в корзине и корзину продуктов, отображаемую как боковую панель.

Давайте посмотрим на пример компонента, который отображает содержимое корзины.

import React from "react";
import { makeStyles, Theme } from "@material-ui/core/styles";
import { List, Divider, Box, Typography, CircularProgress } from "@material-ui/core";
import CartContentItem from "./CartContentItem";
import { CartItemState } from "@frontend-clean-architecture/core";
import { useCartPloc } from "../app/App";
import { usePlocState } from "../common/usePlocState";
const useStyles = makeStyles((theme: Theme) => ({
    totalPriceContainer: {
        display: "flex",
        alignItems: "center",
        padding: theme.spacing(1, 0),
        justifyContent: "space-around",
    },
    itemsContainer: {
        display: "flex",
        alignItems: "center",
        padding: theme.spacing(1, 0),
        justifyContent: "space-around",
        minHeight: 150,
    },
    itemsList: {
        overflow: "scroll",
    },
    infoContainer: {
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
    },
}));
const CartContent: React.FC = () => {
    const classes = useStyles();
    const ploc = useCartPloc();
    const state = usePlocState(ploc);
const cartItems = (items: CartItemState[]) => (
        <List className={classes.itemsList}>
            {items.map((item, index) => (
                <CartContentItem key={index} cartItem={item} />
            ))}
        </List>
    );
const emptyCartItems = () => (
        <React.Fragment>
            <Typography variant="h6" component="h2">
                Empty Cart :(
            </Typography>
        </React.Fragment>
    );
switch (state.kind) {
        case "LoadingCartState": {
            return (
                <div className={classes.infoContainer}>
                    <CircularProgress />
                </div>
            );
        }
        case "ErrorCartState": {
            return (
                <div className={classes.infoContainer}>
                    <Typography display="inline" variant="h5" component="h2">
                        {state.error}
                    </Typography>
                </div>
            );
        }
        case "UpdatedCartState": {
            return (
                <React.Fragment>
                    <Box flexDirection="column" className={classes.itemsContainer}>
                        {state.items.length > 0 ? cartItems(state.items) : emptyCartItems()}
                    </Box>
                    <Divider />
                    <Box flexDirection="row" className={classes.totalPriceContainer}>
                        <Typography variant="h6" component="h2">
                            Total Price
                        </Typography>
                        <Typography variant="h6" component="h2">
                            {state.totalPrice}
                        </Typography>
                    </Box>
                </React.Fragment>
            );
        }
    }
};
export default CartContent;

Крючки

При использовании чистой архитектуры хуки не используются? Да, они используются, но для чего строго необходимо.

Состояние не будет управляться с помощью хуков, побочные эффекты не будут вызваны хуками, это ответственность PloC в основном пакете.

Но мы будем использовать их для хранения окончательного состояния компонента, которое возвращает нам его PloC, и мы будем использовать их для совместного использования контекста между компонентами или реакции на изменение состояния, которое возвращает нам PloC.

Давайте посмотрим, как определяется хук usePLocState, который мы использовали в компоненте:

export function usePlocState<S>(ploc: Ploc<S>) {
    const [state, setState] = useState(ploc.state);
useEffect(() => {
        const stateSubscription = (state: S) => {
            setState(state);
        };
ploc.subscribe(stateSubscription);
return () => ploc.unsubscribe(stateSubscription);
    }, [ploc]);
return state;
}

Эта настраиваемая ловушка отвечает за подписку на изменения состояния PloC и сохранение конечного состояния.

Приложение Vue

Во Vue у нас также будут те же компоненты, что и в версии React.

Теперь давайте посмотрим на компонент, который отображает содержимое корзины покупок в версии Vue:

<template>
    <div id="info-container" v-if="state.kind === 'LoadingCartState'">
        <ProgressSpinner />
    </div>
    <div id="info-container" v-if="state.kind === 'ErrorCartState'">Error</div>
    <div id="items-container" v-if="state.kind === 'UpdatedCartState'">
        <div v-if="state.items.length > 0" style="overflow: scroll">
            <div v-for="item in state.items" v-bind:key="item.id">
                <CartContenttItem v-bind="item" />
            </div>
        </div>
        <h2 v-if="state.items.length === 0">Empty Cart :(</h2>
    </div>
    <Divider />
    <div id="total-price-container">
        <h3>Total Price</h3>
        <h3>{{ state.totalPrice }}</h3>
    </div>
</template>
<script lang="ts">
import { defineComponent, inject } from "vue";
import { CartPloc } from "@frontend-clean-architecture/core";
import { usePlocState } from "../common/usePlocState";
import CartContenttItem from "./CartContenttItem.vue";
export default defineComponent({
    components: {
        CartContenttItem,
    },
    setup() {
        const ploc = inject<CartPloc>("cartPloc") as CartPloc;
        const state = usePlocState(ploc);
return { state };
    },
});
</script>
<style scoped>
#info-container {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
}
#items-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 150px;
    justify-content: space-around;
}
#total-price-container {
    display: flex;
    align-items: center;
    padding: 8px 0px;
    justify-content: space-around;
}
</style>

Как видите, он очень похож на версию React с использованием API композиции.

Composition API

В версии Vue у нас также будут хуки, такие как тот, который управляет подпиской на изменения в состоянии PLoC:

import { Ploc } from "@frontend-clean-architecture/core";
import { DeepReadonly, onMounted, onUnmounted, readonly, Ref, ref } from "vue";
export function usePlocState<S>(ploc: Ploc<S>): DeepReadonly<Ref<S>> {
    const state = ref(ploc.state) as Ref<S>;
const stateSubscription = (newState: S) => {
        state.value = newState;
    };
onMounted(() => {
        ploc.subscribe(stateSubscription);
    });
onUnmounted(() => {
        ploc.unsubscribe(stateSubscription);
    });
return readonly(state);
}

Внедрение зависимости

В приложении React и Vue мы должны создать или повторно использовать структуру PloC для каждого компонента: вариантов использования и репозиториев.

Если эти концепции были определены в базовом пакете, то часть, ответственная за их создание, также может быть в базовом пакете.

На этот раз я статически использую шаблон Service Locator:

function provideProductsPloc(): ProductsPloc {
    const productRepository = new ProductInMemoryRepository();
    const getProductsUseCase = new GetProductsUseCase(productRepository);
    const productsPloc = new ProductsPloc(getProductsUseCase);
return productsPloc;
}
function provideCartPloc(): CartPloc {
    const cartRepository = new CartInMemoryRepository();
    const getCartUseCase = new GetCartUseCase(cartRepository);
    const addProductToCartUseCase = new AddProductToCartUseCase(cartRepository);
    const removeItemFromCartUseCase = new RemoveItemFromCartUseCase(cartRepository);
    const editQuantityOfCartItemUseCase = new EditQuantityOfCartItemUseCase(cartRepository);
    const cartPloc = new CartPloc(
        getCartUseCase,
        addProductToCartUseCase,
        removeItemFromCartUseCase,
        editQuantityOfCartItemUseCase
    );
return cartPloc;
}
export const dependenciesLocator = {
    provideProductsPloc,
    provideCartPloc,
};

Мы также могли бы использовать динамический локатор сервисов вместе с корнем композиции или библиотеку внедрения зависимостей.

В приложении React есть глобальное состояние, которым нужно поделиться, это корзина покупок. Следовательно, CartPloc, который управляет этим состоянием, должен быть доступен для всех компонентов.

Реагировать

В React мы решаем эту проблему с помощью createContext и настраиваемого хука с помощью useContext.

export function createContext<T>() {
    const context = React.createContext<T | undefined>(undefined);
function useContext() {
        const ctx = React.useContext(context);
        if (!ctx) throw new Error("context must be inside a Provider with a value");
        return ctx;
    }
    return [context, useContext] as const;
}
const [blocContext, usePloc] = createContext<CartPloc>();
export const useCartPloc = usePloc;
const App: React.FC = () => {
    return (
        <blocContext.Provider value={dependenciesLocator.provideCartPloc()}>
            <MyAppBar />
            <ProductList />
            <CartDrawer />
        </blocContext.Provider>
    );
};
export default App;

Используя настраиваемый useCartPloc, мы получаем доступ из любого компонента к этому PloC и его состоянию.

Приложение Vue

В Vue мы решаем эту проблему с помощью функции обеспечения.

<template>
    <div id="app">
        <MyAppBar />
        <ProductList searchTerm="Element" />
        <CartSidebar />
    </div>
</template>
<script lang="ts">
import { dependenciesLocator } from "@frontend-clean-architecture/core";
import { defineComponent } from "vue";
import MyAppBar from "./appbar/MyAppBar.vue";
import ProductList from "./products/ProductList.vue";
import CartSidebar from "./cart/CartSidebar.vue";
export default defineComponent({
    name: "App",
    components: {
        ProductList,
        MyAppBar,
        CartSidebar,
    },
    provide: {
        cartPloc: dependenciesLocator.provideCartPloc(),
    },
});
</script>

Позже из любого компонента у нас есть доступ к PLoC и его состоянию, используя:

const cartPloc = inject <CartPloc> (“cartPloc”) as CartPloc;

Исходный код

Исходный код можно найти здесь: frontend-clean-architecture.

Связанные статьи и ресурсы

Выводы

В этой статье мы увидели реализацию чистой архитектуры во внешнем интерфейсе.

У нас есть версия приложения React и Vue, которая повторно использует как можно больше кода между ними и помещает его в основной пакет.

С помощью этого упражнения по наличию основного пакета со всей логикой, отделенной от фреймворка, мы можем оценить мощь, которую чистая архитектура может предложить нам в интерфейсе.

Организация проекта как монорепозитория и наличие основного пакета были необходимы для этого примера, но это не обязательно при разработке приложения на React или Vue.

Тем не менее, это интересное упражнение, заставляющее вас отключиться от инфраструктуры пользовательского интерфейса, поскольку иногда может быть трудно увидеть, что вы связаны, особенно в начале.

Больше контента на plainenglish.io