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

Столкнулся с занятной особенностью мобильного Safari. Если явно не указать внутренние боковые отступы (paddings) у элемента button, браузер проставит их автоматически отталкиваясь от размера шрифта.

Кстати, если вы хотите полностью сбросить стили кнопок по умолчанию:


button {
padding: 0;
margin: 0;
font-size: inherit;
border: none;
background: none;
appearance: none;
}


#development #css #safari
👍3🔥1
Vite vs Webpack: нюанс с React.memo()

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

Важно: В этом примере не использовался параметр sideEffects, про него напишу отдельно.

Итак, есть некий модуль, из которого торчит два компонента:


// Простой компонент
export { Test } from './test';

// Компонент обёрнутый в React.memo
export { TestMemo } from './test-memo';


Однако, используется только один — Test. И тут главный вопрос: что произойдёт с TestMemo при сборке проекта? Логично предположить, что раз он не используется, то и в бандл он не попадёт. Но это не совсем так!

Фрагмент сборки от Webpack:


function f() {
return (0, d.jsx)("p", {children: "TEST"})
}


Фрагмент сборки от Vite:


function a() {
return e.jsx("p", {children: "TEST"})
}

// Пу пу пу…
t.memo(() => e.jsx("p", {children: "MEMO"}));


Vite, в отличие от Webpack, оставляет в бандле все компоненты которые обёрнуты в memo, forwardRef и подобные функции. И как следствие, он оставляет и всё что используется из этих компонентов. В общем, в большом проекте это может быть довольно много кода, который может тянуть внешние библиотеки и т.д. и т.п.

#development #frontend #webpack #vite
🔥4
Children в SolidJS и порядок выполнения

Недавно столкнулся с не совсем очевидным моментом, связанным с props.children в SolidJS. Для понимания проблемы возьмём вот такой код:


function Wrapper(props) {
return props.when
? props.wrap(props.children, props.when)
: props.children;
}

function App() {
return (
<Wrapper
when={true}
wrap={(content) => (
<Provider>{content}</Provider>
)}
>
<Content />
</Wrapper>
);
}


Примечание: Опустим момент, что компонент Wrapper не реактивный, сейчас нас это не интересует.

Вопреки ожиданиям, компоненты выполнятся в следующем порядке: App > Wrapper > Content > Provider. В результате чего, контекст из Provider будет недоступен в компоненте Content. Чтобы понять, что пошло не так, давайте посмотрим во что превращается JSX при сборке проекта:


function App() {
return _$jsx(Wrapper, {
when: true,
wrap: content => _$jsx(Provider, {
children: content
}),
get children() {
return _$jsx(Content, {});
}
});
}


Параметр children превратился в getter функцию, которая будет выполнена при любой попытке чтения! Например, при передаче в качестве аргумента в функцию:


props.wrap(props.children, props.when)


Очевидным решением проблемы, в данном случае, будет замена интерфейса параметра children компонента Wrapper, с JSX на функцию, вызов которой вернёт этот самый JSX:


function App() {
return (
<Wrapper
when={true}
wrap={(content) => (
<Provider>{content()}</Provider>
)}
>
{() => <Content />}
</Wrapper>
);
}


#development #frontend #solidjs #javascript #jsx
🔥2
React != SolidJS на примере эффектов

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

П.С. Менять состояние из эффектов — довольно плохая идея и лучше такого кода избегать, в React так точно.

#development #frontend #solidjs #react #javascript
🔥1
На счёт предыдущего поста. Если очень сильно хочется как в SolidJS но не хочется/можется уйти от React, то есть вот такой пакет: https://www.npmjs.com/package/@preact/signals-react
🔥1
Причина закрытия WebSocket соединения

Если вы работали с WebSocket без каких либо обёрток, возможно вы обратили внимание, что метод close может принимать два параметра: код и причину закрытия. А CloseEvent, в свою очередь, оба эти значения может содержать.


const socket = new WebSocket(url);

socket.onclose = (event) => {
console.log(event.reason);
};

socket.close(1000, 'Reason');


Однако, этот код не гарантирует что в логе будет 'Reason'! А всё потому, что при вызове метода close, сначала отправляется соответствующая команда на сервер, который кстати может и не ответить. Но даже если он ответил, нет гарантий что он вернёт нам эту же причину.

В общем, CloseEvent.reason — причина закрытия с сервера, поэтому там может быть всё что угодно.

П.С. Есть ещё CloseEvent.wasClean, по которому можно понять было ли соединение закрыто “чисто” или что-то пошло не так.

#development #frontend #javascript
👍2👎1
До кучи, вот небольшое приложение https://github.com/SanichKotikov/ws-test-app/ Писал чтобы потестировать как WebSocket будет себя вести на мобильных устройствах, в том числе в PWA. Там же есть ссылка на уже развёрнутое приложение.
👍2
Принудительное выполнение компонента в SolidJS

Помним, что компоненты в SolidJS, в отличие от React, выполняются только “один” раз. Однако, бывают случаи когда необходимо выполнить компонент “заново” при изменении каких-то данных. Например, нужно полностью обновить форму редактирования при выборе другого блюда:


<Resource data={dishData} onReload={dishActions.refetch}>
{(dish) => (
<Show keyed when={dish().id}>
<DishForm
dish={dish()}
categories={categories()}
onSubmit={onDishSave}
/>
</Show>
)}
</Resource>


Параметр keyed компонента Show как раз для этого и предназначен. При этом, в параметре when необходимо указать значение, при изменении которого произойдёт повторное выполнение.

#development #frontend #javascript #solidjs
🔥1
TypeScript + .filter(Boolean)


const numbers = [1, 2, null, void 0, 3];
const foo = numbers.filter(Boolean);


Тип переменной foo будет соответствовать (number | null | undefined)[], хотя в действительности ни null ни undefined в массиве не будет.

Обычно предлагается следующее решение:


numbers.filter((n): n is number => !!n);


Но есть вариант получше:


// global.d.ts

type Falsy = false | '' | 0 | 0n | null | undefined;
type Truthy<T> = T extends Falsy ? never : T;

interface Array<T> {
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
filter<S extends T>(predicate: BooleanConstructor, thisArg?: any): Truthy<S>[];
}

interface ReadonlyArray<T> {
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
filter<S extends T>(predicate: BooleanConstructor, thisArg?: any): Truthy<S>[];
}


Теперь numbers.filter(Boolean) будет возвращать number[].

П.С. Есть аналогичный “хак” и для Object.keys (хотя он и не совсем корректен):


// global.d.ts

interface ObjectConstructor {
keys<T extends {}>(obj: T): ReadonlyArray<keyof T>;
}


#development #frontend #typescript
👍2🔥2
Бизнес логика vs функциональное мышление

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

Напомню базовое определение:

> Бизнес логика — совокупность правил, принципов, зависимостей поведения объектов предметной области.

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

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

К тому же, есть отличная альтернатива — функциональное мышление (проектирование), о котором я писал (https://t.me/around_dev/48) не так давно. Этот подход подразумевает разделение всего кода на три составляющие: данные, вычисления и действия. При этом, деление на “умные” и “глупые” компоненты прекрасно ложится на этот подход, где глупые компоненты это вычисления, а умные — действия.

#development
🔥1
Vite vs Webpack: PURE комментарии

В продолжении поста “нюанс с React.memo” и перед темой про sideEffects хочется ещё немного накинуть про разницу в сборке проекта с помощью Vite и Webpack.

Расширим прошлый пример до:


// Простой компонент
export { Test } from './test';

// Ещё один простой компонент
export { TestRaw } from './test-raw';

// Компонент обёрнутый в React.memo
export { TestMemo } from './test-memo';

// Компонент обёрнутый в React.forwardRef
export { TestRef } from './test-ref';

// Ленивый компонент
// через функцию поверх React.lazy
export { LazyTest } from './lazy';


Примечание: Из всего этого, в коде используется только компонент Test.

Собрав проект мы получаем следующие результаты:

Webpack оставил в бандле только Test и сайд эффекты, а все остальные компоненты и вспомогательные функции он удалил.

Vite же оставил всё кроме компонента TestRaw…

Ну что ж, если вы читали прошлый пост, то результат для вас ожидаем. Но возможно вас удивит тот момент, что в обоих случаях файл lazy.js (в котором лежит тот самый ленивый компонент LazyTest) присутствует в сборке. С Vite всё понятно, но почему Webpack оставил этот файл хотя компонент не используется? Давайте разбираться.

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


// Фрагмент из сборки от Webpack:
const TestMemo = /*#__PURE__*/(null && (memo(() => {
return /*#__PURE__*/_jsx("p", {
children: "MEMO"
});
})));

// Для сравнения, тот же фрагмент от Vite:
react.memo(() => {
return /* @__PURE__ */ jsxRuntime.jsx("p", {
children: "MEMO"
});
});


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

Но и тут Vite действует иначе… удаление не используемого кода происходит до этапа минификации (обратите внимание, он удалил константу TestMemo).

Что касается вопроса про lazy.js, то сам файл создаётся в момент сборки, а минификатор работает лишь с содержимым уже созданных файлов, поэтому файл и остаётся в бандле, хотя и не используется.

П.С. Кстати, именно поэтому анализаторы бандла могут показывать то, чего в бандле на самом деле нет.

#development #frontend #webpack #vite
🔥1
Комментарии в коде — “хорошо” или “плохо”?

Наткнувшись, в очередной раз, на споры о комментариях в коде, решил “зафиксировать” свои мысли на этот счёт.

Вопреки популярному мнению, заметное количество комментариев в коде — это не признак хорошего кода, а намёк на “код с душком”. Я уж молчу про примеры типа “мне тут лень было разбираться поэтому…”

В идеале, хороший код должен быть самодокументируемым. Понятные названия и типы, небольшие функции, явная структура и т.д. Комментарии действительно нужны лишь в ограниченном количестве случаев. Например, когда вы описываете публичный интерфейс, когда умышленно жертвуете читаемостью в пользу оптимизации и т.п.

В общем, комментарии — не хорошо и не плохо, это инструмент которым нужно уметь пользоваться, а не прикрывать им плохой код.
👍4
Насколько React “жирный”?

Без лишней лирики, сразу к цифрам: пустое приложение на React (для веба) обойдётся вам в 187 kB пожатого js, и это только React, без каких либо дополнительных библиотек. Причём примерно 94% это react-dom-client.

Напомню, что количество js кода, это не только про скорость загрузки, но и про время парсинга, и в целом про то, как быстро будет рисоваться ваш UI.

А теперь сравним с пустым приложением на SolidJS: всего 6 kB!

Во-первых: SolidJS сам по себе компактный.
Во-вторых: он поддерживает tree-shaking (чего не может React).

Или вот реальный проект на SolidJS: 7 зависимостей (роутер и т.п.), 16 страниц, куча UI компонентов, таблицы, формы, списки и т.д., всего 702 модуля. И вот это всё весит 270 kB (233 kB без keycloak).

П.С. Пока писал, вспомнил что уже в 2015 г. были легковесные варианты React. Именно тогда и появился Preact.
🔥2
audio/ogg; codecs=opus

Если вам понадобится реализовать запись и/или воспроизведение аудио файлов audio/ogg; codecs=opus, вы можете обнаружить что поддержка этой пары (контейнер + кодек) довольно скромная. Особенно грустно дела обстоят с записью, и если я правильно помню, на это способен только Firefox.

В качестве решения подойдёт npm пакет opus-recorder
https://github.com/chris-rudmin/opus-recorder

Также стоит обратить внимание на пакет wasm-audio-decoders
https://github.com/eshaz/wasm-audio-decoders
Деградация кодовой базы

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

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

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

А теперь давайте на это всё накинем то, что этим шкафом пользуются несколько человек одновременно. Если, при этом, нет чёткого разделения на зоны ответственности (зоны владения), то смело можно считать что никто ни за что не отвечает. А к чему приводит безответственность, я думаю, объяснять не надо.

Подытожим:

Определите зоны ответственности. В кодовой базе не должно быть “серых зон”.
⁃ Прежде чем добавить свой код, разберитесь как работает текущий. Если вам что-то не понятно, найдите автора или старшего разработчика который подскажет.
Регулярно проводите чистку. Если ваша задача связана с выпиливанием чего-либо, постарайтесь не оставлять “хвостов”.
⁃ Избегайте накопления технического долга, а в идеале следуйте правилу бойскаута: оставь место стоянки чище, чем оно было до тебя.
⁃ Старайтесь писать простой и понятный код, в конце концов 😎

И помните: Чисто не там где убирают, а там где не мусорят.
🔥4💩1
Не очевидный плюс сторонних пакетов, и что насчёт самописных решений

Обычно я, не то чтобы прям против, но точно не выступаю за использование таких библиотек как Axios. Есть Fetch API, и написать базовую обвязку для него, на уровне проекта, ничего не стоит (правда делать это лучше на старте проекта).

Но сегодня не об этом, а о том что у сторонних решений, помимо очевидных плюсов, есть ещё как минимум один не очевидный — «закрытость» от шаловливых ручек 🙌. К сожалению, любой код лежащий в репозитории проекта практически ни как не застрахован от изменений, в том числе негативных. Чем больше у вас проект и чем больше разработчиков над ним работает, тем выше шанс что кто-то что-то сломает, а архитектор (ну или кто там у вас ответственный, он же у вас есть, да?) даже не увидит этих изменений. И да, не все поломки очевидны и сразу исправляются.

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

Если вы знаете другие варианты защиты от тихих «не санкционированных» изменений, пишите в комментариях.

П.С. Если что, вариант «просто договориться» не работает. 😅
11👎1
CSS свойство, которое работает только в Safari

Оказывается, и такое бывает. 😅 Safari — единственный (на момент написания этого поста) браузер, который поддерживает hanging-punctuation, аж с 2016 года.
hanging-punctuation: first allow-end last;

Это свойство отвечает за “висячую пунктуацию”, которая широко известна в кругах профессиональных верстальщиков (тех, что верстают печатную продукцию типа журналов и т.п., а не эти ваши css + html 😎).

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

В общем, если типографика для вас не пустой звук, не забывайте что есть такое свойство.
🔥2
Приколы setState в React, и как оно в SolidJS

Сравним один и тот же код изменения состояния в этих двух, похожих, но совершенно разных, библиотеках.

React:


const [state, setState] = useState(() => Date.now());

const onClick = () => {
console.log(1);
setState(() => {
console.log(2);
return Date.now();
});
console.log(3);
};

const computed = useMemo(() => {
console.log(4);
return (new Date(state)).toDateString();
}, [state]);


Результат при первом выполнении: 1 2 3 4
При последующих: 1 3 2 4

SolidJS:


const [state, setState] = createSignal(Date.now());

const onClick = () => {
console.log(1);
setState(() => {
console.log(2);
return Date.now();
});
console.log(3);
};

const computed = createMemo(() => {
console.log(4);
return (new Date(state())).toDateString();
});


Результат всегда одинаковый: 1 2 4 3

А теперь, предлагаю подумать над следующими вопросами, а может и обсудить в комментариях:

- Это ожидаемый результат для вас?
- Почему такая разница в React, между первым и последующими выполнениями?
- Как добиться стабильного результата в React?
- Как добиться результата 1 2 3 4 в обоих случаях?
👍3
Линтеры и форматтеры вредят разработке?

Прочитал тут пару “набросов” от коллег по цеху:

Как Линтеры и Форматтеры вредят разработке
Чем проще - тем хуже

Несколько цитат, для тех кому лень переходить по ссылкам:

— «Чем больше автоматизации в написании кода, тем ниже его осознанность»
— «Отсутствие автоформаттера - это инструмент оценки ответственности и отношения к регламентам»
— «линтеры прямо сейчас медленно убивают ваш проект»
— «Упрощение убивает профессию»

Да, к линтерам и форматтерам у меня есть вопросы, как и к любым другим инструментам. И да, в своих проектах стараюсь их не использовать. Но одно дело когда это твой личный проект (или единственный проект компании, где ты единственный разработчик), и совсем другое когда ты работаешь в большой компании с большим количеством разработчиков разного уровня и опыта.

Одно дело ездить по деревне без прав и знания ПДД, и совсем другое — быть участником дорожного движения многомиллионного города. Вы же в курсе, что ППД, буквально, написаны кровью? Надеюсь аналогия ясна.

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

П.С. Ну и да, хуки появились не потому что было сложно с классами.
👍2
Как проверить формат файла и почему фильтра по accept и type не достаточно

Начнём с самого простого варианта:


<input type="file" accept="image/*" />


Несмотря на вполне конкретные указания, что input может принимать только изображения, операционные системы предоставляют возможность обойти это и выбрать любой файл. Так что не стоит воспринимать параметр accept как полноценный фильтр, по факту это всего лишь подсказка.

Вариант «побогаче» — drag & drop. Тут пользователь вообще ни как не ограничен и нужно программно фильтровать список dataTransfer.items, во-первых, по kind а во-вторых по type (он же MIME тип).

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

Самое простое, что можно сделать, это проверить сигнатуру файла:


function getSignature(buffer: ArrayBuffer, length = 4): string {
return Array.from(new Uint8Array(buffer.slice(0, length)))
.map(num => num.toString(16))
.join("");
}

file.arrayBuffer()
.then(getSignature)
.then(checkSignature);


Данные необходимых вам сигнатур можно найти в интернетах, например тут. Остальное — дело техники.

П.С. Где делать подобные проверки, на клиенте или на сервере (а возможно и там и там), зависит от задачи.
🔥71