Философия программирования
192 subscribers
17 photos
33 links
Frontend без воды: говорим о технологиях, архитектуре, принципах, кодстайле и том, как превращать хаос в систему.
Download Telegram
Автоматическая генерация TypeScript типов API, помогает или нет?

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

- Типы всегда соответствуют актуальному API.
- Не нужно писать типы руками.

На практике же, обычно это превращается в следующее:

- Файл(ы) с типами со временем становятся очень большими, и редакторы начинают не слабо так подвисать при работе с ними.
- Типы действительно соответствуют API, вот только 100% соответствующего реальности API я почти ни когда не видел.
- Генераторы имеют свойство переставать работать в самый не подходящий момент. На решения этих проблем приходится тратить дополнительное время.
- Автоматическое обновление не получается сделать полностью автоматическим, например из-за проблемы выше, поэтому файл(ы) с типами могут не слабо так устаревать. Доходит до обновления типов отдельной задачей.

В итоге вместо экономии времени и соответствия типов между фронтом и беком, получаем ровно противоположный эффект. Мало того, что вы потратите время на настройку и внедрение этой системы, так ещё придётся периодически её донастраивать и решать появляющиеся проблемы.

При этом, ни кто не говорит что типы нужно писать руками, есть куда более простые решения не требующие настройки и внедрения, например внешний инструмент (https://transform.tools/json-to-typescript) который сформирует типы на основе json примера.

Если уж и настраивать генератор типов в проекте, стоит хотя бы позаботиться о том, чтобы типы генерировались в отдельные файлы (папки), и только те которые нужны для конкретной задачи, а не все подряд.
Демо-проект с реализацией SRP (Secure Remote Password) + подпись последующих запросов:

Исходный код: https://github.com/SanichKotikov/srp-demo
А тут можно поиграться: https://sanichkotikov.github.io/srp-demo/

> Протокол SRP позволяет пользователю идентифицировать себя на сервере, при этом не передавая своего пароля, то есть подтвердить тот факт, что он знает свой пароль, и только этот факт.

#security #frontend #sourcecode #development #demo
Странная ошибка ERR_UPLOAD_FILE_CHANGED в Chrome при воспроизведении аудио

Столкнулся с такой ситуацией, что у некоторых пользователей при попытке воспроизвести аудио возникает ошибка ERR_UPLOAD_FILE_CHANGED. При этом, повторит её локально не получается, в инкогнито она не воспроизводится, какой-то закономерности обнаружить не удалось, в общем магия…

Подопытный код:


const blob = await fetch(src).then((resp) => resp.blob());
audio.src = URL.createObjectURL(blob);
audio.play();


Меняем на:


const { type, file } = await fetch(src).then((resp) => (
resp.arrayBuffer().then((file) => ({
type: resp.headers.get('content-type'),
file,
}))
));
const blob = new Blob([file], { type: type || void 0 });
audio.src = URL.createObjectURL(blob);
audio.play();


И чудесным образом всё начинает работать.

#frontend #development
Про технические собеседования

Технические собеседования на позицию фронтенд разработчика, что с ними не так и как это исправить. Обычно, тех. интервью состоит из двух этапов: теория и “лайф-кодинг”. Давайте по порядку:

Теория

Важно понимать кого и на какую позицию вы собеседуете. Одно дело начинающего, и совсем другое старшего разработчика или вообще тех. лида. Но и в том и другом случае, это должно быть именно СО-беседование, т.е. двух-стороннее общение, а не допрос или экзамен как это обычно бывает. Да, схемы типа “мы тебя сейчас поспрашиваем, а потом в конце ты задашь свои вопросы” не тянут на общение.

Самое худшее что вы можете сделать, это просто сухо пройтись по вопросам, начиная от того что такое замыкание и прототипное наследование, до каких-то специфических кейсов библиотеки которую вы используете (например, React). Если для начинающих такая схема ещё терпима, то для разработчиков с опытом 10+ лет это просто плевок в лицо. Например, от просьб рассказать в каком порядке выполнятся console.log в коде с тайм-аутами и промисами разного вида, хочется сказать “удачи в поисках” и прекратить пустую трату времени.

Вместо этого необходимо расспросить кандидата про его опыт, в чём по его мнению он силён, а в чём не очень, углубиться в сильные стороны, и вскользь затронуть слабые. Узнать какие крутые штуки он делал. Не для галочки, как это обычно происходит, а с дополнительными вопросами о реализации, что и почему было сделано именно так, с какими проблемами столкнулся и т.д. Идеально, если вы вспомните похожую задачу в вашем проекте и обсудите это. Задача — узнать, как именно кандидат может дополнить команду и насколько подходит на позицию.

Опять же, не забываем про уровень. Начинающий вряд ли успел поделать что-то интересное, нужно это проговорить, и попросить рассказать про типовые задачи, может быть что-то сложное было и т.д. Если у кандидата есть open source или пет-проекты, хорошо бы посмотреть их заранее и обсудить по возможности. Если кандидат имеет большой опыт, бОльший упор стоит делать на архитектуру, как бы он построил новое приложение, какие библиотеки выбрал бы и почему.

Расскажите какие у вас сейчас задачи, что бы хотели изменить, спросите мнение кандидата, как бы он подошёл к решению (естественно, при наличии соответствующего опыта).

Лайф-кодинг

Как по мне, этот этап ни каким образом не даст вам представление о том как именно кандидат думает и пишет код. Никто и никогда не пишет код в браузере в незнакомом редакторе без автодополнений, без интернета, да ещё и когда на него смотрят “следователи” и ждут что он это всё как-то будет комментировать. Даже очень опытные, в такой ситуации, могут забыть про элементарные вещи и выглядеть глупо.

Просить решать абстрактные задачки на алгоритмы, когда ваши обычные задачи это “красить кнопочки” и “клепать формы” — тоже максимально не эффективный подход. И да, не льстите себе, у вас именно такие задачи, ну если вы не пишете очередной клон ворда или экселя.

Лучше подготовьте небольшой абстрактный (или реальный) код с какими-то ошибками и попросите провести ревью этого кода, заодно поговорите про ревью в целом, зачем оно, как его проводить, на что обращать внимание и т.п.

Если у кандидата есть GitHub, посмотрите код там.

За сим, хороших вам собеседований и отличных вакансий.

#interview #frontend #development
👍5
Предупреждение для тех, кто хочет перейти с Webpack на Vite

Небольшой пост-предупреждение для тех, кто (вдруг) задумывается перейти с Webpack на Vite и у кого не крохотный проект, а что-то более комплексное.

Если совсем коротко, не делайте этого!

Если чуть подробнее, то:

- Нормальных инструментов анализа бандла нет, Statoscope вы будете видеть только в своих влажных снах;
- Нормальной настройки, чтобы в лучшем случае обновлялся хеш одного (последнего) чанка, а не всей цепочки до него, нет (писал про это);
- Чтобы нормально настроить разделение чанков вам придётся пострадать, но скорее всего у вас вообще ничего не получится. Про вендор чанк авторы вообще написали: думайте сами, но так уж и быть вот вам deprecated плагин (который работает не так, как вы можете этого ожидать);
- Нормального (единого) кода ошибки загрузки css нет, только ошибка браузера (про это тоже писал);
- Плагинов всё же маловато, и их качество и работоспособность порой сильно хромает.

П.С. Если вы думаете что Webpack медленный, то скорее всего у вас что-то не правильно настроено.

П.П.С. Если вам всё равно на бандлы, чанки, анализ и прочие страшные вещи, а 10 мб вашего великолепного Javascript кода это нормально, пользователи потерпят, то можно и Vite 😅

#frontend #development #webpack #vite
[SolidJS] Suspense + createResource

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

Возьмём для примера следующий код:


<Suspense fallback={<div>Loading app...</div>}>
<List />
</Suspense>



const List: Component = () => {
const [list] = createResource(fetchList);

return (
<section>
<header>
<h4>Title</h4>
</header>
<Suspense fallback={<div>Loading list...</div>}>
<ul>
<For each={list()}>
{(item) => <li>Item #{item}</li>}
</For>
</ul>
</Suspense>
</section>
);
};


Тут всё ожидаемо: пока приложение загружает список, пользователь видит заголовок и надпись “Loading list”.

Но давайте, например, добавим отображение количество элементов списка в заголовке:


const List: Component = () => {
const [list] = createResource(fetchList);

const count = createMemo(() => {
return list()?.length;
});

return (
<section>
<header>
<h4>Title <span>{count()}</span></h4>
</header>
<Suspense fallback={<div>Loading list...</div>}>
<ul>
<For each={list()}>
{(item) => <li>Item #{item}</li>}
</For>
</ul>
</Suspense>
</section>
);
};


И вот теперь, вместо казалось бы ожидаемого “Loading list”, пользователь увидит “Loading app”.

С одной стороны всё логично — мы добавили чтение из list за пределы внутреннего Suspense, поэтому отработал Suspense выше. Но как мне кажется, простое добавление createMemo (или чего-то подобного) не должно менять порядок отрисовки.

#frontend #development #solidjs
👍1
Что не так с React

Вот уже почти 10 лет я использую React, как для коммерческой разработки, так и для персональной. Из них 4 года, параллельно, использую и SolidJS. И как известно, именно в сравнении начинаешь лучше понимать плюсы и минусы обеих сторон.

Почему именно SolidJS, а не Angular, Vue или что-то другое? Всё просто — верхнеуровнево они очень похожи. Я бы даже сказал что SolidJS умышленно скопировал JSX компоненты и “хуки”, но это лишь на уровне идеи, работает же всё иначе.

Что же не так с React?

Количество выполнений тела функции-компонента (и всего содержимого по цепочке вниз). Бороться с лишними выполнениями несложно, но мало кто хочет тратить на это время. В реальности почти ни кто не хочет думать нужно ли в текущем компоненте применять memo, useMemo, useCallback, как часто будут заново создаваться массивы и объекты, как правильно инициализировать какой-нибудь класс и т.п. Зачастую это приводит к проблемам производительности в больших приложениях, и порой достаточно серьёзным. А на решение этих проблем приходится тратить дополнительное время.

Необходимость (правильно) указывать зависимости хуков. Даже правило для линтера придумали специальное… которое иногда больше мешает, чем помогает. А реализацию сложного эффекта (или вычисления), который должен зависеть от одной переменной, но при этом не должен зависеть от пары других, всегда имея их актуальные значения, кроме как через костыль в виде useRef, кажется, и не решить. В общем, опять требуется дополнительные временные затраты. Ах да, ещё вы точно не обойдёте срача на ревью по поводу нужен в каком-то конкретном месте useMemo или нет.

Виртуальный DOM. Да да, то что когда-то было преимуществом, теперь становится недостатком (в контексте веб-разработки). SolidJS прекрасно обходится без него, при этом меняя в реальном DOM-е только то, что нужно, благодаря системе сигналов. Нет VDOM-а — нет дополнительных вычислений и потребления памяти.

Большой вес и отсутствие tree shaking. За столько лет в библиотеку много чего было написано, и хотите вы того или нет, всё это попадёт в вашу сборку.

Тем не менее это не делает React плохим, а Solid хорошим, у всего есть свои плюсы и минусы. Но всё же, если есть такая возможность, я выберу именно SolidJS.

#frontend #development #react #solidjs
👍1
Телеграмм такой удобный что приходится постить картинки отдельно от текста 😅 Текст тоже придётся разделить на два.
Архитектура: размазывание ответственности

Рассмотрим одну из основных проблем при разработке (архитектуре) веб-приложений, которую я называю “размазывание ответственности”. В данном случае, речь пойдёт только про один аспект — прямая связь UI компонентов со слоем данных (через импорты) и, как организовать эту связь так, что бы и сам код и работа с ним была чуть более предсказуема и понятна.

Пример №1

[См. первое изображение]

Есть некоторое приложение (Router), на котором есть страница (Screen) и хук с классом, и всё это завязано на работу с одним значением в локальном хранилище (Storage). Обратите внимание на жёлтые стрелки — это то, как именно компоненты связаны с хранилищем:

- Router содержит действие, которое удаляет значение;
- Router выполняет hook, который создаёт Class (читает и меняет значение);
- ScreenHeader читает значение.

В этих связях и заключается проблема, их много и они разбросаны по разным частям приложения как придётся. Всё это усугубляется ещё и тем, что кто-то обращается к хранилищу напрямую (через API браузера), а кто-то через специальный хук (его нет на схеме). В подобном коде вообще не очевидно что и где происходит, как говорится “без поллитра не разберёшься”.

А вот, как это должно быть:

[См. второе изображение]

Во-первых, все действия с ячейкой хранилища должны быть собраны в ОДНОМ единственном месте. В нашем случае это Class. Это кстати не означает что, например, ScreenHeader теперь должен быть (напрямую) связан с Class, что и отображено на схеме но может быть не очевидно.

Во-вторых, точка связи между UI и Storage должна быть также ОДНА (“где вошли, там и вышли”), через hook и Class. А до ScreenHeader значение можно пробросить, например, через параметры компонентов.

#frontend #development
👍1
...продолжение:

Пример №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, но на самом деле, при желании, отменять можно всё что угодно.

Самым вкусным, на мой взгляд, является отмена пачки подписок на события одной командой:


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
👍3
Рендер массивов в 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
🔥2
Освобождение памяти в 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


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
🔥1
Пятничный наброс... 🙃

Пара слов о Feature-Sliced Design (FSD)

Есть такая модная “архитектурная методология”, которая представляет набор правил и соглашений по организации кода. В официальной документации написано, что главная цель этой методологии — сделать проект понятнее и стабильнее.

Читаешь документацию, туториал, статьи… в целом, вроде всё понятно, садишься делать реальный проект — начинаются вопросы. Смотришь примеры собранные на официальном сайте, а там везде по разному (я про детали, само собой). И тут понимаешь, что не понятно не только тебе.

И вот вопрос (риторический): а как тогда эта методология поможет сделать проект понятным, если сама методология не так уж и понятна и каждый разработчик трактует её по своему?

Стоит отметить, что в версии 2.1 FSD сделал шаг в сторону более логичного подхода, когда всё нужное для страницы не размазано по разным корневым папкам, а находится рядом (если не нужно шарить с другими страницами). Но как-то это уж больно похоже на изобретение колеса.

Как нибудь ещё вернусь к вопросу “как раскладывать файлы по папочкам” и покажу “свою” схему которую я использую уже более 5 лет.

П.С. FSD в шутку расшифровывают как Freaky-Sliced Design 😅
👍2
Интересное ограничение Typescript

При написании хука для более удобной работы со списками данных (объектов), столкнулся с интересной особенностью 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

Хотя кто знает, может решение всё же есть. Если найдёте, напишите.
👍2