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

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

1. Обработка ошибок загрузки (не существующих) чанков

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


Для JS: error.name = ChunkLoadError
Для CSS: error.code = CSS_CHUNK_LOAD_FAILED


Vite же, для js вернёт обычную ошибку, сообщение в которой может варьироваться в зависимости от браузера, а для css — заготовленную ошибку Unable to preload CSS.

Про обработку таких ошибок расскажу как нибудь в другой раз.

2. Ручное именование чанков

Иногда возникает потребность в более детальной настройке того, что и как попадёт в конкретный чанк. Для этого, в webpack, есть так называемые магические комментарии https://webpack.js.org/api/module-methods/#magic-comments. В данный момент нас интересует webpackChunkName, который позволяет указать название чанка (то, как будет именоваться js файл).

Естественно, подобные комментарии не работают в Vite. Но, мы не первые, кто столкнулся с этим вопросом, поэтому уже есть плагин https://www.npmjs.com/package/vite-plugin-webpackchunkname. Лично пока не пробовал, могут быть нюансы.

#development #frontend #javascript #webpack #vite
Экспериментальная оптимизация Webpack

Как сократить время локального запуска проекта, например, с 16 до 1 секунды? Если вы используете Webpack 5.17+, можно отложить сборку конкретного кода (чанка) до момента пока он не понадобится:

experiments: {
// true должно быть только при локальном запуске
lazyCompilation: true,
},

В итоге, чем лучше код разделён на чанки, тем меньше времени придётся ждать.

#development #frontend #webpack #optimisation #javascript

https://webpack.js.org/configuration/experiments/#experimentslazycompilation
Как в Webstorm найти не используемый код?

Заходим в меню Code > Analyze Code > Run Inspection By Name, в появившемся поле вводим “unused” и из списка подсказок выбираем Unused global symbol. Ну а далее настраиваем параметры и нажимаем ОК.

#development #frontend #javascript #webstorm
Про код-стайл, зачем он нужен и как должен выглядеть

Код-стайл (он же code style, coding standards, coding convention или programming style) — некоторый набор правил и соглашений для написания кода, над которым работает более одного разработчика.

Зачем нужны эти правила?

Первое, и самое важное — убрать разногласия между разработчиками, т.к. каждый привык писать по своему и считает что именно его вариант самый верный.
Второе — предотвратить появление распространённых ошибок в коде. Например then() написали, а catch() забыли и в итоге у нас ошибка на проде.
Третье — упростить чтение кодовой базы.

В каком виде эти правила и соглашения должны быть реализованы?

Самый эффективный способ — настроить автоматические инструменты для максимального количества правил. При этом, что бы все стилистические правила (табы vs пробелы, длинна строки и т.п.) применялись автоматически, например на pre-commit hook! Почему именно так? Да всё просто, всё что делается автоматом, не должно мешать во время написания кода. Важно отметить, что автоматическое применение стилей не должно происходить до момента пока код не дописан, т.к. есть правила которые удаляют не используемый код.

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

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

Самые популярные инструменты, на данный момент:

https://eslint.org
https://prettier.io

#codestyle #eslint #prettier #frontend #development #javascript
Typescript: Union типы на практике

Представим, что мы пишем свою реализацию “хранилища” данных, которое должно возвращать примерно такое состояние:


interface State<T = unknown> {
status: 'loading' | 'error' | 'success',
error: Error | undefined,
data: T | undefined,
}


На первый взгляд может показаться что всё ок, однако это не совсем так. К примеру, проверка status ничего не скажет нам о состоянии других полей:


if (state.status === 'success') {
console.log(state.data.toFixed());
// Получаем ошибку
// 'state.data' is possibly 'undefined'.
}


Решаем проблему через union тип, добавляя три (по количеству состояний) отдельных интерфейса в каждом из которых error и data напрямую зависят от значения status:


interface LoadingState {
status: 'loading';
error: undefined;
data: undefined;
}

interface ErrorState {
status: 'error';
error: Error;
data: undefined;
}

interface SuccessState<T = unknown> {
status: 'success';
error: undefined;
data: T;
}

type State<T = unknown> =
| LoadingState
| ErrorState
| SuccessState<T>;


Проверяем наш код, и видим что всё работает без ошибок 🎉:


if (state.status === 'success') {
console.log(state.data.toFixed());
}

if (state.status === 'error') {
console.log(state.error.message);
}


Кстати, тоже самое можно провернуть и с типизацией props для React компонента, который должен принимать разный набор параметров в зависимости от значения одного общего.


type GridProps = SharedProps & (GridTypeProps | FlexTypeProps);


#typescript #frontend #development #react
👾1
Про чанки

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

Стоит сделать важное замечание: даже если нужно выполнить какую-то небольшую часть кода, загрузить и проанализировать браузер его должен полностью.

Предположим у нас есть веб-приложение с 10 страницами, из которых пользователи используют обычно 2-3, а к остальным обращаются редко, а то и вообще никогда. Будет странно загружать весь код (для всех страниц) одним куском (файлом).

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

Разделение кода — не (совсем) автоматический процесс и требует осознанного подхода. Обычно, в отдельный чанк выносится та часть кода (со всеми её зависимостями), которая была использована через динамический импорт:


import('./something')


Но то, как именно код будет разделён, в конечном счёте зависит от настроек инструментов сборки, которые вы используете.

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

Кстати, если вы пишите приложение с использованием React, то встроенная функция lazy и компонент Suspense должны быть вам хорошо знакомы.

#frontend #development #codesplitting #javascript #react
Архитектурная ошибка модальных окон в React

Один из примеров того, как не нужно делать в React.

Есть некий компонент в котором через меню открывается одно из модальных окон:


function Comp() {
return (
<>
<Menu /* выбор какое окно открыть */ />
<ModalA isOpen={modal === 'a'} />
<ModalB isOpen={modal === 'b'} />
<ModalC isOpen={modal === 'c'} />
</>
);
}


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

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

Решается это через условный render, ну или использованием функции по типу:


ui.openModal(<ModalA />)


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

#react #frontend #development #architecture
Паттерн switch(true)

Интересный паттерн который однако встречается достаточно редко. Настолько, что его полноценная поддержка в typescript появилась только в версии 5.3 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#switch-true-narrowing

Если в коде видим большое количество else if условий, например:


if (/* condition #1 */) {
// code #1
} else if (/* condition #2 */) {
// code #2
} else if (/* condition #3 */) {
// code #3
} else {
// code #4
}


Возможно, это то самое место, где можно и нужно применить switch(true):


switch(true) {
case /* condition #1 */:
// code #1
break;
case /* condition #2 */:
// code #2
break;
case /* condition #3 */:
// code #3
break;
default:
// code #4
}


Конечно это субъективно, но на мой взгляд, так выглядит понятнее и самое главное логичнее.

Кстати, в SolidJS есть компоненты которые используют этот паттерн:


<Switch fallback={<div>Not Found</div>}>
<Match when={state.route === "home"}>
<Home />
</Match>
<Match when={state.route === "settings"}>
<Settings />
</Match>
</Switch>


#javascript #typescript #frontend #development #patterns #react #solidjs
Если вы вдруг хотели посмотреть исходный код реального коммерческого проекта на SolidJS, то вот вам ссылка: https://github.com/spsaucier/clearspend-ui

#solidjs #frontend #development #sourcecode
👾1
Типичная ошибка использования .sort()

Даже опытные разработчики могут не увидеть ошибку в следующем коде:


function sortItems<T>(items: T[]): T[] {
return items.sort(/* some sort function */);
}


А она заключается в том, что метод sort не создаёт нового массива, а изменяет текущий. И это не очевидное поведение может привезти к появлению ошибок. Решается это довольно просто:


function sortItems<T>(items: T[]): T[] {
return [...items].sort(/* some sort function */);
}


Видимо это всех настолько “утомило”, что в спецификацию добавили метод toSorted.

Ещё одним “решением” для предотвращения появления этой ошибки в typescript является использование ReadonlyArray. Но, к сожалению, про него практически ни кто не знает.

#frontend #development #javascript #typescript
https://transform.tools — большой набор всевозможных конвертеров для разработчиков.
Например SVG > JSX или JSON > TypeScript.

#frontend #development #devtools
Пример улучшения функции сортировки

Сортировка — довольно распространённый вид операции с данными в JavaScript / TypeScript. Например, в одном из рабочих проектов .sort встречается 97 раз в 65 файлах. При этом важно, чтобы этот код был максимально читаемым и компактным.

Обратите внимание, что некоторые примеры, для упрощения восприятия, будут без типизации. В конце будет ссылка на рабочий TypeScript код целиком.

Рассмотрим следующий пример:


const MAP_TYPE_TO_ORDER: Record<FeatureType, number> = {
[FeatureType.Default]: 0,
[FeatureType.Local]: 1,
[FeatureType.Unknown]: 2,
};

function sortFeaturesByTypeAndTitle(features: Feature[]): Feature[] {
return [...features].sort((a, b) => {
const numA = MAP_TYPE_TO_ORDER[a.type];
const numB = MAP_TYPE_TO_ORDER[b.type];
if (numA !== numB) {
return numA - numB;
}
return a.title.localeCompare(b.title);
});
}


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

Для сортировки по алфавиту:


function inAbcOrderBy(prop) {
return (a, b) => a[prop].localeCompare(b[prop]);
}


Для сортировки по полю с перечислением (в нашем примере это type) в заданном порядке в виде массива (почему именно массив, объясню в конце):


function inCustomOrderBy(prop, order) {
return (a, b) => order.indexOf(a[prop]) - order.indexOf(b[prop]);
}


Для объединения нескольких функций сортировки в один метод .sort() напишем ещё одну вспомогательную функцию:


function inPriority(...sorters) {
return (a, b) => {
for (let i = 0; i < sorters.length; i++) {
const result = sorters[i](a, b);
if (result !== 0) return result;
}
return 0;
};
}


Осталось переписать наш пример используя эти функции:


const TYPE_ORDER = [
FeatureType.Default,
FeatureType.Local,
FeatureType.Unknown,
];

function sortFeaturesByTypeAndTitle(features: Feature[]): Feature[] {
return [...features].sort(
inPriority(
inCustomOrderBy('type', TYPE_ORDER),
inAbcOrderBy('title')
)
);
}


В итоге мы получили:

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

Более удобную, с точки зрения поддержки, настройку сортировки по типу, т.к. в случае изменения списка FeatureType не нужно будет обновлять индексы.

Код полностью: https://gist.github.com/SanichKotikov/c8c3dac1f0de4b722b13c4af49b29c61

#frontend #development #javascript #typescript #sort
Разделение кода (на чанки) в Webpack

Не так давно я писал зачем нужно разделять код на чанки, а теперь давайте рассмотрим некоторые нюансы настройки Webpack, связанные с этим вопросом.

Для начала, создадим папку src и добавим туда три файла, index.js и два других (предположим foo.js и baz.js), динамически импортируемых в первый. Затем добавим минимальный файл конфигурации:


// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
// про contenthash как нибудь в другой раз
filename: '[name].[contenthash].js',
},
};


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

Запускаем сборку командой webpack --mode=production и видим папку dist в которой появилось три файла: main.js, foo.js и baz.js. Из чего понимаем, что некоторая настройка разделения кода в Webpack по умолчанию уже есть (на самом деле их там много).

Также видим что файлы минифицированны, так как мы указали --mode=production. Давайте, на время изучения, выключим эту оптимизацию что-бы лучше понять во что Webpack собирает наш код.


module.exports = {
/* ... */
optimization: {
minimize: false,
},
};


Собираем проект ещё раз (теперь без минификации) и видим что помимо нашего кода в файлах (в папке `dist`) содержится дополнительный сервисный код. В динамически загружаемых файлах он выглядит примерно так:


(self["..."] = self["..."] || [])
.push([[/* chunk id */], {
[/* module id */]: (() => {
/* код модуля */
})
}]);


А вот в main.js его побольше, но оно и понятно, всё же это основной файл. Нас же интересует карта, в которой описано какой чанк (по chunk id) в каком файле лежит. Выглядит это примерно вот так:


return "" +
{ "386": "baz", "957": "foo" }[chunkId] + "." +
{ "386": "22...33", "957": "cf...d0" }[chunkId] +
".js";


Из всего этого понимаем, что каждому файлу присваивает некоторый chunk id. И вот тут давайте остановимся поподробнее. Если почитать документацию, то окажется что можно настраивать то, как именно это id будет формироваться. Но почему это важно? Дело в том, что код проекта может доставляться на сервер по несколько раз в сутки, но при этом меняться должно только то что мы действительно поменяли, что бы браузер пользователя не скачивал тот же самый код заново. Резонный вопрос — а разве может поменяться то, что мы не меняли? Да, может.

Если у вас всё ещё Webpack 4, то по умолчанию эти id — обычные порядковые номера. В итоге, может получиться так что вы поменяли одну строку исходного кода, а по факту изменилось половина файлов (в папке `dist`), потому что изменились id. Однако, начиная с 5 версии это поведение изменили (https://webpack.js.org/blog/2020-10-10-webpack-5-release/#deterministic-chunk-module-ids-and-export-names), и теперь эти id не меняются между сборками.

Есть ещё один момент, почему файлы могут и
змениться.

Давайте вернёмся к нашему примеру и изменим исходный код в файле foo.js. Собираем проект, и видим что поменялся не только foo.js, но и main.js. Всё дело в той самой карте, т.к. обновился foo.js, соответственно обновился и его contenthash, а он записывается в main.js. И этот момент тоже можно настроить:


optimization: {
/* ... */
runtimeChunk: true,
},


Благодаря чему весь сервисный код, в том числе и карта, будут собираться в отдельный небольшой файл (чанк). Соответственно, теперь при изменении какого-то файла в исходном коде, в сборке поменяется только файл, где этот код лежит и runtime чанк.

#devtools #webpack #frontend #development #optimization
Разделение кода (на чанки) в Webpack (Выводы):

⁃ Включите runtimeChunk.
⁃ Если вы поменяли значение optimization.chunkIds или moduleIds — проверьте чтобы значение соответствовало deterministic.
⁃ Если вы всё ещё собираете проект с помощью Webpack 4, то пора переходить на 5.
⁃ Если перейти на 5 ну ни как не получается — добавьте HashedModuleIdsPlugin (https://v4.webpack.js.org/plugins/hashed-module-ids-plugin/).
⁃ Если у вас Webpack 5, проверьте что вы не используете HashedModuleIdsPlugin (если конечно не было веской причины его добавить).

#devtools #webpack #frontend #development #optimization
В продолжение темы "разделение кода в Webpack", сравним с Vite

Реальный пример production кода (после сборки):

app.js — основной код приложения;
client.js — страница работы с клиентом;
subscription.js — всплывающее окно подписки клиента (открывается со страницы клиента).

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

Соберём исходный код проекта с помощью Webpack и Vite, с настройками по умолчанию, и сравним результаты сборки после внесения изменений в код модального окна (subscription.js).

Webpack:

Изменения будут только в двух файлах, subscription.js (очевидно) и app.js, потому что там находится карта всех чанков. Примечание: если у вас включен runtime chunk, то вместо app.js изменённая карта будет в нём. Подробнее про карту и runtime я писал ранее.

Vite:

Изменения будут во всей цепочке: в subscription.js (очевидно), в client.js потому что именно из него открывается модальное окно, и в app.js потому что из него открывается страница. Так происходит, потому что Vite использует стандартный JavaScript modules (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) и прописывает путь и название файла непосредственно в то место где оно используется.

#devtools #webpack #frontend #development
Автоматическая генерация TypeScript типов API, помогает или нет?

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

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

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

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

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

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

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