Pull to refresh
5
0.2
Константин Роман @nihil-pro

User

Send message

У Вас в руках появился молоток, и теперь все вокруг кажется гвоздями.

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

По 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 в которою передаем вложенные страницы.
Это также позволяет делать что-то такое:

const isExperimental = localStorage.getItem('layout') === 'new';
return isExperimental 
  ? <NewUsersPage pages={nestedPages} router={router} />
  : <OldUsersPage pages={nestedPages} router={router} />;

Во фреймворках обычно контроллеры не имеют связи с маршрутом.

// классический контроллер в 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 function getUsers() {
  try {
    const res = await fetch('/api/users');
    if (!res.ok) throw new Error(`${res.status}`);
    return await res.json();
  } catch (e) {
    logger.error('Users fetch failed', e);
    throw e; // пробрасываем дальше
  }
}

async function createUser(data) {
  try {
    const res = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(data)
    });
    if (!res.ok) throw new Error(`${res.status}`);
    return await res.json();
  } catch (e) {
    logger.error('User creation failed', e);
    throw e;
  }
}

А вы действительно так код писали?

не менее, а более безопасно) в случае чего ошибки вывалятся нормально, а не uncaught (in promise)

class ModuleB {

  constructor() {
    this.init()
  }

  async init() {
    this.data = await fetch('someApi');
    throw new Error('surprise')
  }
}

// moduleA.js
import { ModuleB } from './moduleB'

class ModuleA {
  moduleB = new ModuleB();

  // вы, видимо, пропустили
  constructor() {
    this.init()
  }

  async init() {
    this.data = await fetch('someApi')
  }
}

new ModuleA()
Uncaught (in promise) Error: surprise

Вы просто создаете floating promises.

Ну и в чем смысл этих await-ов, если в итогде вы пришли к моему первому примеру, только менее безопасно?

Чтобы эмулировать реальное поведение программы с использованием 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 выполняются паралельно
async function doWork1() {
  return new Promise(resolve => {
    setTimeout(resolve, 3000)
  })
}

async function doWork2() {
  return new Promise(resolve => {
    setTimeout(resolve, 3000)
  })
}

async function main() {
  await doWork1()
  await doWork2()
}

const t1 = performance.now()
await main()
const t2 = performance.now();
console.log('script took', t2-t1, 'ms')
script took 6002.199999988079 ms

Против:

function doWork1() {
  return new Promise(resolve => {
      setTimeout(resolve, 3000)
  })
}

function doWork2() {
  return new Promise(resolve => {
      setTimeout(resolve, 3000)
  })
}

function main() {
  return Promise.all([doWork1(), doWork2()])
}

const t1 = performance.now()
main().then(() => {
  const t2 = performance.now();
  console.log('script took', t2-t1, 'ms')
})
script took 3002 ms

Чем только люди не занимаются, лишь бы не использовать async/await

Думают, наверное.

async/await превращает асинхронный код – в сихронный.

Да, просто если хочется видеть сами вызовы .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 по странице, а режим чтения не выдает какие-то иероглифы. Я такой победой был бы неудовлетворен ¯\_(ツ)_/¯

А если играть по честному, без виртуализации?

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

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

Information

Rating
2,695-th
Registered
Activity