В целом к SW отношусь осторожно — недостатки очевидны ("Для её эффективного использования придётся пройти тернистый путь из квестов, связанных с особенностями жизненного цикла воркеров, неполной поддержкой браузерами, проблемами с политикой кэширования и попутными побочными эффектами (устаревшее содержимое кэша браузера или, к примеру, сломанные ссылки)"). А единственный плюс по сравнению со стандартным браузерным кешированием — возможность работы приложения в оффлайне, что конкретно для сервиса ипотеки — лишнее, т.к. пользователь ожидает интерактив (чаты, актуальные данные, услуги) и может нарушиться безопасность (авторизация в оффлайне не работает). Также процесс обновления клиентских файлов, если апдейт приложения произошел когда юзер был в оффлайне, потребует дополнительного кода.
Есть ли реальная польза в этом проекте от PWA, или только значительное усложнение поддержки?
Конечно, это с вариантом без кеширования. Не знаю, как сейчас, но тогда мне не удалось подружить jade.compileFile('./templates/foo.jade', { cache: true }); и передачу кастомных параметров на каждый запрос, то есть было впечатление, что с кешем он просто формирует набор готовых строк, и при новых параметрах рендерит заново.
Также первичный рендер был значительно медленнее, поэтому детально разбираться я не стал — возможно, тут мое мнение о Jade некорректное и можно добиться лучших результатов.
Лучше использовать не дату сборки как идентификатор версии, а contenthash — хэш-строку на основе анализа содержимого файла, либо commithash — хэш последнего коммита. Это поможет избежать лишних срабатываний (при использовании балансера или пересборок).
Также рекомендую включать комментарий в каждый из итоговых js и css файлов с версией, пример для Webpack:
/**
* @docs: https://webpack.js.org/plugins/banner-plugin
*
*/
import webpack from 'webpack';
import pkg from '../../package.json';
import { env } from '../../env';
export const pluginBanner: webpack.WebpackPluginInstance = new webpack.BannerPlugin({
banner: `@env ${env.NODE_ENV}:${pkg.version} @commit ${env.GIT_COMMIT}`,
entryOnly: false,
});
Это позволит осуществлять дебаг без открытия доп. файла, узнавать у клиентов версии загруженных ресурсов (для решения проблем с кэшем, особенно актуально при использовании Service Worker), решить проблему с асинхронной загрузкой чанков (в случае, если их имена не модифицируются — можно в script.onload проверять совпадение версии в комментарии чанка и версии основного файла).
По поводу потери данных — можно определить интерактивные части (формы) и при обновлении версии фронтенда стор сохранять в Local Storage и восстанавливать после перезагрузки. Если меняется формат данных в сторе — то соответственно нужна схема миграции. Если компания нацелена на создание качественного продукта, это все несложно внедрить.
Да, и если есть SSR, то посылать информацию об обновлении файлов можно по сокету с ноды, а не методом опроса раз в минуту — выглядеть будет приятней, лишние запросы уйдут.
Когда-то в не столь бородатые времена (2015-2016) использовал Marko 2-3 для шаблонизации на ноде, важны были возможность выделять кастомные компоненты и максимальная скорость работы.
Благодаря преобразованию шаблонов в чистые функции скорость была просто ошеломительной, фактически соответствуя скорости работы самой ноды. Делал сравнения с несколькими имевшимися тогда шаблонизаторами типа Pug, Jade — разрыв по скорости ответа и потреблению памяти исчислялся десятками раз, а так как проект был хайлоадом, Marko стал спасением. Да и синтаксис довольно приятный, и локализацию легко встроить, и кастомизировать рендеринг не сложно. Это все к вопросу, где он может быть востребован — для render-сервера, который принимает параметры из других сервисов и отдает локализованный шаблонизированный html — подходит отлично. А как фреймворк не пробовал использовать, тут явно другие лидеры
Вспоминается проект из нескольких ифреймов по вынужденной причине, т.к. контент располагался на разных доменах и не мог работать с одного. В этом кроссфреймовом протоколе пришлось делать массу методов — для контроля отображения (мобильный, десктопный, версии для определенных устройств, тема), локализации, синхронизации событий (клики по управляющим элементам в верхнеуровневых фреймах передавать в дочерние и наоборот, т.е. двусторонний эвент-биндинг), синхронизации роутинга и состояния… Все это требовало значительного количества времени на поддержку, в десятки раз большего, чем в монолите с единым состоянием. Еще и с пробросом куки проблемы, с безопасностью (каждый метод нужно подписывать и строго определять, чтобы сторонние ресурсы не могли встроить себе и вызывать все что угодно, а это было критично для проекта — платежная система).
Я к тому, что можно, если нужно — но лучше не связываться.
Ну вот, кто-то начинает триггерить фазу "освобождение от иллюзий" по хукам. По поводу "В текущем ландшафте JavaScript нет ничего подобного" — хук useEffect как паттерн это стандартная асинхронщина через event loop:
let prevMyProp = null;
function Component({ myProp }) {
setTimeout(() => {
if (prevMyProp === myProp) return;
1 + 1;
prevMyProp = myProp;
}, Component.didRenderTimeout) // по большей части 0
// то же самое хуком
React.useEffect(() => {
1 + 1;
}, [myProp])
return <div />
}
Это превращает чистые синхронные функции в смесь синхронного и асинхронного кода с сайд-эффектами. В классовом подходе, несмотря на необходимость дубляжа в CDM / CDU, это вынесено в явные методы жизненного цикла и понимается проще, хотя комбинируется чуть сложнее.
Спасибо за пояснение, действительно я сознательно сравниваю все паттерны, которые когда-либо использовал, с чем-то новым, чтобы понять выгоду. Для меня цели архитектуры — комфорт в разработке бизнес-функционала, минимизация возможных ошибок, простота масштабирования и возможность легкой интеграции сторонних решений. С этого угла зрения, к сожалению, полностью фрактальная архитектура не подходит.
А как идея — интересно. Если абстрагироваться от всплытия по yield (это ключевая фича, как понимаю, но все же) семантически подход очень напомнил effector, когда на каждый тип данных формируется локальный стор с методами для изменения
export function newUser() {
const Name = fraction('John')
const Age = fraction(33)
const handleNameChange = (e) => Name.use(e.target.value)
const handleAgeChange = (e) => Age.use(parseInt(e.target.value))
while (true) {
yield (
<NameInput onChange={handleNameChange} defaultValue={yield* Name} />
<AgeInput onChange={handleAgeChange} defaultValue={yield* Age} />
)
}
}
// vs
const increment = createEvent('increment')
const decrement = createEvent('decrement')
const resetCounter = createEvent('reset counter')
const counter = createStore(0)
.on(increment, state => state + 1)
.on(decrement, state => state - 1)
.reset(resetCounter)
const App = () => {
const value = useStore(counter)
return (
<>
<div>{value}</div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={resetCounter}>reset</button>
</>
)
}
Для крупных приложений такие подходы неудобны, в реализациях, которые видел, постепенно все равно код стремится к формированию более крупных и независимых от компонентов состояний, абстрагированию от библиотеки для реализации потоков данных и минимизации кода для проброса в видимые части интерфейса. Абстрагироваться же от yield-перехватчиков на разных уровнях не получится, все должно быть в этой парадигме. А это быстро приведет к устареванию подхода и через какое-то время к полному переписыванию на более гибкие рельсы и на новые технологии (во фронтенде значимые изменения часты).
Вот такой вот прагматичный подход, тоже не в обиду, но я так вижу)
В моей же схеме данные от User лежали бы в общедоступном хранилище, с ними удобно работать, нормализовывать, мутировать, создавать сложные селекторы из композиции различных параметров, сериализовывать в json-структуры, использовать для общения с другими сервисами по АПИ.
А ваш подход скорее из мира, когда данные хранились в дата-атрибутах DOM, постоянно извлекались и сериализовывались-десериализовывались, и так же работать с постоянными yield для извлечение набора актуальных данных — это затратно. Код будет погрязать в бесконечных
Не очень понял про то, как вы хотите одну сущность переводить одновременно в json и html — то есть в объектах описывать все возможные комбинации раскладки и атрибуты? Вроде нет, в ваших примерах на гитхабе используются вполне классический JSX в xml-формате, только с очень сложным извлечением данных.
Что-то почитал репозитории и, кажется, толковую дискуссию мы не создадим. Мне банально непонятен весь этот звездочный код и его смысл
const Counters = fractal(async function* _Counters() {
yield* TODO_MODE(TodoMode.Service)
while (true) {
let active = 0
let completed = 0
for (const Todo of yield* Todos) {
const { Done } = (yield* Todo) as TodoService
;(yield* Done) ? completed++ : active++
}
yield { completed, active }
}
})
Тоже много лет назад писал подобные системы, полные функциональщины и скрытых слоев, с привкусом Алохоморы при выполнении. Но потом дорос до понимания, что лучший код — максимально простой и понятный, так как работаю в корпоративных продуктах, где это — ключ к успеху. Бывает, приходят ребята с очень нестандартными подходами, но их код не понимают и приходится переписывать после их ухода. В общем, не вижу пользы от вашего подхода, кроме как для саморазвития)
Вы, похоже, говорите о приложениях, в которых не используется js-хранилище с данными, а все они содержатся в атрибутах дом-дерева? И для операции с элементами вызываются getElementById, а затем в него записываются значения и дата-атрибуты? Да, подобная схема используется в крошечных простых приложениях, и все, о чем мы тут говорим, там не нужно.
Разговор о том, что первично, тем более если вы сравниваете с концепцией "начало начал не имеет начала, безначальность бесконечна" (фрактал), был бы не особо уместен. В моем понимании это лишь одна из составляющих — движение есть следствие взаимодействия других сил, и для меня оно не первично.
Не знаю, как вы сделали вывод, что я отрицаю структуру в интерфейсах — но это другой слой, который не относится к данным, и представлен в виде иерархичного DOM-дерева, создаваемого опять же иерархичной композицией реакт-компонентов. Прокидывать же в инпут данные через всех родителей, а сигналы от него собирать по всей цепочке — это и есть ограничение, заставляющее "учитывать везде предельную скорость взаимодействия", портальность снимает эту проблему. Но она присутствует в цепочках react props, когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться, а в предлагаемой "фрактальной" архитектуре, похоже, именно так.
А вот неплохо, радует, что опыт познания мира начинает проникать в программирование. Тоже потихоньку пишу статью со своим симбиозом, но в основе не "всплытие" данных по дереву (как если смотреть на эту фрактальную картину сбоку), а единое глобальное состояние с доступом в любую точку. В материальном мире мы работаем с неэффективными передатчиками — свет, волны — которые обладают ограниченной скоростью и не позволяют получать доступ к краю Вселенной мгновенно, но в программировании можно обойти это ограничение и каждый элемент связывать "порталом" с глобальным стейтом:
@portal
class Input {
render() {
const { store } = this.portal;
return <input value={store.any.data.from.the.universe} />;
}
}
Этот же компонент имеет моральное и фактическое право менять глобальный стейт, как человек может шевелиться, меняя глобальное состояние Вселенной, при этом оно всегда остается цельным и согласованным.
То есть относительно ваших идей я предлагаю не пробираться сквозь фракталы для доступа к компонентам через длинную цепочку (под капотом) вызовов, а обращаться к любому объекту напрямую, будто до него рукой подать. Надо до Сириуса — пожалуйста, как будто он за окном, и за всеми двойными звездами, которые есть за ним. Если вы работали с observable-паттерном, то это должно быть знакомо.
Проверяю эту архитектуру на практике последний год, пока не могу наткнуться на ограничения, идеально масштабируется, разбивается на отдельные репозитории, состояние всегда синхронизировано с DOM и доступно из любого места.
Да, не очень, конечно, говорить про свой подход, когда вы ждете комментариев про свою идею, но не вижу, чем бы была полезна такая вот фрактальность, которая добавляет лишний код и цепочки parent-child. Навскидку код будет сильно запутываться и при малейшей небрежности уходить в глубины call stack is too small for you imagination
Я давно остановился на observable (в данном случае mobx), для SPA ключевой момент — синхронизация js-состояния (стор) и DOM (компоненты с жизненным циклом). Если у компонента есть доступ к любым свойствам и методам хранилища, а ререндер происходит только при изменении использованных при прошлом рендере свойств, то такая система становится идеальной по удобству и перфомансу. При добавлении любых дополнительных прослоек — ручного проброса свойств и методов (коннект редакса), вручную написанных SCU, вызова методов через диспетчер, маппинга изменений в хранилище исходя из констант (типов экшенов), отсутствие прямого доступа к нескольким подсторам, иммутабельность и т.п., и удобство разработки, и скорость приложения ухудшаются. Так что тут минимализм вполне оправдан)
У реакт-роутера много недостатков, причем легко какой-либо функционал не внедрить. Надо блокировать переход на роут по каким-то условиям? Ставь версию, в которой есть Prompt. Нужно задавать роуты конфигом, а не свичом в компонентах? Ставь соответствующую версию. Выполнять действия перед загрузкой страницы и после ухода? Тоже подбирай из многообразия версий. Куда проще этой возни написать свой роутер, весь этот функционал займет не так уж много строк кода, включая синхронизацию с URL, безопасный парсинг, анимации переходов между страницами, промисные цепочки переходов.
Даже и не знаю, чем может быть нужен next.js энтерпрайзу, все что он умеет, несложно сделать и так, только более гибко и по соответствующим стандартам. Может быть, кому-то и будет проще сделать прототип приложения на нем, но впоследствии, как с любым комплексным сложно кастомизируемым инструментом, будет больше возни, чем профита.
Подобный путь проходит большинство компаний с крупными реакт-приложениями. В статье, на мой взгляд, примерно середина этого пути "от первого прототипа до грамотно спроектированной системы". Чтобы комментарий не был пустословным, попробую предсказать дальнейший путь разработчиков в том проекте.
Как исходный уровень:
есть достаточно структурированная кодовая база
единообразие стиля написания кода и некоторых принципов (наименование, запрет использования определенных элементов, реэкспорт), наверняка внедрен и Prettier
пишутся юнит- и интеграционные тесты
есть страница с визуализацией компонентов и примерами использования (Storybook) без выделения в отдельный репозиторий (что намного более удобно, если не нужно ее использовать в совсем других проектах)
есть типы (с линтингом ESLint, что более современно)
организована двойная проверка кода — на гит хуки и в CI/CD
код группируется по модулям
Пространства для улучшения:
к тестовым прогонам можно прикрутить систему наподобие Allure
можно разделить тесты на части, чтобы не прогонять полные регрессы на пуш в каждую ветку, оставив полные только для релизных веток или ручного запуска
стек redux+saga чрезмерно многословен и запутан, в будущем явно разработчики поймут, что редюсеры, константы, селекторы, контейнеры, пробросы многочисленных импортов через пропсы, вручную написанные SCU (если не все на селекторах и каждый компонент контеризируется, что, наверное, еще хуже) намного более эффективно было бы заменить на мутабельные реактивные структуры
react-router, вполне возможно, будет заменен на другое решение. Эта библиотека меняет подходы от версии к версии и делает код несовместимым, а в последних версиях работа с конфигами практически не поддерживается, что приводит к вынужденному разбрасыванию роутов по различным файлам. Недостатки очевидны, разбиение на асинхронные чанки, поддержка и документирование осложнены (из конфига можно ее генерировать автоматически)
для локализации понадобится система автоматического создания id для сообщений + сбор всех констант в отдельный файл при сборке для отправки в систему локализации и независимого обновления текстов на сайте, без трат времени разработчика и изменения кода в гите
все, про что рассказывается в статье, нужно будет поместить в техническую базу знаний, так как количество соглашений и условностей уже стало большим, и обучить новых разработчиков будет не так просто лишь путем менторинга
вполне возможно, что от такого количества юнит-тестов ребята откажутся, оставив их только для утилит, так как интеграционные намного лучше справляются с обеспечением итогового качества, а shallow render реактовых компонентов пригождается в очень немногих случаях (например, когда один компонент может работать в десятках режимов)
в CI/CD внедрят проверку производительности (скорость рендеринга и т.п.) для истории перфоманса и анализа возможных узких мест
про CRA в начале было упомянуто, в дальнейшем про него не вижу рассуждений (как и ответов на вопросы из вступления), но если эта штука используется, то, безусловно, первым делом нужно будет переходить на кастомную сборку. Промышленная разработка и слишком высокоуровневые пакеты (особенно zero config) несовместимы по причине отсутствия гибкости, так что если ребята там присматриваются к Next.js, то лучше отказаться сразу от этой идеи.
Итогом этих нескольких шагов будет качественная смена состояния на более стабильное (примат интеграционных тестов над юнитами, анализ скорости), быстрее разрабатываемое (автоматика в локализации, отказ от неэффективных архитектур), быстрее выкладываемое (разделение тестов), проще изучаемое (техническая база знаний, автогенерация документации из конфигов, например — роутинга или апи). Потом, отталкиваясь от нового состояния этих организационных моментов и, зная специфику конкретного проекта, можно затронуть темы работы с гитом, связки различных систем (база знаний + дизайн-система + таск-трекер + гит-хранилище), работы с CDN, системы метрик… Много всего.
По выводу темы я бы не согласился с автором, сказавшим "А самое важное — стремитесь к минимализму". По моему опыту главное — это удобство, которое подразумевает довольно большую автоматизацию.
Одна из базовых характеристик отказоустойчивых систем — явно определенные потоки ошибок, которые попадают в соответствующие перехватчики. То есть, к примеру, при работе с апи бэка можно использовать такой флоу:
function authUser({ store, api, actions }, { login, password }) {
return Promise.resolve()
.then(() => api.postUserData({ login, password }))
.then(({ userInfo }) => (store.user = userInfo))
.catch(error => {
if (error.name === "SOLVED") return;
if (error.name === "INVALID_LOGIN") {
return actions.pushFormValidators({
password: { value => value === login, message: 'Пользователя не существует' }
})
}
return actions.showNotification('Неизвестная ошибка')
})
}
При этом обработка HTTP-ошибок будет производиться в api-слое, то есть имеются следующие сценарии:
при 404/403 метод api.postUserData сам выдаст нотификацию и вернет Promise.reject(Err.SOLVED), таким образом обработчик выше не будет предпринимать действий.
при 402 (ошибка валидации или формата данных, присланная бэком) отдаст Promise.reject(Err[response.errorName]), обработчик подсветит эту ошибку в форме и не даст пользователю второй раз ввести некорректные данные
при неизвестной ошибке в самом экшене authUser (например, опечатка stAre.user = userInfo) либо при багах кода в процессе запроса, ошибка попадет в последнюю строчку кэтчера. Этот же сценарий в случае, если бэк вернет неизвестную константу на фронт вместо "INVALID_LOGIN", то есть при ошибке в контракте общения.
Таким образом явно определяются потоки всех возможных ошибок и стратегии их обработки (можно и дополнительные слои выносить, я привел базовый пример), четко ограничивается скоуп их распространения и исключается влияние на остальной функционал приложения. В целом, хотя при throw вместо Promise.reject будет работать так же, для типизации+семантичности все же лучше использовать Promise.reject, тут я с автором согласен, для предсказуемых "ошибок", которые по сути являются перенаправлением в другой блок обработки промиса (catch вместо then). Но учитывать возможность возникновения неожиданных исключений всегда необходимо, то есть, если заменить reject на throw, должно работать так же, иначе может возникнуть ситуация, что при выполнении действия ломается вся страница или большая часть функционала (очень часто встречаю это при работе).
Хорошие ответы, только не хватает этих оговорок в самом тексте статьи) Нужен контекст, что примененные подходы работают именно в вашем большом проекте, где:
в BFF и API Gateway проникла бизнес-логика и запутанные взаимосвязи (да, в статье об этом говорится, но как о возможности, а не об опорных исходных данных)
каждый байт на счету при передаче данных с бэка по апи (очень редкий кейс, характерный только для хайлоада, так как в остальных корп продуктах заботятся о перфомансе выполнения операций, стабильности и унифицированности, но не о наличии некоторых дополнительных данных)
архитектурные практики недостаточно контролируются, поэтому востребована система с жестким паттерном
Нетипизированные req & res в Express действительно проблема при интенсивном использовании, но никто не заставляет использовать их как хранилища. Так, сессии и кэш лучше держать в Redis, как и дополнительные данные типа traceId запросов, а работа с базой отлично типизируется и стандартизируется, что исключит "неконтролируемые мутации". В Nest же, как вижу, предлагается использовать неявный слой IoC, а про дополнительные данные, привязанные к сессии пользователя, не говорится, так что сравнить не с чем. В целом, не думаю, что это значительный недостаток.
В создании системы распределенного рендеринга участвовал, по сути она состоит из "определить микрофронтенды, которые ответственны за рендеринг страницы" + "ответы от их bff склеить в единый html-документ", эти задачи можно решить массой способов. Наверняка решение "подключить декоратором к контроллеру" продиктовано архитектурой Nest, то есть и в этом случае задействована специфика вашего проекта, в котором жесткие подходы выгоднее свободного архитектурирования. Для максимальной прозрачности удобнее было бы иметь сервис-детектор, который достаточно просто покрывается тестами, а сбор данных произвести через const requiredBffs = detector(currentRoute); const htmlParts = Promise.all(requiredBffs.map(fetch)) через слой кэширования, а затем провести склейку по маркерам. В идеале система должна работать автоматически, а информация о частях, из которых состоит конкретная страница, содержаться в конфиге роутов, что не потребует от разработчиков вообще задумываться об этом механизме и проставлять декораторы. То есть я к тому, что, возможно, жесткие паттерны в данном случае ухудшили экспириенс, а не улучшили)
И "как же писать бизнес-логику" в BFF — тоже контекстуальная тема, которая не должна затрагивать большинство корп проектов. Когда фронтовый бэк из рендерера для seo и ускорения первой отрисовки превращается в толстый бэк с кучей наворотов, пора бы задуматься, туда ли все повернуло, и не стоит ли остальную логику вынести на "хардкорный" бэк. Так что логичнее было бы вынести это в отдельную статью, сравнив несколько паттернов организации сущностей и взаимодействия между ними, ибо в итоге раздел "архитектурные слои" получился очень немногословным и смятым
Возможно, кому-то понравятся высказанные в статье подходы, но в тех приложениях, которые я писал, это либо неактуально, либо избыточно.
То, что можно делать SSR на ноде, создавая сервер-прослойку (BFF) — тема избитая и в корпоративных приложениях используется уже несколько лет.
Проблема необходимости разного набора данных разным приложениям решается версионированием АПИ, тут ничем BFF не поможет
API Gateway как маппер, в какой микросервис пойти в зависимости от урла запроса (https://api.domain.com/ms1/users | https://api.domain.com/ms2/auth) и по какому протоколу, действительно можно вынести в BFF, но фронтендерам придется следить за изменением инфраструктуры бэковых сервисов и синхронизироваться. Удобнее все же отдать реализацию этого маппера бэкендерам, в BFF же просто проксируя запросы в единую точку.
То, что бизнес-логика в виде нормализации данных, стейт-машин, синхронизации между различными источниками (офлайн режим, сторонние сервисы), проверка прав есть и на фронтенде — это логично. Делать запрос на бэк "юзер нажал кнопку Восстановить пароль, есть ли у него права для отображения этой страницы?" или "можно ли ему показать кнопку редактирования?" действительно нет смысла, если можно проверить эти права во фронтовом стейте с пермишенами, полученными заранее. Хотя это и выглядит "размазыванием ответственности", в основном этот код обслуживает нужды интерфейса.
Пример кода на Express, в котором "все слои перемешаны" — дело рук не этого простого, гибкого и удобного инструмента, а разработчика. Конечно, можно написать любую дичь и сказать "поддерживать такой код не хочется", но вот нападки на Express этим не обоснованы. Валидации, обработка ошибок, логирование, метод отправки на фронт (стримами в данном случае), миддлвары обработки запроса, схемы — все это выносится в удобные слои, которые хоть и склеиваются внутри миддлвар в итоге, но позволяют абстрагироваться.
"протащить созданный экземпляр логгера через всё приложение" можно и через global (ничего страшного в таких микроприложениях не будет) или более удобным декоратором @useLogger class UserEntity extends EntityClass {}, для чего дополнительные невидимые контейнеры — не понятно. Это же BFF, простой сервер-рендерер-прокси, а не сложная система с кучей сервисов.
Миддлвары — отличный паттерн, явно определяющий порядок обработки запроса и позволяющий удобно логировать этапы и в любой момент прервать этот поток, выбросив исключение, либо отправив response. При этом структурировать обработку можно как угодно — через мапперы, классы, функциональные контроллеры, схемы обработки, описанные в json, — все зависит от видения разработчика и применяемых подходов. Nest переусложнен и жестковато структурирован для BFF, в котором основной функционал — по схеме роутинга отдать рендер фронтендового фреймворка + проксировать запросы на бэк с соответствующими валидациями и нормализацией + логирование + работа с сессиями. Express идеален для подобных задач благодаря своей простоте и развитой экосистеме.
Ну и как-то я не уловил момент перехода с BFF с простым примером на всяческие UserEntity, домены, сервисы, пайпы, гарды… Это, видимо, когда фронтендеру после создания сервера-прокси пришла мысль "а почему бы весь бэк не переделать на ноду? Я могу!") Ну максимум там будут сервисы хранения сессии пользователей и кэширования. Или BFF был просто вступлением, а суть — в рекламе ролика с гексагональным паттерном для высчитывания остатка денег на счету пользователя? Критикую не ролик, а несоответствие теме статьи и в целом не совсем логичное повествование.
Ну только не {isCondition && <SomeComponent1 />} конечно, а {Boolean(isCondition) && <SomeComponent1 />}, так как этот оператор приводит к boolean лишь при сравнении, но выводит в итоге оригинальное значение. Была в одном известном проекте система прав и пермишенов, основанная на 0 и 1 вместо булеанов, то есть isCondition был при отсутствии прав === 0, соответственно на странице оказывалось немало нод-нулей, отчего ехала верстка. А так как разработчиков было много, пришлось очень тщательно следить за этим на код-ревью.
Такое предложение в трекере уже есть, дописал коммент с предложением скопировать функционал из раздела Prettier версии 2020.2. Должно быть действительно полезно всем фронтендерам, так как обычное форматирование "Reformat Code" слабо стандартизируется. Реализация такой фичи в моих глазах бы значительно увеличила привлекательность данного IDE, если бы уже не был прочно завербован в армию использующих)
Спасибо, в основном пользуюсь Eslint --fix on save, тоже недавно появилась в вашем редакторе — так как в моих проектах prettier интегрирован в eslint, то фиксы обоих инструментов применяются одновременно.
Такой вопрос — планируется ли для Stylelint сделать похожий функционал? Пока приходится настраивать отдельный вотчер, который после сохранения файла переформатирует его. Это занимает по времени в районе 1 секунды, по сравнению с моментальным реформаттингом js-файлов кажется, что уже не круто)
О, тоже лидил ипотеку в той компании)
В целом к SW отношусь осторожно — недостатки очевидны ("Для её эффективного использования придётся пройти тернистый путь из квестов, связанных с особенностями жизненного цикла воркеров, неполной поддержкой браузерами, проблемами с политикой кэширования и попутными побочными эффектами (устаревшее содержимое кэша браузера или, к примеру, сломанные ссылки)"). А единственный плюс по сравнению со стандартным браузерным кешированием — возможность работы приложения в оффлайне, что конкретно для сервиса ипотеки — лишнее, т.к. пользователь ожидает интерактив (чаты, актуальные данные, услуги) и может нарушиться безопасность (авторизация в оффлайне не работает). Также процесс обновления клиентских файлов, если апдейт приложения произошел когда юзер был в оффлайне, потребует дополнительного кода.
Есть ли реальная польза в этом проекте от PWA, или только значительное усложнение поддержки?
Конечно, это с вариантом без кеширования. Не знаю, как сейчас, но тогда мне не удалось подружить
jade.compileFile('./templates/foo.jade', { cache: true });
и передачу кастомных параметров на каждый запрос, то есть было впечатление, что с кешем он просто формирует набор готовых строк, и при новых параметрах рендерит заново.Также первичный рендер был значительно медленнее, поэтому детально разбираться я не стал — возможно, тут мое мнение о Jade некорректное и можно добиться лучших результатов.
Лучше использовать не дату сборки как идентификатор версии, а contenthash — хэш-строку на основе анализа содержимого файла, либо commithash — хэш последнего коммита. Это поможет избежать лишних срабатываний (при использовании балансера или пересборок).
Также рекомендую включать комментарий в каждый из итоговых js и css файлов с версией, пример для Webpack:
Это позволит осуществлять дебаг без открытия доп. файла, узнавать у клиентов версии загруженных ресурсов (для решения проблем с кэшем, особенно актуально при использовании Service Worker), решить проблему с асинхронной загрузкой чанков (в случае, если их имена не модифицируются — можно в script.onload проверять совпадение версии в комментарии чанка и версии основного файла).
По поводу потери данных — можно определить интерактивные части (формы) и при обновлении версии фронтенда стор сохранять в Local Storage и восстанавливать после перезагрузки. Если меняется формат данных в сторе — то соответственно нужна схема миграции. Если компания нацелена на создание качественного продукта, это все несложно внедрить.
Да, и если есть SSR, то посылать информацию об обновлении файлов можно по сокету с ноды, а не методом опроса раз в минуту — выглядеть будет приятней, лишние запросы уйдут.
Когда-то в не столь бородатые времена (2015-2016) использовал Marko 2-3 для шаблонизации на ноде, важны были возможность выделять кастомные компоненты и максимальная скорость работы.
К примеру, такой вот компонент (template.marko):
Преобразовывался при первичном рендере в чистую функцию (библиотека создает рядом файл template.marko.js):
И это все отдавалось нодой:
Благодаря преобразованию шаблонов в чистые функции скорость была просто ошеломительной, фактически соответствуя скорости работы самой ноды. Делал сравнения с несколькими имевшимися тогда шаблонизаторами типа Pug, Jade — разрыв по скорости ответа и потреблению памяти исчислялся десятками раз, а так как проект был хайлоадом, Marko стал спасением. Да и синтаксис довольно приятный, и локализацию легко встроить, и кастомизировать рендеринг не сложно. Это все к вопросу, где он может быть востребован — для render-сервера, который принимает параметры из других сервисов и отдает локализованный шаблонизированный html — подходит отлично. А как фреймворк не пробовал использовать, тут явно другие лидеры
Вспоминается проект из нескольких ифреймов по вынужденной причине, т.к. контент располагался на разных доменах и не мог работать с одного. В этом кроссфреймовом протоколе пришлось делать массу методов — для контроля отображения (мобильный, десктопный, версии для определенных устройств, тема), локализации, синхронизации событий (клики по управляющим элементам в верхнеуровневых фреймах передавать в дочерние и наоборот, т.е. двусторонний эвент-биндинг), синхронизации роутинга и состояния… Все это требовало значительного количества времени на поддержку, в десятки раз большего, чем в монолите с единым состоянием. Еще и с пробросом куки проблемы, с безопасностью (каждый метод нужно подписывать и строго определять, чтобы сторонние ресурсы не могли встроить себе и вызывать все что угодно, а это было критично для проекта — платежная система).
Я к тому, что можно, если нужно — но лучше не связываться.
Ну вот, кто-то начинает триггерить фазу "освобождение от иллюзий" по хукам. По поводу "В текущем ландшафте JavaScript нет ничего подобного" — хук useEffect как паттерн это стандартная асинхронщина через event loop:
Это превращает чистые синхронные функции в смесь синхронного и асинхронного кода с сайд-эффектами. В классовом подходе, несмотря на необходимость дубляжа в CDM / CDU, это вынесено в явные методы жизненного цикла и понимается проще, хотя комбинируется чуть сложнее.
Спасибо за пояснение, действительно я сознательно сравниваю все паттерны, которые когда-либо использовал, с чем-то новым, чтобы понять выгоду. Для меня цели архитектуры — комфорт в разработке бизнес-функционала, минимизация возможных ошибок, простота масштабирования и возможность легкой интеграции сторонних решений. С этого угла зрения, к сожалению, полностью фрактальная архитектура не подходит.
А как идея — интересно. Если абстрагироваться от всплытия по yield (это ключевая фича, как понимаю, но все же) семантически подход очень напомнил effector, когда на каждый тип данных формируется локальный стор с методами для изменения
Для крупных приложений такие подходы неудобны, в реализациях, которые видел, постепенно все равно код стремится к формированию более крупных и независимых от компонентов состояний, абстрагированию от библиотеки для реализации потоков данных и минимизации кода для проброса в видимые части интерфейса. Абстрагироваться же от yield-перехватчиков на разных уровнях не получится, все должно быть в этой парадигме. А это быстро приведет к устареванию подхода и через какое-то время к полному переписыванию на более гибкие рельсы и на новые технологии (во фронтенде значимые изменения часты).
Вот такой вот прагматичный подход, тоже не в обиду, но я так вижу)
Ну вот не знаю про "шаг впереди". В ваших примерах есть как раз "обмен данными между компонентами" в виде
В моей же схеме данные от User лежали бы в общедоступном хранилище, с ними удобно работать, нормализовывать, мутировать, создавать сложные селекторы из композиции различных параметров, сериализовывать в json-структуры, использовать для общения с другими сервисами по АПИ.
А ваш подход скорее из мира, когда данные хранились в дата-атрибутах DOM, постоянно извлекались и сериализовывались-десериализовывались, и так же работать с постоянными yield для извлечение набора актуальных данных — это затратно. Код будет погрязать в бесконечных
Не очень понял про то, как вы хотите одну сущность переводить одновременно в json и html — то есть в объектах описывать все возможные комбинации раскладки и атрибуты? Вроде нет, в ваших примерах на гитхабе используются вполне классический JSX в xml-формате, только с очень сложным извлечением данных.
Что-то почитал репозитории и, кажется, толковую дискуссию мы не создадим. Мне банально непонятен весь этот звездочный код и его смысл
Тоже много лет назад писал подобные системы, полные функциональщины и скрытых слоев, с привкусом Алохоморы при выполнении. Но потом дорос до понимания, что лучший код — максимально простой и понятный, так как работаю в корпоративных продуктах, где это — ключ к успеху. Бывает, приходят ребята с очень нестандартными подходами, но их код не понимают и приходится переписывать после их ухода. В общем, не вижу пользы от вашего подхода, кроме как для саморазвития)
Вы, похоже, говорите о приложениях, в которых не используется js-хранилище с данными, а все они содержатся в атрибутах дом-дерева? И для операции с элементами вызываются getElementById, а затем в него записываются значения и дата-атрибуты? Да, подобная схема используется в крошечных простых приложениях, и все, о чем мы тут говорим, там не нужно.
В отрыве от фреймворка это называется "двухсторонний биндинг" (https://seemple.js.org/), только с глобальным состоянием, а не локальным.
Разговор о том, что первично, тем более если вы сравниваете с концепцией "начало начал не имеет начала, безначальность бесконечна" (фрактал), был бы не особо уместен. В моем понимании это лишь одна из составляющих — движение есть следствие взаимодействия других сил, и для меня оно не первично.
Не знаю, как вы сделали вывод, что я отрицаю структуру в интерфейсах — но это другой слой, который не относится к данным, и представлен в виде иерархичного DOM-дерева, создаваемого опять же иерархичной композицией реакт-компонентов. Прокидывать же в инпут данные через всех родителей, а сигналы от него собирать по всей цепочке — это и есть ограничение, заставляющее "учитывать везде предельную скорость взаимодействия", портальность снимает эту проблему. Но она присутствует в цепочках react props, когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться, а в предлагаемой "фрактальной" архитектуре, похоже, именно так.
А вот неплохо, радует, что опыт познания мира начинает проникать в программирование. Тоже потихоньку пишу статью со своим симбиозом, но в основе не "всплытие" данных по дереву (как если смотреть на эту фрактальную картину сбоку), а единое глобальное состояние с доступом в любую точку. В материальном мире мы работаем с неэффективными передатчиками — свет, волны — которые обладают ограниченной скоростью и не позволяют получать доступ к краю Вселенной мгновенно, но в программировании можно обойти это ограничение и каждый элемент связывать "порталом" с глобальным стейтом:
Этот же компонент имеет моральное и фактическое право менять глобальный стейт, как человек может шевелиться, меняя глобальное состояние Вселенной, при этом оно всегда остается цельным и согласованным.
То есть относительно ваших идей я предлагаю не пробираться сквозь фракталы для доступа к компонентам через длинную цепочку (под капотом) вызовов, а обращаться к любому объекту напрямую, будто до него рукой подать. Надо до Сириуса — пожалуйста, как будто он за окном, и за всеми двойными звездами, которые есть за ним. Если вы работали с observable-паттерном, то это должно быть знакомо.
Проверяю эту архитектуру на практике последний год, пока не могу наткнуться на ограничения, идеально масштабируется, разбивается на отдельные репозитории, состояние всегда синхронизировано с DOM и доступно из любого места.
Да, не очень, конечно, говорить про свой подход, когда вы ждете комментариев про свою идею, но не вижу, чем бы была полезна такая вот фрактальность, которая добавляет лишний код и цепочки parent-child. Навскидку код будет сильно запутываться и при малейшей небрежности уходить в глубины call stack is too small for you imagination
Я давно остановился на observable (в данном случае mobx), для SPA ключевой момент — синхронизация js-состояния (стор) и DOM (компоненты с жизненным циклом). Если у компонента есть доступ к любым свойствам и методам хранилища, а ререндер происходит только при изменении использованных при прошлом рендере свойств, то такая система становится идеальной по удобству и перфомансу. При добавлении любых дополнительных прослоек — ручного проброса свойств и методов (коннект редакса), вручную написанных SCU, вызова методов через диспетчер, маппинга изменений в хранилище исходя из констант (типов экшенов), отсутствие прямого доступа к нескольким подсторам, иммутабельность и т.п., и удобство разработки, и скорость приложения ухудшаются. Так что тут минимализм вполне оправдан)
У реакт-роутера много недостатков, причем легко какой-либо функционал не внедрить. Надо блокировать переход на роут по каким-то условиям? Ставь версию, в которой есть Prompt. Нужно задавать роуты конфигом, а не свичом в компонентах? Ставь соответствующую версию. Выполнять действия перед загрузкой страницы и после ухода? Тоже подбирай из многообразия версий. Куда проще этой возни написать свой роутер, весь этот функционал займет не так уж много строк кода, включая синхронизацию с URL, безопасный парсинг, анимации переходов между страницами, промисные цепочки переходов.
Даже и не знаю, чем может быть нужен next.js энтерпрайзу, все что он умеет, несложно сделать и так, только более гибко и по соответствующим стандартам. Может быть, кому-то и будет проще сделать прототип приложения на нем, но впоследствии, как с любым комплексным сложно кастомизируемым инструментом, будет больше возни, чем профита.
Спасибо за перевод!
Подобный путь проходит большинство компаний с крупными реакт-приложениями. В статье, на мой взгляд, примерно середина этого пути "от первого прототипа до грамотно спроектированной системы". Чтобы комментарий не был пустословным, попробую предсказать дальнейший путь разработчиков в том проекте.
Как исходный уровень:
Пространства для улучшения:
Итогом этих нескольких шагов будет качественная смена состояния на более стабильное (примат интеграционных тестов над юнитами, анализ скорости), быстрее разрабатываемое (автоматика в локализации, отказ от неэффективных архитектур), быстрее выкладываемое (разделение тестов), проще изучаемое (техническая база знаний, автогенерация документации из конфигов, например — роутинга или апи). Потом, отталкиваясь от нового состояния этих организационных моментов и, зная специфику конкретного проекта, можно затронуть темы работы с гитом, связки различных систем (база знаний + дизайн-система + таск-трекер + гит-хранилище), работы с CDN, системы метрик… Много всего.
По выводу темы я бы не согласился с автором, сказавшим "А самое важное — стремитесь к минимализму". По моему опыту главное — это удобство, которое подразумевает довольно большую автоматизацию.
Одна из базовых характеристик отказоустойчивых систем — явно определенные потоки ошибок, которые попадают в соответствующие перехватчики. То есть, к примеру, при работе с апи бэка можно использовать такой флоу:
При этом обработка HTTP-ошибок будет производиться в api-слое, то есть имеются следующие сценарии:
api.postUserData
сам выдаст нотификацию и вернетPromise.reject(Err.SOLVED)
, таким образом обработчик выше не будет предпринимать действий.Promise.reject(Err[response.errorName])
, обработчик подсветит эту ошибку в форме и не даст пользователю второй раз ввести некорректные данныеstAre.user = userInfo
) либо при багах кода в процессе запроса, ошибка попадет в последнюю строчку кэтчера. Этот же сценарий в случае, если бэк вернет неизвестную константу на фронт вместо "INVALID_LOGIN", то есть при ошибке в контракте общения.Таким образом явно определяются потоки всех возможных ошибок и стратегии их обработки (можно и дополнительные слои выносить, я привел базовый пример), четко ограничивается скоуп их распространения и исключается влияние на остальной функционал приложения. В целом, хотя при throw вместо Promise.reject будет работать так же, для типизации+семантичности все же лучше использовать Promise.reject, тут я с автором согласен, для предсказуемых "ошибок", которые по сути являются перенаправлением в другой блок обработки промиса (catch вместо then). Но учитывать возможность возникновения неожиданных исключений всегда необходимо, то есть, если заменить reject на throw, должно работать так же, иначе может возникнуть ситуация, что при выполнении действия ломается вся страница или большая часть функционала (очень часто встречаю это при работе).
Хорошие ответы, только не хватает этих оговорок в самом тексте статьи) Нужен контекст, что примененные подходы работают именно в вашем большом проекте, где:
Нетипизированные req & res в Express действительно проблема при интенсивном использовании, но никто не заставляет использовать их как хранилища. Так, сессии и кэш лучше держать в Redis, как и дополнительные данные типа traceId запросов, а работа с базой отлично типизируется и стандартизируется, что исключит "неконтролируемые мутации". В Nest же, как вижу, предлагается использовать неявный слой IoC, а про дополнительные данные, привязанные к сессии пользователя, не говорится, так что сравнить не с чем. В целом, не думаю, что это значительный недостаток.
В создании системы распределенного рендеринга участвовал, по сути она состоит из "определить микрофронтенды, которые ответственны за рендеринг страницы" + "ответы от их bff склеить в единый html-документ", эти задачи можно решить массой способов. Наверняка решение "подключить декоратором к контроллеру" продиктовано архитектурой Nest, то есть и в этом случае задействована специфика вашего проекта, в котором жесткие подходы выгоднее свободного архитектурирования. Для максимальной прозрачности удобнее было бы иметь сервис-детектор, который достаточно просто покрывается тестами, а сбор данных произвести через
const requiredBffs = detector(currentRoute); const htmlParts = Promise.all(requiredBffs.map(fetch))
через слой кэширования, а затем провести склейку по маркерам. В идеале система должна работать автоматически, а информация о частях, из которых состоит конкретная страница, содержаться в конфиге роутов, что не потребует от разработчиков вообще задумываться об этом механизме и проставлять декораторы. То есть я к тому, что, возможно, жесткие паттерны в данном случае ухудшили экспириенс, а не улучшили)И "как же писать бизнес-логику" в BFF — тоже контекстуальная тема, которая не должна затрагивать большинство корп проектов. Когда фронтовый бэк из рендерера для seo и ускорения первой отрисовки превращается в толстый бэк с кучей наворотов, пора бы задуматься, туда ли все повернуло, и не стоит ли остальную логику вынести на "хардкорный" бэк. Так что логичнее было бы вынести это в отдельную статью, сравнив несколько паттернов организации сущностей и взаимодействия между ними, ибо в итоге раздел "архитектурные слои" получился очень немногословным и смятым
Возможно, кому-то понравятся высказанные в статье подходы, но в тех приложениях, которые я писал, это либо неактуально, либо избыточно.
https://api.domain.com/ms1/users
|https://api.domain.com/ms2/auth
) и по какому протоколу, действительно можно вынести в BFF, но фронтендерам придется следить за изменением инфраструктуры бэковых сервисов и синхронизироваться. Удобнее все же отдать реализацию этого маппера бэкендерам, в BFF же просто проксируя запросы в единую точку.@useLogger class UserEntity extends EntityClass {}
, для чего дополнительные невидимые контейнеры — не понятно. Это же BFF, простой сервер-рендерер-прокси, а не сложная система с кучей сервисов.Ну и как-то я не уловил момент перехода с BFF с простым примером на всяческие UserEntity, домены, сервисы, пайпы, гарды… Это, видимо, когда фронтендеру после создания сервера-прокси пришла мысль "а почему бы весь бэк не переделать на ноду? Я могу!") Ну максимум там будут сервисы хранения сессии пользователей и кэширования. Или BFF был просто вступлением, а суть — в рекламе ролика с гексагональным паттерном для высчитывания остатка денег на счету пользователя? Критикую не ролик, а несоответствие теме статьи и в целом не совсем логичное повествование.
Ну только не
{isCondition && <SomeComponent1 />}
конечно, а{Boolean(isCondition) && <SomeComponent1 />}
, так как этот оператор приводит к boolean лишь при сравнении, но выводит в итоге оригинальное значение. Была в одном известном проекте система прав и пермишенов, основанная на 0 и 1 вместо булеанов, то есть isCondition был при отсутствии прав === 0, соответственно на странице оказывалось немало нод-нулей, отчего ехала верстка. А так как разработчиков было много, пришлось очень тщательно следить за этим на код-ревью.Такое предложение в трекере уже есть, дописал коммент с предложением скопировать функционал из раздела Prettier версии 2020.2. Должно быть действительно полезно всем фронтендерам, так как обычное форматирование "Reformat Code" слабо стандартизируется. Реализация такой фичи в моих глазах бы значительно увеличила привлекательность данного IDE, если бы уже не был прочно завербован в армию использующих)
Спасибо, в основном пользуюсь Eslint --fix on save, тоже недавно появилась в вашем редакторе — так как в моих проектах prettier интегрирован в eslint, то фиксы обоих инструментов применяются одновременно.
Такой вопрос — планируется ли для Stylelint сделать похожий функционал? Пока приходится настраивать отдельный вотчер, который после сохранения файла переформатирует его. Это занимает по времени в районе 1 секунды, по сравнению с моментальным реформаттингом js-файлов кажется, что уже не круто)