Недавно мы провели митап с экспертами Студии, мобильными разработчиками на проектах Сбера, на тему многопоточности. Ребята поделились лучшими практиками работы с потоками при создании сложных приложений.
Для тех, кто пропустил эфир, выделили несколько важных тезисов о многопоточной мобильной разработке на iOS.
→ Читать тезисы
→ Смотреть митап
Делитесь в комментариях вашим опытом многопоточной разработки и задавайте вопросы нашим спикерам.
В следующий раз опишем главные принципы многопоточности для Android. Не пропустите!
Для тех, кто пропустил эфир, выделили несколько важных тезисов о многопоточной мобильной разработке на iOS.
→ Читать тезисы
→ Смотреть митап
Делитесь в комментариях вашим опытом многопоточной разработки и задавайте вопросы нашим спикерам.
В следующий раз опишем главные принципы многопоточности для Android. Не пропустите!
🔥31👍2🤩2❤1
Всем привет!
Сегодня давайте вместе раскроем тайну одного из самых противоречивых аспектов JavaScript: почему typeof null возвращает 'object'. Это путешествие во времена зарождения JavaScript обещает быть увлекательным.
🕵️♂️ Разгадываем головоломку. Начало 90-х. JavaScript еще молод и полон потенциала, но в его коде уже заложена эта загадка. Противоречивый вывод typeof null как 'object', хотя, на самом деле, null — это примитив. Звучит как ошибка? Давайте разберемся.
🏛 Код из древности. В самой первой версии JavaScript все данные были упакованы в 32-битные блоки. Эти блоки разделялись на две части: небольшую, где хранился «тег» (маркер), который сообщал о типе данных (1-3 бита). И другую, где хранились сами данные.
Представьте это как ящик, на котором есть этикетка (тег), говорящая, что внутри. А затем само содержимое (данные).
Было пять этикеток:
1. 000: объект. Внутри — ссылка на объект.
2. 1: целое число. Внутри — целое число с 31-битной знаковостью.
3. 010: число с плавающей запятой (double). Внутри — ссылка на такое число.
4. 100: строка. Внутри — ссылка на строку.
5. 110: логическое значение (true или false). Внутри — само логическое значение.
Но два значения были особенными:
— undefined имело числовое значение 230, это число находится вне диапазона обычных целых чисел;
— null было представлено как указатель NULL в машинном коде. Это значит, что у него была метка «объект» и ссылка, равная нулю.
Таким образом, когда мы использовали typeof с null, он видел его как «объект» из-за его метки.
🎭 Время проб и ошибок. Разработчики знали об этой странности, но исправление могло «сломать» старый код, который уже работал в интернете. Итак, они решили оставить все как есть, чтобы не вызвать проблемы.
✨ Совет для начинающих. Чтобы не запутаться при работе с null, всегда используйте «строгое сравнение» (===) с null:
value === null; // возвращает true или false в зависимости от значения.
Теперь вы знаете одну из многих тайн JavaScript и как с ней работать. Оставайтесь на связи для новых интересных историй!
Сегодня давайте вместе раскроем тайну одного из самых противоречивых аспектов JavaScript: почему typeof null возвращает 'object'. Это путешествие во времена зарождения JavaScript обещает быть увлекательным.
🕵️♂️ Разгадываем головоломку. Начало 90-х. JavaScript еще молод и полон потенциала, но в его коде уже заложена эта загадка. Противоречивый вывод typeof null как 'object', хотя, на самом деле, null — это примитив. Звучит как ошибка? Давайте разберемся.
🏛 Код из древности. В самой первой версии JavaScript все данные были упакованы в 32-битные блоки. Эти блоки разделялись на две части: небольшую, где хранился «тег» (маркер), который сообщал о типе данных (1-3 бита). И другую, где хранились сами данные.
Представьте это как ящик, на котором есть этикетка (тег), говорящая, что внутри. А затем само содержимое (данные).
Было пять этикеток:
1. 000: объект. Внутри — ссылка на объект.
2. 1: целое число. Внутри — целое число с 31-битной знаковостью.
3. 010: число с плавающей запятой (double). Внутри — ссылка на такое число.
4. 100: строка. Внутри — ссылка на строку.
5. 110: логическое значение (true или false). Внутри — само логическое значение.
Но два значения были особенными:
— undefined имело числовое значение 230, это число находится вне диапазона обычных целых чисел;
— null было представлено как указатель NULL в машинном коде. Это значит, что у него была метка «объект» и ссылка, равная нулю.
Таким образом, когда мы использовали typeof с null, он видел его как «объект» из-за его метки.
🎭 Время проб и ошибок. Разработчики знали об этой странности, но исправление могло «сломать» старый код, который уже работал в интернете. Итак, они решили оставить все как есть, чтобы не вызвать проблемы.
✨ Совет для начинающих. Чтобы не запутаться при работе с null, всегда используйте «строгое сравнение» (===) с null:
value === null; // возвращает true или false в зависимости от значения.
Теперь вы знаете одну из многих тайн JavaScript и как с ней работать. Оставайтесь на связи для новых интересных историй!
🔥71❤8🏆3👍1😱1
Продолжаем раскрывать секреты многопоточной мобильной разработки, о которой эксперты Студии рассказывали на недавнем митапе.
На этот раз поделимся популярными инструментами для работы с многопоточностью и асинхронным программированием в разработке Android-приложений.
→ Узнать об инструментах
→ Посмотреть митап
Делитесь в комментариях вашим опытом многопоточной разработки и задавайте вопросы нашим спикерам.
@chulakov_dev
На этот раз поделимся популярными инструментами для работы с многопоточностью и асинхронным программированием в разработке Android-приложений.
→ Узнать об инструментах
→ Посмотреть митап
Делитесь в комментариях вашим опытом многопоточной разработки и задавайте вопросы нашим спикерам.
@chulakov_dev
🔥34❤13🤩6🤯2
А для любителей лонгридов и желающих разобраться в теме многопоточной Android-разработки более детально мы написали большую статью на Хабре.
Приятного чтения👇
Приятного чтения👇
Хабр
Многопоточность в мобильной разработке
Всем привет! На связи Сергей, Android-разработчик Студии Олега Чулакова на проектах Сбера. В этой статье я хочу рассмотреть один из важнейших аспектов мобильной разработки — многопоточность....
🔥51❤8😍5👍3🤩2🙏1
Привет, друзья!
А вы знали, что на самом деле объекты в JavaScript могут быть: обычными, стандартными и экзотическими? Приготовьтесь узнать больше о мире объектов в JS! 🧐
🔸 Обычные объекты (Ordinary Objects)
Это объекты, имеющие стандартное поведение для всех внутренних методов, которые должны поддерживаться всеми объектами. Они являются наиболее распространенными и простыми в использовании объектами в JavaScript.
📝 Пример обычного объекта:
Это объекты, чья семантика определена спецификацией ECMAScript. Они являются основой для построения всех других объектов в JavaScript и включают в себя такие встроенные конструкторы как `Array`,
📝 Пример стандартного объекта:
Это объекты, которые имеют нестандартное поведение для одного или нескольких внутренних методов. В отличие от обычных, экзотические объекты обладают особыми свойствами и могут выполнять специальные задачи.
📌 Примечание: все объекты, которые не являются обычными, являются экзотическими объектами.
@chulakov_dev
А вы знали, что на самом деле объекты в JavaScript могут быть: обычными, стандартными и экзотическими? Приготовьтесь узнать больше о мире объектов в JS! 🧐
🔸 Обычные объекты (Ordinary Objects)
Это объекты, имеющие стандартное поведение для всех внутренних методов, которые должны поддерживаться всеми объектами. Они являются наиболее распространенными и простыми в использовании объектами в JavaScript.
📝 Пример обычного объекта:
const ordinaryObject = {
name: 'John',
age: 30
};
🔸 Стандартные объекты (Standard Objects)Это объекты, чья семантика определена спецификацией ECMAScript. Они являются основой для построения всех других объектов в JavaScript и включают в себя такие встроенные конструкторы как `Array`,
Date, Function и Object.📝 Пример стандартного объекта:
const standardDate = new Date();
console.log(standardDate); // Вывод: текущая дата и время
🔸 Экзотические объекты (Exotic Objects)Это объекты, которые имеют нестандартное поведение для одного или нескольких внутренних методов. В отличие от обычных, экзотические объекты обладают особыми свойствами и могут выполнять специальные задачи.
📌 Примечание: все объекты, которые не являются обычными, являются экзотическими объектами.
@chulakov_dev
🔥51👍10❤8😱1🤩1🐳1
🐯 Несколько примеров экзотических объектов:
1. Array — экзотический объект, поскольку у него есть своеобразное поведение, когда изменяется свойство 'length'. В отличие от обычных объектов, изменение 'length' массива может привести к добавлению или удалению элементов.
@chulakov_dev
1. Array — экзотический объект, поскольку у него есть своеобразное поведение, когда изменяется свойство 'length'. В отличие от обычных объектов, изменение 'length' массива может привести к добавлению или удалению элементов.
const exoticArray = [1, 2, 3];
exoticArray.length = 2; // Элемент 3 будет удален из массива
2. String — экзотический объект, поскольку его элементы не могут быть изменены после создания, и доступ к ним возможен только через индекс. Также у него есть особые методы, например, `charAt` или slice.
const exoticString = 'Hello';
exoticString[0] = 'W'; // Не сработает, потому что строки неизменяемы
3. Proxy — экзотический объект, поскольку он позволяет создавать объекты с настраиваемыми обработчиками для различных операций. Прокси-объекты могут перехватывать и модифицировать основные операции с объектами, такие как доступ к свойствам, присваивание значений и вызов функций.
const target = { message: 'Hello, World!' };
const handler = {
get: function (obj, prop) {
return prop in obj ? obj[prop] : 'Property not found';
},
};
const exoticProxy = new Proxy(target, handler);
console.log(exoticProxy.message); // Вывод: 'Hello, World!'
console.log(exoticProxy.unknown); // Вывод: 'Property not found'
Теперь, когда вы знаете разницу между обычными, стандартными и экзотическими объектами в JavaScript, вы можете использовать их наиболее эффективно в своих проектах. Экзотические объекты могут показаться сложными, но они предоставляют больше возможностей для решения задач и создания более гибких и мощных приложений. 🚀@chulakov_dev
👍47🔥22❤7👏4😍1🐳1
Всем привет!
Сегодня мы поговорим о библиотеке React Query и о том, как правильно настроить параметры
Что такое React Query?
React Query — это библиотека для управления асинхронными данными в приложениях React. Она абстрагирует процессы получения, кэширования, синхронизации и обновления данных, позволяя разработчикам сконцентрироваться на бизнес-логике приложения.
Что такое staleTime и cacheTime?
@chulakov_dev
Сегодня мы поговорим о библиотеке React Query и о том, как правильно настроить параметры
staleTime и cacheTime.Что такое React Query?
React Query — это библиотека для управления асинхронными данными в приложениях React. Она абстрагирует процессы получения, кэширования, синхронизации и обновления данных, позволяя разработчикам сконцентрироваться на бизнес-логике приложения.
Что такое staleTime и cacheTime?
staleTime определяет, сколько времени (в миллисекундах) после последнего успешного обновления данных запрос считается «свежим». Если запрос устарел, вы все равно получите данные из кеша, но при определенных условиях может произойти фоновая повторная загрузка.cacheTime — это время (также в миллисекундах), в течение которого неактивные (неиспользуемые) данные запроса остаются в кэше, прежде чем они окончательно удаляются. Запросы переходят в неактивное состояние, когда все компоненты, использующие этот запрос, размонтированы.@chulakov_dev
🔥20👍7⚡3🤔1
Как настроить эти параметры?
Выбор оптимальных значений
Что должно быть больше,
Допустим, что вы задали
Пример конфигурации.
Данная конфигурация минимизирует обращения к серверу. То есть подходит для редко меняющихся данных.
Выбор оптимальных значений
staleTime и cacheTime зависит от специфики вашего приложения.staleTime: если ваши данные меняются довольно часто, вы можете установить staleTime на низкое значение (например, 1000 мс). Если данные меняются редко, можно использовать большое значение (например, 5 минут).cacheTime: опять же, все зависит от того, что это за данные и насколько активно они используются. Если данные постоянно запрашиваются, можно установить большое значение cacheTime. Однако, если данные редко используются, следует выбрать меньшее значение, чтобы освободить ресурсы. Нужно стараться балансировать между актуальностью данных и нагрузкой на сервер / пользовательское устройство.Что должно быть больше,
cacheTime или staleTime?Допустим, что вы задали
staleTime 5 минут, а cacheTime 2 минуты. Если пользователь вернется через 3 минуты, вы ожидаете, что у вас все еще будут свежие данные. А вместо данных пользователь увидит спиннер (или скелетон), так как все данные будут удаленны сборщиком мусора. Разумно задавать время cacheTime больше, чем время staleTime, чтобы избежать потери все еще свежих данных.Пример конфигурации.
Данная конфигурация минимизирует обращения к серверу. То есть подходит для редко меняющихся данных.
export const reactQueryConfig = {
defaultOptions: {
queries: {
staleTime: 1 * 60 * 60 * 1000, // 1 hour
cacheTime: 5 * 60 * 60 * 1000, // 5 hours
refetchOnmount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
},
};
@chulakov_dev👍22🔥10❤4🤩3👏2
Намного проще понять утверждение, чем отрицание этого утверждения.
О чем это мы? 🤔
Читайте в карточках➡
@chulakov_dev
О чем это мы? 🤔
Читайте в карточках
@chulakov_dev
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥28⚡4👍3👏1🤩1
Всем привет!
Опубликовали вторую большую статью о многопоточности на Хабре. На этот раз для iOS-разработчиков.
Читайте и подписывайтесь на наш блог — будет еще много интересного👇
Опубликовали вторую большую статью о многопоточности на Хабре. На этот раз для iOS-разработчиков.
Читайте и подписывайтесь на наш блог — будет еще много интересного👇
Хабр
Многопоточность в iOS-разработке
Здравствуйте, уважаемые читатели Хабра! Меня зовут Кирилл, я iOS-разработчик приложений Сбера в Студии Олега Чулакова. Когда я не занят написанием кода, мне нравится изучать новые технологии и...
🔥23👍2👏2⚡1
Всем привет!
Сегодня расскажем об опыте миграции с npm на pnpm на одном из проектов и преимуществах такого решения.
pnpm отличается от пакетных менеджеров npm и yarn двумя ключевыми преимуществами — скоростью работы и безопасностью.
⚫ Скорость установки пакетов с pnpm значительно выше, чем в npm и yarn. Это достигается за счет использования специальных hard links, с помощью которых один и тот же npm-пакет не копируется многократно, а извлекается из кэша, если он был ранее загружен. Также pnpm способен устанавливать несколько пакетов параллельно, что отражается на скорости установки.
⚫ Безопасность работы обеспечивается за счет специального алгоритма, при котором создается файл с контрольными суммами всех установленных пакетов, и каждый npm-пакет перед выполнением кода проверяется на предмет целостности.
Проект должен был работать в docker-окружении, поэтому задачу с внедрением pnpm с помощью docker мы решили следующим образом:
✅ Собрали образ с помощью Dockerfile. За основу взяли Node.js 18 версию alpine
✅ Собрали образ с именем Node.js:18-pnpm-8.6.1, который загрузили в свой gitlab registry
✅ Подготовили docker-compose файл для запуска frontend-приложения
Сегодня расскажем об опыте миграции с npm на pnpm на одном из проектов и преимуществах такого решения.
pnpm отличается от пакетных менеджеров npm и yarn двумя ключевыми преимуществами — скоростью работы и безопасностью.
Проект должен был работать в docker-окружении, поэтому задачу с внедрением pnpm с помощью docker мы решили следующим образом:
FROM node:18.16-alpine3.18
# Install pnpm
RUN apk add curl \
&& curl -fsSL "https://github.com/pnpm/pnpm/releases/download/v8.6.1/pnpm-linuxstatic-x64" -o /bin/pnpm; chmod +x /bin/pnpm;
services:Пакетный менеджер pnpm может быть особенно полезен разработчикам JavaScript или TypeScript проектов. В нашем случае его использование помогло значительно ускорить установку пакетов и обеспечить безопасность выполнения кода.
# Nextjs applicaton container
app:
image: gitlab.com/images/nodejs:18-pnpm-8.6.1
logging:
driver: 'json-file'
options:
max-file: '2'
max-size: '5m'
container_name: app
restart: always
environment:
PORT: 3000
HOST_URL: "${HOST_URL}"
command: [ "pnpm", "start" ]
working_dir: "/app"
volumes:
- ./../../src:/app:rw
networks:
- network_name
Please open Telegram to view this post
VIEW IN TELEGRAM
👍22⚡7❤3🔥2👏1🤯1
Всем привет!
Хотим поделиться с вами древней JS мудростью. Если кто-то вам скажет, что классы — это «синтаксический сахар» для функций-конструкторов, то смело опровергайте это заявление, потому что у вас есть как минимум 3 веские причины утверждать обратное.
Сегодня рассмотрим первую из них.
↪️ При создании объекта через класс его внутренний флаг [[isClassConstructor]] будет равен true, в отличие от создания через функцию-конструктор. Это означает, что конструктор, вызванный как обычная функция, немедленно выбросит ошибку. Другими словами, конструктор класса не является классическим функциональным объектом.
➡️ В целом, этот пункт не столько про экземпляры объектов, сколько про сравнение конструкторов. Но важно понимать, что «синтаксический сахар» подразумевает изменение синтаксиса без изменения функционала, а здесь мы наблюдаем и то, и другое.
➡️ Остальные причины разберем в следующих постах, не пропустите!
@chulakov_dev
Хотим поделиться с вами древней JS мудростью. Если кто-то вам скажет, что классы — это «синтаксический сахар» для функций-конструкторов, то смело опровергайте это заявление, потому что у вас есть как минимум 3 веские причины утверждать обратное.
Сегодня рассмотрим первую из них.
function UserFn(name, age) {
this.name = name;
this.age = age;
}
class UserClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
console.log(UserFn('John', 25)) // выполняется
console.log(UserClass('John', 25)) // Error: Class constructor UserClass cannot be invoked without "new"
Данный пример не в полной мере обеспечивает понимание отличия класса от функции-конструктора, если рассматривать его в контексте боевой разработки. Скорее, это рассуждение на уровне внутренностей JS, но дальше — интереснее.@chulakov_dev
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥28⚡6👍5❤2😁2
Привет, друзья!
В пятницу мы писали про то, что классы — это не «синтаксический сахар» для функций-конструкторов и назвали первую причину, почему это так.
⚫ Сегодня разбираем вторую причину.
Методы класса не являются его свойствами, а значит, не могут быть использованы как ключи в цикле for-in.
Осталось разобрать последнюю особенность классов — следите за постами в канале🕓 .
@chulakov_dev
В пятницу мы писали про то, что классы — это не «синтаксический сахар» для функций-конструкторов и назвали первую причину, почему это так.
Методы класса не являются его свойствами, а значит, не могут быть использованы как ключи в цикле for-in.
function UserFn(name, age) {
this.name = name;
this.age = age;
return {
name,
age,
greet() {
console.log('Hi!');
},
};
}
class UserClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log('Hi!');
}
}
const UserFromFunction = new UserFn('John', 25);
const UserFromClass = new UserClass('John', 25);
for (const key in UserFromFunction) {
console.log(key); // name, age, greet
}
for (const key in UserFromClass) {
console.log(key); // name, age
}
Также мы можем это проверить с помощью метода Object.getOwnPropertyDenoscriptor, который возвращает информацию обо всех дескрипторах свойства объекта. У метода класса их нет, а это доказывает, что метод класса технически не является его свойством. console.log(Object.getOwnPropertyDenoscriptor(UserFromFunction, 'greet')) // Объект с дескрипторамиЭто особенно полезно в тех случаях, когда вам нужно циклом перебрать свойства объекта, и вы не хотите включать его методы в итерации цикла.
console.log(Object.getOwnPropertyDenoscriptor(UserFromClass, 'greet')) // undefined
Осталось разобрать последнюю особенность классов — следите за постами в канале
@chulakov_dev
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥26👍7⚡1❤1
Всем привет!
Завершаем тему с классами. Итак, третья причина, по которой классы нельзя назвать «синтаксическим сахаром».
📌 Код класса всегда выполняется в strict mode, и нет ни одного способа выполнить его иначе, в отличие от функции-конструктора. Как мы можем доказать это?
Первое, что приходит на ум — вспомнить, что this в строгом режиме всегда равен undefined. Поэтому нам необходимо использовать его так, чтобы он не являлся методом объекта. Для этого достаточно указать его внутри колбэка какой-либо функции, которая будет внутри метода. Оператор цикла forEach — отличный помощник в нашем примере.
Если остались вопросы или просто хотите поболтать о сахаре, пишите в комментарии⬇️ .
Завершаем тему с классами. Итак, третья причина, по которой классы нельзя назвать «синтаксическим сахаром».
Первое, что приходит на ум — вспомнить, что this в строгом режиме всегда равен undefined. Поэтому нам необходимо использовать его так, чтобы он не являлся методом объекта. Для этого достаточно указать его внутри колбэка какой-либо функции, которая будет внутри метода. Оператор цикла forEach — отличный помощник в нашем примере.
function UserFn(name, age) {
this.name = name;
this.age = age;
return {
name,
age,
greet() {
[0].forEach(function () {
console.log(this);
});
},
};
}
class UserClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
[0].forEach(function () {
console.log(this);
});
}
}
const UserFromFunction = new UserFn('John', 25);
const UserFromClass = new UserClass('John', 25);
console.log(UserFromFunction.greet()); // object Window
console.log(UserFromClass.greet()); // undefined
Теперь вы знаете чуть больше о классах — эта информация пригодится вам на пути становления JS-сэнсэями. Если остались вопросы или просто хотите поболтать о сахаре, пишите в комментарии
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥22👍6❤2🤩2
Всем привет!
Сегодня поговорим о важном аспекте разработки приложений — обеспечении взаимодействия между backend- и frontend-приложениями в режиме реального времени. Речь пойдет о технологии Centrifugo.
Расскажем, что это такое и какие полезные функции и преимущества есть у данной технологии. А также разберем примеры, в каких случаях ее стоит применять, а в каких — нет.
➡ Читать про Centrifugo
Centrifugo — ценный инструмент при разработке современных приложений. Главное, не забывать про принцип YAGNI (You Ain't Gonna Need It) и использовать его с умом.
@chulakov_dev
Сегодня поговорим о важном аспекте разработки приложений — обеспечении взаимодействия между backend- и frontend-приложениями в режиме реального времени. Речь пойдет о технологии Centrifugo.
Расскажем, что это такое и какие полезные функции и преимущества есть у данной технологии. А также разберем примеры, в каких случаях ее стоит применять, а в каких — нет.
Centrifugo — ценный инструмент при разработке современных приложений. Главное, не забывать про принцип YAGNI (You Ain't Gonna Need It) и использовать его с умом.
@chulakov_dev
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12🔥8👍2⚡1
Друзья, привет!
Сегодня предлагаем немного пошевелить 🧠 и проверить свои знания. Интересно, кто решит задачку первым?)
Пишите свои варианты ответа в комментарии.
Ниже правильный ответ👇🏻
Только давайте по-честному! Пишем свой вариант, потом проверяем себя.
Правильный ответ — false
1. typeof [] возвращает строку "object"
2. "object" instanceof Array — false, так как строка не инстанс массива.
@chulakov_dev
Сегодня предлагаем немного пошевелить 🧠 и проверить свои знания. Интересно, кто решит задачку первым?)
Пишите свои варианты ответа в комментарии.
Ниже правильный ответ👇🏻
Только давайте по-честному! Пишем свой вариант, потом проверяем себя.
1. typeof [] возвращает строку "object"
2. "object" instanceof Array — false, так как строка не инстанс массива.
👍15🔥6🤔1🤯1
Всем привет!
CSS стремительно развивается, появляются новые возможности визуального оформления страниц.
Сегодня давайте обсудим плюсы и минусы разных инструментов для работы со стилями и выберем самый удобный.
➡ Читать о стилях
@chulakov_dev
CSS стремительно развивается, появляются новые возможности визуального оформления страниц.
Сегодня давайте обсудим плюсы и минусы разных инструментов для работы со стилями и выберем самый удобный.
@chulakov_dev
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥6⚡2❤1🤔1