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

Reactjs

Перевод 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-проектах?


Подробнее..

Перевод Почему стоит использовать тег ltpicturegt вместо ltimggt

05.05.2021 10:13:29 | Автор: admin
image

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

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

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

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

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

Почему тега img недостаточно для современных веб-приложений?


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

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

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

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

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

Смена разрешения при помощи атрибутов srcset и sizes


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

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

Это может привести к более долгой загрузке изображений и частичной загрузке изображений сверху вниз.


Проблема загрузки изображения сверху вниз

Эту проблему можно легко решить тегом picture при помощи атрибутов srcset и sizes.

<picture>   <source      srcset="small-car-image.jpg 400w,              medium-car-image.jpg 800w,              large-car-image.jpg 1200w"      sizes="(min-width: 1280px) 1200px,             (min-width: 768px) 400px,             100vw">   <img src="medium-car-image.jpg" alt="Car"></picture>

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

Атрибут sizes задаёт пространство, которое изображение будет занимать на экране. В показанном выше примере изображение займёт до 1200px, если минимальная ширина экрана равна 1280px.

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

<img srcset="small-car-image.jpg 400w,             medium-car-image.jpg 800w,             large-car-image.jpg 1200w"     sizes="(min-width: 1280px) 1200px,            (min-width: 768px) 400px,            100vw"          src="medium-car-image.jpg" alt="Car">

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

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

Ориентация графики при помощи атрибута media


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

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

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

<picture>      <source ....>   <source ....>   <source ....></picture>

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

В показанном ниже примере демонстрируется полная реализация ориентации графики и смены разрешения при помощи тега picture.

<picture>        <source media="(orientation: landscape)"                   srcset="land-small-car-image.jpg 200w,              land-medium-car-image.jpg 600w,              land-large-car-image.jpg 1000w"                   sizes="(min-width: 700px) 500px,             (min-width: 600px) 400px,             100vw">        <source media="(orientation: portrait)"                   srcset="port-small-car-image.jpg 700w,              port-medium-car-image.jpg 1200w,              port-large-car-image.jpg 1600w"                   sizes="(min-width: 768px) 700px,             (min-width: 1024px) 600px,             500px">        <img src="land-medium-car-image.jpg" alt="Car"></picture>

Если экран находится в альбомной ориентации, то браузер будет отображать изображения из первого набора, а если в портретной, то из второго набора. Кроме того, можно использовать атрибут media с параметрами max-width и min-width:

<picture>     <source media="(max-width: 767px)" ....>     <source media="(min-width: 768px)" ....></picture>

Последний тег img используется для обратной совместимости с браузерами, не поддерживающими теги picture.

Использование с частично поддерживаемыми типами изображений


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

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

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

<picture>  <source srcset="test.avif" type="image/avif">  <source srcset="test.webp" type="image/webp">  <img src="test.png" alt="test image"></picture>

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

Ситуация с тегом picture стала ещё интереснее, когда разработчики Chrome объявили о том, что во вкладке Rendering инструментов DevTools появится две новые эмуляции для эмулирования частично поддерживаемых типов изображений.

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


Использование Chrome DevTools для эмулирования совместимости изображений

В заключение


Хоть мы и говорили о том, насколько лучше тег picture по сравнению с тегом img, я уверен, что img не умер и умрёт ещё не скоро.

Если мы будем с умом использовать имеющиеся у него атрибуты srcset и size, то можем выжать из тега img максимум. Например, можно решить проблему смены разрешения при помощи одного только тега img.

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

Среди прочих достоинств тега picture способность работать с частично поддерживаемыми типами изображений и поддержка Chrome DevTools.

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



На правах рекламы


Эпичные серверы это VDS для размещения сайтов от маленького интернет-магазина на Opencart до серьёзных проектов с огромной аудиторией. Создавайте собственные конфигурации серверов в пару кликов!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Перевод Немного о том, как работает виртуальный DOM в React

22.05.2021 14:05:19 | Автор: admin

image


Настоящий или реальный (real) DOM


DOM расшифровывается как Document Object Model (объектная модель документа). Проще говоря, DOM это представление пользовательского интерфейса (user interface, UI) в приложении. При каждом изменении UI, DOM также обновляется для отображения этих изменений. Частые манипуляции с DOM негативно влияют на производительность.


Что делает манипуляции с DOM медленными?


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


Манипуляции с DOM являются сердцем современного интерактивного веба. К сожалению, они намного медленнее большинства JavaScript-операций. Ситуация усугубляется тем, что многие JavaScript-фреймворки обновляют DOM чаще, чем необходимо.


Допустим, у нас имеется список из 10 элементов. Мы изменяем первый элемент. Большинство фреймворков перестроят весь список. Это в 10 раз больше работы, чем требуется! Только 1 элемент изменился, остальные 9 остались прежними.


Перестроение списка это легкая задача для браузера, но современные веб-сайты могут осуществлять огромное количество манипуляций с DOM. Поэтому неэффективное обновление часто становится серьезной проблемой. Для решения данной проблемы команда React популяризовала нечто под названием виртуальный (virtual) DOM (VDOM).


Виртуальный DOM


В React для каждого объекта настоящего DOM (далее RDOM) существует соответствующий объект VDOM. VDOM это объектное представление RDOM, его легковесная копия. VDOM содержит те же свойства, что и RDOM, но не может напрямую влиять на то, что отображается на экране.


Виртуальный DOM (VDOM) это концепция программирования, где идеальное или виртуальное представление UI хранится в памяти и синхронизируется с реальным DOM, используемая такими библиотеками, как ReactDOM. Данный процесс называется согласованием (reconcilation).

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


Почему VDOM является более быстрым?


Когда в UI добавляются новые элементы, создается VDOM в виде дерева. Каждый элемент является узлом этого дерева. При изменении состояния любого элемента, создается новое дерево. Затем это новое дерево сравнивается (diffed) со старым.


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


На изображениях ниже представлено виртуальное DOM-дерево и процесс согласования.



Красным цветом обозначены узлы, которые были обновлены. Эти узлы представляют элементы UI, состояние которых изменилось. После этого вычисляется разница между предыдущей и текущей версиями виртуального DOM-дерева. Затем все родительское поддерево подвергается повторному рендерингу для представления обновленного UI. Наконец, это обновленное дерево используется для обновления RDOM.



Как React использует VDOM?


После того, как мы рассмотрели, что такое VDOM, настало время поговорить о том, как он используется в React.


1. React использует паттерн проектирования Наблюдатель (observer) и реагирует на изменения состояния


В React каждая часть UI является компонентом и почти каждый компонент имеет состояние (state). При изменении состояния компонента, React обновляет VDOM. После обновления VDOM, React сравнивает его текущую версию с предыдущей. Этот процесс называется поиском различий (diffing).


После обнаружения объектов, изменившихся в VDOM, React обновляет соответствующие объекты в RDOM. Это существенно повышает производительность по сравнению с прямыми манипуляциями DOM. Именно это делает React высокопроизводительной библиотекой JavaScript.


2. React использует механизм пакетного (batch) обновления RDOM


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


Повторная отрисовка UI самая затратная часть, React обеспечивает точечную и групповую перерисовку RDOM.


3. React использует эффективный алгоритм поиска различий


React использует эвристический O(n) (линейный) алгоритм, основываясь на двух предположениях:


  1. Два элемента разных типов приводят к построению разных деревьев


  2. Разработчик может обеспечить стабильность элементов между рендерингами посредством пропа key (ключ)



На практике эти предположения являются верными почти во всех случаях.


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


Элементы разных типов


  • Если корневые элементы имеют разные типы, React уничтожает старое дерево и строит новое с нуля


  • Вместе со старым деревом уничтожаются все старые узлы DOM. Экземпляры компонента получают componentWillUnmount(). При построении нового дерева, новые узлы DOM встраиваются в DOM. Экземпляры компонента получают сначала UNSAFE_componentWillMount(), затем componentDidMount(). Любое состояние, связанное со старым деревом, утрачивается


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



<div><Counter /></div><span><Counter /></span>

Старый Counter будет уничтожен и создан заново.


Элементы одинакового типа


При сравнении двух элементов одинакового типа, React смотрит на атрибуты этих элементов. Узлы DOM сохраняются, изменяются только их атрибуты. Например:


<div className="before" title="stuff" /><div className="after" title="stuff" />

После сравнения этих элементов будет обновлен только атрибут className.


После обработки узла DOM, React рекурсивно перебирает всех его потомков.


Рекурсивный перебор дочерних элементов


По умолчанию React перебирает два списка дочерних элементов DOM-узла и генерирует мутацию при обнаружении различий.


Например, при добавлении элемента в конец списка дочерних элементов, преобразование одного дерева в другое работает хорошо:


<ul><li>первый</li><li>второй</li></ul><ul><li>первый</li><li>второй</li><li>третий</li></ul>

React "видит", что в обоих деревьях имеются <li>первый</li> и <li>второй</li>, пропускает их и вставляет в конец <li>третий</li>.


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


<ul><li>первый</li><li>второй</li></ul><ul><li>нулевой</li><li>первый</li><li>второй</li></ul>

React не сможет понять, что <li>первый</li> и <li>второй</li> остались прежними и мутирует каждый элемент.


Использование ключей


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


<ul><li key="1">первый</li><li key="2">второй</li></ul><ul><li key="0">нулевой</li><li key="1">первый</li><li key="2">второй</li></ul>

Теперь React знает, что элемент с ключом 0 является новым, а элементы с ключами 1 и 2 старыми.


На практике в качестве ключей, как правило, используются идентификаторы:


<li key={item.id}>{item.name}</li>

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


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


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




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


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


Поскольку виртуальный DOM это в большей степени паттерн, нежели конкретная технология, данное понятие может означать разные вещи. В мире React виртуальный DOM, обычно, ассоциируется с React-элементами, которые являются объектами, представляющими пользовательский интерфейс. Тем не менее, React также использует внутренние объекты, которые называются волокнами (fibers). В этих объектах хранится дополнительная информация о дереве компонентов. Fiber это новый движок согласования, появившийся в React 16. Его основная цель заключается в обеспечении инкрементального рендеринга VDOM.


Как выглядит VDOM?


Название виртуальный DOM делает концепцию немного магической (мистической). На самом деле, VDOM это обычный JavaScript-объект.


Представим, что у нас имеется такое DOM-дерево:



Это дерево может быть представлено в виде такого объекта:


const vdom = {tagName: 'html',children: [{ tagName: 'head' },{tagName: 'body',children: [{tagName: 'ul',attributes: { class: 'list' },children: [{tagName: 'li',attributes: { class: 'list_item' },textContent: 'Элемент списка',}, // конец li],}, // конец ul],}, // конец body],} // конец html

Это наш VDOM. Как и RDOM, он является объектным представлением HTML-документа (разметки). Однако, поскольку он представляет собой всего лишь объект, мы можем свободно и часто им манипулировать, не прикасаясь к RDOM без крайней необходимости.


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


const list = {tagName: 'ul',attributes: { class: 'list' },children: [{tagName: 'li',attributes: { class: 'list_item' },textContent: 'Элемент списка',},],}

VDOM под капотом


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


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


Первым делом, нам нужна копия VDOM с изменениями, которые мы хотим осуществить. Поскольку нам не нужно использовать DOM API, мы можем просто создать новый объект.


const copy = {tagName: 'ul',attributes: { class: 'list' },children: [{tagName: 'li',attributes: { class: 'list_item' },textContent: 'Первый элемент списка',},{tagName: 'li',attributes: { class: 'list_item' },textContent: 'Второй элемент списка',},],}

Данная копия используется для создания различия (diff) между оригинальным VDOM (list) и его обновленной версией. Diff может выглядеть следующим образом:


const diffs = [{newNode: {/* новая версия первого элемента списка */},oldNode: {/* оригинальная версия первого элемента списка */},index: {/* индекс элемента в родительском списке */},},{newNode: {/* второй элемент списка */},index: {/* ... */},},]

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


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


const domElement = document.quesrySelector('list')diffs.forEach((diff) => {const newElement = document.createElement(diff.newNode.tagName)/* Добавляем атрибуты ... */if (diff.oldNode) {// Если имеется старая версия, заменяем ее новойdomElement.replaceChild(diff.newNode, diff.oldNode)} else {// Если старая версия отсутствует, создаем новый узелdomElement.append(diff.newNode)}})

Обратите внимание, что это очень упрощенная версия того, как может работать VDOM.


VDOM и фреймворки


Обычно, мы имеем дело с VDOM при использовании фреймворков.


Копцепция VDOM используется такими фреймворками, как React и Vue для повышения производительности обновления DOM. Например, с помощью React наш компонент list может быть реализован следующим образом:


import React from 'react'import ReactDOM from 'react-dom'const list = React.createElement('ul',{ className: 'list' },React.createElement('li', { className: 'list_item' }, 'Элемент списка'))// для создания элементов в React обычно используется специальный синтаксис под названием JSX// const list = <ul className="list"><li className="list_item">Элемент списка</li></ul>ReactDOM.render(list, document.body)

Для обновления списка достаточно создать новый шаблон и снова передать его ReactDOM.render():


const newList = React.createElement('ul',{ className: 'list' },React.createElement('li',{ className: 'list_item' },'Первый элемент списка'),React.createElement('li', { className: 'list_item' }, 'Второй элемент списка'))const timerId = setTimeout(() => {ReactDOM.render(newList, document.body)clearTimeout(timerId)}, 5000)

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



Заключение


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


Подход, используемый Angular, который является фреймворком, благодаря которому одностраничные приложения (single page applications, SPA) обрели столь широкую известность, называется Dirty Model Checking (грязной проверкой моделей). Следует отметить, что DMC и VDOM не исключают друг друга. MVC-фреймворк вполне может использовать оба подхода. В случае с React это не имеет особого смысла, поскольку React это, в конце концов, всего лишь библиотека для слоя представления (view).




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


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


Подробнее..

Только 39 функций в node_modules уникальны в дефолтном Angular проекте

30.04.2021 10:21:30 | Автор: admin

39% это количество уникальных функций в папке node_modules в дефолтном Angular проекте, созданном командой ng new my-app.


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



Как сравнить функции в Javascript?


Если вы захотите сравнить пару функци в javascript, то сделать это проще простого, после пребразования их в строковый вид:


const a = () => 'hi';a.toString(); // "() => 'hi'"

А если названия переменных разные?


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


Как же извлечь функции из Javascript файла?


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


В общем план действий я нарисовал следующий


  1. Перебираем все *.js файлы в директории
  2. Парсим каждый файл и извлекаем функции типов ArrowFunctionExpression, FunctionExpression и FunctionDeclaration
  3. Ужимаем каждую из функций при помощи UglifyJs и записываем её в файл, имя которого это хеш функции
  4. В отдельный файл info.csv записываем строку с id записи, названием файла, из которого была извлечена функция и хешем функции
  5. Загружаем файл info.csv в SQLite и делаем по нему всякие запросы, ведь эта база не игрушка!

Детали реализации


  • Всем стрелочным функциям и фунциональным выражениям я дал название z;
  • Обычные функции я переименовал в MORK, но сохранил отдельно названия функций для учета, потому что функция может быть той же самой, но называться по-другому;
  • Возможно с этими переименованиями я потерял часть статистики, связанной с рекурсивными функциями, ну и ладно!

Пример извлечения функций из файла


Код javascript файла, и которого будем извлекать функции:


(function () {    const arbuz = (test) => {        function apple(t) {            function test () {                return 'ttt';            }            return t + 3;        }        const aa = 1;        const b1 = () => 2;        // comment        return aa + b1() + apple(test);    }    return arbuz; })();

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


"const z=function(){return n=>{return 3+(n+3)}};";"const z=n=>{return 3+(n+3)};";"function MORK(n){return n+3}";"function MORK(){return"ttt"}";"const z=()=>2;";

Полный скрипт можно найти тут


Первый объект исследования: node_modules дефолтного проекта Angular 11


Итак, создаём проект при помощи @angular/cli: ng new my-app, запускаем скрипт на парсинг node_modules и оставляем его на ночь.


Посмотреть package.json
{  "name": "my-app",  "version": "0.0.0",  "scripts": {    "ng": "ng",    "start": "ng serve",    "build": "ng build --prod",    "test": "ng test",    "lint": "ng lint",    "e2e": "ng e2e"  },  "private": true,  "dependencies": {    "@angular/animations": "~11.2.10",    "@angular/common": "~11.2.10",    "@angular/compiler": "~11.2.10",    "@angular/core": "~11.2.10",    "@angular/forms": "~11.2.10",    "@angular/platform-browser": "~11.2.10",    "@angular/platform-browser-dynamic": "~11.2.10",    "@angular/router": "~11.2.10",    "rxjs": "~6.6.0",    "tslib": "^2.0.0",    "zone.js": "~0.11.3"  },  "devDependencies": {    "@angular-devkit/build-angular": "~0.1102.9",    "@angular/cli": "~11.2.9",    "@angular/compiler-cli": "~11.2.10",    "@types/jasmine": "~3.6.0",    "@types/node": "^12.11.1",    "codelyzer": "^6.0.0",    "jasmine-core": "~3.6.0",    "jasmine-spec-reporter": "~5.0.0",    "karma": "~6.1.0",    "karma-chrome-launcher": "~3.1.0",    "karma-coverage": "~2.0.3",    "karma-jasmine": "~4.0.0",    "karma-jasmine-html-reporter": "^1.5.0",    "protractor": "~7.0.0",    "ts-node": "~8.3.0",    "tslint": "~6.1.0",    "typescript": "~4.1.5"  }}

Полный package-lock.json тут


Результаты


В папке node_modules 26982 *.js файлов:


$ find . -name '*.js' | wc -l26982

А в них найдено 338230 функций:


sqlite> select count(*) from info;338230

Из которых 130886 уникальных:


sqlite> Select count(*) from (SELECT hash, count(id) as c FROM info group By hash);130886

То есть 130886/338230 * 100% =39% функций действительно уникальны, а остальные это дубликаты уже существующих.


Скачать csv файл для самостоятельной проверки можно тут.


Топ 20 самых популярных функций в node_modules для проекта Angular


То есть функции с самым большим количеством дубликатов.


SELECT hash, count(id) as c FROM info group By hash order by c desc LIMIT 20;

# id количество дубликатов
1 285d00ca29fcc46aa113c7aefc63827d 2730
2 cf6a0564f1128496d1e4706f302787d6 1871
3 12f746f2689073d5c949998e0216f68a 1174
4 7d1e7aad635be0f7382696c4f846beae 772
5 c2da306af9b041ba213e3b189699d45c 699
6 c41eb44114860f3aa1e9fa79c779e02f 697
7 5911b29c89fa44f28ce030aa5e433327 691
8 05c2b9b254be7e4b8460274c1353b5ad 653
9 fcaede1b9e574664c893e75ee7dc1d8b 652
10 e743dd760a03449be792c00e65154a48 635
11 777c390d3cc4663f8ebe4933e5c33e9d 441
12 27628ad740cff22386b0ff029e844e85 385
13 f6822db5c8812f4b09ab142afe908cda 375
14 d98a03a472615305b012eceb3e9947d5 330
15 4728096fca2b3575800dafbdebf4276a 324
16 7b769d3e4ba438fc53b42ad8bece86ba 289
17 7d6f69751712ef9fa94238b38120adc6 282
18 b7081aad7510b0993fcb57bfb95c5c2c 255
19 d665499155e104f749bf3a67caed576a 250
20 99fa7dfce87269a564fc848a7f7515b9 250

  1. 285d00ca29fcc46aa113c7aefc63827d, 2730 идентичных


    const z=function(){};
    

  2. cf6a0564f1128496d1e4706f302787d6, 1871 идентичных, названия функций одинаковые: __export


    function MORK(r){for(var o in r)exports.hasOwnProperty(o)||(exports[o]=r[o])}
    

  3. 12f746f2689073d5c949998e0216f68a, 1174 идентичных, названия функций: _interopRequireDefault и __importDefault


    function MORK(e){return e&&e.__esModule?e:{default:e}}
    

  4. 7d1e7aad635be0f7382696c4f846beae, 772 идентичных, у всех у них было примерно 300 уникальных названий


    function MORK(){}
    

  5. c2da306af9b041ba213e3b189699d45c, 699 идентичных


    const z=function(o,_){o.__proto__=_};
    

  6. c41eb44114860f3aa1e9fa79c779e02f, 697 идентичных, имя __


    function MORK(){this.constructor=d}
    

  7. 5911b29c89fa44f28ce030aa5e433327, 691 идентичная


    const z=function(n,o){for(var r in o)o.hasOwnProperty(r)&&(n[r]=o[r])};
    

  8. 05c2b9b254be7e4b8460274c1353b5ad, 653 идентичных


    const z=function(t,n){return extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,n){t.__proto__=n}||function(t,n){for(var o in n)n.hasOwnProperty(o)&&(t[o]=n[o])},extendStatics(t,n)};
    

  9. fcaede1b9e574664c893e75ee7dc1d8b, 652 идентичных


    const z=function(t,o){function e(){this.constructor=t}extendStatics(t,o),t.prototype=null===o?Object.create(o):(e.prototype=o.prototype,new e)};
    

  10. e743dd760a03449be792c00e65154a48, 635 идентичных


    function(){var r=function(t,o){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,o){t.__proto__=o}||function(t,o){for(var n in o)o.hasOwnProperty(n)&&(t[n]=o[n])})(t,o)};return function(t,o){function n(){this.constructor=t}r(t,o),t.prototype=null===o?Object.create(o):(n.prototype=o.prototype,new n)}};
    

  11. 777c390d3cc4663f8ebe4933e5c33e9d, 441 идентичная, имена функций различные, чаще всего: Rule, AsapScheduler, ComplexOuterSubscriber и другие


    function MORK(){return null!==_super&&_super.apply(this,arguments)||this}
    

  12. 27628ad740cff22386b0ff029e844e85, 385 идентичных, имена функций чаще разные, чаще всего identity, forwardResolution и тд


    function MORK(n){return n}
    

  13. f6822db5c8812f4b09ab142afe908cda, 375 идентичных


    const z=function(n){};
    

  14. d98a03a472615305b012eceb3e9947d5, 330 идентичных


    const z=function(n,c){};
    

  15. 4728096fca2b3575800dafbdebf4276a, 324 идентичных


    const z=function(n){return n};
    

  16. 7b769d3e4ba438fc53b42ad8bece86ba, 289 идентичных, все имена plural


    function MORK(t){var r=Math.floor(Math.abs(t)),t=t.toString().replace(/^[^.]*\.?/,"").length;return 1===r&&0===t?1:5}
    

  17. 7d6f69751712ef9fa94238b38120adc6, 255 идентичных


    const z=function(){return this};
    

  18. b7081aad7510b0993fcb57bfb95c5c2c, 250 идентичных


    const z=function(){return!1};
    

  19. d665499155e104f749bf3a67caed576a, 250 идентичных


    const z=function(n){return null==n};
    

  20. 99fa7dfce87269a564fc848a7f7515b9, 255 идентичных


    const z=function(a,c){this._array.forEach(a,c)};
    


Файлы с самым большим количеством функций


SELECT count(id) as c, path FROM info group By path order by c desc LIMIT 20;

Количество Файл
13638 typescript/lib/tsserver.js
13617 typescript/lib/tsserverlibrary.js
12411 typescript/lib/typescriptServices.js
12411 typescript/lib/typescript.js
12411 @schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript.js
10346 sass/sass.dart.js
8703 typescript/lib/typingsInstaller.js
8528 typescript/lib/tsc.js
3933 @angular/compiler/bundles/compiler.umd.js
3803 @angular/compiler/bundles/compiler.umd.min.js
2602 selenium-webdriver/lib/test/data/js/tinymce.min.js
2264 @angular/core/bundles/core.umd.js
2028 @angular/core/bundles/core.umd.min.js
1457 terser/dist/bundle.min.js
1416 rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/schematics/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/core/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/build-webpack/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/build-angular/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/architect/node_modules/rxjs/bundles/rxjs.umd.js

Вы спросите А как насчет собранного бандла?


На самом деле, ничего интересного. Сборшики работают эффективно. Из 1282 используемых функций, 95% уникальны. Привожу пятерку функций, которые имеют дубликаты:


Количество Функция
11 const z=function(){};
10 const z=()=>R;
8 const z=function(n){return new(n||t)};
6 const z=function(n){};
5 const z=()=>{};

А что там у нас с React'ом?


Я так же проверил и React. Сравнение я вынес в таблицу ниже:


В node_modules Angular React
всего файлов *.js 26982 23942
всего функций 338230 163385
уникальных функций 130886 92766
% уникальных функций 39% 57%

Скачать csv файл для самостоятельной проверки можно тут.


Топ 20 самых популярных функций в node_modules для проекта React


Я использовал create-react-app. Файлы package.json и yarn.lock можно найти тут.


# id количество дубликатов
1 12f746f2689073d5c949998e0216f68a 1377
2 285d00ca29fcc46aa113c7aefc63827d 1243
3 3f993321f73e83f277c20c178e5587b9 989
4 54782ec6cef850906484808b86946b33 299
5 7d1e7aad635be0f7382696c4f846beae 278
6 d11004e998280b565ad084b0ad5ca214 239
7 a02c66d8928b3353552e4804c6714326 237
8 79e9bd3cdf15cf0af97f73ccaed50fa0 231
9 7d6f69751712ef9fa94238b38120adc6 189
10 b8dd34af96b042c23a4be7f82c881fe4 176
11 863a48e36413feba8bb299623dbc9b20 174
12 2482d2afd404031c67adb9cbc012768b 174
13 4728096fca2b3575800dafbdebf4276a 170
14 bf8b05684375b26205e50fa27317057e 157
15 fd114ee6b71ee06738b5b547b00e8102 156
16 df1c43e5a72e92d11bdefcead13a5e14 156
17 094afc30995ff28993ec5326e8b3c4d4 156
18 042490db7093660e74a762447f64f950 156
19 5c5979ec3533f13b22153de05ffc64d5 154
20 50645492c50621c0847c4ebd1fdd65cd 154

  1. 12f746f2689073d5c949998e0216f68a, 1377 идентичных, названия функций обычно: _interopRequireDefault


    function MORK(e){return e&&e.__esModule?e:{default:e}}
    

  2. 285d00ca29fcc46aa113c7aefc63827d, 1243 идентичных


    const z=function(){};
    

  3. 3f993321f73e83f277c20c178e5587b9, 989 идентичных


    const z=function(){return data};
    

  4. 54782ec6cef850906484808b86946b33, 299 идентичных


    const z=()=>{};
    

  5. 7d1e7aad635be0f7382696c4f846beae, 278 идентичных, имена функций чаще всего emptyFunction, Generator


    function MORK(){}
    

  6. d11004e998280b565ad084b0ad5ca214, 239 идентичных


    const z=function(){return cache};
    

  7. a02c66d8928b3353552e4804c6714326, 237 идентичных, имя функции _getRequireWildcardCache


    function MORK(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return _getRequireWildcardCache=function(){return e},e}
    

  8. 79e9bd3cdf15cf0af97f73ccaed50fa0, 231 идентичных, имя функции _interopRequireWildcard


    function MORK(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=_getRequireWildcardCache();if(t&&t.has(e))return t.get(e);var r,n,o={},c=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(r in e)Object.prototype.hasOwnProperty.call(e,r)&&((n=c?Object.getOwnPropertyDescriptor(e,r):null)&&(n.get||n.set)?Object.defineProperty(o,r,n):o[r]=e[r]);return o.default=e,t&&t.set(e,o),o}
    

  9. 7d6f69751712ef9fa94238b38120adc6, 189 идентичных


    const z=function(){return this};
    

  10. b8dd34af96b042c23a4be7f82c881fe4, 176 идентичных


    const z=function(n,o,c,i){n[i=void 0===i?c:i]=o[c]};
    

  11. 863a48e36413feba8bb299623dbc9b20, 174 идентичных


    const z=function(e,n,t,o){void 0===o&&(o=t),Object.defineProperty(e,o,{enumerable:!0,get:function(){return n[t]}})};
    

  12. 2482d2afd404031c67adb9cbc012768b, 174 идентичных


    const z=function(){return m[k]};
    

  13. 4728096fca2b3575800dafbdebf4276a, 170 идентичных


    const z=function(n){return n};
    

  14. bf8b05684375b26205e50fa27317057e, 157 идентичных


    const z=s=>exposed.has(s);
    

  15. fd114ee6b71ee06738b5b547b00e8102, 156 идентичных


    const z=(r,e,p)=>{var t=makeWrapper(r);return exports.setup(t,r,e,p)};
    

  16. df1c43e5a72e92d11bdefcead13a5e14, 156 идентичных


    const z=t=>utils.isObject(t)&&t instanceof Impl.implementation;
    

  17. 094afc30995ff28993ec5326e8b3c4d4, 156 идентичных


    const z=i=>utils.isObject(i)&&utils.hasOwn(i,implSymbol)&&i[implSymbol]instanceof Impl.implementation;
    

  18. 042490db7093660e74a762447f64f950, 156 идентичных


    const z=(r,e,t)=>{t=exports.create(r,e,t);return utils.implForWrapper(t)};
    

  19. 5c5979ec3533f13b22153de05ffc64d5, 154 идентичных


    const z=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&__createBinding(t,e,r);return __setModuleDefault(t,e),t};
    

  20. 50645492c50621c0847c4ebd1fdd65cd, 154 идентичных


    const z=function(e,n){Object.defineProperty(e,"default",{enumerable:!0,value:n})};
    


В каких файлах используется функция 8 (79e9bd3cdf15cf0af97f73ccaed50fa0)


Посмотреть список файлов

Список длинный


/jest-worker/build/base/BaseWorkerPool.js/@svgr/hast-util-to-babel-ast/lib/index.js/@svgr/hast-util-to-babel-ast/lib/handlers.js/@svgr/hast-util-to-babel-ast/lib/stringToObjectStyle.js/@svgr/hast-util-to-babel-ast/lib/getAttributes.js/babel-jest/node_modules/@babel/core/lib/transform-file.js/babel-jest/node_modules/@babel/core/lib/config/files/configuration.js/babel-jest/node_modules/@babel/core/lib/config/files/utils.js/babel-jest/node_modules/@babel/core/lib/config/full.js/babel-jest/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-jest/node_modules/@babel/core/lib/transformation/file/file.js/babel-jest/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-jest/node_modules/@babel/core/lib/index.js/jest-pnp-resolver/node_modules/jest-resolve/build/defaultResolver.js/jest-pnp-resolver/node_modules/jest-resolve/build/ModuleNotFoundError.js/jest-circus/build/utils.js/jest-haste-map/build/ModuleMap.js/jest-haste-map/build/lib/normalizePathSep.js/jest-haste-map/build/lib/fast_path.js/jest-haste-map/build/lib/WatchmanWatcher.js/jest-haste-map/build/worker.js/jest-haste-map/build/getMockName.js/jest-haste-map/build/HasteFS.js/jest-haste-map/build/crawlers/watchman.js/jest-jasmine2/build/index.js/eslint/node_modules/@babel/code-frame/lib/index.js/mini-css-extract-plugin/dist/index.js/react-scripts/node_modules/@babel/core/lib/transform-file.js/react-scripts/node_modules/@babel/core/lib/config/files/configuration.js/react-scripts/node_modules/@babel/core/lib/config/files/utils.js/react-scripts/node_modules/@babel/core/lib/config/full.js/react-scripts/node_modules/@babel/core/lib/transformation/normalize-file.js/react-scripts/node_modules/@babel/core/lib/transformation/file/file.js/react-scripts/node_modules/@babel/core/lib/tools/build-external-helpers.js/react-scripts/node_modules/@babel/core/lib/index.js/react-scripts/node_modules/jest-resolve/build/defaultResolver.js/react-scripts/node_modules/jest-resolve/build/ModuleNotFoundError.js/eslint-plugin-flowtype/dist/utilities/index.js/jest-util/build/index.js/jest-util/build/createDirectory.js/jest-util/build/installCommonGlobals.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/lib/targets-parser.js/babel-preset-react-app/node_modules/@babel/preset-env/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/lib/utils.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/core/lib/index.js/@babel/code-frame/lib/index.js/@babel/traverse/lib/path/inference/inferer-reference.js/@babel/traverse/lib/path/inference/inferers.js/@babel/traverse/lib/path/inference/index.js/@babel/traverse/lib/path/comments.js/@babel/traverse/lib/path/replacement.js/@babel/traverse/lib/path/ancestry.js/@babel/traverse/lib/path/conversion.js/@babel/traverse/lib/path/index.js/@babel/traverse/lib/path/introspection.js/@babel/traverse/lib/path/removal.js/@babel/traverse/lib/path/lib/hoister.js/@babel/traverse/lib/path/lib/virtual-types.js/@babel/traverse/lib/path/modification.js/@babel/traverse/lib/path/family.js/@babel/traverse/lib/path/generated/asserts.js/@babel/traverse/lib/path/generated/validators.js/@babel/traverse/lib/path/generated/virtual-types.js/@babel/traverse/lib/index.js/@babel/traverse/lib/visitors.js/@babel/traverse/lib/context.js/@babel/traverse/lib/scope/index.js/@babel/traverse/lib/scope/lib/renamer.js/@babel/traverse/lib/types.js/@babel/helper-hoist-variables/lib/index.js/@babel/helper-wrap-function/lib/index.js/@babel/helper-builder-binary-assignment-operator-visitor/lib/index.js/@babel/helper-explode-assignable-expression/lib/index.js/@babel/helper-replace-supers/lib/index.js/@babel/helper-module-imports/lib/import-builder.js/@babel/helper-module-imports/lib/import-injector.js/@babel/helper-skip-transparent-expression-wrappers/lib/index.js/@babel/helper-compilation-targets/lib/index.js/@babel/types/lib/definitions/jsx.js/@babel/types/lib/definitions/misc.js/@babel/types/lib/definitions/typescript.js/@babel/types/lib/definitions/flow.js/@babel/types/lib/definitions/experimental.js/@babel/types/lib/definitions/core.js/@babel/types/lib/index.js/@babel/helpers/lib/index.js/@babel/helper-remap-async-to-generator/lib/index.js/@babel/helper-split-export-declaration/lib/index.js/@babel/helper-simple-access/lib/index.js/@babel/helper-module-transforms/lib/rewrite-this.js/@babel/helper-module-transforms/lib/rewrite-live-references.js/@babel/helper-module-transforms/lib/index.js/@babel/preset-env/lib/targets-parser.js/@babel/preset-env/lib/index.js/@babel/preset-env/lib/utils.js/@babel/highlight/lib/index.js/@babel/generator/lib/generators/jsx.js/@babel/generator/lib/generators/base.js/@babel/generator/lib/generators/template-literals.js/@babel/generator/lib/generators/typescript.js/@babel/generator/lib/generators/classes.js/@babel/generator/lib/generators/expressions.js/@babel/generator/lib/generators/statements.js/@babel/generator/lib/generators/flow.js/@babel/generator/lib/generators/modules.js/@babel/generator/lib/generators/types.js/@babel/generator/lib/generators/methods.js/@babel/generator/lib/node/parentheses.js/@babel/generator/lib/node/index.js/@babel/generator/lib/node/whitespace.js/@babel/generator/lib/printer.js/@babel/helper-get-function-arity/lib/index.js/@babel/helper-function-name/lib/index.js/@babel/helper-annotate-as-pure/lib/index.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/transform-file.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/config/files/configuration.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/config/files/utils.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/config/full.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/transformation/normalize-file.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/transformation/file/file.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/tools/build-external-helpers.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/index.js/@babel/helper-create-class-features-plugin/lib/fields.js/@babel/plugin-transform-classes/lib/transformClass.js/@babel/template/lib/parse.js/@babel/template/lib/formatters.js/@babel/template/lib/populate.js/@babel/template/lib/index.js/@babel/core/lib/transform-file.js/@babel/core/lib/config/files/configuration.js/@babel/core/lib/config/files/utils.js/@babel/core/lib/config/full.js/@babel/core/lib/transformation/normalize-file.js/@babel/core/lib/transformation/file/file.js/@babel/core/lib/tools/build-external-helpers.js/@babel/core/lib/index.js/@babel/helper-optimise-call-expression/lib/index.js/jest-snapshot/build/SnapshotResolver.js/jest-snapshot/build/State.js/jest-snapshot/build/index.js/jest-serializer/build/index.js/react-dev-utils/node_modules/@babel/code-frame/lib/index.js/jest-resolve/build/defaultResolver.js/jest-resolve/build/ModuleNotFoundError.js/pretty-format/build/plugins/ReactElement.js/jest-each/build/table/array.js/@jest/transform/build/shouldInstrument.js/@jest/transform/build/index.js/@jest/reporters/build/NotifyReporter.js/@jest/reporters/build/CoverageWorker.js/@jest/reporters/build/utils.js/@jest/reporters/build/generateEmptyCoverage.js/@jest/core/build/collectHandles.js/@jest/core/build/watch.js/@testing-library/react/dist/@testing-library/react.umd.js/@testing-library/react/dist/@testing-library/react.pure.umd.js/@testing-library/dom/dist/@testing-library/dom.umd.js/jest-config/build/getCacheDirectory.js/jest-config/build/resolveConfigPath.js/jest-config/build/constants.js

А какие выводы?


Возможно это небольшое исследование кого-нибудь натолкнет на мысли какие-нибудь мысли о рефакторинге. Или изменении подхода к программированию.


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


Плохо что очень много копипасты.


P.S.


Подробнее..

Перевод Почему мы перешли с Webpack на Vite

06.05.2021 20:17:21 | Автор: admin
image


Миссия Replit сделать программирование более доступным. Мы предоставляем людям бесплатные вычисления в облаке, чтобы они могли создавать приложения на любом устройстве. Одним из самых популярных способов создания приложений в Интернете на сегодняшний день является React. Однако исторически инструменты React были медленными на Replit. В то время как экосистема JavaScript создала отличные инструменты для профессиональных разработчиков, многие из самых популярных из них, такие как Create React App и Webpack, становятся все более сложными и неэффективными.

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

Этот новый опыт основан на Vite, инструменте сборки JavaScript, который обеспечивает быструю и экономичную разработку. Vite поставляется с рядом функций, включая HMR или Hot Module Replacement, команду сборки, которая объединяет ваши инструменты с Rollup, и встроенную поддержку TypeScript и JSX.

Vite ускоряет разработку с React. Очень сильно ускоряет. С HMR изменения, которые вы вносите, визуализируются в течении миллисекунд, что значительно ускоряет создание прототипов пользовательского интерфейса. Имея это в виду, мы решили переписать наш шаблон React, используя Vite, и были шокированы, увидев, насколько он стал быстрее. Вот как он выглядит по сравнению с нашим старым шаблоном CRA:



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

image

Как это работает


Vite работает, по-разному обрабатывая ваш исходный код и ваши зависимости. В отличие от вашего исходного кода, зависимости не так часто меняются во время разработки. Vite использует этот факт, предварительно связывая ваши зависимости с помощью esbuild. Esbuild это сборщик JS, написанный на Go, который связывает зависимости в 10-100 раз быстрее, чем альтернативы на основе JavaScript, такие как Webpack и Parcel.

Затем он обслуживает ваш исходный код через собственные модули ES или ESM, что позволяет браузеру обрабатывать фактический бандл.

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

Начнем


Для начала просто сделайте форк нашего шаблона React или выберите React.js в раскрывающемся списке при создании нового репла.

Vite также не зависит от фреймворка, поэтому, если React вам не нравится, вы также можете использовать наши шаблоны Vue и Vanilla JS.

Мы надеемся, что это поможет воплотить ваши идеи в жизнь еще быстрее, и с нетерпением ждем того, что вы создадите!
Подробнее..

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

Подробнее..

Перевод 5 приемов по разделению бандла и ленивой загрузке компонентов в React

25.05.2021 16:13:10 | Автор: admin

image


Разделение Javascript-кода на несколько файлов называется разделением бандла или сборки (bundle splitting). Это позволяет загружать только тот код, который который используется приложением в данный момент, другие части загружаются по необходимости (по запросу пользователя).


Распространенные случаи разделения сборки и ленивой или отложенной загрузки (lazy loading) включают в себя следующее:


  • Загрузка дополнительного кода при переходе пользователя к новому представлению (view слой, отвечающий за визуальное отображение)


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


  • Также можно реализовать предварительную загрузку определенных ресурсов, которые представляют потенциальный интерес для пользователя


  • Это приводит к тому, что когда пользователь захочет получить доступ к определенной функциональности, она уже будет готова



    1. Динамический импорт с помощью Webpack



Webpack позволяет загружать модули (компоненты) динамически во время выполнения кода. Рассмотрим пример:


import { useState } from 'react'function MainComponent() {const [isModalDisplayed, setModalDisplayed] = useState(false)const [ModalComponent, setModalComponent] = useState(null)const loadModalComponent = async () => {const loadResult = await import('./components/Modal.js')setModalComponent(() => loadResult.default)}return (<div>{isModalDisplayed && ModalComponent ? <ModalComponent /> : null}<buttononClick={() => {setModalDisplayed(true)loadModalComponent()}}>Load Modal Component</button></div>)}

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


Динамический импорт позволяет каждому компоненту выступать в роли микрофронтенда (microfrontend).


2. Split API для загрузки React-компонентов


Пакет fusion-react предоставляет интерфейс split, компонент-обертку для отображения различных компонентов во время загрузки сборки:


  • Резервного компонента при возникновении ошибки


  • Настоящего компонента после загрузки сборки



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


import { Link, Switch, Route } from 'react-router-dom'import { split } from 'fusion-react'const Loading = () => <div>Loading...</div>const Error = () => <div>Error</div>const Hello = split({load: () => import('./components/hello.js'),Loading,Error,})const Root = () => (<><div><ul><li><Link to='/'>Home</Link></li><li><Link to='/hello'>Hello</Link></li></ul></div><Switch><Route path='/' exact component={Home} /><Route path='/hello' component={Hello} /></Switch></>)

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


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


Интерфейс split в приведенном примере откладывает загрузку компонента Hello до того момента, когда пользователь перейдет по соответствующему маршруту. Загружаемый компонент указывается в свойстве load.


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


Прим. пер.: существуют более специализированные и популярные решения для ленивой загрузки React-компонентов, например, react-loadable или react-lazyload.


3. Создание вендорного бандла (vendor bundle)


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


Вот как можно извлечь вендорный бандл из директории node_modules:


const path = require('path')module.exports = {entry: path.resolve(__dirname, 'src/index.js'),output: {path: path.resolve(__dirname, 'dist'),filename: '[name].[contenthash].js',},}

Если после этого вы запустите сборку (yarn build или npm run build), то увидите что-то вроде этого:


 webpack: Build Finished webpack: assets by status 128 KiB [emitted]asset 935.js 124 KiB [emitted] [minimized] (id hint: vendors) 2 related assetsasset main.js 3.24 KiB [emitted] [minimized] (name: main) 1 related assetasset index.html 267 bytes [emitted]assets by status 7.9 KiB [compared for emit]asset main.css 7.72 KiB [compared for emit] (name: main) 1 related assetasset 34.js 187 bytes [compared for emit] [minimized] 1 related assetEntrypoint main 135 KiB (326 KiB) = 935.js 124 KiB main.css 7.72 KiB main.js 3.34 KiB 3 auxiliary assets...webpack 5.5.0 compiled successfully in 4856 ms

4. Создание нескольких вендорных бандлов


Обычно, все модули объединяются в один вендорный бандл.


Знаете ли вы, что мы можем создать несколько таких бандлов?


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


Файл с настройками Webpack принимает свойство optimization, позволяющее разделять вендорный бандл:


module.exports = {splitChunks: {chunks: 'async',cacheGroups: {default: {minChunks: 2,reuseExistingChunk: true,},vendor_react: {test: /.*\/node_modules\/react\/index\.js/,name: 'vendor-react',chunks: 'initial',enforce: true,},},},}

После этого вендорный бандл будет разделен на client-vendor.js и clietn-vendor-react.js.


5. Ленивая загрузка компонентов с помощью React.lazy()


React.lazy() это функция, позволяющая рендерить динамически импортируемые компоненты как обычные компоненты.


Обычный импорт:


import MyComponent from './MyComponent'

Динамический импорт с помощью React.lazy():


const OtherComponent = React.lazy(() => import('./OtherComponent')

Компоненты, загружаемые с помощью React.lazy(), должны быть обернуты в компонент Suspense, который позволяет отображать резервный контент (например, индикатор загрузки) до полной загрузки импортируемого компонента:


import { lazy, Suspense } from 'react'const OtherComponent = lazy(() => import('./OtherComponent'))function MyComponent() {return (<><Suspense fallback={<div>Loading...</div>}><OtherComponent /></Suspense></>)}

Проп fallback принимает любой элемент (компонент). Компонент Suspense может быть помещен на любом родительском по отношению к ленивому компоненту уровне.


Suspense может оборачивать как отдельный компонент, так и группу компонентов:


import { lazy, Suspense } from 'react'const OtherComponent = lazy(() => import('./OtherComponent'))const AnotherComponent = lazy(() => import('./AnotherComponent'))function MyComponent() {return (<><Suspense fallback={<div>Loading...</div>}><section><OtherComponent /><AnotherComponent /></section></Suspense></>)}

Заключение


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


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


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




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


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


Подробнее..

Перевод 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-router Три метода рендеринга маршрутов (компонентный, рендеринговый и дочерний)

08.06.2021 18:13:14 | Автор: admin

Введение

В прошлом посте я рассказывал об учебнике по настройке react-router. Если вы не читали предыдущий пост, нажмите здесь! Я хочу добавить несколько важных тем о методах рендеринга маршрутов.

Методы рендеринга маршрута

Существует несколько способов рендеринга HTML компонента или тега с помощью <Route>. Я использовал этот способ в своем последнем посте.

<Route path="/">  <Home /></Route>

В этом сниппете нет ничего плохого, и компонент <Home/> будет рендирован. Однако у вас не будет доступа к трем пропсам маршрута match, location и history. Я расскажу об этих трех реквизитах в следующем посте. Оставайтесь с нами! Итак, давайте рассмотрим, как мы можем получить доступ к этим реквизитам, если мы используем эти три метода рендеринга маршрута.

1. Компонентный метод <Route component/>

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

<Route path="/" component={Home} />
const Home = (props) => {  console.log(props);  return <div>Home</div>;};

Вот и все. Вы получите эти пропсы.

Подождите. Как мы можем передать компоненту еще один проп? Ответ заключается в том, что мы не можем использовать этот метод рендеринга. Однако мы можем использовать render и children

2. Рендеринговый метод <Route render/>

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

<Route path="/contact" render={(routeProps) => {  return <Contact name={name} address={address} {...routeProps} />; }}/>

С помощью <Route render/> можно также рендировать HTML тег, и для этого не требуется рендировать такой компонент, как <Route component/>.

<Route path="/contact" render={() => {  return (   <div>    <h2>Contact</h2>    <p>Name: {name}</p>    <p>Adress: {address}</p>   </div>  ); }}/>

Я лично предпочитаю использовать этот метод рендеринга.

3. Дочерний метод <Route children />

По сути, дочерний и рендеринговый методы одинаковы. Оба они получают функцию, но если вы используете дочерний метод, она будет рендирована, когда путь не совпадает. Также следует убедиться, что вы не используете <switch>.

<Route path="/" exact component={Home} /><Route path="/about" render={() => <About></About>} /><Route path="/portfolio" children={() => <Portfolio></Portfolio>} /><Route path="/contact" children={() => <Contact></Contact>} />

В этом случае, когда пользователи сталкиваются с /, компоненты <Portfolio/> и <Contact/> будут рендированы, поскольку они используют метод рендеринга дочерних элементов. Честно говоря, я не знаю, когда следует использовать этот метод в реальном проекте, но вы можете посмотреть документацию здесь.

Заключение

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

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


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

Подробнее..

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

Пишем переиспользуемые компоненты, соблюдая SOLID

01.06.2021 12:20:22 | Автор: admin
Всем привет! Меня зовут Рома, я фронтендер в Я.Учебнике. Сегодня расскажу, как избежать дублирования кода и писать качественные переиспользуемые компоненты. Статья написана по мотивам (но только по мотивам!) доклада с Я.Субботника видео есть в конце поста. Если вам интересно разобраться в этой теме, добро пожаловать под кат.

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



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

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

Почему сложно перестать дублировать код?


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

Разберём пример с библиотекой компонентов Я.Учебника. Когда-то давно проект был монолитом. Позже для удобства разработчики решили вынести переиспользуемые компоненты в отдельную библиотеку. Одним из первых туда попал компонент кнопки. Компонент развивался, со временем появились новые умения и настройки для кнопки, увеличилось количество визуальных кастомизаций. Спустя некоторое время компонент стал настолько навороченным, что использовать его для новых задач и расширять дальше стало неудобно.

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



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

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

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

Почти весь набор негативных последствий дублирования кнопки повторился в компонентах иконки. Было немного легче потому, что иконки используются в проекте реже. Хотя, если быть честным, тут всё зависит от фичи. Причём и для кнопки, и для иконки старый компонент не удалялся при создании нового, потому что удаление требовало большого рефакторинга с возможным появлением багов по всему проекту. Что же объединяет случаи с кнопкой и иконкой? Одинаковая схема появления дубликатов в проекте. Нам было сложно переиспользовать текущий компонент, адаптировать его к новым условиям, поэтому мы создавали новый.

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

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

SOLID на пути к переиспользуемым компонентам


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

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

  • S принцип единственной ответственности.
  • O принцип открытости/закрытости.
  • L принцип подстановки Лисков.
  • I принцип разделения интерфейсов.
  • D принцип инверсии зависимостей.

Какие-то из этих принципов хорошо подходят для описания компонентов. Другие же выглядят более притянутыми за уши в контексте фронтенда. Но все вместе они хорошо описывают моё видение качественного компонента.

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

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

Базовое


Принцип open/close


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

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


Кнопка написана так, что её нельзя изменить без редактирования кода

Чтобы применить другие стили в текущей версии, придётся отредактировать компонент кнопки. Проблема заключается в том, что в компонент не заложена кастомизируемость. Вариант написать глобальные стили рассматривать не будем, так как он ненадёжен. Всё может сломаться при любой правке. Последствия легко представить, если на место кнопки поставить что-то более сложное, например, компонент выбора даты.

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

// утилита для формирования класса, можно использовать любой аналогimport cx from 'classnames';// добавили новый проп  mixconst Button = ({ children, mix }) => {  return (    <button      className={cx("my-button", mix)}    >      {children}    </button>}

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



Этот довольно популярный способ позволяет кастомизировать внешний вид компонента. Его называют миксом, потому что дополнительный класс подмешивается к собственным классам компонента. Отмечу, что проброс класса не единственная возможность стилизовать компонент извне. Можно передавать в компонент объект с CSS-свойствами. Можно использовать CSS-in-JS решения, суть не изменится. Миксы используют многие библиотеки компонентов, например: MaterialUI, Vuetify, PrimeNG и другие.

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

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

Изменчивость компонентов


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

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

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

Темизация


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

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

import cx from 'classnames';import b from 'b_';const Button = ({ children, mix, theme }) => (  <button    className={cx(     b("my-button", { theme }), mix)}  >    {children}  </button>)

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

Но есть и минус способ подходит только для кастомизации визуала и не позволяет влиять на поведение компонента.

Вложенность компонентов


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

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

Продвинутое


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

Single Responsibility Principle


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

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

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


Один модуль редактируется разными людьми по разным причинам

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

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


Желаемая картина. Тема отдельная сущность и может примениться к кнопке

Тема оборачивает кнопку. Такой подход используется в Лего, нашей внутренней библиотеке компонентов. Мы используем HOC (High Order Components), чтобы обернуть базовый компонент и добавить ему новые возможности. Например, возможность отображаться с темой.

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

Пример простого HOC для темизации кнопки:

const withTheme1 = (Button) =>(props) => {    return (        <Button            {...props}            styles={theme1Styles}        />    )}const Theme1Button = withTheme1(Button);

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

Использование нескольких HOCов для сбора кнопки с нужными темами:

import "./styles.css"; // Базовый компонент кнопки. Принимает содержимое кнопки и стилиconst ButtonBase = ({ style, children }) => { console.log("styl123e", style); return <button style={style}>{children}</button>;}; const withTheme1 = (Button) => (props) => { // HOC применяет стили, только если выбрана тема "theme1" if (props.theme === "theme1") {   return <Button {...props} style={{ color: "red" }} />; }  return <Button {...props} />;}; const withTheme2 = (Button) => (props) => { // HOC применяет стили, только если выбрана тема "theme2" if (props.theme === "theme2") {   return <Button {...props} style={{ color: "green" }} />; }  return <Button {...props} />;}; // ф-я для оборачивания компонента в несколько HOCconst compose = (...hocs) => (BaseComponent) => hocs.reduce((Component, nextHOC) => nextHOC(Component), BaseComponent); // собираем кнопку, передав нужный набор темconst Button = compose(withTheme1, withTheme2)(ButtonBase); export default function App() { return (   <div className="App">     <Button theme="theme1">"Red"</Button>     <Button theme="theme2">"Green"</Button>   </div> );}

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

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

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

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

Композитные компоненты


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



  • Container связь между остальными компонентами.
  • Field текст для обычного селекта и инпут для компонента CobmoBox, где пользователь что-то вводит.
  • Icon традиционный для селекта значок в поле.
  • Menu компонент, который отображает список элементов для выбора.
  • Item отдельный элемент в меню.

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

Overrides


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

<Select  ...  overrides={{    Field: {      props: {theme: 'dark'}    },    Icon: {      props: {size: 'big'},    },    Menu: {      style: {backgroundColor: '#CCCCCC'}    },  }}/>

Я привёл простой пример, где мы переопределяем пропы. Но override можно рассматривать как глобальный конфиг он настраивает всё, что поддерживает компоненты. Увидеть, как это работает на практике, можно в библиотеке BaseWeb.

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

Dependency Inversion Principle


Чтобы разобраться в альтернативе override-конфигам, обратимся к букве D в SOLID. Это принцип инверсии зависимостей. Он утверждает, что код, реализующий верхнеуровневую политику, не должен зависеть от кода, реализующего низкоуровневые детали.

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

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

import InputField from './InputField';import Icon from './Icon';import Menu from './Menu';import Option from './Option';

Разберём зависимости между компонентами, чтобы понять, что может пойти не так. Сейчас более высокоуровневый Select зависит от низкоуровневого Menu, потому что импортит его в себя. Принцип инверсии зависимостей нарушен. Это создаёт проблемы.
  • Во-первых, при изменении Menu придётся править Select.
  • Во-вторых, если мы захотим использовать другой компонент меню, нам тоже придётся вносить правки в компонент селекта.


Непонятно, что делать, когда понадобится Select с другим меню

Нужно развернуть зависимость. Сделать так, чтобы компонент меню зависел от селекта. Инверсия зависимостей делается через инъекцию зависимостей Select должен принимать компонент меню как один из параметров, пропов. Здесь нам поможет типизация. Мы укажем, какой компонент ожидает Select.

// теперь вместо прямого импорта Select принимает одним из параметров компонент менюconst Select = ({  Menu: React.ComponentType<IMenu>}) => {  return (    ...    <Menu>      ...    </Menu>    ...  )...}

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


Стрелка развёрнута, так работает инверсия зависимостей

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

Каждый раз прокидывать все зависимости в компонент в месте рендера утомительно, но в библиотеке bem-react есть реестр зависимостей и процесс композиции. С их помощью можно упаковать зависимости и настройки один раз, а дальше просто использовать готовый компонент.

import { compose } from '@bem-react/core'import { withRegistry, Registry } from '@bem-react/di'const selectRegistry = new Registry({ id: cnSelect() })...selectRegistry.fill({    'Trigger': Button,    'Popup': Popup,    'Menu': Menu,    'Icon': Icon,})const Select = compose(    ...    withRegistry(selectRegistry),)(SelectDesktop)

В примере выше показана часть сборки компонента на примере bem-react. Полный код примера и песочницу можно посмотреть в сторибуке yandex UI.

Что мы получаем от использования Dependency Inversion?

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

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

Сложности разработки переиспользуемых компонентов


В чём же сложность разработки переиспользуемых компонентов?

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

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

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

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

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

Liskov Substitution Principle


Предыдущие принципы были о том, что нужно делать, а последние два будут о том, что нужно не сломать.

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

Мы обычно не пишем компоненты как классы и не используем наследование. Все компоненты взаимозаменяемы из коробки. Это основа современного фронтенда. Не совершать ошибок и поддерживать совместимость помогает строгая типизация.

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

  • читать и писать что-то в глобальный стор,
  • взаимодействовать с window,
  • взаимодействовать с cookie,
  • читать/писать local storage,
  • делать запросы в сеть.



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

Чтобы соблюдать принцип подстановки Лисков нужно:

  • использовать возможности типизации,
  • избегать взаимодействия в обход API компонента,
  • избегать побочных эффектов.

Как избежать взаимодействия не через API? Вынести всё, от чего зависит компонент, в API и написать обёртку, которая будет пробрасывать данные из внешнего мира в пропы. Например, так:
const Component = () => {   /*      К сожалению, использование хуков приводит к тому, что компонент много знает о своем окружении.      Например, тут он знает о наличии стора и его внутренней структуре.      При переносе в другой проект, где стор отсутствует, код может сломаться.   */   const {userName} = useStore();    // Тут компонент знает о куках, что не очень хорошо для переиспользования (может сломаться, если в другом проекте это не так).   const userToken = getFromCookie();    // Аналогично  доступ к window может стать проблемой при переиспользовании компонента.   const {taskList} = window.ssrData;    const handleTaskUpdate = () => {       // Компонент знает об API сервера. Это допустимо только для верхнеуровневых компонентов.       fetch(...)   }    return <div>{'...'}</div>;  }; /*   Здесь компонент принимает только необходимый ему набор данных.   Его можно легко переиспользовать, потому что только верхнеуровневые компоненты будут знать все детали.*/const Component2 = ({   userName, userToken, onTaskUpdate}) => {   return <div>{'...'}</div>;};

Interface Segregation Principle


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

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

Где и как переиспользуем?


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

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

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

Переиспользование в том же проекте. Вы написали компонент и почти уверены, что захотите его переиспользовать в другом месте текущего проекта. Здесь актуально всё написанное выше. Только теперь обязательно нужно убрать все зависимости во внешние обёртки и крайне желательно иметь возможность кастомизации извне (темы или миксы). Если компонент содержит много логики, стоит задуматься, везде ли она нужна, или в каких-то местах её стоит модифицировать. Для второго варианта предусмотрите возможность кастомизации. Также здесь важно подумать над структурой компонента и разбить его на части при необходимости.

Переиспользование в похожем стеке. Вы понимаете, что компонент будет полезен в соседнем проекте, у которого тот же стек, что и у вас. Здесь всё сказанное выше становится обязательным. Кроме этого, советую внимательно следить за зависимостями и технологиями. Точно ли соседний проект использует те же версии библиотек, что и вы? Использует ли SASS и TypeScript той же версии?

Отдельно хочу выделить переиспользование в другой среде исполнения, например, в SSR. Решите, действительно ли ваш компонент может и должен уметь рендериться на SSR. Если да, заранее удостоверьтесь, что он рендерится, как ожидается. Помните, что существуют другие рантаймы, например, deno или GraalVM. Учитывайте их особенности, если используете.

Библиотеки компонентов


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

Стек


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

Размер


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

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

Важно, чтобы модульная библиотека не собиралась в один файл. Также нужно следить за версией JS, в которую собирается библиотека. Если вы собираете библиотеку в ES.NEXT, а проекты в ES5, возникнут проблемы. Ещё нужно правильно настроить сборку для старых версий браузеров и сделать так, чтобы все пользователи библиотеки знали, во что она собирается. Если это слишком сложно, есть альтернатива настроить собственные правила сборки библиотеки в каждом проекте.

Обновление


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

Вынося компоненты в библиотеку, которую использует кто-то ещё, вы теряете лёгкость рефакторинга. Чтобы груз рефакторинга не стал неподъёмным, советую не тащить в библиотеки новые компоненты. Они с высокой вероятностью будут меняться, а значит, придётся тратить много времени на обновление и поддержку совместимости.

Кастомизация и дизайн


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

Витрина компонентов


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

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

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



Дизайн-система


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

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

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

Выводы


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

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

Как видите, задача непростая. Разрабатывать качественные переиспользуемые компоненты сложно и долго. Стоит ли оно того? Я считаю, что каждый ответит на этот вопрос сам. Для небольших проектов накладные расходы могут оказаться слишком высокими. Для проектов, где не планируется длительное развитие, вкладывать усилия в повторное использование кода тоже спорное решение. Однако, сказав сейчас нам это не нужно, легко не заметить, как окажешься в ситуации, где отсутствие переиспользуемых компонентов принесёт массу проблем, которых могло бы и не быть. Поэтому не повторяйте наших ошибок и dont repeat yourself!

Смотреть доклад
Подробнее..

Почему мы должны выбросить 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

Перевод Десятикратное улучшение производительности 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% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Реализация подписки на обновления с помощью Google Sheets, Netlify Functions и React. Часть 1

04.06.2021 10:04:57 | Автор: admin

В этом туториале мы реализуем ~~Real World App~~ подписку на обновления с помощью гугл таблиц, бессерверных функций и реакта.


Основной функционал нашего приложения будет следующим:


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

Дополнительный функционал:


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

В первой части туториала мы реализуем основной функционал, во второй дополнительный.


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


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


Код приложения находится здесь.


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


  • netlify-cli интерфейс командной строки для запуска сервера для разработки (инициализации бессерверных функций) и "деплоя" приложения на Netlify; требуется глобальная установка: yarn global add netlify-cli или npm i -g netlify-cli; обязательно
  • google-spreadsheet JavaScript-библиотека для работы с гугл таблицами; обязательно
  • react на мой взгляд, это лучший JavaScript-фреймворк для фронтенда, но вы можете использовать любую другую библиотеку; наши бессерверные функции не будут зависеть от конкретного фреймворка
  • react-router-dom React-библиотека для маршрутизации
  • semantic-ui-react React-CSS-фреймворк (компоненты с готовыми стилями, ну, почти готовыми, мы их немного поправим)
  • react-google-recaptcha React-компонент, позволяющий напрямую взаимодействовать с соответствующим сервисом
  • nodemailer наиболее популярная Node.js-библиотека для работы с электронной почтой (рассылки писем)
  • dotenv утилита для доступа к переменным среды окружения

Разумеется, на вашей машине должен быть установлен Node.js и, желательно, yarn (после того, как вы поработаете с этим пакетным менеджером, вы едва ли вернетесь к npm).


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


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


Подготовка таблицы


Заходим в Google Cloud Platform (по ссылке, приведенной выше) и выполняем следующие действия:


  • создаем новый проект под названием, например, mail-list
  • ожидаем завершения создания проекта и выбираем его
  • переходим к обзору API (Go to APIs overview)
  • включаем Google Sheets API (Enable APIs and services)
  • создаем сервис-аккаунт для доступа к API (Create credentials)
  • переходим в созданный сервис-аккаунт
  • открываем вкладку Keys и добавляем ключ в формате JSON (Add key -> Create new key)
  • в скачанном файле (например, mail-list-315211-ca347b50f56a.json) нас интересуют свойства private_key и client_email; сохраните их где-нибудь, позже мы запишем их в переменные среды окружения

.


.


.


.


.


.


.


.


.


.


.


.


.


Заходим в Google Speadsheets и создаем новую таблицу (Пустой файл) с двумя графами: username и email.


.


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


.


В поисковой строке между d/ и /edit находится идентификатор таблицы, также где-нибудь его сохраните.


На этом настройка нашей таблицы завершена.


Бессерверные функции


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


О том, что такое Netlify Functions, можно почитать здесь.


Функции, как правило, размещаются в директории functions в корне проекта. Создаем новый React-проект (mail-list название нашего проекта):


yarn create react-app mail-list# илиnpx create ...

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


cd mail-listyarn add google-spreadsheet dotenv# илиnpm i ...

В корне проекта создаем файл .env (touch .env) и записываем в него сохраненные данные в следующем формате:


GOOGLE_SERVICE_ACCOUNT_EMAIL="YOUR_CLIENT_EMAIL"GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY----- YOUR_PRIVATE_KEY -----END PRIVATE KEY-----\n"GOOGLE_SPREADSHEET_ID="YOUR_SPREADSHEET_ID"

Создаем директорию functions, переходим в нее, создаем файл subscribe.js и открываем его в редакторе кода:


mkdir functionscd !$touch subscribe.jscode subscribe.js

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


// Загружаем переменные среды окружения из файла ".env"require('dotenv').config()const { GoogleSpreadsheet } = require('google-spreadsheet')// Бессерверная функция (о ее сигнатуре мы поговорим позже)// В данном случае, нас интересует только первый аргумент, принимаемый функцией - `event`// `event` - это тоже самое, что `req` в `express`, т.е. объект запросаexports.handler = async (event) => {  // Создаем экземпляр класса, представляющего внутренний документ гугл таблиц  // Конструктор класса принимает идентификатор таблицы  const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREADSHEET_ID)  try {    // Выполняем авторизацию с помощью сервис-аккаунта    await doc.useServiceAccountAuth({      client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,      private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n')    })    // Загружаем данные документа    await doc.loadInfo()    // Получаем ссылку на созданную нами таблицу    const sheet = doc.sheetsByIndex[0]    // Получаем данные от клиента в формате JSON и преобразуем их в объект    const data = JSON.parse(event.body)    // Получаем строки таблицы    const rows = await sheet.getRows()    // Обратите внимание, что заголовки столбцов таблицы становятся одноименными свойствами строк    // Если какая-либо из строк содержит email, указанный пользователем,    // значит, пользователь уже оформил подписку на обновления    if (rows.some((row) => row.email === data.email)) {      // Формируем ответ      const response = {        statusCode: 400,        body: JSON.stringify({          error: 'Пользователь с таким email уже оформил подписку'        }),        // Про это поговорим позже        headers: {          'Access-Control-Allow-Origin': '*',          'Access-Control-Allow-Credentials': 'true'        }      }      // и возвращаем его      return response    }    // Добавляем данные пользователя в таблицу в виде новой строки    await sheet.addRow(data)    // Формируем ответ    const response = {      statusCode: 200,      body: JSON.stringify({ message: 'Спасибо за подписку!' }),      headers: {        'Access-Control-Allow-Origin': '*',        'Access-Control-Allow-Credentials': 'true'      }    }    // и возвращаем его    return response  } catch (err) {    // Обрабатываем ошибку, возникшую на стороне сервера    console.error(err)    const response = {      statusCode: 500,      body: JSON.stringify({ error: 'Что-то пошло не так. Попробуйте позже' }),      headers: {        'Access-Control-Allow-Origin': '*',        'Access-Control-Allow-Credentials': 'true'      }    }    return response  }}

Бессерверные функции имеют такую сигнатуру:


exports.handler = (event, context, callback) => {...}

Если очень коротко, то event, как было отмечено ранее, это объект запроса (данные, поступающие от клиента), context любая дополнительная информация, связанная с запросом, например, статус пользователя, callback нужен в случае синхронной (блокирующей) функции для возврата ответа (мы используем асинхронную функцию, поэтому нам данный аргумент не нужен, впрочем, как и аргумент context).


Что касается этих заголовков ответа:


headers: {  'Access-Control-Allow-Origin': '*',  'Access-Control-Allow-Credentials': 'true'}

То они связаны с внутренними настройками Netlify (с выполняемыми перенаправлениями при обращении к функции из клиента). Перенаправления блокируются CORS (Cross-Origin Resource Sharing доступ к ресурсу из другого источника), потому что бессерверные функции не совсем бессерверные, под капотом они работают на основе централизованного сервера. Эти заголовки не требуются для локальной разработки, но развернуть приложение на хостинге без них не получится. В официальной документации про это ни слова. Возможно, к тому моменту, когда вы будете читать данную статью, этот недостаток будет устранен.


Следует отметить, что эти заголовки можно указать для всех ответов в файле netlify.toml.


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


Клиент


Устанавливаем зависимости для клиента:


yarn add react-router-dom semantic-ui-css semantic-ui-react react-google-recaptcha# илиnpm i ...

Код клиента находится в директории src. Удаляем из нее лишние файлы (оставляем только index.js и index.css), создаем директорию pages для страниц и hooks для пользовательских хуков. В директории pages создаем следующие файлы:


  • Home.js домашняя/главная страница
  • Subscribe.js страница с формой
  • Success.js страница с сообщением об успехе операции
  • NotFound.js резервная страница (ошибка 404)

В директории hooks создаем три файла:


  • useDeferredRoute.js хук для отложенной маршрутизации (опционально)
  • useTimeout.js хук-обертка для setTimeout
  • index.js экспорт индикатора загрузки и ре-экспорт хуков

Структура директории src:


src  hooks    index.js    useDeferredRoute.js    useTimeout.js  pages    Home.js    NotFound.js    Subscribe.js    Success.js  index.css  index.js

В index.css мы подключаем кастомный шрифт и вносим небольшие правки в стили semantic-ui:


@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');* {  font-family: 'Montserrat', sans-serif !important;}body {  min-height: 100vh;  display: grid;  align-content: center;  background: #8360c3;  background: linear-gradient(135deg, #2ebf91, #8360c3);}h2 {  margin-bottom: 3rem;}.ui.container {  max-width: 480px !important;  margin: 0 auto !important;  text-align: center;}.ui.form {  max-width: 300px;  margin: 0 auto;}.ui.form .field > label {  text-align: left;  font-size: 1.2rem;  margin-bottom: 0.8rem;}.ui.button {  margin-top: 1.5rem;  font-size: 1rem;  letter-spacing: 1px;  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;}.email-error {  color: #f93154;  text-align: left;}

В index.js мы импортируем компоненты приложения и реализуем разделение кода на уровне маршрутов с помощью lazy и Suspense:


import React, { lazy, Suspense } from 'react'import ReactDOM from 'react-dom'// Средства для маршрутизацииimport { BrowserRouter as Router, Switch, Route } from 'react-router-dom'// Индикатор загрузкиimport { Spinner } from './hooks'// Стили `semantic-ui`import 'semantic-ui-css/semantic.min.css'// Кастомные стилиimport './index.css'// "Ленивые" компоненты - динамический импортconst Home = lazy(() => import('./pages/Home'))const Subscribe = lazy(() => import('./pages/Subscribe'))const Success = lazy(() => import('./pages/Success'))const NotFound = lazy(() => import('./pages/NotFound'))ReactDOM.render(  <Suspense fallback={<Spinner />}>    <Router>      <Switch>        <Route path='/' exact component={Home} />        <Route path='/subscribe' component={Subscribe} />        <Route path='/success' component={Success} />        <Route component={NotFound} />      </Switch>    </Router>  </Suspense>,  document.getElementById('root'))

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


Хук useDeferredRoute добавляет искусственную задержку при начальной загрузке приложения и переходе пользователя к другой странице. Это совершенно не обязательно, возможно, кто-то даже скажет, что это плохая практика, но мне показалось, что так приложение будет более "живым". Кроме того, не так давно при работе над одним из проектов я столкнулся с необходимостью скрытия от пользователя позиционирования элементов фона отрендеренного компонента через искусственную задержку отображения компонента (вот где бы пригодился хук useTransition, но он пока еще экспериментальный).


import { useState, useEffect } from 'react'const sleep = (ms) => new Promise((r) => setTimeout(r, ms))export const useDeferredRoute = (ms) => {  const [loading, setLoading] = useState(true)  useEffect(() => {    const wait = async () => {      await sleep(ms)      setLoading(false)    }    wait()  }, [ms])  return { loading }}

Хук useTimeout, как было отмечено, это всего лишь обертка над нативным setTimeout:


import { useEffect, useRef } from 'react'export const useTimeout = (cb, ms) => {  const cbRef = useRef()  useEffect(() => {    cbRef.current = cb  }, [cb])  useEffect(() => {    function tick() {      cbRef.current()    }    if (ms > 1) {      const id = setTimeout(tick, ms)      return () => {        clearTimeout(id)      }    }  }, [ms])}

А вот как выглядит hooks/index.js:


// Мне не хотелось создавать директорию `components` для одного компонентаimport { Loader } from 'semantic-ui-react'export const Spinner = () => <Loader active inverted size='large' />export { useDeferredRoute } from './useDeferredRoute'export { useTimeout } from './useTimeout'

Теперь займемся страницами.


В Home.js нет ничего особенного. После скрытия индикатора загрузки, мы приветствуем пользователя и предлагаем ему подписаться на (кнопка "Подписаться" это на самом деле ссылка на страницу Subscribe):


import { Link } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute } from '../hooks'function Home() {  const { loading } = useDeferredRoute(1500)  if (loading) return <Spinner />  return (    <Container>      <h2>Доброго времени суток!</h2>      <h3>        Подпишитесь на обновления, <br /> чтобы оставаться в курсе событий!      </h3>      <Button color='teal' as={Link} to='/subscribe'>        Подписаться      </Button>    </Container>  )}export default Home

В Success.js также нет ничего особенного. После скрытия индикатора загрузки, мы благодарим пользователя за подписку и выполняем автоматическое перенаправление на главную страницу через 3 секунды с помощью нашего хука useTimeout. На случай, если автоматического перенаправления не произошло, имеется кнопка-ссылка на страницу Home:


import { Link, useHistory } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute, useTimeout } from '../hooks'function Success() {  const { loading } = useDeferredRoute(500)  const history = useHistory()  const redirectToHomePage = () => {    history.push('/')  }  useTimeout(redirectToHomePage, 3000)  if (loading) return <Spinner />  return (    <Container>      <h2>Спасибо за подписку!</h2>      <h3>Сейчас вы будете перенаправлены на главную страницу</h3>      <Button color='teal' as={Link} to='/'>        На главную      </Button>    </Container>  )}export default Success

Еще одна простая страница NotFound пользователь попадает на эту страницу при отсутствии совпадения с маршрутами приложения:


import { Link, useHistory } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute, useTimeout } from '../hooks'function NotFound() {  const { loading } = useDeferredRoute(500)  const history = useHistory()  const redirectToHomePage = () => {    history.push('/')  }  useTimeout(redirectToHomePage, 2000)  if (loading) return <Spinner />  return (    <Container>      <h2>Страница отсутствует</h2>      <h3>Сейчас вы будете перенаправлены на главную страницу</h3>      <Button color='teal' as={Link} to='/'>        На главную      </Button>    </Container>  )}export default NotFound

На странице Subscribe используется компонент react-google-recaptcha, которому в качестве пропа передается ключ сайта (sitekey). Данный ключ можно получить в административной консоли Google ReCAPTCHA, но для этого приложение надо сначала развернуть на Netlify. К счастью, для локальной разработки можно использовать этот тестовый ключ (это официальный ключ для тестирования): 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI. Позже мы вернемся к этому вопросу.


Еще один важный момент это конечная точка отправки пользовательских данных. Она должна начинаться с /.netlify/, затем указывается путь к соответствующей функции: functions/subscribe /.netlify/functions/subscribe (название функции часть пути). Следует отметить, что часть пути /.netlify/functions можно изменить в netlify.toml.


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


import { useState } from 'react'import { useHistory } from 'react-router-dom'import { Container, Form, Button } from 'semantic-ui-react'import ReCAPTCHA from 'react-google-recaptcha'import { Spinner, useDeferredRoute } from '../hooks'// Утилита для проверки того, что все поля заполненыconst isEmpty = (fields) => fields.some((f) => f.trim() === '')// Простой вариант утилиты для проверки адреса электронной почтыconst isEmail = (v) => /\w+@\w+\.\w+/i.test(v)function Subscribe() {  const [formData, setFormData] = useState({    username: '',    email: ''  })  const [error, setError] = useState(null)  const [recaptcha, setRecaptcha] = useState(false)  const { loading } = useDeferredRoute(1000)  const history = useHistory()  const onChange = ({ target: { name, value } }) => {    setError(null)    setFormData({      ...formData,      [name]: value    })  }  const onSubmit = async (e) => {    e.preventDefault()    const email = isEmail(formData.email)    if (!email) {      return setError('Введен неправильный email')    }    try {      const response = await fetch('/.netlify/functions/subscribe', {        method: 'POST',        body: JSON.stringify(formData),        headers: {          'Content-Type': 'application/json'        }      })      if (!response.ok) {        const json = await response.json()        return setError(json.error)      }      history.push('/success')    } catch (err) {      console.error(err)    }  }  // Учитывая, что мы используем тестовый ключ, капча всегда будет иметь истинное значение  const disabled = isEmpty(Object.values(formData)) || !recaptcha  const { username, email } = formData  if (loading) return <Spinner />  return (    <Container>      <h2>Подписаться на уведомления</h2>      <Form onSubmit={onSubmit}>        <Form.Field>          <label>Ваше имя</label>          <input            placeholder='Имя'            type='text'            name='username'            value={username}            onChange={onChange}            required          />        </Form.Field>        <Form.Field>          <label>Ваш email</label>          <input            placeholder='Email'            type='email'            name='email'            value={email}            onChange={onChange}            required          />        </Form.Field>        <p className='email-error'>{error}</p>        <ReCAPTCHA          sitekey='6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'          onChange={() => setRecaptcha(true)}        />        <Button color='teal' type='submit' disabled={disabled}>          Подписаться        </Button>      </Form>    </Container>  )}export default Subscribe

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


Если вы еще не установили netlify-cli, самое время это сделать:


yarn global add netlify-cli# илиnpm i -g ...

Все, что нужно сделать для запуска приложения и инициализации функции, это выполнить следующую команду:


netlify dev

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


Прелесть в том, что netlify-cli умеет автоматически определять, какой фреймворк используется в проекте, и выполнять нужные команды для его запуска.


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


.


.


.


Отлично, приложение работает, как ожидается.


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


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




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


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


Подробнее..

Реализация подписки на обновления с помощью Google Sheets, Netlify Functions и React. Часть 2

08.06.2021 12:19:01 | Автор: admin

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


Вот ссылка на первую часть.


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


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

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


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

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


Код приложения находится здесь.


Для реализации приложения используются следующие технологии:


  • netlify-cli интерфейс командной строки для запуска сервера для разработки (инициализации бессерверных функций) и "деплоя" приложения на Netlify; требуется глобальная установка: yarn global add netlify-cli или npm i -g netlify-cli; обязательно
  • google-spreadsheet JavaScript-библиотека для работы с гугл таблицами; обязательно
  • react на мой взгляд, это лучший JavaScript-фреймворк для фронтенда, но вы можете использовать любую другую библиотеку; наши бессерверные функции не зависят от конкретного фреймворка
  • react-router-dom React-библиотека для маршрутизации
  • semantic-ui-react React-CSS-фреймворк
  • react-google-recaptcha React-компонент, позволяющий напрямую взаимодействовать с соответствующим сервисом
  • nodemailer наиболее популярная Node.js-библиотека для работы с электронной почтой (рассылки писем)
  • dotenv утилита для доступа к переменным среды окружения

Начнем с деплоя приложения на Netlify.


Деплой приложения


Заходим на Netlify, создаем аккаунт, затем вводим в терминале следующую команду:


netlify login

Вводим логин и пароль, получаем сообщение об успешной авторизации.


Выполняем сборку проекта:


yarn build# илиnpm run build

И разворачиваем приложение в тестовом режиме:


netlify deploy

Отвечаем на вопросы (новое приложение, название приложения (например, mail-list), директория для деплоя (build) и т.д.), получаем ссылку на развернутое приложение.


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


Переходим в раздел sites, открываем наше приложение, выбираем вкладку Site settings, затем вкладку Build & deploy, находим раздел Environment, добавляем переменные (Environment variables).




Не будем ходить вокруг да около, а сразу развернем приложение в продакшн-режиме:


netlify deploy --prod

Готово. Легко, правда? Вот за что я люблю Netlify.


Теперь, когда у нас имеется URL, мы можем зарегистрировать наше приложение в Google ReCAPTCHA.


Заходим в консоль администратора и создаем новое приложение (+). Вводим название сайта (ярлык), выбираем reCAPTCHA v2, указываем домен (URL нашего приложения без протокола), принимаем условия использования (флажок "Отправлять владельцам оповещения" можно снять), нажимаем "Отправить". Получаем ключ сайта и секретный ключ, нам нужен только первый.





Добавляем в .env такую переменную:


REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY=YOUR_SITE_KEY

Вносим изменение в Subscribe.js:


<ReCAPTCHAsitekey={process.env.REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY}onChange={() => setRecaptcha(true)}/>

Повторно собираем и разворачиваем приложение:


yarn build# илиnpm run build# иnetlify deploy --prod

Если все сделано правильно, то на странице с формой появится настоящая капча.



Отлично.


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


Автоматическая рассылка уведомлений


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


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


Для реализации автоматической рассылки уведомлений мы будем использовать nodemailer, а для тестирования Mailtrap.


Создаем аккаунт на Mailtrap, открываем автоматически созданный проект MyInbox, на вкладке SMTP Settings в разделе Integrations выбираем Node.js -> Nodemailer, получаем данные для авторизации.




Сохраняем эти данные в .env:


SMTP_USER='USER'SMTP_PASS='PASS'

В корне проекта создаем директорию send-mail, а в ней index.js следующего содержания:


require('dotenv').config()const nodemailer = require('nodemailer')const { GoogleSpreadsheet } = require('google-spreadsheet')const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREADSHEET_ID)// Тестовый транспортер для отправки сообщенийconst testTransporter = nodemailer.createTransport({host: 'smtp.mailtrap.io',port: 2525,auth: {user: process.env.SMTP_USER,pass: process.env.SMTP_PASS}})// Функция для создания сообщения в формате HTML// Она принимает имя пользователя и его email// Обратите внимание на значение атрибута `href` тега `a` -// URL соответствующей страницы нашего приложения (скоро мы ее создадим) + email пользователяconst createMessage = (username, email) => `<p><strong>Уважаемый ${username} </strong>, <em>спасибо за подписку</em>!</p><p>Для того, чтобы отписаться от обновлений, перейдите по <a href="http://personeltest.ru/aways/mail-list.netlify.app/unsubscribe/${email}" target="_blank">этой ссылке</a></p>`const sendMail = async () => {try {await doc.useServiceAccountAuth({client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n')})await doc.loadInfo()const sheet = doc.sheetsByIndex[0]const rows = await sheet.getRows()// Перебираем строки таблицы  данные пользователей,// создаем сообщение и отправляем его// text  резервный контент на случай, если почтовый клиент пользователя не поддерживаем сообщения в формате HTMLrows.forEach(async (row) => {await testTransporter.sendMail({from: 'Mail list <mail-list.netlify.app>',to: row.email,subject: 'Благодарность за подписку',text: 'Спасибо за подписку',html: createMessage(row.username, row.email)})})console.log('Сообщения отправлены')} catch (err) {console.error(err)}}sendMail()

Добавим в package.json (раздел scripts) команду для рассылки уведомлений:


send: node send-mail/index.js

Запускаем скрипт (разумеется, в таблице должны быть какие-то данные):


yarn send# илиnpm run send

Получаем Сообщения отправлены в терминале и письмо в Mailtrap.



Для взаимодействия с реальными почтовыми службами (yahoo в моем случае) нужен реальный SMTP-провайдер.


Среди наиболее популярных решений можно назвать SendGrid и SendingBlue, но в случае выбора одного из этих сервисов, нам придется долго и упорно убеждать их владельцев в том, что мы не собираемся заниматься рассылкой спама. Еще есть Mailgun, но он платный с трехмесячным free trial.


Поэтому мы будем использовать Gmail.


Безусловно, если очень хочется, можно поднять собственный SMPT-сервер. Также существуют инструменты для рассылки писем, которые, как заявляют их разработчики, работают без SMTP, например, sendmail.


Добавляем в .env переменные с данными вашего Gmail-аккаунта:


GMAIL_USER='USER'GMAIL_PASS='PASS'

И вносим изменения в send-mail/index.js:


/*const testTransporter = nodemailer.createTransport({host: 'smtp.mailtrap.io',port: 2525,auth: {user: process.env.SMTP_USER,pass: process.env.SMTP_PASS}})*/const gmailTransporter = nodemailer.createTransport({service: 'gmail',auth: {user: process.env.GMAIL_USER,pass: process.env.GMAIL_PASS}})rows.forEach(async (row) => {await gmailTransporter.sendMail({// ...})})

Запускаем скрипт (в таблице должен быть указан ваш email):


yarn send


Существует один нюанс, связанный с использованием Gmail в качестве сервиса для рассылки писем гугл может блокировать к нему доступ, считая приложение ненадежным (существует платная версия Gmail Google Workspace, которая с точки зрения гугла, конечно же, является надежной).


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



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


Отписка от обновлений


Добавляем в приложение (src/pages) новую страницу Unsubscribe.js. На этой странице после скрытия индикатора загрузки, мы пытаемся получить email пользователя из параметров строки запроса с помощью хука useParams. Если email отсутствует, выполняется перенаправление на главную страницу. Иначе мы отправляем email в функцию, которая удаляет из таблицы соответствующую строку. Если пользователь с указанным email не оформлял подписку на обновления, выбрасывается исключение. При успешном завершении операции отображается сообщение о том, что пользователь больше не будет получать уведомлений.


import { useState, useEffect } from 'react'import { Link, useParams, useHistory } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute } from '../hooks'function Unsubscribe() {const { loading } = useDeferredRoute(1000)const [error, setError] = useState(null)// Извлекаем email из параметров строки запросаconst { email } = useParams()const history = useHistory()useEffect(() => {// Если email отсутствует, выполняем перенаправление на главную страницуif (!email) {return history.push('/')}async function unsubscribe() {try {// Отправляем email в функциюconst response = await fetch('/.netlify/functions/unsubscribe', {method: 'POST',body: JSON.stringify(email),headers: {'Content-Type': 'application/json'}})// Если возникла ошибка, значит, пользователь не оформлял подпискуif (!response.ok) {const json = await response.json()setError(json.error)}} catch (err) {console.error(err)}}unsubscribe()// eslint-disable-next-line}, [])if (loading) return <Spinner />return (<Container>{error ? (<h3>{error}</h3>) : (<h3>Вы больше не будете получать уведомлений</h3>)}<Button color='teal' as={Link} to='/'>На главную</Button></Container>)}export default Unsubscribe

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


require('dotenv').config()const { GoogleSpreadsheet } = require('google-spreadsheet')exports.handler = async (event) => {const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREADSHEET_ID)try {await doc.useServiceAccountAuth({client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n')})await doc.loadInfo()const sheet = doc.sheetsByIndex[0]// Получаем email пользователяconst data = JSON.parse(event.body)const rows = await sheet.getRows()// Выполняем поиск соответствующей строкиconst index = rows.findIndex((row) => row.email === data)// Если строка не найдена, значит, пользователь не оформлял подпискуif (index === -1) {const response = {statusCode: 400,body: JSON.stringify({error: 'Пользователь с указанным email не найден'}),headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': 'true'}}return response}// Удаляем строкуawait rows[index].delete()const response = {statusCode: 200,body: JSON.stringify({ message: 'Пользователь удален' }),headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': 'true'}}return response} catch (err) {console.error(err)const response = {statusCode: 500,body: JSON.stringify({ error: 'Что-то пошло не так. Попробуйте позже' }),headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Credentials': 'true'}}return response}}

Еще раз (обещаю, что в последний) собираем и разворачиваем проект:


yarn build# илиnpm run build# иnetlify deploy --prod

Не забудьте обновить переменные среды окружения на Netlify.


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




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


Как бы то ни было, если вы в точности следовали инструкциям, а еще лучше реализовали какие-то дополнительные возможности, то в вашем портфолио появилось настоящее Real World App, разработанное с использованием самых современных технологий (да, мы не использовали TypeScript, но для нашего небольшого проекта это было бы слишком круто).




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


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 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