Русский
Русский
English
Статистика
Реклама

React

Перевод 5 React-хуков, которые пригодятся в любом проекте

02.05.2021 12:15:03 | Автор: admin
Хочу рассказать о пяти простых React-хуках, которые пригодятся в любом проекте. Причём, полезность этих хуков не зависит от того, в каком именно приложении их будут использовать. Описывая каждый из них, я рассказываю о его реализации и привожу пример его использования в клиентском коде.



Хук useModalState


Модальные окна широко используются в веб-приложениях, они применяются в самых разных ситуациях. При работе с подобными окнами быстро приходит понимание того, что управление их состоянием это трудоёмкая задача, решая которую, приходится постоянно выполнять одни и те же рутинные действия. А если имеется код, написание которого отнимает немало сил и времени, код, который приходится постоянно повторять в разных местах приложения, это значит, что такой код имеет смысл абстрагировать, оформив в виде самостоятельной сущности. Хук useModalState это именно такой код, используемый для управления состоянием модальных окон.

Собственные версии этого хука предоставляют многие библиотеки. Одна из них это Chakra UI. Если вас интересуют подробности об этой библиотеке вот мой материал о ней.

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

Вот код этого хука:

import React from "react";import Modal from "./Modal";export const useModalState = ({ initialOpen = false } = {}) => {const [isOpen, setIsOpen] = useState(initialOpen);const onOpen = () => {setIsOpen(true);};const onClose = () => {setIsOpen(false);};const onToggle = () => {setIsOpen(!isOpen);};return { onOpen, onClose, isOpen, onToggle };};

А вот пример его использования:

const Client = () => {const { isOpen, onToggle } = useModalState();const handleClick = () => {onToggle();};return (<div><button onClick={handleClick} /><Modal open={isOpen} /></div>);};export default Client;

Хук useConfirmationDialog


Хук useConfirmationDialog тоже имеет отношение к модальным окнам. И им я тоже пользуюсь довольно часто. Если пользователь некоего приложения выполняет какие-то важные действия, вроде удаления чего-либо, у него принято запрашивать подтверждение выполнения подобных действий. Поэтому такую логику имеет смысл абстрагировать в виде хука. Вот один из вариантов реализации хука useConfirmationDialog:

import React, { useCallback, useState } from 'react';import ConfirmationDialog from 'components/global/ConfirmationDialog';export default function useConfirmationDialog({headerText,bodyText,confirmationButtonText,onConfirmClick,}) {const [isOpen, setIsOpen] = useState(false);const onOpen = () => {setIsOpen(true);};const Dialog = useCallback(() => (<ConfirmationDialogheaderText={headerText}bodyText={bodyText}isOpen={isOpen}onConfirmClick={onConfirmClick}onCancelClick={() => setIsOpen(false)}confirmationButtonText={confirmationButtonText}/>),[isOpen]);return {Dialog,onOpen,};}

Вот пример его использования:

import React from "react";import { useConfirmationDialog } from './useConfirmationDialog'function Client() {const { Dialog, onOpen } = useConfirmationDialog({headerText: "Delete this record?",bodyText:"Are you sure you want delete this record? This cannot be undone.",confirmationButtonText: "Delete",onConfirmClick: handleDeleteConfirm,});function handleDeleteConfirm() {//TODO: удалить}const handleDeleteClick = () => {onOpen();};return (<div><Dialog /><button onClick={handleDeleteClick} /></div>);}export default Client;

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

Хук useAsync


Грамотная поддержка асинхронных операций в приложении это задача, решить которую сложнее, чем кажется на первый взгляд. Так, может иметься множество переменных, хранящихся в состоянии, за которыми нужно наблюдать в процессе выполнения подобных операций. Приложение может сообщать пользователю о ходе выполнения асинхронной операции, показывая ему индикатор прогресса. Кроме того, нужно обрабатывать ошибки асинхронных операций и, если они происходят, выдавать адекватные сообщения о них. В результате наличие в React-проекте хорошо проработанного фреймворка, обеспечивающего поддержку асинхронных операций, окупится сторицей. Хук useAsync может оказаться полезным именно для решения вышеописанных задач. Вот его код:

export const useAsync = ({ asyncFunction }) => {const [loading, setLoading] = useState(false);const [error, setError] = useState(null);const [result, setResult] = useState(null);const execute = useCallback(async (...params) => {try {setLoading(true);const response = await asyncFunction(...params);setResult(response);} catch (e) {setError(e);}setLoading(false);},[asyncFunction]);return { error, result, loading, execute };};

А ниже показан пример его использования:

import React from "react";export default function Client() {const { loading, result, error, execute } = useAsync({asyncFunction: someAsyncTask,});async function someAsyncTask() {// выполнение асинхронной операции}const handleClick = () => {execute();};return (<div>{loading && <p>loading</p>}{!loading && result && <p>{result}</p>}{!loading && error?.message && <p>{error?.message}</p>}<button onClick={handleClick} /></div>);}

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

Хук useTrackErrors


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

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

import React, { useState } from "react";import FormControl from "./FormControl";import Input from "./Input";import onSignup from "./SignupAPI";export const useTrackErrors = () => {const [errors, setErrors] = useState({});const setErrors = (errsArray) => {const newErrors = { ...errors };errsArray.forEach(({ key, value }) => {newErrors[key] = value;});setErrors(newErrors);};const clearErrors = () => {setErrors({});};return { errors, setErrors, clearErrors };};

Вот как можно пользоваться этим хуком:

import React, { useState } from "react";import FormControl from "./FormControl";import Input from "./Input";import onSignup from "./SignupAPI";export default function Client() {const { errors, setErrors, clearErrors } = useTrackErrors();const [name, setName] = useState("");const [email, setEmail] = useState("");const handleSignupClick = () => {let invalid = false;const errs = [];if (!name) {errs.push({ key: "name", value: true });invalid = true;}if (!email) {errs.push({ key: "email", value: true });invalid = true;}if (invalid) {setErrors(errs);return;}onSignup(name, email);clearErrors();};const handleNameChange = (e) => {setName(e.target.value);setErrors([{ key: "name", value: false }]);};const handleEmailChange = (e) => {setEmail(e.target.value);setErrors([{ key: "email", value: false }]);};return (<div><FormControl isInvalid={errors["name"]}><FormLabel>Full Name</FormLabel><InputonKeyDown={handleKeyDown}onChange={handleNameChange}value={name}placeholder="Your name..."/></FormControl><FormControl isInvalid={errors["email"]}><FormLabel>Email</FormLabel><InputonKeyDown={handleKeyDown}onChange={handleEmailChange}value={email}placeholder="Your email..."/></FormControl><button onClick={handleSignupClick}>Sign Up</button></div>);}

Хук useDebounce


То, что называется debouncing, способно найти применение в любом приложении. В частности, речь идёт об уменьшении частоты выполнения ресурсоёмких операций. Например, это предотвращение вызова API поиска данных после каждого нажатия на клавишу в ходе ввода пользователем поискового запроса. Обращение к API будет выполнено после того, как пользователь завершит ввод данных. Хук useDebounce упрощает решение подобных задач. Вот его простая реализация, которая основана на AwesomeDebounceLibrary:

import AwesomeDebouncePromise from "awesome-debounce-promise";const debounceAction = (actionFunc, delay) =>AwesomeDebouncePromise(actionFunc, delay);function useDebounce(func, delay) {const debouncedFunction = useMemo(() => debounceAction(func, delay), [delay,func,]);return debouncedFunction;}

Вот практический пример использования этого хука:

import React from "react";const callAPI = async (value) => {// вызов дорогого API};export default function Client() {const debouncedAPICall = useDebounce(callAPI, 500);const handleInputChange = async (e) => {debouncedAPICall(e.target.value);};return (<form><input type="text" onChange={handleInputChange} /></form>);}

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

  1. Можно объявить дорогую функцию за пределами функционального компонента (так, как сделано в примере).
  2. Можно обернуть такую функцию с помощью хука useCallback.

Итоги


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

Какими React-хуками вы пользуетесь чаще всего?

Подробнее..

Перевод Рассказ о том, почему в 2021 году лучше выбирать TypeScript, а не JavaScript

15.05.2021 14:20:40 | Автор: admin
Недавно я, используя React Native, занимался разработкой мобильного приложения для медитации Atomic Meditation. Эта программа помогает тем, кто ей пользуется, выработать привычку медитировать, ежедневно уделяя этому занятию какое-то время. В ходе работы у меня появились серьёзные причины приступить к изучению TypeScript и начать пользоваться им вместо JavaScript в проектах среднего и крупного размера.

Прежде чем я начну свой рассказ, мне хотелось бы отметить, что вы сможете разобраться в этой статье, даже если никогда не пользовались React Native. Я буду всё подробно объяснять. А если вы делали какие-нибудь проекты на React, то, читая эту статью, можете считать, что React и React Native это одно и то же.



А теперь расскажу о том, как обычный JavaScript втянул меня в неприятности.

День 1: всё идёт как надо


В React Native есть объект AsyncStorage, который представляет собой хранилище данных типа ключ/значение с асинхронным доступом к значениям по ключам. Он даёт разработчику очень простой механизм для организации постоянного хранения данных на мобильном устройстве пользователя.

Например, воспользоваться им можно так:

AsyncStorage.setItem("@key", value)

AsyncStorage позволяет хранить лишь строковые данные. Поэтому для того чтобы поместить в это хранилище число это число сначала надо конвертировать в строку.

Ниже показано применение React-хука useState для объявления переменной sessionCount и для установки её начального значения в 0. Тут же имеется и функция setSessionCount, которая позволяет менять состояние sessionCount:

const [sessionCount, setSessionCount] = useState(0)

Предположим, пользователь завершил сеанс медитации (я, напомню, занимался разработкой приложения для медитации). В sessionCount хранится общее количество сеансов медитации, завершённых пользователем (я буду теперь называть этого пользователя Anxious Andy беспокойный Энди). Это значит, что нам надо прибавить 1 к значению, хранящемуся в sessionCount. Для этого вызывается функция setSessionCount, в которой и выполняется прибавление 1 к предыдущему значению sessionCount. А потом количество завершённых медитаций нужно сохранить в AsyncStorage в виде строки.

Всё это надо сделать в некоей функции, которую я предлагаю назвать saveData:

// Пользователь завершил сеанс медитацииconst saveData = () => {setSessionCount(prev => {const newSessionCount = prev + 1AsyncStorage.setItem("@my_number", newSessionCount.toString())return newSessionCount})}

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

День 2: затишье перед бурей


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

Теперь, когда программа загружается, данные сессии Энди нужно прочитать из хранилища. В React хук useEffect позволяет выполнять функцию-коллбэк при монтировании компонента.

В коллбэке мы асинхронно получаем данные из хранилища, а после этого вызываем функцию setSessionCount(), передавая ей эти данные, то есть 1:

useEffect(() => {AsyncStorage.getItem("@my_number").then(data => setSessionCount(data))}, [])

Беспокойный Энди успешно справляется с ещё одной медитацией. Поэтому к sessionCount надо добавить 1, что позволит сохранить общее число завершённых сеансов медитации.

Новое значение, как и прежде, мы записываем в хранилище:

// Пользователь завершил сеанс медитацииconst saveData = () => {setSessionCount(prev => {const newSessionCount = prev + 1AsyncStorage.setItem("@my_number", newSessionCount.toString())return newSessionCount})}

К настоящему моменту пользователь завершил 2 сеанса медитации.

День 3: буря


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

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

Но его любовь к этой программе быстро сходит на нет

Программа сообщает ему о том, что он провёл 11 сеансов медитации. А он-то медитировал всего два раза!


Неправильная статистика по сеансам медитации

Что пошло не так?


В первый день мы записали в sessionCount начальное значение число 0.

Пользователь завершил сеанс медитации поэтому мы добавили к sessionCount 1. Затем мы преобразовали то, что получилось, в строку в 1, после чего записали это в асинхронное хранилище (вспомните оно может хранить только строковые данные).

Во второй день мы загружаем данные из хранилища и записываем в sessionCount загруженное значение. То есть 1 (строку, а не число).

Пользователь завершает сеанс медитации и мы прибавляем к sessionCount 1. А в JavaScript 1 + 1 равняется 11, а не 2.

Мы забыли преобразовать строковые данные, считанные из хранилища, в число.

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

JavaScript позволил нам свободно, не сознавая того, что мы делаем, поменять в ходе выполнения программы тип данных, хранящихся в переменной.

Решить эту и другие подобные проблемы можно с помощью TypeScript.

Что такое TypeScript?


Если вы не знакомы с TypeScript, то знайте, что это, в сущности, то же самое, что и JavaScript, но оснащённое некоторыми полезными возможностями. В частности, переменные не могут менять типы. А если это случится TypeScript выдаст сообщение об ошибке.

Браузеры не могут выполнять TypeScript-код. Поэтому TypeScript-файлы проекта надо транспилировать в JavaScript. На выходе получится несколько JavaScript-файлов (или один большой бандл с JS-кодом проекта).

Использование TypeScript в React Native-проектах


Добавить поддержку TypeScript в существующий React Native-проект очень просто. А именно, надо будет кое-что установить из npm и сделать пару настроек.

Теперь нужно будет лишь переименовать файлы с кодом, например App.js в App.tsx, после чего заработает автоматическая система контроля типов.

После того, как изменено расширение файла, TypeScript разразится гневной тирадой о том, что аргумент типа 'string | null' нельзя назначить параметру типа 'SetStateAction<number>'.


TypeScript предупреждает разработчика о том, что с типами данных что-то не так

Это значит, что мне тут, чтобы избавиться от сообщения об ошибке, надо, во-первых, проверить data на null, а во-вторых преобразовать из строки в число (воспользовавшись parseInt()):

useEffect(() => {AsyncStorage.getItem("@my_number").then(data => {if (data) {setSessionCount(parseInt(data))}})}, [])

Использование TypeScript подталкивает разработчика к написанию более качественного и надёжного кода. Это просто замечательно!

По каким материалам изучать TypeScript?


Я изучал TypeScript по этому видеокурсу канала Net Ninja. И если бы мне надо было бы что-нибудь изучить, то я в первую очередь поинтересовался бы тем, нет ли на этом канале курса по тому, что мне нужно.

Кроме того, официальная документация по TypeScript очень даже хороша.

Итоги


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

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

Используете ли вы TypeScript в своих React-проектах?


Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 465 (26 2 мая 2021)

02.05.2021 22:08:33 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст proConf #94: GraphQL Galaxy
podcast Новости 512 от CSSSR: Chrome 91 Beta, postcss-easy-z, tree-shakeable библиотеки, гайды по кастомным CSS-свойствам и CLS
podcast Подкаст Callback Hell: Падение последнего оплота Dart, визуальные ЯП на примере Enso, Lucy DSL для стейт-машин
podcast Подкаст Фронтенд Юность#184: Матрица для мешка с картошкой


Веб-разработка


История фронтенда: JavaScript как отражение новой эпохи
habr HTMHell адовая разметка
habr HTML-теги и атрибуты, о которых вы, возможно, не знали
habr Адаптивный дизайн как антипаттерн
en Скромный элемент img и Core Web Vitals
en Как реализовать выбор дейстий для выделенного текста с помощью SelectionAPI




CSS


habr Примеры применения переменных CSS на практике
habr Контейнерные запросы в CSS
habr Как обеспечить глассморфизм с помощью HTML и CSS
VDS (value definition syntax)
en fit-content и fit-content()
en Полное руководство по Custom Properties
en Первый взгляд на CQFill, полифилл для CSS Container Queries
en Изучение color-contrast() в первый раз
en GPT-3 и CSS-фреймворки
en Понимание easing-функций для анимации и переходов в CSS
en Новые возможности WebKit в Safari 14.1 (Flexbox Gap, Date & Time Inputs, CSS Individual Transform Properties)

JavaScript


habr Целительная сила JavaScript
habr Человеко-читаемый JavaScript: история о двух экспертах
Принцип мозаики, или Как мы сделали JavaScript по-настоящему модульным
en Fower утилитарная CSS in JS библиотека для быстрой разработки интерфейсов.
en Клиентские шаблоны API, о которых должен знать каждый разработчик фронтенда








Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 466 (3 9 мая 2021)

10.05.2021 00:07:48 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст 'Callback Hell': Убийцы вебпака, Microsoft + Bytecode Alliance, удалёнка
podcast Новости 512 от CSSSR: Bootstrap 5, V8 9.1, дженерики в TypeScript, RxJS в Angular, e2e-тесты с Cypress, баг в Safari 14.1
podcast Новости 512 от CSSSR: История фронтенда ч.2, Safari 14.1, CORS, Cookie Store API, Next.js 10.2, RxJS 7, Google I/O 2021
podcast Подкаст Веб-стандарты 280. Safari 14.1, гэпы во флексах, история JS, мозаичный JS, кому нужны алгоритмы
video Видеокаст Front-end. Вопросы на собеседовании #1
podcast video Подкаст Pro Conf #95: HollyJS Moscow 2020

Веб-разработка


habr Вышел Bootstrap 5: оцениваем 7 главных нововведений
habr Почему стоит использовать тег <picture> вместо <img>
habr Базовая структура HTML-документа с объяснением каждой строчки
habr HTML трюки
en Эволюция Jamstack
en Как мы используем веб-компоненты на GitHub
en Аудит дизайн-систем на предмет доступности
en Ускорение процесса разработки с помощью Bootstrap 5
en Как мы ускорили трассировку стека Chrome DevTools в 10 раз




CSS


en Состояние кроссбраузерной разработки CSS
en Container Queries: разъяснения и предложения
en Два варианта использования кастомных свойств
en Полное руководство по веб-шрифтам в шаблонах писем
en Является ли CSS языком программирования?
en CSS Hell Сборник распространенных ошибок в CSS и способы их исправления
en Текст размером 16 пикселей или больше предотвращает масштабирование формы в iOS
en Fluid typography Создавайте текст, масштабируемый в соответствии с размером окна, чтобы заголовки отлично смотрелись на любом экране.
en Вендорные префиксы мертвы?
en Компиляция CSS по запросу с помощью последней версии компилятора Tailwind


JavaScript


habr Как я написал браузерный 3D FPS шутер на Three.js, Vue и Blender
Кастомные типы данных в TypeScript: валидация на этапе компиляции
en Возможны ли 0kb JavaScript в вашем будущем?
en Vue Composition API против React Hooks основная разница
en Создайте трекер спутников с нуля 30-ю строками JavaScript кода







Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 467 (10 16 мая 2021)

17.05.2021 00:12:32 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Подкаст Веб-стандарты 281. SpiderMonkey 25 лет, Safari TP, Bootstrap 5, Гитхаб, префиксы, монорепы и свой git в Яндексе
podcast Подкаст Фронтенд Юность #186. Утюжить веб. В гостях создатель и главный редактор Smashing Magazine Виталий Фридман.
video Видеокаст Front-end. Вопросы на собеседовании #2
video Нужен ли джуну идеальный код: интервью с Вадимом Макеевым
podcast video Подкаст Да как так-то?. Выпуск 4: из филолога-япониста во фронтенд на фрилансе

Веб-разработка


Солидные фронтенды: мониторинг
en Регистрация обработчика протокола URL для PWA
en Различия между WebSockets и Socket.IO
habr Переход к Meta GSAP: поиски идеальной бесконечной прокрутки




CSS


habr Выявление устройств с сенсорными экранами на чистом CSS
habr Венец эволюции CSS-in-JS уже здесь: полностью типизированные стили без рантайма в vanilla-extract
habr Сравнение производительности CSS и CSS-in-JS в реальном мире
habr Инструменты для аудита CSS
Родительский селектор :has() в реальность!


en Дизайн для чтения: советы по оптимизации контента для режимов чтения и приложений-читалок
en Продвинутая CSS-анимация с использованием cubic-bezier()
en aspect-ratio и grid
en Создание Stylesheet Feature Flags с помощью Sass!default
en Плавная прокрутка Sticky ScrollSpy Navigation с фиксированным фоном на CSS
en Взгляд на CSS Tailwind

JavaScript


habr Отслеживание и визуализация положения МКС с помощью 30 строк JavaScript-кода
habr Шпаргалка по JS-методам для работы с DOM
habr Паттерны отложенной инициализации свойств объектов в JavaScript
habr Я выпустил Grafar JS-библиотеку для визуализации
en 7 шагов для безопасного JavaScript в 2021 году
en Современный Javascript: все, что вы пропустили за последние 10 лет (ECMAScript 2020)
en Создайте тетрис с помощью современного JavaScript








Браузеры


en Использование обработчиков пользовательских протоколов для кросс-браузерного отслеживания в Tor, Safari, Chrome и Firefox
Идентификация через анализ внешних обработчиков протоколов в браузере

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 468 (17 23 мая 2021)

24.05.2021 00:09:13 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Подкаст Веб-стандарты #282: Rome, CloudFront Functions, кроссбраузерность, has() и другой современный CSS, мониторинг, GDE
podcast Подкаст Фронтенд Юность #187: Bootstrap круче чем все сраные фреймворки
podcast Подкаст Callback Hell: Производительность CSS-in-JS, языки логического программирования, ООП в современном фронтенде
podcast Новости 512 от CSSSR: Angular 12, Deno 1.10, мониторинг, тестирование UI, :has(), курс по git, Rome + $, TypeScript 4.3 RC
podcast Подкаст Callback Hell Поддержка нескольких мажорных версий, венчурный капитал в Open Source и возвращение тонкого клиента
podcast Подкаст proConf #96: DeveloperWeek 2020
podcast video Подкаст Цинковый Прод #113: Сайт сына маминой подруги

Веб-разработка


W3C представил черновой вариант стандарта WebGPU
en Google AMP мертв! AMP-страницы больше не пользуются приоритетом в поиске Google
en Incremental Static Regeneration: создавайте статические сайты понемногу
en Тестирование фронтенд-приложений что, где, как?





CSS


habr Трюки CSS, которые сделают из вас ниндзя верстки
habr Взгляд на Tailwind CSS
en Новая отзывчивость: веб-дизайн в мире компонентов
en Нет, утилитарные классы это не то же самое, что инлайн стили
en Как создать неоновый текст с помощью CSS
en Как стилизовать любое поле ввода советы и методы
en 82% разработчиков неправильно проходят этот трехстрочный тест по CSS
en Learn CSS Постоянно обновляемый курс CSS и справочник для повышения вашего уровня знаний в области стилизации веба
en aspect-ratio

JavaScript


habr Швейцарский нож отладки JavaScript
habr Трасси что? Доклад Яндекса
en DOM Events изучение системы событий DOM с помощью визуального исследования
en ES12 сделает вашу жизнь проще
en Справочник по массивам JavaScript методы работы с JS-массивами с примерами
en Двухмерные оптические демки в Javascript
en JavaScript API для распознавания людей и ботов в Chrome







Браузеры


habr Microsoft прекратит поддержку приложения Internet Explorer 11 в Windows 10 с июня 2022 года
habr Кросс-браузерный трекинг на основе перебора обработчика внешних протоколов
В Chrome экспериментируют с поддержкой RSS, чисткой User-Agent и автосменой паролей
Компания Mozilla представила режим строгой изоляции сайтов для Firefox
Выпуск перенастраиваемого web-браузера Nyxt 2.0.0

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 469 (24 30 мая 2021)

31.05.2021 00:23:47 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Подкаст Веб-стандарты 83. Sublime Text 4, Sass, Svelte после React, Container Queries, Learn CSS, Google I/O, новые GDE
podcast Подкаст proConf #97: JavaScript for WordPress 2020
podcast Подкаст Callback Hell: Sublime Text 4 и другие редакторы, проблемы написания читаемого кода, завершение эпохи IE
podcast Новости 512 от CSSSR: Chrome 91, TypeScript 4.3, Server-Sent Events API, logux и logux/state, postTask, Parcel 2 beta 3
podcast Новости 512 от CSSSR: Sublime Text 4, PostCSS 8.3, ненадежность TypeScript, Angular DevTools, WebContainers, Google I/O 21
podcast Пилотный выпуск подкаста Goose & Duck: Babel, деньги, два гуся

Веб-разработка


habr Самая серьёзная проблема HTML? Разработчики, разработчики, разработчики
habr Использование веб-компонентов при работе над GitHub
habr Наиболее полное руководство по практическому использованию Web Speech API
en Эволюция и новое определение Jamstack
en 10 вариантов клиентских хранилищ и когда их использовать
en Нарушаете ли вы патент, публикуя PWA?
en Создайте эффект плавного наведения с помощью GSAP и SVG



CSS


habr HTML и CSS ошибки, ухудшающие UX
en Тщательный анализ CSS-in-JS
en CSS Container Queries для дизайнеров
en 25 лет CSS
en CSS Container Queries: примеры использования и стратегии миграции
en Новый способ уменьшить влияние загрузки шрифтов: дескрипторы шрифтов в CSS

JavaScript


habr Карманная книга по TypeScript. Часть 1. Основы, Часть 2. Типы на каждый день
habr 3 способа визуального извлечения данных с помощью JavaScript
en Sparkplug неоптимизирующий компилятор JavaScript
en Новые стандарты доступа к оборудованию устройств с использованием JavaScript
en 7 инструментов, трансформирующих JavaScript-разработку
en Введение в Clio lang: несложная реализация производительного critical js






Браузеры


habr Mozilla примет Manifest v3 для дополнений Firefox, но без мер против блокировщиков рекламы
Релиз Chrome 91
en Призрак Google Reader находит свой путь в новой сборке Chrome Canary

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 470 (1 6 июня 2021)

07.06.2021 00:10:32 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast video Подкаст Goose&Duck #1 Ржавеющий JavaScript
podcast CSSSR Callback Hell: Rescript, мысли пьяного Senior-разработчика, слежка за сотрудниками
podcast Новости 512 от CSSSR: Server-Sent Events: ограничения, поддержка Node.js-проектов, плагины для VSCode, 12 лет Node.js
podcast Подкаст Фронтенд Юность #189 Рон-дом-дом

Веб-разработка


habr С помощью перехода на микросервис мы ускорили бизнес-процесс в 60 раз
en Создание нескольких прогрессивных веб-приложений в одном домене
en Тестирование фронтенда для всех
en Разрушение мифов: Jamstack не может обрабатывать динамический контент
en История веба: часть 1
en Некоторые из лучших пасхальных яиц, спрятанных на сайтах в Интернете





CSS


habr 25 лет CSS
Нативная валидация ввода в CSS
en CSS in SVG in CSS: добавление конфетти в дизайн-систему Stack Overflow
en Новые функциональные селекторы псевдоклассов CSS: is() и: where()
en Тригонометрия в CSS и JavaScript: Введение в тригонометрию
en Тригонометрия в CSS и JavaScript: творческий подход с помощью тригонометрических функций
en The CSS Layout Generator визуальный инструмент для создания компонентов лейаута на CSS Grid
en Inherit, initial, unset, revert
en Шестиугольники и не только: гибкие, отзывчивые сеточные шаблоны, без медиа-запросов

JavaScript


habr Управление зависимостями в Node.js
habr Как мы потерпели неудачу, а затем преуспели в переходе на TypeScript
habr Создание нейронной сети Хопфилда на JavaScript
ES12 сделает вашу жизнь проще!
en Обеспечение быстрой работы JavaScript в WebAssembly
en Еще одна альтернатива Javascript: ReScript
en Взгляд на компиляцию в JavaScript-фреймворках






Браузеры


habr Firefox 89 обновил интерфейс браузера
Релиз Firefox 89 с переработанным интерфейсом
Mozilla, Google, Apple и Microsoft объединили усилия в стандартизации платформы для браузерных дополнений
en Что нового в DevTools (Chrome 92)

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 472 (7 13 июня 2021)

14.06.2021 00:15:08 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Новости 512 от CSSSR: Firefox 89, Safari 15 Beta, Jest 27, цикл статей о работе браузера, разработка базовых компонентов, обзорная статья о тестировании фронтенда и анонс WebExtensions Community Group.
podcast Подкаст Веб-стандарты #285: Бета Chrome92, Firefox89, якоря ирасширения, TeamCity, JSвнутри WASM, TypeScript4.3
podcast Подкаст Фронтенд Юность #190: Как подступиться к старому проекту и не сесть на кулак
podcast Новости 512 от CSSSR: React 18, Vue 3.1, анонс ESLint 8, курсы от CSSSR, :is(), where() и :has(), как прилёг Интернет
podcast Подкаст Callback Hell: Сервисы Google с плохими Web Vitals, шеринг логики между фронтом и бэком, документация на проектах


Веб-разработка


habr Будущее веба: станет ли рендеринг в <canvas> заменой DOM?
en Правильный тег для работы: почему следует использовать семантический HTML
en 5 проблем фронтенда, которые нельзя игнорировать





CSS


habr Выкладка нетрадиционной ориентации
en Полное руководство по CSS Grid с шпаргалкой
en Системные цвета CSS
en CSS определяет значения цвета, соответствующие системным настройкам.
en Media Queries во времена @container
en Давайте узнаем об Aspect Ratio в CSS
en CSS size-adjust для @font-face
en Равные столбцы с Flexbox: это сложнее, чем вы думаете
en Эксперимент с сортируемыми мультиколоночными таблицами
en Знакомьтесь с :has: нативный CSS селектор
en Рог изобилия ContainerQueries
en Создание правил для font-size CSS и создание Fluid Type Scale

JavaScript


habr Как я ускорил движок на 13%
habr Прогнозирование временных рядов на JS: анализ данных для самых маленьких фронтендеров
habr Sparkplug неоптимизирующий компилятор JavaScript в подробностях
en Как создать фулстек-приложение с помощью Supabase и Next.js
en Реализация приватных полей в JavaScript
en Forever Functional: Мемоизация промисов
en Как реализовать принципы SOLID в JavaScript
en Автоматизируйте форматирование и исправление JavaScript кода с помощью Prettier и ESLint
en Современный JavaScript
en Выходя за рамки ESLint: обзор статического анализа в JavaScript
en Доберенные типы API для безопасности JavaScript DOM
en Как создать NFT с помощью JavaScript
en Rust с точки зрения JavaScript





Браузеры


habr Vivaldi 4.0 Первое приближение
Google признал неудачным эксперимент с показом только домена в адресной строке Chrome
en Возможности WebKit в Safari, продемонстрированные на WWDC21


Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 473 (14 20 июня 2021)

21.06.2021 00:15:47 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст Веб-стандарты 286: Высокопроизводительное хранилище для вашего приложения: Storage Foundation API
podcast Подкаст Callback Hell: Микрофронтенды и Module Federation, почему компании боятся открывать свой код, игровая выставка E3
podcast Новости 512 от CSSSR: Canvas-рендеринг, Lighthouse 8, пропорции в CSS, PHP 8.1 alpha, Next.js 11, Линус и антипрививочник
podcast video Подкаст Ленивый фронтендер #2 Kaiwa Show | Как сохранить любовь к веб-разработке
podcast Подкаст Фронтенд Юность #191: HR'ы немножко осатанели


Веб-разработка


habr <img>. Доклад Яндекса
habr Темизация. История, причины, реализация
habr DIV должен уйти: улучшаем HTML
en Изучение Eleventy с нуля. Бесплатный курс, состоящий из 31 урока
en Как я использовал WAAPI для создания библиотеки анимации
en Десять лет веб-компонентам



CSS


video :has в CSS псевдокласс из будущего на примере карточки новости
en Использование свойства `outline` в качестве схлопывающейся границы
en Идеальные всплывающие подсказки с обрезкой и маскированием CSS
en Оптический размер, скрытая сверхспособность вариативных шрифтов
en Краткое руководство по логическим свойствам CSS
en Застенчивая кнопка стоимостью 8 миллионов долларов
en Создание таблиц с липким верхним и нижним колонтитулами стало немного проще

JavaScript


habr Скрываем номера курьеров и клиентов с помощью key-value хранилища
habr Юмористичный обзор Rust с перспективы JavaScript
en Управление состоянием: двусторонние биндинги и расширенные средства форматирования биндингов
en Что такое букмарклеты? Как использовать JavaScript для создания букмарклета в Chromium и Firefox
en Тестирование использования памяти в JavaScript
en Двойные кавычки против одинарных кавычек против обратных кавычек в JavaScript
en sorting-algos-visualizer Визуализация популярных алгоритмов сортировки: QuickSort, MergeSort, HeapSort, BubbleSort, InsertionSort







Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

ReactRedoor IPC мониторинг

12.05.2021 18:04:50 | Автор: admin

В одном из наших проектов, мы использовали IPC (inter-process communication) на сокетах. Довольно большой проект, торгового бота, где были множество модулей которые взаимодействовали друг с другом. По мере роста сложности стал вопрос о мониторинге, что происходит в микросервисах. Мы решили создать свое приложение для отслеживания, потока данных на всего двух библиотеках react и redoor. Я хотел бы поделиться с вами нашим подходом.

Микросервисы обмениваются между собой JSON объектами, с двумя полями: имя и данные. Имя - это идентификатор какому сервису предназначается объект и поле данные - полезная нагрузка. Пример:

{ name:'ticket_delete', data:{id:1} }

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

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

Создадим простой Web Socket сервер.

/** src/ws_server/echo_server.js */const WebSocket = require('ws');const wss = new WebSocket.Server({ port: 8888 });function sendToAll( data) {  let str = JSON.stringify(data);  wss.clients.forEach(function each(client) {    client.send(str);  });}// Отправляем данные каждую секундуsetInterval(e=>{  let d = new Date();  let H = d.getHours();  let m = ('0'+d.getMinutes()).substr(-2);  let s = ('0'+d.getSeconds()).substr(-2);  let time_str = `${H}:${m}:${s}`;  sendToAll({name:'timer', data:{time_str}});},1000);

Сервер каждую секунду формирует строку с датой и отправляет всем подключившимся клиентам. Открываем консоль и запускаем сервер:

node src/ws_server/echo_server.js

Теперь перейдем к проекту приложения. Для сборки и отладки будем использовать rollup конфигурация ниже.

rollup.config.js
import serve from 'rollup-plugin-serve';import babel from '@rollup/plugin-babel';import { nodeResolve } from '@rollup/plugin-node-resolve';import commonjs from '@rollup/plugin-commonjs';import hmr from 'rollup-plugin-hot'import postcss from 'rollup-plugin-postcss';import autoprefixer from 'autoprefixer'import replace from '@rollup/plugin-replace';const browsers = [  "last 2 years",  "> 0.1%",  "not dead"]let is_production = process.env.BUILD === 'production';const replace_cfg = {  'process.env.NODE_ENV': JSON.stringify( is_production ? 'production' : 'development' ),  preventAssignment:false,}const babel_cfg = {    babelrc: false,    presets: [      [        "@babel/preset-env",        {          targets: {            browsers: browsers          },        }      ],      "@babel/preset-react"    ],    exclude: 'node_modules/**',    plugins: [      "@babel/plugin-proposal-class-properties",      ["@babel/plugin-transform-runtime", {         "regenerator": true      }],      [ "transform-react-jsx" ]    ],    babelHelpers: 'runtime'}const cfg = {  input: [    'src/main.js',  ],  output: {    dir:'dist',    format: 'iife',    sourcemap: true,    exports: 'named',  },  inlineDynamicImports: true,  plugins: [    replace(replace_cfg),    babel(babel_cfg),    postcss({      plugins: [        autoprefixer({          overrideBrowserslist: browsers        }),      ]    }),    commonjs({        sourceMap: true,    }),    nodeResolve({        browser: true,        jsnext: true,        module: false,    }),    serve({      open: false,      host: 'localhost',      port: 3000,    }),  ],} ;export default cfg;

Точка входа нашего проекта main.js создадим его.

/** src/main.js */import React, { createElement, Component, createContext } from 'react';import ReactDOM from 'react-dom';import {Connect, Provider} from './store'import Timer from './Timer/Timer'const Main = () => (  <Provider>    <h1>ws stats</h1>    <Timer/>  </Provider>);const root = document.body.appendChild(document.createElement("DIV"));ReactDOM.render(<Main />, root);

Теперь создадим стор для нашего проекта

/** src/store.js */import React, { createElement, Component, createContext } from 'react';import createStoreFactory from 'redoor';import * as actionsWS from './actionsWS'import * as actionsTimer from './Timer/actionsTimer'const createStore = createStoreFactory({Component, createContext, createElement});const { Provider, Connect } = createStore(  [    actionsWS,     // websocket actions    actionsTimer,  // Timer actions  ]);export { Provider, Connect };

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

/** src/actionsWS.js */export const  __module_name = 'actionsWS'let __emit;// получаем функцию emit от redoorexport const bindStateMethods = (getState, setState, emit) => {  __emit = emit};// подключаемся к серверуlet wss = new WebSocket('ws://localhost:8888')// получаем все сообщения от сервера и отправляем их в поток redoorwss.onmessage = (msg) => {  let d = JSON.parse(msg.data);  __emit(d.name, d.data);} 

Здесь надо остановиться поподробнее. Наши сервисы отправляют данные в виде объекта с полями: имя и данные. В библиотеке redoor можно так же создавать потоки событий в которые мы просто передаем данные и имя. Выглядит это примерно так:

   +------+    | emit | --- events --+--------------+----- ... ------+------------->+------+              |              |                |                      v              v                v                 +----------+   +----------+     +----------+                 | actions1 |   | actions2 | ... | actionsN |                 +----------+   +----------+     +----------+

Таким образом каждый модуль имеет возможность "слушать" свои события и по надобности и чужие тоже.

Теперь создадим собственно сам модуль таймера. В папке Timer создадим два файла Timer.js и actionsTimer.js

/** src/Timer/Timer.js */import React from 'react';import {Connect} from '../store'import s from './Timer.module.css'const Timer = ({timer_str}) => <div className={s.root}>  {timer_str}</div>export default Connect(Timer);

Здесь все просто, таймер берет из глобального стейта timer_str который обновляется в actionsTimer.js. Функция Connect подключает модуль к redoor.

/** src/Timer/actionsTimer.js */export const  __module_name = 'actionsTimer'let __setState;// получаем метод для обновления стейтаexport const bindStateMethods = (getState, setState) => {  __setState = setState;};// инициализируем переменную таймераexport const initState = {  timer_str:''}// "слушаем" поток событий нам нужен "timer"export const listen = (name,data) =>{  name === 'timer' && updateTimer(data);}// обновляем стейт function updateTimer(data) {  __setState({timer_str:data.time_str})}

В акшес файле, мы "слушаем" событие timer таймера (функция listen) и как только оно будет получено обновляем стейт и выводим строку с данными.

Подробнее о функциях redoor:

__module_name - зарезервированная переменная нужна просто для отладки она сообщает в какой модуль входят акшенсы.

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

initState - функция или объект инициализации данных модуля в нашем случае это timer_str

listen- функция в которую приходят все события сгенерированные redoor.

Готово. Запускаем компиляцию и открываем браузер по адресу http://localhost:3000

npx rollup -c rollup.config.js --watch

Должны появиться часики с временем. Перейдём к более сложному. По аналогии с таймером добавим еще модуль статистики. Для начала добавим новый генератор данных в echo_server.js

/** src/ws_server/echo_server.js */...let g_interval = 1;// Данные статистикиsetInterval(e=>{  let stats_array = [];  for(let i=0;i<30;i++) {    stats_array.push((Math.random()*(i*g_interval))|0);  }  let data  = {    stats_array  }  sendToAll({name:'stats', data});},500);...

И добавим модуль в проект. Для этого создадим папку Stats в которой создадим Stats.js и actionsStats.js

/** src/Stats/Stats.js */import React from 'react';import {Connect} from '../store'import s from './Stats.module.css'const Bar = ({h})=><div className={s.bar} style={{height:`${h}`px}}>  {h}</div>const Stats = ({stats_array})=><div className={s.root}>  <div className={s.bars}>    {stats_array.map((it,v)=><Bar key={v} h={it} />)}  </div></div>export default Connect(Stats);
/** src/Stats/actionsStats.js */export const  __module_name = 'actionsStats'let __setState = null;export const bindStateMethods = (getState, setState, emit) => {  __setState = setState;}export const initState = {  stats_array:[],}export const listen = (name,data) =>{  name === 'stats' && updateStats(data);}function updateStats(data) {  __setState({    stats_array:data.stats_array,  })}

и подключаем новый модуль к стору

/** src/store.js */...import * as actionsStats from './Stats/actionsStats'const { Provider, Connect } = createStore(  [    actionsWS,    actionsTimer,    actionsStats //<-- модуль Stats  ]);...

В итоге мы должны получить это:

Как видите модуль Stats принципиально не отличается от модуля Timer, только отображение не строки, а массива данных. Что если мы хотим не только получать данные, но и отправлять их на сервер? Добавим управление статистикой.

В нашем примере переменная g_interval это угловой коэффициент наклона нормировки случайной величины. Попробуем ей управлять с нашего приложения.

Добавим пару кнопок к графику статистики. Плюс будет увеличвать значение interval минус уменьшать.

/** src/Stats/Stats.js */...import Buttons from './Buttons' // импортируем модуль...const Stats = ({cxRun, stats_array})=><div className={s.root}>  <div className={s.bars}>    {stats_array.map((it,v)=><Bar key={v} h={it} />)}  </div>  <Buttons/> {/*Модуль кнопочки*/}</div>...

И сам модуль с кнопочками

/** src/Stats/Buttons.js */import React from 'react';import {Connect} from '../store'import s from './Stats.module.css'const DATA_INTERVAL_PLUS = {  name:'change_interval',  interval:1}const DATA_INTERVAL_MINUS = {  name:'change_interval',  interval:-1}const Buttons = ({cxEmit, interval})=><div className={s.root}>  <div className={s.btns}>      <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_PLUS)}>        plus      </button>      <div className={s.len}>interval:{interval}</div>      <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_MINUS)}>        minus      </button>  </div></div>export default Connect(Buttons);

Получаем панель с кнопочками:

И модифицируем actionsWS.js

/** src/actionsWS.js */...let wss = new WebSocket('ws://localhost:8888')wss.onmessage = (msg) => {  let d = JSON.parse(msg.data);  __emit(d.name, d.data);}// "слушаем" событие отправить данные на серверexport const listen = (name,data) => {  name === 'ws_send' && sendMsg(data);}// отправляем данныеfunction sendMsg(msg) {  wss.send(JSON.stringify(msg))}

Здесь мы в модуле Buttons.js воспользовались встроенной функции (cxEmit) создания события в библиотеке redoor. Событие ws_send "слушает" модуль actionsWS.js. Полезная нагрузка data - это два объекта: DATA_INTERVAL_PLUS и DATA_INTERVAL_MINUS. Таким образам если нажать кнопку плюс на сервер будет отправлен объект { name:'change_interval', interval:1 }

На сервере добавляем

/** src/ws_server/echo_server.js */...wss.on('connection', function onConnect(ws) {  // "слушаем" приложение на событие "change_interval"  // от модуля Buttons.js  ws.on('message', function incoming(data) {    let d = JSON.parse(data);    d.name === 'change_interval' && change_interval(d);  });});let g_interval = 1;// меняем интервалfunction change_interval(data) {  g_interval += data.interval;  // создаем событие, что интервал изменен  sendToAll({name:'interval_changed', data:{interval:g_interval}});}...

И последний штрих необходимо отразить изменение интервала в модуле Buttons.js. Для этого в actionsStats.js начнём слушать событие "interval_changed" и обновлять переменную interval

/** src/Stats/actionsStats.js */...export const initState = {  stats_array:[],  interval:1 // добавляем переменную интервал}export const listen = (name,data) =>{  name === 'stats' && updateStats(data);    // "слушаем" событие обновления интервала  name === 'interval_changed' && updateInterval(data);}// обнавляем интервалfunction updateInterval(data) {  __setState({    interval:data.interval,  })}function updateStats(data) {  __setState({    stats_array:data.stats_array,  })}

Итак, мы получили три независимых модуля, где каждый модуль следит только за своим событием и отображает только его. Что довольно удобно когда еще не ясна до конца структура и протоколы на этапе прототипирования. Надо только добавить, что поскольку все события имеют сквозную структуру то надо четко придерживаться шаблона создания события мы для себя выбрали такую: (MODULEN AME)_(FUNCTION NAME)_(VAR NAME).

Надеюсь было полезно. Исходные коды проекта, как обычно, на гитхабе.

Подробнее..
Категории: Javascript , React , Node.js , Reactjs , Nodejs , Ipc , Webscoket

Кэш или стэйт, пробуем React-query

16.05.2021 14:08:59 | Автор: admin
Небо и мореНебо и море

Введение

Популярная библиотека для работы с состоянием веб-приложений на react-js это redux. Однако у нее есть ряд недостатков такие как многословность(даже в связке с redux-toolkit), необходимость выбирать дополнительный слой(redux-thunk, redux-saga, redux-observable). Возникает ощущение, что как-то это все слишком сложнои уже давно появились хуки и в частности хук useContext.. Так что я попробовал другое решение.

Приложение для теста

У меня было простое веб приложение Прогноз погоды написанное с помощью create react app, typescript, redux-toolkit, redux saga. Потом я заменил весь redux на context + react-query. Это очень маленькое, однако рабочее приложение, которым я сам пользуюсь, позволило мне использовать react-query для описания уже существующей логики. Т.е. не делать абстрактный нерабочий проект, который просто раскрывает базовые возможности библиотеки.. В приложении есть выбор городов, получение текущей погоды и прогноза. Т.е. максимум три последовательных запроса к серверу.

Скрины тестового приложенияСкрины тестового приложения

Новый стэйт

Библиотека react-query позволяет работать запросами к серверу, предоставляет доступ данным, позволяет задавать порядок запросов.. Однако для того чтобы с этим работать надо разделить весь стэйт который есть в redux на 2 части. Первая это как раз данные, полученные с сервера. Вторая это все остальное, в моем случае это города выбранные пользователем.

Вторую часть реализовал с помощью react-context. Примерно так:

export const CitiesProvider = ({  children,}: {  children: React.ReactNode;}): JSX.Element => {  const [citiesState, setCitiesState] = useLocalStorage<CitiesState>(    'citiesState',    citiesStateInitValue,  );  const addCity = (id: number) => {    if (citiesState.citiesList.includes(id)) {      return;    }    setCitiesState(      (state: CitiesState): CitiesState => ({        ...state,        citiesList: [...citiesState.citiesList, id],      }),    );  }; // removeCity..,  setCurrentCity..  return (    <СitiesContext.Provider      value={{        currentCity: citiesState.currentCity,        cities: citiesState.citiesList,        addCity,        removeCity,        setCurrentCity,      }}    >      {children}    </СitiesContext.Provider>  );};

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

React-query

Для загрузки, хранения, обновления данных с сервера использовал библиотеку react-query. Подключается примерно так:

import { QueryClient, QueryClientProvider } from 'react-query';import { ReactQueryDevtools } from 'react-query/devtools';import { CitiesProvider } from './store/cities/cities-provider';const queryClient = new QueryClient();ReactDOM.render(<React.StrictMode><QueryClientProvider client={queryClient}><CitiesProvider><App />

Простой пример использования:

const queryCities = useQuery('cities', fetchCitiesFunc);const cities = queryCities.data || [];

Первый параметр 'cities' это ключ строка, которая должна быть уникальной для каждого запроса. Второй - это функция, которая возвращает Promise, который резолвит данные или отдает ошибку. Также можно передать третьим параметром объект с настройками.

useQuery возвращает объект UseQueryResult, который содержит данные о состоянии запроса, ошибку или данные

const { isLoading, isIdle, isError, data, error } = useQuery(..

Для выполнения последовательных запросов мне показалось удобным написать отдельный хук

export function useCurrentWeather(): WeatherCache {const { currentCity } = useContext(СitiesContext); // запрашиваем список городовconst queryCities = useQuery('cities', fetchCitiesFunc, {refetchOnWindowFocus: false,staleTime: 1000 * 60 * 1000,});const citiesRu = queryCities.data || [];// ищем идентификатор текущего города..const city = citiesRu.find((city) => {if (city === undefined) return false;const { id: elId } = city;if (currentCity === elId) return true;return false;});const { id: weatherId } = city ?? {}; // запрашиваем текущую погодуconst queryWeatherCity = useQuery(['weatherCity', weatherId],() => fetchWeatherCityApi(weatherId as number),{enabled: !!weatherId,staleTime: 5 * 60 * 1000,},);const { coord } = queryWeatherCity.data ?? {}; // запрашиваем прогноз по координатам из предыд. запросаconst queryForecastCity = useQuery(['forecastCity', coord],() => fetchForecastCityApi(coord as Coord),{enabled: !!coord,staleTime: 5 * 60 * 1000,},);return {city,queryWeatherCity,queryForecastCity,};}

staleTime Время, по истечении которого, данные считаются устаревшими. Устаревшие данные перезапрашиваются автоматически при монтировании нового экземпляра, перефокусировке или переподключении сети. Интересно, что по умолчанию staleTime =0.

enabled: !!weatherId, Эта настройка позволяет выполнять запрос только при определенном условии. Пока условие не будет выполнено useQuery будет возвращать состояние isIdle. Таким образом можно описать последовательность выполнения запросов.

const queryWeatherCity = useQuery(['weatherCity', weatherId],.. 

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

Вот так использую этот хук в компоненте:

export function Forecast(): React.ReactElement {const {queryForecastCity: { isFetching, isLoading, isIdle, data: forecast },} = useCurrentWeather();if (isIdle) return <LoadingInfo text="Ожидание загрузки дневного прогноза" />;if (isLoading) return <LoadingInfo text="Загружается дневной прогноз" />;const { daily = [], alerts = [], hourly = [] } = forecast ?? {};const dailyForecastNext = daily.slice(1) || [];return (<><Alerts alerts={alerts} /><HourlyForecast hourlyForecast={hourly} /><DailyForecast dailyForecast={dailyForecastNext} />{isFetching && <LoadingInfo text="Обновляется дневной прогноз" />}</>);}

Есть два разных состояния isLoading это первая загрузка и isFetching - это обновление.

Инструменты разработчика

У React-query есть возможность вывести окошко инструментов разработчика. Оно немного похоже на окно Redux, но появляется в виде фиксированного окошка поверх приложения(можно закрыть и останется только кнопка)

Окно инструментов разработчикаОкно инструментов разработчика

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

import { ReactQueryDevtools } from 'react-query/devtools';

В документации сказано, что при process.env.NODE_ENV === 'production' , в релизную сборку это не попадет автоматически. У меня в Create React App все корректно.

Другие возможности

Также у react-query есть возможности, которые мне не понадобились, однако я все же опишу некоторые из них, примеры кода будут из документации.

  • useQueries позволяет динамически формировать массив запросов. Это нужно т.к. мы не можем опционально вызывать хуки useQuery.

const userQueries = useQueries(users.map(user => {return {queryKey: ['user', user.id],queryFn: () => fetchUserById(user.id),}})
  • По умолчанию настроен автоматический перезапрос данных, при получении ошибки, 3 попытки. Это можно настроить с помощью конфига retry.

  • Для запросов на создание, обновление, удаление данных есть хук useMutations

const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
  • Можно делать постраничные запросы, для бесконечных запросов есть хук useInfiniteQuery

  • Также есть предзагрузка, инвалидация запросов, оптимистичное обновление и еще много всего, что можно посмотреть в документации.

Заключение

После замены redux-toolkit + redux-saga и context + react-query код мне показался значительно проще и я получил из коробки больший функционал для работы с запросами к серверу. Однако часть с react-context не имеет специальных инструментов отладки и вообще вызывает опасения, но она оказалось совсем небольшой и мне вполне хватило react-devtools. В целом я доволен библиотекой react-query и вообще идея отделения кэша в отдельную сущность кажется мне интересной. Но все же это очень маленькое приложение с несколькими get запросами..

Ссылки

Верстка корректна только для мобильных устройств

Есть ветка с redux

Документация react-query

Подробнее..

Перевод 7 лучших библиотек для создания молниеносно быстрых приложений ReactJS

27.05.2021 18:04:31 | Автор: admin

Некоторые необходимые инструменты для rock-star разработчика

Привет, Хабр. В рамках набора на курс "React.js Developer" подготовили перевод материала.

Всех желающих приглашаем на открытый демо-урок "Webpack и babel". На занятии рассмотрим современные и мощные фишки JavaScript Webpack и Babel. Пошагово покажем, как с нуля создать проект на React, используя Webpack. Присоединяйтесь!


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

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

Давайте начнем.

. . .

1. React Query

Известно, что React Query, библиотека управления состоянием для React, отсутствует.

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

Да. Это именно то, что она делает. Она помогает нам управлять состоянием сервера без лишних хлопот. Это может уменьшить необходимость использования библиотеки управления состоянием, такой как Redux.

Преимущества

  • Автоматическое кэширование

  • Автоматическое обновление данных в фоновом режиме

  • Значительно сокращает объем кода

До использования React Query

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

const useFetch = (url) => {  const [data, setData] = useState();  const [isLoading, setIsLoading] = useState(false);  const [error, setError] = useState(false);   useEffect(() => {    const fetchData = async () => {      setIsError(false);      setIsLoading(true);      try {        const result = await fetch(url);        setData(result.data);      } catch (error) {        setError(error);      }      setIsLoading(false);    };    fetchData();  }, [url]);    return {data , isLoading , isError}}

После (использования) React Query

Вот код, если мы хотим использовать React Query. Посмотрите, какой он маленький.

import { useQuery } from 'react-query'const { isLoading, error, data } = useQuery('repoData', () =>    fetch(url).then(res =>res.json()  ))

Посмотрите, насколько сильно сократился наш код.

. . .

2. React Hook Form

React Hook Form - это современная библиотека обработки форм, которая может поднять эффективность работы вашей формы на совершенно новый уровень.

Преимущества

  • Уменьшает объем кода

  • Сокращает ненужный ре-рендеринг.

  • Легко интегрируется с современными библиотеками пользовательского интерфейса (UI)

Ниже приведен пример, демонстрирующий, как React Hook Form может улучшить качество кода.

Без React Hook Form

Вот пример создания формы авторизации вручную.

function LoginForm() {  const [email, setEmail] = React.useState("");  const [password, setPassword] = React.useState("");  const handleSubmit = (e: React.FormEvent) => {    e.preventDefault();    console.log({email, password});  }    return (    <form onSubmit={handleSubmit}>          <input        type="email"        id="email"        value={email}        onChange={(e) => setEmail(e.target.value)}      />            <input        type="password"        id="password"        value={password}        onChange={(e) => setPassword(e.target.value)}      />          </form>  );}

С помощью React Form

Вот тот же пример с React Hook Form.

function LoginForm() {  const { register, handleSubmit } = useForm();    const onSubmit = data => console.log(data);     return (    <form onSubmit={handleSubmit(onSubmit)}>      <input {...register("email")} />      <input {...register("password")} />      <input type="submit" />    </form>  );}

Выглядит аккуратно и в то же время эффективно. Попробуйте.

. . .

3. React Window

React Window используется для рендеринга длинных списков. Представьте, что у вас есть список из 1 000 элементов. На экране отображаются только десять, но ваш код пытается визуализировать 1000 элементов одновременно.

Это может привести к серьезным задержкам в вашем приложении. Данная библиотека очень популярна и является обязательным инструментом в вашем арсенале.

Ручной рендеринг 1 000 элементов

import React, {useEffect, useState} from 'react';const names = [] // 1000 namesexport const LongList = () => {    return <div>       {names.map(name => <div> Name is: {name} </div>)}     <div/>}

Но этот код рендерит 1000 элементов одновременно, хотя на экране можно увидеть не более 10-20 элементов.

Использование React Window

Теперь давайте используем React Window.

import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => <div style={style}> Name is {names[index]}</div> const LongList = () => (  <List    height={150}    itemCount={1000}    itemSize={35}    width={300}  >    {Row}  </List>);

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

. . .

4. React LazyLoad

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

React LazyLoad - это библиотека, специально созданная для этой цели. Вы просто оборачиваете свой компонент, а эта библиотека позаботится обо всем остальном.

Преимущества

  • Повышенная производительность

  • Поддерживает рендеринг на стороне сервера

Без LazyLoad

Вот пример, в котором мы загружаем пять изображений вручную.

import React from 'react';const ImageList = () => {    return <div>    <img src ='image1.png' />    <img src ='image2.png' />    <img src ='image3.png' />    <img src ='image4.png' />    <img src ='image5.png' />  </div>}

С LazyLoad

Вот тот же пример с компонентом LazyLoad.

import React from 'react';import LazyLoad from 'react-lazyload';const ImageList = () => {    return <div>    <LazyLoad> <img src ='image1.png' /> <LazyLoad>    <LazyLoad> <img src ='image2.png' /> <LazyLoad>    <LazyLoad> <img src ='image3.png' /> <LazyLoad>    <LazyLoad> <img src ='image4.png' /> <LazyLoad>    <LazyLoad> <img src ='image5.png' /> <LazyLoad>  </div>}

. . .

5. Почему вы выполняете рендеринг (Why Did You Render)

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

Этот замечательный пакет, Why Did You Render, помогает нам найти проблемы с производительностью и решить их. Вы просто включаете его в любом компоненте, и он сообщает вам, почему именно происходит рендеринг.

Ниже представлен компонент с возникающими проблемами рендеринга.

import React, {useState} from 'react'const WhyDidYouRenderDemo = () => {    console.log('render')        const [user , setUser] = useState({})    const updateUser = () => setUser({name: 'faisal'})    return <>        <div > User is : {user.name}</div>        <button onClick={updateUser}> Update </button>    </>}export default WhyDidYouRenderDemo

После включения эта библиотека будет записывать в консоль следующий результат.

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

. . .

6. Reselect

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

Reselect решает эту проблему, меморизуя значения и передавая только то, что необходимо.

Преимущества (из документации)

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

  • Селекторы эффективны. Селектор не пересчитывается, если один из его аргументов не изменился.

  • Селекторы являются составными. Они могут быть использованы в качестве входных данных для других селекторов.

Пример

Ниже приведен пример получения значений из хранилища и их изменения в селекторе.

import { createSelector } from 'reselect'const shopItemsSelector = state => state.shop.itemsconst subtotalSelector = createSelector(  shopItemsSelector,  items => items.reduce((subtotal, item) => subtotal + item.value, 0))const exampleState = {  shop: {    items: [      { name: 'apple', value: 1.20 },      { name: 'orange', value: 0.95 },    ]  }}

. . .

7. Deep Equal

Deep Equal - это известная библиотека, которую можно использовать для сравнения. Это очень удобно. Ведь в JavaScript, несмотря на то, что два объекта могут иметь одинаковые значения, они считаются разными, поскольку указывают на разные области памяти.

Вот почему мы видим следующий результат.

const user1 = {    name:'faisal'}const user2 ={    name:'faisal'}const normalEqual = user1 === user2 // false

Но если нужно проверить равенство (для мемоизации), то это становится затратной (и сложной) операцией.

Если мы используем Deep Equal, то это повышает производительность в 46 раз. Ниже приведен пример того, как мы можем это сделать.

var equal = require('deep-equal');const user1 = {    name:'faisal'}const user2 ={    name:'faisal'}const deepEqual = equal(user1 , user2); // true -> exactly what we wanted!

. . .

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

Оставляйте комментарии, если у вас на примете есть другие. Хорошего дня!

Ресурсы

  1. Веб-сайт React Query

  2. Веб-сайт React Hook Form

  3. Примеры React Window

  4. Пакет Why Did You Render

  5. Пакет React Lazy Load

  6. Reselect Репозиторий

  7. Пакет Deep Equal


Узнать подробнее о курсе "React.js Developer"

Смотреть открытый онлайн-урок "Webpack и babel"

Подробнее..

2d-графика в React с three.js

06.06.2021 12:22:20 | Автор: admin

У каждого из вас может возникнуть потребность поработать с графикой при создании React-приложения. Или вам нужно будет отрендерить большое количество элементов, причем сделать это качественно и добиться высокой производительности при перерисовке элементов. Это может быть анимация либо какой-то интерактивный компонент. Естественно, первое, что приходит в голову это Canvas. Но тут возникает вопрос: Какой контекст использовать?. У нас есть выбор 2d-контекст либо WebGl. А как на счёт 2d-графики? Тут уже не всё так очевидно.

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

Но тут возникает проблема. Вы сможете ощутить её, если начнете работать с документацией WebGl. С первых мгновений становится понятно, что она слишком низкоуровневая, в отличие от 2d context. Поэтому, чтобы не писать тонны кода, перед нами встаёт очевидное решение использование библиотеки. Для реализации этой задачи подходят библиотеки pixi.js и three.js с качественной документацией, большим количеством примеров и крупным комьюнити разработчиков.

Pixi.js или three.js

На первый взгляд, выбрать подходящий инструмент несложно: используем pixi.j для 2d-графиков, а three.js для 3d. Однако, чем 2d отличается от 3d? По сути дела, отсутствием 3d-перспективы и фиксированным значением по третьей координате. Для того чтобы не было перспективы, мы можем использовать ортографическую камеру.

Вероятно, вы спросите: Что за камера?. Camera это одно из ключевых понятий при реализации графики, наряду со scene и renderer. Для наглядности приведу аналогию. Представим, что вы стоите в комнате, держите в руках смартфон и снимаете видеоролик. Та комната, в которой вы снимаете видео это scene. В комнате могут быть различные предметы, например, стол и стулья это объекты на scene. В роли camera выступает камера смартфона, в роли renderer матрица смартфона, которая проецирует 3d-комнату на 2d-экран.

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

Таким образом, three.js также подходит для 2d-графики. Так что же в итоге выбрать? Мы попробовали оба варианта и выявили на практике несколько преимуществ three.js.

  • Во-первых, нам нужно было выполнить интерактивное взаимодействие с элементами на сцене. Написать собственную реализацию достаточно трудозатратно, но в обеих библиотеках уже есть готовые решения: в pixi.js из коробки, в three.js библиотека three.interaction.

Казалось бы, в этом плане библиотеки равноценны, но это лишь первое впечатление. Особенность реализации интерактивности в pixi.js предполагает, что интерактивные элементы должны иметь заливку. Но как быть с линейными графиками? У них же нет заливки. Без собственного решения в этом случае не обойтись. Что же касается three.js, то тут этой проблемы нет, и линейные графики также интерактивны.

  • Еще одна задача это экспорт в SVG. Нам нужно было реализовать функциональность, которая позволит экспортировать в SVG то, что мы видим на сцене, чтобы потом это изображение можно было использовать в печати. В three.js для этого есть готовый пример, а вот в pixi.js нет.

  • Ну и будем честны с собой, в three.js больше примеров реализации тех или иных задач. К тому же, изучив эту библиотеку, при желании мы можем работать с 3d-графикой, а вот в случае pixi.js такого преимущества у нас нет.

Исходя из всего вышеописанного, наш выбор очевиден это three.js.

Three.js и React

После выбора библиотеки мы сталкиваемся с новой дилеммой использовать react-обертку или каноническую three.js.

Для react есть реализация обёртки это react-three-fiber. На первый взгляд, в ней довольно мало документации, что может показаться проблемой. Действительно, при переносе кода из примеров three.js в react-three-fiber возникает много вопросов по синтаксису.

Однако, на практике все не так уж сложно. У этой библиотеки есть обёртка drei с неплохим storybook с готовой реализацией множества различных примеров. Впрочем, всё, что за находится пределами этой реализации, по-прежнему может причинять боль.

Еще одна проблема это жёсткая привязка к react. А если мы отлично реализуем view с графикой и захотим использовать где-то ещё? В таком случае снова придётся поработать.

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

Вот пример нашей архитектуры. В центре сцены нарисован квадрат, который при нажатии на него меняет цвет с синего на серый и с серого на синий.

Создаём класс three.js для работы с библиотекой three.js. По сути, всё взаимодействие с ней будет проходить в объекте данного класса.

class Three {  constructor({    canvasContainer,    sceneSizes,    rectSizes,    color,    colorChangeHandler,  }) {    // Для использования внутри класса добавляем параметры к this    this.sceneSizes = sceneSizes;    this.colorChangeHandler = colorChangeHandler;     this.initRenderer(canvasContainer); // создание рендерера    this.initScene(); // создание сцены    this.initCamera(); // создание камеры    this.initInteraction(); // подключаем библиотеку для интерактивности    this.renderRect(rectSizes, color); // Добавляем квадрат на сцену    this.render(); // Запускаем рендеринг  }   initRenderer(canvasContainer) {    // Создаём редерер (по умолчанию будет использован WebGL2)    // antialias отвечает за сглаживание объектов    this.renderer = new THREE.WebGLRenderer({antialias: true});     //Задаём размеры рендерера    this.renderer.setSize(this.sceneSizes.width, this.sceneSizes.height);     //Добавляем рендерер в узел-контейнер, который мы прокинули извне    canvasContainer.appendChild(this.renderer.domElement);  }   initScene() {    // Создаём объект сцены    this.scene = new THREE.Scene();     // Задаём цвет фона    this.scene.background = new THREE.Color("white");  }   initCamera() {    // Создаём ортографическую камеру (Идеально подходит для 2d)    this.camera = new THREE.OrthographicCamera(      this.sceneSizes.width / -2, // Левая граница камеры      this.sceneSizes.width / 2, // Правая граница камеры      this.sceneSizes.height / 2, // Верхняя граница камеры      this.sceneSizes.height / -2, // Нижняя граница камеры      100, // Ближняя граница      -100 // Дальняя граница    );     // Позиционируем камеру в пространстве    this.camera.position.set(      this.sceneSizes.width / 2, // Позиция по x      this.sceneSizes.height / -2, // Позиция по y      1 // Позиция по z    );  }   initInteraction() {    // Добавляем интерактивность (можно будет навешивать обработчики событий)    new Interaction(this.renderer, this.scene, this.camera);  }   render() {    // Выполняем рендеринг сцены (нужно запускать для отображения изменений)    this.renderer.render(this.scene, this.camera);  }   renderRect({width, height}, color) {    // Создаём геометрию - квадрат с высотой "height" и шириной "width"    const geometry = new THREE.PlaneGeometry(width, height);     // Создаём материал с цветом "color"    const material = new THREE.MeshBasicMaterial({color});     // Создаём сетку - квадрат    this.rect = new THREE.Mesh(geometry, material);     //Позиционируем квадрат в пространстве    this.rect.position.x = this.sceneSizes.width / 2;    this.rect.position.y = -this.sceneSizes.height / 2;     // Благодаря подключению "three.interaction"    // мы можем навесить обработчик нажатия на квадрат    this.rect.on("click", () => {      // Меняем цвет квадрата      this.colorChangeHandler();    });     this.scene.add(this.rect);  }   // Служит для изменения цвета квадрат  rectColorChange(color) {    // Меняем цвет квадрата    this.rect.material.color.set(color);     // Запускаем рендеринг (отобразится квадрат с новым цветом)    this.render();  }}

А теперь создаём класс ThreeContauner, который будет React-обёрткой для нативного класса Three.

import {useRef, useEffect, useState} from "react"; import Three from "./Three"; // Размеры сцены и квадратаconst sceneSizes = {width: 800, height: 500};const rectSizes = {width: 200, height: 200}; const ThreeContainer = () => {  const threeRef = useRef(); // Используется для обращения к контейнеру для canvas  const three = useRef(); // Служит для определения, создан ли объект, чтобы не создавать повторный  const [color, colorChange] = useState("blue"); // Состояние отвечает за цвет квадрата   // Handler служит для того, чтобы изменить цвет  const colorChangeHandler = () => {    // Просто поочерёдно меняем цвет с серого на синий и с синего на серый    colorChange((prevColor) => (prevColor === "grey" ? "blue" : "grey"));  };   // Создание объекта класса Three, предназначенного для работы с three.js  useEffect(() => {    // Если объект класса "Three" ещё не создан, то попадаем внутрь    if (!three.current) {      // Создание объекта класса "Three", который будет использован для работы с three.js      three.current = new Three({        color,        rectSizes,        sceneSizes,        colorChangeHandler,        canvasContainer: threeRef.current,      });    }  }, [color]);   // при смене цвета вызывается метод объекта класса Three  useEffect(() => {    if (three.current) {      // Запускаем метод, который изменяет в цвет квадрата      three.current.rectColorChange(color);    }  }, [color]);   // Данный узел будет контейнером для canvas (который создаст three.js)  return <div className="container" ref={threeRef} />;}; export default ThreeContainer;

А вот пример работы данного приложения.

При первом открытии мы получаем, как и было описано ранее, синий квадрат в центре сцены, которая имеет серый цвет.

После нажатия на квадрат он меняет цвет и становится белым.

Как мы видим, использование нативного three.js внутри React-приложения не вызывает каких-либо проблем, и этот подход достаточно удобен. Однако, на плечи разработчика в этом случае ложится нагрузка, связанная с добавлением/удалением узлов со сцены. Таким образом, теряется тот подход, который берёт на себя virtual dom внутри React-приложения. Если вы не готовы с этим мириться, обратите внимание на библиотеку react-three-fiber в связке с библиотекой drei этот способ позволяет мыслить в контексте React-приложения.

Рассмотрим реализованный выше пример с использованием этих библиотек:

import {useState} from "react";import {Canvas} from "@react-three/fiber";import {Plane, OrthographicCamera} from "@react-three/drei"; // Размеры сцены и квадратаconst sceneSizes = {width: 800, height: 500};const rectSizes = {width: 200, height: 200}; const ThreeDrei = () => {  const [color, colorChange] = useState("blue"); // Состояние отвечает за цвет квадрата   // Handler служит для того, чтобы  const colorChangeHandler = () => {    // Просто поочерёдно меняем цвет с серого на синий и с синего на белый    colorChange((prevColor) => (prevColor === "white" ? "blue" : "white"));  };   return (    <div className="container">      {/* Здесь задаются параметры, которые отвечают за стилизацию сцены */}      <Canvas className="container" style={{...sceneSizes, background: "grey"}}>        {/* Камера задаётся по аналогии с нативной three.js, но нужно задать параметр makeDefault,         чтобы применить именно её, а не камеру заданную по умолчанию */}        <OrthographicCamera makeDefault position={[0, 0, 1]} />        <Plane          // Обработка событий тут из коробки          onClick={colorChangeHandler}          // Аргументы те же и в том же порядке, как и в нативной three.js          args={[rectSizes.width, rectSizes.height]}        >          {/* Материал задаётся по аналогии с нативной three.js,               но нужно использовать attach для указания типа прикрепления узла*/}          <meshBasicMaterial attach="material" color={color} />        </Plane>      </Canvas>    </div>  );}; export default ThreeDrei;

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

В этой статье мы с вами рассмотрели два подхода в использовании библиотеки three.js внутри React-приложения. Каждый из этих подходов имеет свои плюсы и минусы, поэтому выбор за вами.

Спасибо за внимание! Надеемся, что наш опыт был для вас полезен.

Подробнее..

React. Не в глубь, а в ширь. Композиция против реальности

16.06.2021 14:10:53 | Автор: admin

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

Задача: в проект нужны тултипы. Сказано сделано.

interface OwnProps {  hint: string}export const Tooltip: FC<OwnProps> = ({ hint, children }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)    useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )

Спустя какое-то время в проекте должен появиться ещё один тултип, он красивее и принимает обработчик клика. Самое простое и кратчайшее решение изменить имеющийся компонент Tooltip.

interface TooltipProps {  hint: string  onClick?: () => void}export const Tooltip: FC<TooltipProps> = ({ hint, children, onClick }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)  useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    // А ВОТ И НОВЙ ВАРИАНТ ИСПОЛЬЗОВАНИЯ!!!  // в этом компоненте уже обязательно нужен onClick  if (onClick) {    return (      <div ref={ref}>      {children}      <AnotherTooltipComponent config={config} hint={hint} onClick={onClick} />    </div>    )  }  return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )}

Мы модифицировали старый компонент, добавили инструкцию if и всё заработало. Единственное, что несколько смущает на данном этапе, это то, что из интерфейса TooltipProps совсем не очевидно, что обработчик onClick на самом деле не просто опциональное свойство, а ещё и определитель: какой вариант тултипа нужно вернуть. В общем, может и не очевидно, а может и очевидно, ясно одно: Done is better than perfect.

И вот нас снова просят добавить новый тултип DiscountTooltipComponent, который тоже обязательным свойством принимает обработчик onClick. Чтобы отличать два компонента DiscountTooltipComponent от AnotherTooltipComponent мы используем дополнительное свойство type.

interface TooltipProps {  hint: string  type?: 'another' | 'discount'  onClick?: () => void}export const Tooltip: FC<TooltipProps> = ({ type, hint, children, onClick }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)  useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    // А ВОТ И НОВЙ ВАРИАНТ ИСПОЛЬЗОВАНИЯ!!!  // в этом компоненте уже обязательно нужен onClick  if (type && onClick) {    return (      <div ref={ref}>      {children}      {type === 'another' ? (        <AnotherTooltipComponent config={config} hint={hint} onClick={onClick} />      ) : (        <DiscountTooltipComponent config={config} hint={hint} onClick={onClick} />      }    </div>    )  }  return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )}

Условие в инструкции if усложнилось, внутри появился тернарный оператор, само собой код стало сложнее читать. И вот когда условия становятся сложнее и специфичнее, стоит это дело проанализировать.

Начнём сверху, с интерфейса TooltipProps. Глядя на него, совсем не очевидно, что поля type и onClick связаны между собой. Следовательно, не очевидны и варианты использования компонента Tooltip. Мы можем указать type = "another", но не передать onClick, и тогда typescript не выдаст ошибки.

Самое время обратиться к принципу разделения интерфейсов (Interface Segregation Principle), который на уровне компонентов называется принципом совместного повторного использования. Он гласит:

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

Чтобы проблема стала видна отчётливее, представим, что прошло ещё немного времени.

Аналитики просят залогировать нажатие на DiscountTooltipComponent.

interface TooltipProps {  hint: string  type?: 'another' | 'discount'  onClick?: () => void}export const Tooltip: FC<TooltipProps> = ({ type, hint, children, onClick }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)    useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    // ЗДЕСЬ М БУДЕМ ЛОГИРОВАТЬ  const handleClick = () => {    if (type === 'discount') {      // произвести логирование    }    if (onClick) {  onClick()    }}  // А ВОТ И НОВЙ ВАРИАНТ ИСПОЛЬЗОВАНИЯ!!!  // в этом компоненте уже обязательно нужен onClick  if (type) {    return (    <div ref={ref}>      {children}      {type === 'another' ? (        <AnotherTooltipComponent config={config} hint={hint} onClick={handleClick} />      ) : (        <DiscountTooltipComponent config={config} hint={hint} onClick={handleClick} />      }    </div>    )  }    return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )}

Теперь все, кто использовал Tooltip в его первозданном виде, получили в нагрузку handleClick, который ими никак не используется, но ресурсы на него расходуются. А те, кто использовал компонент с type='another', получили не нужную обертку handleClick. Что, если мы разделим интерфейсы, например:

interface Tooltip {  hint: string}interface TooltipInteractive extends Tooltip {  onClick: () => void}

Теперь выделим общую логику в компонент TooltipSettings:

interface TooltipSettingsProps {  hint: string  render: (config: any, hint: string) => JSX.Element}export const TooltipSettings: FC<TooltipSettingsProps> = ({ render }) => {  // допустим в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)  useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])  return (    <div ref={ref}>      {children}      {render(config, hint)}    </div>  )}

Реализуем интерфейс Tooltip:

export const Tooltip: FC<Tooltip> = ({ hint }) => (  <TooltipSettings hint={hint} render={(config, hint) => <TooltipComponent config={config} hint={hint} />} />)

Реализуем интерфейс TooltipInteractive:

export const AnotherTooltip: FC<TooltipInteractive> = ({ hint, onClick }) => (  <TooltipSettings    hint={hint}    render={(config, hint) => <AnotherTooltipComponent onClick={onClick} config={config} hint={hint} />}  />)

В частности DiscountTooltipComponent:

export const DiscountTooltip: FC<TooltipInteractive> = ({ hint, onClick }) => {  const handleClick = () => {    // произвести логирование    // вызвать обработчик    onClick()  }  return (    <TooltipSettings      hint={hint}      render={(config, hint) => <DiscountTooltipComponent onClick={handleClick} config={config} hint={hint} />}    />  )}

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

Подробнее..

Темизация. История, причины, реализация

18.06.2021 22:18:25 | Автор: admin

Введение. Причины появления

Когда веб только зарождался единственной его целью было размещение контента (гипертекстовые страницы), чтобы у пользователей из всемирной паутины был к нему доступ. В то время не могло идти и речи о дизайне, ведь зачем нужен дизайн страницам с научными публикациями, разве они станут от этого полезнее (первый сайт). Времена меняются и сегодня во всемирной паутине далеко не только научные публикации. Блоги, сервисы, социальные сети и многое, многое другое. Каждый сайт нуждается в своей индивидуальности, ему необходимо заинтересовывать и привлекать пользователей. Даже научные сайты постепенно это понимают, ведь большинство ученых хотят не просто изучать те или иные аспекты, а доносить их до людей, тем самым повышая свою популярность и ценность своих исследований (пример 15 из 15 научных сайтов списка сделали редизайн в последние 6 лет). Рядовым обывателям не интересен серый сайт с непонятным содержанием. Наука становится доступнее, а сайты преобразуются в приложения с удобным и приятным интерфейсом.

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

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

Темная тема для ночного периода это не единственная причина добавления темизации на сайт. Другой важной задачей стоит доступность сервиса. Во все мире 285 млн людей с полной или частичной потерей зрения, в России 218т [ист.], до 2,2 млрд с различными дефектами [ист.] почти треть детей в России заканчивает школу в очках[ист.]. Статистика поражает воображение. Однако, большинство людей не лишено зрения полностью, а имеют лишь небольшие отклонения. Это могут быть цветовые отклонения или качественные. Если для качественных отклонений доступность достигается за счет добавления поддержки разных размеров шрифтов, то для цветовых отличным решением является именно добавление темы.

История развития. Бесконечный путь

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

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

Добавление темизации в проект может быть крайне простой задачей, если эта задача ставится на этапах планирования проекта. Несмотря на то, что она стала популярна только в последние годы, сама эта технология совсем не нова. Этот процесс, как и многие другие отлаживался и активно развивался с каждым годом последние 5-10 лет. Сегодня даже страшно представить, как это делали первопроходцы. Нужно было поменять всем элементам классы, оптимизировать это через наследование цветов, обновлять почти весь ДОМ. А это все во временя такого монстра, как IE, снящегося в худших кошмарах бывалым разработчикам, и до появления ES6. Сейчас же, все эти проблемы уже далеки от разработчиков. Многие невероятно трудные процессы под влиянием времени постепенно уходят в былое, оставляя будущим поколениям разработчиков память о тех ужасных временах и прекрасные решения, доведенные во многом до идеала.

JS один из самых динамично развивающихся языков программирования, но в вебе развивается далеко не только он. Добавляются новые возможности и устраняются старые проблемы в таких технологиях, как HTML и CSS. Это, конечно же, невозможно без обновления и самих браузеров. Развитие и популяризация современных браузеров скидывают большой груз с плеч программистов. На этом все эти технологии не останавливаются и уверен, что через годы, о них будут отзываться также, как программисты сейчас отзываются об IE. Все эти обновления дают нам не только упрощение разработки и повышение ее удобства, но и добавляет ряд новых возможностей. Одной из таких возможностей стали переменные в css, впервые появившиеся в браузерах в 2015 году. 2015 год во многом получился знаменательным для веба это исторически важное обновления JS, утверждения стандарта HTTP/2, появление WebAssembly, популяризация минимализма в дизайне и появление ReactJS. Эти и многие другие нововведения нацелены на ускорение сайта, упрощение разработки и повышение удобства взаимодействия с интерфейсом.

Немного из истории css-переменных:

Первое упоминание переменных, которое мне удалось откопать, числится за 2012 годом. В апреле сего года в документации появилось описание новой концепции для css переменные.

Был описан весьма интересный способ создания и использования переменных:

:root {  var-header-color: #06c;}h1 { background-color: var(header-color); }

Однако, до появления этой функциональности в браузерах, должно было пройти значительное время на продумывание и отладку. Так, впервые поддержка css-переменных была добавлена в firefox лишь в 2015 году. Затем, в 2016, к нему присоединились google и safari.

Итоговая реализация отличалась от первоначальной идеи и выглядела следующим, уже привычным нам, образом:

:root {  --header-color: #06c;}h1 { background-color: var(--header-color); }

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

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

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

В параллель спецификации Css развиваются также его пре и постпроцессоры. Их развитие было значительно быстрее, так как им не нужно было описывать спецификацию и продвигать ее во все браузеры. Одним из первых препроцессоров был stylus, созданный в далеком 2011, позднее были созданы sass и less. Они дают ряд преимуществ и возможностей, за счет того, что все сложные функции и модификации во время сборки конвертируются в css. Одной из таких возможностей являются переменные. Но это уже совершенно иные переменные, больше похожие на js, нежели css. В сочетании с миксинами и js можно было настроить темизацию.

Прошло уже 10 лет с появления препроцессора, гигантский отрезок по меркам веба. Произошло множество изменений и дополнений. HTML5, ES6,7,8,9,10. JS обзавелся целым рядом библиотек, отстроив вокруг себя невообразимый по масштабам зверинец. Некоторые из этих библиотек стали стандартом современного веба react, vue и angular, заменив привычный разработчикам HTML на свои альтернативы, основанные на js. JS заменяет и css, сделав такую замечательную технологию, как css in js, дающую те же возможности, но только динамичнее и в привычном формате (порою большой ценой, но это уже совсем другая история). JS захватил веб, а затем перешел на захват всего мира.

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

Проектирование дизайна

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

Так как тема это элемент интерфейса часть работ по планированию возьмут на себя дизайнеры. Подходы к разработке дизайн-систем не стоят на месте. Если раньше дизайн сайта разрабатывали в программах, подобных фотошопу (хотя есть отдельные личности, которые занимаются подобным и сейчас, доводя разработчиков до состояния истинного ужаса). У них была масса минусов, особенно во времена медленных компьютеров и больших идей клиентов. Конечно же, эти программы не канут в лету, они будут использоваться по их основному назначению обработка фотографий, рисование иллюстраций. Их роль получают современные альтернативы, предназначенные в первую очередь для веба Avocode, Zeplin, Figma, Sketch. Удобно, когда основные инструменты, используемые программистом предназначены именно для целей разработки. В таких случаях, развитие инструментов идет в ногу с развитием сфер, для которых они предназначены. Эти инструменты являются отличным тому подтверждением. Когда они появились в них можно было копировать css стили, делать сетки, проверять margin-ы и padding-и. Не прямоугольниками и даже не линейкой, а просто движением мыши. Затем появились переменные, после в мир веба пришел компонентный подход и этот подход появился в данных инструментах. Они следят за тенденциями, делая те или иные утилиты, добавляют наборы инструментов и не останавливаются на всем этом, чудесным образом поспевая за этой, разогнавшейся до невероятных скоростей, машиной.

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

Темизация может выходить и за рамки страницы сайта. Одной из таких возможностей является цвет строки состояния и вкладки в некоторых мобильных браузерах. Для этих элементов также стоит продумать оттенки.

Цветовая гамма

Просматривая дизайн нового проекта, часто можно заметить странный, но весьма популярный способ именования цветов blue200. Конечно же, за подобное можно сказать спасибо дизайнеру, ведь это тоже верный подход, однако для иных целей. Такой способ хорошо подходит, если разработчики будут использовать атомарный css, ставшим в последние годы самым интересным и приятным для разработчиков, но все еще значительно отстающим по использованию от БЭМ-а [ист.]. Однако, ни такой способ именования переменных, ни атомарный css не годятся для сайтов, которые проектируются с учетом темизации. Причин тому много, одна из них заключается в том, что blue200 это всегда светло-синий цвет и для того, чтобы цвет у всех светло-синих кнопок стал темно-синим нужно у всех кнопок поменять его на blue800. Значительно более верным вариантом будет назвать цвет primary-color, потому что такое имя может быть как blue200, так и blue800, но всем участникам разработки будет понятно, что эта переменная означает основной цвет сайта.

colors: {  body: '#ECEFF1',  antiBody: '#263238',  shared: {    primary: '#1565C0',    secondary: '#EF6C00',    error: '#C62828',    default: '#9E9E9E',    disabled: '#E0E0E0',  },},

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

colors: {  ...  text: {    lvl1: '#263238',    lvl3: '#546E7A',    lvl5: '#78909C',    lvl7: '#B0BEC5',    lvl9: '#ECEFF1',  },},

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

Примеры названия переменных:

shared-primary-color,

text-lvl1-color.

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

Теперь, разобравшись с проектированием дизайна в контексте разработки, можно переходить на следующий этап.

Проектирование кода.

Как уже говорилось, на уровне кода есть 3 основных пути проектирования темизации через нативные переменные (с препроцессорами или без), через css in js, через замену файлов стилей. Каждое решение может так или иначе свестись к нативным переменным, но беда заключается в том, что в IE нет их поддержки. Дальше будет описано 2 варианта проектирования темизации с помощью переменных на нативном css и с помощью css in js.

Основные шаги при темизации сайта:

  1. Создание стилей каждой темы (цвета, тени, рамки);

  2. Настройка темы по умолчанию, в зависимости от темы устройства пользователя (в случае с темной и светлой темой);

  3. Настройка манифеста и мета тегов;

  4. Создание стилизованных компонентов;

  5. Настройка смены темы при нажатии на кнопку;

  6. Сохранение выбранной темы на устройстве пользователя.

Третий шаг универсален для любого варианта темизации. Поэтому, сперва кратко о нем.

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

Theme_color цвет темы сайта. Указанный цвет будет использоваться в качестве цвета вкладки и строки состояния на мобильных устройствах на системе Android. У данного функционала крайне скудная поддержка браузеров, однако доля этих браузеров составляет 67%.

caniuse.comcaniuse.com

Background_color цвет, который будет загружаться до загрузки стилей. Поддержка данного атрибута еще более скудная, чем у цвета темы:

caniuse.comcaniuse.com

Переменные

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

caniuse.comcaniuse.com

Полное отсутствие поддержки в IE, долгое ее отсутствие в популярных браузерах и в Safari являются не критическими проблемами, но ощутимыми, хоть и соотносятся с фриками, не готовыми обновлять свои браузеры и устройства. Однако, IE все еще используется и даже популярнее Safari (5,87% против 3,62% по данным на 2020г).

Теперь о реализации данного способа.

1. Создание классов dark и light, содержащих переменные темы.

Способ именования переменных описан в разделе Проектирование дизайна.

В классах темы должны храниться все переменные, используемые для темизации.

.theme-light {  --body-color: #ECEFF1;  --antiBody-color: #263238;  --shared-primary-color: #1565C0;  --shared-secondary-color: #EF6C00;  --shared-error-color: #C62828;  --shared-default-color: #9E9E9E;  --shared-disabled-color: #E0E0E0;  --text-lvl1-color: #263238;  --text-lvl3-color: #546E7A;  --text-lvl5-color: #78909C;  --text-lvl7-color: #B0BEC5;  --text-lvl9-color: #ECEFF1;}.theme-dark {--body-color: #263238;  --antiBody-color: #ECEFF1;  --shared-primary-color: #90CAF9;  --shared-secondary-color: #FFE0B2;  --shared-error-color: #FFCDD2;  --shared-default-color: #BDBDBD;  --shared-disabled-color: #616161;  --text-lvl1-color: #ECEFF1;  --text-lvl3-color: #B0BEC5;  --text-lvl5-color: #78909C;  --text-lvl7-color: #546E7A;  --text-lvl9-color: #263238;}

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

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

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

Для решения этой задачи есть как минимум 2 корректных подхода

2.1) Настройка темы по умолчанию внутри css

Добавляется новый класс, который устанавливается по умолчанию - .theme-auto

Для этого класса добавляются переменные в зависимости от темы устройства посредством media запросов:

@media (prefers-color-scheme: dark) {body.theme-auto {--background-color: #111;--text-color: #f3f3f3;}}@media (prefers-color-scheme: light) {body.theme-auto {--background-color: #f3f3f3;    --text-color: #111;}}

Плюсы данного способа:

  • отсутствие скриптов

  • быстрое выполнение

Минусы:

  • дублирование кода (переменные повторяются с .theme-dark и .theme-light)

  • для определения темы, выбранной при последнем посещении все-равно потребуется скрипт

2.2) Установка класса по умолчанию с помощью js

У js есть полезная функция отслеживание и проверка правил css. Одним из таких правил, как уже было описано выше, является тема устройства пользователя.

Для проверки темы и добавления класса нужной темы нужно добавить следующий код:

if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {body.classlist.add('theme-dark')} else {body.classlist.add('theme-light')}

Дополнительно вы можете подписаться на изменение темы устройства:

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {    if (e.matches) {        body.classlist.remove('theme-light')        body.classlist.add('theme-dark')    } else {        body.classlist.remove('theme-dark')        body.classlist.add('theme-light')    }});

Плюсы:

  • отсутствие дублирования переменных

Минусы:

  • Чтобы не было прыжков темы данный код должен выполняться на верхнем уровне (head или начало body). То есть он должен выполняться отдельно от основного бандла.

3. Создание стилизованных классов для элементов

./button.css

.button {  color: var(--text-lvl1-color);  background: var(--shared-default-color);  ...  &:disabled {    background: var(--shared-disabled-color);  }}.button-primary {background: var(--shared-primary-color);}.button-secondary {background: var(--shared-secondary-color)}

./appbar.css

.appbar {display: flex;  align-items: center;  padding: 8px 0;  color: var(--text-lvl9-color);  background-color: var(--shared-primary-color);}

4. Настройка смены класса при нажатии на кнопку смены темы

Пожалуй, самый простой пункт. Вам необходимо добавить на кнопку слушатель, который будет:

  • удалять прошлые классы, связанные с темой:

body.classlist.remove('theme-light', 'theme-high')
  • добавлять класс выбранной темы:

body.classlist.add('theme-dark')

5. Сохранение выбранной темы на устройстве пользователя.

Тему можно сохранять как в куки, так и в локальном хранилище. Структура и в первом, и во втором случае будет одинаковая: theme: 'light' | 'dark' | 'rose'

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

const savedTheme = localStorage.getItem('theme')if (['light', 'dark', 'rose'].includes(savedTheme)) {body.classlist.remove('theme-light', 'theme-dark', 'theme-rose')body.classList.add(`theme-${savedTheme}`)}

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

Css-in-js

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

В качестве примера будет показана связка React + styled-components + typescript.

1. Создание объектов dark и light, содержащих переменные темы.

Способ именования переменных описан в разделе Проектирование дизайна.

В объектах темы должны храниться все переменные, используемые для темизации.

Вы должны решить, какая тема будет использоваться по умолчанию и передать нужный объект Provider-у.

./App.tsx

import { useState } from 'react'import { ThemeProvider } from 'styled-components'import themes from './theme'const App = () => {const [theme, setTheme] = useState<'light' | 'dark'>('light')const onChangeTheme = (newTheme: 'light' | 'dark') => {setTheme(newTheme)}return (<ThemeProvider theme={themes[theme]}>// ...</ThemeProvide>)}

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

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

Для этого можно настроить тему по умолчанию на верхнем уровне приложения:

useEffect(() => {  if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {    onChangeTheme('dark')  }}, [])

Дополнительно вы можете подписаться на изменение темы устройства:

useEffect(() => {  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {    if (e.matches) {      onChangeTheme('dark')    } else {      onChangeTheme('light')    }  })}, [])

3. Создание стилизованных компонентов

./src/components/atoms/Button/index.tsx - git

import type { ButtonHTMLAttributes } from 'react'import styled from 'styled-components'interface StyledProps extends ButtonHTMLAttributes<HTMLButtonElement> {  fullWidth?: boolean;  color?: 'primary' | 'secondary' | 'default'}const Button = styled.button<StyledProps>(({ fullWidth, color = 'default', theme }) => `  color: ${theme.colors.text.lvl9};  width: ${fullWidth ? '100%' : 'fit-content'};  ...  &:not(:disabled) {    background: ${theme.colors.shared[color]};    cursor: pointer;    &:hover {      opacity: 0.8;    }  }  &:disabled {    background: ${theme.colors.shared.disabled};  }`)export interface Props extends StyledProps {  loading?: boolean;}export default Button

./src/components/atoms/AppBar/index.tsx - git

import styled from 'styled-components'const AppBar = styled.header(({ theme }) => `  display: flex;  align-items: center;  padding: 8px 0;  color: ${theme.colors.text.lvl9};  background-color: ${theme.colors.shared.primary};`)export default AppBar

4. Настройка смены класса при нажатии на кнопку смены темы

Через context api или redux/mobx изменяется имя текущей темы

./App.tsx - git

import { useState } from 'react'import { ThemeProvider } from 'styled-components'import themes from './theme'const App = () => {  const [theme, setTheme] = useState<'light' | 'dark'>('light')  const onChangeTheme = (newTheme: 'light' | 'dark') => {    setTheme(newTheme)  }  return (    <ThemeProvider theme={themes[theme]}>    <ThemeContext.Provider value={{ theme, onChangeTheme }}>    ...</ThemeContext.Provider>    </ThemeProvide>)}

.src/components/molecules/Header/index.tsx - git

import { useContext } from 'react'import Grid from '../../atoms/Grid'import Container from '../../atoms/Conrainer'import Button from '../../atoms/Button'import AppBar from '../../atoms/AppBar'import ThemeContext from '../../../contexts/ThemeContext'const Header: React.FC = () => {  const { theme, onChangeTheme } = useContext(ThemeContext)  return (    <AppBar>      <Container>        <Grid container alignItems="center" justify="space-between" gap={1}>          <h1>            Themization          </h1>          <Button color="secondary" onClick={() => onChangeTheme(theme === 'light' ? 'dark' : 'light')}>            set theme          </Button>        </Grid>      </Container>    </AppBar>  )}export default Header

5. Сохранение выбранной темы на устройстве пользователя.

Тему можно сохранять как в куки, так и в локальном хранилище. Структура и в первом, и во втором случае будет одинаковая: theme: 'light' | 'dark' | 'rose'

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

./App.tsx - git

...function App() {  const [theme, setTheme] = useState<'light' | 'dark'>('light')  const onChangeTheme = (newTheme: 'light' | 'dark') => {    localStorage.setItem('theme', newTheme)    setTheme(newTheme)  }  useEffect(() => {    const savedTheme = localStorage?.getItem('theme') as 'light' | 'dark' | null    if (savedTheme && Object.keys(themes).includes(savedTheme)) setTheme(savedTheme)    else if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {      onChangeTheme('dark')    }  }, [])  useEffect(() => {    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {      if (e.matches) {        onChangeTheme('dark')      } else {        onChangeTheme('light')      }    })  }, [])  return (  ...  )}

Финальный код

Демо

Итоги

Вариантов внедрения темизации много от создания файлов со всеми стилями для каждой темы и их смены при необходимости до css-in-js решений (с нативными css переменными или встроенными в библиотеки решениями). Браузерное api дает возможности для настройки сервиса под каждого конкретного пользователя, считывая и отслеживая тему его устройства.

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

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

Сервисы Google и apple, банки, соц. сети, редакторы, github и gitlab. Продолжать список можно бесконечно, несмотря на то, что это только начало развития технологии, а дальше больше, лучше и проще.

Подробнее..

За что я не люблю Redux

19.06.2021 18:15:23 | Автор: admin

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

Flux - это вовсе не что-то новое либо революционное

Не то, чтобы я не люблю его за это. Скорее, в этом даже нет ничего плохого - если решение хорошее и проверенное временем, разве это плохо? Скорее мне просто удивительно слышать, как подход, реализованный в Flux в целом (и в Redux в частности) некоторые пытаются выдавать за что-то инновационное и революционное. Да и само решение, на мой взгляд, как минимум не лишено недостатков. Но об этом далее, а пока вспомню молодость.

В начале нулевых я разрабатывал ПО и библиотеки компонент на Delphi под Windows (сначала Win9x, потом XP). В операционных системах Windows с самых первых, если не ошибаюсь, версий, для визуальных элементов интерфейса (кнопки, поля ввода) существует понятие окна - да, окно это не только то, что с рамкой, почти любой визуальный элемент управления имел свое собственное окно. Окно в данном случае - это некая структура в памяти, которая имеет ассоциированный с ним идентификатор (window handle) и оконную функцию (см. далее). Если мы хотим выполнить какое-либо действие над элементом, например - изменить текст кнопки, мы должны упаковать это действие в специальную структуру-сообщение (Window message) и отправить ее соответствующему окну. Структура состоит из закодированного типа сообщения (например WM_SETTEXT - для установки текста) и собственно payload. Будучи отправленным, сообщение не попадает в обработчик напрямую - вместо этого оно отправится в очередь, из которой его извлекает некий диспетчер и вызывает оконную функцию того окна, в которое мы сообщение отправили, передав его в виде параметра. Оконная функция в простейшем случае - это большой switch, где в зависимости от типа сообщения мы передаем управление более конкретному обработчику. Ничего не напоминает?

Те времена давно прошли и больших сожалений на этот счет нет. Более широкое использование ООП со временем значительно улучшило качество кода по сравнению с чистым WinAPI с его сообщениями и оконными функциями. И сегодня, наблюдая код с использованием Redux, определенное чувство дежавю возникает.

Нарушение принципа "Low coupling, high cohesion"

Если вы ищите простую и понятную формулировку, что такое качественный дизайн, то эти четыре слова из подзаголовка коротко и емко его описывают - внутри модуля или компонента его элементы должны быть тесно связанны друг с другом, в то время как связи между отдельными модулями/компонентами должны быть слабыми. Это базовая ценность. Все остальные принципы и подходы в проектировании - следствия из этого принципа. "Low coupling, high cohesion" отвечает на вопрос "чего мы хотим добиться", в то время как, скажем, SOLID-принципы или любой из Design Pattern указывает нам "как мы можем этого добиться".

И вот тут Redux подводит - то, что должно быть цельным внутри компонента, оказывается размазанным по множеству файлов и сущностей - получаем Low cohesion вместо High. Связи, которые должны оставаться внутри, выходят наружу. Если нарушение принципа Low Coupling обычно представляют себе в виде переплетений из лапши, то здесь у меня в голове всплывает другое кулинарное блюдо. Позаимствовав терминологию у Java-разработчиков, если отдельный компонент - это фасолинка (Bean) - цельная, замкнутая вещь в себе, то тут мы получаем что-то вроде рагу, где фасоль полопалась и его содержимое вытекло, образовав густую однородную кашу, обволакивающую всю систему целиком, и не позволяющую на нее смотреть как на композицию отдельных законченных и слабо-зависимых сущностей.

Множество Boilerplate кода

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

Неуместное использование

А еще мне не нравится, что Redux или схожие с ним инструменты пытаются использовать там, где они не нужны - скажем, в Angular (angular-redux, NgRx). Redux предназначен для решения проблемы передачи данных в компоненты путем использования глобального State, и в React.js действительно существует такая проблема, там его использование кажется уместным. Но в Angular такой проблемы нет, Injectable-сервисы прекрасно справляются с этой задачей. Зачем решать несуществующую проблему, порождая при этом новые (о которых было написано выше)?

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

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

Подробнее..
Категории: Javascript , React , Reactjs , Web , Redux , Flux

Почему мы должны выбросить React и взяться за Angular

15.06.2021 14:10:21 | Автор: admin

Хочу представить перевод довольно интересной статьи Сэма Редмонда, Why We Should Throw Out React and Pick Up Angular. На мой взгляд, статья описывает основные возможности Angular. Она может показаться довольно вызывающей, но постарайтесь отнестись к ней немного с юмором :)

Итак, я уже немного написал о том, почему, как мне кажется, мы должны перестать использовать React. Чтобы подвести итог этой статьи, я перечислю несколько проблем, с которыми я столкнулся в React:

  1. Он популярен в основном от того, что вокруг него много шумихи.

  2. Он даёт слишком много свободы. Это приводит к фундаментальным ошибкам на ранней стадии разработки приложения, которые проявляются не сразу.

  3. Использует не оправдано много памяти и не поддаётся оптимизации(not tree-shakable).

  4. Сложность React приложения растёт экспоненциально с ростом размера приложения и это затрудняет его обслуживание.

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

  6. Обновление вашего приложения до последней версии React часто сопряжено с полным переписыванием этого самого приложения.

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

All aboard the hype train

Angular также получает изрядное количество хайпа, так что я не могу сказать, что Angular решает эту проблему. Однако, я не думаю, что Angular получает такое же количество хайпа, как React. Мне кажется, что в основном это связано с ребрендингом, который сделал Google.. Сначала был AngularJs, который был как дымящаяся куча мусора. Но надо отдать должное Google, они решили полностью отремонтировать AngularJs и превратить его в Angular (или Angular 2), что является гигантским улучшением. Впрочем, это улучшение стоило ему популярности, как мне кажется.

Лично я думаю, что им следовало бы переименовать Angular во что-то другое, потому что AngularJs и Angular совсем не похожи. Единственное сходство - это названия фреймворков и компания, которая их разработала.

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

Вы получаете то, что вам нужно

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

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

Кроме того вы получаете такую штуку, как Angular CLI, который один из самых мощных инструментов на поясе у Angular.

разработчики, использующие Angular CLIразработчики, использующие Angular CLI

Одна из самых больших проблем с React - это отсутствие стандартов. Если вы изучили одно React приложение, то вы изучили одно React приложение, потому что все они совершенно разные. С другой стороны, если вы изучили одно Angular приложение, то вы изучили все Angular приложения и Angular CLI является основным драйвером, стоящим за этим.

В отличие от React в Angular есть правильные и неправильные способы. Использование Angular CLI обычно всегда гарантирует, что всё будете делать правильно. Давайте возьмём самое начало. Мы хотим создать новое приложение. Как мы это сделаем?

ng new my-app

Да, вот, пожалуй и всё. Запустите эту команду и CLI настроит кучу вещей за вас. Он даже даст вам некоторый выбор, такой как использование линтинга и роутинга, перед тем, как будет создано приложение. Итак, вот что сделает CLI:

  1. Он создаст новую директорию my-app и проинициализирует в ней Angular приложение. А также установит зависимости.

  2. Настраивает инфраструктуру и интегрирует в неё приложение, снабдив вас всем, что нужно для запуска.

  3. Вместе с инфраструктурой вы также получаете встроенную базовую реализацию end-to-end тестов на Protractor, в которую вы можете добавлять тесты, по мере развития вашего приложения.

  4. Angular даёт вам простой в использовании конфигурационный файл (angular.json). В нём вы можете легко настроить, как Angular собирает приложение и даже то, как эта сборка делается средой окружения.

  5. Говоря о среде окружения, Angular имеет в своём составе простую и хорошо типизированную систему управления этой самой средой.

Ещё много есть того, чего я возможн коснусь, но и это уже очень круто, да ещё и прямо из коробки. В React вы такого не получите. Что ещё даёт вам CLI?

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

ng generate component my-component OR ng g c my-component

Существует два способа как создать компонент и они довольно долгие, по сравнению с короткой командой, что довольно приятно. Эта команда делает следующее:

  1. Создаёт директорию с именем my-component и помещает в него пустой компонент.

  2. Автоматически генерирует unit tests для данного компонента.

  3. Автоматически встраивает ваш компонент в инфраструктуру приложения.

Путь Angular

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

https://stackblitz.com/edit/angular-examples-modules

ng g m buttonng g c button

В этом примере у нас есть папка button. В этой папке есть модуль button, компонент button, тестовый файл, файл стилей и HTML файл.

Это было автоматически сгенерировано Angular CLI. Если вы заметили, команды имеют определённый порядок и на это есть причина. Во-первых нам нужен модуль, чтобы включить в него наш компонент button. Затем мы создаём компонент button и CLI автоматически импортирует его в модуль. Затем мы экспортируем наш компонент для того, чтобы мы могли использовать его в других модулях нашего приложения.

Чтобы всё было просто, давайте импортируем это в app.module.ts. Всё, что мы сделаем, это импортируем наш компонент ButtonModule в app.module.ts и потом включим его в раздел imports декоратора @NgModule приложения AppModule.

Вот и всё. Теперь мы можем использовать тэг <app-button></app-button> в app.component.html файле.

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

Доставьте меня из пункта А в пункт Б

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

Как хороший CLI, Angular имеет опции. Одна из них задействует роутинг во время создания инфраструктуры приложения.

ng new my-app --routing

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

В созданном конфиге роутинга вы сможете легко настроить ленивую загрузку модуля:

const routes: Routes = [   {   path: 'main',   loadChildren: () =>    import('src/app/routes/main/main.module').then((mod) => mod.MainModule)   },];

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

Переиспользование делает вашу жизнь проще

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

Полагаю, что вы уже говорите - React тоже использует TypeScript, и будете правы. Но React использует Typescript не в полной мере. Типизация - это только часть того, что делает TypeScript таким продвинутым. Другая часть состоит в том, что вы можете в полной мере использовать классы и декораторы. React дал ясно понять, что движется в сторону функциональных компонент, а не в сторону компонент основанных на классах, что отнимает половину всей мощи TypeScript. Теперь стало намного сложнее переиспользовать функциональность в моём приложении и оно стало более сложным в обслуживании.

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

@my-decorator()export class MyClass {}

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

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

Understanding Angular Ivy: Incremental DOM and Virtual DOM

В предыдущей статье я немного рассказывал о том, что виртуальный DOM это своего рода пожиратель памяти и что невозможно оптимизировать расход этой памяти (tree shaking), поскольку виртуальное дерево создаётся всякий раз заново при перерисовке. Всё изменилось с приходом Angular Ivy. Вы сможете прочитать как работает Ivy в статье Виктора. Я лишь приведу некоторые моменты из неё.

Angular Ivy использует так называемый инкрементный DOM. Идея заключается в том, что каждый компонент ссылается на набор инструкций, которые известны на стадии компиляции. И если некоторые инструкции не используются, то они исключаются из сборки.

В отличие от виртуального DOM, в инкрементном DOM память расходуется при перерисовке дерева DOM, если только в нем произошли изменения (то есть были удалены или добавлены элементы). Кроме того, выделение памяти пропорционально размеру изменений. Это здорово повышает эффективность в рантайме, особенно для мобильных устройств.

Помните, как я ранее сказал: Я уже знаю, что вы думаете и мы к этому вернёмся? Давайте разберём это. В том месте статьи я уверен, что вы подумали про себя: это всё прелестно, но что если мне не понадобиться ВСЁ, что Angular предоставляет? Хорошо, ну вы сами вдумайтесь, если вы не используете какую-то часть Angular, она просто не попадёт в сборку! Их технология оптимизации сборки постоянно улучшается и вы получите более стройные билды, в особенности если вы не используете абсолютно всё, что есть в Angular.

Апгрейд - проще простого

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

Я долго работал с Angular и видел все апдейты с первого релиза (я даже работал с AngularJs, но предпочитаю не говорить об этом). Безусловно Angular прошёл долгий путь, как и CLI. Где в 2018 году в Angular CLI появилась ещё одна команда - ng update. Вы можете использовать её так:

ng update @angular/core

Дальше происходит магия. Все зависимости ядра Angular обновятся до последней версии. Если ваш код нуждается в обновлении, CLI сделает это за вас и, если нельзя, то скажет, где вам нужно самим вручную обновить свой код. Обновление до последней версии Angular займёт от нескольких секунд до нескольких минут, в то время, как с React это может занять от нескольких часов до нескольких дней (или даже недель).

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

Давайте свяжем всё вместе

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

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

Хорошая совместимость - уменьшение сложности обслуживания. Когда ваш проект растёт, стоимость обслуживания не будет возрастать экспоненциально. Наверное это самая большая проблема в React приложениях, которую превосходно решает Angular. Следуя тому же ходу мысли, мы также получаем всё, что нам нужно прямо из ядра Angular. Обработка форм? Пожалуйста. Роутинг? Пожалуйста. Ленивая загрузка? Пожалуйста. Я мог бы продолжить, но остановлюсь на этом. Даже если вы что-то не используете, то это не войдет в ваш билд, потому что всё, что в ядре Angualr является оптимизируемым деревом (tree shakable), включая рендеринг.

Чтобы завершить статью, скажу последнее. Используйте Angular. Смотрите видео на YouTube. Читайте документацию или учитесь, как у вас получается лучше всего. А затем смело бросайте React в мусор, потому что он вам больше не понадобится.

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

Подробнее..
Категории: Typescript , React , Reactjs , Angular , Angular2 , Upgrade

Антипаттерн Ёлочка

02.05.2021 10:10:12 | Автор: admin

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

В этой статье я расскажу вам об антипаттерне проектирования Ёлочка или о том, как не превратить свой код в дрова.

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

Рождение проблемы

Представим, что существует несложный компонент, отображающий некое число Counter

const Counter = ({ value }) => {return <div>{ value }</div>;};

Counter не следует воспринимать буквально. Это просто упрощённая модель для контрастного выражения сути проблемы.

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

const Counter = ({ value, digits }) => {return <div>{ `${value} ${digits}` }</div>};

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

const Counter = ({ value, digits, type }) => {const temp = type ? `(${digits})` : digits;return <div>{ `${value} ${temp}` }</div>};

Пожалуй, достаточно. Будем считать, что Counter достаточно функциональный компонент и пригоден к использованию в том виде, в котором сейчас существует. Теперь рассмотрим его с точки зрения алгоритмизации. Представим, что входные параметры этого компонента a, b и c, а y - это некая функция вида f(a, b, c), решение которой будет символизировать обработку всех возможных состояний и отрисовку компонента.

Из реализации Counter видно, что как минимум два аргумента (digits и type) взаимозависимы, т. е. всё множество вариантов их обработки имеет решение a * b, соответствеено. Итоговая формула, в этом случае, будет выглядеть так:

y = a * b + c

Выглядит не так и плохо, ведь digits это однотипная константа, как и value, а это значит, что аргумент a, можно принять за единицу. В итоге мы смогли показать, что общее количество обрабатываемых состояний растёт линейно, т. е. только вместе с type.

y = b + 1

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

При попытке усложнить условие, например, на основании digits выбирать тип скобок, наша функция проявит себя резким скачком своего значения, а это означает, что и содержание компонента Counter будет резко усложняться. Код растёт, как ёлочка...

Где ошибка?

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

const getCoveredDigits = (digits) => `(${digits})`;<Counter  value={value}  digits={getCoveredDigits(digits)}/>

В итоге, записать функцию обработки состояний можно следующим образом:

y = a + b, b = f(x)

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

Выводы

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

Подробнее..

Перевод Десятикратное улучшение производительности React-приложения

14.06.2021 16:14:45 | Автор: admin

Сталкивались ли вы с такой ошибкой? Пытались ее решить? Пробовали найти решение в сети и ничего не находили? Обычно, данная проблема решается простой перезагрузкой страницы.


Около года назад в Techgoise я получил возможность поработать с большим React-приложением. Мы получили (унаследовали) готовую кодовую базу, внесли основные правки и начали добавлять в приложение новые интересные возможности.


Однако, мы часто получали жалобы от тестировщиков и конечных пользователей о том, что они видят эту злополучную ошибку. После проведенного анализа мы установили, что причина происходящего состоит в том, что приложение расходует целых 1,5 Гб памяти!


В данной статье я расскажу о том, как нам удалось добиться уменьшения этой цифры с 1,5 Гб до 150 Мб, что, как следствие, привело к улучшению производительности почти в 10 раз, и мы больше никогда не сталкивались с Ошибкой.


Поиск узких мест в производительности


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


1. Профилирование компонентов с помощью расширения для Google Chrome


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


2. Снимки используемой памяти в Firefox


Данный инструмент предоставляет снимок "кучи", которая используется текущей вкладкой браузера. Древовидное представление снимка показывает следующие вещи:


  1. Объекты: объекты JavaScript и DOM, такие как функции, массивы или, собственно, объекты, а также типы DOM, такие как Window и HTMLDivElement.
  2. Скрипты: источники JavaScript-кода, загружаемые страницей.
  3. Строки.
  4. Другое: внутренние объекты, используемые SpiderMonkey.

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


3. Пакет why-did-you-render


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


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


Как же нам удалось решить эту задачу?


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


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


1. Удаление встроенных функций


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


import Child from 'components/Child'const Parent = () => ( <Child onClick={() => {   console.log('Случился клик!') }} />)export default Parent

В нашем коде имеется встроенная функция. С такими функциями сопряжено 2 главных проблемы:


  1. Они запускают повторный рендеринг компонента даже в случае, когда пропы остались прежними.
  2. Это, в свою очередь, увеличивает расход памяти.

В основном, это связано с тем, что в данном случае метод "передается по ссылке", поэтому на каждом цикле рендеринга создается новая функция и изменяется ссылка на нее. Это присходит даже при использовании PureComponent или React.memo().


Решение: выносим встроенные функции из рендеринга компонента.


import Child from 'components/Child'const Parent = () => { const handleClick = () => {   console.log('Случился клик!') } return (   <Child onClick={handleClick} /> )}

Это позволило снизить расход памяти с 1,5 Гб до 800 Мб.


2. Сохранение состояния при отсутствии изменений хранилища Redux


Как правило, для хранения состояния мы используем хранилище Redux. Предположим, что мы повторно обращаемся к API и получаем те же данные. Должны ли мы в этом случае обновлять хранилище? Короткий ответ нет. Если мы это сделаем, то компоненты, использующие такие данные будут повторно отрисованы, поскольку изменилась ссылка на данные.


В унаследованной кодовой базе для этого использовался такой хак: JSON.stringify(prevProps.data) !== JSON.stringify(this.props.data). Однако, на нескольких страницах он не давал желаемого эффекта.


Решение: перед обновлением состояния в хранилище Redux проводим эффективное сравнение данных. Мы обнаружили два хороших пакета, отлично подходящих для решения этой задачи: deep-equal и fast-deep-equal.


Это привело с уменьшению Цифры с 800 до 500 Мб.


3. Условный рендеринг компонентов


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


import { useState } from 'react'import { Modal, Button } from 'someCSSFramework'const Modal = ({ isOpen, title, body, onClose }) => { const [open, setOpen] = useState(isOpen || false) const handleClick =   typeof onClose === 'function'     ? onClose     : () => { setOpen(false) } return (   <Modal show={open}>     <Button onClick={handleClick}>x<Button>     <Modal.Header>{title}</Modal.Header>     <Modal.Body>{body}</Modal.Body>   </Modal> )}

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


Решение: рендеринг таких компонентов по условию (условный рендеринг). Также можно рассмотреть вариант с "ленивой" (отложенной) загрузкой кода таких компонентов.


Это привело к снижению расхода памяти с 500 до 150 Мб.


Перепишем приведеный выше пример:


import { useState } from 'react'import { Modal, Button } from 'someCSSFramework'const Modal = ({ isOpen, title, body, onClose }) => { const [open, setOpen] = useState(isOpen || false) const handleClick =   typeof onClose === 'function'     ? onClose     : () => { setOpen(false) } // условный рендеринг if (!open) return null return (   <Modal show={open}>     <Button onClick={handleClick}>x<Button>     <Modal.Header>{title}</Modal.Header>     <Modal.Body>{body}</Modal.Body>   </Modal> )}

4. Удаление ненужных await и использование Promise.all()


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


Обычно, для получения начальных данных мы обращаемся к API. Представьте, что для инициализации приложение требуется получить данные от 3-5 API, как в приведенном ниже примере. Методы get... в примере связанны с соответствующими запросами к API:


const userDetails = await getUserDetails()const userSubscription = await getUserSubscription()const userNotifications = await getUserNotifications()

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


В нашем случае это увеличило скорость начальной загрузки приложения на 30%.


const [ userSubscription, userDetails, userNotifications] = await Promise.all([ getUserSubscription(), getUserDetails(), getUserNotifications()])

Рассмотренные в данной статье приемы по оптимизации производительности React-приложения это лишь вершина айсберга. О более тонких приемах мы поговорим в одной из следующих статей.


Заключение


Итак, для повышения производительности React-приложения необходимо придерживаться следующих правил:


  1. Избегайте использования встроенных функций. В небольших приложениях это не имеет особого значения, но по мере роста приложения это негативно отразится на скорости работы приложения.
  2. Помните о том, что иммутабельность (неизменяемость) данных это ключ к предотвращению ненужного рендеринга.
  3. В отношении скрытых компонентов вроде модальных окон и раскрывающихся списков следует применять условный или отложенный рендеринг. Такие компоненты не используются до определенного момента, но их рендеринг влияет на производительность.
  4. По-возможности, отправляйте параллельные запросы к API. Их последовательное выполнение занимает гораздо больше времени.

Спустя 3 недели разработки (включая тестирование), мы, наконец, развернули продакшн-версию приложения. С тех пор мы ни разу не сталкивались в ошибкой "Aw! Snap".


Благодарю за внимание и хорошего дня!




Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru