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

Javascript

Перевод Введение в React, которого нам не хватало

21.09.2020 16:09:27 | Автор: admin
React это самая популярная в мире JavaScript-библиотека. Но эта библиотека не потому хороша, что популярна, а потому популярна, что хороша. Большинство существующих вводных руководств по React начинается с примеров того, как пользоваться этой библиотекой. Но эти руководства ничего не говорят о том, почему стоит выбрать именно React.

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



Этот материал (вот, если интересно, его видеоверсия) написан для тех, кто хочет найти ответ на следующие вопросы: Почему React? Почему React работает именно так? С какой целью API React устроены так, как устроены?.

Почему React?


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

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

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

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

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

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

Отсутствие детерминизма = параллельные вычисления + мутабельное состояние.

Мартин Одерски


Главной задачей команды разработки React было решение этой проблемы. Они с ней справились, применив два основных инновационных подхода:

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

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

Том Оччино, JSConfUS 2013


Библиотека React смогла серьёзно снизить остроту проблемы неконтролируемых мутаций благодаря использованию архитектуры Flux. Вместо того чтобы присоединять к произвольному количеству произвольных объектов (моделей) обработчики событий, вызывающие обновления DOM, библиотека React дала разработчикам единственный способ управления состоянием компонента. Это диспетчеризация действий, влияющих на хранилище данных. Когда меняется состояние хранилища, система предлагает компоненту перерендериться.


Архитектура Flux

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

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

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

JSX


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

  • Применение простой декларативной разметки.
  • Код разметки расположен там же, где и код компонента.
  • Реализация принципа разделения ответственностей (например отделение описания интерфейса от логики состояния и от побочных эффектов). Причём, реализация, основанная не на использовании различных технологий (например HTML, CSS, JavaScript).
  • Абстрагирование управления изменениями DOM.
  • Абстрагирование от особенностей различных платформ, для которых создают React-приложения. Дело в том, что благодаря использованию React можно создавать приложения, предназначенные для множества платформ (речь идёт, например, о разработке для мобильных устройств с использованием React Native, о приложениях для систем виртуальной реальности, о разработке для Netflix Gibbon, о создании Canvas/WebGL-интерфейсов, о проекте react-html-email).

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

В наши дни, если взглянуть на разные фронтенд-инструменты, окажется, что без специального синтаксиса, вроде директивы *ngFor из Angular, тоже не обойтись. Но, так как JSX можно назвать надмножеством JavaScript, создавая JSX-разметку можно пользоваться существующими возможностями JS.

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

Вот пример JSX-кода:

const ItemList = ({ items }) => (<ul>{items.map((item) => (<li key={item.id}><div>{item.name}</div></li>))}</ul>);

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

  • Тут используется подход к именованию атрибутов элементов, отличающийся от того, который принят в HTML. Например, class превращается в className. Речь идёт о применении стиля именования camelCase.
  • У каждого элемента списка, который нужно вывести, должен быть постоянный уникальный идентификатор, предназначенный для использования в JSX-атрибуте key. Значение идентификатора должно оставаться неизменным в ходе различных манипуляций с элементами списка. На практике большинство элементов списков в моделях данных имеют уникальные id, эти идентификаторы обычно отлично показывают себя в роли значений для key.

React не навязывает разработчику единственно правильный способ работы с CSS. Например, компоненту можно передать JavaScript-объект со стилями, записав его в свойство style. При таком подходе большинство привычных имён стилей будет заменено на их эквиваленты, записанные по правилам camelCase. Но этим возможности по работе со стилями не ограничиваются. На практике я одновременно пользуюсь разными подходами к стилизации React-приложений. Выбор конкретного подхода зависит от того, что именно нужно стилизовать. Например, глобальные стили я применяю для оформления тем приложений и макетов страниц, а локальные стили для настройки внешнего вида конкретного компонента.

Вот мои любимые возможности React, касающиеся работы со стилями:

  • CSS-файлы, которые можно загружать в заголовочной части страницы. Они могут использоваться для настройки макетов страниц, шрифтов и прочих подобных элементов. Это надёжный, работоспособный механизм стилизации.
  • CSS-модули это CSS-файлы область применения которых ограничена локальной областью видимости. Их можно импортировать непосредственно в JavaScript-файлы. Для того чтобы применять CSS-модули, нужно воспользоваться правильно настроенным загрузчиком модулей. В Next.js, например, этот механизм активирован по умолчанию.
  • Пакет styled-jsx, который позволяет объявлять стили прямо в коде React-компонентов. Это напоминает использование тега <style> в HTML. Область видимости таких стилей можно назвать гиперлокальной. Речь идёт о том, что стили воздействуют только на элементы, к которым они применяются, и на их дочерние элементы. При применении Next.js пакетом styled-jsx можно пользоваться без необходимости самостоятельно что-то подключать и настраивать.

Синтетические события


React даёт в наше распоряжение кроссбраузерную обёртку SyntheticEvents, представляющую синтетические события и предназначенную для унификации работы с событиями DOM. Синтетические события весьма полезны по нескольким причинам:

  1. Они позволяет унифицировать особенности различных платформ, связанные с обработкой событий. Это упрощает разработку кроссбраузерных приложений.
  2. Они автоматически решают задачи по управлению памятью. Если вы, например, собираетесь создать некий список с бесконечной прокруткой, пользуясь лишь чистыми JavaScript и HTML, то вам придётся делегировать события или подключать и отключать обработчики событий по мере появления и скрытия элементов списка. Всё это нужно будет делать для того чтобы избежать утечек памяти. Синтетические события автоматически делегируются корневому узлу, что приводит к тому, что React-разработчикам не приходится решать задачи по управлению памятью.
  3. В их работе используются пулы объектов. Механизмы поддержки синтетических событий способны генерировать тысячи объектов в секунду и организовывать высокопроизводительную работу с такими объектами. Если решать подобные задачи, каждый раз создавая новые объекты, это приведёт к частой потребности в вызове сборщика мусора. А это, в свою очередь, может привести к замедлению программы, к видимым задержкам в работе пользовательского интерфейса и анимаций. Объекты синтетических событий создаются заранее и помещаются в пул объектов. Когда надобности в событии нет, оно возвращается обратно в пул. В результате разработчик может не беспокоиться о том, что сборщик мусора заблокирует главный поток JavaScript, очищая память от ставших ненужными объектов.

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

Жизненный цикл компонента


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

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

В React, начиная с версии 0.14, появился синтаксис описаний компонентов, основанных на классах, позволяющий обрабатывать события жизненного цикла компонентов. В жизненном цикле React-компонентов можно выделить три важнейших этапа: Mount (монтирование), Update (обновление) и Unmount (размонтирование).


Жизненный цикл компонента

Этап Update можно разделить на три части: Render (рендеринг), Precommit (подготовка к внесению изменений в дерево DOM), Commit (внесение изменений в дерево DOM).


Структура этапа Update

Остановимся на этих этапах жизненного цикла компонента подробнее:

  • Render на этом этапе жизненного цикла компонента производится его рендеринг. Метод компонента render() должен представлять собой детерминированную функцию, не имеющую побочных эффектов. Эту функцию стоит рассматривать как чистую функцию, получающую данные из входных параметров компонента и возвращающую JSX.
  • Precommit на этом этапе можно прочитать данные из DOM, пользуясь методом жизненного цикла компонента getSnapShotBeforeUpdate. Это может оказаться очень кстати, например, если перед повторным рендерингом компонента нужно узнать нечто вроде позиции скроллинга или размеров визуализированного элемента.
  • Commit на этой фазе жизненного цикла компонента React обновляет DOM и рефы. Здесь можно воспользоваться методом componentDidUpdate или хуком useEffect. Именно здесь можно выполнять эффекты, планировать обновления, использовать DOM и решать другие подобные задачи.

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


Жизненный цикл React-компонентов

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

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

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

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

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

Хуки React


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

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

Хук useEffect позволяет ставить побочные эффекты в очередь для их последующего выполнения. Они будут вызываться в подходящее время жизненного цикла компонента. Это время может настать сразу после монтирования компонента (например при вызове метода жизненного цикла componentDidMount), во время фазы Commit (метод componentDidUpdate), непосредственно перед размонтированием компонента (componentWillUnmount).

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

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

Вот что дают нам хуки React:

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

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

Компоненты-контейнеры и презентационные компоненты


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

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

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

Презентационные компоненты


Рассмотрим особенности презентационных компонентов:

  • Они не взаимодействуют с сетевыми ресурсами.
  • Они не сохраняют данные в localStorage и не загружают их оттуда.
  • Они не выдают неких непредсказуемых данных.
  • Они не обращаются напрямую к текущему системному времени (например, путём вызова метода Date.now()).
  • Они не взаимодействуют напрямую с хранилищем состояния приложения.
  • Они могут использовать локальное состояние компонента для хранения чего-то наподобие данных, введённых в формы, но при этом они должны поддерживать возможность приёма таких данных и задания на их основе своего исходного состояния, что облегчает их тестирование.

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

Лучшее враг хорошего.

Вольтер


Компоненты-контейнеры


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

Компоненты высшего порядка


Компонент высшего порядка (Higher Order Component, HOC) это компонент, который принимает другие компоненты и возвращает новый компонент, реализующий новый функционал, основанный на исходных компонентах.

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

В отличие от хуков React и от механизма render props, компоненты высшего порядка поддаются композиции с использованием стандартного подхода к композиции функций. Это позволяет декларативно описывать результаты композиции возможностей, предназначенных для использования в разных местах приложения. При этом готовые компоненты не должны знать о существовании тех или иных возможностей. Вот пример HOC с EricElliottJS.com:

import { compose } from 'lodash/fp';import withFeatures from './with-features';import withEnv from './with-env';import withLoader from './with-loader';import withCoupon from './with-coupon';import withLayout from './with-layout';import withAuth from './with-auth';import { withRouter } from 'next/router';import withMagicLink from '../features/ethereum-authentication/with-magic-link';export default compose(withEnv,withAuth,withLoader,withLayout({ showFooter: true }),withFeatures,withRouter,withCoupon,withMagicLink,);

Тут показана смесь множества возможностей, совместно используемых всеми страницами сайта. А именно, withEnv читает настройки из переменных окружения, withAuth реализует механизм GitHub-аутентификации, withLoader показывает анимацию во время загрузки данных пользователя, withLayout({ showFooter: true }) выводит стандартный макет с подвалом, withFeature показывает настройки, withRouter загружает маршрутизатор, withCoupon отвечает за работу с купонами, а withMagicLing поддерживает аутентификацию пользователей без пароля с использованием Magic.

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

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

import LessonPage from '../features/lesson-pages/lesson-page.js';import pageHOC from '../hocs/page-hoc.js';export default pageHOC(LessonPage);

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

import FeatureProvider from '../providers/feature-provider';import EnvProvider from '../providers/env-provider';import LoaderProvider from '../providers/loader-provider';import CouponProvider from '../providers/coupon-provider';import LayoutProvider from '../providers/layout-provider';import AuthProvider from '../providers/auth-provider';import RouterProvider from '../providers/RouterProvider';import MagicLinkProvider from '../providers/magic-link-provider';import PageComponent from './page-container';const WrappedComponent = (...props) => (<EnvProvider { ...props }><AuthProvider><LoaderProvider><LayoutProvider showFooter={ true }><FeatureProvider><RouterProvider><CouponProvider><MagicLinkProvider><YourPageComponent /></MagicLinkProvider></CouponProvider></RouterProvider></FeatureProvider></LayoutProvider></LoaderProvider></AuthProvider></EnvProvider>);

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

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

Итоги


  • Почему React? React даёт нам детерминированный рендеринг визуальных представлений компонентов, в основе которого лежит однонаправленная привязка данных и иммутабельное состояние компонентов.
  • JSX даёт нам возможность простого декларативного описания интерфейсов в JavaScript-коде.
  • Синтетические события сглаживают кросс-платформенные различия систем обработки событий и облегчают управление памятью.
  • Концепция жизненного цикла компонентов направлена на защиту состояния компонентов. Жизненный цикл компонента состоит из фаз монтирования, обновления и размонтирования. Фаза обновления состоит из фазы рендеринга, фазы подготовки к внесению изменений в DOM и фазы внесения изменений в DOM.
  • Хуки React позволяют подключаться к методам жизненного цикла компонентов без использования синтаксиса, основанного на классах. Применение хуков, кроме того, облегчает совместное использование одного и того же кода в разных компонентах.
  • Компоненты-контейнеры и презентационные компоненты позволяют отделить задачи формирования визуального представления интерфейсов от задач по управлению состоянием приложения и от побочных эффектов. Это улучшает возможности по многократному использованию и тестированию компонентов и бизнес-логики приложения.
  • Компоненты высшего порядка упрощают совместное использование возможностей, представляющих собой композицию других возможностей. При этом компонентам не нужно знать об этих возможностях (и не нужно, чтобы компоненты были бы тесно связаны с ними).

Что дальше?


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

Я рекомендую использовать React совместно с Redux, Redux-Saga и RITEway. Redux рекомендуется использовать совместно с Autodux и Immer. Для организации сложных схем работы с состоянием можно попробовать воспользоваться Redux-DSM.

Когда вы разберётесь с основами и будете готовы к созданию реальных React-приложений, обратите внимание на Next.js и Vercel. Эти инструменты помогут автоматизировать настройку системы сборки проекта и CI/CD-конвейера, с их помощью можно подготовить проект к оптимизированному развёртыванию на сервере. Они дают тот же эффект, что и целая команда DevOps-специалистов, но пользоваться ими можно совершенно бесплатно.

Какие вспомогательные инструменты вы применяете при разработке React-приложений?



Подробнее..

Перевод Первое знакомство с Moon.js

24.09.2020 16:22:36 | Автор: admin
Сегодня речь пойдёт об очередной JavaScript-библиотеке, предназначенной для разработки интерфейсов. Возникает такое ощущение, что такие библиотеки появляются всё чаще и чаще. В этом материале мы рассмотрим библиотеку Moon.js и раскроем её особенности, о которых нужно знать для того чтобы приступить к работе с ней. В частности, мы поговорим о том, как создавать новые Moon.js-проекты, о том, как создавать элементы интерфейсов, как обрабатывать события. Освоив это руководство, вы сможете пользоваться Moon.js для разработки собственных приложений.



Библиотека Moon.js


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

В Moon.js используется подход к проектированию интерфейсов, основанный на компонентах. Для создания компонентов применяются шаблоны. Эта библиотека весьма похожа на Vue.js.

Сильные стороны Moon.js


  • Moon.js отличается компактными размерами (в минифицированном и сжатом виде это около 2 Кб). Это меньше, чем размеры других библиотек и фреймворков вроде React и Angular.
  • Эта библиотека отличается высокой скоростью рендеринга интерфейсов.
  • Moon.js это библиотека, основанная на функциональных методах разработки. При работе с ней используется подход к проектированию интерфейсов, основанный на так называемых драйверах.

Начало работы


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

Если решено воспользоваться NPM-вариантом библиотеки, то сначала нужно будет установить пакет moon-cli, инструмент командной строки:

$ npm i moon-cli -g

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

Для создания проекта, основанного на Moon.js, можно выполнить следующую команду:

$ moon create moon-prj

Эта команда создаёт новый проект в папке moon-prj. После того, как будет завершено создание проекта, в вашем распоряжении окажется основа будущего приложения.

Второй вариант использования Moon.js предусматривает её подключение к странице, на которой её планируется использовать. У библиотеки есть модуль moon-browser, который позволяет пользоваться её возможностями непосредственно на странице, к которой она подключена.

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

<script src="http://personeltest.ru/aways/unpkg.com/moon"></script><script src="http://personeltest.ru/aways/unpkg.com/moon-browser"></script>

Как видите, соответствующие скрипты загружаются с CDN unpkg. В первом теге импортируется основная библиотека. Во втором библиотека moon-browser. Она отвечает за компиляцию шаблонов Moon.js, за приведение их к виду, пригодному для вывода браузером.

Теперь, для того чтобы воспользоваться синтаксическими конструкциями Moon.js на странице, нужно будет включить их в тег <script>, не забыв задать его атрибут type как text/moon.

<!-- Подключение к странице внешнего скрипта --><script src="./main-script.js" type="text/moon"></script><!-- Описание интерфейса в коде, встроенном в страницу --><script type="text/moon">...</script>

Подключение Moon.js-приложения к странице


Moon.js, как и другие библиотеки и фреймворки, используемые для создания одностраничных приложений, подключается к определённому элементу страницы. Обычно роль контейнера для Moon.js-приложения играет элемент <div>:

<div id="root"></div>

Подобный элемент, являющийся корневым элементом Moon.js-приложения, размещают в коде файла index.html, представляющего собой точку входа в проект.

Для подключения Moon.js-приложения к этому элементу используется драйвер view (ниже мы поговорим о драйверах подробнее):

Moon.use({view: Moon.view.driver("#root")})

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

Moon.use({view: Moon.view.driver(document.getElementById("root"))})

Теперь поговорим о том, как в Moon.js организована работа с данными, и о том, как с помощью этой библиотеки создавать элементы интерфейсов.

Синтаксис описания элементов интерфейса


Для описания Moon.js-интерфейсов используется язык программирования Moon View Language (MVL), который был разработан специально для решения данной задачи. Он напоминает JSX. Этот язык применяется для описания элементов и для настройки их взаимоотношений. Вот пример:

<script type="text/moon">function aView(data) {return (<div>Hi from Moon</div>)}</script>

Несложно заметить то, что в этом фрагменте Moon.js-кода, ответственного за формирование элемента <div>, используются синтаксические структуры, напоминающие HTML. Но эти структуры используются в JavaScript-коде. Такой JavaScript-код браузер выполнить не сможет, но это от него и не требуется, так как Moon.js компилирует подобные конструкции в обычный JavaScript.

Работа с данными


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

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

Задать начальные данные Moon.js-приложения можно с помощью API Moon.use:

Moon.use({data: Moon.data.driver})

Записывать новые данные в состояния можно, возвращая их из соответствующих функций:

Moon.run(({ data }) => {console.log(data) // undefinedreturn {data: "Nnamdi"}})

API Moon.run отвечает за запуск приложения. Коллбэк, переданный этому API, получает ссылку на глобальные данные в аргументе data. Так как на момент вызова этой функции в data пока ничего нет, команда console.log из этого примера выведет undefined.

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

Механизм работы с данными в Moon.js мы рассмотрели. Теперь подробнее поговорим о работе с элементами интерфейса.

Работа с элементами интерфейса


В Moon.js имеется драйвер view, который предназначен для создания элементов и для монтирования их в DOM.

Мы уже рассматривали фрагмент кода, повторённый ниже, в котором к элементу <div> подключается базовый элемент Moon.js:

Moon.use({view: Moon.view.driver("#root")})

Именно тут производится монтирование элемента. Теперь функции могут возвращать элементы, которые способны заменять старые элементы. Их можно представлять в виде объектов, содержащих свойство view, в которое записаны соответствующие данные. Библиотека берёт значение свойства view из возвращаемого функцией объекта и записывает его в элемент, подключённый к элементу с идентификатором root.

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

function handleClick() {return {};}Moon.run(() => ({view: <button @click=handleClick>Click Me!</button>}));

Здесь коллбэк, передаваемый Moon.run, выводит в DOM кнопку. Происходит это из-за того, что функция возвращает объект со свойством view. Значение, назначенное этому свойству, попадает в DOM.

У кнопки имеется обработчик события click, представленный функцией handleClick. Эта функция возвращает пустой объект, её вызов не приводит к внесению изменений в DOM.

Создание элементов


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

const { div, text, node, p } = Moon.view.m

Moon.js экспортирует функции, имена которых соответствуют именам создаваемых с их помощью элементов. Так, функция div позволяет создавать элементы <div>. Функция text создаёт текстовые узлы. Функция node позволяет создавать пользовательские элементы. Функция p создаёт элементы <p>. Как видите, имена этих функций ясно указывают на их предназначение.

Создадим элемент <div>:

const Div = div({});

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

const Div = div({class: "DivClass"});

Здесь мы описали элемент <div>, в атрибут class которого должно быть записано значение DivClass.

Вот как создать текстовый элемент:

const Text = text({ data: "A text node" });

В свойстве data объекта, переданного функции text, имеется текст для элемента.

Создадим пользовательский элемент:

const CustomEl = node("custom-el");

Для того чтобы задать этому элементу какой-нибудь атрибут, можно поступить так:

CustomEl({ "attr": "attr-value"})

События


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

function handleClick() {return {};}Moon.run(() => ({view: <button @click=handleClick>Click Me!</button>}));

В результате на страницу будет выведена кнопка с текстом Click Me, по нажатию на которую будет вызвана функция handleClick.

Компоненты


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

Предположим, у нас есть такая функция:

function aView({ data }) {return <div>A View</div>}

Эта функция, aView, возвращает элемент, который может быть отрендерен:

Moon.run(() => {view: <div><aView /></div>})

Имя функции в этом примере используется в роли имени элемента. В результате выполнения этого кода окажется, что то, что возвращает функция, будет помещено в тег <div>. Когда всё это попадёт в DOM, там окажется такая разметка:

<div><div>A View</div></div>

Разработка приложений, основанных на Moon.js


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

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

Вот как выглядит страница этого приложения.


Страница приложения

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

Начнём работу с создания файла index.html. Здесь мы подключим Moon.js непосредственно к странице:

<html><body><div id="root"></div></body><script src="http://personeltest.ru/aways/unpkg.com/moon"></script><script src="http://personeltest.ru/aways/unpkg.com/moon-browser"></script><!-- Воспользуемся скриптом, встроенным в страницу --><script type="text/moon">function viewTodos({data, view}) {return (<div><input type="text" value=data.todo @input=updateTodo/><button @click=createTodo>Create</button><ul children=(data.todos.map(todo =><li>{todo}</li>))/></div>)}function updateTodo({ data, view }) {const dataNew = { ...data, todo: view.target.value };return {data: dataNew,view: <viewTodos data=dataNew/>}}function createTodo({ data }) {const dataNew = {todo: "",todos: [...data.todos, data.todo]};return {data: dataNew,view: <viewTodos data=dataNew/>}}<!-- Настройка драйверов data и view -->Moon.use({data: Moon.data.driver,view: Moon.view.driver("#root")})<!-- Запуск приложения -->Moon.run(() => {data: [],view: <viewTodos data=[]>})</script></html>

Функция viewTodos выводит элементы, необходимые для ввода сведений о новых делах и для вывода их в виде списка. Её аргументами являются data и view.

Функция createTodo создаёт новое дело и возвращает его в свойстве data возвращаемого ей объекта.

Функция updateTodo записывает новое дело в состояние приложения.

Обратите внимание на обработчики событий @click и @input, которые имеются в функции viewTodos. Событие @input вызывается при вводе текста, описывающего дело, в соответствующее поле. При обработке этого события вызывается функция updateTodo. Аргумент view в этой функции представляет произошедшее событие. Пользуясь им, мы обращаемся к DOM и получаем данные, введённые в поле. Затем эти данные попадают в состояние в виде свойства todo.

Событие @click вызывается после нажатия на кнопку. Оно выполняет запись нового дела в список дел. Для решения этой задачи используется функция createTodo. Эта функция обращается к свойству состояния todo и записывает новые данные в свойство todos, после чего, в свойстве view возвращаемого ей объекта, возвращает элемент <viewTodos>, представленный соответствующей функцией, в атрибут data которого записано значение dataNew.

Это приведёт к выполнению повторного рендеринга viewTodos и к обновлению DOM. В список дел, выводимый на странице, будет добавлено новое дело.

Запустите это приложение в браузере и поэкспериментируйте с ним.

Итоги


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

Мне кажется, что Moon.js это приятная библиотека. И, если даже не говорить о других её достоинствах, она мне симпатична из-за её компактного размера.

Пользовались ли вы Moon.js?



Подробнее..

Обрабатываем данные на стороне клиента с помощью WebAssembly

28.09.2020 14:11:21 | Автор: admin


WebAssembly (сокр. WASM) технология запуска предварительно скомпилированного бинарного кода в браузере на стороне клиента. Впервые была представлена в 2015 году и на текущий момент поддерживается большинством современных браузеров.

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

Перед началом


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

Работа с WebAssembly начинается с предварительной сборки артефактов, необходимых для запуска скомпилированного кода на стороне клиента. Их два: собственно сам бинарный WASM файл и JavaScript прослойка, через которую можно вызывать экспортированные в него методы.

Пример простейшего кода на C++ для компиляции

#include <algorithm>extern "C" {int calculate_gcd(int a, int b) {  while (a != 0 && b != 0) {    a %= b;    std::swap(a, b);  }  return a + b;}}

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

em++ main.cpp --std=c++17 -o gcd.html \    -s EXPORTED_FUNCTIONS='["_calculate_gcd"]' \    -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'

Указанием в качестве объекта *.html файла подсказывает компилятору, что нужно создать также простую html-разметку с js консолью. Теперь если запустить сервер на полученных файлах, увидим эту консоль с возможностью запуска _calculate_gcd:



Обработка данных


Разберем ее на простом примере lz4-компрессии с помощью библиотеки, написанной на C++. Замечу, что на этом множество поддерживаемых языков не заканчивается.

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

Весь код целиком можно найти тут.

С++ часть


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

#include "lz4.h"extern "C" {uint32_t compress_data(uint32_t* data, uint32_t data_size, uint32_t* result) {  uint32_t result_size = LZ4_compress(        (const char *)(data), (char*)(result), data_size);  return result_size;}uint32_t decompress_data(uint32_t* data, uint32_t data_size, uint32_t* result, uint32_t max_output_size) {  uint32_t result_size = LZ4_uncompress_unknownOutputSize(        (const char *)(data), (char*)(result), data_size, max_output_size);  return result_size;}}

Как можно видеть, в нем просто объявлены внешние (используя ключевое слово extern) функции, внутри вызывающие соответствующие методы из библиотеки с lz4.

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

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

em++ main.cpp lz4.c -o wasm_compressor.js \    -s EXPORTED_FUNCTIONS='["_compress_data","_decompress_data"]' \    -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \    -s WASM=1 -s ALLOW_MEMORY_GROWTH=1

Размер полученных артефактов настораживает:

$ du -hs wasm_compressor.*112K    wasm_compressor.js108K    wasm_compressor.wasm

Если открыть JS файл-прослойку, можно увидеть примерно следующее:



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

После этого js код выглядит более приятно:



Клиентский код


Нужно как-то вызвать обработчик на стороне клиента. Первым делом загрузим файл, предоставленный пользователем, через FileReader, хранить сырые данные будем в примитиве Uint8Array:

var rawData = new Uint8Array(fileReader.result);

Далее нужно передать загруженные данные в виртуальную машину. Для этого сначала аллоцируем нужное количество байт методом _malloc, затем скопируем туда JS массив методом set. Для удобства выделим эту логику в функцию arrayToWasmPtr(array):

function arrayToWasmPtr(array) {  var ptr = Module._malloc(array.length);  Module.HEAP8.set(array, ptr);  return ptr;}

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

compressDataFunction = Module.cwrap('compress_data', 'number', ['number', 'number', 'number']);

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

function wasmPtrToArray(ptr, length) {  var array = new Int8Array(length);  array.set(Module.HEAP8.subarray(ptr, ptr + length));  return array;}

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

Итог


Поиграться с прототипом можно здесь.

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



Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 434 (21 27 сентября 2020)

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


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



Медиа


podcast Подкаст Фронтенд Юность (18+) #156 Пересядь с иглы Styleguidist на Cosmos
podcast Подкаст CSSSR: Релиз Vue 3, PostCSS 8, TypeScript 4.1 Beta, Github CLI 1.0, закат moment.js, история интернета

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


habr Математика верстальщику не нужна 2: Матрицы, базовые трансформации, построение 3D и фильтры для картинок
Почему веб-сайтам нужен заголовок HTTP Strict Transport Security (HSTS)
Несбывшиеся надежды веб-компонентов
en Упростите свой стек с помощью кастомного генератора статических сайтов
en Начало работы с Eleventy
en Folding the Web: делаем действительно адаптивный дизайн для складных устройств
en Vital Web Performance. Основные метрики производительности для вашего сайта





CSS


Адаптивное видео с помощью встроенных математических функций CSS
Математические функции в CSS
en RatioBuddy простой онлайн-инструмент, создающий SCSS сниппет для любого соотношения сторон.
en Что происходит с CSS Houdini?
en 4 относительно неизвестных свойства для построения CSS лейаутов
en Поиск причины ошибки CSS
en Стилизация элемента details
en 3 вещи о CSS переменных, о которых вы могли не знать
en Почему псевдоэлементы ::before и ::after не работают для полей ввода и картинок
en Линейное масштабирование размера шрифта с помощью CSS-clamp() на основе вьюпорта

JavaScript


Осваиваем JavaScript ES6 Symbol
en Создание JavaScript promise с нуля, Часть 1: Конструктор
en Обзор функций: ECMAScript 2021
en Все о null в JavaScript
en Объектно-ориентированный JavaScript: примитивы и объекты
en Перебирающие методы массивов изнутри







Браузеры


Релиз Firefox 81
Microsoft анонсировал начало тестирования Linux-версии Edge в октябре
Проблемы в Chrome Web Store: Google окончательно закрывает Payments API
Выпуск Firefox Reality 12, браузера для устройств виртуальной реальности

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

Перевод Вышел Vue.js 3.0 One piece

21.09.2020 16:09:27 | Автор: admin

Название - это отсылка к манге "One piece" про парня, который хочет стать пиратским королем, прим. перев.

Сегодня мы с гордостью объявляем об официальном выпуске Vue 3.0 One Piece. Эта новая мажорная версия фреймворка с повышенной производительностью, меньшим размером, лучшей поддержкой TypeScript, новыми API-интерфейсами для крупномасштабных проектов (а для средних - можно избавиться от vuex, прим. перев.) и прочной основой для будущих итераций платформы.

Версия 3.0 - это более двух лет усилий по разработке, включающих 30+ RFC, 2600+ коммитов, 628 пулл-реквестов от 99 участников, а также огромный объем работы по разработке и написанию документации за пределами основного репозитория. Мы хотим выразить нашу глубочайшую благодарность членам команды за то, что они взяли на себя эту работу, нашим контрибуторам за пулл-реквесты, нашим спонсорам за финансовую поддержку, а также сообществу за участие в обсуждениях дизайна и отзывам на предрелизные версии. Vue - это независимый проект, созданный для сообщества и поддерживаемый сообществом, и Vue 3.0 был бы невозможен без вашей постоянной поддержки.

Развитие концепции "Прогрессивного фреймворка"

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

Сегодня Vue используют более 1,3 миллиона пользователей (это число основано на количестве активных пользователей за неделю у расширения Chrome Vue Devtools в соответствии со статистикой Google) (я так и не понял, включает ли это число пользователей расширения для firefox, прим. перев.) и мы видим, что Vue используется в самых разнообразных сценариях, от добавления интерактивности на "традиционные" страницы, генерируемые на сервере, до полноценных одностраничных приложений с сотнями компонентов. Vue 3 расширяет эту гибкость.

Многослойные внутренние модули

Vue 3.0 по прежнему можно использовать из CDN с помощью тега <script>, однако его внутреннее устройство было переписано с нуля в набор раздельных модулей. Новая архитектура обеспечивает лучшую управляемость и позволяет сократить время выполнения в некоторых случаев в два раза, за счет tree-shaking.

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

  • Компилятор поддерживает трансформацию абстрактного синтаксического дерева (AST) в процессе сборки, например для интернационализации

  • Ядро предоставляет первоклассное API для создания своих визуализаторов, для рендеринга не только в браузере, но и, например, на мобильных устройствах, в контексте WebGL или в консоли. Базовый DOM-визуализатор использует этот же API

  • Модуль @vue/reactivity экспортирует функции, которые обеспечивают прямой доступ к системе реактивности Vue, и может использоваться как отдельный пакет. Его можно использовать в паре с другими решениями для создания шаблонов (например, lit-html) или даже в сценариях без пользовательского интерфейса.

Новые API для крупных приложений

Объектный API версии 2.x практически полностью поддерживается в Vue 3. Однако в версии 3.0 также представлен Composition API - новый набор API, направленных на устранение проблем при использовании Vue в крупномасштабных приложениях. Composition API построен на основе ранее упоминавшегося API реактивности. Он обеспечивает логическую связность и возможность повторного использования, похожую на хуки React, что позволяет более гибко организовывать код и надежнее определять типы (по сравнению с объектным API 2.x).

Composition API также можно использовать в Vue 2.x через плагин @vue/composition-api, и уже существуют библиотеки на базе Composition API, которые работают как для Vue 2, так и для 3 (например, vueuse, vue-composable).

Улучшение производительности

Vue 3 показывает значительные улучшения производительности по сравнению с Vue 2: размер пакета (до 41% меньше при использовании tree-shaking), первичный рендеринг (до 55% быстрее) и обновление отображения (до 133% быстрее), использования памяти (до 54% меньше).

В Vue 3 мы использовали подход виртуальный DOM с информацией от компилятора (compiler-informed Virtual DOM, как лучше перевести - не знаю, прим. перев.): компилятор шаблона выполняет агрессивную оптимизацию и генерирует код рендер-функции, который отделяет статический контент, оставляет подсказки для определения типов и, что наиболее важно, выравнивает (flatten, прим. перев.) динамические узлы внутри шаблона для снижения стоимости обхода дерева документа во время выполнения. Таким образом пользователь получает лучшее из обоих миров: оптимизированную компилятором производительность при использовании шаблонов или непосредственное управление рендерингом с помощью рендер-функции, если нужно.

Улучшенная интеграция с TypeScript

Кодовая база Vue 3 написана на TypeScript с автоматически сгенерированными, протестированными и объединенными определениями типов, поэтому эти определения всегда актуальны. Composition API отлично работает с выводом типов. Vetur, наше официальное расширение VSCode, теперь поддерживает проверку типов у шаблона и свойств (props прим. перев.) с использованием улучшенной внутренней типизации Vue 3. Кстати, определение типов полностью поддерживаются в TSX, если он вам больше нравится.

Экспериментальные возможности

Предлагаем вам две новые функции для однофайловых компонентов (SFC, также известные как файлы .vue):

Эти функции уже реализованы и доступны в Vue 3.0, однако предоставляются только для сбора отзывов. Они останутся экспериментальными до тех пор, пока соответствующие RFC не будут приняты.

Мы также реализовали недокументированный компонент <Suspense>, который реализует ожидание вложенных асинхронных зависимостей (асинхронные компоненты или компоненты с async setup()) при начальном рендеринге или перерендеринге при изменении ветвей (v-if и т.п.). Мы тестируем эту функциональность вместе с командой Nuxt.js (Nuxt 3 уже готовится) и, вероятно, утвердим её к версии 3.1.

Постепенный выпуск

Выпуск версии 3.0 обозначает общую готовность фреймворка. Несмотря на то, что некоторым из подпроектов фреймворка может потребоваться доработка для достижения стабильного статуса (в частности, интеграция vue router и Vuex в инструменты разработки), мы считаем целесообразным начинать новые проекты с нуля с Vue 3 уже сейчас. Мы также рекомендуем авторам библиотек начать обновлять ваши проекты для поддержки Vue 3.

Ознакомьтесь с соответствующим разделом документации Vue 3 для получения подробной информации обо всех подпроектах фреймворка.

Миграция и поддержка IE11

Мы отозвали сборку для миграции (сборка v3 с поведением, совместимым с v2 + предупреждения о несовместимых возможностях для упрощения миграции) и сборку с поддержкой IE11 из-за ограничений по времени. Мы сосредоточимся на их выпуске в 4 квартале 2020 года. Если вы планируете миграцию существующего приложения v2 или вашему приложению требуется поддержка IE11, вы должны знать об этих ограничениях.

Дальнейшие шаги

В ближайшее время мы сосредоточимся на:

  • Сборка для миграции

  • Поддержка IE11

  • Интеграция Router и Vuex в новые инструменты разработчика

  • Дальнейшие улучшения вывода типов в шаблонах в Vetur

В настоящее время веб-сайты с документацией, ветки на GitHub и теги npm для проектов Vue 3 и подпроектов, ориентированных на третью версию Vue, будут оставаться со статусом next. Это означает, что npm install vue по-прежнему будет устанавливать Vue 2.x, а npm install vue@next установит Vue 3. К концу 2020 года мы планируем переключить все ссылки на документацию, ветки и теги по умолчанию на 3.0.

В то же время мы планируем выпуск версии 2.7, который будет последним минорным выпуском линейки 2.x. В 2.7 будет выполнен обратный перенос совместимых улучшений из версии 3 и выдача предупреждений об использовании API, которые были удалены/изменены в версии 3, чтобы помочь с потенциальной миграцией. Мы планируем работать над версией 2.7 в первом квартале 2021 года. Этот выпуск станет LTS-версией с периодом поддержки 18 месяцев.

Попробовать

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

Подробнее..

Перевод Почему это антипаттерн?

21.09.2020 20:06:45 | Автор: admin
Всем привет. В сентябре в OTUS стартует сразу несколько курсов по JS-разработке, а именно: JavaScript Developer. Professional, JavaScript Developer. Basic и React.js Developer. В преддверии старта этих курсов мы подготовили для вас еще один интересный перевод, а также предлагаем записаться на бесплатные демо-уроки по следующим темам:



А теперь перейдём к статье.





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

Как один компонент получает доступ к информации (особенно к переменной состояния), которая находится в другом компоненте?
Как один компонент вызывает функцию, которая находится в другом компоненте?

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

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

Стандартный подход: используем пропсы для передачи значений


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

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

App обращается к ContentArea
ContentArea обращается к MainContentArea
MainContentArea обращается к MyDashboard
MyDashboard обращается к MyOpenTickets
MyOpenTickets обращается к TicketTable
TicketTable обращается к последовательности TicketRow
Каждый TicketRow обращается к TicketDetail

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

Может ли компонент TicketDetail в примере выше считывать значения состояния, которые хранятся в ContentArea? Или. Может ли компонент TicketDetail вызывать функции, которые находятся в ContentArea?
Ответ на оба вопроса да. Теоретически все потомки могут знать обо всех переменных, которые хранятся в родительских компонентах. Они также могут вызывать функции предков но с большой оговоркой. Это возможно, только если такие значения (значения состояния или функции) в явном виде переданы потомкам через пропсы. В противном случае значения состояния или функции компонента не будут доступны его дочернему компоненту.

В небольших приложениях и утилитах это особой роли не играет. Например, если компоненту TicketDetail нужно обратиться к переменным состояния, которые хранятся в TicketRow, достаточно сделать так, чтобы компонент TicketRow передавал эти значения своему потомку TicketDetail через один или несколько пропсов. Точно так же дело обстоит в случае, когда компоненту TicketDetail нужно вызвать функцию, которая находится в TicketRow. Компонент TicketRow передаст эту функцию своему потомку TicketDetail через проп. Головная боль начинается, когда какому-нибудь компоненту, расположенному далекоооо вниз по дереву, нужно получить доступ к состоянию или функции компонента, расположенного вверху иерархии.
Для решения этой проблемы в React переменные и функции традиционно передаются на все уровни вниз. Но это загромождает код, отнимает ресурсы и требует серьезного планирования. Нам пришлось бы передавать значения на много уровней примерно так:
ContentArea MainContentArea MyDashboard MyOpenTickets TicketTable TicketRow TicketDetail
То есть для того чтобы передать переменную состояния из ContentArea в TicketDetail, нам нужно проделать огромную работу. Опытные разработчики понимают, что возникает безобразно длинная цепочка передачи значений и функций в виде пропсов через промежуточные уровни компонентов. Решение настолько громоздкое, что из-за него я даже пару раз бросал изучение React.

Чудовище по имени Redux


Я не единственный, кто считает, что передавать через пропсы все значения состояния и все функции, общие для компонентов, очень непрактично. Вряд ли вы найдете хоть сколько-нибудь сложное React-приложение, к которому не прилагался бы инструмент управления состоянием. Таких инструментов не так уж мало. Лично я обожаю MobX. Но, к сожалению, отраслевым стандартом считается Redux.

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

Поэтому они придумали Redux.
Если React это Мона Лиза, то Redux это пририсованные ей усы. Если вы используете Redux, вам придется написать тонну шаблонного кода почти в каждом файле проекта. Устранение проблем и чтение кода становятся адом. Бизнес-логика выносится куда-то на задворки. В коде разброд и шатание.

Но если перед разработчиками стоит выбор: React + Redux или React без каких-либо сторонних инструментов управления состоянием, они почти всегда выбирают React + Redux. Поскольку библиотеку Redux разработали авторы ядра React, она по умолчанию считается одобренным решением. А большинство разработчиков предпочитают использовать решения, которые были вот так молчаливо одобрены.

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

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

Новый взгляд на общие значения в React


Я слишком упрям, чтобы так просто сдаться. Взглянув на Redux, я понял, что нужно искать другие решения. Я могу использовать Redux. И я работал в командах, которые пользовались этой библиотекой. В общем, я понимаю, что она делает. Но это не значит, что Redux мне нравится.
Как я уже говорил, если без отдельного инструмента для управления состоянием не обойтись, то MobX примерно в миллион раз лучше, чем Redux! Но меня мучает более серьезный вопрос. Он касается коллективного разума React-разработчиков:

Почему первым делом мы всегда хватаемся за инструмент управления состоянием?

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

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

// components.jslet components = {};export default components;


И все.Только две короткие строчки кода. Мы создаем пустой объект старый добрый JS-объект. Экспортируем его по умолчанию с помощью export default.

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

// content.area.jsimport components from './components';import MainContentArea from './main.content.area';import React from 'react';export default class ContentArea extends React.Component {   constructor(props) {      super(props);      components.ContentArea = this;   }   consoleLog(value) {      console.log(value);   }   render() {      return <MainContentArea/>;   }}


По большей части он выглядит, как вполне нормальный классовый React-компонент. У нас есть простая функция render(), которая обращается к следующему компоненту вниз по дереву. У нас есть небольшая функция console.log(), которая выводит в консоль результат выполнения кода, и конструктор. Но в конструкторе есть некоторые нюансы.

В самом начале мы импортировали простой объект components. Затем в конструкторе мы добавили новое свойство к объекту components с именем текущего React-компонента (this).В этом свойстве мы ссылаемся на компонент this. Теперь при каждом обращении к объекту components у нас будет прямой доступ к компоненту <ContentArea>.

Давайте посмотрим, что происходит на нижнем уровне иерархии. Компонент <TicketDetail> может быть таким:

// ticket.detail.jsimport components from './components';import React from 'react';export default class TicketDetail extends React.Component {   render() {      components.ContentArea.consoleLog('it works');      return <div>Here are the ticket details.</div>;   }}


А происходит вот что. При каждом рендере компонента TicketDetail будет вызываться функция consoleLog(), которая хранится в компоненте ContentArea.

Обратите внимание, что функция consoleLog() не передается по всей иерархии через пропсы. Фактически функция consoleLog() не передается никуда вообще никуда, ни в один компонент.
И тем не менее TicketDetail может вызвать функцию consoleLog(), которая хранится в ContentArea, потому что мы выполнили два действия:

  1. Компонент ContentArea при загрузке добавил в общий объект components ссылку на себя.
  2. Компонент TicketDetail при загрузке импортировал общий объект components, то есть у него был прямой доступ к компоненту ContentArea, несмотря на то что свойства ContentArea не передавались компоненту TicketDetail через пропсы.


Этот подход работает не только с функциями/колбэками. Его можно использовать для прямого запроса значений переменных состояния. Представим, что <ContentArea> выглядит так:

// content.area.jsimport components from './components';import MainContentArea from './main.content.area';import React from 'react';export default class ContentArea extends React.Component {   constructor(props) {      super(props);      this.state = { reduxSucks:true };      components.ContentArea = this;   }   render() {      return <MainContentArea/>;   }}


Тогда мы можем написать <TicketDetail> вот так:

// ticket.detail.jsimport components from './components';import React from 'react';export default class TicketDetail extends React.Component {   render() {      if (components.ContentArea.state.reduxSucks === true) {         console.log('Yep, Redux is da sux');      }      return <div>Here are the ticket details.</div>;   }}


Теперь при каждом рендере компонента <TicketDetail> он будет искать значение переменной state.reduxSucks в <ContentArea>. Если переменная вернет значение true, функция console.log() выведет в консоль сообщение. Это произойдет, даже если значение переменной ContentArea.state.reduxSucks никогда не передавалось вниз по дереву ни одному из компонентов через пропсы. Таким образом, благодаря одному простому базовому JS-объекту, который обитает за пределами стандартного жизненного цикла React, мы можем сделать так, чтобы любой потомок мог считывать переменные состояния непосредственно из любого родительского компонента, загруженного в объект components. Мы даже можем вызывать функции родительского компонента в его потомке.

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

Для начала в компоненте <ContentArea> создадим простую функцию, которая меняет значение переменной reduxSucks.

// content.area.jsimport components from './components';import MainContentArea from './main.content.area';import React from 'react';export default class ContentArea extends React.Component {   constructor(props) {      super(props);      this.state = { reduxSucks:true };      components.ContentArea = this;   }   toggleReduxSucks() {      this.setState((previousState, props) => {         return { reduxSucks: !previousState.reduxSucks };      });   }   render() {      return <MainContentArea/>;   }}


Затем в компоненте <TicketDetail> мы вызовем этот метод через объект components:

// ticket.detail.jsimport components from './components';import React from 'react';export default class TicketDetail extends React.Component {   render() {      if (components.ContentArea.state.reduxSucks === true) {         console.log('Yep, Redux is da sux');      }      return (         <>            <div>Here are the ticket details.</div>            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>         </>      );   }}


Теперь после каждого рендера компонента <TicketDetail> пользователь сможет нажимать кнопку, которая будет изменять (переключать) значение переменной ContentArea.state.reduxSucks в режиме реального времени, даже если функция ContentArea.toggleReduxSucks() никогда не передавалась вниз по дереву через пропсы.

С таким походом родительский компонент может вызвать функцию непосредственно из своего потомка. Вот как это можно сделать.
Обновленный компонент <ContentArea> будет выглядеть так:

// content.area.jsimport components from './components';import MainContentArea from './main.content.area';import React from 'react';export default class ContentArea extends React.Component {   constructor(props) {      super(props);      this.state = { reduxSucks:true };      components.ContentArea = this;   }   toggleReduxSucks() {      this.setState((previousState, props) => {         return { reduxSucks: !previousState.reduxSucks };      });      components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();   }   render() {      return <MainContentArea/>;   }}


А теперь добавим логику в компонент <TicketTable>. Вот так:

// ticket.table.jsimport components from './components';import React from 'react';import TicketRow from './ticket.row';export default class TicketTable extends React.Component {   constructor(props) {      super(props);      this.state = { reduxSucksHasBeenToggledXTimes: 0 };      components.TicketTable = this;   }   incrementReduxSucksHasBeenToggledXTimes() {      this.setState((previousState, props) => {         return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};      });         }   render() {      const {reduxSucksHasBeenToggledXTimes} = this.state;      return (         <>            <div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>            <TicketRow data={dataForTicket1}/>            <TicketRow data={dataForTicket2}/>            <TicketRow data={dataForTicket3}/>         </>      );   }}


В результате компонент <TicketDetail> не изменился. Он все еще выглядит так:

// ticket.detail.jsimport components from './components';import React from 'react';export default class TicketDetail extends React.Component {   render() {      if (components.ContentArea.state.reduxSucks === true) {         console.log('Yep, Redux is da sux');      }      return (         <>            <div>Here are the ticket details.</div>            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>         </>      );   }}


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

А вот и нет.

Смотрите. Мы создали в приложении несколько уровней, и есть только один путь вызова функции toggleReduxSucks(). Вот так.
  1. Монтируем и рендерим ContentArea.
  2. В ходе этого процесса в объект components загружается ссылка на ContentArea.
  3. В результате монтируется и рендерится TicketTable.
  4. В ходе этого процесса в объект components загружается ссылка на TicketTable.
  5. В результате монтируется и рендерится TicketDetail.
  6. У пользователя появляется кнопка Изменить значение reduxSucks (Toggle reduxSucks).
  7. Пользователь нажимает кнопку Изменить значение reduxSucks.
  8. Нажатие кнопки вызывает функцию toggleReduxSucks(), которая записана в компоненте ContentArea.
  9. Это в свою очередь вызывает функцию incrementReduxSucksHasBeenToggledXTimes() из компонента TicketTable .
  10. Все работает, потому что к тому моменту, когда пользователь сможет нажать кнопку Изменить значение reduxSucks, ссылка на компонент TicketTable будет загружена в объект components. А функция toggleReduxSucks() при вызове из ContentArea сможет найти ссылку на функцию incrementReduxSucksHasBeenToggledXTimes(), записанную в TicketTable, в объекте components.

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

Инструменты управления состоянием на свалку


Как я уже объяснил, я глубоко уверен в том, что Redux не идет ни в какое сравнение с MobX. И когда мне выпадает честь работать над проектом с нуля (к сожалению, нечасто), я всегда агитирую за MobX. Не за Redux. Но когда я разрабатываю собственные приложения, я вообще редко использую сторонние инструменты управления состоянием практически никогда. Вместо этого я просто-напросто кеширую объекты/компоненты, когда это возможно. А если это подход не работает, я частенько возвращаюсь к решению, которое используется в React по умолчанию, то есть просто передаю функции/переменные состояния через пропсы.

Известные проблемы этого подхода


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

  • Лучше всего он работает с одиночками.
    Например, в нашей иерархии в компоненте находятся компоненты со связью ноль-ко-многим. Если вы захотите кешировать ссылку на каждый потенциальный компонент внутри компонентов (и их дочерних компонентов ) в кеш components, вам придется сохранить их в массив, и тут могут возникнуть сложности. Я всегда избегал этого.
    При кешировании объекта components предполагается, что мы не можем использовать переменные/функции из других компонентов, если они не были загружены в объект components. Это очевидно.
    Если архитектура вашего приложения делает такой подход нецелесообразным, то не надо его использовать. Он идеально подходит для одностраничных приложений, когда мы уверены в том, что родительский компонент всегда монтируется раньше потомка. Если вы решили сослаться на переменные/функции потомка непосредственно из родительского компонента, создавайте такую структуру, которая будет выполнять эту последовательность только после загрузки потомка в кеш components.
    Можно считывать переменные состояния из других компонентов, ссылки на которые хранятся в кеше components, но если вы захотите обновить такие переменные (через setState()), вам придется вызвать функцию setState(), которая записана в соответствующем компоненте.


    Ограничение ответственности


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

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

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

    Я обнаружил, что JS-разработчики и, в частности, React-разработчики бывают слишком категоричны. Иногда они действительно объясняют, почему подход А неправильный, а подход Б правильный. Но в большинстве случаев они просто смотрят на фрагмент кода и объявляют, что он плохой, даже если сами не могут объяснить, почему.

    Так почему же этот подход так раздражает React-разработчиков?


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

    • С таким подходом о чистых функциях можно забыть, он захламляет приложение жесткими зависимостями.
      Окей...Понял. Но те самые люди, которые с ходу отвергли этот подход, с удовольствием будут использовать Redux (или MobX, или любое другое средство управления состоянием) почти со всеми классами/функциями в своих React-приложениях. Я не отрицаю, что иногда без инструментов управления состоянием действительно сложно обойтись. Но любой такой инструмент по своему характеру это гигантский генератор зависимостей. Каждый раз, когда вы используете инструменты управления состоянием с функциями/классами, вы захламляете приложение зависимостями. Обратите внимание: я не говорил, что нужно отправлять каждую функцию или класс в кеш объекта components. Вы самостоятельно решаете, какие именно функции/классы будут кешироваться в components, а какие функции/классы будут обращаться к тому, что вы поместили в кеш components. Если вы пишете чистую вспомогательную функцию/класс, то наверняка моя идея с кешем components вам не подходит, потому что для кеширования в components компоненты должны знать о других компонентах приложения.Если вы пишете компонент, который будет использоваться в разных фрагментах кода вашего приложения или даже в разных приложениях, не применяйте этот подход. Но опять же, если вы создаете такой глобальный компонент, в нем не нужно использовать ни Redux, ни MobX, ни какое-либо еще средство управления состоянием.
    • Просто в React так не делается. Или Это не соответствует отраслевым стандартам.
      Ага Это мне говорили не раз. И знаете что? Когда я это слышу, я даже немножко перестаю уважать своего собеседника. Если единственная причина это какое-то размытое так не делается или отраслевой стандарт, который сегодня один, а завтра другой, то разработчик просто чертов лентяй. Когда появилась React, у нас не было вообще никаких инструментов управления состоянием. Но люди начали изучать эту библиотеку и решили, что они нужны. И их создали.Если вы действительно хотите соответствовать отраслевым стандартам, просто передавайте все переменные состояния и все обратные вызовы функций через пропсы.Но если вам кажется, что базовая реализация React не удовлетворяет ваши потребности на 100 %, откройте глаза (и разум) и взгляните повнимательней на нестандартные решения, которые не были одобрены лично господином Дэном Абрамовым.


    Итак, что скажете В?


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

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

    Мне очень хочется узнать ваше мнение по этому поводу даже если вы разнесете меня в пух и прах.

    Бесплатные уроки:


Подробнее..

Проверяем формы по стандартам с Validation API

22.09.2020 06:06:12 | Автор: admin
В свое время мне почти всем нравился Angular 2+, это хорошо спроектированный фреймворк, который на голову выше остальных популярных фронтенд фреймворков по инженерному уровню исполнения. Но были у него и весьма странные недоработки. Одна из них это невозможность ручного вызова валидации или ревалидации формы, которая наблюдалась как минимум, до 8ой версии. Нельзя сказать чтобы там сильно увлекаются реактивностью, но вот в этой подсистеме похоже какие-то реактивные соображения побудили разработчиков реализовать валидацию только через привязку, вынудив разработчиков прикладных решений обращаться к костылям навроде установки состояния нетронуто для полей и вообще усложняя написание сложных валидаторов с продвинутой логикой и участием нескольких полей сразу. Опыт борьбы с ангуляровским валидатором и некоторыми другими особенностями фреймворка усилил мое впечатление от того насколько элегантным и простым оказалось после этого использование HTML5 API для валидации форм, которое просто работает в любом современном браузере даже без подключения фреймворков и библиотек.

Базой для работы валидаторов являются атрибуты элементов. С помощью атрибутов мы сразу можем задать следующие ограничения:
required поле обязательное, т.е. требует заполнения
min max step минимально и максимально допустимые значения, а также шаг изменения
minlength и maxlength ограничители по количеству допустимых символов ввода
pattern регулярное выражение
Вроде бы не густо, однако, pattern дает нам довольно богатые возможности по проверке значений, легко нагугливаются регулярки позволяющие сходу проверять номера телефонов, емейл адреса и урлы и многое другое востребованное.
Расставленные на элементы формы эти атрибуты автоматически не позволят сработать кнопке из той же формы выполняющей submit значений на сервер, правда сегодня такой кейс может показаться многим анахроничным. Но это еще не беда, т.к. с помощью клиентского кода на JavaScript мы можем точно также и даже лучше пользоваться всеми этими валидаторами. Поэтому мы не будем использовать input type=email, а попробуем сделать свое поле с проверкой вводимого на соответствие правилам формирования адресов электронной почты. Сделаем простую форму:
<form name="myform" id="myform">   <input type="text" pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" placeholder="email here"/>   <input type="submit"></form>

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



Соответственно ввод mail@example.com дает успешный сабмит формы.
Чтобы развить свое поведение надо получить доступ к инстансу формы, это можно сделать через глобальный document по имени, индексу (id) или порядковому номеру начиная с нуля.
<script type="module">   document.forms.myform.onsubmit = (event) => {       console.log('validate');       return false;   };</script>

или по селектору одним из методов, таким как document.getElementById() или document.querySelector()
для проверки результатов запустим http-server
npx http-server

после того как команда отработает, в браузере можно открывать 127.0.0.1:8080/ или тот адрес, что он напишет вам в консоли и отлаживать результаты.

Заменим сабмит на обычную кнопку, и будем вызывать проверку формы вручную, немного изменив пример.
<form id="myform" action="#">   <input type="text" pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" required placeholder="email here" />   <input type="button" name="subm" value="OK" /></form><script type="module">;   myform.subm.onclick = (event) => {       console.log(form.checkValidity());       return false;   };</script>

В данном примере видно, что мапинг объектов формы по их id и name работает и для дочерних по отношению к форме элементов, что выглядит очень изящно. Теперь наш код выводит в консоль состояние валидности формы.
Наличие методов ручного запуска валидации, не означает, что она не осуществляется без их вызова.
Результаты ввода и других изменений на форме сразу отражаются на ее состоянии, что проявляется в наличии псевдоклассов стилей valid и invalid. Если добавить выделение цветом, то можно заметить как сразу срабатывает валидация.
<style>  :valid {       border: 1px solid green;   }  :invalid {       border: 1px solid red;   }</style>




Для того, чтобы форма не мозолила глаза красным до того как пользователь попробовал что-то в нее вводить, можно использовать лайфхак с плейсхолдером:
<style>   input:valid {       border: 1px solid green;   }   input:not(:placeholder-shown):invalid {       border: 1px solid red;   }</style>

На элементы формы можно навесить внешние обработчики для событий валидации.
<script type="module">   myform.email.oninvalid = (event) => {       alert('Wrong email !!11');   };   myform.subm.onclick = (event) => {       console.log(form.checkValidity());       return false;   };</script>

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

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

Соответственно мы можем, обработать это поведение:
myform.subm.onclick = (event) => {   if (myform.checkValidity()) {       alert('Valid !');   } else {       alert('Invalid !')   }   return false;};

В реальной жизни нам может потребоваться также вызов event.preventDefault() если валидация не пройдена, чтобы прервать процедуру сабмита формы.

У checkValidity() есть аналог reportValidity(), который возвращает результат не вызывая при этом повторной валидации.

А как узнать какое поле неправильное?

Свойство .validity есть у каждого элемента ввода формы как и возможность вызвать на нем методы валидации, свойство имеет следующую структуру:

ValueState: {
valid общий признак корректности значения
valueMissing значение требуется, но не задано
typeMismatch введено неправильное по типу значение
patternMismatch введен не соответствующее шаблону значение
tooLong значение больше чем maxlength
tooShort значение меньше чем minlength
rangeUnderflow значение меньше min
rangeOverflow значение больше max
stepMismatch значение не соответствует шагу
badInput ввод не может быть приведен к значению
customError произвольная ошибка
}

В основном представлены, как мы видим, свойства ошибок соответствующие стандартным атрибутам валидации, тогда как .customError это наш задел для расширения.
Вызвав метод .setCustomValidity() с аргументом в виде строки с текстом ошибки мы можем обозначить элемент формы как невалидный. Установить или получить текст ошибки можно также через свойство .validationMessage.
Чтобы не задавливать браузерные валидации можно использовать свойство .willValidate, которое сообщает будут ли вызваны стандартные валидации на поле.
Передав пустую строку в качестве аргумента .setCustomValidity() мы можем вернуть его состояние к валидному.
Давайте добавим поддержку собственного атрибута my-pattern, который для наглядности будет точно так же проверять значение на соответствие регулярному выражению.
В случае ошибки сообщение, помимо того как это предусмотрено в браузере, будет выводиться рядом с полем
Валидация будет срабатывать на изменении значения нашего альтернативного поля и при нажатии кнопки.
<form id="myform" action="#">   <div>       <input type="text" name="email" id="email" value="" pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" required placeholder="email here" />       <span class="msg"></span>   </div>   <div>       <input type="text" name="customInput" id="customInput" my-pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" required placeholder="text here" />       <span class="msg"></span>   </div>   <button type="submit" name="subm" value="OK">OK</button></form><style>   input:valid {       border: 1px solid green;   }   input:not(:placeholder-shown):invalid {       border: 1px solid red;   }</style><script type="module">   myform.customInput.oninvalid = (event) => {       let el = event.target;       let msg = el.parentElement.querySelector('.msg');       msg.innerText = el.validationMessage;       console.log('oninvalid, id: ', el.id);   };   myform.customInput.oninput = (event) => {       let el = event.currentTarget;       validateWithMyPattern(el);       markValidity(el);   };   function markValidity(el) {       el.checkValidity();       let msg = el.parentElement.querySelector('.msg');       if (el.validity.valid) {           msg.innerText = '';       } else {           msg.innerText = el.validationMessage;       }   }   function validateWithMyPattern(field) {       if (field.value) {           if (field.hasAttribute('my-pattern') &&               field.value.match(field.getAttribute('my-pattern'))) {               field.setCustomValidity('');           } else {               field.setCustomValidity('My pattern error');           }       }   }   myform.subm.onclick = (event) => {       for (let formEl of myform.querySelectorAll('input')) {           validateWithMyPattern(formEl);           markValidity(formEl);       }       if (myform.reportValidity()) {           alert('Valid !');       } else {           alert('Invalid !')       }       return false;   };</script>

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



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

Из ограничений Validation API мне более запомнилось только исходная невалидность полей. Для ее кроме хитрости с placeholder или специальными состояниями a-la untouched можно производить всю валидацию программно на событиях input и submit сочетая собственные валидаторы со стандартными.
Решая свои задачи, я пришел к необходимости создать свой компонент, выполняющий задачи формы заодно для поддержки собственных элементов ввода, позволяющий задавать разное поведение валидации и уведомлений и вешать любые валидаторы и использующий при этом стандартизированный Validation API. Посмотреть на него можно вот тут: https://bitbucket.org/techminded/skinny-widgets/src/master/src/form/
а код примера из этой статьи найти вот тут:
https://bitbucket.org/techminded/myform/
Подробнее..

Математика верстальщику не нужна 2 Матрицы, базовые трансформации, построение 3D и фильтры для картинок

22.09.2020 12:22:19 | Автор: admin


В прошлый раз мы говорили о графиках и траекториях для покадровых анимаций, а сегодня речь пойдет про матрицы. Мы разберемся с тем, как строятся базовые трансформации в CSS, SVG и WebGL, построим отображение 3D-мира на экране своими руками, попутно проводя параллель с таким инструментом, как Three.js, а также поэкспериментируем с фильтрами для фотографий и разберемся, что же за магия такая лежит в их основе.

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

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

Немного определений


Матрица в математике это такая абстракция, можно сказать, что это тип данных в каком-то смысле, и записываетя она в виде прямоугольной таблицы. Количество столбцов и строк может быть любым, но в вебе мы почти всегда имеем дело с квадратными матрицами 2x2, 3x3, 4x4, и 5x5.

Также нам понадобится такое определение, как вектор. Думаю из школьной геометрии вы можете вспомнить определение, связанное со словами длина и направление, но вообще в математике вектором может называться очень много чего. В частности мы будем говорить о векторе, как об упорядоченном наборе значений. Например координаты вида (x, y) или (x, y, z), или цвет в формате (r, g, b) или (h, s, l, a) и.т.д. В зависимости от того, сколько элементов вошло в такой набор мы будем говорить о векторе той или иной размерности: если два элемента двумерный, три трехмерный и.т.д. Также, в рамках рассматриваемых тем, может быть удобно иногда думать о векторе, как о матрице размера 1x2, 1x3, 1x4 и.т.д. Технически можно было бы ограничиться только термином матрица, но мы все же будем использовать слово вектор, чтобы отделить эти два понятия друг от друга, хотя бы в логическом смысле.

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

function multiplyMatrices(a, b) {    const m = new Array(a.length);    for (let row = 0; row < a.length; row++) {        m[row] = new Array(b[0].length);        for (let column = 0; column < b[0].length; column++) {            m[row][column] = 0;            for (let i = 0; i < a[0].length; i++) {                m[row][column] += a[row][i] * b[i][column];            }        }    }    return m;}

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

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

Мы будем использовать только квадратные матрицы в очень частном наборе задач, так что будет достаточно набора простых правил:

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



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

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



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

В этом примере мы используем буквы (x, y), и как вы уже можете догадаться, дальше речь пойдет о координатах в 2D. Но зачем добавлять третью координату и оставлять ее единицей? спросите вы. Все дело в удобстве, или даже лучше сказать, что в универсальности. Мы очень часто добавляем +1 координату для упрощения расчетов, и работа с 2D идет с матрицами 3x3, работа с 3D с матрицами 4x4, а работа с 4D, например с цветами в формате (r, g, b, a) идет с матрицами 5x5. На первый взгляд это кажется безумной идеей, но дальше мы увидим, насколько это унифицирует все операции. Если вы захотите более подробно разобраться в этой теме, то можете загуглить выражение однородные координаты.

Но довольно теории, перейдем к практике.

I. Базовые трансформации в компьютерной графике


Давайте возьмем выражения из упомянутого примера и посмотрим на них как есть, вне контекста матриц:

newX = a*x + b*y + cnewY = d*x + e*y + f

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

newX = 1*x + 0*y + 0 = xnewY = 0*x + 1*y + 0 = y

Здесь ничего не меняется новые координаты (x, y) идентичны старым. Если подставить эти коэффициенты в матрицу и внимательно присмотреться, то увидим, что получится единичная матрица.

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

newX = 1*x + 0*y + A = x + AnewY = 0*x + 1*y + 0 = y

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

Меняя эти 6 коэффициентов a, b, c, d, e, f и наблюдая за изменениями x и y, рано или поздно мы придем к четырем их комбинациям, которые кажутся полезными и удобными для практического применения. Запишем их сразу в форме матриц, возвращаясь к изначальному примеру:



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

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

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



Базовые трансформации в CSS


Но это все слова. Давайте посмотрим, как это выглядит в реальном фронтенде. В CSS у нас (внезапно) есть функция matrix. Выглядит она в контексте кода как-то так:

.example {    transform: matrix(1, 0, 0, 1, 0, 0);}

Многих новичков, которые впервые видят ее, накрывает вопрос почему здесь шесть параметров? Это странно. Было бы 4 или 16 еще куда не шло, но 6? Что они делают?

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



Также в CSS есть функция matrix3d для того, чтобы задавать с помощью матрицы трансформацию в 3D. Там уже есть 16 параметров, ровно чтобы сделать матрицу 4x4 (не забываем, что мы добавляем +1 размерность).

Матрицы для базовых 3D трансформаций строятся так же, как и для 2D, только нужно больше коэффициентов, чтобы расставлять их не по двум координатам, а по трем. Но принципы те же.

Естественно, каждый раз городить матрицу и следить за корректной расстановкой коэффициентов при работе с простыми трансформациями в CSS было бы странно. Мы, программисты, обычно все же стараемся упростить себе жизнь. Так что у нас в CSS появились краткие функции для создания отдельных трансформаций translateX, translateY, scaleX и.т.д. Обычно мы используем именно их, но важно понимать, что внутри они создают эти же самые матрицы, о которых мы говорили, просто скрывая от нас этот процесс за еще одним слоем абстракции.

Эти же самые трансформации translate, rotate, scale и skew, а также универсальная функция matrix для задания трансформаций, присутствуют и в SVG. Синтаксис немного другой, но суть такая же. При работе с трехмерной графикой, например с WebGL, мы тоже будем прибегать к этим же трансформациям. Но об этом чуть позже, сейчас важно понять, что они есть везде, и работают везде по одному и тому же принципу.

Промежуточные итоги


Обобщим вышесказанное:

  • Матрицы могут быть использованы в качестве трансформаций для векторов, в частности для координат каких-то объектов на странице.
  • Почти всегда мы оперируем квадратными матрицами и добавляем +1 размерность для упрощения и унификации вычислений.
  • Есть 4 базовых трансформации translate, rotate, scale и skew. Они используются везде от CSS до WebGL и везде работают схожим образом.

II. Построение 3D сцены своими руками


Логичным развитием темы про трансформации координат будет построение 3D сцены и отображение ее на экране. В той или иной форме эта задача обычно есть во всех курсах по компьютерной графике, но вот в курсах по фронтенду ее обычно нет. Мы посмотрим может быть немного упрощенный, но тем не менее полноценный вариант того, как можно сделать камеру с разными углами обзора, какие операции нужны, чтобы посчитать координаты всех объектов на экране и построить картинку, а также проведем параллели с Three.js самым популярным инструментом для работы с WebGL.

Здесь должен возникнуть резонный вопрос зачееем? Зачем учиться делать все руками, если есть готовый инструмент? Ответ кроется в вопросах производительности. Вероятно вы бывали на сайтах с конкурсами вроде Awwwards, CSS Design Awards, FWA и им подобных. Вспомните, насколько производительные сайты принимают участие в этих конкурсах? Да там почти все тормозят, лагают при загрузке и заставляют ноутбук гудеть как самолет! Да, конечно, основная причина обычно это сложные шейдеры или слишком большое количество манипуляций с DOM, но вторая невероятное количество скриптов. Это катастрофическим образом влияет на загрузку подобных сайтов. Обычно все происходит так: нужно сделать что-то на WebGL берут какой-нибудь 3D движок (+500КБ) и немного плагинов для него (+500КБ); нужно сделать падение объекта или разлетание чего-то берут физический движок (+1МБ, а то и больше); нужно обновить какие-то данные на странице ну так добавляют какой-нибудь SPA-фреймворк с десятком плагинов (+500КБ) и.т.д. И таким образом набирается несколько мегабайтов скриптов, которые мало того, что нужно загрузить клиенту (и это еще вдобавок к большим картинкам), так еще и браузер с ними что-то будет делать после загрузки они же не просто так к нему прилетают. Причем, в 99% случаев, пока скрипты не отработают, пользователь не увидит всей той красоты, которую ему бы нужно с самого начала показывать.

Народная примета гласит, что каждые 666КБ скриптов в продакшене увеличивают время загрузки страницы на время, достаточное, чтобы пользователь отправил разработчика сайта на следующий круг ада. Three.js в минимальной комплектации весит 628КБ...

При этом часто задачи просто не требуют подключения сложных инструментов. Например чтобы показать пару плоскостей с текстурами в WebGL и добавить пару шейдеров, чтобы картинки расходились волнами, не нужен весь Three.js. А чтобы сделать падение какого-то объекта не нужен полноценный физический движок. Да, он скорее всего ускорит работу, особенно если вы с ним знакомы, но вы заплатите за это временем пользователей. Тут каждый решает сам для себя, что ему выгоднее.

Цепочка преобразований координат


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

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



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

У модели, как мы и сказали, есть система координат. Но обычно мы хотим иметь много моделей, хотим сделать сцену с ними. Сцена, наш 3D-мир, будет иметь свою, глобальную систему координат. Если мы просто будем интерпретировать координаты модели как глобальные координаты то наша модель будет находиться как бы в центре мира. Иными словами, ничего не поменяется. Но мы хотим добавить много моделей в разные места нашего мира, примерно так:



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

Например для кубиков будут примерно такие матрицы:

// Центральный кубик в центре мира.// Единичная матрица ничего не меняет и координаты просто интерпретируются как глобальные.const modelMatrix1 = [    [1, 0, 0, 0],    [0, 1, 0, 0],    [0, 0, 1, 0],    [0, 0, 0, 1]];// Кубик, смещенный по глобальной оси X.const modelMatrix2 = [    [1, 0, 0, 1.5],    [0, 1, 0, 0  ],    [0, 0, 1, 0  ],    [0, 0, 0, 1  ]];// Кубик, смещенный по глобальной оси X в другую сторону.const modelMatrix3 = [    [1, 0, 0, -1.5],    [0, 1, 0, 0   ],    [0, 0, 1, 0   ],    [0, 0, 0, 1   ]];

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

для каждой точки модели {    глобальные координаты точки = [ матрица этой модели ] * локальные кординаты}

Соответственно для каждой модели нужна своя матрица.

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


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



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

для каждой точки в мире {    координаты точки для камеры = [ матрица камеры ] * глобальные кординаты}

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

Давайте посмотрим на сцену с того места, где стоит наша условная камера:



Сейчас, преобразовав все точки в систему координат камеры, мы можем просто выбросить ось Z, а оси X и Y интерпретировать как горизонтальную и вертикальную. Если нарисовать все точки моделей на экране, то получится картинка, как в примере никакой перспективы и сложно понять, какая часть сцены на самом деле попадает в кадр. Камера как бы бесконечного размера во все стороны. Мы можем как-то все подогнать, чтобы то, что нужно, влезло на экран, но было бы неплохо иметь универсальный способ определения того, какая часть сцены попадет в поле зрения камеры, а какая нет.

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

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

// Пусть угол обзора будет 90 градусовconst s = 1 / (Math.tan(90 * Math.PI / 360));const n = 0.001;const f = 10;const projectionMatrix  = [    [s, 0, 0,          0],    [0, s, 0,          0],    [0, 0, -(f)/(f-n), -f*n/(f-n)],    [0, 0, -1,         0]];

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

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

Теперь, произведя уже знакомые преобразования:

для каждой точки в системе координат камеры {    координаты точки = [ матрица проекции ] * координаты в камере}

Получим в нашем поле зрения именно то, что и ожидаем. Увеличивая угол видим больше всего по сторонам, уменьшая угол видим только то, что ближе к направлению, куда камера направлена. PROFIT!



Но на самом деле нет. Мы забыли про перспективу. Бесперспективная картинка нужна мало где, так что нужно ее как-то добавить. И здесь, внезапно, нам не нужны матрицы. Задача выглядит очень сложной, но решается банальным делением координат X и Y на W для каждой точки:



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

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

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

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

Здесь у вас, вероятно заболела голова от количества информации, так что притормозим и повторим еще раз все трансформации в одном месте:



Если эти трансформации объединить в одну, то получится паровозик.

Как это выглядит в Three.js?


Теперь, когда мы понимаем, откуда этот паровозик взялся, посмотрим на пример вершинного шейдера по умолчанию в Three.js, который ничего не делает:

void main() {    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}

или в более полном варианте:

void main() {    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);}

Ничего он вам не напоминает? Да, это именно этот паровозик. И под ничего не делает мы подразумеваем, что он как раз делает всю работу по пересчету координат, на основе заботливо переданных со стороны Three.js матриц. Но никто не мешает эти матрицы сделать своими руками, не так ли?

Типы камер в компьютерной графике и Three.js


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

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

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



Для каждой половины экрана мы берем свою камеру и получается стерео-картинка. А CubeCamera это 6 обычных камер, расположенных с разных сторон от какой-то точки, не более того.

Что дальше?


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

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

Про текстуры и эффекты для картинок на WebGL, в том числе без библиотек, мы говорили в предыдущих статьях уже не раз. Вы можете обратиться к ним, если вам эта тема интересна. Таким образом, объединив все эти знания воедино, мы сможем строить полноценные красочные 3D-штуки своими руками.

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

Промежуточные итоги


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

Итак:

  • Можно построить 3D мир и рассчитать координаты объектов на экране своими руками, используя паровозик из матриц.
  • В 3D мире мы оперируем такой абстракцией, как камера. Она имеет расмоложение, направление и угол обзора. Все это задается с помощью все тех же матриц. И есть два базовых вида камер с перспективой и без перспективы.
  • В контексте WebGL ручное построение картинки на экране или физические расчеты часто позволяют выбросить тяжелые зависимости и ускорить загрузку страниц. Но важно соблюдать баланс между своими скриптами, готовыми инструментами, а также альтернативными вариантами решения задач, уделяя внимание не только своему удобству, но и вопросам скорости загрузки и конечной производительности, в том числе на телефонах.

III. Фильтры для изображений


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



И применить это для картинки по очевидному принципу:

для каждого пикселя картинки {    новый цвет пикселя = [ фильтр ] * старый цвет пикселя}

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



Оу. Получились фильтры яркости и контраста. Занятно.

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

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



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

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

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



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

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



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

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

Фильтры в CSS


В CSS у нас есть свойство filter. И там, в частности, есть вот такие варианты фильтров, связанных с цветами:

  • brightness (мы его сделали)
  • contrast (сделали)
  • invert (то же, что и контраст, только коэффициенты по главной диагонали с другим знаком)
  • saturate (сделали)
  • grayscale (как уже отметили, это частный случай saturate)
  • sepia (очень размытое понятие, разные варианты сепии получаются игрой с коэффициентами, где мы так или иначе уменьшаем присутствие синего цвета)

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

Фильтры-матрицы в SVG


В рамках SVG у нас есть такая штука, как feColorMatrix, которая применяется при создании фильтров для изображений. И здесь у нас уже есть полная свобода можем сделать матрицу на свой вкус. Синтаксис там примерно такой:

<filter id=my-color-filter>    <feColorMatrix in=SourceGraphics        type=matrix,        matrix=1 0 0 0 0                0 1 0 0 0                0 0 1 0 0                0 0 0 1 0                0 0 0 0 1    /></filter>

А еще SVG фильтры можно применить к обычным DOM-элементам в рамках CSS, там для этого есть специальная функция url Но я вам этого не говорил!

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

А что еще бывает?


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

Kernel matrix


В частности во фронтенде мы встречаем такую штуку, как kernel matrix, и связанными с ней эффектами. Суть проста есть квадратная матрица, обычно 3x3 или 5x5, хотя может быть и больше, а в ней хранятся коэффициенты. В центре матрицы для текущего пикселя, вокруг центра для соседних пикселей. Если матрица 5x5 то появляется еще один слой вокруг центра для пикселей, находящихся через один от текущего. Если 7x7 то еще один слой и.т.д. Иными словами мы рассматриваем матрицу как такое двумерное поле, на котором можно расставить коэффициенты по своему усмотрению, уже без привязки к каким-то уравнениям. А трактоваться они будут следующим образом:

для каждого пикселя картинки {    новый цвет пикселя =        сумма цветов соседних пикселей, умноженных на их коэффициенты из матрицы}

Чистый канвас не очень подходит для таких задач, а вот шейдеры очень даже. Но нетрудно догадаться, что чем больше матрица, тем больше соседних пикселей мы будем использовать. Если матрица 3x3 мы будем складывать 9 цветов, если 5x5 25, если 7x7 49 и.т.д. Больше операций больше нагрузка на процессор или видеокарту. Это неизбежно повлияет на производительность страницы в целом.

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


В рамках SVG у нас есть специальный тег feConvolveMatrix, который сделан как раз для создания подобных эффектов:

<filter id=my-image-filter>    <feConvolveMatrix        kernelMatrix=0 0 0                      0 1 0                      0 0 0    /></filter>

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

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

Если мы начнем расставлять числа по слоям, от большего у меньшему, то получится blur:



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

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

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



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

Промежуточные итоги


Обобщим сказанное в этой части:

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

Заключение


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

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

Приглашаем на DINS JS EVENING (online) обсуждаем рефакторинг приложений и SvelteJS

22.09.2020 14:11:12 | Автор: admin
Встречаемся 30 сентября в 19:00.

В этот вечер Андрей Владыкин из DINS расскажет, с какими трудностями столкнулся при рефакторинге Chrome Extension и с помощью каких технических решений справился с этой задачей. Михаил Кузнецов из ING Bank сделает обзор нового фреймворка SvelteJS и проведет демо с разработкой простого приложение в прямом эфире. Участники встречи смогут задать вопросы спикерам.

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

image


Программа


19:00-19:30 Рефакторинг приложения на примере Chrome Extension (Андрей Владыкин, DINS)
При рефакторинге Chrome Extension Андрею пришлось написать приложение, которое на тот момент не имело аналогов: WebRTC-клиент для звонков через браузер. Вы узнаете, с какими трудностями он столкнулся, как выбирал инженерные решения, какие задачи получилось решить, а какие нет. Еще Андрей расскажет, как выстроить работу, если требования к продукту постоянно меняются.
Доклад будет интересен начинающим фронтенд-разработчикам.

Андрей Владыкин Frontend Developer в DINS. Успел поработать в Enterprise-разработке и с сервисами для контактных центров. Сейчас работает над приложением для видеоконференций. В основном пишет на React.

19:30-20:10 Разработка быстрых и легких веб-приложений на SvelteJS (Михаил Кузнецов, ING Bank)
Михаил расскажет о SvelteJS новом фреймворке, при использовании которого генерируется минимальный итоговый бандл. Вы узнаете, с чем связана популярность фреймворка и почему его стоит применить в вашем следующем проекте.
Во время доклада Михаил сделает обзор функций SvelteJS, поделится опытом его использования. Вы увидите разработку простого приложения в прямом эфире и сможете на практике познакомиться с этим продуктом, его синтаксисом и компонентами.
Уровень начальный. Доклад будет интересен тем, кто еще не сталкивался со SvelteJS на практике и только хочет попробовать им воспользоваться.

Михаил Кузнецов Team Lead в ING Bank. Разработчик, спикер, тимлид, преподаватель. Хорошо относится к фронтенду, также пишет на других стеках.

Как присоединиться


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

Как проходят встречи


Записи предыдущих митапов можно посмотреть на нашем YouTube-канале.

О нас


DINS IT EVENING это место встречи и обмена знаниями технических специалистов по направлениям Java, DevOps, QA и JS. Несколько раз в месяц мы организуем встречи, чтобы обсудить с коллегами из разных компаний интересные кейсы и темы. Открыты для сотрудничества, если у вас есть наболевший вопрос или тема, которой хочется поделиться пишите на itevening@dins.ru!
Подробнее..

Разбираем тестовое задание на должность фронтенд-разработчика на Vue.js

22.09.2020 20:11:13 | Автор: admin

Первое правило тестовых заданий - никогда не делайте тестовые задания!

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

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

Для ознакомления с заданием, которое я получил, прошу под спойлер:

Техническое задание:

Средствами Vue.js реализуйте небольшое SPA приложение для заметок.

Каждая заметка имеет название и список задач (todo list), далее - Todo. Каждый пункт Todo состоит из чекбокса и относящейся к нему текстовой подписи.

Приложение состоит всего из 2х страниц.

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

  • перейти к созданию новой заметки

  • перейти к изменению

  • удалить (необходимо подтверждение)

Страница изменения заметки позволяет определенную заметку отредактировать, отметить пункты Todo, а после сохранить изменения. Действия с заметкой:

  • сохранить изменения

  • отменить редактирование (необходимо подтверждение)

  • удалить (необходимо подтверждение)

  • отменить внесенное изменение

  • повторить отмененное изменение Действия с пунктами Todo:

  • добавить

  • удалить

  • отредактировать текст

  • отметить как выполненный

Требования к функционалу:

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

  • Подтверждение действий (удалить заметку) выполняется с помощью диалогового окна.

  • Интерфейс должен отвечать требованиям usability.

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

  • Можно пренебречь несоответствием редактирования текста с помощью кнопок отменить/повторить и аналогичным действиям с помощью комбинацияй клавиш (Ctrl+Z, Command+Z, etc.).

Технические требования:

  • Диалоговые окна должны быть реализованы без использования "alert", "prompt" и "confirm".

  • В качестве языка разработки допускается использовать JavaScript или TypeScript.

  • В качестве сборщика, если это необходимо, используйте Webpack.

  • Верстка должна быть выполнена без использования UI библиотек (например Vuetify).

  • Адаптивность не обязательна, но приветствуется.

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

Особое внимание стоит обратить на следующие моменты:

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

  • Читабельность и наличие элементарной архитектуры.

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

Тестовое задание должно быть предоставлено в следующем виде:

  • Ссылка на публичный репозиторий (GitHub, BitBucket, GitLab) с исходным кодом.

  • Ссылка на сайт для тестирования функционала. Или Dockerfile и docker-compose.yaml, позволяющие развернуть локально командой docker-compose up работоспособную копию сайта.ехническое заданиеехническое заданиеехническое задание

Вот что у меня в итоге получилось.

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

Разберем по пунктам задание и возможные способы его решения:

Средствами Vue.js реализуйте небольшое SPA приложение для заметок. Тут все просто: используем Vue CLI для создания проекта.

Каждая заметка имеет название и список задач todo list, (далее - Todo). Каждый пункт Todo состоит из чекбокса и относящейся к нему текстовой подписи. - А вот и первая сложность, у нас будет два уровня абстракции: множество заметок и множество дел, которые составляют заметку.

Приложение состоит всего из 2х страниц. Тут возник вопрос - использовать ли Vue Router? Это же всего две страницы. Дальше из задания мы узнаем, что первая страница используется для отображения списка заметок, а вторая - для редактирования отдельной заметки. Конечно, можно переключать компоненты условным оператором, но раутер даст возможность перехода на сгенерированные страницы для каждой заметки. Но это уже больше двух страниц, или сгенерированные страницы считаются за одну, так как они по одному шаблону сделаны? После недолгих колебаний я решил все-таки его использовать.

Дальше следуют подробности по каждой странице, обратим внимание на два пункта:

  • отменить внесенное изменение

  • повторить отмененное изменение

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

Do и RedoDo и Redo
  • Все действия на сайте должны происходить без перезагрузки страницы. Это означает стандартное SPA, мы и так используем Vue CLI.

  • Подтверждение действий (удалить заметку) выполняется с помощью диалогового окна.

  • Диалоговые окна должны быть реализованы без использования "alert", "prompt" и "confirm".

    Диалоговое окно должно создавать Promise, который зависит от решения юзера. Желательно, чтобы решение было переиспользуемым для всех наших случаев. Помучавшись над решением, я пришел к выводу, что лучше использовать готовое решение, к тому же запретов на пакеты не было. Я использовал vue-modal-dialogs - очень удобная библиотека, рекомендую. Надеюсь её перепишут для Vue 3.

  • Интерфейс должен отвечать требованиям usability. Другими словами, он должен быть удобным. Лучше бы написали конкретные требования, так не очень понятно.

  • После перезагрузки страницы состояние списка заметок должно сохраняться. - Так как серверной части у нашего приложения не планируется, заметки следует сохранять на стороне клиента, для этого есть два решения Cookie и localStorage. Выбираем кому что ближе. Я выбрал localStorage. К тому же я решил не использовать Vuex, а вместо него использовать локальное хранилище, как единыйисточник истины. Для небольшого приложения без бэка это выглядит разумным решением, экономящем время, в других случаях я не рекомендовал бы так делать.

  • В качестве языка разработки допускается использовать JavaScript или TypeScript. - а разве есть еще варианты? Честно говоря, ТЗ оставляет ощущение, что тот кто его составлял плохо знаком с Vue. TypeScript на Vue 2 спорно применять, слабая поддержка. Посмотрим, что будет на Vue 3.

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

  • В качестве сборщика, если это необходимо, используйте Webpack. Vue CLI это и есть Webpack, настроенный для удобства создания SPA на Vue.js

  • Верстка должна быть выполнена без использования UI библиотек (например Vuetify) - это минус. Для того чтобы "оживить" приложение, я использовал Material Icons от Гугла, вместо кнопок. Не знаю, оценил ли заказчик, он так и не ответил.

  • Адаптивность не обязательна, но приветствуется. - flexbox в помощь.

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

    Остальные пункты менее важны.

Еще из вкусностей:

Я использовал пакет v-click-outside. Название говорит само за себя. Он добавляет директиву, которая срабатывает при клике вне элемента. Можно было написать самому, но я решил не изобретать велосипед. Использовал для отмены редактирования тудушки, если пользователь кликнул где-то еще. Это в задании не было, включим это в юзабилити.

Еще мне пришла в голову такая мысль: а что делать, если юзер захочет покинуть страницу редактирования заметки? Куда повесить вызов диалогового окна: на историю браузера, на кнопки меню? Есть элегантное решение. Vue-Router добавляет хуки жизненного цикла в компонент. Хук beforeRouteLeave поможет нам во всех ситуациях когда пользователь пытается покинуть страницу. Пусть наш хук вызывает модальное окно. Только не забыть сделать его асинхронным, ведь окно возвращает промис. Например, вот так:

  async beforeRouteLeave (to, from, next) {    if (await confirm('Do you realy want to leave this page?',       'All unsaved changes will be lost.')) {        this.clearNote()        next()      } else{        next(from)      }  }

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

Если возникли вопросы и замечания, прошу в комментарии или личку.

Подробнее..

Создание модуля на фреймворке Htmlix

23.09.2020 12:10:47 | Автор: admin
В данной статье будет описаны базовые принципы создания модулей на javascript фреймворке htmlix.

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

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

Далее будет рассмотрен модуль addDrawCirclePane приложения Collage_n который рисует круги на канвас с помощью кликов мыши. Перед рисованием модуль принимает два параметра цвет и диаметр круга с помощью свойств с типом inputvalue. Далее после нажатия кнопки рисовать вызывает emiter событие emiter-operation-with со значением draw-circle чтобы включить активное состояние модуля, и выключить другие модули приложения.

Модуль создается также как и обычный компонент:

(function(){//разметка модуля// создаем контейнер для модуля data-draw_circle_panel="container"//все используемые в js поля и кнопки обозначены именами (для удобства)// name="draw_circle_btn", name="draw_sircle_radius", name="draw_sircle_color"  var html = `   <div data-draw_circle_panel="container"  class="form-group" name="draw_circle_panel"><label for="exampleFormControlInput1" style="font-size: 15px;">                       Рисовать окружность        </label><div class="form-row"><div class="form-group col-md-4">                     <button type="button" name="draw_circle_btn" class="btn btn-success btn-sm">                            Рисовать                      </button></div><div class="form-group col-md-4">   <input name="draw_sircle_radius" type="text" class="form-control form-control-sm"></div><div class="form-group col-md-4">     <input name="draw_sircle_color" type="text" class="form-control form-control-sm"></div></div> </div>`  ;  //динамическое добавление разметки модуля в общую разметку приложения.  var div = document.createElement("div");  div.innerHTML = html;  div = div.querySelector("div");  var parent = document.querySelector("[data-main_form]");  var insert_before = document.querySelector("[name='common_btns_class']")  var insertedElement = parent.insertBefore(div, insert_before);    //js код модуля  var draw_circle_panel = {    container: "draw_circle_panel", //контейнер модуля  props: [               ///свойства модуля["draw_circle_btn", "click", "[name='draw_circle_btn']"], ["draw_sircle_radius", "inputvalue", "[name='draw_sircle_radius']"],["draw_sircle_color", "inputvalue", "[name='draw_sircle_color']"], ///два свойства-события основного core приложения: клики по канвас и событие смены операции["canvas_click", "emiter-mousedown-canvas", ""], ["operation_with", "emiter-operation-with", ""],  ],  methods: { //отключает слушателей canvas событий ( mousedown) если модуль находится в пассивном состоянии  operation_with: function(){    if(this.emiter.prop != "draw-circle"){     this.parent.props.canvas_click.disableEvent();    }else{    this.parent.props.canvas_click.enableEvent();    }    },//при нажатии на кнопку рисовать - вызывает событие "emiter-operation-with" и устанавливает свойство prop = "draw-circle" чтобы другие модули отключили прослушивание событий и скрыли ненужные кнопки.  draw_circle_btn: function(){  this.$$("emiter-operation-with").set("draw-circle");       },//слушает событие приложения  "emiter-mousedown-canvas" и в активном состоянии рисует круги при кликах мышью. canvas_click: function(){if(this.$$("emiter-operation-with").prop == "draw-circle"){//данные из свойств модуля  var props = this.parent.props;  var radius = props.draw_sircle_radius.getProp();  var color = props.draw_sircle_color.getProp();                       var point = this.emiter.prop;//данные из события основного (core) приложения с координатами точки на канвас             saveStep(saveImg, this.$props().commonProps.area_1);  //обычная функции из глобальной области для сохранения шагов, редактирования ctx.save();            ctx.putImageData(saveImg, 0, 0);ctx.beginPath();ctx.arc(point[0], point[1], radius, 0, 2*Math.PI, false);ctx.fillStyle =  color;ctx.fill();ctx.lineWidth = 1;ctx.strokeStyle =  color;ctx.stroke();                               //переменная из глобальной области для сохранения картинки после рисования saveImg = ctx.getImageData(0,0, srcWidth, srcHeight);ctx.restore();}}    }    }//добавляем описание модуля в общее описание приложения  HM.description.draw_circle_panel  = draw_circle_panel;//создаем контейнер модуля  HM.containerInit(div , HM.description, "draw_circle_panel");  HM.eventProps["emiter-operation-with"].emit(); //вызываем чтобы отключить слушателей canvas событий при старте модуля})()

В примере выше мы подключили контейнер с помощью функции: HM.containerInit(htmlLink, HM.description, module_name);
где HM ссылка на экземпляр приложения.

Для подключения массива нужно использовать функцию HM.arrayInit(htmlLink, HM.description, module_name);

В редакторе Collage_n модули подключаются в панели Загрузить модуль, изменить настройки.

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

Перевод Как Chrome DevTools с велосипеда на стандарт пересели

25.09.2020 14:16:06 | Автор: admin


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

Введение


Как вы, возможно, знаете, Chrome DevTools это веб-приложение HTML, CSS и JavaScript. За эти годы DevTools стала многофункциональной, умной и хорошо осведомленной о современной веб-платформе. Хотя DevTools расширился, его архитектура во многом напоминает первоначальную, когда инструмент был частью WebKit.

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

Вначале не было ничего


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

Первое упоминание о модульной системе в DevTools относится к 2012 году: это было введение списка модулей с соответствующим списком исходников. Часть инфраструктуры Python, используемой в то время для компиляции и сборки DevTools. В 2013 года модули были извлечены в файл frontend_modules.json этим коммитом, а затем, в 2014 году, в отдельные module.json (здесь). Пример module.json:

{  "dependencies": [    "common"  ],  "scripts": [    "StylePane.js",    "ElementsPanel.js"  ]}


С 2014 года module.json используется в инструментах разработчика для указания на модули и исходные файлы. Тем временем экосистема веба быстро развивалась, и было создано множество форматов модулей: UMD, CommonJS и в конечном итоге стандартизированные модули JavaScript. Однако DevTools застрял на module.json. Не стандартизированная, уникальной модульная система имела несколько недостатков:

  1. module.json требовал собственные инструменты сборки.
  2. Не было интеграции с IDE. Конечно же, она требовала специальных инструментов для создания файлов, понятных ей: (оригинальный скрипт генерации jsconfig.json для VS Code).
  3. Функции, классы и объекты были помещены в глобальную область видимости, чтобы сделать возможным совместное использование между модулями.
  4. Был важен порядок перечисления файлов. Не было никакой гарантии, что код, на который вы полагаетесь, загружен, кроме проверки человеком.

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

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


Мы выбрали модули JavaScript. Когда принималось это решение, модули в языке еще включались флагом в Node.js и большое количество пакетов NPM не поддерживало их. Несмотря на это, мы пришли к выводу, что модули JavaScript были лучшим вариантом.

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

Поскольку модули JavaScript были стандартными, это означало, что IDE, такие как VS Code, инструменты проверки типов, похожие на компилятор Closure/TypeScript, и инструменты сборки вроде Rollup и минификаторов смогут понять написанный исходный код. Более того, когда новый человек присоединяется к команде DevTools, ему не приходится тратить время на изучение проприетарного module.json.

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

Сколько стоит блеск новизны?


Несмотря на то, что модули JavaScript имели множество преимуществ, которые мы хотели бы использовать, мы оставались в мире module.json. Использование преимуществ модулей языка означало, что мы должны вкладывать значительные усилия в технический долг. Между тем, миграция могла вывести из строя функции и ввести баги регрессии.

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

Последний пункт оказался очень важным. Несмотря на то, что теоретически мы могли добраться до модулей JavaScript, во время миграции мы получили бы код, который должен был бы учитывать оба типа модулей. Такое не только технически сложно, но и означает, что все инженеры, работающие над DevTools, должны знать, как работать в такой среде. Они должны были бы постоянно спрашивать себя: Что происходит в этом коде, это module.json или JS, и как я могу внести изменения?.

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


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

  1. Убедиться, что стандартные модули максимально полезны.
  2. Убедиться, что интеграция с существующими модулями на базе module.json безопасна и не приводит к негативному воздействию на пользователя (ошибки регрессии, разочарование пользователя).
  3. Давать руководства по миграции DevTools. В первую очередь с помощью встроенных в процесс сдержек и противовесов, чтобы предотвратить случайные ошибки.

Электронная таблица, преобразования и технический долг


Цель была ясна. Но ограничения module.json оказалось трудно обойти. Потребовалось несколько итераций, прототипов и архитектурных изменений, прежде чем мы разработали удобное решение. Мы закончили тем, что написали проектный документ со стратегией миграции. В этом документе указывалась первоначальная оценка времени: 2-4 недели.

Самая интенсивная часть миграции заняла 4 месяца, а от начала до конца прошло 7 месяцев!


Первоначальный план, однако, выдержал испытание временем: мы хотели научить среду выполнения DevTools загружать все файлы, старым способом использовать перечисленные в массиве scripts module.json, в то время как все файлы, перечисленные в массиве modules должны были загружаться динамическим импортом языка. Любой файл, который будет находиться в массиве modules, может работать с import и export из ES6.

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


Фрагмент таблицы миграции здесь.

Фаза экспорта


Первым этапом было добавление операторов экспорта для всех сущностей, которые должны быть разделены между модулями/файлами. Трансформация автоматизировалась с помощью запуска скрипта для каждой папки. Допустим, в module.json есть такая сущность:

Module.File1.exported = function() {  console.log('exported');  Module.File1.localFunctionInFile();};Module.File1.localFunctionInFile = function() {  console.log('Local');};


Здесь Module это имя модуля. File1 имя файла. В дереве кода это выглядит так: front_end/module/file1.JS.

Код выше преобразуется в такой:

export function exported() {  console.log('exported');  Module.File1.localFunctionInFile();}export function localFunctionInFile() {  console.log('Local');}/** Legacy export object */Module.File1 = {  exported,  localFunctionInFile,};


Первоначально мы планировали переписать импорт в один файл на этом этапе. Например, в приведенном выше примере мы бы переписали Module.File1.localFunctionInFile на localFunctionInFile. Однако мы осознали, что было бы проще автоматизировать и безопаснее разделить эти два преобразования. Таким образом, перенос всех сущностей в один файл станет второй подфазой импорта.

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

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

В конце концов, обновление самой первой папки (добавление export) заняло около недели и несколько попыток с перекладываниями.

Фаза импорта


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

Например, следующие сущности module.json:

Module.File1.exported();AnotherModule.AnotherFile.alsoExported();SameModule.AnotherFile.moduleScoped();


Преобразуются в:

import * as Module from '../module/Module.js';import * as AnotherModule from '../another_module/AnotherModule.js';import {moduleScoped} from './AnotherFile.js';Module.File1.exported();AnotherModule.AnotherFile.alsoExported();moduleScoped();


Однако при таком подходе есть оговорки:

  1. Не каждая сущность была названа по принципу Module.File.symbolName. Некоторые сущности были названы Modele.File или даже Module.CompletelyDifferentName. Несоответствие означало, что мы должны были создать внутреннее сопоставление от старого глобального объекта к новому импортированному объекту.
  2. Иногда возникали конфликты имен moduleScoped. Наиболее заметно, что мы использовали шаблон объявления определенных типов Events, там, где каждая сущность названа просто Events. Это означало, что при прослушивании нескольких типов событий, объявленных в разных файлах, в операторе import у этих Events возникнет коллизия именования.
  3. Как оказалось, между файлами существовали циклические зависимости. Это было прекрасно в контексте глобальной области видимости, так как сущность использовалась после загрузки всего кода. Однако, если вам требуется импорт, циклическая зависимость проявит себя. Такое не приводит к проблеме сразу, если только у вас нет побочных вызовов функций в коде глобальной области видимости, (они были у DevTools). В общем, потребовалось некоторое хирургическое вмешательство и рефакторинг, чтобы обезопасить трансформацию.


Дивный новый мир модулей JavaScript


В феврале 2020 года, через 6 месяцев после старта в сентябре 2019 года, были выполнены последние очистки в папке ui/. Так миграция неофициально закончилась. Когда осела пыль, мы официально отметили миграцию как законченную 5 марта 2020 года.

Теперь DevTools работают только с модулями JavaScript. Мы все еще помещаем некоторые сущности в глобальную область видимости (в файлах легаси module.js) для устаревших тестов или интеграций с другими частями инструментов разработчика архитектуры. Они будут удалены со временем, но мы не рассматриваем их как блокирующие развитие. У нас также есть руководство по стилю работы с модулями JavaScript.

Статистика


Консервативные оценки количества CL (аббревиатура changelist термин, используемый в Gerrit, аналогичное пул-реквесту GitHub), участвующих в этой миграции, составляют около 250 CL, в основном выполняемых 2 инженерами. У нас нет окончательной статистики о размере внесенных изменений, но консервативная оценка измененных строк (сумма абсолютной разницы между вставками и удалениями для каждого CL) составляет примерно 30 000 строк, то есть около 20% всего интерфейсного кода DevTools.

Первый файл с применением экспорта поддерживается в Chrome 79, выпущенном в стабильном релизе в декабре 2019 года. Последнее изменение для перехода на импорт поставляется в Chrome 83, выпущенном в стабильном релизе в мае 2020 года.

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

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

Чему мы научились?


  1. Принятые в прошлом решения могут оказать долгосрочное влияние на ваш проект. Несмотря на то, что модули JavaScript (и другие форматы модулей) были доступны в течение довольно долгого времени, DevTools не был в состоянии оправдать миграцию. Решение о том, когда мигрировать, а когда нет, трудно и держится на обоснованных догадках.
  2. Первоначальные оценки времени недели, а не месяцы. В значительной степени это связано с тем, что мы обнаружили больше неожиданных проблем, чем ожидали при первоначальном анализе затрат. Даже при том, что план миграции был основательным, технический долг блокировал работу чаще, чем нам хотелось бы.
  3. Миграция включала большое количество (казалось, не связанных между собой) очисток технического долга. Переход к современному стандартизированному формату модулей позволил нам перестроить лучшие практики кодирования на современную веб-разработку. Например, мы смогли заменить пользовательский упаковщик Python на минимальную конфигурацию Rollup.
  4. Несмотря на большое влияние на нашу кодовую базу (~20% измененного кода), было зарегистрировано очень мало регрессий. Хотя было много проблем с миграцией первых двух файлов, через некоторое время у нас был надежный, частично автоматизированный рабочий процесс. Это означало, что негативное влияние на наших стабильных пользователей было минимальным.
  5. Научить коллег тонкостям конкретной миграции трудно, а иногда и невозможно. Миграции такого масштаба трудно отслеживать и они требуют большого объема знаний в предметной области. Передача этих знаний другим людям, работающим в той же кодовой базе, сама по себе нежелательна для работы, которую они выполняют. Знание того, чем делиться, а чем не делиться необходимое искусство. Поэтому крайне важно сократить количество крупных миграций или, по крайней мере, не выполнять их одновременно.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя онлайн-курсы SkillFactory:



Подробнее..

Перевод Compose повсюду композиция функций в JavaScript

25.09.2020 18:15:34 | Автор: admin
Перевод статьи подготовлен специально для студентов курса JavaScript Developer.Basic.





Введение


Кажется, библиотеки Lodash и Underscore теперь используются повсюду, и в них до сих пор есть известный нам суперэффективный метод compose.

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

Основы


Мы рассмотрим много функций Lodash, потому что 1) мы не собираемся писать собственные базовые алгоритмы это отвлечет нас от того, на чем я предлагаю сконцентрироваться; и 2) библиотека Lodash используется многими разработчиками, и ее можно без проблем заменить на Underscore, любую другую библиотеку или ваши собственные алгоритмы.

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

var compose = function(f, g) {    return function(x) {        return f(g(x));    };};


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

А теперь рассмотрим этот код:

function reverseAndUpper(str) {  var reversed = reverse(str);  return upperCase(reversed);}


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

var reverseAndUpper = compose(upperCase, reverse);


Теперь можно использовать функцию reverseAndUpper:

reverseAndUpper('тест'); // ТСЕТ


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

function reverseAndUpper(str) {  return upperCase(reverse(str));}


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

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

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

var compose = function() {  var funcs = Array.prototype.slice.call(аргументы);   return funcs.reduce(function(f,g) {    return function() {      return f(g.apply(this, аргументы));    };  });};


С такой функцией мы можем написать примерно такой код:

Var doSometing = compose(upperCase, reverse, doSomethingInitial); doSomething('foo', 'bar');


Существует много библиотек, в которых реализована функция compose. Наша функция compose нужна нам, чтобы понять, как она работает. Конечно, в разных библиотеках она реализована по-своему. Функции compose из разных библиотек решают одну и ту же задачу, поэтому они взаимозаменяемы. Теперь, когда нам понятно, в чем суть функции compose, давайте на следующих примерах рассмотрим функцию _.compose из библиотеки Lodash.

Примеры


Начнем с простого:

function notEmpty(str) {    return ! _.isEmpty(str);}


Функция notEmpty это отрицание значения, возвращаемого функцией _.isEmpty.

Мы можем добиться такого же результата с использованием функции _.compose из библиотеки Lodash. Напишем функцию not:

function not(x) { return !x; } var notEmpty = _.compose(not, _.isEmpty);


Теперь можно использовать функцию notEmpty с любым аргументом:

notEmpty('foo'); // truenotEmpty(''); // falsenotEmpty(); // falsenotEmpty(null); // false


Это очень простой пример. Давайте рассмотрим что-нибудь посложнее:
функция findMaxForCollection возвращает максимальное значение из коллекции объектов со свойствами id и val (значение).

function findMaxForCollection(data) {    var items = _.pluck(data, 'val');    return Math.max.apply(null, items);} var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data);


Для решения этой задачи можно использовать функцию compose:

var findMaxForCollection = _.compose(function(xs) { return Math.max.apply(null, xs); }, _.pluck); var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data, 'val'); // 6


Здесь есть над чем поработать.

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

function pluck(key) {    return function(collection) {        return _.pluck(collection, key);    }}


Функцию findMaxForCollection нужно еще немного подкрутить. Давайте создадим собственную функцию max.

function max(xs) {    return Math.max.apply(null, xs);}


Теперь можно сделать функцию compose более элегантной:

var findMaxForCollection = _.compose(max, pluck('val')); findMaxForCollection(data);


Мы написали собственную функцию pluck и можем использовать ее только со свойством 'val'. Возможно, вам непонятно, зачем писать собственный метод выборки, если в Lodash уже есть готовая и удобная функция _.pluck. Проблема в том, что _.pluck ожидает коллекцию в качестве первого аргумента, а мы хотим сделать по-другому. Изменив порядок следования аргументов, мы можем применить функцию частично, передав ключ в качестве первого аргумента; возвращаемая функция будет принимать данные (data).
Можно еще немного подшлифовать наш метод выборки. В Lodash есть удобный метод _.curry, который позволяет записать нашу функцию так:

function plucked(key, collection) {    return _.pluck(collection, key);} var pluck = _.curry(plucked);


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

function max(xs) {    return Math.max.apply(null, xs);} function plucked(key, collection) {    return _.pluck(collection, key);} var pluck = _.curry(plucked); var findMaxForCollection = _.compose(max, pluck('val')); var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data); // 6


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

var findMaxForCollection = _.compose(max, pluck('val'));


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

var data = [{id: 1, val: 5, active: true},             {id: 2, val: 6, active: false },             {id: 3, val: 2, active: true }];


Назовем эту функцию getMaxIdForActiveItems(data). Она принимает коллекцию объектов, отфильтровывает все активные объекты и возвращает максимальное значение из отфильтрованных.

function getMaxIdForActiveItems(data) {    var filtered = _.filter(data, function(item) {        return item.active === true;    });     var items = _.pluck(filtered, 'val');    return Math.max.apply(null, items);}


А можно сделать этот код поэлегантнее? В нем уже есть функции max и pluck, поэтому нам остается лишь добавить фильтр:

var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter); getMaxIdForActiveItems(data, function(item) {return item.active === true; }); // 5


С функцией _.filter возникает та же проблема, которая была с _.pluck: мы не можем частично применить эту функцию, потому что она ожидает коллекцию в качестве первого аргумента. Мы можем изменить порядок следования аргументов в фильтре, обернув начальную функцию:

function filter(fn) {    return function(arr) {        return arr.filter(fn);    };}


Добавим функцию isActive, которая принимает объект и проверяет, присвоено ли флагу active значение true.

function isActive(item) {    return item.active === true;}


Функцию filter с функцией isActive можно применить частично, поэтому в функцию getMaxIdForActiveItems мы будем передавать только данные.

var getMaxIdForActiveItems = _.compose(max, pluck('val'), filter(isActive));


Теперь нам нужно лишь передать данные:

getMaxIdForActiveItems(data); // 5


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

var isNotActive = _.compose(not, isActive); var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive));


Заключение


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

Ссылки


lodash
Hey Underscore, You're Doing It Wrong! (Эй, Underscore, ты все делаешь не так!)
@sharifsbeat



Читать ещё:


Подробнее..

Перевод Используем Chrome DevTools профессионально

25.09.2020 20:20:19 | Автор: admin
И снова здравствуйте. В преддверии старта курса JavaScript Developer. Professional перевели

11 советов для тех, кто использует Chrome в качестве среды разработки.





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



Иногда вы открываете консоль, чтобы посмотреть вывод своей программы, или вкладку Elements, чтобы проверить CSS-стили элементов DOM.



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

Для начала рассмотрим командное меню. Командное меню в Chrome это как командная оболочка в Linux. В нем вы можете писать команды для управления Chrome.

Открываем Chrome Developer Tools. Для доступа к командному меню используем горячие клавиши:

  • WindowsCtrl + Shift + P
  • macOSCmd + Shift + P


Открыть его можно и через графический интерфейс, вот так:



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



Расширенные функции скриншотов


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

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


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

Вот они:

  • Screenshot Capture full size screenshot (сделать снимок страницы целиком)
  • Screenshot Capture node screenshot (сделать снимок отдельного узла)


Пример


Откройте любую веб-страницу, например самые популярные статьи о JavaScript на Medium: medium.com/tag/javascript.

Откройте командное меню и выполните команду Screenshot Capture full size screenshot.



Мы сделали снимок всей текущей страницы целиком.


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

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

Сначала откройте вкладку Elements и выберите нужный элемент. Затем выполните команду.



Вот результат:



Использование результата последней операции в консоли


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

'abcde'.split('').reverse().join('')




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



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



$_ это специальная переменная, значение которой всегда равно результату последней выполненной в консоли операции. Этот прием сильно облегчает процесс отладки.



Повторная отправка запроса XHR


Во фронтенд-проектах часто приходится использовать XHR для отправки запросов на получение данных с сервера. Что делать, если нужно отправить запрос повторно?

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



  • Откройте вкладку Network.
  • Нажмите кнопку XHR.
  • Выберите запрос XHR, отправку которого вы хотите повторить.
  • Выберите Replay XHR в контекстном меню, чтобы повторить запрос.


Вот анимированный пример:



Отслеживание статуса загрузки страницы


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

В Chrome DevTools можно делать скриншоты страницы в ходе ее загрузки, поставив галочку напротив Capture Screenshots на вкладке Network.



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



Копирование переменных


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



ECMAScript не содержит определения функции copy, это функция Chrome. С ее помощью можно скопировать значение переменной JavaScript в буфер обмена.

Копирование изображения как URI с приставкой data:


Есть два способа вставить изображение на страницу: можно дать ссылку на внешний файл или внедрить изображение при помощи data: URL.

Data: URL (URL с приставкой data:) это схема, позволяющая встраивать небольшие файлы в документ в качестве строковых элементов.Раньше она называлась data: URI, но WHATWG отказалась от этого названия.

Встраивание маленьких изображений непосредственно в код по схеме data: URL сокращает количество HTTP-запросов к серверу, благодаря чему страница загружается быстрее.
Как это сделать в Chrome?

Посмотрите анимацию:



Вывод массива объектов в таблицу


Допустим, у нас есть массив объектов:

let users = [{name: 'Jon', age: 22},  {name: 'bitfish', age: 30},  {name: 'Alice', age: 33}]




Воспринимать такую информацию в консоли тяжело. А если массив длиннее и содержит более сложные элементы, то потеряться в нем еще проще.
К счастью, в Chrome есть функция, которая выводит массив объектов в таблицу.



Она вам пригодится, и не раз.

Перетаскивание на вкладке Elements


Иногда нужно переместить некоторые элементы DOM на странице, чтобы протестировать пользовательский интерфейс. На вкладке Elements можно перетащить любой HTML-элемент в любое место в коде:



В этом примере я перетащил элемент на вкладке Elements, и его расположение на веб-странице тоже моментально изменилось.

Обращение к текущему выделенному элементу в консоли


$0 это еще одна волшебная переменная, которая содержит элемент, выделенный на вкладке Elements.



Активация псевдоклассов CSS


Псевдоклассы позволяют задать стиль для элемента не только в зависимости от его расположения в дереве документа, но и в зависимости от внешних факторов, таких как история просмотра (например, :visited), состояние контента (например, :checked в некоторых формах), положение указателя мыши (например, псевдокласс :hover изменяет стиль элемента при наведении на него указателя мыши).

Для одного элемента можно написать несколько псевдоклассов. Чтобы было проще тестировать стили, псевдоклассы можно активировать прямо на вкладке Elements.



Пример


Посмотрите на код страницы:

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>body{font-size: 150px;}div:hover{color: red;}div:active{color: blue;}div:focus{color: brown;}</style></head><body><div>hello world</div></body></html>


Открываем страницу в браузере, на вкладке Elements проверяем, как работают псевдоклассы, и при необходимости вносим изменения.



Горячая клавиша для скрытия элементов


Во время отладки CSS-стилей часто возникает необходимость скрыть элемент. В Chrome это делается быстро: достаточно лишь выделить элемент и нажать клавишу H.


Нажмите H на клавиатуре

Эта операция применяет к элементу стиль visibility: hidden !important;.

Сохранение элемента DOM в качестве глобальной временной переменной


Если мы хотим быстро сослаться на элемент DOM в консоли, можно сделать это так:

  • Выбрать элемент.
  • Открыть контекстное меню правой кнопкой мыши.
  • Выбрать Store as a global variable (Сохранить как глобальную переменную).


Подробнее..

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

26.09.2020 22:13:23 | Автор: admin


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

Что такое Bootstrap


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

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

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

Startup


Startup это drag-n-drop конструктор Bootstrap-тем, который позволяет быстро создавать лендинги для бизнеса. Инструмент предлагает более 300 готовых блоков, которые можно использовать в интерфейсе. В несколько кликов собранный дизайн можно экспортировать в чистый HTMl, CSS, JavaScript.



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

Pinegrow


Это десктоп-редактор для macOS, Windows и даже Linux, который позволяет создавать Bootstrap-сайты. Это инструмент уже скорее для разработчиков и верстальщиков, ведь он позволяет углубляться в такие моменты, как верстка CSS-сеток и правил, rich visual controls, SASS и LESS и т.п.



Помимо прочего, с помощью Pinegrow можно создавать интерфейсы под фреймворк Foundation и WordPress.

Bootstrap Magic


Еще один инструмент создания тем для Bootstrap 4.0, который подойдет более опытным разработчикам. Это продукт с открытым кодом, который позволяет писать HTML-код прямо в специальном редакторе и тут же генерировать его превью.



Bootstrap Build


Это бесплатный билдер тем на Bootstrap 4 (и как уточняется, скоро появится поддержка пятой версии). Пользователи могут использовать до 500 элементов UI, а также создавать собственные темы на основе готовых шаблонов в специальном редакторе, а затем экспортировать результат работы в SASS-файлы.



Bootstrap Studio


Как и Pinegrow, это десктоп-приложение, но которое работает в формате drag-n-drop. Здесь есть большая библиотека встроенных компонентов, включая хедеры, футеры, галереи и слайдшоу.



Все это позволяет его использование для людей с небольшим опытом веб-разработки.

Codeply


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



Заключение


Это лишь некоторые интересные инструменты, которые позволят создавать красивые Bootstrap-сайты быстрее и делать их функциональными. Делитесь в комментариях ссылками на другие полезные продукты так наш список будет еще более полным.
Подробнее..

Принимаем криптовалютные платежи с Coinbase Commerce

27.09.2020 00:06:12 | Автор: admin


Если Вы планиуете подключать криптовалютные платежи и еще не знакомы с Coinbase Commerce, стоит потратить 5 минуты Вашего времени. Расскажу о подключении, настройке и поделюсь готовым open source решениями для Nodejs.


Coinbase Commerce это крипто-эквайринг без комиссий, паспортов, с отличным API и Вашим личным счетом.


Привет, Хабр!


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


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


Список доступных криптовалют: USD Coin, Dai, Bitcoin, Bitcoin Cash, Ethereum, Litecoin


Coinbase Commerce (далее CC).


Плюсы и минусы


  • Быстрая настройка
  • Нет комиссий клиент переводит деньги напрямую на Ваш счет
  • Принимает стейблкоины USD Coin & DAI, а также много других
  • Глобальный сервис у Америки в приоритете биржа "Coinbase", имеет фактор доверия даже со стороны государства. Для всего остального мира не принципиально
  • Нет посредников. Только Ваш кошелек и Ваш аккаунт
  • Отсутствие возвратных платежей. Конечно, клиенту можно вернуть средства по требованию, но это на Ваше усмотрение
  • Тестирование хуков с панели сервиса можно отправлять тестовые вебхуки

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


Регистрация



Процесс проходит в 5 этапов.


  1. Регистрация аккаунта email + пароль
  2. Подключение 2х этапной верификации
  3. Настройка кошелька
  4. Бекап кошелька
  5. Доступ к интерфейсу и прием платежей

Подключение 2х этапной верификации




Настройка кошелька



При создании кошелька, CC генерирует seed-фразы, которые нужно сохранить.




После ручного ввода seed-фраз, CC предлагает использовать Google Drive для бекапа кошелька.




Доступ к интерфейсу и прием платежей



Прием платежей


Есть два способа приема платежей, мы рассмотрим оба.


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

С использованием интерфейса



Создание позиции товара\услуги с фиксированной ценой.



Прием пожертвований.



Последним этапом, CC предлагает выбрать, какую информацию о пользователе будет собирать.



После того, как создаем позицию для оплаты, система нам предлагает варианты интеграции (ссылка или скрипт).




// link// https://commerce.coinbase.com/checkout/<id><a href="http://personeltest.ru/aways/commerce.coinbase.com/checkout/<id>" target="_blank">// embed<div>  <a class="buy-with-crypto"     href="http://personeltest.ru/aways/commerce.coinbase.com/checkout/<id>">Buy with Crypto</a>  <script src="http://personeltest.ru/aways/commerce.coinbase.com/v1/checkout.js?version=201807">  </script></div>

С использованием API


Вариант с API формирует платежку вручную и она активна ограниченное количество времени.


Схема работы


С клиента передаем информацию о сумме платежа на сервер, далее формируем временный
"checkout" и возвращаем ссылку https://commerce.coinbase.com/checkout/<id> на клиент, по которой пользователь переходит на страницу оплаты или происходит автоматическая переадресация.


В случае успешной оплаты, отмены или, если время платежки истечет, CC направит информацию на вебхук.


Настройка WebHook


Необходимо добавить url публичного вебхука Settings => Webhook subscriptions.



Протестировать работу вебхука можно на бесплатном сервисе Webhook.site.


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


// install$ npm install ngrok -g// `http://localhost:3000/coinbase-webhook` => `https://<ngrok-host>/coinbase-webhook`$ ngrok http 3000

Как вариант, можно использовать пример ответа вебхука с сайта Webhook.site и далее отправлять через Postman на локальную точку доступа.


После добавления вебхука, его можно протестировать. Интерфейс позволяет менять только события: charge:created, charge:confirmed, charge:failed, charge:delayed, charge:pending, charge:resolved.


Документация Webhook



Open Source


Подготовил вариант c использованием API coinbase-commerce-node.



Заключение


В целом, мне понравилась интеграции Coinbase Commerce. Поделитесь своим опытом подключения платежей для приложений.


Спасибо за внимание!

Подробнее..

Как я умный аквариум делал (frontend)

27.09.2020 22:21:48 | Автор: admin

Пролог



Как я рассказывал тут, я начал постройку умного аквариума на основе платы NodeMCU. На ней я использовал прошивку с micropython, поднял веб сервер и сделал API для манипуляции всеми периферийными устройствами и датчиками. Поскольку мой вариант умного аквариума изначально планировался как автономный, я хотел сделать некий UI для отслеживания всех процессов ну и для ручных корректировок. Каждый раз обращаться по роутам типа: http://192.168.1.70/led_controller?impulse=4000&level=200&ledName=white было очень муторно и неудобно. Особенно когда ты уже лег спать и под рукой только телефон. Да и опять же, хотелось получить levelup в разработке и сделать что-то увлекательное.


За основу UI взял Vue.js. Авторизация как таковая не нужна, т.к. мой "умный друг" был только локально в пределах моего WI-FI окружения. Да и если бы его взломали, ничего страшного не случилось. Другое дело когда я буду делать умный дом, там уже безопасность на первом месте, но сейчас не об этом. Итак, никакой авторизации, только SPA("Одностраничное приложение": "single page application"), никакого роутинга, все показатели и манипуляторы на одной странице. Из того что было сделано на backend контроль за LED-матрицами и температурный датчик. Создаем новый проект на гите, делаем клон на рабочем месте и запускаем vue-cli:


$ vue ui  Starting GUI...  Ready on http://localhost:8000


Создаем новый проект, добавляем туда все необходимые плагины:


  • vue-bootstrap сам себе дизайнер.
  • axios для работы с backend по API.
  • vuex для отделения бизнес логики

Для axios настроил базовый url


plugin/axios.js


import Vue from 'vue';import axios from 'axios';import VueAxios from 'vue-axios';axios.defaults.baseURL = 'http://192.168.1.70';Vue.use(VueAxios, axios);

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




App.vue
<template>  <div id="app">    <b-navbar type="dark" variant="primary" class="rounded">      <b-navbar-brand tag="h1" class="mb-0">Fish Tank</b-navbar-brand>      <b-icon         icon="brightness-alt-high"         font-scale="3"         variant="light"         class="rounded bg-primary p-1"      />    </b-navbar>    <list-of-range-controllers/>  </div></template><script>import ListOfRangeControllers from './components/ListOfRangeControllers';export default {    name: 'App',    components: {        ListOfRangeControllers    }}</script><style scoped>  #app {      margin: 50px 20px;  }</style>


Далее думал как организовать саму бизнес логику и отделить ее от шаблона. Решил попробовать полностью через Vuex Сам вьюкс не стал дробить, а сделал все в одном файлике. Для уровня LED я использую шкалу от 0 - 100 %, в то время когда на backend сам уровень света устанавливается от 0 - 1024 единиц. Округлив я подумал, что буду просто умножать на 10, когда данные будут уходить POST запросом или делить на 10, когда данные будут приходить GET запросом.


store/index.js


import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({    state: {        whiteLED         : 0,        waterTemperature : 0,    },    mutations: {        'SYNC_WHITE_LED' (state, level) {            state.whiteLED = level;        },        'SYNC_WATER_TEMPERATURE' (state, level) {            state.waterTemperature = level;        },        'SET_WHITE_LED' (state, level) {            state.whiteLED = level;        },        'SET_HEATER_LEVEL' (state, level) {            state.waterTemperature = level;        }    },    actions: {        async syncWhiteLED({commit}) {            try {                const response = await Vue.axios.get('/get_led_info?ledName=white');                commit('SYNC_WHITE_LED', response.data['level']/10);            }            catch(error) {                console.error(error);            }        },        async syncWaterTemperature({commit}) {            try {                const response = await Vue.axios.get('/get_water_tmp');                commit('SYNC_WATER_TEMPERATURE', response.data['water_temperature_c']);            }            catch(error) {                console.error(error);            }        },        async setWhiteLED({commit}, level) {            try {                await Vue.axios.get(`/led_controller?impulse=4000&level=${level*10}&ledName=white`);                commit('SET_WHITE_LED', level);            }            catch(error) {                console.error(error);            }        },        async setWaterTemperature({commit}, level) {            try {                await Vue.axios.get(`/heater_control?params=${level}`);                commit('SET_HEATER_LEVEL', level);            }            catch(error) {                console.error(error);            }        },    },    getters: {        whiteLED: state => {            return state.whiteLED;        },        waterTemperature: state => {          return state.waterTemperature;        },    }})


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

components/ui/RangeController.vue


<template>    <b-card         :title="header"     >        <b-alert show>            Change to : {{ controllerValue }}                         {{                            name.match(/Water/gi)                             ? 'C\u00B0' : '%'                        }}        </b-alert>        <b-form-input             type="range"            :min="min"            :max="max"            v-model="controllerValue"        />        <b-button             variant="outline-primary"             size="sm"            @click="$emit(`${buttonChangeName}Change`, controllerValue)"        >            {{ changeButton }}        </b-button>        <b-button             class="float-right"            variant="outline-success"             size="sm"            @click="$emit(`${buttonChangeName}Sync`)"        >            Sync value        </b-button>    </b-card></template><script>export default {    props: {        name: {            type    : String,            default : 'Header',        },        value: {            type    : Number,            default : 0,        },        buttonChangeName: {            type    : String,            default : 'Change'        },        min: {            type    : Number,            default : 0        },        max: {            type    : Number,            default : 100        }    },    data() {        return {            controllerValue: this.min,        }    },    computed: {        header() {            const isWater = this.name.match(/Water/gi);            const postfix = isWater ? 'C\u00B0' : '%';            const sufix = isWater ? 'Temperature' : this.name.match(/Pump/gi)? '' : 'LED';            return `${this.name} ${sufix} is : ${this.value} ${postfix}`;        },        changeButton() {            return `${this.buttonChangeName} change`;        },    }}</script>


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


components/ListOfRangeControllers.vue


<template>    <b-container class="bv-example-row mt-4 mb-4">      <h1>Backlight</h1>      <b-row>        <b-col v-for="led in leds" :key="led.name">          <range-controller            :name="led.name"            :value="led.value"            :buttonChangeName="led.buttonName"            v-on="{               ledWhiteChange : ledWhiteChange,              ledWhiteSync   : ledWhiteSync,            }"          />        </b-col>      </b-row>      <h1>Temperature</h1>      <b-row>        <b-col>          <range-controller              name="Water"              :value="waterTemperature"              :min="20"              :max="45"              buttonChangeName="temperature"              @temperatureChange="temperatureChange"              @temperatureSync="temperatureSync"          />        </b-col>      </b-row>    </b-container></template><script>import RangeController from './ui/RangeController';import { mapActions, mapGetters } from 'vuex'export default {    components: {        RangeController    },    methods: {        ...mapActions([            'syncWhiteLED',            'syncWaterTemperature',            'setWhiteLED',        ]),        ledWhiteChange(value) {            this.setWhiteLED(value);        },        // не реализовано        temperatureChange(value) {            console.log('temp is changed!' + `${value}`);        },        ledWhiteSync() {            this.syncWhiteLED();        },        async temperatureSync() {            await this.syncWaterTemperature();            console.log(this.waterTemperature);        },    },    computed: {        ...mapGetters([            'waterTemperature',            'whiteLED',        ]),        leds() {            return [                {                    name: 'White',                    value: this.$store.getters.whiteLED,                    buttonName: 'ledWhite',                },            ]        },    },}</script>

На компе


На мобилке



Вот я и получил UI для моего умного аквариума, где я мог получить информацию об освещенности и температуре, и в ручном режиме выставить нужный свет и его интенсивность. Пришло время все это запустить вместе, повесить над аквариумом и проверить. Vue приложение запустил на старом ноуте, лег на кровать и открыл браузер на телефоне чтож верстка немного поехала на небольшом экране, но меня вполне устраивала, я знал, что все это еще будет переделываться и автоматизироваться. Но это была рабочая связка моего устройства на NodeMCU и Vue приложения. Я был рад и горд собой. В голове летали мысли о том, что же будет в конечном итоге, самое страшное для меня было реализация химического анализа воды. Ведь хороший анализ делается путем опускания в воду бумажных палочек, пропитанных определенным химическим составом. От чего она меняет цвет и уже по карте цветов можно определить есть ли каки либо отклонения от нормы. А анализ нужен не один, а именно, анализы на:
  • Аммоний
  • Нитриты
  • Нитраты
  • Фосфаты
  • Кислотно-щелочной баланс (Ph)
  • Карбонатная жесткость (kH)
  • Кальций
  • Магний
  • Силикаты

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


Подробнее..
Категории: Javascript , Vuejs , Ui , Frontend

Никогда не писали автотесты? Попробуйте Cypress

28.09.2020 20:13:28 | Автор: admin

Никогда не писали автотесты? Попробуйте Cypress


Автотесты на Cypress
Первое впечатление и встречающиеся проблемы


Дмитрий Кочергин, Lead Software Developer Luxoft

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

Статья будет интересна всем, кто интересовался автотестированием с нуля на JS, но боялся спросить.

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

Быстрый поиск по интернету дал более молодые и перспективные инструменты: WebDriver.IO, Pupeteer (а сейчас лучше Playwright) и Cypress. Выбрал последний, купился на красивые обещания и несколько комментов из холиваров по лучшим инструментам для автотестов.

Так выглядит окно браузера запущенного теста. Слева выполненные команды теста и статус, справа просмотр приложение во время исполнения теста:
image

Вот так выглядит код теста (в Cypress весь код на JS, а селекторы это обычные CSS селекторы):
image

В runtime выглядит так:
image

Пока искал инструмент для автотестов в голове у меня была картинка, что тесты можно создавать прямо в браузере, чтобы просто нажал на запись скрипта система записала мои действия (CSS селекторы элементов, на которые я нажимал). Потом я смотрю сгенерированный тест, подправляю селекторы если нужно, и сохраняю в Test Suite. Эх

Сказка оказалась тогда недостижимой и таких инструментов я не нашел (возможно кто-то в комментах подскажет правильный путь). Но в Cypress подкупило то, что тесты выполняются в настоящем браузере, и даже можно параллельно с выполнением теста исследовать DOM любимыми инструментами разработчика Chrome (если например селектор не сработал можно открыть консоль и сразу при исполнении теста посмотреть почему).
image

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

Итак, Cypress, первая страница фреймворка говорит нам что это JavaScript End to End Testing Framework (cypress.io). Далее бегло читаем документацию, она действительно полная, и можно найти ответы практически на все вопросы (все остальное я быстро находил на StackOverflow):
image

Дальше по списку фичей с сайта:
  • Time travel каждый шаг выполнения теста в консоли можно нажать и вернуться на конкретное состояние всего приложения в прошлом, которое отображается прямо в браузере. И это не просто записанная картинка, а реальный DOM, можно откатиться и исследовать страницу через Chrome devtools.
  • Real time reloads как во всей модерн JS теме, поменяли исходники в тот же момент произошёл перезапуск теста в браузере (hot reload).
  • Automatic waiting многие инструкции теста асинхронные, как то переход на страницу, и Cypress сам автоматически дожидается окончания загрузки. Кроме конечно момента, когда вызовы сервера делает приложение.
  • Network traffic control Cypress может захватывать/видеть вызовы сервера и можно задать инструкцию, чтобы подождать ответ от сервера.
  • Screenshots and videos во время выполнения теста Cypress записывает видео экрана (MP4) браузера, вместе с инструкциями в окне.

Все это добро конечно же можно запускать и без открытого браузера на CI, используется headless Chrome или Electron.

Но были и некоторые проблемы.

По началу я не знал, что можно ждать окончания XHR запросов на сервер, и вставлял вместо них timeouts, и тесты рандомно падали. Поправил.
image

Потом через раз работал Hot Reload, постоянно приходилось перезапускать весь Cypress, потому что не было уверенности, что мои изменения применились. Оказалось, что в моей IDE (IntelliJ IDEA) есть такие нехорошие галочки, которые еще и включены по умолчанию, из-за которых получается что сохранение файла это не сохранение, а eventual сохранение.
image

Следующая проблема была в том, что мое приложение использовало window fetch для запросов на сервер, а Cypress видит только XHR запросы. Dirty hack из StackOverflow помог (я так понял, что метод fetch удаляется из window, чтобы браузер сделал fallback на XHR вместо fetch):
image

Далее встала проблема эмуляции мобильного браузера, просто в коде теста user agent перезаписать не получилось, но в отдельном специальном файле все получилось.
image

Далее решение CORSпроблемы:
image

Потом file upload, с наскоку не получилось, стандартные решения не сработали, зато помогла библиотека cypress-file-upload:
image

Единственная проблема которую я не смог решить это воспроизводимость теста. А именно стабильные и одинаковые начальные данные для запуска теста (fixtures), это более конфигурационная задача, а не Cypress, но все равно пока нерешенная.

В итоге, Cypress выглядит отличным инструментом для внедрения автотестирования в проект, если:
  1. Есть знание JS
  2. Нет необходимости тестировать во всех видах браузеров начиная с IE6 (на данный момент Cypress поддерживает Chrome, Chromium, Edge, Electron, Firefox). Вот обсуждение темы. Но могу сказать, что год назад, когда я начинал работать с Cypress он поддерживал только последнюю версию Chrome и Electron для запусков без UI.
  3. Хотите быстро сделать тесты и забыть о них, пока кто-то не сломает приложение :)

Cypress: берите и пользуйтесь!
Подробнее..

Нестабильная сортировка в JavaScript

29.09.2020 12:13:11 | Автор: admin

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

  • Зачем это нужно знать, если есть встроенные методы сортировки?

  • Зачем изобретать велосипед заново?

  • Это нужно, чтобы пройти собеседование, объективно больше незачем это знать

  • В "любой движок javascript" работают не дураки, и уже сделали все как нужно

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

Сразу к делу

Как думаете, что произойдет после выполнения данного кода?

Кажется, что ничего странного, но есть нюансы.

Случай номер раз

Сделали задачу, техническое решение, код, unit-тесты. По бизнес-процессу тоже все хорошо. При поверхностном тестировании проблем не нашли. Но когда дело дошло до авто-тестов, начались странности. Конфигурация, которую просчитывал Node.js 10, в основном отдавала корректный результат, но иногда конфигурация отличалась. Что усложняло процесс поиска проблемы, учитывая, что в режиме отладки конфигурация отдавалась всегда корректной. А еще у одних членов команды некорректная конфигурация воспроизводилась, а у других нет. И мы сделали вывод, что в старой версии, наверное, баг, и решили обновить версию на более новую.

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

Случай номер два

На этот раз конфигурации отличались уже в разных версиях браузера: в Google Chrome 80 был корректный результат, а в его 69 версии нет. И тут нам стало интересно, почему же конфигурация отличается.

  • Увидел, что версии отличаются

  • Открыл Release notes Google Chrome

  • Обнаружил, что Google Chrome 69 это последняя сборка, где используется 6-я версия V8

  • Открыл Release notes V8

  • И начал просматривать все статьи между 6 и 7 версией V8

Спустя некоторое время я наткнулся на статью Getting things sorted in V8, в которой говорится, что с 7-й версии V8 переходит на стабильный алгоритм сортировки TimSort, вместо предыдущего нестабильного QuickSort. И тут сразу стало понятно, что никакой магии нет, и все эти баги из-за нестабильной сортировки.

Нюансы сортировки

Сортировка в Node.js 10.22 (движок V8 v6.8) QuickSort.

Как видите, массив из первого скриншота изменил порядок, хотя функция сравнения всегда возвращала 0.

Сортировка в Node.js 14.5 (движок V8 v7.0) TimSort.

На этот раз сортировка уже не изменила данные.

Как дальше жить

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

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

Сравнение разных вариантов решения с нативной реализацией

Мы решили сравнить:

  • lodash.sortby

  • WikiSort javascript адаптация (WikiSort)

  • QuickSort нативная реализация V8 (node.js 10.22.0)

  • TimSort нативная реализация V8 (node.js 14.5.0)

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

Из этого графика можно сделать вывод: после оптимизаций, которые провел движок V8, сортировка WikiSort не уступает нативной реализации TimSort, хотя при первых сортировках разница довольно заметная. А вот lodash я бы не стал советовать.

Посмотреть результаты теста подробнее можно тут sort-test-js, а исходный код тут Tihon-Ustinov/sort-test-js

Где стабильно?

версия

дата релиза

движок JavaScript

стабильность

Node.js

11.0.0

2018-10-23

V8 7.0.276.28

+

Node.js

10.22.0

2020-07-21

V8 6.8.275.32

-

Google Chrome

70.0.3538

2018-10-16

V8 7.0.276

+

Google Chrome

69.0.3497

2018-09-04

V8 6.9.427

-

Выводы

  • Не верьте в магию JavaScript, у всех странных кейсов в этом языке есть объяснение

  • Изучайте технологии, которые вы используете

  • Читайте официальную документацию

  • Следите за тем, что появляется в новых версиях

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

Подробнее..

Термостат на ThingJS (beta)

29.09.2020 18:09:04 | Автор: admin


Почти год назад я представил свой pet-проект IoT платформу ThingJS. Честно сказать, я не достиг всех целей, которые ставил перед собой публикуя ту статью. Но работа окупилась. Удалось получить нечто иное полезную критику.


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


Но сначала немного общей информации.



Введение


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


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


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


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


Если приводить простейшую аналогию, платформа напоминает ОС. Ее развитием занимаются майнтейнеры ядра (core) и разработчики драйверов (Resource Interfaces). Прикладные разработчики делают важные и нужные программы (приложения платформы), которые оживляют UI девелоперы. Все это, конечно, для любимого пользователя.


Слой разработки покрыт профессиональным стеком. Нет специально выдуманных IDE. Только проф: СLion, CMake, webpack, npm и т.д. Лучшие практики полностью релевантны при разработке для ThingJS. TDD инструмент в комплекте.


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


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


Архитектура


В настоящий момент runtime платформы представляет из себя прошивку для контроллеров ESP32. Любой желающий может расширять ее функциональность и адаптировать под себя.


На базе прошивки функционируют приложения платформы. Приложение это комплект backend и frontend скриптов поставляемых в специальном файле бандле приложения. В него входят:
Манифест приложения (обязательно);


  • Frontend (опция);
  • Backend (опция);
  • Иконка приложения (обязательно);
  • Предзаполненные хранилища (опция);
  • Мультиязычный пакет (опция);
  • Прочие файлы (опция).

Backend скрипты исполняются контроллером. Для этого используется облегченный JavaScript mJS.


Frontend почти классическое SPA WEB-приложение. Отличие заключается во встроенных интерфейсах взаимодействия с контроллером. Взаимодействуют части приложения исключительно через два интерфейса: UBUS и Storage.



UBUS


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


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


Сообщения могут распространяться любыми доступными средствами. На схеме контроллер и frontend обмениваются сообщениями по WEBSocket.


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


Важным свойством шины является негарантированная доставка сообщений.


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


Storage


Формализованное хранилище приложения. Описывается в манифесте. Доступ к нему имеет как frontend, так и backend.


Хранилище не является СУБД. Необходимо рассматривать его как файл, в который могут вносить изменения несколько потоков. Когда в нем происходят изменения, платформа генерирует событие об изменении в хранилище. Событие генерируется тогда, когда данные сохранены.


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


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


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


API


Базовый набор функций платформы реализуется через API. Например, доступ к ресурсам через глобальный интерфейс $res или к шине через $bus.


Перечень API функций минимизирован для упрощения и унификации. Все, что возможно, вынесено в понятие ресурсных интерфейсов (Resource interfaces).


Resource interfaces


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


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


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


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


Разделение ресурсов


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


Разделение ресурсов возможно двумя путями:


  • При установке приложение запрашивает ресурсы и пользователь по своему выбору делегирует их;
  • Приложение требует совершенно конкретные ресурсы.

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


Но такой подход требует определенных знаний у пользователя. Эта концепция хорошо подходит для любительской разработки.


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


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


Ресурсы делятся на физические и виртуальные. К физическим ресурсам относятся, например, GPIO, внутренние таймеры, UART драйвера и т.п. Эти ресурсы лимитированы. К виртуальным ресурсам можно отнести софтовые таймеры, TCP сокеты и т.д. Их количество, условно, не ограничено.


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


Термостат на ThingJS


Готовая реализация приложения Thermostat в исходниках доступна тут.


Прикладное назначение


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


Аппаратное обеспечение


Для проекта выбран чип ESP32 являющийся приемником известного ESP8266. Это, пожалуй, самый бюджетный и функциональный чип на рынке сегодня. Его тактовая частот 240МГц, два ядра, ОЗУ до 520Кб, Bluetooth, Wifi. Разнообразные GPIO. Немаловажным является наличие аппаратного криптографического ускорителя: ES, SHA-2, RSA, ECC, RNG.


Оптимальным вариантом для энтузиаста станет ESP32-DevKitC.



Развертывание среды разработки приложений


Разработка приложений ведется в хорошо знакомом для JavaScript разработчиков окружении VUE CLI. Можно смело утверждать, что любой frontend разработчик быстро разберется со средой и получит массу удовольствия при работе с ней.


Ключевыми преимуществами среды являются:


  • Внутрисистемная отладка Вы можете отлаживать работу приложения непосредственно на контроллере.
  • Горячая перезагрузка скрипта на контроллер (hot reload) Работает аналогично горячей перезагрузкой скриптов в браузере. Только загружает скрипты на контроллер. Это радикально упрощает разработку.
  • Локальный dev сервер Разрабатывать приложения вы сможете в привычной среде локального dev-сервера на NodeJS. При этом, нужные запросы будут отсылаться на физический контроллер. В некоторых случаях разработка приложений вообще не требует железа.

Развертывание несложное:


git clone --branch beta https://github.com/rpiontik/ThingJS-frontcd ThingJS-frontnpm install

Далее, запускаем dev сервер:


npm run dev

Перейдите в браузере по ссылке http://0.0.0.0:8080/. Откроется вполне рабочая платформа в dev-режиме. Все приложения будут почти работать.



Чтобы приложения работали на 100%, нужно подключить среду к железу. Воспользуйтесь пользовательским пакетом, чтобы подготовить устройство. Подключите его к локальной сети. Затем, в файле /config/dev.env.js укажите IP устройства.


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


Теперь перезапустите dev сервер и запустите приложение thermostat с рабочего стола. Приложение должно полноценно функционировать.



Для эксперимента, внесите изменения в файл src/applications/thermostat/scripts/thermostat.js. Например, поставьте вначале команду debugger. Сохраните файл.


Скрипт будет загружен на контроллер, а dev-среда отобразит запрос на отладку:


Кликнув по ссылке Start debugger вы попадете в отладчик.



Код можно выполнять пошагово. Есть возможность мониторить значение переменных в watch панели справа. Снизу выводится лог. А слева навигатор по физическому контроллеру.


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


npm run build

Собранные бандлы приложений будут размещены в папке dist/apps/


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


Создание проекта приложения Thermostat


Для создания нового приложения достаточно скопировать и доработать наиболее подходящий из уже существующих примеров. Исходные коды приложения находятся в папке /src/applications/. Для термостата основой станет blink. При копировании папки указывается новое название thermostat.


Далее следует сконфигурировать приложение. Делается это через manifest.json. Именно в этом файле содержится вся ключевая информация о развертывании приложения и его работе. Меняем его под нужды нового приложения и одновременно модифицируем код.


Общее описание приложения


"name": "Thermostat","vendor": "rpiontik","version": 1,"subversion": 0,"patch": 0,"description": {    "ru": "Термостат",    "en": "Thermostat"},

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


components


Блок описывает frontend компоненты приложения.


"components": { "thermostat-app": {   "source": "thermostat.js",   "intent_filter": [     {       "action": "thingjs.intent.action.MAIN",       "category": "thingjs.intent.category.LAUNCH"     }   ] }},

Приложение содержит один компонент thermostat-app. Его код расположен в файле thermostat.js. Компонент будет вызван при возникновении намерения удовлетворяющего фильтру указанному в intent_filter. Описанный фильтр соответствует намерению запуска приложения.


Переименовываем файл blink.js -> thermostat.js Код будет таким:


import App from './Thermostat.vue';import Langs from './langs';$includeLang(Langs);$exportComponent('thermostat-app', App);

В листинге подключается VUE компонент Thermostat.vue. Именно он будет реализовывать интерфейс пользователя. Необходимо связать идентификатор компонента с его реализацией. Это делается так:


$exportComponent('thermostat-app', App);

Почему так? Ведь в манифесте уже указана связь. На самом деле нет. Манифест сообщил системе, что результатом выполнения файла thermostat.js станет регистрация VUE компонента с идентификатором thermostat-app. Но в этом же файле могут происходить другие действия по инициализации компонента. В этом случае подключается мультиязычный пакет langs.js.


Для завершения описания frontend осталось реализовать сам компонент Thermostat.vue. Шаблон я спрячу подкатом. Рассмотрим код.


template
<template>  <v-flex fill-height style="max-width: 600px">    <h1>{{ 'TITLE'|lang }}</h1>    <v-container>      <v-layout>        <v-flex xs12 md12>          {{ 'DESCRIPTION'|lang }}        </v-flex>      </v-layout>    </v-container>    <v-tabs        centered        icons-and-text    >      <v-tab href="#tab-1">        {{ 'CONTROL'|lang }}        <v-icon>dashboard</v-icon>      </v-tab>      <v-tab href="#tab-2">        {{ 'CLOUD'|lang }}        <v-icon>cloud</v-icon>      </v-tab>      <v-tab-item value="tab-1">        <v-container>          <v-layout>            <v-flex class="current-temp" xs12 md4>                <span>                  <template v-if="state.temp !== null">                    {{ state.temp.toFixed(1) }}                  </template>                  <template v-else>                    --.--                  </template>                </span>            </v-flex>            <v-flex xs12 md4 style="text-align: center; padding: 12px; ">              <template v-if="state.state === 1">                <v-icon                    title="Power on"                    class="indicator"                >power                </v-icon>              </template>              <template v-else-if="state.state === 0">                <v-icon                    title="Power off"                    class="indicator"                >power_off                </v-icon>              </template>            </v-flex>            <v-flex xs12 md4 style="text-align: center; padding: 12px;">              <template v-if="!!state.connected">                <v-icon                    title="Connected"                    class="indicator"                >cloud                </v-icon>              </template>              <template v-else>                <v-icon                    title="Disconnected"                    class="indicator"                >cloud_off                </v-icon>              </template>            </v-flex>          </v-layout>        </v-container>        <v-container grid-list-xl>          <v-layout>            <v-flex xs12 md3>              <v-select                  label="Mode"                  :items="modes"                  v-model="state.mode"                  @change="onChangeMode"              ></v-select>            </v-flex>            <v-flex xs12 md9>              <v-slider v-if="state.mode <= 1"                        thumb-label="always"                        v-model="state.target"                        :disabled="!state.target"                        @change="onChangeTarget"              ></v-slider>            </v-flex>          </v-layout>        </v-container>      </v-tab-item>      <v-tab-item value="tab-2">        <v-container>          <p>            Android applications:            <ul>              <li><a href="http://personeltest.ru/aways/play.google.com/store/apps/details?id=net.routix.mqttdash" target="_blank">MQTT Dash (RUS)</a></li>              <li><a href="http://personeltest.ru/aways/play.google.com/store/apps/details?id=snr.lab.iotmqttpanel.prod" target="_blank">IoT MQTT Panel (EN)</a></li>            </ul>          </p>          <p>            Server params:            <ul>              <li>Address: mqtt.eclipse.org</li>              <li>port: 1883</li>            </ul>          </p>          <table class="topic-table">            <tr>              <th>{{ 'TOPIC'|lang }}</th>              <th>{{ 'TOPIC_DESCRIPTION'|lang }}</th>            </tr>            <tr>              <td>/thingjs/{{ state.chip_id }}/temp</td>              <td>{{ 'TOPIC_TEMP_DESC'|lang }}</td>            </tr>            <tr>              <td>/thingjs/{{ state.chip_id }}/state</td>              <td>{{ 'TOPIC_STATE_DESC'|lang }}</td>            </tr>            <tr>              <td>/thingjs/{{ state.chip_id }}/target/out</td>              <td>{{ 'TOPIC_TARGET_OUT'|lang }}</td>            </tr>            <tr>              <td>/thingjs/{{ state.chip_id }}/target/in</td>              <td>{{ 'TOPIC_TARGET_IN'|lang }}</td>            </tr>            <tr>              <td>/thingjs/{{ state.chip_id }}/mode/out</td>              <td>{{ 'TOPIC_MODE_OUT'|lang }}</td>            </tr>            <tr>              <td>/thingjs/{{ state.chip_id }}/mode/in</td>              <td>{{ 'TOPIC_MODE_IN'|lang }}</td>            </tr>          </table>        </v-container>      </v-tab-item>    </v-tabs>  </v-flex></template>

data () {   return {       modes: [ // Доступные режимы срабатывания термостата.           { text: 'Less then', value: 0 },           { text: 'More then', value: 1 },           { text: 'On', value: 2 },           { text: 'Off', value: 3 }       ],       isHold: false, // Флаг ожидания. Если он установлен, приложение не обрабатывает сообщения из шины.       state: { // Последнее актуальное состояние           connected: null, //  Состояние соединения с MQTT брокером           mode: null, // Текущий режим работы           target: null, // Целевая температура           temp: null, // Текущая температура           state: null, // Состояние нагрузки (вкл/выкл)           chip_id: null // Уникальный идентификатор для MQTT брокера.       }   };}

Дам дополнительные пояснения:


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


chip_id Уникальный идентификатор чипа. Он необходим для создания уникальных топиков MQTT брокера.


mounted () {   this.$bus.$on($consts.EVENTS.UBUS_MESSAGE, (type, data) => {       if (this.isHold) return;       switch (type) {       case 'thermostat-state':           this.state = JSON.parse(data);           break;       }   });   this.refreshState();},

При монтировании компонента происходит подписка на сообщения шины. Обрабатывается событие thermostat-state. В сообщении содержится структура состояния контроллера. Если установлен флаг isHold обработка событий не происходит.


refreshState () {   this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-refresh-state');},

Метод отправляет сообщение для принудительного обновления статуса контроллера. В ответ контроллер отправляет свое состояние.


flushData () {   if (this.isHold) { clearTimeout(this.isHold); }   this.isHold = setTimeout(() => {       this.isHold = null;       this.refreshState();   }, 1000);},

Метод приостанавливает на секунду обработку сообщений из шины. Затем, запрашивает текущее состояние.


onChangeTarget (val) {   this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-set-target', val);   this.flushData();},onChangeMode (val) {   this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-set-mode', val);   this.flushData();}

При изменении параметров они передаются через шину на контроллер.


Вот в общем-то и все с frontend. Осталось оживить контроллер.


requires


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


"requires": { "interfaces": {   "mqtt": {     "type": "mqttc",     "required": true   },   "timers": {     "type": "timers",     "required": true,     "description": {       "ru": "Таймеры системы",       "en": "System timers"     }   },   "ds18x20": {     "type": "DS18X20",     "required": true   },   "relay": {     "type": "bit_port",     "required": true,     "default": 2,     "description": {       "ru": "Реле",       "en": "Relay"     }   },   "sys_info": {     "type": "sys_info",     "required": true,     "description": {       "ru": "Информация о системе",       "en": "System information"     }   } }}

В листинге описывается необходимость в ресурсах:


  • mqttc Интерфейс интеграции по MQTT протоколу. Потребуется приложению для дистанционного управления.
  • timers Системные таймеры. Скрипт будет ежесекундно проверять температуру с датчиков и принимать решение о включении или отключении нагрузки.
  • DS18X20 Интерфейс к шине OneWire с подключенными датчиками температуры.
  • bit_port Интерфейс дискретного управления каналом. Именно он будет управлять реле нагрузки.
  • sys_info Интерфейс общесистемной информации. Необходим для получения уникального идентификатора устройства.

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


Может быть выделено несколько ресурсов одного и того же типа. Например, можно указать несколько ресурсов типа DS18X20. В этом случае, приложение получит возможность работать с несколькими шинами OneWire.


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


scripts


Блок содержит информацию о скрипте исполняемом на стороне контроллера.


"scripts": { "entry": "thermostat", "subscriptions": ["tmst-refresh-state", "tmst-set-target", "tmst-set-mode"], "modules": {   "thermostat": {     "hot_reload": true,     "source": "scripts/thermostat.js",     "optimize": false   } }},

  • entry Точка входа при выполнении скриптов. Содержит идентификатор модуля. Модулей скриптов может быть несколько. Поле необходимо для указания платформе с какого скрипта начинать выполнение.
  • subscriptions Подписка на события в шине. Скрипт начнет выполняться только при наступлении какого-то из описанных событий. Пока событие не наступило, скрипт находится в спячке. Это позволяет оптимизировать использование ресурсов в платформе.
  • modules Список моделей скриптов.
    • thermostat идентификатор модуля.
    • hot_reload Признак горячей загрузки скриптов на контроллер. Если он установлен в true, изменении скрипта в проекте будут тут же загружаться на контроллер. Скрипт будет перезапускаться. Для работы фичи требуется запущенный dev-сервер.
    • source Путь к коду модуля.
    • optimize Если true, скрипт будет оптимизироваться средствами webpack.

Для начала необходимо переименовать файл scripts/blink.js в scripts/thermostat.js. Теперь в коде определить константы.


let MQTT_SERVER = 'wss://mqtt.eclipse.org:443/mqtt';

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


let CHIP_ID = $res.sys_info.chip_id;

Это первый случай использования подключенного ресурса в скрипте. Все выделенные ресурсы доступны через глобальную переменную $res. Ключом ресурса является ключ указанный в манифесте. В данном случае через ресур sys_info константа инициализируется уникальным идентификатором устройства. Он потребуется для определения топиков MQTT брокера.


let TOPIC_TEMP = '/thingjs/' + CHIP_ID + '/temp';let TOPIC_TARGET_OUT = '/thingjs/' + CHIP_ID + '/target/out';let TOPIC_TARGET_IN = '/thingjs/' + CHIP_ID + '/target/in';let TOPIC_MODE_OUT = '/thingjs/' + CHIP_ID + '/mode/out';let TOPIC_MODE_IN = '/thingjs/' + CHIP_ID + '/mode/in';let TOPIC_MODE_STATE = '/thingjs/' + CHIP_ID + '/state';

Константы определяют топики MQTT брокера. Некоторый топики имеют постфиксы out и in. Они указывают направление данных относительно контроллера. out из контроллера, in в контроллер.


Константы режимов работы:


// Включение нагрузки если значение целевой температуры выше фактической. Актуально для нагревателей.let MODE_LESS = 0;// Включение нагрузки если значение целевой температуры ниже фактической. Актуально для охладителей.let MODE_MORE = 1;// Принудительное включение нагрузки.let MODE_ON = 2;// Принудительное отключение нагрузки.let MODE_OFF = 3;

Переменные:


// Флаг установленного соединения с MQTT брокеромlet isConnected = false;// Текущий режим работыlet mode = MODE_LESS;// Целевая температураlet target = 32;// Состояние нагрузкиlet state = 0;// Адрес сенсора температурыlet sensor = null;// Текущая температураlet temp = null;// Если сенсор не найден, приложение будет эмулировать его наличие через эту переменную. Используется для демонстратора. В боевом приложении этот функционал следует удалить.let fakeVector = 0.5;

Начинается самое интересное. Вызывается функция поиска датчиков на шине OneWire. В переменную sensor записывается адрес первого найденного датчика.


$res.ds18x20.search(function (addr) {   if (sensor === null) {       sensor = addr;   }});function publishState () {   $bus.emit('thermostat-state', JSON.stringify({       connected: isConnected,       mode: mode,       target: target,       temp: temp,       state: state,       chip_id: CHIP_ID   }));   if (isConnected) {       $res.mqtt.publish(TOPIC_MODE_OUT, JSON.stringify(mode));       $res.mqtt.publish(TOPIC_TARGET_OUT, JSON.stringify(target));       $res.mqtt.publish(TOPIC_MODE_STATE, JSON.stringify(state));       $res.mqtt.publish(TOPIC_TEMP, JSON.stringify(temp));   }}

publishState публикует состояние термостата сразу в два места:


  • В шину данных UBUS. Эта шина является встроенной в платформу. Нет необходимости указывать потребность в ней в манифесте. Публикуемые данные получат все, кто подписался на это события. Как скрипты на стороне контроллера, так и на стороне frontend. Именно так скрипт на контроллере общается с frontend.
  • В MQTT брокер. Эта публикация является опциональной. Она происходит только при активном соединении с брокером.

Далее устанавливаются обработчики событий MQTT клиента:


// При установке соединения происходит подписка на топики$res.mqtt.onconnected = function () {   print('MQTT client is connected');   isConnected = true;   $res.mqtt.subscribe(TOPIC_TARGET_IN);   $res.mqtt.subscribe(TOPIC_MODE_IN);   publishState();};// Отслеживается разрыв соединения и меняется online статус$res.mqtt.disconnected = function () {   print('MQTT client is disconnected');   isConnected = false;   publishState();};// Получение данных по подписке с MQTT брокера$res.mqtt.ondata = function (topic, data) {   print('MQTT client received from topic [', topic, '] with data [', data, ']');   if (topic === TOPIC_TARGET_IN) {       target = JSON.parse(data);   } else if (topic === TOPIC_MODE_IN) {       mode = JSON.parse(data);   }};

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


$bus.on(function (event, data) {   if (event === 'tmst-set-target') {       target = JSON.parse(data);   } else if (event === 'tmst-set-mode') {       mode = JSON.parse(data);   }   publishState();}, null);

Ежесекундно происходит оценка текущей ситуации и принимается решения в соответствии с установленным режимом.


$res.timers.setInterval(function () {   if (sensor !== null) {       $res.ds18x20.convert_all();       temp = $res.ds18x20.get_temp_c(sensor);   } else { // Fake temperature       if (temp > 99) {           fakeVector = -0.5;       } else if (temp < 1) {           fakeVector = 0.5;       }       temp += fakeVector;   }   // Refresh sensor data   if (mode === MODE_ON) {       state = 1;   } else if (mode === MODE_OFF) {       state = 0;   } else if (mode === MODE_LESS) {       if (temp < target) {           state = 1;       } else {           state = 0;       }   } else if (mode === MODE_MORE) {       if (temp > target) {           state = 1;       } else {           state = 0;       }   }   publishState();   // По результату принятого решения нагрузка включается или выключается   $res.relay.set(!state);}, 1000);

Последние штрихи:


// Устанавливается начальное значение температуры для демонстратора. temp = 34.5;// Конфигурируется выход GPIO$res.relay.direction($res.relay.DIR_MODE_OUTPUT);// Публикуется начальное состояниеpublishState();// Запускается процесс установки соединения с MQTT сервером.$res.mqtt.connect(MQTT_SERVER);

Мультиязычность


В платформе предусмотрена мультиязычность. Она реализуется через VUE фильтры. Использование этой фичи выглядит так:


<h1>{{ 'TITLE'|lang }}</h1>

Языковые константы содержатся в файле langs.js и подключаются при инициализации frontend компонента.


favicon


Для узнаваемости приложений используются ассоциативные иконки. Иконка размещается в файле favicon.svg Иконка будет демонстрироваться пользователю на рабочей столе и при установке приложения.


Сборка приложения


Сборка приложений происходит пакетно. Т.е. собираются все приложения которые есть в папке /src/applications/. Для начала сборки необходимо выполнить стандартную команду npm


npm run build


Результатом станет генерация smt файлов. Одним из которых будет thermostat.smt Это и есть собранное и готовое к установке приложение Thermostat. Найти сборки можно в папке /dist/apps/



Развертывание приложения на контроллере


Тут все просто. Необходимо зайти на контроллер по WEB. Перейти в раздел Настройки и на плитке Приложения кликнуть ссылку Установить приложение. В открывшемся окне выбрать файл thermostat.smt.



Когда файл будет выбран, платформа предложит распределить запрашиваемые приложением ресурсы. Некоторые требования не используют аппаратные ресурсы. К таким относятся, например, таймеры или MQTT клиент. Другие используют. Например, для подключения шины OneWire требуется указать UART порт и GPIO на которых будет реализована шина. Также, требуется определить GPIO на котором будет реле управляющее нагрузкой.


Для удобства пользователя все назначения можно указать в поле default требования. В этом случае, пользователю останется только нажать кнопку Установить. При этом у него остается возможность переконфигурировать приложение по своему усмотрению.


Такой подход решает несколько задач:


  1. Разделение ресурсов и многозадачность. На контроллере можно устанавливать несколько приложений распределяя между ними ресурсы.
  2. Безопасность и прозрачность. Пользователь имеет полную информацию о доступных ресурсах для приложения.
  3. Кроссплатформенность. Приложение заявляет о необходимых ресурсах, а предоставляет их конкретная реализация платформы на контроллере.
    Контроль совместимости. Приложение не будет установлено на контроллеры, которые не предоставляют обязательные для приложения ресурсы.

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


Использование приложения


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



Ниже находятся элементы конфигурирования. Слева выпадающий список доступных режимов. Справа целевая температура.


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


Подключение дистанционной консоли



На плашке интеграции есть перечень рекомендованных приложений для телефонов Android. В примере будет рассмотрено приложение MQTT Dash.


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


Установите понравившееся вам приложение. Создайте новое соединение с MQTT брокером.



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



Информация для заполнения полей топиков находится на плашке интеграции. Для температуры есть только один топик /thingjs/TJS-030BE4/temp Тут данные только для чтения.


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


Теперь добавим управление целевой температурой (порогом).


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


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


Приятного использования!


Безопасность


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


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


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


В перспективе планируется ЭЦП приложений и прошивок.


Что дальше?


  • Поиск партнеров;
  • Стабилизация кода и повышение надежности;
  • Создание собственных облачных сервисов, где смогут хостится приложения;
  • Установка приложений на мобильные устройства;
  • Разработка и выпуск ThingJS dev-kit для любителя;
  • Интеграция с популярными IoT экосистемами;
  • Внедрение голосового управления.

Ссылки


Ресурсы проекта ThingJS:



Репозитории проекта ThingJS:



Где уже используется платформа:



Где будет использоваться:



Используемые проекты:


Подробнее..

Категории

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

© 2006-2020, personeltest.ru