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

Webdev

Перевод Картинка, которая одновременно является кодом на Javascript

08.09.2020 10:16:23 | Автор: admin

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

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

https://codesandbox.io/s/executable-gif-8yq0j?file=/index.html

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

https://executable-gif.glitch.me/image.gif

Выбор подходящего типа изображения


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

/*ALL OF THE BINARY IMAGE DATA*/

Это будет допустимый файл Javascript. Однако файлы изображений должны начинаться с определённой последовательности байтов, заголовка файла, специфичного для формата изображения. Например, файлы PNG всегда должны начинаться с последовательности байтов 89 50 4E 47 0D 0A 1A 0A. Если изображение будет начинаться с /*, то файл перестанет быть файлом изображения.

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

PNG=`ALL OF THE BINARY IMAGE DATA`;

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

К сожалению, большинство последовательностей байтов в заголовках файлов изображений содержат непечатаемые символы, которые нельзя использовать в именах переменных. Но есть один формат, который мы можем использовать: GIF. Блок заголовка GIF имеет вид 47 49 46 38 39 61, что удобно преобразуется в ASCII в строку GIF89a абсолютно допустимое имя переменной!

Выбор подходящих размеров изображения


Теперь, когда мы нашли формат изображения, начинающийся с допустимого имени переменной, нам нужно добавить символы знака равенства и обратного апострофа (backtick). Следовательно следующими четырьмя байтами файла будут: 3D 09 60 04


Первые байты изображения

В формате GIF четыре байта после заголовка определяют размеры изображения. Нам нужно уместить в них 3D (знак равенства) и 60 (обратный апостроф, открывающий строку). В GIF используется порядок little endian, поэтому второй и четвёртый символы имеют огромное влияние на размеры изображения. Они должны быть как можно меньше, чтобы изображение не получилось шириной и высотой в десятки тысяч пикселей. Следовательно, нам нужно хранить большие байты 3D и 60 в наименее значимых байтах.

Второй байт ширины изображения должен быть допустимым пробельным символом (whitespace), потому что он будет пробелом между знаком равенства и началом строки GIF89a= `.... Стоит также помнить, что шестнадцатеричный код символов должен быть как можно меньше, иначе изображение окажется огромным.

Наименьший пробельный символ это 09 (символ горизонтальной табуляции). Он даёт нам ширину изображения 3D 09, что в little endian равно 2365; немного шире, чем бы мне хотелось, но всё равно вполне приемлемо.

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

Засовываем в файл скрипт


Пока наш исполняемый GIF почти ничего не делает. Он просто присваивает глобальной переменной GIF89a длинную строку. Мы хотим, чтобы происходило что-нибудь интересное! Основная часть данных внутри GIF используется для кодирования изображения, поэтому если мы попробуем вставить туда Javascript, то изображение, вероятно, будет сильно искажённым. Но по какой-то причине формат GIF содержит нечто под названием Comment Extension. Это место для хранения метаданных, которые не интерпретируются декодером GIF идеальное место для нашей Javascript-логики.

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

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

GIF89a= ` BINARY COLOR TABLE DATA ... COMMENT BLOCK:`;alert("Javascript!");/*REST OF THE IMAGE */

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

alert('Javascript');/*0x4A*/console.log('another subblock');/*0x1F*/...

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

https://gist.github.com/SebastianStamm/c2433819cb9e2e5af84df0904aa43cb8

Подчищаем двоичные данные


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

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

GIF89a= ` BINARY DATA `;

Если двоичные данные будут содержать символ ` или сочетание символов ${, то у нас возникнет проблема, ведь они или завершат шаблонную строку, или создадут недопустимое выражение. Исправить это довольно просто: достаточно изменить двоичные данные! Например, вместо символа ` (шестнадцатеричный код 60) можно использовать символ a (шестнадцатеричный код 61). Так как эта часть файла содержит палитру цветов, то это приведёт к незначительному изменению некоторых цветов, например, к использованию цвета #286148 вместо #286048. Маловероятно, что кто-нибудь заметит разницу.

Боремся с искажениями


В конце Javascript-кода мы открыли многострочный комментарий, чтобы двоичные данные изображения не влияли на парсинг Javascript:

alert("Script done");/*BINARY IMAGE DATA ...

Если данные изображения будут содержать последовательность символов */, то комментарий закончится преждевременно, из-за чего файл Javascript окажется недопустимым. Здесь мы снова можем вручную изменять один из двух символов, чтобы они не завершали комментарий. Однако поскольку теперь мы находимся в разделе закодированного изображения, то в результате получим повреждённое изображение, например такое:


Повреждённое изображение

В самых неблагоприятных случаях изображение может вообще не отображаться. Аккуратно выбирая бит, который нужно инвертировать, мне удалось минимизировать искажения. К счастью, было всего несколько случаев повреждающих сочетаний*/. На готовом изображении всё равно заметны небольшие искажения, например, в нижней части строки Valid Javascript File, но в целом я вполне доволен результатом.

Завершаем файл


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

/* BINARY DATA*/// 00 3B

Уговариваем браузер исполнить изображение


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

Refused to execute script from 'http://localhost:8080/image.gif' because its MIME type ('image/gif') is not executable. [Отказ от исполнения скрипта из 'http://localhost:8080/image.gif', потому что его MIME-тип не является исполняемым.]

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

Без информации о MIME-типе из заголовка браузер не знает, что это изображение и делает именно то, что лучше всего подходит в контексте: отображает его как изображение в теге <img> или исполняет как Javascript в теге <script>.

Но зачем это всё?


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



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


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

Подробнее..

Перевод Компоновщик в JavaScript

10.09.2020 18:21:56 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса JavaScript Developer. Basic.



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

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

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

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

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



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

Внутреннее строение


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

Где применяется этот шаблон?


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

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

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

Чем интересен этот шаблон?


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

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

Примеры


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

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

Вот так будет выглядеть Document:

class Document {  constructor(title) {    this.title = title    this.signature = null  }  sign(signature) {    this.signature = signature  }}


Теперь, применив компоновщик, мы обеспечим поддержку методов, схожих с теми, что определены в Document.

class DocumentComposite {  constructor(title) {    this.items = []    if (title) {      this.items.push(new Document(title))    }  }  add(item) {    this.items.push(item)  }  sign(signature) {    this.items.forEach((doc) => {      doc.sign(signature)    })  }}


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



Отлично! Похоже, мы на верном пути. То, что у нас получилось, соответствует схеме, представленной выше.



Итак, наша древовидная структура содержит два листа/узла Document и DocumentComposite. Оба они совместно используют один и тот же интерфейс и, следовательно, действуют как части единого композитного дерева.

Здесь следует отметить, что лист/узел дерева, не являющийся композитом (Document), не является коллекцией или группой объектов и, следовательно, не продолжит ветвления. Тем не менее лист/узел, являющийся композитом, содержит коллекцию частей (в нашем случае это items). Также помните, что Document и DocumentComposite используют общий интерфейс, а следовательно, разделяют и метод sign.
Так в чем же заключается эффективность такого подхода? Несмотря на то что DocumentComposite использует единый интерфейс, поскольку задействует метод sign, как и Document, в нем реализован более эффективный подход, позволяющий при этом достичь конечной цели.

Поэтому вместо такой структуры:

const pr2Form = new Document(  'Primary Treating Physicians Progress Report (PR2)',)const w2Form = new Document('Бланк Налогового управления (W2)')const forms = []forms.push(pr2Form)forms.push(w2Form)forms.forEach((form) => {  form.sign('Bobby Lopez')})


Мы можем видоизменить код и сделать его эффективнее, воспользовавшись преимуществами компоновщика:

const forms = new DocumentComposite()const pr2Form = new Document(  'Текущие сведения о производственных врачах (PR2)',)const w2Form = new Document('Бланк Налогового управления (W2)')forms.add(pr2Form)forms.add(w2Form)forms.sign('Роман Липин')console.log(forms)


При таком подходе нам потребуется лишь единожды выполнить sign после добавления всех нужных документов, и эта функция подпишет все документы
Убедиться в этом можно, просмотрев вывод функции console.log(forms):



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

Также не стоит забывать, что наш DocumentComposite может включать коллекцию документов.

Поэтому, когда мы проделали вот это:

forms.add(pr2Form) // Документforms.add(w2Form) // Документ


Наша схема обрела следующий вид:



Мы добавили две формы, и теперь эта схема почти полностью соответствует исходной:



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

const forms = new DocumentComposite()const pr2Form = new Document(  'Текущие сведения о производственных врачах (PR2)',)const w2Form = new DocumentComposite('Бланк Налогового управления (W2)')forms.add(pr2Form)forms.add(w2Form)forms.sign('Роман Липин')console.log(forms)


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



В конечном итоге мы бы достигли той же цели все документы были бы подписаны:



В этом и заключается польза компоновщика.

Заключение


На этом у меня пока все! Надеюсь, эта информация оказалась для вас полезной. Дальше больше!
Найти меня на medium



Читать ещё:


Подробнее..

Перевод React-компоненты шаблонов проектирования

17.03.2021 18:23:31 | Автор: admin

Введение

Эта документация поможет найти компромиссы между различными шаблонами (patterns) React, а также определить, когда использование каждого из них будет наиболее целесообразным. Нижеприведенные шаблоны позволят получить более практичный и многократно используемый код, придерживаясь принципов проектирования, таких как разделение ответственности, DRY (Dont repeat yourself - не повторяй себя) и повторное использование кода. Некоторые из этих шаблонов помогут решить проблемы, которые возникают в больших React приложениях, таких как пробрасывание (prop drilling) или управление состоянием. Каждый основной шаблон включает пример, размещенный на CodeSandBox.

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

Обзор

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

Подумайте о составных компонентах, таких как элементы <select> и <option> в HTML. Порознь, они не слишком много делают, но вместе они позволяют создать полноценный результат. (Кент С. Доддс)

Зачем использовать составные компоненты? Какую ценность они обеспечивают?

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

Пример

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

Мы создадим один родительский компонент (parent component), RadioImageForm, который будет отвечать за логику формы, и один дочерний, "подкомпонент", RadioInput, который будет отображать изображения радиовходов. Вместе они создадут один составной компонент.

{/* The parent component that handles the onChange events and managing the state of the currently selected value. */}<RadioImageForm>  {/* The child, sub-components.   Each sub-component is an radio input displayed as an image  where the user is able to click an image to select a value. */}  <RadioImageForm.RadioInput />  <RadioImageForm.RadioInput />  <RadioImageForm.RadioInput /></RadioImageForm>

В файле src/components/RadioImageForm.tsx мы имеем 1 основной компонент:

  1. RadioImageForm Сначала мы создаем родительский компонент, который будет управлять состоянием и обрабатывать события изменения формы. Потребитель компонента, другие инженеры, использующие компонент, могут подписаться на текущее выбранное значение радиовходов, с помощью функции поддержки обратного вызова (callback function prop), onStateChange. При каждом изменении формы компонент будет обрабатывать обновления радиовходов и предоставлять потребителю текущее значение.

Внутри компонента RadioImageForm имеется один статический компонент или подкомпонент:

  1. RadioInput Далее создадим статический компонент, элемент подмножества компонента RadioImageForm. RadioInput это статический компонент, который вызывается через точечную запись в синтаксисе, например, <RadioImageForm.RadioInput/>. Это позволяет потребителю нашего компонента легко получить доступ к нашим подкомпонентам и позволяет ему иметь представление о том, как RadioInput отображается в форме.

Компонент RadioInput является статическим свойством класса RadioImageForm. Составной компонент состоит из родительского компонента RadioImageForm и статического компонента RadioInput. Далее я буду называть статические компоненты подкомпонентами ( "sub-components.").

Давайте сделаем первые шаги по созданию нашего компонента RadioImageForm.

export class RadioImageForm extends React.Component<Props, State> {  static RadioInput = ({    currentValue,    onChange,    label,    value,    name,    imgSrc,    key,  }: RadioInputProps): React.ReactElement => (    //...  );  onChange = (): void => {    // ...  };  state = {    currentValue: '',    onChange: this.onChange,    defaultValue: this.props.defaultValue || '',  };  render(): React.ReactElement {    return (      <RadioImageFormWrapper>        <form>        {/* .... */}        </form>      </RadioImageFormWrapper>    )  }}

При создании многократно используемых компонентов мы хотим предоставить продукт, в котором потребитель имеет контроль над тем, где именно элементы отображаются в его коде. Для корректной работы компонентов RadioInput потребуется доступ к внутреннему состоянию, внутренней функции onChange, а также к пропсам пользователя (user's props). Но как передать эти данные подкомпонентам? Здесь в игру вступают React.children.map и React.cloneElement. Для подробного объяснения того, как это работает вы можете углубиться в документацию React:

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

render(): React.ReactElement {  const { currentValue, onChange, defaultValue } = this.state;  return (    <RadioImageFormWrapper>      <form>        {          React.Children.map(this.props.children,             (child: React.ReactElement) =>              React.cloneElement(child, {                currentValue,                onChange,                defaultValue,              }),          )        }      </form>    </RadioImageFormWrapper>  )}

Следует отметить в этой имплементации:

  1. RadioImageFormWrapper наши стили компонентов со стилизованными компонентами. Мы можем это проигнорировать, так как стили CSS не имеют отношения к шаблону компонентов.

  2. React.children.map итерация выполняется через дочерние компоненты напрямую, что позволяет нам манипулировать каждым дочерним компонентом непосредственно.

  3. React.cloneElement из документов React docs:

  • Клонируйте React-элемент и возвращайте новый, используйте его в качестве отправной точки. Полученный элемент будет иметь пропс (props) оригинала, который будет плавно объединен с новым пропсом. Новые дочерние элементы заменят существующие.

С помощью React.children.map и React.cloneElement мы можем выполнять итерацию и манипулировать каждым из дочерних элементов. Таким образом, мы можем передавать дополнительные пропсы, которые четко определяем в этом процессе трансформации. В этом случае мы можем передать внутреннее состояние RadioImageForm каждому дочернему компоненту RadioInput. Так как React.cloneElement выполняет мягкое слияние, то любой пропс, определенный пользователем на RadioInput, будет передан компоненту.

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

static RadioInput = ({  currentValue,  onChange,  label,  value,  name,  imgSrc,  key,}: RadioInputProps) => (  <label className="radio-button-group" key={key}>    <input      type="radio"      name={name}      value={value}      aria-label={label}      onChange={onChange}      checked={currentValue === value}      aria-checked={currentValue === value}    />    <img alt="" src={imgSrc} />    <div className="overlay">      {/* .... */}    </div>  </label>);

Отметим, что в RadioInputProps мы однозначно определили в качестве образца, какие пропсы пользователь может передавать подкомпонентам RadioInput.

Затем пользователь компонента может ссылаться на RadioInput с помощью точечной записи синтаксиса (dot-syntax notation) в своем коде (RadioImageForm.RadioInput):

// src/index.tsx<RadioImageForm onStateChange={onChange}>  {DATA.map(    ({ label, value, imgSrc }): React.ReactElement => (      <RadioImageForm.RadioInput        label={label}        value={value}        name={label}        imgSrc={imgSrc}        key={imgSrc}      />    ),  )}</RadioImageForm>

Поскольку RadioInput является статическим свойством, он не имеет доступа к элементу RadioImageForm. Следовательно, вы не можете напрямую ссылаться на состояние или методы, определённые в классе RadioImageForm. Например, this.onChange не будет работать в следующем примере: static RadioInput = () => <input onChange={this.onChange} //

Заключение

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

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

Недостатки

В то время как мы создали удобный интерфейс для пользователей наших компонентов, тем не менее есть брешь в нашем проекте. Что если <RadioImageForm.RadioInput/> будет погребен в куче div-элементов? Что произойдет, если пользователь захочет переупорядочить компоновку? Компонент всё также отобразит, но радиовход не получит текущее значение из состояния RadioImageForm, что нарушит пользовательский функционал. Этот проект шаблона компонентов не является гибким, что подводит нас к нашему следующему варианту создания шаблона компонентов.

Составные компоненты CodeSandBox

Пример составных компонентов с функциональными компонентами и React хуками (React hooks):

Составные компоненты и функциональные компоненты CodeSandBox


Гибкие компоненты соединения

Обзор

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

Зачем использовать гибкие составные компоненты? Какую ценность они представляют?

С помощью гибких составных компонентов, мы можем неявно получить доступ к внутреннему состоянию компонента нашего класса, независимо от того, где они находятся в дереве компонентов. Другая причина использования гибких составных компонентов это когда несколько компонентов должны иметь общее состояние, независимо от их положения в дереве компонентов. Пользователь компонента должен иметь возможность гибко выбирать, где будут рендерится наши составные компоненты. Для этого мы будем использовать React's Context API.

Но сначала мы должны получить некоторый контекст (context) о React's Context API, прочитав официальные React docs.

Пример

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

Давайте создадим некоторый контекст для нашего компонента RadioImageForm, чтобы мы могли передавать данные дочерним компонентам (например, RadioInput) в любом месте в родительском дереве компонентов. Будем надеяться, что вы почистили React's Context, но вот краткое резюме из документа React's doc:

  • Контекст обеспечивает способ передачи данных через дерево компонентов без необходимости передавать реквизит вручную на каждом уровне.

Во-первых, мы называем метод React.createContext, предоставляя значения по умолчанию в нашем контексте. Затем мы присваиваем отображаемое имя объекту контекста. Мы добавим его в верхнюю часть нашего файла RadioImageForm.tsx.

const RadioImageFormContext = React.createContext({  currentValue: '',  defaultValue: undefined,  onChange: () => { },});RadioImageFormContext.displayName = 'RadioImageForm';
  1. Вызовом React.createContext мы создали контекстный объект, содержащий пару Provider и Consumer. Первые будут предоставлять данные вторым; в нашем примере Provider будет показывать наше внутреннее состояние подкомпонентам.

  2. Назначив displayName нашему контекстному объекту, мы можем легко различать компоненты контекста в React Dev Tool (React Developer Tools). Таким образом, вместо Context.Provider или Context.Consumer у нас будут RadioImageForm.Provider и RadioImageForm.Consumer. Это помогает повысить удобство чтения, если у нас есть несколько компонентов, использующих Context во время отладки.

Далее мы можем рефакторизовать рендер-функцию компонента RadioImageForm и удалить из него устаревшие функции React.children.map и React.cloneElement, а также выполнить рендеринг пропсов дочерних элементов.

render(): React.ReactElement {  const { children } = this.props;  return (    <RadioImageFormWrapper>      <RadioImageFormContext.Provider value={this.state}>        {children}      </RadioImageFormContext.Provider>    </RadioImageFormWrapper>  );}

RadioImageFormContext.Provider принимает один проп (prop-свойство) с именем value. Данные, передаваемые в проп это контекст, который мы хотим предоставить потомкам (descendants) этого Provider. Подкомпонентам необходим доступ к нашему внутреннему состоянию, а также к внутренней функции onChange. Назначив метод onChange, currentValue и defaultValue объекту state, мы можем передать this.state в контекстное значение.

  • ? Всякий раз, когда value меняется на что-то другое, оно осуществляет ререндеринг себя и всех своих потребителей. React - это постоянный рендеринг, поэтому, передавая объект в пропс value, он будет ререндерить (re-render) все дочерние компоненты, потому что объект переназначается на каждом рендеринге (созданием нового объекта). Это неизбежно может привести к проблемам с производительностью, потому что переданный в объект пропс value будет воссоздаваться каждый раз, когда дочерняя компонента ререндируется (re-renders) даже в том случае, если значения в объекте не изменились. НЕ ДЕЛАЙТЕ ЭТОГО: <RadioImageFormContext.Provider value={{ currentValue: this.state.currentValue, onChange: this.onChange }}>. Вместо этого передайте this.state, чтобы предотвратить лишний ререндеринг дочерних компонентов.

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

export class RadioImageForm extends React.Component<Props, State> {  static Consumer = RadioImageFormContext.Consumer;  //...

В качестве альтернативы, если у вас есть внешние компоненты, которые должны быть подписаны на контекст, вы можете экспортировать RadioImageFormContext.Consumer в файл, например, экспортировать const RadioImageFormConsumer = RadioImageFormContext.Consumer.

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

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

static SubmitButton = ({ onSubmit }: SubmitButtonProps) => (  <RadioImageForm.Consumer>    {({ currentValue }) => (      <button        type="button"        className="btn btn-primary"        onClick={() => onSubmit(currentValue)}        disabled={!currentValue}        aria-disabled={!currentValue}      >        Submit      </button>    )}  </RadioImageForm.Consumer>);

Следует отметить, что Consumer требует функцию в качестве дочерней; он использует шаблон render props, например ({ currentValue }) => (// Render content)). Эта функция получает текущее контекстное значение, подписываясь на изменения внутреннего состояния. Это позволяет нам явно указывать, какие данные нам нужны от Provider. Например, SubmitButton ожидает свойство currentValue, которое было ссылкой на класс RadioImageForm. Но теперь он получает прямой доступ к этим значениям через Context.

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

Благодаря этим изменениям пользователь нашего компонента может использовать наши составные компоненты (compound components) в любом месте дерева компонентов (component tree).

В файле src/index.tsx вы можете посмотреть, как потребитель нашего компонента может его использовать.

Заключение

Благодаря такой схеме мы можем разрабатывать компоненты, пригодные для многократного применения, при этом потребитель может гибко использовать наши компоненты в различных контекстах. Мы обеспечили удобный интерфейс, в котором потребитель компонента не нуждается в знании внутренней логики. С помощью Context API мы можем передать неявное состояние нашего компонента подкомпонентам независимо от их глубины в иерархии. Это дает пользователю контроль для улучшения их стилистического восприятия. И в этом вся прелесть компонентов Flexible Compound Components: они помогают отделить презентацию от внутренней логики. Реализация составных компонентов с помощью Контекстного API является более выгодной, и поэтому я бы рекомендовал начинать с шаблона Гибкие составные компоненты, а не Составные компоненты.

Гибкий составной компонент CodeSandBox

Пример гибких составных компонентов с функциональными компонентами и React hooks:

Гибкие составные компоненты и Функциональные компоненты CodeSandBox


Шаблон Provider

Обзор

Шаблон Провайдера (provider pattern) является элегантным решением для совместного использования данных в дереве компонентов React. Шаблон провайдера использует предыдущие концепции, две основные из которых контекстный API в React и рендеринг пропсов.

Для получения более подробной информации посетите React docs on Context API и Render Props.

Context API:

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

Рендеринг Пропсов (Render Props):

  • Термин "render prop" относится к технике разделения кода между React-компонентами, использующей реквизит (prop), значение которого является функцией.

Зачем использовать шаблон провайдера (provider pattern)? Какую ценность он представляет?

Шаблон провайдера это мощная концепция, которая помогает при проектировании сложного приложения, так как решает несколько задач. С помощью React мы имеем дело с однонаправленным потоком данных, и при объединении нескольких компонентов мы должны обеспечить пробрасывание пропов (prop drill) общего состояния от родительского уровня к дочерним. Это может привести к неприглядному спагетти-коду (spaghetti code).

Проблема загрузки и отображения общих данных на странице заключается в обеспечении этого общего состояния для дочерних компонентов, которые нуждаются в доступе к нему. С помощью React's Context API мы можем создать компонент поставщика данных, который будет заниматься извлечением данных и предоставлением общего состояния для всего дерева компонентов. Таким образом, несколько дочерних компонентов, независимо от того, насколько глубоко они расположены, могут получить доступ к одним и тем же данным. Сбор данных и отображение данных это две отдельные задачи. В идеале, один компонент имеет только одну задачу. Родительский компонент, обертывающий данные (провайдер), в первую очередь отвечает за сбор данных и обработку общего состояния, в то время как дочерние компоненты могут сосредоточиться на том, как рендировать эти данные. Компонент провайдера также может обрабатывать бизнес-логику нормализации (normalizing) и массирования (massaging) данных при отклике (response data), чтобы дочерние компоненты последовательно получали одну и ту же модель даже при обновлении конечных точек API и изменении модели отклика данных (response data model). Такое разделение обязанностей имеет большое значение при построении больших приложений, так как помогает в обслуживании и упрощает разработку. Другие разработчики могут легко определить сферу ответственности каждого компонента.

Некоторые могут задаться вопросом, почему бы не использовать библиотеку управления состоянием, такую как Redux, MobX, Recoil, Rematch, Unstated, Easy Peasy, или ряд других? Хотя эти библиотеки могут помочь в решении проблемы управления состоянием, нет необходимости в чрезмерном совершенствовании технологии решения этой проблемы. Внедрение библиотеки управления состоянием создает массу повторяющегося кода шаблонов, сложных потоков, которые необходимо изучить другим разработчикам, а также раздувает приложение, что увеличивает его размер. Я не говорю вам, что библиотека управления состоянием бесполезна, и что вы не должны ее использовать, но важно точно знать, какое преимущество она сможет обеспечить, чтобы обосновать использование новой библиотеки. Когда я инициализировал свое приложение с помощью React, я отказался от использования библиотеки управления состоянием, несмотря на то, что так происходило во всех других проектах React. Хотя мои потребности могут отличаться от других требований, я не увидел причин усложнять нашу кодовую базу (codebase) при помощи инструмента управления состоянием, который, возможно, придется освоить будущим разработчикам. Вместо этого я выбрал решение с использованием шаблона провайдера.

Пример

После этого длительного вступления, давайте рассмотрим пример. На этот раз мы создадим очень простое приложение, чтобы продемонстрировать, как мы можем легко делиться состоянием между компонентами и даже страницами, при этом придерживаясь таких принципов проектирования, как разделение проблем (SoC) и DRY (Don't Repeat Yourself). Вы можете проследить за конечным результатом в CodeSandBox. В нашем примере, мы создадим социальное приложение для собак, где наш пользователь сможет просматривать их профиль и список друзей собак.

Сначала создадим компонент провайдера данных, DogDataProvider, который будет отвечать за получение наших данных и предоставление их дочерним компонентам, независимо от их положения в дереве компонентов, с помощью контекстного API React's Context.

// src/components/DogDataProvider.tsxinterface State {  data: IDog;  status: Status;  error: Error;}const initState: State = { status: Status.loading, data: null, error: null };const DogDataProviderContext = React.createContext(undefined);DogDataProviderContext.displayName = 'DogDataProvider';const DogDataProvider: React.FC = ({ children }): React.ReactElement => {  const [state, setState] = React.useState<State>(initState);  React.useEffect(() => {    setState(initState);    (async (): Promise<void> => {      try {        // MOCK API CALL        const asyncMockApiFn = async (): Promise<IDog> =>          await new Promise(resolve => setTimeout(() => resolve(DATA), 1000));        const data = await asyncMockApiFn();        setState({          data,          status: Status.loaded,          error: null        });      } catch (error) {        setState({          error,          status: Status.error,          data: null        });      }    })();  }, []);  return (    <DogDataProviderContext.Provider value={state}>      {children}    </DogDataProviderContext.Provider>  );};

Примечательно в этой имплементации:

  1. Сначала мы создаем контекстный объект DogDataProviderContext с помощью React Context API через React.createContext. Это будет использоваться для обеспечения состояния потребляющих компонентов с помощью пользовательского React's хук (hook), который мы применим позже.

  2. Назначив displayName нашему контекстному объекту, мы сможем легко различать компоненты контекста в React Dev Tool. Поэтому вместо Context.Provider в React Dev Tools мы будем использовать DogDataProvider.Provider. Это поможет повысить удобство чтения, если во время отладки мы используем несколько компонентов, использующих Context.

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

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

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

Далее мы создадим наш пользовательский React hook в том же файле, в котором мы создали компонент DogDataProvider. Пользовательский хук (hook) будет предоставлять контекстное состояние от компонента DogDataProvider к потребляющим компонентам.

// src/components/DogDataProvider.tsxexport function useDogProviderState() {  const context = React.useContext(DogDataProviderContext);  if (context === undefined) {    throw new Error('useDogProviderState must be used within DogDataProvider.');  }  return context;}

Пользовательский хук использует [React.useContext](http://personeltest.ru/aways/reactjs.org/docs/hooks-reference.html#usecontext) для получения предоставленного контекстного значения из компонента DogDataProvider, и он вернет состояние контекста, когда мы его вызовем. Выставляя пользовательский хук, компоненты-потребители могут подписаться на состояние, которое управляется в компоненте данных провайдера.

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

Наконец, мы отображаем данные при загрузке в потребляющие компоненты. Сконцентрируемся на компоненте Profile, который загружается по маршруту личного каталога (home path), но вы также можете посмотреть примеры потребительских компонентов в DogFriends и Nav компонентах.

Сначала в файле index.tsx мы должны обернуть компонент DogDataProvider на корневом уровне (root level):

// src/index.tsxfunction App() {  return (    <Router>      <div className="App">        {/* The data provder component responsible         for fetching and managing the data for the child components.        This needs to be at the top level of our component tree.*/}        <DogDataProvider>          <Nav />          <main className="py-5 md:py-20 max-w-screen-xl mx-auto text-center text-white w-full">            <Banner              title={'React Component Patterns:'}              subtitle={'Provider Pattern'}            />            <Switch>              <Route exact path="/">                {/* A child component that will consume the data from                 the data provider component, DogDataProvider. */}                <Profile />              </Route>              <Route path="/friends">                {/* A child component that will consume the data from                 the data provider component, DogDataProvider. */}                <DogFriends />              </Route>            </Switch>          </main>        </DogDataProvider>      </div>    </Router>  );}

Затем в компоненте Profile мы можем использовать пользовательский хук

useDogProviderState:

const Profile = () => {  // Our custom hook that "subscirbes" to the state changes in   // the data provider component, DogDataProvider.  const { data, status, error } = useDogProviderState();  return (    <div>      <h1 className="//...">Profile</h1>      <div className="mt-10">        {/* If the API call returns an error we will show an error message */}        {error ? (          <Error errorMessage={error.message} />          // Show a loading state when we are fetching the data        ) : status === Status.loading ? (          <Loader isInherit={true} />        ) : (          // Display the content with the data           // provided via the custom hook, useDogProviderState.          <ProfileCard data={data} />        )}      </div>    </div>  );};

Следует отметить в этой имплементации:

  1. При получении данных мы покажем состояние загрузки.

  2. Если API запрос вернет ошибку, то мы покажем сообщение об ошибке.

  3. Наконец, после того, как данные будут получены и предоставлены через пользовательский хук, useDogProviderState, мы рендерируем компонент ProfileCard.

Заключение

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

Шаблон Provider с пользовательским примером

Поскольку React хуки были представлены для React v16.8, но если вам нужна поддержка версий ниже v16.8, то здесь представлен тот же пример без хуков: CodeSandBox.


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

- JavaScript Developer. Basic

- JavaScript Developer. Professional

- React.js Developer

Подробнее..

Перевод Что же такого особенного в IAsyncEnumerable в .NET Core 3.0?

10.08.2020 18:20:12 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Разработчик C#.





Одной из наиболее важных функций .NET Core 3.0 и C# 8.0 стал новый IAsyncEnumerable<T> (он же асинхронный поток). Но что в нем такого особенного? Что же мы можем сделать теперь, что было невозможно раньше?

В этой статье мы рассмотрим, какие задачи IAsyncEnumerable<T> предназначен решать, как реализовать его в наших собственных приложениях и почему IAsyncEnumerable<T> заменит Task<IEnumerable<T>> во многих ситуациях.

Ознакомьтесь со всеми новыми функциями .NET Core 3

Жизнь до IAsyncEnumerable<T>


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

Представьте, что мы создаем библиотеку для взаимодействия с данным, и нам нужен метод, который запрашивает некоторые данные из хранилища или API. Обычно этот метод возвращает Task<IEnumerable<T>>, как здесь:

public async Task<IEnumerable<Product>> GetAllProducts()

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

public async Task<IEnumerable<Product>> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    var products = new List<Product>();    while (iterator.HasMoreResults)    {        foreach (var product in await iterator.ReadNextAsync())        {            products.Add(product);        }    }    return products;}

Обратите внимание, что мы пролистываем все результаты в цикле while, создаем экземпляры объектов product, помещаем их в List, и, наконец, возвращаем все целиком. Это довольно неэффективно, особенно на больших наборах данных.

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

public IEnumerable<Task<IEnumerable<Product>>> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    while (iterator.HasMoreResults)    {        yield return iterator.ReadNextAsync().ContinueWith(t =>         {            return (IEnumerable<Product>)t.Result;        });    }}

Вызывающий объект будет использовать метод следующим образом:

foreach (var productsTask in productsRepository.GetAllProducts()){    foreach (var product in await productsTask)    {        Console.WriteLine(product.Name);    }}

Эта реализация более эффективна, но метод теперь возвращает IEnumerable<Task<IEnumerable<Product>>>. Как мы видим из вызывающего кода, вызов метода и обработка данных не интуитивны. Что еще более важно, подкачка страниц это деталь реализации метода доступа к данным, о которой вызывающая сторона не должна ничего знать.

IAsyncEnumerable<T> спешит на помощь


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

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

public IEnumerable<Product> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    while (iterator.HasMoreResults)    {        foreach (var product in iterator.ReadNextAsync().Result)        {            yield return product;        }    }}

Однако, НИКОГДА ТАК НЕ ДЕЛАЙТЕ! Приведенный выше код превращает асинхронный вызов базы данных в блокирующий и не масштабируется.

Если только мы могли бы использовать yield return с асинхронными методами! Это было невозможно до сих пор.

IAsyncEnumerable<T> был представлен в .NET Core 3 (.NET Standard 2.1). Он предоставляет энумератор, у которого есть метод MoveNextAsync(), который может быть ожидаемым. Это означает, что инициатор может совершать асинхронные вызовы во время (посреди) получения результатов.

Вместо возврата Task<IEnumerable<T>> наш метод теперь может возвращать IAsyncEnumerable<T> и использовать yield return для передачи данных.

public async IAsyncEnumerable<Product> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    while (iterator.HasMoreResults)    {        foreach (var product in await iterator.ReadNextAsync())        {            yield return product;        }    }}

Чтобы использовать результаты, нам нужно использовать новый синтаксис await foreach(), доступный в C# 8:

await foreach (var product in productsRepository.GetAllProducts()){    Console.WriteLine(product);}

Это намного приятнее. Метод производит данные по мере их поступления. Код вызова использует данные в своем темпе.

IAsyncEnumerable<T> и ASP.NET Core


Начиная с .NET Core 3 Preview 7, ASP.NET может возвращать IAsyncEnumerable из экшена контроллера API. Это означает, что мы можем возвращать результаты нашего метода напрямую эффективно передавая данные из базы данных в HTTP ответ.

[HttpGet]public IAsyncEnumerable<Product> Get()    => productsRepository.GetAllProducts();

Замена Task<IEnumerable<T>> на IAsyncEnumerable<T>


С течением времени по ходу освоения .NET Core 3 и .NET Standard 2.1, ожидается, что IAsyncEnumerable<T> будет использоваться в местах, где мы обычно использовали Task<IEnumerable>.

Я с нетерпением жду возможности увидеть поддержку IAsyncEnumerable<T> в библиотеках. В этой статье мы видели подобный код для запроса данных с помощью SDK Azure Cosmos DB 3.0:

var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");while (iterator.HasMoreResults){    foreach (var product in await iterator.ReadNextAsync())    {        Console.WriteLine(product.Name);    }}

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

Чтобы посмотреть, как это могло бы выглядеть, если бы GetItemQueryIterator<Product>() вместо этого возвращал IAsyncEnumerable<T>, мы можем создать метод-расширение в FeedIterator:

public static class FeedIteratorExtensions{    public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this FeedIterator<T> iterator)    {        while (iterator.HasMoreResults)        {            foreach(var item in await iterator.ReadNextAsync())            {                yield return item;            }        }    }}

Теперь мы можем обрабатывать результаты наших запросов намного более приятным способом:

var products = container    .GetItemQueryIterator<Product>("SELECT * FROM c")    .ToAsyncEnumerable();await foreach (var product in products){    Console.WriteLine(product.Name);}

Резюме


IAsyncEnumerable<T> является долгожданным дополнением к .NET и во многих случаях сделает код более приятным и эффективным. Узнать об этом больше вы можете на этих ресурсах:




Шаблон проектирования Состояние (state)



Читать ещё:


Подробнее..

Перевод Что происходит, когда вы выполняете manage.py test?

25.10.2020 20:05:49 | Автор: admin

Перевод статьи подготовлен специально для студентов курса Python Web-Developer.


Вы запускаете тесты командой manage.py test, но знаете ли вы, что происходит под капотом при этом? Как работает исполнитель тестов (test runner) и как он расставляет точки, E и F на экране?

Когда вы узнаете, как работает Django, то откроете для себя множество вариантов использования, таких как изменение файлов cookie, установка глобальных заголовков и логирование запросов. Аналогично, поняв то, как работают тесты, вы сможете кастомизировать процессы, чтобы, например, загружать тесты в другом порядке, настраивать параметры тестирования без отдельного файла или блокировать исходящие HTTP-запросы.

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

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

Выходные данные тестов

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

from django.test import TestCaseclass ExampleTests(TestCase):    def test_one(self):        pass

При выполнении тестов мы получаем знакомые выходные данные:

$ python manage.py testCreating test database for alias 'default'...System check identified no issues (0 silenced)..----------------------------------------------------------------------Ran 1 test in 0.001sOKDestroying test database for alias 'default'...

Чтобы понять, что происходит, попросим программу рассказать об этом подробнее, добавив флаг -v 3:

$ python manage.py test -v 3Creating test database for alias 'default' ('file:memorydb_default?mode=memory&cache=shared')...Operations to perform:  Synchronize unmigrated apps: core  Apply all migrations: (none)Synchronizing apps without migrations:  Creating tables...    Running deferred SQL...Running migrations:  No migrations to apply.System check identified no issues (0 silenced).test_one (example.core.tests.test_example.ExampleTests) ... ok----------------------------------------------------------------------Ran 1 test in 0.004sOKDestroying test database for alias 'default' ('file:memorydb_default?mode=memory&cache=shared')...

Отлично, этого достаточно! Теперь давайте разбираться.

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

В этом проекте я использую SQLite, поэтому Django автоматически ставит mode=memory в поле адреса базы данных. Так операции с базой данных станут быстрее примерно раз в 10. Другие базы данных, такие как PostgreSQL, не имеют подобных режимов, но для них есть другие методы запуска in-memory.

Вторая строка Operations to perform и несколько последующих строк это выходные данные команды migrate в тестовых базах данных. То есть вывод тут получается идентичным с тем, который мы получаем при выполнении manage.py migrate на пустой базе данных. Сейчас я использую небольшой проект без миграций, но если бы они были, то на каждую миграцию в выводе приходилась бы одна строка.

Дальше идет строка с надписью System check identified no issues. Он взялась из Django, который запускает ряд проверок перед полетом, чтобы убедиться в правильной конфигурации вашего проекта. Вы можете запустить проверку отдельно с помощью команды manage.py check, и также она выполнится автоматически с запуском большинства команд управления. Однако в случае с тестами, она будет отложена до тех пор, пока тестовые базы данных не будут готовы, так как некоторые этапы проверки используют соединения баз данных.

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

Следующие строки про наши тесты. По умолчанию test runner выводит по одному символу на тест, но с повышением verbosity в Django для каждого теста будет выводиться отдельная строка. Здесь у нас есть всего один тест testone, и когда он закончил выполнение, test runner добавил к строке ok.

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

Последняя строка сообщает нам, что тестовая база данных была удалена.

В итоге у нас получается следующая последовательность шагов:

  1. Создание тестовых баз данных.

  2. Миграция баз данных.

  3. Запуск проверок системы.

  4. Запуск тестов.

  5. Отчет о количестве тестов и успешном/неуспешном завершении.

  6. Удаление тестовых баз данных.

Давайте разберемся, какие компоненты внутри Django отвечают за эти шаги.

Django и unittest

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

Мы можем найти компоненты каждой стороны взглянув на код.

Команда управления тестами test

Первое, на что нужно посмотреть, это команда управления тестами, которую Django находит и выполняет при запуске manage.py test. Находится она в django.core.management.commands.test.

Что касается команд управления, то они довольно короткие меньше 100 строк. Метод handle() отвечает за обработку в TestRunner. Если упрощать до трех основных строк:

def handle(self, *test_labels, **options):    TestRunner = get_runner(settings, options['testrunner'])    ...    test_runner = TestRunner(**options)    ...    failures = test_runner.run_tests(test_labels)    ...

Полный код.

Так что же представляет из себя класс TestRunner? Это компонент Django, который координирует процесс тестирования. Он настраиваемый, но класс по умолчанию, и единственный в самом Django это django.test.runner.DiscoverRunner. Давайте рассмотрим его следующим.

Класс DiscoverRunner

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

Начинается он как-то так:

class DiscoverRunner:    """A Django test runner that uses unittest2 test discovery."""    test_suite = unittest.TestSuite    parallel_test_suite = ParallelTestSuite    test_runner = unittest.TextTestRunner    test_loader = unittest.defaultTestLoader

(Документация, исходный код)

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

Обратите внимание на то, что один из них называется test_runner, таким образом у нас получается два различных понятия, который называются test runner это DiscoverRunner из Django и TextTestRunner из unittest. DiscoverRunner делает гораздо больше, чем TextTestRunner, и у него другой интерфейс. Возможно, в Django можно было бы обозвать DiscoverRunner по-другому, например, TestCoordinator, но сейчас об этом уже поздно думать.

Основной поток в DiscoverRunner находится в методе runtests(). Если убрать кучу деталей, run_tests() будет выглядеть примерно так:

def run_tests(self, test_labels, extra_tests=None, **kwargs):    self.setup_test_environment()    suite = self.build_suite(test_labels, extra_tests)    databases = self.get_databases(suite)    old_config = self.setup_databases(aliases=databases)    self.run_checks(databases)    result = self.run_suite(suite)    self.teardown_databases(old_config)    self.teardown_test_environment()    return self.suite_result(suite, result)

Шагов здесь совсем немного. Многие из методов соответствуют шагам из списка, который мы приводили выше:

  • setup_databases() создает тестовые базы данных. Но этот метод создает только те базы данных, которые необходимы для выбранных тестов, отфильтрованных с помощью get_databases(), поэтому если вы запускаете только SimpleTestCases без баз данных, то Django ничего создавать не будет. Внутри этого метода создаются базы данных и выполняется команда migrate.

  • run_checks() запускает проверки.

  • run_suite() запускает набор тестов, включая все выходные данные.

  • Функция teardown_databases() удаляет тестовые базы данных.

И еще парочка методов, о которых можно рассказать:

  • setup_test_environment() и teardown_test_environment() устанавливают или убирают некоторые настройки, такие как локальный сервер электронной почты.

  • suite_result() возвращает количество ошибок в ответ команде управления тестированием.

Все эти методы полезно рассмотреть, чтобы разобраться с настройками процесса тестирования. Но они все являются частью Django. Другие методы передаются компонентам в unittest - build_suite() и run_suite().

Давайте поговорим и о них.

buildsuite()

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

def build_suite(self, test_labels=None, extra_tests=None, **kwargs):    suite = self.test_suite()    test_labels = test_labels or ['.']    for label in test_labels:        tests = self.test_loader.loadTestsFromName(label)        suite.addTests(tests)    if self.parallel > 1:        suite = self.parallel_test_suite(suite, self.parallel, self.failfast)    return suite

В этом методе используются три из четырех классов, к которым, как мы видели, обращается DiscoverRunner:

  • test_suite - компонент unittest, который служит контейнером для запуска тестов.

  • parallel_test_suite - оболочка для набора тестов, которая используется с функцией параллельного тестирования в Django.

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

runsuite()

Еще один метод DiscoverRunner, о котором надо поговорить это run_suite(). Его мы упрощать не будем, и просто посмотрим, как он выглядит:

def run_suite(self, suite, **kwargs):    kwargs = self.get_test_runner_kwargs()    runner = self.test_runner(**kwargs)    return runner.run(suite)

Его единственная задача создавать test runner и говорить ему запустить собранный набор тестов. Это последний из компонентов unittest, на который ссылается атрибут класса. Он использует unittest.TextTestRunner - test runner по умолчанию для вывода результатов в виде текста, в отличие, например, от XML-файла для передачи результатов в вашу CI-систему.

Закончим мы наше небольшое расследование, заглянув в класс TextTestRunner.

Класс TextTestRunner

Этот компонент unittest берет тест-кейс или набор и выполняет его. Начинается он вот так:

class TextTestRunner(object):    """A test runner class that displays results in textual form.    It prints out the names of tests as they are run, errors as they    occur, and a summary of the results at the end of the test run.    """    resultclass = TextTestResult    def __init__(self, ..., resultclass=None, ...):

(Исходный код)

По аналогии с DiscoverRunner, он использует атрибут класса для ссылки на другой класс. Класс TextTestResult по умолчанию отвечает за текстовый вывод. В отличие от ссылок класса DiscoverRunner, мы можем переопределить resultclass, передав альтернативу TextTestRunner._init_().

Теперь мы наконец-то можем кастомизировать процесс тестирования. Но сначала вернемся к нашему маленькому исследованию.

Карта

Теперь мы можем расширить карту и показать на ней классы, которые мы нашли:

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

Как кастомизировать

Django предлагает два способа кастомизации процесса выполнения тестов:

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

Супербыстрый Test Runner

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

# example/test.pyfrom django.test.runner import DiscoverRunnerclass SuperFastTestRunner(DiscoverRunner):    def run_tests(self, *args, **kwargs):        print("All tests passed! A+")        failures = 0        return failures

Затем воспользуемся им в файле с настройками следующим образом:

TEST_RUNNER = "example.test.SuperFastTestRunner"

А затем выполним manage.py test, и получим результат за рекордно короткое время!

$ python manage.py testAll tests passed! A+

Отлично, очень полезно!

А теперь давайте сделаем еще практичнее, и перейдем к выводу результатов теста в виде эмодзи!

Вывод в виде эмодзи

Мы уже выяснили, что компонент TextTestResult из unittest отвечает за вывод. Мы можем заменить его в DiscoverRunner, передав значение resultclass в TextTestRunner.

В Django уже есть опции для замены resultclass, например, опция --debug-sql option, которая выводит выполненные запросы для неудачных тестов.

DiscoverRunner.run_suite() создает TextTestRunner с аргументами из метода DiscoverRunner.get_test_runner_kwargs():

<img alt="Изображение выглядит как текст

def get_test_runner_kwargs(self):    return {        'failfast': self.failfast,        'resultclass': self.get_resultclass(),        'verbosity': self.verbosity,        'buffer': self.buffer,    }

Он же в свою очередь вызывает get_resultclass(), который возвращает другой класс, если был использован один из двух параметров тестовой команды (--debug-sql или --pdb):

def get_resultclass(self):    if self.debug_sql:        return DebugSQLTextTestResult    elif self.pdb:        return PDBDebugResult

Если ни один из параметров не задан, метод неявно возвращает None, говоря TextTestResult использовать по умолчанию resultclass. Мы можем увидеть этот None в нашем собственном подклассе и заменить его подклассом TextTestResult:

class EmojiTestRunner(DiscoverRunner):    def get_resultclass(self):        klass = super().get_resultclass()        if klass is None:            return EmojiTestResult        return klass

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

class EmojiTestResult(unittest.TextTestResult):    def __init__(self, *args, **kwargs):        super().__init__(*args, **kwargs)        # If the "dots" style was going to be used, show emoji instead        self.emojis = self.dots        self.dots = False    def addSuccess(self, test):        super().addSuccess(test)        if self.emojis:            self.stream.write('')            self.stream.flush()    def addError(self, test, err):        super().addError(test, err)        if self.emojis:            self.stream.write('?')            self.stream.flush()    def addFailure(self, test, err):        super().addFailure(test, err)        if self.emojis:            self.stream.write('')            self.stream.flush()    def addSkip(self, test, reason):        super().addSkip(test, reason)        if self.emojis:            self.stream.write("")            self.stream.flush()    def addExpectedFailure(self, test, err):        super().addExpectedFailure(test, err)        if self.emojis:            self.stream.write("")            self.stream.flush()    def addUnexpectedSuccess(self, test):        super().addUnexpectedSuccess(test)        if self.emojis:            self.stream.write("")            self.stream.flush()    def printErrors(self):        if self.emojis:            self.stream.writeln()        super().printErrors()

После указания TEST_RUNNER в EmojiTestRunner, мы можем запустить тесты и увидеть эмодзи:

$ python manage.py testCreating test database for alias 'default'...System check identified no issues (0 silenced).?...----------------------------------------------------------------------Ran 8 tests in 0.003sFAILED (failures=1, errors=1, skipped=1, expected failures=1, unexpected successes=1)Destroying test database for alias 'default'...

Урааа!

Вместо заключения

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

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

Я знаком лишь с двумя библиотеками, предоставляющими кастомные подклассы DiscoverRunner:

  • unittest-xml-reporting обеспечивает вывод в формате XML для вашей CI-системы.

  • django-slow-tests обеспечивает измерение времени выполнения тесты для поиска самых медленных тестов.

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

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

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

Конец

Спасибо, что отправились со мной в это путешествие. Я надеюсь, что вы узнали что-то новое о том, как Django запускает ваши тесты, и научились кастомизировать их.

Читать ещё:

Подробнее..

Категории

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

  • Имя: Макс
    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