[Разработка веб-сайтов, JavaScript, ReactJS] Реализация архитектуры Redux на MobX. Часть 2: «Пример на MobX»
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Ссылка на первую часть статьи: «Проблемные места Redux».В этой части я опишу, приблизительно какую архитектуру использую в своих проектах. MobX взят, так как он довольно простой и удобный, из коробки есть готовая реализация паттерна Observer, автоматическая мемоизация и автоматическое обновление компонентов при изменении состояния хранилища.Я много раз читал, как кто-то попробовал MobX, у него код получился запутанным с не контролируемыми изменениями, после чего он продолжил писать на Redux. Для MobX нет рекомендованной архитектуры. Но при использовании и соблюдении в MobX строгой и однообразной (имеется ввиду одинаковой в различных участках проекта) архитектуры, можно получить понятный код с контролируемыми изменениями в сколь угодно большом проекте. Я опишу один из вариантов, как этого добиться. Отмечу, что последние 5 лет я работал только с REST-подобными API, поэтому код в статье заточен под работу с REST API.Будет описано разделение на слои. Моя цель - не показать, как правильно реализовать каждый слой в приложении, а показать, что количество составляющих каждого слоя может быть гораздо меньше, чем в Redux, а их взаимодействие проще.Подход будет рассмотрен на примере простого списка дел.
Код в примерах будет приведен не полностью. Полный пример кода находится в github и в codesanbox.
Структура папок проекта по большей части - Folder-by-feature. Если у вас проекте есть одна общая папка вроде ducks/stores, где находятся все редьюсеры/actions/stores, то структура папок у вас вряд ли хорошо масштабируется и вам стоит обратить внимание на структуру в моем примере. Суть такая, что файлы, которые относятся к конкретной feature/странице, стоит располагать рядом с ней, а не размещать в разных участках проекта.Содержание
- Пример слоя сторов на MobX
- Сервисный слой (API и другие сервисы)
- Пример слоя controller (альтернатива middleware в моем примере)
- Пример инициализации сторов, api и контроллеров
- Пример использования стора и контроллера в компонентах
- Заключение. Схема архитектуры
Пример слоя сторов на MobXКак и в Redux, этот слой не зависит от других слоев.Используется несколько сторов - один для работы со списком, другой для работы с формой, третий для работы с параметрами поиска (фильтрация, сортировка, пагинация). В статье приведен пример только одного стора. Не обязательно так разделять стор для каждой feature пока не будет видно, что от разделения будет польза. Без разделения, в моем случае объявление стора выглядело бы так: "BaseStore<TListItem, TEditItem, TSearchParams>", что как минимум затрудняет читабельность.Для сторов и некоторых других программных сущностей (API, middleware) я буду использовать базовые классы. На github можно заметить, что у меня в папке todos (то есть в папке реализации конкретной страницы/feature) практически нет кода в файлах api.js, controllers.js, stores.js, т.к. общий код вынесен в базовые классы. Конечно, при разрастании кодовой базы, ситуация может измениться.В рассматриваемом подходе не обязательно использовать базовые классы и наследоваться от них, т.к. они могут не подходить для всех случаев. К тому же можно не использовать классы, а делать отдельные функции. Классы здесь удобны тем, что позволяют стандартным механизмом избавиться от дублирования функций с одинаковым кодом и позволяют задать в конструкторе общие зависимости для всех методов.В общем, менее важно, как слои реализованы внутри. Самое главное, что слои API, контроллеров (о них будет рассказано позже), сторов и компонентов отделены друг от друга.Пример стора
// Общие базовые типы
// src/core/types/index.ts
export type ObjectType = Record<string, unknown> | null | undefined;
export type ErrorType = string | ObjectType;
export interface IIdentifiable { id: number; }
// Базовый класс для сторов, хранящих списки объектов
// src/core/store/BaseListStore.ts
import { observable, action, computed, makeObservable } from 'mobx';
import { ErrorType, ObjectType, IIdentifiable } from 'core/types';
export interface IListState<TListItem extends IIdentifiable> {
results: TListItem[];
count?: number; // число элементов на сервере. Нужно для пэйджинга.
isLoading?: boolean;
error?: ErrorType;
}
export default class BaseListStore<TListItem extends IIdentifiable> {
@observable
protected listState: IListState<TListItem> = {
results: [],
};
constructor() {
makeObservable<BaseListStore<TListItem>>(this);
}
@computed
get list(): TListItem[] {
return Array.isArray(this.listState.results)
? this.listState.results
: [];
}
@action
setListState(list: IListState<TListItem>) {
this.listState = list;
}
@action
addToList(item: TListItem) {
this.list.push(item);
}
@action
updateListItem(item: TListItem) {
const foundTodo = this.list.find((i) => item && i.id === item.id);
if (foundTodo && item) {
Object.assign(foundTodo, item);
}
}
...
}
// для удобства экспортируется тип стора
export type BaseListStoreType = BaseListStore<IIdentifiable>;
Далее стор для списка Todo. Пока-что нет необходимости создавать уникальных методов для функционала списка, поэтому вместо наследования можно воспользоваться обобщенным базовым классом BaseListStire<T>.
// src/pages/todos/stores.tsx
import { IIdentifiable } from 'core/types';
export interface ITodoModel extends IIdentifiable {
title: string;
completed: boolean;
}
export type TodoListStoreType = BaseListStore<ITodoModel>;
Сервисный слой (API и другие сервисы)Чтобы избежать дублирования логики и не засорять код контроллеров, в отдельный слой вынесен код для взаимодействия с сервером. В примере используется библиотека axios. Данный слой ничего не знает о других слоях. Он ни в коем случае не должен изменять стор или читать из него. В Redux ничего не сказано про этой слой, но многие создают его, как и я.В моем примере общий код для всех запросов и инициализация axios вынесены в отдельный сервис. Т.к. пример маленький, пользы от этого не видно. Но в большом проекте это с большой вероятностью пригодится в будущем. Например, если потребуется задать общие заголовки или преобразовывать формат всех данных перед отправкой или при получении. В статье в качестве сервисного слоя приведены только api сервисы. Но могут быть и другие сервисы. core/api/apiService.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ObjectType } from '../types';
axios.defaults.baseURL = process.env.REACT_APP_BASE_API_URL;
export type ApiServiceResponseType = Promise<AxiosResponse<any>>;
const apiService = {
get: function (
url: string,
config?: AxiosRequestConfig,
): ApiServiceResponseType {
return axios.get(url, config);
},
post: function (
url: string,
data: ObjectType,
config?: AxiosRequestConfig,
): ApiServiceResponseType {
return axios.post(url, data, config);
},
patch: function (
url: string,
data: ObjectType,
config?: AxiosRequestConfig,
): ApiServiceResponseType {
return axios.patch(url, data, config);
},
delete: function (
url: string,
config?: AxiosRequestConfig,
): ApiServiceResponseType {
return axios.delete(url, config);
},
};
export default apiService;
Далее базовый класс с методами для работы с конкретным маршрутом, использующий apiService. Этот класс тоже часть сервисного слоя.core/api/BaseApi.ts
import apiService from './apiService';
import { IResponseList, IResponseModel, IResponseError } from './types';
import { IIdentifiable, ObjectType } from '../types';
export default class BaseApi<T extends IIdentifiable> {
private readonly _apiUrl: string;
get apiUrl(): string {
return this._apiUrl;
}
constructor(apiUrl: string) {
this._apiUrl = apiUrl;
}
async getList(
params?: ObjectType,
): Promise<IResponseList<T> | IResponseError> {
try {
const ret = await apiService.get(this._apiUrl, { params });
return { results: ret.data || [] };
} catch (error) {
return this.handleError(error);
}
}
async update(
modelData: { id: number },
params?: ObjectType,
): Promise<IResponseModel<T> | IResponseError> {
try {
const ret = await apiService.patch(
`${this._apiUrl}/${modelData.id}`,
modelData,
{ params },
);
return { model: ret.data };
} catch (error) {
return this.handleError(error);
}
}
protected handleError(e): IResponseError {
let message = '';
if (e.response) {
message = e.message;
}
return { isError: true, message };
}
}
export type BaseApiType = BaseApi<IIdentifiable>;
Пример слоя controller (альтернатива Redux middleware в моем примере)В своем коде вместо middleware я буду использовать термин из MVC - Controller. В Redux есть возможность объединять middlewares в цепочки. Т.к. это далеко не всегда нужно, я не стал без необходимости усложнять и не реализовывал у себя в контроллерах объединение действий в цепочки. К тому же, промежуточное ПО можно разместить при получении данных с сервера или при передачи данных из API в контроллер. Так оно будет располагаться в зависимости от своего назначения, а не вперемешку.Основное назначение контроллера в данном подходе - быть посредником между api, стором и компонентом. То есть вызываться через компонент, вызывать api и методы обновления стора, а также содержать в методах логику, для выбора, какой api метод и стор использовать. Не стоит переусложнять бизнес-логикой контроллер. Общую для всех контроллеров бизнес-логику лучше выносить отдельно, как сделано в случае api. В идеале в больших проектах стоит стремиться к активной модели контроллера, но с другой стороны, в небольших проектах это может быть ненужным усложнением.Контроллер используются только в слое View (компоненты) и в других контроллерах. Этот слой зависим от слоя сторов и слоя API.Я использую базовый класс, в котором находятся несколько общих методов для обновления стора и для получения данных с сервера типичными CRUD методами.src/core/controllers/BaseController.ts
import { BaseStoreType } from '../store/BaseStore';
import { SearchParamsStoreType } from '../store/SearchParamsStore';
import { BaseApiType } from '../api/BaseApi';
import IIdentifiable from '../types/IIdentifiable';
import ObjectType from '../types/ObjectType';
import { isIResponseError } from '../api/types';
import { toast } from 'react-toastify';
export default class BaseController {
private readonly _mainStore: BaseStoreType;
private readonly _searchParamsStore: SearchParamsStoreType;
private readonly _api: BaseApiType;
constructor(mainStore: BaseStoreType,
searchParamsStore: SearchParamsStoreType,
api: BaseApiType) {
this._mainStore = mainStore;
this._searchParamsStore = searchParamsStore;
this._api = api;
}
async getList() {
const searchParams = this._searchParamsStore.getSearchParamsMergedToJS();
const response = await this.api.getList(searchParams);
if (isIResponseError(response)) {
toast.error(response.message);
} else {
this.listStore.setListState({
results: response.results,
count: response.count,
});
}
}
async create(modelData: ObjectType) {
const response = await this.api.create(modelData);
if (isIResponseError(response)) {
toast.error(response.message);
} else {
await this.getList(); // for apply filters
}
}
setFilters = (filters: ObjectType) => {
this.searchParamsStore.setFilters(filters);
};
...
}
Пример инициализации сторов, api и controllersДалее пример создания экземпляров классов сторов, api и контроллеров.Чтобы передавать экземпляры сторов и контроллеров в компоненты, а также не мокать импортируемый функционал в тестах, в этом примере я воспользовался контекстом.src/contexts.ts + src/App.tsx
import { createContext } from 'react';
import BaseListStore from 'core/store/BaseListStore';
import BaseEditStore from 'core/store/BaseEditStore';
import SearchParamsStore from 'core/store/SearchParamsStore';
import {
TodoListStoreType,
TodoEditStoreType,
TodoSearchParamsStoreType,
} from './pages/todos/stores';
import { createTodoAPI } from './pages/todos/api';
import BaseController from 'core/сontrollers/BaseController';
import TodoPage from './pages/todos/views/Page';
export interface IStoresContextValue {
todoListStore: TodoListStoreType;
todoEditStore: TodoEditStoreType;
todoSearchParamsStore: TodoSearchParamsStoreType;
}
export const StoresContext =
createContext<IStoresContextValue | null>(null)
as Context<IStoresContextValue>;
export const stores: IStoresContextValue = {
todoListStore: new BaseListStore(),
todoEditStore: new BaseEditStore(),
todoSearchParamsStore: new SearchParamsStore(),
};
export interface IControllersContextValue {
todoController: BaseController;
}
export const ControllersContext =
createContext<IControllersContextValue | null>(null)
as Context<IControllersContextValue>;
export const controllers: IControllersContextValue = {
todoController: new BaseController(
stores.todoListStore,
stores.todoEditStore,
stores.todoSearchParamsStore,
createTodoAPI('/todos'),
),
};
const App = () => {
return (
<div>
<StoresContext.Provider value={stores}>
<ControllersContext.Provider value={controllers}>
<TodoPage />
</ControllersContext.Provider>
</StoresContext.Provider>
</div>
);
};
В данном примере в использовании контекста нет необходимости. Можно было бы экспортировать объект со сторами и объект с контроллерами напрямую, а не через context. Честно говоря, я пока не вижу ситуации, где один из подходов работает, а другой нет.
Писать тесты с подменой сторов и контроллеров можно и без контекста. Например, в случае использования Jest, если у вас есть файл "srс/pageA/myStore.js", то в папке pageA надо создать папку __mocks__ и создать в ней файл myStore.js для использования его в тестах вместо оригинального файла. То есть расположить по такому пути: "srс/pageA/__mocks__ /myStore.js". А в файле с тестом (например: "srс/pageA/__tests__ /MyComponent.js") после import-ов достаточно написать "jest.mock('../myStore');".Пример использования стора и контроллера в компонентахsrc/pages/todos/views/List.jsx
import { useEffect, useContext } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import { observer } from 'mobx-react-lite';
import { ControllersContext, StoresContext } from 'contexts';
import { ITodoModel } from '../stores';
const TodoList = observer(() => {
const { todoListStore } = useContext(StoresContext);
const { todoController } = useContext(ControllersContext);
const handleChange = (item) => {
todoController.update({
id: item.id,
completed: !item.completed,
} as ITodoModel);
};
useEffect(() => {
todoController.getList();
}, []);
return (
<List>
{todoListStore.list.map((item) => (
<ListItem key={item.id} dense button>
...
</ListItem>
))}
</List>
);
});
export default TodoList;
Заключение. Схема архитектурыПолучилась архитектура со следующими составляющими:
- Service (сервисы для работы с API, а также сервисы, в которые вынесен общий функционал для контроллеров)
- Controller (для связи между API, сторами и компонентами)
- Store (для работы с общими данными (состоянием) приложения)
- View (компоненты)
Сравнение ее составляющих с Redux:ReduxServices (опцио-нально)Middle-ware's Action creatorsActionsReducersSelectorsCompo-nentsмой подходServicesControllers с функциями-действиямиStores с функциями, аналогичными сеттерам и геттерам.Compo-nents
Вместо 7-ми видов сущностей, которые нужно постоянно создавать, получилось 4-ре. Масштабируемость, на мой взгляд, примерно такая же.Ниже изображены 2 схемы:
1) Схема зависимостей, отображающая, какие сущности использует такая-то сущность.2) Схема потока данных, отображающая из каких сущностей в какие передаются данные. Под "get" имеется ввиду, что сущность сама запрашивают данные, а под "pass" имеется ввиду, что другая сущность является инициатором передачи данных.
Получившееся похоже на вариацию MV* с добавление стора. Вместо View Model как в MVVM, здесь используется стор, остальное же - обычное MVC.Подобную архитектуру я успешно использую с 2016 года. Я далеко не сразу пришел к тому виду, который описан в статье. Что-то улучшил после предложений других разработчиков в команде. Что-то сам решил изменить. Что-то еще в будущем буду менять.Напоследок рассмотрю несколько ситуаций с использованием описанной архитектуры.1. Что делать, если один стор должен использовать данные другого стора?
Я стараюсь избегать прямой связи одного стора с другим. Вместо этого я передаю эту обязанность контроллеру. В действии контроллера, которое должно обновить первый стор, считываю данные из второго стора и передаю их вместе с остальными данными в первый стор.
2. Что делать, когда наблюдаемые данные одного стора зависят от наблюдаемых данных другого стора и происходит обновление первого стора?
Я стараюсь избегать таких цепочек обновлений и выношу вычисления в контроллер. То есть
из сторов считываю необходимые данные, обрабатываю их, и затем передаю их сторам, использующим эти связанные данные.3. Если в нескольких компонентах нужно вычислить и подписаться на значение, состоящее из данных нескольких сторов, можно вынести вычисление этого значения в отдельную функцию. Можно сделать custom hook или воспользоваться функцией MobX - computed. Спасибо @DmitryKazakov8за его комментарий к предыдущей части статьи! После него решил добавить этот пункт.4. Уменьшение бойлерплейта.
В описанном подходе, если для множества страниц приходиться писать однотипный функционал, можно написать обертку для инициализации и связывания экземпляров контроллеров, сторов, api.
Для примера, черновая версия у меня есть в отдельной ветке:
wrapFeature.ts (создает экземпляры переданных, либо базовых api, stores, controller для одной feature и возвращает их)
Пример использованияВажно не переусердствовать и не писать сложные универсальные решения.Если вы не видите необходимости в использовании context или предпочитаете использовать import/export, то можно еще немного уменьшить количество кода.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, NoSQL, Node.JS] ArangoDB в реальном проекте
- [Разработка веб-сайтов, Программирование, HTML, Лайфхаки для гиков] 5 HTML-трюков, о которых никто не говорит (перевод)
- [CMS, Разработка веб-сайтов, Node.JS] Strapi сохранение файлов на Яндекс Object Storage
- [CSS, JavaScript, HTML] Веб-компоненты проще, чем вы думаете (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, Проектирование и рефакторинг, ReactJS] Фреймворк-независимое браузерное SPA (перевод)
- [Разработка веб-сайтов, PHP, Программирование, Учебный процесс в IT, Карьера в IT-индустрии] Курсы PHP-программирования в Минске. Куда пойти учиться?
- [CSS, JavaScript] Заметки фронтендера #1
- [Разработка веб-сайтов, JavaScript, ReactJS] Нарушает ли React DOM-стандарты?
- [Веб-дизайн, Разработка веб-сайтов, CSS, Программирование] Погружаемся в логические свойства CSS (перевод)
- [Разработка веб-сайтов, JavaScript, ReactJS] Реализация архитектуры Redux на MobX. Часть 1: «Проблемные места Redux»
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_reactjs, #_mobx, #_react, #_redux, #_javascript, #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:21
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Ссылка на первую часть статьи: «Проблемные места Redux».В этой части я опишу, приблизительно какую архитектуру использую в своих проектах. MobX взят, так как он довольно простой и удобный, из коробки есть готовая реализация паттерна Observer, автоматическая мемоизация и автоматическое обновление компонентов при изменении состояния хранилища.Я много раз читал, как кто-то попробовал MobX, у него код получился запутанным с не контролируемыми изменениями, после чего он продолжил писать на Redux. Для MobX нет рекомендованной архитектуры. Но при использовании и соблюдении в MobX строгой и однообразной (имеется ввиду одинаковой в различных участках проекта) архитектуры, можно получить понятный код с контролируемыми изменениями в сколь угодно большом проекте. Я опишу один из вариантов, как этого добиться. Отмечу, что последние 5 лет я работал только с REST-подобными API, поэтому код в статье заточен под работу с REST API.Будет описано разделение на слои. Моя цель - не показать, как правильно реализовать каждый слой в приложении, а показать, что количество составляющих каждого слоя может быть гораздо меньше, чем в Redux, а их взаимодействие проще.Подход будет рассмотрен на примере простого списка дел. Код в примерах будет приведен не полностью. Полный пример кода находится в github и в codesanbox. Структура папок проекта по большей части - Folder-by-feature. Если у вас проекте есть одна общая папка вроде ducks/stores, где находятся все редьюсеры/actions/stores, то структура папок у вас вряд ли хорошо масштабируется и вам стоит обратить внимание на структуру в моем примере. Суть такая, что файлы, которые относятся к конкретной feature/странице, стоит располагать рядом с ней, а не размещать в разных участках проекта.Содержание
// Общие базовые типы
// src/core/types/index.ts export type ObjectType = Record<string, unknown> | null | undefined; export type ErrorType = string | ObjectType; export interface IIdentifiable { id: number; } // Базовый класс для сторов, хранящих списки объектов
// src/core/store/BaseListStore.ts import { observable, action, computed, makeObservable } from 'mobx'; import { ErrorType, ObjectType, IIdentifiable } from 'core/types'; export interface IListState<TListItem extends IIdentifiable> { results: TListItem[]; count?: number; // число элементов на сервере. Нужно для пэйджинга. isLoading?: boolean; error?: ErrorType; } export default class BaseListStore<TListItem extends IIdentifiable> { @observable protected listState: IListState<TListItem> = { results: [], }; constructor() { makeObservable<BaseListStore<TListItem>>(this); } @computed get list(): TListItem[] { return Array.isArray(this.listState.results) ? this.listState.results : []; } @action setListState(list: IListState<TListItem>) { this.listState = list; } @action addToList(item: TListItem) { this.list.push(item); } @action updateListItem(item: TListItem) { const foundTodo = this.list.find((i) => item && i.id === item.id); if (foundTodo && item) { Object.assign(foundTodo, item); } } ... } // для удобства экспортируется тип стора export type BaseListStoreType = BaseListStore<IIdentifiable>; // src/pages/todos/stores.tsx
import { IIdentifiable } from 'core/types'; export interface ITodoModel extends IIdentifiable { title: string; completed: boolean; } export type TodoListStoreType = BaseListStore<ITodoModel>; import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ObjectType } from '../types'; axios.defaults.baseURL = process.env.REACT_APP_BASE_API_URL; export type ApiServiceResponseType = Promise<AxiosResponse<any>>; const apiService = { get: function ( url: string, config?: AxiosRequestConfig, ): ApiServiceResponseType { return axios.get(url, config); }, post: function ( url: string, data: ObjectType, config?: AxiosRequestConfig, ): ApiServiceResponseType { return axios.post(url, data, config); }, patch: function ( url: string, data: ObjectType, config?: AxiosRequestConfig, ): ApiServiceResponseType { return axios.patch(url, data, config); }, delete: function ( url: string, config?: AxiosRequestConfig, ): ApiServiceResponseType { return axios.delete(url, config); }, }; export default apiService; import apiService from './apiService';
import { IResponseList, IResponseModel, IResponseError } from './types'; import { IIdentifiable, ObjectType } from '../types'; export default class BaseApi<T extends IIdentifiable> { private readonly _apiUrl: string; get apiUrl(): string { return this._apiUrl; } constructor(apiUrl: string) { this._apiUrl = apiUrl; } async getList( params?: ObjectType, ): Promise<IResponseList<T> | IResponseError> { try { const ret = await apiService.get(this._apiUrl, { params }); return { results: ret.data || [] }; } catch (error) { return this.handleError(error); } } async update( modelData: { id: number }, params?: ObjectType, ): Promise<IResponseModel<T> | IResponseError> { try { const ret = await apiService.patch( `${this._apiUrl}/${modelData.id}`, modelData, { params }, ); return { model: ret.data }; } catch (error) { return this.handleError(error); } } protected handleError(e): IResponseError { let message = ''; if (e.response) { message = e.message; } return { isError: true, message }; } } export type BaseApiType = BaseApi<IIdentifiable>; import { BaseStoreType } from '../store/BaseStore';
import { SearchParamsStoreType } from '../store/SearchParamsStore'; import { BaseApiType } from '../api/BaseApi'; import IIdentifiable from '../types/IIdentifiable'; import ObjectType from '../types/ObjectType'; import { isIResponseError } from '../api/types'; import { toast } from 'react-toastify'; export default class BaseController { private readonly _mainStore: BaseStoreType; private readonly _searchParamsStore: SearchParamsStoreType; private readonly _api: BaseApiType; constructor(mainStore: BaseStoreType, searchParamsStore: SearchParamsStoreType, api: BaseApiType) { this._mainStore = mainStore; this._searchParamsStore = searchParamsStore; this._api = api; } async getList() { const searchParams = this._searchParamsStore.getSearchParamsMergedToJS(); const response = await this.api.getList(searchParams); if (isIResponseError(response)) { toast.error(response.message); } else { this.listStore.setListState({ results: response.results, count: response.count, }); } } async create(modelData: ObjectType) { const response = await this.api.create(modelData); if (isIResponseError(response)) { toast.error(response.message); } else { await this.getList(); // for apply filters } } setFilters = (filters: ObjectType) => { this.searchParamsStore.setFilters(filters); }; ... } import { createContext } from 'react';
import BaseListStore from 'core/store/BaseListStore'; import BaseEditStore from 'core/store/BaseEditStore'; import SearchParamsStore from 'core/store/SearchParamsStore'; import { TodoListStoreType, TodoEditStoreType, TodoSearchParamsStoreType, } from './pages/todos/stores'; import { createTodoAPI } from './pages/todos/api'; import BaseController from 'core/сontrollers/BaseController'; import TodoPage from './pages/todos/views/Page'; export interface IStoresContextValue { todoListStore: TodoListStoreType; todoEditStore: TodoEditStoreType; todoSearchParamsStore: TodoSearchParamsStoreType; } export const StoresContext = createContext<IStoresContextValue | null>(null) as Context<IStoresContextValue>; export const stores: IStoresContextValue = { todoListStore: new BaseListStore(), todoEditStore: new BaseEditStore(), todoSearchParamsStore: new SearchParamsStore(), }; export interface IControllersContextValue { todoController: BaseController; } export const ControllersContext = createContext<IControllersContextValue | null>(null) as Context<IControllersContextValue>; export const controllers: IControllersContextValue = { todoController: new BaseController( stores.todoListStore, stores.todoEditStore, stores.todoSearchParamsStore, createTodoAPI('/todos'), ), }; const App = () => { return ( <div> <StoresContext.Provider value={stores}> <ControllersContext.Provider value={controllers}> <TodoPage /> </ControllersContext.Provider> </StoresContext.Provider> </div> ); }; Писать тесты с подменой сторов и контроллеров можно и без контекста. Например, в случае использования Jest, если у вас есть файл "srс/pageA/myStore.js", то в папке pageA надо создать папку __mocks__ и создать в ней файл myStore.js для использования его в тестах вместо оригинального файла. То есть расположить по такому пути: "srс/pageA/__mocks__ /myStore.js". А в файле с тестом (например: "srс/pageA/__tests__ /MyComponent.js") после import-ов достаточно написать "jest.mock('../myStore');".Пример использования стора и контроллера в компонентахsrc/pages/todos/views/List.jsx import { useEffect, useContext } from 'react';
import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import { observer } from 'mobx-react-lite'; import { ControllersContext, StoresContext } from 'contexts'; import { ITodoModel } from '../stores'; const TodoList = observer(() => { const { todoListStore } = useContext(StoresContext); const { todoController } = useContext(ControllersContext); const handleChange = (item) => { todoController.update({ id: item.id, completed: !item.completed, } as ITodoModel); }; useEffect(() => { todoController.getList(); }, []); return ( <List> {todoListStore.list.map((item) => ( <ListItem key={item.id} dense button> ... </ListItem> ))} </List> ); }); export default TodoList;
Вместо 7-ми видов сущностей, которые нужно постоянно создавать, получилось 4-ре. Масштабируемость, на мой взгляд, примерно такая же.Ниже изображены 2 схемы: 1) Схема зависимостей, отображающая, какие сущности использует такая-то сущность.2) Схема потока данных, отображающая из каких сущностей в какие передаются данные. Под "get" имеется ввиду, что сущность сама запрашивают данные, а под "pass" имеется ввиду, что другая сущность является инициатором передачи данных. Получившееся похоже на вариацию MV* с добавление стора. Вместо View Model как в MVVM, здесь используется стор, остальное же - обычное MVC.Подобную архитектуру я успешно использую с 2016 года. Я далеко не сразу пришел к тому виду, который описан в статье. Что-то улучшил после предложений других разработчиков в команде. Что-то сам решил изменить. Что-то еще в будущем буду менять.Напоследок рассмотрю несколько ситуаций с использованием описанной архитектуры.1. Что делать, если один стор должен использовать данные другого стора? Я стараюсь избегать прямой связи одного стора с другим. Вместо этого я передаю эту обязанность контроллеру. В действии контроллера, которое должно обновить первый стор, считываю данные из второго стора и передаю их вместе с остальными данными в первый стор. 2. Что делать, когда наблюдаемые данные одного стора зависят от наблюдаемых данных другого стора и происходит обновление первого стора? Я стараюсь избегать таких цепочек обновлений и выношу вычисления в контроллер. То есть из сторов считываю необходимые данные, обрабатываю их, и затем передаю их сторам, использующим эти связанные данные.3. Если в нескольких компонентах нужно вычислить и подписаться на значение, состоящее из данных нескольких сторов, можно вынести вычисление этого значения в отдельную функцию. Можно сделать custom hook или воспользоваться функцией MobX - computed. Спасибо @DmitryKazakov8за его комментарий к предыдущей части статьи! После него решил добавить этот пункт.4. Уменьшение бойлерплейта. В описанном подходе, если для множества страниц приходиться писать однотипный функционал, можно написать обертку для инициализации и связывания экземпляров контроллеров, сторов, api. Для примера, черновая версия у меня есть в отдельной ветке: wrapFeature.ts (создает экземпляры переданных, либо базовых api, stores, controller для одной feature и возвращает их) Пример использованияВажно не переусердствовать и не писать сложные универсальные решения.Если вы не видите необходимости в использовании context или предпочитаете использовать import/export, то можно еще немного уменьшить количество кода. =========== Источник: habr.com =========== Похожие новости:
Разработка веб-сайтов ), #_javascript, #_reactjs |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:21
Часовой пояс: UTC + 5