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

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

А что насчёт раскладывания кода по папкам? Да, это тоже часть архитектуры. В FSD (Feature-Sliced Design), например, есть чёткие правила, как разделять код на слои, срезы и сегменты, и как эти части взаимодействуют. В моей версии feature-based архитектуры тоже есть чёткое разделение, но в отличие от FSD, в центре стоит предметная область и функциональное мышление.

Главное — понимать, что архитектура — это очень широкое и абстрактное понятие, и к программированию оно относится не только через код, но и через процессы, структуру команд и даже инфраструктуру. Поэтому раскладывать код по папкам — это один из важных, но не единственных, аспектов архитектуры.
👍41
Малоизвестные возможности Intl

Intl часто ассоциируется с простым форматированием чисел, дат и валют. Однако это API содержит и другие полезные функции:

Intl.ListFormat — форматирует перечисления, корректно вставляя запятые и союзы:


// "яблоки, груши и бананы"
(new Intl.ListFormat("RU")).format(
["яблоки", "груши", "бананы"]
);


Intl.RelativeTimeFormat — форматирует относительное время:


const rtf = new Intl.RelativeTimeFormat("RU");
rtf.format(3, "days"); // "через 3 дня"
rtf.format(-2, "hours"); // "2 часа назад"


Intl.DurationFormat — форматирует интервалы времени:


const time = { hours: 2, minutes: 35 };

// 2 часа 35 минут
(new Intl.DurationFormat("RU", {
style: "long",
})).format(time);

// 02:35:00
(new Intl.DurationFormat("RU", {
hours: "2-digit",
})).format(time);


Intl.Segmenter — позволяет разбивать строки на сегменты с учётом особенностей локали и правил Юникод, что обеспечивает корректную работу даже для сложных языков и символов.


// ["Несколько", " ", "слов", " ", "в", " ", "строке", " ", "😎", "."]
Array.from(
(new Intl.Segmenter("RU", { granularity: "word" }))
.segment("Несколько слов в строке 😎.")
)
.map((item) => `"${item.segment}"`);


Например, в отличие от обычного .split(""), Segmenter не разбивает эмодзи:


// ['\uD83D', '\uDE0E']
"😎".split("");

// => [{segment: "😎"}]
[...(new Intl.Segmenter("RU")).segment("😎")];
🔥8
Несколько слов о TanStack Query (React Query)

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

Рассмотрим базовый пример:


import { useQuery } from "@tanstack/react-query";

export function Example() {
const query = useQuery({
queryKey: ['example', 'data'],
queryFn: () => {
return fetch('/api/example/data')
.then((res) => res.json());
},
});

if (query.isLoading) return 'Loading...';
if (query.error) return 'An error has occurred';

return <div>{query.data}</div>;
}


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

Как вы думаете, столько параметров состояния возвращает useQuery? Вот неполный список: status, isPending, isSuccess, isError, isLoadingError, isRefetchError, isFetched, isFetchedAfterMount, fetchStatus, isFetching, isRefetching, isLoading, isInitialLoading… Сможете без документации разобраться, чем отличаются, например, isPending, isLoading и isFetching?

А сколько методов возвращает useQuery? Всего один — refetch, хотя библиотека предоставляет и другие необходимые функции, например setQueryData (для обновления данных) и invalidateQueries (для инвалидации).

Но чтобы ими воспользоваться, нужно знать queryKey запроса, а также получить инстанс queryClient через хук useQueryClient (или прямой импорт):


import { useQuery, useQueryClient } from "@tanstack/react-query";

function getExampleApiKey() {
return ['example', 'data'];
}

export function Example() {
const queryClient = useQueryClient();

const query = useQuery({
queryKey: getExampleApiKey(),
queryFn: () => {
return fetch('/api/example/data')
.then((res) => res.json());
},
});

function handleInvalidate() {
void queryClient.invalidateQueries({
queryKey: getExampleApiKey(),
});
}

if (query.isLoading) return 'Loading...';
if (query.error) return 'An error has occurred';

return (
<div>
<div>{query.data}</div>
<button onClick={handleInvalidate}>
Update
</button>
</div>
);
}


Как-то многовато кода добавилось из-за одной простой операции…

TanStack Query действительно упрощает определённые операции, но у всего есть своя цена, и понимать её, как обычно, начинаешь только после длительного использования. Большое количество возможностей в одном инструменте — это как плюс, так и минус (особенно для больших команд). К тому же, «из коробки» вам придётся писать довольно много шаблонного кода, а в последствии думать как его сокращать.
🌚3💯3💩1🤣1🗿1
Фиксированное соотношение сторон в CSS

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

Старое решение:

Требует дополнительного элемента в разметке.


<div class="grid">
<div class="card">
<div class="content">
...
</div>
</div>
...
</div>



.grid {
display: flex;
flex-wrap: wrap;
}

.card {
position: relative;
padding-top: 33.33%; /* !!! */
width: 33.33%;
}

.content {
position: absolute;
top: 0;
width: 100%;
height: 100%;
}


Новое решение (Chrome 105+, Safari 16+):


<div class="grid">
<div class="card">
...
</div>
...
</div>



.grid {
container-type: size; /* !!! */
display: flex;
flex-wrap: wrap;
}

.card {
width: 33.33%;
height: 33.33cqw; /* !!! */
}


Лучшее решение (Chrome 88+, Safari 15+):


.grid {
display: flex;
flex-wrap: wrap;
}

.card {
width: 33.33%;
aspect-ratio: 1 / 1; /* !!! */
}


П.С. В качестве контейнера можно использовать как flex так и grid, однако стили блоков будут слегка отличаться.
🔥7
Проверяем импорты через ESLint

В процессе улучшения структуры проекта, которую я описывал здесь, задумался над базовыми проверками корректности импортов. Эта структура строится на «функциональном мышлении» (далее ФМ), в котором код должен быть разделён на данные, вычисления и действия, поэтому необходимо проверять корректность импортов между определёнными сегментами. Например, чтобы сегмент ui (в контексте ФМ это вычисления) не мог импортировать сегмент $api (в контексте ФМ это действия). На самом деле причин и проверок больше, но не суть.

Для эксперимента был выбран eslint-plugin-boundaries. В целом неплохой инструмент, хотя сильно не хватает разделения контекста, но разработка в этом направлении вроде как ведётся. В моём случае, например, необходимы разные правила для проверки верхнего уровня папок и уровня сегментов.

eslint.config.js


import { defineConfig } from 'eslint/config';
import parser from '@typescript-eslint/parser';
import boundaries from './eslint-boundaries.js';

export default defineConfig([
{
extends: [boundaries],
files: ['src/**/*.{ts,tsx}'],
languageOptions: { parser },
settings: { 'import/resolver': { typescript: true } },
},
]);


eslint-boundaries.js


import boundariesPlugin from 'eslint-plugin-boundaries';

export default {
plugins: {
boundaries: boundariesPlugin,
},
settings: {
'boundaries/elements': [
{ type: '$control', pattern: 'src/*/$control/**' },
{ type: '_core', pattern: 'src/_core/*', mode: 'full' },
// ...
],
},
rules: {
'boundaries/element-types': [
'warn', {
default: 'allow',
message: '[${file.type}] is not allowed to import [${dependency.type}]',
rules: [
{ from: '$control', disallow: ['$page'] },
{ from: '_core', disallow: ['_ui-kit', 'admin', 'app'] },
// ...
],
},
],
},
};


Полная версия файла тут.

Да, все возможные варианты этот код не покрывает, но для начала этих проверок вполне достаточно.
🔥2
Что за звери такие cohesion и coupling

Возможно, вы слышали, что при выборе архитектуры (или её проектировании) нужно стремиться к high cohesion (высокому сцеплению) и low coupling (низкой связанности). Так вот, cohesion и coupling — два базовых критерия оценки качества архитектуры.

Cohesion — это то, насколько части одного модуля логически относятся к одной задаче. Чем выше cohesion, тем больше «про одно» этот модуль, тем проще его понять и менять. Можно даже сказать, что high cohesion это принцип единой ответственности из SOLID.

Coupling — это то, насколько один модуль зависит от других. Чем ниже coupling, тем меньше модули знают друг о друге и тем легче менять их независимо.

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

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

П.С. Дедовский лайфхак, как проверить, что у вас хорошая (или не очень) архитектура: попробуйте поудалять разные (конечные) модули вашей системы. Чем меньше изменений придётся вносить в оставшуюся часть системы, тем лучше архитектура.
🔥3🤔1
Channel name was changed to «Философия программирования»
Знакомая всем вам двоичная система счисления, которая используется практически во всех современных компьютерах, была описана Г. В. Лейбницем в 17 веке. Важный момент тут в том, что она была именно описана, а не изобретена, ведь как известно, всё уже изобретено до нас. Идеи двоичной записи появились за долго до Лейбница, например в древнекитайской «Книге Перемен» о которой он точно знал, как человек увлекающийся китайской культурой. На минутку, возраст этой книги около 3000 лет! Книга Перемен — философский текст, включающий 64 символа, каждый из которых состоит из 6 черт (сплошных или прерывистых, что соответствует 1 и 0).

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

Расширяйте свои горизонты!
С Новым годом!
🎄4🔥1
Самые изменяемые файлы проекта, о чём говорят и как посмотреть

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

Собрать список самых изменяемых файлов, можно, например, с помощью git log.

Важная ремарка: не всегда полученный список будет отражать что-то полезное. Вполне может быть так, что в нём будут одни index, config, types и тому подобные файлы. Хотя, слишком частое изменение одного и того же index файла — тоже, своего рода, знак.


git log master \
--name-only \
--pretty=format: \
--since="6.month" \
-- apps/***/src/ \
| sort | uniq -c | sort -nr | head -5



1564
53 apps/***/use-some-dialog.ts
42 apps/***/submit.tsx
31 apps/***/main.tsx
26 apps/***/some-content.tsx


В моём случае, после беглого изучения файлов из полученного списка, первое что бросилось в глаза — размер: 670, 450, 490 и 390 строк кода. Многовато, особенно в первом из них. На нём и заострим своё внимание.


git log master \
--no-merges \
--since="6.month" \
--pretty=format:"%ae %ad %s" --date=short \
apps/***/use-some-dialog.ts \
| head -100


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

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

Всё это: частота изменения файла, количество строк кода, количество и (не) связанность задач, в данном случае, явно сообщает нам о том, что это типичный «god-компонент» (в нашем случае хук) и первый кандидат на ближайший рефакторинг, а также требует особого внимание на ревью и даже в тестировании.
7
Императив vs Декларатив. Точно понимаете разницу?

Императивный стиль описывает КАК достичь результата (пошаговые инструкции), а декларативный — ЧТО нужно получить (описание желаемого исхода).

Скорее всего, именно так вы ответите на вопрос, чем отличаются эти стили программирования, и по сути вы будете правы. Но давайте рассмотрим простой пример:


const isEven = count % 2 === 0;


Сможете чётко ответить, какой именно стиль программирования тут используется, ЧТО или КАК? Если вы засомневались или подумали что это императивный стиль, то добро пожаловать в клуб, т.к. это декларативный стиль.

Вместо использования КАК и ЧТО, предлагаю следующее определение:

Императивный стиль характеризуется явным выполнением операций, которые что‑то «делают» во «внешнем мире» (мутируют данные, вызывают системные API и т.п.), тогда как декларативный стиль сводится к написанию чистых выражений и функций, которые лишь вычисляют значения.

Кстати, если вы помните (и понимаете) концепцию функционального мышления, то в целом можно сказать что императивный стиль — «действия», а декларативный — «вычисления».
👍1👎1🔥1
Простой пример сокрытия императивного кода за декларативным интерфейсом

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

Простой пример, где isLoading отвечает за блокировку кнопки Submit:


function CreateOrder(props) {
const [isLoading, setLoading] = createSignal(false);

function onSubmit() {
setLoading(true);
props.onSubmit()
.then(() => {/* ... */})
.catch(() => {/* ... */})
.finally(() => setLoading(false));
}

return <Form onSubmit={onSubmit}>...</Form>;
}


Императивным тут является только onSubmit, который, помимо основной задачи, контролирует состояние формы через setLoading.

Разделяем ответственность и скрываем часть императивного кода внутри useLoading:


function CreateOrder(props) {
const [isLoading, submit] = useLoading(props.onSubmit);

function onSubmit() {
submit()
.then(() => {/* ... */})
.catch(() => {/* ... */});
}

return <Form onSubmit={onSubmit}>...</Form>;
}


Теперь onSubmit выглядит чище и отвечает только за то, что и должен.

П.С. Для SolidJS useLoading может выглядеть так:


import { type Accessor, createSignal } from 'solid-js';

export function useLoading<
T,
P extends any[],
A extends ((...args: P) => Promise<T>) | undefined
>(action: A): [Accessor<boolean>, A] {
const [loading, setLoading] = createSignal(false);

const handler = action && (((...args: P): Promise<T> => {
setLoading(true);
return action(...args).finally(() => setLoading(false));
}) as A);

return [loading, handler];
}
Vite vs Webpack: нюансы сборки

В проекте на Vite обнаружил неожиданное (для меня) поведение: при загрузке чанка страницы, браузер также загружает чанки двух других, не связанных с ней, страниц:


./page-a.js
./page-b.js
./page-c.js


Почему так? Vite (rollup) помещает общий компонент, который используется в нескольких асинхронных чанках, в один из них. Другими словами, при загрузке любого из этих чанков, браузер вынужден загрузить и тот чанк, в котором находятся общий компонент.

Однако, это не является поведением по умолчанию. Всему виной параметр experimentalMinChunkSize. Без него, Vite выносит все общие компоненты в отдельные чанки. Даже если компонент весит всего 106 байт, как этот:


import {c as o} from "./createIcon.js";
const c = o("24", "Stroked", "Cancel");
export {c as C};


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

На заметку: experimentalMinChunkSize работает не всегда и в проекте всё равно остаются микро-чанки.

Webpack работает иначе: помещает копию компонента в каждый асинхронный чанк, где этот компонент необходим, если его размер не превышает значения указанного в splitChunks.minSize (по умолчанию 20000 байт). В противном случае, компонент выносится в отдельный чанк.
👍8🤔1
Nullish coalescing assignment (??=)

Интересно, что в отличии от nullish coalescing operator (??), assignment (??=) как будто ни кто не использует.


// Вместо:
if (!date) {
date = Date.now();
}

// Пишем:
date ??= Date.now();
👍7
Vite меняет хеши всех js чанков при любом изменении

Реальный проект на React, собирается Vite, на выходе получается примерно 60 js файлов:


index-DGZT2d08.js
page-DMUtS7f0.js
page-CIOhQsD0.js
и т.д.


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

Vite использует es6 modules:


// index-DGZT2d08.js
const Page = lazy(() => (
__vitePreload(() => (
import("./page-DMUtS7f0.js")
))
));


Заглядываем в любой другой модуль, и видим следующее:


// page-DMUtS7f0.js
import { jsx } from "./index-DGZT2d08.js";


Все синхронные зависимости, в том числе node_modules, Vite складывает во «входную точку» — index-DGZT2d08.js. В результате получаем следующее поведение:

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

Это плохо, т.к. вынуждает браузер пользователя скачивать все необходимые файлы заново, даже если в них, кроме путей импортов и экспортов, ни чего не поменялось.
👍3🤔3
Why did this render? Props changed!

Это не совсем так! Вот список того, что на самом деле вызывает повторные рендеры в React:

• Изменение локального состояния компонента
• Повторный рендер родительского компонента
• Изменения значения контекста, на который подписан компонент

Это важно понимать: при повторном рендере компонента, также выполнится рендер и всех его потомков, даже если props у них не менялись!
3
React: memo и useMemo

Это инструменты, решающие конкретные задачи, или головная боль разработчиков?

Прежде чем отвечать на этот вопрос, рассмотрим три примера:

1️⃣ Контекст


function Page() {
const { id } = useParams();
const { data } = useQueryData();
// ...fetching other data

return (
<Context.Provider
value={{ id, value: data.value }}
>
<Content />
</Context.Provider>
);
}


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


180ms <= первый рендер
161ms <= данные пришли
107ms <= данные не меняются
156ms
107ms


Обернём данные для контекста в useMemo:


197ms <= первый рендер
160ms <= данные пришли
10ms <= !!!
18ms
9ms


Экономия ~330 ms.

2️⃣ Список из 100 карточек


function Page() {
const [items, showMore] = useSomeList();

return (
<>
<List>
{items.map((item) => (
<Item {...item} />
))}
</List>
<Button onClick={showMore}>
Ещё
</Button>
</>
);
}


Результаты первого и второго рендера:


51ms <= 50 items
97ms <= 100 items


Обернём Item в React.memo:


58ms <= 50 items
63ms <= 100 items


Во втором рендере получаем экономию ~34 ms.

3️⃣ Поиск по массиву из ~900 объектов


function Modal() {
const search = useSearch();
const users = useUsers();

const list = users
.filter(bySearch(search))
.sort(toSomeOrder)
.slice(0, 20);

return (
<>
<Search {...search} />
<UsersList data={list} />
</>
);
}


Добавим debounce для поиска и useMemo для list. В итоге, среднее значение рендера на каждый введённый, в поисковую строку, символ будет:


В исходном варианте: ~22ms
debounce + useMemo: ~20ms


При этом, с debounce рендеров будет больше, а формирование list в обоих случаях занимает ~2ms.

*️⃣ Выводы

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

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

П.С. Все замеры производились на MacBook Air m1 в Chrome с 4х замедлением CPU и React в dev режиме.
👍1
Нужно ли знать алгоритмы и структуры данных?

Вы когда-нибудь задумывались, почему этот вопрос возникает исключительно в контексте оценки одних разработчиков другими, например, на собеседованиях?

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

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

Не нужно превращать алгоритмы в культ. Хороший разработчик не обязан зубрить LeetCode, чтобы пройти техническое собеседование!
👍7💔1
Подключил React Compiler к реальному проекту, и вот что из этого получилось.

Входные данные:


vite v6.2
react v18.3
babel-plugin-react-compiler v1.0
react-compiler-runtime v1.0


Время сборки выросло в 2 раза:


plugin-react ~21 s
plugin-react + react-compiler ~40 s


Размер чанков (с React компонентами) вырос почти в 1.5 раза:


chunk-a.js 26.6 => 38.2 Kb
chunk-b.js 30.6 => 38.9 Kb


Справедливости ради, в целом проект подрос не так заметно:


6.1 => 6.8 Mb


Время рендера страниц (обычная сборка => compiler):


page-a 18/72/60 => 20/74/30 ms
page-b 26/128 => 37/126 ms


Стоит отметить, что на этих страницах отсутствует какая-либо «ручная» мемоизация.

Выводы каждый сделает сам.

П.С. Замеры производились на MacBook Air m1 + Chrome с 4х замедлением CPU.
Tree Shaking не работает по умолчанию!

Production сборка большинства проектов включает два основных этапа: сборка исходного кода в JavaScript файлы (чанки), и их последующая минификация. При этом, удаление не используемого кода возможно на обоих этапах. На первом за это отвечает Tree Shaking, а на втором — минификатор.

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

С настройками по умолчанию, Tree Shaking в Vite работает лишь частично, а в Webpack вообще не работает. Чтобы его включить, необходимо в package.json добавить:


{ "sideEffects": false }


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

Подробнее можно почитать в документации Webpack.

Для контекста рекомендую ознакомиться со следующими постами:
Vite vs Webpack: нюанс с React.memo
Vite vs Webpack: PURE комментарии
🔥2
Как структурировать код, а не просто раскладывать файлы по папкам

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

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

Итак, код любого приложения можно разделить на три главных компонента:

Данные

В оригинале, это «факты, относящиеся к событиям», которые я дополняю структурами данных и системой типов. По сути, это любые данные, которыми оперирует код, а также типы описывающие их.

Вычисления

Компоненты, «преобразующие ввод в вывод», также называемые чистыми функциями. Сюда же относятся и презентационные (глупые) UI компоненты.

Действия

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

Дополнительно, каждый компонент можно разделить на две категории:

Платформа

Включает код для работы с базовыми типами данных (объекты, массивы и т.д.) и API платформы. Сюда же попадают и npm пакеты не привязанные к конкретной предметной области, например react.

Предметная область

Включает код отвечающий за описание структуры предметной области, и содержащий ту самую «бизнес логику».

Резюме

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

Действия — зона повышенных рисков!

Для снижения рисков и потенциальных проблем, при проектировании и разработке, необходимо:

— Строго разграничивать код, отвечающий за данные, вычисления и действия;

— Снижать количество кода отвечающего за действия, в пользу вычислений;

— Объединять (логически) связанные действия в одном месте (вспоминаем принцип SRP).
👍2
Vite и importmap

Не так давно я писал, что Vite меняет хеши всех JS чанков при любом изменении. В комментариях резонно спросили, а есть ли способ это исправить. Давайте разбираться.

Недавно Vite переехал на rolldown, у которого, в свою очередь, есть экспериментальная опция chunkImportMap. При включении которой, rolldown создаёт файл importmap.json с картой путей до всех JS файлов бандла, где ключи это пути указанные в импортах, а значения — то, как в действительности называются файлы:


{
"imports": {
"/index-BbVZ37fA.js": "/index-CEAfcI3S.js",
"/admin-V_WO83n5.js": "/admin-BACbm3dr.js",
"/user-BR8BcyAZ.js": "/user-CYtl3HzT.js"
}
}


Это именно то, что нужно, т.к. хеши в ключах не меняются. Вследствие чего, исчезает и необходимость менять ссылки из других чанков, и, соответственно, менять их хеши.

Однако, чтобы всё заработало, этого не достаточно.

Начиная с марта 2023 года, всеми основными браузерами поддерживается новый тип тега <script type="importmap">, в который и нужно положить карту из importmap.json. Эта задача легко решается через добавление соответствующей строки в index.html и плагина для Vite:


{
name: "inject-import-map",
transformIndexHtml(html, { bundle }) {
const chunkImportMap = bundle["importmap.json"];
if (chunkImportMap?.type !== "asset") return html;

const updated = html.replace(
/<script\s+type="importmap"[^>]*>[\s\S]*?<\/script>/i,
`<script type="importmap">${chunkImportMap.source}</script>`,
);

delete bundle["importmap.json"];
return updated;
},
},


И вроде бы всё работает, но всплыл неприятный момент — если в вашем бандле есть асинхронные CSS чанки, то они больше никогда не загрузятся.

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

Как итог, единственным реально рабочим вариантом остаётся дождаться появления соответствующей опции в Vite v8.1 (если верить соответствующему PR). Ну или перейти на rsbuild т.к. он быстрее, предлагает больше опций и точнее собирает код.

П.С. Интересно, что два года назад уже был похожий PR, но он до сих пор так и висит открытым.
4👍2