У Вас в руках появился молоток, и теперь все вокруг кажется гвоздями.
Кажется что в статье моя позиция достаточно обоснована. Я не слепо пытаюсь следовать каким-то принципам, а описываю реальные проблемы. Если для тестирования отдельного компонента мне нужно запускать приложение с корня, настраивать контексты и т.д., то это мне создает реальные проблемы.
По Dependency Inversion верхнеуровневый модуль здесь это react-router.
Модуль не становится верхнеуровневым, только из-за того, что вы физически расположили выше в иерархии ваших компонентов. В классической чистой архитектуре, верхнеуровневый модуль это бизнес-логика.
Вы пытаетесь написать свой роутер. У которого своим "правильным" образом реализован DI, OCP и тд. И уже по результатам видно, что прямо кардинально лучше и чище конфигурация роутинга не стала.
Реализация своего роутера это следствие того, чего я пытаюсь достичь. Из-за компонентной модели реакта, это действительно кажется необычным. Суть же в другом. С помощью этого достигается то, что реальный компонент отвечающий за отображение, например, пользователя, вообще не знает о существовании роутера и о том, что мы приняли решение отобразить его в браузере по ссылке /users/1, мы это контролируем в слое выше, в данном случая на основе pathname. Можно себе представить другой интерфейс, например чат-бот, и тогда функцию pathname будет выполнять запрос пользователя. Компоненты типа Page это способ зарегистрировать контроллеры которые декларируют какие пути они обрабатывают, а обработав пути вызывают соответствующие сервисы.
Спасибо за комментарий, попробую ответить также развернуто.
Само по себе это врядли нарушение. Все зависит от принятых на проекте паттернов. Например, находясь в слайсе app в FSD он и должен зависеть от низкоуровневых модулей. И нарушение принципа тогда не будет.
Если вы в проекте придумали свои паттерны, следуете им, для вас они понятны и приемлемы, то вы, безусловно, правы. В статье речь про принципы SOLID и Clean Architecture, а они как раз нарушаются.
Опять же, если это у нас посредник, который обобщает в себе настройку разных элементов, то он и должен изменяться.
Это не посредник, а сервис-локатор. Кроме того, это нарушает OCP. В моей реализации новая страница сама включается в систему через автодискавери, как в бэкенде (например, FastAPI с автозагрузкой роутов), а не через внесение изменений в роутер.
Тут опять можно обратиться к предыдущему пункту. То, что посредник настраивает и вызывает представления не является каким-то криминалом.
Иными словами данный модуль является местом инкапсулирующим способ взаимодействия множества объектов. Именно тут слабо связные объекты настраивают друг для друга.
Эти объекты слабо связаны друг с другом, и очень жестко с роутером.
Почему? Вы даже сами создали отдельный элемент <UserSettings />
Так же можете его создать в любом другом месте и тестировать совершенно независимо от всего остального.
Это не так. Чтобы протестировать этот компонент с react-router, нужно смоделировать весь стек роутинга: RouterProvider, createBrowserRouter и т.д. Если ваш компонент использует useMatch, например, то без провайдера он просто упадет с ошибкой.
Вообще, сейчас Реакт Роутер можно использовать по разному. И вы показали только один способ.
В некотором смысле да, но любой способ требует обернуть приложение в RouterProvider.
Получается следующее, у вас некоторая сущность UserSettings во первых, занимается не только собственной поставленной задачей вывода <div>Settings</div>, но еще и обслуживает задачи маршрутизации. Что очень похоже на наружение принципа единственной ответственности. Вот для тестирования такого UserSettings действительно нужно поднимать обертку обеспечивающую маршрутизацию. Что вы в тестах и делаете.
Тут могу ответить цитатой из статьи: Роль Page-компонента – быть контроллером. Он определяет с помощью router.match(), должен ли «обработать этот запрос» (текущий URL), и если да, то делегирует выполнение бизнес-логики и рендеринг нижележащим компонентам (PageImpl). Эти нижележащие компоненты и выступают в роли сервисов, непосредственно занимаясь получением данных и их преобразованием в UI (рендером).
Так получилось, что в реакте у нас все компонент, но если абстрагироваться и вспомнить что это в первую очередь функция, то все встает на свои места.
А во вторых, в данном примере используете хардкод маршрута. Т.е. даже если не меняется задача, которую выполняет UserSettings вам придется лезть и менять этот файл, если поменяются марштуры.
Конечно, это же просто пример. Хардкод пути в match, ничем не хуже хардкода пути в <Route path="..." />, и ничто не мешает вынести его в константу.
Данный index.ts как раз и является аналогом изначального файла роутера. Он так же зависим от всех "ниже лежащих" модулей и конфигурирует их зависимости.
В некотором смысле да, и поэтому ниже предлагается решение. Но даже без этого решения есть разница. Здесь App импортирует только модули первого уровня (Home, Users), но не UsersSettings, UserProfile.
Если честно, я не очень понимаю что тут происходит. Например, зачем вам отдельный RealUsersPage? Ваш User в котором захардкожен маршрут, имеет так же захардкоженный RealUsersPage. Иными словами, вы не можете к данном маршруту привязать другой RealUsersPage. Поэтому существование отдельного RealUsersPage просто бессмысленно.
Page интерфейс. App рендерит верхний модуль Users, но у Users есть вложенные страницы, которые он принимает пропсами. Можно передать их из App, но тогда App начинает зависеть от внутренней реализации Users. В этом примере мы из Users возвращаем другую реализацию Users в которою передаем вложенные страницы. Это также позволяет делать что-то такое:
Во фреймворках обычно контроллеры не имеют связи с маршрутом.
// классический контроллер в Java
@RequestMapping("instances")
public class InstanceController {
@GetMapping("{id}")
public InstanceDto getById(@PathVariable UUID id) {
return service.get(id);
}
}
Не понял. Что не так? Вам не понравилось что index делает импорт страниц и вы сделали их реимпорт из другого места? Но прямо имопорт страниц из модуря pages никуда не делся. У вас index.ts как зависел от этих модулей так и зависит, только теперь добавилась транзитивная зависимость.
Это не совсем так. Было так:
import { HomePage } from '@/pages/home';
import { UsersPage } from '@/pages/users';
Стало так:
import * as Pages from '@/pages';
То есть, теперь мы зависим от интерфейса ESM модуля, доступного по алиасу pages. Это можно читать как:
type Pages = Record<string, Page>
То, что вы написали - это называется вручную, а не автоматически. То, что вы вручную добавляете в один файл, а не в другой дело не меняет.
Меняет. В моем случае добавление новой страницы выглядит так: Создать в директории pages новую страницу, включить ее в index.ts. В случае с react-router тоже самое + импортировать ее в router.
Какой композиции? Вы реэкспорт из index файла называете композицией?
render(
<App
router={router}
// тут будет ошибка если в экспорт попало не то что нужно
pages={pages}
/>)
Вот в этом плане вообще ничего не изменилось, по моему.
Изменилось. Нам больше не нужно редактировать файл App.tsx при добавлении новой страницы.
Я посмотрел финальный результат.
Там так все наворочено, что разобраться просто читая сверху вниз довольно трудно.
В этом и смысл. Это «децентрализованный» роутер, у него нет файла конфигурации с описанием роутов, а пример демонстрирует, что даже в запутанном графе компонентов все роуты корректно определяются и регистрируются, в том числе относительные, с учетом скоупа.
Чтобы эмулировать реальное поведение программы с использованием async/await как предлагает @Sirion. Promise.all никак это проблему не решает, если только мы не найдем способ собрать в одном месте все функции из разных модулей которые стартуют при старте программы, и все их положить в Promise.all.
В реальности это скорее невозможно, а без async/await все достаточно просто:
class ModuleB {
constructor() {
this.init()
}
init() {
fetch('someApi')
.then(data => this.data = data);
}
}
// moduleA.js
import { ModuleB } from './moduleB'
class ModuleA {
moduleB = new ModuleB();
init() {
fetch('someApi')
.then(data => this.data = data);
}
}
new ModuleA()
// ModuleB.init и ModuleA.init выполняются паралельно
Да, просто если хочется видеть сами вызовы .setAttribute и .removeAttributе и т.п., то можно и так
А что еще вы хотите видеть, а самое главное зачем?
Класс HTMLElement наследует класс Element. И в одном и в другом примерно два-три свойства, все остальное пара getter/setter (за исключением readonly свойств, там только getter), следовательно у вас уже есть механизм перехвата чтения/записи без необходимости проксировать
class MyHTMLElement extends HTMLElement {
get innerText() {
// кто-то прочитал innerText
return super.innerText
}
set innerText(value) {
// кто-то хочет зменить innerText
return super.innerText = value;
}
}
Конечно браузеры не могут полностью защитится от таких попыток влезть туда, куда не стоит, но в этом случае они довольно строго предупреждают:
class MyHTMLElement extends HTMLElement {
constructor() {
super();
return new Proxy(this, {});
}
}
TypeError: custom element constructors must call super() first and must not return a different object
TypeError: Failed to execute 'createElement' on 'Document': The result must implement HTMLElement interface
То, как вы попытались обойти это ограничение, представляет интерес только для инженеров которые отвечают за реализацию этих ограничений, чтобы они попытались закрыть и эту брешь.
Только зачем Proxy, если у HTMLElement уже есть методы setAttribute, removeAttributе которые можно переопределять + есть attributeChangedCallbak + MutationObserver чтобы отслеживать вложенные элементы? HTMLElement это очень большой объект, и его проксирование это очень дорогая операция.
Громкий заголовок. Посредственный текст, вообще заголовку не соответствующий. Десятки плюсов в первые же минуты публикации. Для чего вы писали, что за парад тщеславия?
Твой фреймворк самый быстрый, даже быстрее ванильного HTML/JS. Правда ломает поиск по странице, режим для чтения и кто его знает что еще. Но самый лучший.
К этому самому лучшему фреймворку мне нужно дополнительно прикрутить то, что не должно было сломаться – поиск. Поиск, который доступен в браузере по умолчанию.
Этот прикрученный сбоку поиск лучше чем, встроенный в браузер.
Громкое заявление, но мой предыдущий опыт подсказывает что это не так. А выяснять что именно в нем поломано, чтобы он стал «лучшим», мне как-то не хочется. Да и прикручивать его вообще.
Но это мы отвлеклись. Я лишь хотел сказать, что если гонка на 1 круг, и все его честно бегают, а ты пересекаешь стадион и приходишь к финишу первым, то пришел ты конечно первым, но не победил.
Ну если чисто чтобы ачивку «я первый» получить, то да, это победа. С другой стороны, у других решений в этом списке работает, например, CTRL + F по странице, а режим чтения не выдает какие-то иероглифы. Я такой победой был бы неудовлетворен ¯\_(ツ)_/¯
Разумеется. Как раз благодаря этому, всяких кнопок/рычагов стало меньше. Нужно также иметь ввиду, что сами самолеты стали гораздо сложнее, вместо условного датчика давления масла, они теперь анализируют чуть ли не химический состав воздушного потока. То есть, при том, что сами самолеты стали сложнее, приборная панель упростилась.
Кажется что в статье моя позиция достаточно обоснована. Я не слепо пытаюсь следовать каким-то принципам, а описываю реальные проблемы. Если для тестирования отдельного компонента мне нужно запускать приложение с корня, настраивать контексты и т.д., то это мне создает реальные проблемы.
Модуль не становится верхнеуровневым, только из-за того, что вы физически расположили выше в иерархии ваших компонентов. В классической чистой архитектуре, верхнеуровневый модуль это бизнес-логика.
Реализация своего роутера это следствие того, чего я пытаюсь достичь. Из-за компонентной модели реакта, это действительно кажется необычным. Суть же в другом. С помощью этого достигается то, что реальный компонент отвечающий за отображение, например, пользователя, вообще не знает о существовании роутера и о том, что мы приняли решение отобразить его в браузере по ссылке /users/1, мы это контролируем в слое выше, в данном случая на основе pathname. Можно себе представить другой интерфейс, например чат-бот, и тогда функцию pathname будет выполнять запрос пользователя.
Компоненты типа
Page
это способ зарегистрировать контроллеры которые декларируют какие пути они обрабатывают, а обработав пути вызывают соответствующие сервисы.Спасибо за комментарий, попробую ответить также развернуто.
Если вы в проекте придумали свои паттерны, следуете им, для вас они понятны и приемлемы, то вы, безусловно, правы. В статье речь про принципы SOLID и Clean Architecture, а они как раз нарушаются.
Это не посредник, а сервис-локатор. Кроме того, это нарушает OCP.
В моей реализации новая страница сама включается в систему через автодискавери, как в бэкенде (например, FastAPI с автозагрузкой роутов), а не через внесение изменений в роутер.
Эти объекты слабо связаны друг с другом, и очень жестко с роутером.
Это не так. Чтобы протестировать этот компонент с react-router, нужно смоделировать весь стек роутинга:
RouterProvider
,createBrowserRouter
и т.д. Если ваш компонент использует useMatch, например, то без провайдера он просто упадет с ошибкой.В некотором смысле да, но любой способ требует обернуть приложение в RouterProvider.
Тут могу ответить цитатой из статьи:
Роль Page-компонента – быть контроллером. Он определяет с помощью
router.match()
, должен ли «обработать этот запрос» (текущий URL), и если да, то делегирует выполнение бизнес-логики и рендеринг нижележащим компонентам (PageImpl). Эти нижележащие компоненты и выступают в роли сервисов, непосредственно занимаясь получением данных и их преобразованием в UI (рендером).Так получилось, что в реакте у нас все компонент, но если абстрагироваться и вспомнить что это в первую очередь функция, то все встает на свои места.
Конечно, это же просто пример. Хардкод пути в
match
, ничем не хуже хардкода пути в<Route path="..." />
, и ничто не мешает вынести его в константу.В некотором смысле да, и поэтому ниже предлагается решение. Но даже без этого решения есть разница. Здесь App импортирует только модули первого уровня (Home, Users), но не UsersSettings, UserProfile.
Page интерфейс. App рендерит верхний модуль Users, но у Users есть вложенные страницы, которые он принимает пропсами. Можно передать их из App, но тогда App начинает зависеть от внутренней реализации Users. В этом примере мы из Users возвращаем другую реализацию Users в которою передаем вложенные страницы.
Это также позволяет делать что-то такое:
Это не совсем так. Было так:
Стало так:
То есть, теперь мы зависим от интерфейса ESM модуля, доступного по алиасу pages. Это можно читать как:
Меняет. В моем случае добавление новой страницы выглядит так: Создать в директории pages новую страницу, включить ее в index.ts. В случае с react-router тоже самое + импортировать ее в router.
Изменилось. Нам больше не нужно редактировать файл App.tsx при добавлении новой страницы.
В этом и смысл. Это «децентрализованный» роутер, у него нет файла конфигурации с описанием роутов, а пример демонстрирует, что даже в запутанном графе компонентов все роуты корректно определяются и регистрируются, в том числе относительные, с учетом скоупа.
Еще раз спасибо за развернутый комментарий.
А вы действительно так код писали?
Вы просто создаете floating promises.
Ну и в чем смысл этих
await
-ов, если в итогде вы пришли к моему первому примеру, только менее безопасно?Чтобы эмулировать реальное поведение программы с использованием async/await как предлагает @Sirion.
Promise.all
никак это проблему не решает, если только мы не найдем способ собрать в одном месте все функции из разных модулей которые стартуют при старте программы, и все их положить вPromise.all
.В реальности это скорее невозможно, а без async/await все достаточно просто:
Против:
Думают, наверное.
async/await
превращает асинхронный код – в сихронный.А что еще вы хотите видеть, а самое главное зачем?
Класс HTMLElement наследует класс Element. И в одном и в другом примерно два-три свойства, все остальное пара getter/setter (за исключением readonly свойств, там только getter), следовательно у вас уже есть механизм перехвата чтения/записи без необходимости проксировать
Конечно браузеры не могут полностью защитится от таких попыток влезть туда, куда не стоит, но в этом случае они довольно строго предупреждают:
То, как вы попытались обойти это ограничение, представляет интерес только для инженеров которые отвечают за реализацию этих ограничений, чтобы они попытались закрыть и эту брешь.
Только зачем Proxy, если у HTMLElement уже есть методы setAttribute, removeAttributе которые можно переопределять + есть attributeChangedCallbak + MutationObserver чтобы отслеживать вложенные элементы? HTMLElement это очень большой объект, и его проксирование это очень дорогая операция.
Осталось выяснить зачем нам эта информация.
Гипер троллинг ))
Громкий заголовок. Посредственный текст, вообще заголовку не соответствующий. Десятки плюсов в первые же минуты публикации. Для чего вы писали, что за парад тщеславия?
Вот эту еще надо))
Твой фреймворк самый быстрый, даже быстрее ванильного HTML/JS.
Правда ломает поиск по странице, режим для чтения и кто его знает что еще. Но самый лучший.
К этому самому лучшему фреймворку мне нужно дополнительно прикрутить то, что не должно было сломаться – поиск. Поиск, который доступен в браузере по умолчанию.
Этот прикрученный сбоку поиск лучше чем, встроенный в браузер.
Громкое заявление, но мой предыдущий опыт подсказывает что это не так. А выяснять что именно в нем поломано, чтобы он стал «лучшим», мне как-то не хочется. Да и прикручивать его вообще.
Но это мы отвлеклись. Я лишь хотел сказать, что если гонка на 1 круг, и все его честно бегают, а ты пересекаешь стадион и приходишь к финишу первым, то пришел ты конечно первым, но не победил.
Ну если чисто чтобы ачивку «я первый» получить, то да, это победа. С другой стороны, у других решений в этом списке работает, например, CTRL + F по странице, а режим чтения не выдает какие-то иероглифы. Я такой победой был бы неудовлетворен ¯\_(ツ)_/¯
А если играть по честному, без виртуализации?
Плюсы к этому посту уже никогда не догонят минусы. Рано или поздно автор его удалит, потому что кликбейтная ерунда всегда будет утопать в минусах.
Можно и так
Разумеется. Как раз благодаря этому, всяких кнопок/рычагов стало меньше. Нужно также иметь ввиду, что сами самолеты стали гораздо сложнее, вместо условного датчика давления масла, они теперь анализируют чуть ли не химический состав воздушного потока. То есть, при том, что сами самолеты стали сложнее, приборная панель упростилась.