...продолжение:
Пример №2
[См. первое изображение]
⁃ Widget — независимый блок (на большой странице) с ограниченным списком;
- Full List — всплывающая страница с полным списком;
- Add/Edit — всплывающая страница добавления или редактирования элемента списка.
Обратите внимание, что Widget и Full List используют одинаковую цепочку компонентов для отображения списка. При этом, каждый элемент можно удалить или отредактировать, а также добавить новый.
Набросим схему работы с данными:
[См. второе изображение]
Схема уже не кажется такой простой. 😅 А всё потому что мы имеем туже проблему, что и в первом примере — слишком много точек связи с данными (Data Layer). Widget и страницы — “управляющие компоненты”, им положено быть связанными со слоем данных. А вот компоненты Remove и Select — нарушители (поэтому и выделены цветом).
Решается также просто как и в первом примере, правильной настройкой связей:
[См. последнее изображение]
Обратите внимание, что сама схема стала приятнее на вид, и наш слоёный “пирог” приобрёл логический вид — сверху данные, потом управляющие компоненты, а в самом низу “глупые” компоненты. Из чего кстати, легко выводится правило — “умные” компоненты, напрямую связанные с данными, не могут находиться в конце цепочки из “глупых”, т.е. внизу.
П.С. Всегда бывают исключения, но они должны быть сильно оправданы.
#frontend #development
Пример №2
[См. первое изображение]
⁃ Widget — независимый блок (на большой странице) с ограниченным списком;
- Full List — всплывающая страница с полным списком;
- Add/Edit — всплывающая страница добавления или редактирования элемента списка.
Обратите внимание, что Widget и Full List используют одинаковую цепочку компонентов для отображения списка. При этом, каждый элемент можно удалить или отредактировать, а также добавить новый.
Набросим схему работы с данными:
[См. второе изображение]
Схема уже не кажется такой простой. 😅 А всё потому что мы имеем туже проблему, что и в первом примере — слишком много точек связи с данными (Data Layer). Widget и страницы — “управляющие компоненты”, им положено быть связанными со слоем данных. А вот компоненты Remove и Select — нарушители (поэтому и выделены цветом).
Решается также просто как и в первом примере, правильной настройкой связей:
[См. последнее изображение]
Обратите внимание, что сама схема стала приятнее на вид, и наш слоёный “пирог” приобрёл логический вид — сверху данные, потом управляющие компоненты, а в самом низу “глупые” компоненты. Из чего кстати, легко выводится правило — “умные” компоненты, напрямую связанные с данными, не могут находиться в конце цепочки из “глупых”, т.е. внизу.
П.С. Всегда бывают исключения, но они должны быть сильно оправданы.
#frontend #development
❤2🔥1
https://kettanaito.com/blog/dont-sleep-on-abort-controller
Хорошая обзорная статья про AbortController. Обычно его используют только для отмены запросов fetch, но на самом деле, при желании, отменять можно всё что угодно.
Самым вкусным, на мой взгляд, является отмена пачки подписок на события одной командой:
#frontend #development #javascript #react
Хорошая обзорная статья про AbortController. Обычно его используют только для отмены запросов fetch, но на самом деле, при желании, отменять можно всё что угодно.
Самым вкусным, на мой взгляд, является отмена пачки подписок на события одной командой:
useEffect(() => {
const controller = new AbortController();
const options = { signal: controller.signal };
el.addEventListener('pointerdown', handler, options);
el.addEventListener('pointerup', handler, options);
el.addEventListener('pointercancel', handler, options);
el.addEventListener('pointerleave', handler, options);
el.addEventListener('click', handler, options);
return () => {
controller.abort();
};
}, []);
#frontend #development #javascript #react
kettanaito.com
Don't Sleep on AbortController
Learn how to make anything abortable in JavaScript.
👍3
Рендер массивов в SolidJS
Рендер массива в SolidJS можно выполнить тремя способами:
Первый (как в React)
Это самый “тупой” вариант и лучше его не использовать, т.к. при любом изменении массива весь список будет создан заново. Другими словами, если в компоненте Item используются onMount и onCleanup, то для каждого элемента списка сначала будет вызван onCleanup а затем onMount.
Через компонент For
По сути, это тот же
Через компонент Index
Этот вариант подходит для статических массивов. При этом, значения в элементах могут меняться, главное чтобы не менялась длинна самого массива и расположение элементов в нём. В противном случае вы можете столкнуться с ситуацией когда, например в onCleanup, будет значение не того элемента которого вы ожидаете.
Это я всё к тому, что если вдруг у вас появится желание использовать Index для динамического массива, лучше подумайте как избавиться от лишних изменений в самом массиве.
П.С. Ни в одном случае не нужно указывать параметр key, как например того требует React.
#development #frontend #solidjs
Рендер массива в SolidJS можно выполнить тремя способами:
Первый (как в React)
{items().map((item) => (
<Item item={item} />
))}
Это самый “тупой” вариант и лучше его не использовать, т.к. при любом изменении массива весь список будет создан заново. Другими словами, если в компоненте Item используются onMount и onCleanup, то для каждого элемента списка сначала будет вызван onCleanup а затем onMount.
Через компонент For
<For each={items()}>
{(item) => <Item item={item} />}
</For>
По сути, это тот же
.map() но имеющий очень важное отличие: если элемент списка не изменился, то и перерендера этого элемента не будет, как собственно и вызовов onMount и onCleanup. В общем, если массив состоит из объектов, важно следить чтобы они не менялись без необходимости.Через компонент Index
<Index each={items()}>
{(item) => <Item item={item()} />}
</Index>
Этот вариант подходит для статических массивов. При этом, значения в элементах могут меняться, главное чтобы не менялась длинна самого массива и расположение элементов в нём. В противном случае вы можете столкнуться с ситуацией когда, например в onCleanup, будет значение не того элемента которого вы ожидаете.
Это я всё к тому, что если вдруг у вас появится желание использовать Index для динамического массива, лучше подумайте как избавиться от лишних изменений в самом массиве.
П.С. Ни в одном случае не нужно указывать параметр key, как например того требует React.
#development #frontend #solidjs
👍2👎1🔥1💩1🦄1
Про SOLID
Сколько всего уже про этот несчастный SOLID написано, сколько попыток объяснить простыми словами, что это и зачем, а воз и ныне там. Особенно иронично получается, когда статья называется “Понятно про SOLID” или что-то в этом духе, а содержимое наоборот больше запутывает. А что если в погоне за объяснением значения каждой буквы мы теряем что-то важное?
“…использование SOLID способствует созданию такой системы, которую будет легко поддерживать и расширять в течение долгого времени.”
По сути, все пять принципов сводятся к одной единственной мысли — код отвечающий за что-то одно должен лежать в одном месте. К сожалению, наверное единственный способ это реально понять — набить много шишек.
Иногда кажется, что разработчики так зациклились на этом SOLID, что на технических интервью (или на грейдах) просят рассказать именно про эти пять букв, вместо того чтобы спросить как писать простой и расширяемый код. А бывает и так, что разработчик, вооружившись святой верой в SOLID, использует его так, что получается ровно обратный эффект — большой и запутанный код.
П.С. Кстати, SOLID это не только (и не столько) про классы, хотя на них обычно все примеры и строятся.
#frontend #development
Сколько всего уже про этот несчастный SOLID написано, сколько попыток объяснить простыми словами, что это и зачем, а воз и ныне там. Особенно иронично получается, когда статья называется “Понятно про SOLID” или что-то в этом духе, а содержимое наоборот больше запутывает. А что если в погоне за объяснением значения каждой буквы мы теряем что-то важное?
“…использование SOLID способствует созданию такой системы, которую будет легко поддерживать и расширять в течение долгого времени.”
По сути, все пять принципов сводятся к одной единственной мысли — код отвечающий за что-то одно должен лежать в одном месте. К сожалению, наверное единственный способ это реально понять — набить много шишек.
Иногда кажется, что разработчики так зациклились на этом SOLID, что на технических интервью (или на грейдах) просят рассказать именно про эти пять букв, вместо того чтобы спросить как писать простой и расширяемый код. А бывает и так, что разработчик, вооружившись святой верой в SOLID, использует его так, что получается ровно обратный эффект — большой и запутанный код.
П.С. Кстати, SOLID это не только (и не столько) про классы, хотя на них обычно все примеры и строятся.
#frontend #development
🔥2
Освобождение памяти в React
Представим, что у нас есть некоторый провайдер, в котором:
⁃ open создаёт инстанс класса SomeClass и помещает его в useState;
⁃ close затирает это инстанс с помощью null;
⁃ instance передаётся через контекст ниже и где-то там используется
На первый взгляд кажется, что после выполнения метода close и присвоения состоянию instance значения null, ранее созданные инстанс класс SomeClass более недостижим и может быть удалён из памяти сборщиком мусора, но это не так. Ссылка на инстанс всё ещё будет храниться в React Fiber.
Исправить это можно. Например, хранить инстанс в useRef. Однако, в этом случае useState вам всё равно потребуется для хранения состояния наличия этого инстанса. Ну или повторно затереть (с помощью undefined) состояние в метод close, что, к сожалению, вызовет лишнюю отрисовку:
#development #frontend #react
Представим, что у нас есть некоторый провайдер, в котором:
⁃ open создаёт инстанс класса SomeClass и помещает его в useState;
⁃ close затирает это инстанс с помощью null;
⁃ instance передаётся через контекст ниже и где-то там используется
function SomeProvider() {
const [instance, setInstance] = useState(null);
const open = useCallback(() => {
setInstance(new SomeClass());
}, []);
const close = useCallback(() => {
setInstance(null);
}, []);
return <>...</>;
}
На первый взгляд кажется, что после выполнения метода close и присвоения состоянию instance значения null, ранее созданные инстанс класс SomeClass более недостижим и может быть удалён из памяти сборщиком мусора, но это не так. Ссылка на инстанс всё ещё будет храниться в React Fiber.
Исправить это можно. Например, хранить инстанс в useRef. Однако, в этом случае useState вам всё равно потребуется для хранения состояния наличия этого инстанса. Ну или повторно затереть (с помощью undefined) состояние в метод close, что, к сожалению, вызовет лишнюю отрисовку:
// Костыль через setTimeout
setInstance(null);
setTimeout(() => setInstance(void 0));
// Или через api react
flushSync(() => {
setInstance(null);
});
flushSync(() => {
setInstance(void 0);
});
#development #frontend #react
🔥1
Batching обновлений в React и SolidJS
Batching — группировка нескольких обновлений состояния в одно, для оптимизации количества "отрисовок" компонента. Если коротко, в React он автоматический, а вот в SolidJS ручной. Но в случае React есть нюансы, куда же без них. 😅
React
Автоматическую группировку в React вы получите в следующих случаях:
⁃ В синхронном обработчике события;
⁃ В асинхронном обработчике (только для v18+ с createRoot).
Во всех остальных случаях группировки не будет. Обратите внимание, что при переходе на 18 версию (и createRoot), в большом приложении реально словить баги в самых неожиданных местах, т.к. изменения состояний в асинхронных блоках теперь будут группироваться, чего не было в 17 версии и ниже.
Если вам нужно вынести какое-то обновление из автоматической группировки, лучше использовать специальный метод flushSync из пакета react-dom, вместо нулевого setTimeout.
Если совсем подробно, то вот тут всё описано https://github.com/reactwg/react-18/discussions/21
SolidJS
В SolidJS вы сами решаете когда нужна группировка обновлений, через специальную утилиту batch. Использовать её, наверное, имеет смысл только в случае если от обновляемых сигналов (состояния) зависит какое-то дорогое вычисление. В противном случае можно обходится без неё.
#frontend #development #react #solidjs
Batching — группировка нескольких обновлений состояния в одно, для оптимизации количества "отрисовок" компонента. Если коротко, в React он автоматический, а вот в SolidJS ручной. Но в случае React есть нюансы, куда же без них. 😅
React
function Comp() {
const [first, setFirst] = useState();
const [second, setSecond] = useState();
function handleClick() {
setFirst(...);
setSecond(...);
}
return <>...</>;
}
Автоматическую группировку в React вы получите в следующих случаях:
⁃ В синхронном обработчике события;
⁃ В асинхронном обработчике (только для v18+ с createRoot).
Во всех остальных случаях группировки не будет. Обратите внимание, что при переходе на 18 версию (и createRoot), в большом приложении реально словить баги в самых неожиданных местах, т.к. изменения состояний в асинхронных блоках теперь будут группироваться, чего не было в 17 версии и ниже.
Если вам нужно вынести какое-то обновление из автоматической группировки, лучше использовать специальный метод flushSync из пакета react-dom, вместо нулевого setTimeout.
Если совсем подробно, то вот тут всё описано https://github.com/reactwg/react-18/discussions/21
SolidJS
function Comp() {
const [first, setFirst] = createSignal();
const [second, setSecond] = createSignal();
function handleClick() {
batch(() => {
setFirst(...);
setSecond(...);
});
}
return <>...</>;
}
В SolidJS вы сами решаете когда нужна группировка обновлений, через специальную утилиту batch. Использовать её, наверное, имеет смысл только в случае если от обновляемых сигналов (состояния) зависит какое-то дорогое вычисление. В противном случае можно обходится без неё.
#frontend #development #react #solidjs
GitHub
Automatic batching for fewer renders in React 18 · reactwg react-18 · Discussion #21
Overview React 18 adds out-of-the-box performance improvements by doing more batching by default, removing the need to manually batch updates in application or library code. This post will explain ...
🔥1
Пятничный наброс... 🙃
Пара слов о Feature-Sliced Design (FSD)
Есть такая модная “архитектурная методология”, которая представляет набор правил и соглашений по организации кода. В официальной документации написано, что главная цель этой методологии — сделать проект понятнее и стабильнее.
Читаешь документацию, туториал, статьи… в целом, вроде всё понятно, садишься делать реальный проект — начинаются вопросы. Смотришь примеры собранные на официальном сайте, а там везде по разному (я про детали, само собой). И тут понимаешь, что не понятно не только тебе.
И вот вопрос (риторический): а как тогда эта методология поможет сделать проект понятным, если сама методология не так уж и понятна и каждый разработчик трактует её по своему?
Стоит отметить, что в версии 2.1 FSD сделал шаг в сторону более логичного подхода, когда всё нужное для страницы не размазано по разным корневым папкам, а находится рядом (если не нужно шарить с другими страницами). Но как-то это уж больно похоже на изобретение колеса.
Как нибудь ещё вернусь к вопросу “как раскладывать файлы по папочкам” и покажу “свою” схему которую я использую уже более 5 лет.
П.С. FSD в шутку расшифровывают как Freaky-Sliced Design 😅
Пара слов о Feature-Sliced Design (FSD)
Есть такая модная “архитектурная методология”, которая представляет набор правил и соглашений по организации кода. В официальной документации написано, что главная цель этой методологии — сделать проект понятнее и стабильнее.
Читаешь документацию, туториал, статьи… в целом, вроде всё понятно, садишься делать реальный проект — начинаются вопросы. Смотришь примеры собранные на официальном сайте, а там везде по разному (я про детали, само собой). И тут понимаешь, что не понятно не только тебе.
И вот вопрос (риторический): а как тогда эта методология поможет сделать проект понятным, если сама методология не так уж и понятна и каждый разработчик трактует её по своему?
Стоит отметить, что в версии 2.1 FSD сделал шаг в сторону более логичного подхода, когда всё нужное для страницы не размазано по разным корневым папкам, а находится рядом (если не нужно шарить с другими страницами). Но как-то это уж больно похоже на изобретение колеса.
Как нибудь ещё вернусь к вопросу “как раскладывать файлы по папочкам” и покажу “свою” схему которую я использую уже более 5 лет.
П.С. FSD в шутку расшифровывают как Freaky-Sliced Design 😅
👍2
Интересное ограничение Typescript
При написании хука для более удобной работы со списками данных (объектов), столкнулся с интересной особенностью Typescript.
Для начала приведу пример (предполагаемого) использования:
Ключ необходим для таких методов как has, remove и т.п., благодаря чему в качестве аргумента достаточно будет передать значение этого поля. Например, для удаления элемента из списка достаточно вызвать:
Теперь давайте про Typescript. Если в качестве ключа я указал название поля с типом string, то и в методах типа remove, в качестве аргумента должна приниматься именно строка. В чём же проблема? А в том, что при указании типа до аргументов, а название ключа в аргументах, Typescript не поймёт вас и не сможет вывести необходимо значение в возвращаемом типе. Другими словами, и тип и ключ должны оба быть в одном месте:
В обоих случаях получается не так красиво как хотелось бы: в первом приходится дублировать имя ключа, а во втором вообще добавлять ещё один аргумент.
Есть и третий вариант решения (который тоже не очень):
Если что, вот ссылка на сам хук:
https://gist.github.com/SanichKotikov/510004365de8cafbd51623b581b5733c
Хотя кто знает, может решение всё же есть. Если найдёте, напишите.
При написании хука для более удобной работы со списками данных (объектов), столкнулся с интересной особенностью Typescript.
Для начала приведу пример (предполагаемого) использования:
// Если в качестве ключа используется параметр с именем id
const [list, handlers] = useListState<ItemType>();
// Если в качестве ключа необходимо указать конкретное имя поля
const [list, handlers] = useListState<ItemType>('someId');
Ключ необходим для таких методов как has, remove и т.п., благодаря чему в качестве аргумента достаточно будет передать значение этого поля. Например, для удаления элемента из списка достаточно вызвать:
handlers.remove('some-id-value');
Теперь давайте про Typescript. Если в качестве ключа я указал название поля с типом string, то и в методах типа remove, в качестве аргумента должна приниматься именно строка. В чём же проблема? А в том, что при указании типа до аргументов, а название ключа в аргументах, Typescript не поймёт вас и не сможет вывести необходимо значение в возвращаемом типе. Другими словами, и тип и ключ должны оба быть в одном месте:
// или до аргументов:
useListState<ItemType, 'someId'>('someId');
// или в аргументах:
useListState([] as ItemType[], 'someId');
В обоих случаях получается не так красиво как хотелось бы: в первом приходится дублировать имя ключа, а во втором вообще добавлять ещё один аргумент.
Есть и третий вариант решения (который тоже не очень):
// сначала необходимо создать хук с нужным типом
const useListState = createListState<ItemType>();
// а затем, при использовании, указывать ключ
useListState('someId');
Если что, вот ссылка на сам хук:
https://gist.github.com/SanichKotikov/510004365de8cafbd51623b581b5733c
Хотя кто знает, может решение всё же есть. Если найдёте, напишите.
Gist
useListState hook for React
useListState hook for React. GitHub Gist: instantly share code, notes, and snippets.
👍2
Архитектура: функциональное проектирование
Я недавно писал про архитектуру на примере React компонентов, где в конце приводится схема слоёного “пирога”, на которой сверху данные, посередине управляющие компоненты, а снизу “глупые” компоненты. А теперь давайте попробуем взглянуть на код чуть глобальнее.
Итак, в целом весь наш код можно разделить на три условные категории:
Данные
Тут всё просто, данные есть данные, например пользователь в системе или транзакция в банковской системе. Их преимущество в том, что они прозрачны и могут быть интерпретированы по-разному.
Вычисления
Это преобразование данных без побочных эффектов. Они не зависят от места и времени выполнения, и всегда возвращают одинаковый результат при одинаковых входных данных. Другими словами, это чистые функции.
Действия
Действия напротив, зависят от места и времени выполнения. Можно сказать что это функции с побочными эффектами. Например, сохранение или чтение пользователя из базы является действиями.
Обратите внимание на порядок, в котором описаны эти категории, он не случаен. Данные — фундамент любого продукта, и как уже было сказано, они прозрачны и интерпретировать их можно как угодно. Далее идут вычисления (чистые функции), их проще писать и тестировать. Это довольно обширный пласт и может включать в себя много уровней, от работы с типами и структурами языка программирования до всевозможной бизнес-логики разных уровней. А замыкают всё действия, оперируя данными и вычислениями. Этот уровень больше подвержен ошибкам и в него чаще всего вносятся изменения, и при этом его может быть не так просто протестировать. Именно при работе с последним уровнем нужно быть максимально внимательным.
А теперь давайте вернёмся к примеру React компонентов, и возможно вы уже понимаете почему так важно стремиться к тому, что бы у вас было как можно больше “глупых” компонентов и как можно меньше “умных”. Да, всё просто, глупые компоненты являются вычислениями, а умные (управляющие) — действиями. Глупые компоненты реже меняются, их легко переиспользовать и они меньше подвержены ошибкам. Управляющие компоненты напротив, меняются чаще, их практически невозможно использовать повторно, и шанс допустить в них ошибку гораздо выше.
Есть ещё один важный момент — если вычисление использует действие, то оно автоматически становится действием, со всеми вытекающими!
#development #frontend #architecture
Я недавно писал про архитектуру на примере React компонентов, где в конце приводится схема слоёного “пирога”, на которой сверху данные, посередине управляющие компоненты, а снизу “глупые” компоненты. А теперь давайте попробуем взглянуть на код чуть глобальнее.
Итак, в целом весь наш код можно разделить на три условные категории:
Данные
Тут всё просто, данные есть данные, например пользователь в системе или транзакция в банковской системе. Их преимущество в том, что они прозрачны и могут быть интерпретированы по-разному.
Вычисления
Это преобразование данных без побочных эффектов. Они не зависят от места и времени выполнения, и всегда возвращают одинаковый результат при одинаковых входных данных. Другими словами, это чистые функции.
Действия
Действия напротив, зависят от места и времени выполнения. Можно сказать что это функции с побочными эффектами. Например, сохранение или чтение пользователя из базы является действиями.
Обратите внимание на порядок, в котором описаны эти категории, он не случаен. Данные — фундамент любого продукта, и как уже было сказано, они прозрачны и интерпретировать их можно как угодно. Далее идут вычисления (чистые функции), их проще писать и тестировать. Это довольно обширный пласт и может включать в себя много уровней, от работы с типами и структурами языка программирования до всевозможной бизнес-логики разных уровней. А замыкают всё действия, оперируя данными и вычислениями. Этот уровень больше подвержен ошибкам и в него чаще всего вносятся изменения, и при этом его может быть не так просто протестировать. Именно при работе с последним уровнем нужно быть максимально внимательным.
А теперь давайте вернёмся к примеру React компонентов, и возможно вы уже понимаете почему так важно стремиться к тому, что бы у вас было как можно больше “глупых” компонентов и как можно меньше “умных”. Да, всё просто, глупые компоненты являются вычислениями, а умные (управляющие) — действиями. Глупые компоненты реже меняются, их легко переиспользовать и они меньше подвержены ошибкам. Управляющие компоненты напротив, меняются чаще, их практически невозможно использовать повторно, и шанс допустить в них ошибку гораздо выше.
Есть ещё один важный момент — если вычисление использует действие, то оно автоматически становится действием, со всеми вытекающими!
#development #frontend #architecture
👍3
Архитектура: раскладываем код по папкам
Примечание №1: под словом “компонент”, без приставки UI, будет подразумеваться именно компонент системы, а не интерфейса.
Примечание №2: какой бы вы подход не выбрали, этот, FSD, классический или ещё какой-то, это никак не скажется на качестве вашего кода и системы в целом. Правила это не главное, главное то как ими распорядиться.
Пришло время описать схему, по которой я “раскладываю код по папкам”, года так с 2019. Всё это время я пробовал менять в ней разные нюансы, сравнивал, делал выводы, но база оставалась всегда одна — фокус на предметной области. Максимально простое объяснение этой схемы можно выразить следующей строкой:
Предположим, вы пишите приложение для сети общепита. В данном случае предметная область будет включать сотрудников, филиалы, меню и заказы. Добавим ко всему этому “базовую” функциональность и компоненты касающиеся самого приложения и получим верхний уровень структуры:
📁 _core
📁 _ui-kit*
📂 branch
📂 employee
📂 menu
📂 order
📁 app
Папку _core, в теории, можно переносить из проекта в проект, т.к. она не должна содержать ничего что относится к нашей предметной области. А вот что она должна содержать, так это всевозможные утилиты для работы с примитивами языка, api платформы (fetch, storages), какие-то базовые хуки (если мы используем React или SolidJS), типы и т.п. Компоненты из папки _core можно использовать в любой части системы, при этом сама папка не может импортировать что-либо из соседних папок.
В папке _ui-kit лежат все базовые UI компоненты: кнопки, поля ввода, выпадающие списки и т.п. Компоненты этой папки можно использовать в любой части системы, кроме папки _core. В случае если вы используете сторонний ui-kit, то возможно, разумнее будет положить свои дополнительные UI компоненты в _core/ui.
В папке app хранится всё, что относится к специфике текущего приложения, но при этом не относится ни к _core, ни к предметной области. Это может быть общая разметка, интеграции со сторонними сервисами, логирование ошибок и т.п. Эта папка может использовать любые другие, но её использовать ни кто не может.
Все остальные папки верхнего уровня — “фичи” предметной области. Эти папки могут использовать компоненты из любой другой, кроме app. Кстати, в использовании компонентов из одной фичи в другой нет ни чего плохого, главное чтобы не было циклических зависимостей.
Второй уровень — Segment
Этот уровень описывает сегменты из которых может состоять любая папка первого уровня. Перечислю основные:
📁 type — typescript типы;
📁 lib — утилиты и хуки;
📁 context — контексты и их типы;
📁 ui — “глупые” UI компоненты;
📂 api — общение с сервером;
📂 control — “умные” UI компоненты и провайдеры;
📂 page — страницы;
Сразу отмечу что порядок, в котором выстроен этот список, имеет важное значение — сегмент может использовать всё что расположено выше него, и не может использовать всё что ниже. Например, page может импортировать модули из api, а вот ui не может.
Второй момент, на который стоит обратить внимание — api, control и page являются “умными” сегментами, т.к именно тут сосредоточены “действия”. Всё остальное должно относится к “вычислениям” или “данным”.
Иногда я думаю над тем, что бы добавить этим трём папкам какой-то префикс, например !, который бы отличал их от остальных. Но пока это открытый вопрос.
[продолжение ниже]
#development #frontend #architecture
Примечание №1: под словом “компонент”, без приставки UI, будет подразумеваться именно компонент системы, а не интерфейса.
Примечание №2: какой бы вы подход не выбрали, этот, FSD, классический или ещё какой-то, это никак не скажется на качестве вашего кода и системы в целом. Правила это не главное, главное то как ими распорядиться.
Пришло время описать схему, по которой я “раскладываю код по папкам”, года так с 2019. Всё это время я пробовал менять в ней разные нюансы, сравнивал, делал выводы, но база оставалась всегда одна — фокус на предметной области. Максимально простое объяснение этой схемы можно выразить следующей строкой:
Feature/Segment/Component
Предположим, вы пишите приложение для сети общепита. В данном случае предметная область будет включать сотрудников, филиалы, меню и заказы. Добавим ко всему этому “базовую” функциональность и компоненты касающиеся самого приложения и получим верхний уровень структуры:
📁 _core
📁 _ui-kit*
📂 branch
📂 employee
📂 menu
📂 order
📁 app
Папку _core, в теории, можно переносить из проекта в проект, т.к. она не должна содержать ничего что относится к нашей предметной области. А вот что она должна содержать, так это всевозможные утилиты для работы с примитивами языка, api платформы (fetch, storages), какие-то базовые хуки (если мы используем React или SolidJS), типы и т.п. Компоненты из папки _core можно использовать в любой части системы, при этом сама папка не может импортировать что-либо из соседних папок.
В папке _ui-kit лежат все базовые UI компоненты: кнопки, поля ввода, выпадающие списки и т.п. Компоненты этой папки можно использовать в любой части системы, кроме папки _core. В случае если вы используете сторонний ui-kit, то возможно, разумнее будет положить свои дополнительные UI компоненты в _core/ui.
В папке app хранится всё, что относится к специфике текущего приложения, но при этом не относится ни к _core, ни к предметной области. Это может быть общая разметка, интеграции со сторонними сервисами, логирование ошибок и т.п. Эта папка может использовать любые другие, но её использовать ни кто не может.
Все остальные папки верхнего уровня — “фичи” предметной области. Эти папки могут использовать компоненты из любой другой, кроме app. Кстати, в использовании компонентов из одной фичи в другой нет ни чего плохого, главное чтобы не было циклических зависимостей.
Второй уровень — Segment
Этот уровень описывает сегменты из которых может состоять любая папка первого уровня. Перечислю основные:
📁 type — typescript типы;
📁 lib — утилиты и хуки;
📁 context — контексты и их типы;
📁 ui — “глупые” UI компоненты;
📂 api — общение с сервером;
📂 control — “умные” UI компоненты и провайдеры;
📂 page — страницы;
Сразу отмечу что порядок, в котором выстроен этот список, имеет важное значение — сегмент может использовать всё что расположено выше него, и не может использовать всё что ниже. Например, page может импортировать модули из api, а вот ui не может.
Второй момент, на который стоит обратить внимание — api, control и page являются “умными” сегментами, т.к именно тут сосредоточены “действия”. Всё остальное должно относится к “вычислениям” или “данным”.
Иногда я думаю над тем, что бы добавить этим трём папкам какой-то префикс, например !, который бы отличал их от остальных. Но пока это открытый вопрос.
[продолжение ниже]
#development #frontend #architecture
[продолжение]
Третий уровень — Component
Тут только одно, но очень важное, правило: компонент может быть как единственным файлом, так и папкой с экспортом через index-файл внутри. Другими словами, импорт любого компонента вашей системы должен быть из трёх составляющих:
Ещё один важный момент: в папках первого и второго уровней не должно быть ни одного index-файла. Кого-то это может возмутить, мол как же так, но стоит вам попробовать и вы поймёте всю прелесть этого решения. 😎 По сути, в этой системе “публичным” интерфейсом обладают только её компоненты.
Также допустимые импорты:
Примеры не допустимых импортов:
Из этих примеров должно быть понятно, что схема не подразумевает глубокой вложенности папок, всё должно быть максимально плоско.
Думается мне, этим постом можно закрыть серию про архитектуру, по крайней мере на какое-то время.
П.С. Пишите хорошие (маленькие) функции, и хорошая архитектура сама подтянется.
#frontend #development #architecture
Третий уровень — Component
Тут только одно, но очень важное, правило: компонент может быть как единственным файлом, так и папкой с экспортом через index-файл внутри. Другими словами, импорт любого компонента вашей системы должен быть из трёх составляющих:
import { component } from 'feature/segment/component';
Ещё один важный момент: в папках первого и второго уровней не должно быть ни одного index-файла. Кого-то это может возмутить, мол как же так, но стоит вам попробовать и вы поймёте всю прелесть этого решения. 😎 По сути, в этой системе “публичным” интерфейсом обладают только её компоненты.
Также допустимые импорты:
// Соседний компонент
import { component } from '../component';
// Соседний файл
import { component } from './component';
Примеры не допустимых импортов:
../../something, ../../../something и т.п.
../component/file-inside
domain/segment/component/file-inside
Из этих примеров должно быть понятно, что схема не подразумевает глубокой вложенности папок, всё должно быть максимально плоско.
Думается мне, этим постом можно закрыть серию про архитектуру, по крайней мере на какое-то время.
П.С. Пишите хорошие (маленькие) функции, и хорошая архитектура сама подтянется.
#frontend #development #architecture
Интересное мнение про найм в современных реалиях:
https://habr.com/p/882392/
https://habr.com/p/882392/
Habr
Как нанять умного айтишника и не затратить 3-4 зарплаты
В последнее время все чаще в инфополе появляются выплески боли руководителей компаний и их HR на тему того, что наблюдается кадровый голод. Обзоры экспертов, аналитиков, заявления сотрудников...
👍2
Одна из важнейших особенностей работы над большим проектом — цена изменений!
Речь, конечно же, про изменения которые так или иначе касаются всего проекта, какие-то базовые библиотеки, функции, компоненты, подходы и т.п. В общем, всё то, чем постоянно приходится пользоваться и без чего не может обойтись практически ни одна страница проекта.
Сразу хочу предупредить, что какие-то локальные изменения, в последствии, тоже могут стать глобальными. Другими словами, не стоит думать что, например, я вот сейчас тут добавлю какую-то новую модную библиотеку и это ни чего не будет стоить. Вполне может оказаться так, что будет. Добавить что-то в проект и выпилить это потом из него, это далеко не одно и тоже! Помните про это.
Но давайте вернёмся к глобальным частям. Иногда так получается, что некоторые вещи в проекте, которые изначально казались удобными, “вдруг” становятся не такими уж и удобными, и вам приходит мысль это дело исправить. Первое и самое важное, что нужно сделать в этот момент — очень хорошо подумать, ведь ваше новое решение, которое вам в моменте покажется удобным, через пол года может “снова” оказаться не таким уж и удобным. И даже когда вам покажется что вы ну уж точно всё продумали, сделайте паузу, скушайте тви… посоветуйтесь с опытными коллегами, почитайте что-то на релевантную тему и т.д.
Отдельное внимание стоит обратить на моменты, когда планируете не заменить что-то, а сделать аналогичное (только “лучше”) рядом. Рекомендую всё-таки подумать именно над заменой, а к варианту “рядом” обращаться только в крайних случаях. Тут вы возможно хотите возразить, мол мы старый подход/функцию/компонент/библиотеку потом постепенно выпилим. Не выпилите! 🤪
Маленький, но очень важный совет: если всё же делаете “рядом”, например какую-то функцию. Пометьте устаревший вариант как deprecated, только не забудь указать причину или рекомендации, например:
И кстати, если содержимое старой функции можно заменить вызовом новой, это также стоит сделать. Как минимум вы удалите лишний код, а как максимум “источник” правды останется один.
Если всего этого не делать, то ваш большой проект, который со временем станет ещё больше, может превратиться в месиво разных подходов, библиотек делающих одно и тоже, одинаковых компонентов и т.п. Иногда лучше жить с одним не очень хорошо, но работающим решением, чем плодить сущности делающие одно и тоже.
#development
Речь, конечно же, про изменения которые так или иначе касаются всего проекта, какие-то базовые библиотеки, функции, компоненты, подходы и т.п. В общем, всё то, чем постоянно приходится пользоваться и без чего не может обойтись практически ни одна страница проекта.
Сразу хочу предупредить, что какие-то локальные изменения, в последствии, тоже могут стать глобальными. Другими словами, не стоит думать что, например, я вот сейчас тут добавлю какую-то новую модную библиотеку и это ни чего не будет стоить. Вполне может оказаться так, что будет. Добавить что-то в проект и выпилить это потом из него, это далеко не одно и тоже! Помните про это.
Но давайте вернёмся к глобальным частям. Иногда так получается, что некоторые вещи в проекте, которые изначально казались удобными, “вдруг” становятся не такими уж и удобными, и вам приходит мысль это дело исправить. Первое и самое важное, что нужно сделать в этот момент — очень хорошо подумать, ведь ваше новое решение, которое вам в моменте покажется удобным, через пол года может “снова” оказаться не таким уж и удобным. И даже когда вам покажется что вы ну уж точно всё продумали, сделайте паузу, скушайте тви… посоветуйтесь с опытными коллегами, почитайте что-то на релевантную тему и т.д.
Отдельное внимание стоит обратить на моменты, когда планируете не заменить что-то, а сделать аналогичное (только “лучше”) рядом. Рекомендую всё-таки подумать именно над заменой, а к варианту “рядом” обращаться только в крайних случаях. Тут вы возможно хотите возразить, мол мы старый подход/функцию/компонент/библиотеку потом постепенно выпилим. Не выпилите! 🤪
Маленький, но очень важный совет: если всё же делаете “рядом”, например какую-то функцию. Пометьте устаревший вариант как deprecated, только не забудь указать причину или рекомендации, например:
/** @deprecated use %name% from %path% */
И кстати, если содержимое старой функции можно заменить вызовом новой, это также стоит сделать. Как минимум вы удалите лишний код, а как максимум “источник” правды останется один.
Если всего этого не делать, то ваш большой проект, который со временем станет ещё больше, может превратиться в месиво разных подходов, библиотек делающих одно и тоже, одинаковых компонентов и т.п. Иногда лучше жить с одним не очень хорошо, но работающим решением, чем плодить сущности делающие одно и тоже.
#development
Overrides в package.json (NPM)
Пока многие сидят на yarn и pnpm, а может и ещё чём-то, npm тоже не стоит на месте. На днях обнаружил что в нём тоже есть overrides, причём уже довольно давно 😅
Overrides позволяет переопределять версии вложенных пакетов:
Конкретно в этом случае, в пакете storybook-solidjs-vite используется последняя не стабильная версия пакета @storybook/builder-vite. Переопределение версии позволяет избавиться от ошибки запуска storybook.
Кстати, символ $ позволяет переопределять версию на ту, которую мы определили в своих зависимостях, в данном случае в devDependencies.
Вот полная дока:
https://docs.npmjs.com/cli/v10/configuring-npm/package-json?v=true#overrides
#development #frontend #package #npm
Пока многие сидят на yarn и pnpm, а может и ещё чём-то, npm тоже не стоит на месте. На днях обнаружил что в нём тоже есть overrides, причём уже довольно давно 😅
Overrides позволяет переопределять версии вложенных пакетов:
"devDependencies": {
"@storybook/builder-vite": "8.5.6",
"storybook-solidjs-vite": "1.0.0-beta.6"
},
"overrides": {
"storybook-solidjs-vite": {
"@storybook/builder-vite": "$@storybook/builder-vite"
}
}
Конкретно в этом случае, в пакете storybook-solidjs-vite используется последняя не стабильная версия пакета @storybook/builder-vite. Переопределение версии позволяет избавиться от ошибки запуска storybook.
Кстати, символ $ позволяет переопределять версию на ту, которую мы определили в своих зависимостях, в данном случае в devDependencies.
Вот полная дока:
https://docs.npmjs.com/cli/v10/configuring-npm/package-json?v=true#overrides
#development #frontend #package #npm
Npmjs
package.json | npm Docs
Specifics of npm's package.json handling
👍1🔥1
Делюсь лайфхаком как узнавать о новых фишках html, css и js которые уже работают в последних версиях браузеров: После очередного обновления Safari заходим на https://webkit.org и смотрим там описание что добавили 😅
WebKit
Open Source Web Browser Engine
👍2😁1
Branded Types в TypeScript
Branded types — полезный инструмент, хотя и используют его довольно редко.
Почти все данные с которыми мы работаем в коде, так или иначе, сводятся к примитивам. При этом, по смыслу, одни строки могут сильно отличаться от других, собственно, как и числа. Простой пример:
С точки зрения бизнес логики все эти строковые поля имеют разный тип данных, и работать с ними нужно по-разному, но для нашего кода всё это просто строки. Другими словами, типы как бы есть, но их как бы и нет… Ситуация усложняется, если у вас большая распределённая система и сервисам нужно точно знать какие данные передавать друг другу.
В идеале, тип пользователя должен выглядеть вот так:
Хочу сразу обратить внимание на ещё один важный нюанс, который на примере интерфейса пользователя может быть не так очевиден — читабельность. Например, понятно что поле phone содержит телефон, хотя строки с телефонами тоже бывают разные. А вот что по вашему может содержать следующее поле?
Наверное, какой-то id, хотя на деле всё что угодно. Но если использовать branded types, всё становится очевидным:
Что же из себя представляют эти branded types?
По сути, это особый вид примитива, например строки:
Однако для TypeScript это будет отдельный тип. Другими словами, если функция форматирования телефона принимает в качестве аргумента PhoneString, обычную строку передать не получится:
Важно: объект в branded types должен быть уникален для каждого нового типа.
П.С. Можно кстати сделать специальную утилиту:
#development #frontend #typescript
Branded types — полезный инструмент, хотя и используют его довольно редко.
Почти все данные с которыми мы работаем в коде, так или иначе, сводятся к примитивам. При этом, по смыслу, одни строки могут сильно отличаться от других, собственно, как и числа. Простой пример:
interface User {
id: string;
name: string;
phone: string;
email: string;
}
С точки зрения бизнес логики все эти строковые поля имеют разный тип данных, и работать с ними нужно по-разному, но для нашего кода всё это просто строки. Другими словами, типы как бы есть, но их как бы и нет… Ситуация усложняется, если у вас большая распределённая система и сервисам нужно точно знать какие данные передавать друг другу.
В идеале, тип пользователя должен выглядеть вот так:
interface User {
id: UUID;
name: string;
phone: PhoneString;
email: EmailString;
}
Хочу сразу обратить внимание на ещё один важный нюанс, который на примере интерфейса пользователя может быть не так очевиден — читабельность. Например, понятно что поле phone содержит телефон, хотя строки с телефонами тоже бывают разные. А вот что по вашему может содержать следующее поле?
recipientExternalId: string;
Наверное, какой-то id, хотя на деле всё что угодно. Но если использовать branded types, всё становится очевидным:
recipientExternalId: PhoneString;
Что же из себя представляют эти branded types?
По сути, это особый вид примитива, например строки:
type PhoneString = string & { __brand: 'PhoneString' };
Однако для TypeScript это будет отдельный тип. Другими словами, если функция форматирования телефона принимает в качестве аргумента PhoneString, обычную строку передать не получится:
function formatPhone(phone: PhoneString): string {
return phone.replace(
/^(\d)?(\d{3})(\d{3})(\d{4})$/,
'+$1 ($2) $3-$4'
);
}
formatPhone('11111111111');
// => TS Error:
// Argument of type 'string' is not assignable
// to parameter of type 'PhoneString'.
Важно: объект в branded types должен быть уникален для каждого нового типа.
П.С. Можно кстати сделать специальную утилиту:
declare const __brand: unique symbol;
type Branded<T, B extends string> = T & { [__brand]: B };
type PhoneString = Branded<string, 'PhoneString'>;
type EmailString = Branded<string, 'EmailString'>;
#development #frontend #typescript
👍5
Попался доклад про дата-ориентированное программирование.
https://youtu.be/oukpjkET6j4
https://youtu.be/oukpjkET6j4
YouTube
Алексей Золотых — Дата-ориентированное программирование
Подробнее о конференции HolyJS: https://jrg.su/EM4wwV
— —
Доклад посвящен Data-Oriented Programming (DOP) — подходу, который нацелен на оптимизацию работы и производительность через фокусировку на структуре данных.
Спикер разобрал ключевые концепции DOP…
— —
Доклад посвящен Data-Oriented Programming (DOP) — подходу, который нацелен на оптимизацию работы и производительность через фокусировку на структуре данных.
Спикер разобрал ключевые концепции DOP…
Минимальный фронтовый Dockerfile с поддержкой Brotli
Brotli — алгоритм сжатия данных. Наравне с gzip, позволяет уменьшить размер передаваемых по сети данных, но делает это несколько эффективнее. Если gzip можно использовать “из коробки”, то вот brotli приходится настраивать руками.
Для включения, также, необходимо добавить несколько строк в ваш nginx.conf:
#development #frontend #docker #nginx
Brotli — алгоритм сжатия данных. Наравне с gzip, позволяет уменьшить размер передаваемых по сети данных, но делает это несколько эффективнее. Если gzip можно использовать “из коробки”, то вот brotli приходится настраивать руками.
# build container
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm i
RUN npm run build
# runtime container
FROM alpine
RUN apk add brotli nginx nginx-mod-http-brotli
COPY --from=builder /app/dist /usr/share/nginx/html
COPY ./nginx/nginx.conf /etc/nginx/http.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80
EXPOSE 443
Для включения, также, необходимо добавить несколько строк в ваш nginx.conf:
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json;
#development #frontend #docker #nginx
🔥5
Код 10 летней давности 😱
Наткнулся на свою реализацию всем известной игры “Змейка”, было такое тестовое задание. Коду почти 10 лет, там даже es6 ещё нет. 😅
Работает как на клавиатуре (на стрелочках), так и на телефонах (тап по сторонам игрового поля).
Код: https://github.com/SanichKotikov/test-snake-game/blob/master/index.html
Игра: https://sanichkotikov.github.io/test-snake-game/
П.С. Кто помнит для чего эта точка с запятой в начале кода? 😅
Наткнулся на свою реализацию всем известной игры “Змейка”, было такое тестовое задание. Коду почти 10 лет, там даже es6 ещё нет. 😅
Работает как на клавиатуре (на стрелочках), так и на телефонах (тап по сторонам игрового поля).
Код: https://github.com/SanichKotikov/test-snake-game/blob/master/index.html
Игра: https://sanichkotikov.github.io/test-snake-game/
П.С. Кто помнит для чего эта точка с запятой в начале кода? 😅
;(function (){
// code
})();
🔥4