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

Javascript

Перевод Разработка PWA с поддержкой распознавания лица и голоса

08.01.2021 18:05:33 | Автор: admin
Этот материал посвящён продвинутым возможностям PWA (Progressive Web Application, прогрессивное веб-приложение), основанным на некоторых современных API. А именно, здесь мы поговорим о разработке веб-проекта, поддерживающего распознавание лица и голоса. Тем, что раньше было доступно только в обычных приложениях, теперь можно воспользоваться и в PWA. Это открывает веб-разработчикам множество новых возможностей.



Приложение, о котором пойдёт речь, основано на PWA, разработка которого подробно описана в этом материале. Здесь мы уделим основное внимание следующим двум API:

  • Face Detection API, который предназначен для реализации возможностей по распознаванию лица в браузере.
  • Web Speech API, который позволяет преобразовывать речь в текст и озвучивать обычные тексты.

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

О работе с экспериментальными возможностями веб-платформы


Вышеописанные API будут работать только в том случае, если включить в браузере Google Chrome флаг Experimental Web Platform features. Найти его можно по адресу chrome://flags.


Включение флага Experimental Web Platform features

Подготовка проекта


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

git clone https://github.com/petereijgermans11/progressive-web-app

После завершения клонирования нужно перейти в директорию проекта:

cd pwa-article/pwa-app-native-features-rendezvous-init

Далее установим зависимости и запустим проект:

npm i && npm start

Открыть приложение можно, перейдя по ссылке http://localhost:8080.


Приложение в браузере

Общедоступный URL для приложения, к которому можно обратиться с мобильного устройства


Есть много способов для организации доступа к localhost:8080 с мобильных устройств. Например, для этого можно воспользоваться ngrok.

Установим ngrok:

npm install -g ngrok

Выполним в терминале следующую команду:

ngrok http 8080

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

Распознавание лица средствами JavaScript


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

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

Эта библиотека включает в себя API, предназначенный для организации распознавания лиц в браузере. В основе этого API лежит библиотека tensorflow.js.

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


Результаты работы приложения

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

Шаг 1: библиотека face-api.js


Библиотека face-api.js, как уже было сказано, предоставляет приложению API для организации распознавания лиц в браузере. Эта библиотека уже имеется в нашем проекте, она находится в папке public/src/lib.

Шаг 2: модели


Модели это предварительно подготовленные данные, которыми мы будем пользоваться для анализа селфи и определения интересующих нас признаков. Модели находятся в папке public/src/models.

Шаг 3: файл index.html


В файле index.html выполняется импорт следующих материалов:

  • Уже имеющийся в проекте файл facedetection.css, используемый для стилизации приложения.
  • Файл face-api.min.js, представляющий Face Detection API, используемый для обработки данных моделей и для извлечения из снимков интересующих нас признаков.
  • Файл facedetection.js, в котором мы будем писать код, реализующий логику приложения.

В файл index.html сначала нужно импортировать стили:

<link rel="stylesheet" href="src/css/facedetection.css">

Затем, сразу под тегом <div id="create-post">, в файл нужно поместить следующий код:

<video id="player" autoplay></video><div class="container-faceDetection"></div><canvas id="canvas" width="320px" height="240px"></canvas><div class="result-container"><div id="emotion">Emotion</div><div id="gender">Gender</div><div id="age">Age</div></div>

Тут мы используем существующий тег <video>, применяя его для создания селфи. В теге с классом result-container мы выводим результаты определения эмоционального состояния человека, его пола и возраста.

Далее, нужно поместить следующий фрагмент кода в нижней части index.html. Это позволит нам пользоваться API для распознавания лиц:

<script src="src/lib/face-api.min.js"></script><script src="src/js/facedetection.js"></script>

Шаг 4: импорт моделей в PWA


Здесь мы создаём в существующем файле feed.js отдельную функцию, предназначенную для запуска потоковой передачи видео. А именно, переместим следующий код из функции initializeMedia() в функцию startVideo(), ответственную за потоковую передачу видео:

const startVideo = () => {navigator.mediaDevices.getUserMedia({video: {facingMode: 'user'}, audio: false}).then(stream => {videoPlayer.srcObject = stream;videoPlayer.style.display = 'block';videoPlayer.setAttribute('autoplay', '');videoPlayer.setAttribute('muted', '');videoPlayer.setAttribute('playsinline', '');}).catch(error => {console.log(error);});}

В файле feed.js мы используем Promise.all для асинхронной загрузки моделей, используемых API для распознавания лица. После того, как модели будут загружены, мы вызываем только что созданную функцию startVideo():

Promise.all([faceapi.nets.tinyFaceDetector.loadFromUri("/src/models"),faceapi.nets.faceLandmark68Net.loadFromUri("/src/models"),faceapi.nets.faceRecognitionNet.loadFromUri("/src/models"),faceapi.nets.faceExpressionNet.loadFromUri("/src/models"),faceapi.nets.ageGenderNet.loadFromUri("/src/models")]).then(startVideo);

Шаг 5: реализация логики проекта в файле facedetection.js


Поговорим о функциях Face Detection API, которыми мы будем пользоваться в приложении:

  • faceapi.detectSingleFace эта функция использует систему распознавания лиц SSD Mobilenet V1. Функции передают объект videoPlayer и объект с параметрами. Для того чтобы наладить распознавание нескольких лиц detectSingleFace надо заменить на detectAllFaces.
  • withFaceLandmarks данная функция применяется для нахождения 68 ключевых точек (ориентиров) лица.
  • withFaceExpressions эта функция находит на изображении все лица и определяет выражения лиц, возвращая результаты работы в виде массива.
  • withAgeAndGender данная функция тоже находит на изображении все лица, определяет возраст и пол людей и возвращает массив.

Следующий код нужно поместить в файл facedetection.js ниже того кода, который там уже есть.

videoPlayer.addEventListener("playing", () => {const canvasForFaceDetection = faceapi.createCanvasFromMedia(videoPlayer);let containerForFaceDetection = document.querySelector(".container-faceDetection");containerForFaceDetection.append(canvasForFaceDetection);const displaySize = { width: 500, height: 500};faceapi.matchDimensions(canvasForFaceDetection, displaySize);setInterval(async () => {const detections = await faceapi.detectSingleFace(videoPlayer, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions().withAgeAndGender();const resizedDetections = faceapi.resizeResults(detections, displaySize);canvasForFaceDetection.getContext("2d").clearRect(0, 0, 500, 500);faceapi.draw.drawDetections(canvasForFaceDetection, resizedDetections);faceapi.draw.drawFaceLandmarks(canvasForFaceDetection, resizedDetections);if (resizedDetections && Object.keys(resizedDetections).length > 0) {const age = resizedDetections.age;const interpolatedAge = interpolateAgePredictions(age);const gender = resizedDetections.gender;const expressions = resizedDetections.expressions;const maxValue = Math.max(...Object.values(expressions));const emotion = Object.keys(expressions).filter(item => expressions[item] === maxValue);document.getElementById("age").innerText = `Age - ${interpolatedAge}`;document.getElementById("gender").innerText = `Gender - ${gender}`;document.getElementById("emotion").innerText = `Emotion - ${emotion[0]}`;}}, 100);});

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

Тут мы, в первую очередь, подключаем к videoPlayer обработчик события playing. Он срабатывает в ситуации, когда активна видеокамера.

Переменная videoPlayer даёт доступ к HTML-элементу <video>. Видеоматериалы будут выводиться именно в этом элементе.

Затем создаётся элемент canvasElement, который представлен константой canvasForFaceDetection. Он используется для распознавания лица. Этот элемент размещается в контейнере faceDetection.

Функция setInterval() осуществляет вызовы faceapi.detectSingleFace с интервалом в 100 миллисекунд. Эта функция вызывается асинхронно, с применением конструкции async/await. В итоге результаты распознавания лица выводятся в полях с идентификаторами emotion, gender и age.

Распознавание речи средствами JavaScript


Ниже показан интерфейс, который мы собираемся создать для работы с Web Speech API. Как видите, на экране есть поле, в которое можно вводить текст. Но этот текст можно и надиктовать приложению, воспользовавшись значком микрофона.

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


Интерфейс, используемый для решения задач распознавания речи

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

Шаг 1: файл index.html


Импортируем в index.html следующие материалы:

  • Уже имеющиеся в проекте стили из файла speech.css.
  • Файл speech.js, в котором мы реализуем логику, необходимую для распознавания речи.

Сначала импортируем стили:

<link rel="stylesheet" href="src/css/speech.css">

Потом разместим следующий код сразу после тега <form>:

<div id="info"><p id="info_start">Click on the microphone icon and begin speaking.</p><p id="info_speak_now">Speak now.</p><p id="info_no_speech">No speech was detected. You may need to adjust your<a href="http://personeltest.ru/aways/support.google.com/chrome/bin/answer.py?hl=en&answer=1407892">microphone settings</a>.</p><p id="info_no_microfoon" style="display:none">No microphone was found. Ensure that a microphone is installed and that<a href="http://personeltest.ru/aways/support.google.com/chrome/bin/answer.py?hl=en&answer=1407892">microphone settings</a> are configured correctly.</p><p id="info_allow">Click the "Allow" button above to enable your microphone.</p><p id="info_denied">Permission to use microphone was denied.</p><p id="info_blocked">Permission to use microphone is blocked. To change,go to chrome://settings/contentExceptions#media-stream</p><p id="info_upgrade">Web Speech API is not supported by this browser.Upgrade to <a href="http://personeltest.ru/aways/www.google.com/chrome">Chrome</a>version 25 or later.</p></div><div class="right"><button id="start_button" onclick="startButton(event)"><img id="start_img" src="./src/images/mic.gif" alt="Start"></button></div><div class="input-section mdl-textfield mdl-js-textfield mdl-textfield--floating-label div_speech_to_text"><span id="title" contenteditable="true" class="final"></span><span id="interim_span" class="interim"></span><p></div><div class="center"><p><div id="div_language"><select id="select_language" onchange="updateCountry()"></select><select id="select_dialect"></select></div></div>

Здесь, в разделе <div id ="info">, выводятся информационные сообщения, имеющие отношение к использованию Web Speech API.

Обработчик события onclick кнопки с идентификатором start_button используется для запуска системы распознавания речи.

Обработчик события onchange поля select_language позволяет выбирать язык.

Следующий код нужно разместить в нижней части index.html. Он позволит нам пользоваться возможностями Web Speech API.

<script src="src/js/speech.js"></script>

Шаг 2: реализация возможностей распознавания речи


Ниже показан код, который должен быть размещён в файле speech.js. Он отвечает за инициализацию Web Speech Recognition API системы, ответственной за распознавание речи:

if ('webkitSpeechRecognition' in window) {start_button.style.display = 'inline-block';recognition = new webkitSpeechRecognition();recognition.continuous = true;recognition.interimResults = true;recognition.onstart = () => {recognizing = true;showInfo('info_speak_now');start_img.src = './src/images/mic-animate.gif';};recognition.onresult = (event) => {let interim_transcript = '';for (let i = event.resultIndex; i < event.results.length; ++i) {if (event.results[i].isFinal) {final_transcript += event.results[i][0].transcript;} else {interim_transcript += event.results[i][0].transcript;}}final_transcript = capitalize(final_transcript);title.innerHTML = linebreak(final_transcript);interim_span.innerHTML = linebreak(interim_transcript);};recognition.onerror = (event) => {// код обработки ошибок};recognition.onend = () => {// код, выполняемый при завершении распознавания речи};}

Тут, в первую очередь, осуществляется проверка на предмет доступности API webkitSpeechRecognition в объекте window. Этот объект представляет окно браузера (JavaScript является частью этого объекта).

Если webkitSpeechRecognition в window имеется, создаётся объект webkitSpeechRecognition с использованием конструкции recognition = new webkitSpeechRecognition();.

Затем осуществляется настройка следующих свойств API:

  • recognition.continuous = true это свойство позволяет задать для каждого сеанса распознавания речи непрерывный возврат результатов.
  • recognition.interimResults = true это свойство указывает на то, нужно ли возвращать промежуточные результаты распознавания речи.

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

  • recognition.onstart этот обработчик запускается при запуске системы распознавания речи. После этого выводится текст, предлагающий пользователю начать говорить (Speak now), и отображается анимированный значок микрофона (mic-animate.gif).
  • recognition.onresult этот обработчик срабатывает при возврате результатов распознавания речи. Результаты представлены в виде двумерного массива SpeechRecognitionResultList. Свойство isFinal, проверка которого осуществляется в цикле, указывает на то, каким именно является результат окончательным или промежуточным. Свойство transcript даёт доступ к строковому представлению результата.
  • recognition.onend данный обработчик выполняется при завершении операции распознавания речи. При его выполнении не выводится никакого текста. Он лишь выполняет замену значка микрофона на стандартный.
  • recognition.onerror этот обработчик вызывается при возникновении ошибок. Тут выводятся сообщения о возникших ошибках.

Запуск процесса распознавания речи


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

const startButton = (event) => {if (recognizing) {recognition.stop();return;}final_transcript = '';recognition.lang = select_dialect.value;recognition.start();ignore_onend = false;title.innerHTML = '';interim_span.innerHTML = '';start_img.src = './src/images/mic-slash.gif';showInfo('info_allow');start_timestamp = event.timeStamp;};

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

Итоги


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

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

Планируете ли вы пользоваться инструментами для распознавания лица и голоса в своих веб-проектах?

Подробнее..

CRUD для NMAPа решение для мониторинга открытых портов на хостах

14.01.2021 10:07:09 | Автор: admin
Привет, Хабр! Меня зовут Рудаков Александр, я занимаюсь информационной безопасностью в компании "ЛАНИТ-Интеграция". Однажды, в рамках работы над проектом, мне понадобилось организовать небольшой непрерывный мониторинг (с хранением истории) открытых портов в подсети серверов. Требовалось за короткое время сделать рабочий прототип решения для данной задачи. В этой статье я расскажу о том, как с помощью nmap, Node.JS, PostgreSQL и ORM Sequelize организовать мониторинг открытых портов на хостах.

Источник

CRUD-приложение на Node.JS


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

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

При выборе БД для данного решения я решил остановиться на PostgreSQL вместо NoSQL по следующим причинам:

  • open source SQL СУБД;
  • логические требования к данным, включая их структуру, определены заранее;
  • SQL обеспечивает целостность данных;
  • наличие Object-relational mapping (ORM) для Node.JS.

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

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

Архитектура решения



Решение должно выводить отчет по портам для конкретного IP (либо доменного имени) по запросу от бота Telegram. Дополнительно должна присутствовать возможность ручного запуска сканирования по IP-адресу сервера через API посредством curl или Postman. Очень хотелось обойтись без ручного написания SQL-запросов, поэтому я использовал ORM Sequelize.

Основные NPM-пакеты Node.JS, которые использовались в данном решении:

  • Express в качестве веб-сервера;
  • Sequelize для работы с БД;
  • Node-nmap для получения результатов сканирования;
  • Telegraf для взаимодействия с мессенджером.

Структура БД


Исходя из постановки задачи, нам нужно реализовать типичное CRUD-приложение, которое умеет вносить данные в БД и читать их, формируя простейший отчет. CRUD акроним, обозначающий четыре базовые функции, используемые при работе с базами данных: создание (create), чтение (read), модификация (update), удаление (delete). Из данных функций в рамках решаемой задачи пока актуальны только создание и чтение для генерации простого отчета.

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


Здесь представлены следующие таблицы БД:

scan таблица сканирование содержит дату проведения сканирования;

port таблица порт содержит номер открытого порта и описание сервиса на нем;

NetObject таблица сетевой объект содержит IP-адрес/имя хоста, который сканируем;

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

ORM Sequelize


Для простой работы с БД решено было использовать ORM Sequelize. Изучив документацию, подключаем к приложению БД PostgreSQL, которая установлена на том же сервере.

В терминологии Sequelize, модель это абстракция, которая представляет таблицу в БД. Модель сообщает Sequelize информацию о сущности, которую она представляет, например, имя таблицы в базе данных и какие столбцы у нее есть. Объект это экземпляр модели с конкретными характеристиками (например, сервер с адресом 127.0.0.1 или порт 22 с сервисом ssh).

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

Структура БД в ORM
// Table NetObjectconst NetObject = sequelize.define(netObject, {id: {type: DataTypes.INTEGER,autoIncrement: true,primaryKey: true,allowNull: false},IP: {type: DataTypes.STRING,unique: true}});// Table Portconst Port = sequelize.define(port, {id: {type: DataTypes.INTEGER,autoIncrement: true,primaryKey: true,allowNull: false},number: {type: DataTypes.INTEGER},description: {type: DataTypes.STRING,}}, {timestamps: false});// Table Scanconst Scan = sequelize.define(scan, {id: {type: DataTypes.INTEGER,primaryKey: true,autoIncrement: true,},date: {type: DataTypes.DATE}}, {timestamps: false});const ScanNetObject = sequelize.define(ScanNetObject,{id: {type: DataTypes.INTEGER,autoIncrement: true,primaryKey: true,allowNull: false},}, {timestamps: false});


Отношения (связи) между таблицами
ScanNetObjectPort.belongsTo(Scan);ScanNetObjectPort.belongsTo(NetObject);ScanNetObjectPort.belongsTo(Port);Scan.hasMany(ScanNetObjectPort);NetObject.hasMany(ScanNetObjectPort);Port.hasMany(ScanNetObjectPort);


Приведенный выше код создаст нам следующую структуру БД:


Поля createdAt, updatedAt были автоматически созданы Sequelize. Опцией {timestamps: false} эти поля были отключены для моделей scan и port.

Обратим внимание, что в приведенном описании имена таблиц (ports, scans) никогда не определялись явно. Потому, что Sequelize автоматически именуют таблицы, добавляя к имени объекта букву s. За правильное именование в Sequelize для множественных чисел (person -> people) отвечает пакет inflection-js, который в качестве справочника использует Вики-словарь. Это небольшое отступление показывает нам уровень абстракции нашего приложения непосредственно от СУБД. За создание таблиц и написание SQL-запросов у нас отвечает Sequelize, и именно поэтому ORM очень любят использовать при реализации pet-проектов, подобных нашему.

NMAP


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

JSON-результат nmap
[{hostname:localhost,ip:127.0.0.1,mac:null,openPorts:[{port:22,protocol:tcp,service:ssh,method:table}],osNmap:null}]


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

Сохранение результатов сканирования в БД


ORM Sequelize делает за нас большую часть работы по сохранению информации в БД. Напишем отдельную функцию сканирования, которая получает на вход IP-адрес (либо DNS-имя, это тоже валидное значение для nmap). При получении результата сканирования мы должны создать объект Scan, содержащий текущую дату и время сканирования. Далее переходим к сетевому объекту, который мы сканируем, он уже может быть в таблице NetObjects либо еще нет. Оценим удобство ORM: метод findOrCreate применительно к объекту возвращает нам либо новый сетевой объект, либо уже существующий. Затем пробегаем циклом по результатам сканирования, все открытые порты с висящими на них сервисами сохраняем через объект Port. Не забываем привязывать объекты Scan и Port первичными ключами к объекту ScanNetObjectPort.

Итоговая функция сканирования и сохранения информации в БД:

Сканируем и сохраняем данные в БД
exports.scan_art = (IPaddr) => {const nmap_scan = new nmap.NmapScan(IPaddr.toString());console.log(Starting nmap scan...);nmap_scan.on('complete', function(data){let sca = {date: Date.now()};// Циклом проходим результаты скана (там может быть более 1 адреса), создаем Scan, NetObject (если его нет), ScanNetObjectPortScan.create(sca).then ((newScan) => {NetObject.findOrCreate({where:{IP: IPaddr}, defaults: {IP: IPaddr}}).then((newNetObject) => {for (let var_host in data){for (let var_port in data[var_host].openPorts){let str_port = data[var_host].openPorts[var_port].port.toString();let str_desc = data[var_host].openPorts[var_port].service.toString();Port.create({number: str_port, description: str_desc}).then((newPort)=>{const nSNOP = {portId: newPort.id,netObjectId: newNetObject[0].id,scanId: newScan.id};ScanNetObjectPort.create(nSNOP).then(() => {console.log(Port +str_port+ for host +IPaddr+ is added to database);}).catch ((error)=> {console.log(Error on create: newScanNetObjectPort: \n+error.message)});})}}}).catch ((error)=> {console.log(Error on create: newNetObject: \n+error.message)});}).catch ((error)=> {console.log(Error on create: newScan: \n+error.message)});});nmap_scan.on('error', function(error){console.log(error);});nmap_scan.startScan();};


В качестве бонуса в консоли видим SQL-запросы, которые за нас реализовал ORM Sequelize:


Сканирование по таймеру делаем просто. Напишем в nmap.controller.js функцию get_list, которая возвращает массив IP-адресов объектов сканирования, затем, используя setInterval, проводим сканирование всех элементов данного списка в заданный период времени.

Функция get_list
exports.get_list = async()=>{let NetObject_list = await NetObject.findAll();let list = [];await NetObject_list.forEach((NO)=> {list.push(NO.IP.toString());});return list;};


Функция сканирования массива
async function array_scan () {nmap.get_list().then((scan_list)=>{scan_list.forEach((scanListKey) =>{nmap.scan_art(scanListKey.toString());});})}setInterval(array_scan, 360000);


Генерация отчета по результатам сканирования


Процедура генерации отчета с ORM Sequelize довольно проста. Функция генерации отчета получает на вход IP-адрес, затем ищет по данному IP-адресу сетевой объект. Если объект есть в таблице NetObject, то по его первичному ключу методом findAll объекта ScanNetObjectPort мы находим список портов по всем сканированиям. В отчет необходимо вывести дату каждого сканирования, поэтому организуем цикл перебора всех объектов ScanNetObjectPort, с учетом даты текущего сканирования. Информация о каждом сервисе на каждом открытом порту выводится в отчет, в подзаголовке пишется дата сканирования. Отчет в тексте выглядит так:

...
Report for 127.0.0.1Date: Tue Dec 15 2020 16:53:18 GMT+0300 (Moscow Standard Time)Port 22 with service sshPort 5432 with service postgresqlPort 8080 with service http-proxyDate: Tue Dec 15 2020 18:25:50 GMT+0300 (Moscow Standard Time)Port 22 with service sshPort 5432 with service postgresqlPort 8080 with service http-proxy


Вот так отчет составляется:

Функция формирования отчета
exports.test_report = (IPaddr) => {let text_report = ;if (IPaddr === undefined) {console.log(IPaddr is undefined..!); return;}let report_promise = new Promise(function (resolve, reject) {NetObject.findOne({where: {IP: IPaddr}}).then(function (NOp) {if (NOp === null) {console.log(Not found NetObject by IP + IPaddr); return;}text_report += Report for  + NOp.IP.toString() + \n;ScanNetObjectPort.findAll({where:{netObjectId: NOp.id}, include: [{model: Scan}, {model: Port}]}).then(async function (sel) {let scan_id = sel[0].scanId; // запоминаем id первого найденного скана как текущийlet scan_date = null;await Scan.findByPk(scan_id).then(function (scan1) {scan_date = scan1.date;});text_report += Date: + scan_date.toString() +\n;for (const item of sel) {if (item.scanId !== scan_id) { // проверяем, текущий ли сканscan_id = item.scanId;await Scan.findByPk(scan_id).then(function (scan1) {scan_date = scan1.date;text_report += Date: + scan_date.toString() +\n;});}await Port.findByPk(item.portId).then (function (p) {text_report += Port + p.number +  with service  + p.description +\n;});}return sel;}).then (() => { resolve(text_report);}).catch((error) =>{console.log(Error on: ScanNetObjectPort.findAll + error.message); reject(error);});});});


Отправка отчета в Telegram


Используем пакет telegraf для Node.JS, чтобы работать с ботом в Telegram. В отдельный модуль для работы с telegram помещаем токен бота и несколько строчек кода для команд типа помощи. Далее через Bot Father создаем свою команду report, по который будем генерировать отчет по хосту во второй части команды.

Отчет в Telegram
exports.run = async ()=> {bot.command('report', (ctx) => {const netObjectName = ctx.message.text.split( )[1];if (netObjectName === undefined) {return;}let promise = new Promise(async function (resolve, reject) {let var_report = await report.get_test_report(netObjectName.toString());resolve(var_report);});promise.then(function (result) {ctx.reply(result.toString()).then(()=>{console.log(report generating successful!)}).catch((error)=>{console.log(Error on report promise + error)});},function (error) {ctx.reply(error on generating report... + error).then(()=>{console.log(error on generating report... + error)});});});bot.launch().then (function () {console.log(telegram bot is started);})};


Вот так выглядит итоговый отчет:


Выложил проект на github, по традиции, оставив токен в первом коммите. Для запуска необходимо установить на хост nmap, PostgreSQL, настроить подключение к БД, указать токен для бота в Telegram. Запустить приложение и GET-запросом /nmap?IP=127.0.0.1 с идентификатором хоста выполнить сканирование. Хост сразу попадет в базу и далее по таймауту будет производиться сканирование портов с сохранением данных в БД. Новый хост для сканирования добавляется аналогичным образом. Отчеты смотреть через бота, по запросу типа /report <IP/URL>, где IP/URL идентификатор хоста для сканирования.

Итоги


По сравнению с решением n8n, которое я пробовал ранее, программирование на Node.JS показалось мне намного сложнее. Для написания такого простейшего приложения пришлось изучить много новых вещей: от ORM до промисов в Node.JS. Эти вещи оказались немного сложными для понимания, даже учитывая имеющийся небольшой опыт программирования и опыт работы с фреймворками типа Vue.JS или Modx. Очень понравилось использование ORM, для небольших задач он незаменим.

Резюме


Если при решении вашей задачи есть возможность обойтись без программирования, используйте решения типа n8n. Если же решите воспользоваться описанной технологией, то придется изучать JS.

Готов ответить на вопросы. Пишите на arudakov@lanit.ru.

Кстати, у нас еще есть вакансии!

Подробнее..

Перевод Что такое рендеринг на стороне сервера и нужен ли он мне?

04.01.2021 10:13:21 | Автор: admin
Привет, Хабр!

В новом году начнем общение с вами с затравочной статьи о серверном рендеринге (server-side rendering). В случае вашей заинтересованности возможна более свежая публикация о Nuxt.js и дальнейшая издательская работа в этом направлении


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

До пришествия приложений, полностью генерируемых на JS в браузере, HTML-разметка выдавалась клиенту в ответ на HTTP-вызов. Это могло происходить путем возврата статического HTML-файла с контентом, либо путем обработки отклика при помощи какого-либо серверного языка (PHP, Python или Java), причем, более динамическим образом.

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

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

<!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8">    <link rel="shortcut icon" href="http://personeltest.ru/aways/habr.com/favicon.ico">    <title>React App</title>  </head>  <body>    <div id="root"></div>    <script src="http://personeltest.ru/aways/habr.com/app.js"></script>  </body></html>


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

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

Почему это проблема?



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

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


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

Ладно, но в демографическом отношении моя целевая аудитория точно не относится ни к одной из этих групп, так стоит ли мне волноваться?

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

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

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

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

Как решить эту проблему



Есть несколько способов ее решения.

A Попробуйте оставить все ключевые страницы вашего сайта статическими



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

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

B Генерируйте части вашего приложения в виде HTML-страниц в процессе сборки



В проект можно добавить такие библиотеки как react-snapshot; они используются для генерации HTML-копий страниц вашего приложения и для сохранения их в специально предназначенном каталоге. Затем этот каталог развертывается наряду с пакетом JS. Таким образом, HTML будет подаваться с сервера вместе с откликом, и ваш сайт увидят в том числе те пользователи, у которых отключен JavaScript, а также заметят поисковики и т.д.

Как правило, сконфигурировать react-snapshot не составляет труда: достаточно добавить библиотеку в ваш проект и изменить сборочный скрипт следующим образом:

"build": "webpack && react-snapshot --build-dir static"

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

C Создать на JS приложение, использующее серверный рендеринг



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

Два наиболее популярных решения, обеспечивающих серверный рендеринг для React:



Создайте собственную реализацию SSR



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

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

Давайте создадим входную точку:

// index.jsimport React from 'react';import { render } from 'react-dom';import App from './App.js';render(<App />, document.getElementById('root'));


И компонент-приложение (App):

// App.jsimport React from 'react';const App = () => {  return (    <div>      Welcome to SSR powered React application!    </div>  );}


А также оболочку, чтобы загрузить наше приложение:

// index.html<!doctype html><html>  <head>    <meta charset="utf-8" />  </head>  <body>    <div id="root"></div>    <script src="http://personeltest.ru/aways/habr.com/bundle.js"></script>  </body></html>


Как видите, приложение получилось довольно простым. В рамках этой статьи мы не будем пошагово разбирать все шаги, необходимые для генерации правильной сборки webpack+babel.
Если запустить приложение в его текущем состоянии, то на экране появится сообщение-приветствие. Просмотрев исходный код, вы увидите содержимое файла index.html, но приветственного сообщения там не будет. Для решения этой проблемы добавим серверный рендеринг. Для начала добавим 3 пакета:

yarn add express pug babel-node --save-dev

Express это мощный веб-сервер для node, pug движок-шаблонизатор, который можно использовать с express, а babel-node это обертка для node, обеспечивает транспиляцию на лету.

Сначала скопируем наш файл index.html и сохраним его как index.pug:

// index.pug
<!doctype html>
<html>
<head>
<meta charset=utf-8 />
</head>
<body>
<div id=root>!{app}</div>
<script src=bundle.js></script>
</body>
</html>

Как видите, файл практически не изменился, не считая того, что теперь в HTML вставлено !{app}. Это переменная pug, которая впоследствии будет заменена реальным HTML.

Создадим наш сервер:

// server.jsimport React from 'react';import { renderToString } from 'react-dom/server';import express from 'express';import path from 'path';import App from './src/App';const app = express();app.set('view engine', 'pug');app.use('/', express.static(path.join(__dirname, 'dist')));app.get('*', (req, res) => {  const html = renderToString(    <App />  );  res.render(path.join(__dirname, 'src/index.pug'), {    app: html  });});app.listen(3000, () => console.log('listening on port 3000'));


Разберем этот файл по порядку.

import { renderToString } from 'react-dom/server';

Библиотека react-dom содержит отдельный именованный экспорт renderToString, работающий подобно известному нам render, но отображает не DOM, а HTML в виде строки.

const app = express();app.set('view engine', 'pug');app.use('/', express.static(path.join(__dirname, 'dist')));


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

В последней строке мы приказываем express искать файл в каталоге dist, и, если запрос (напр. /bundle.js) совпадает с файлом, присутствующем в этом каталоге, то выдать его в ответ.

app.get('*', (req, res) => {});


Теперь мы приказываем express добавить обработчик на каждый несовпавший URL в том числе, на наш несуществующий файл index.html (как вы помните, мы переименовали его в index.pug, и его нет в каталоге dist).

const html = renderToString(  <App />);


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

res.render(path.join(__dirname, 'src/index.pug'), {  app: html});


Теперь, когда у нас есть отображенный HTML, мы приказываем express отобразить в ответ файл index.pug и заменить переменную app тем HTML, что мы получили.

app.listen(3000, () => console.log('listening on port 3000'));

Наконец, мы обеспечиваем запуск сервера и настраиваем его так, чтобы он слушал порт 3000.
Теперь нам осталось всего лишь добавить нужный скрипт в package.json:

"scripts": {  "server": "babel-node server.js"}


Теперь, вызвав yarn run server, мы должны получить подтверждение, что сервер действительно работает. Переходим в браузере по адресу localhost:3000, где мы, опять же, должны увидеть наше приложение. Если мы просмотрим исходный код на данном этапе, то увидим:

<!doctype html><html>  <head>    <meta charset="utf-8" />  </head>  <body>    <div id="root">div data-reactroot="">Welcome to SSR powered React application!</div></div>    <script src="bundle.js"></script>  </body></html>


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

Зачем же нам по-прежнему нужен bundle.js?



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

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

О чем необходимо помнить



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

  • Любое состояние, сгенерированное на стороне сервера, не будет передаваться в состояние клиентского приложения. Это означает, что, если ваша серверная часть выберет некоторые данные и использует их для отображения HTML, то эти данные не попадут в this.state, которое увидит браузер
  • componentDidMount не вызывается на сервере это означает, что не будут вызываться никакие операции по выборке данных, которые вы привыкли там размещать. В принципе, это хорошо, поскольку вы должны предоставлять нужные вам данные в виде пропсов. Помните, что отображение нужно отложить (вызвав res.render) до тех пор, пока данные не будут выбраны. Из-за этого посетители могут заметить некоторые задержки в работе сайта
  • если вы собираетесь использовать роутер react (напр. @reach/router или react-router) то должны убедиться, что приложению передается правильный URL, когда оно отображается на сервере. Обязательно почитайте об этом в документации!
Подробнее..

React.js формошлепство или работа с формами при помощи пользовательских хуков

06.01.2021 20:05:30 | Автор: admin


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


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


К слову, в React для удобной работы с формами уже набрали популярность 3 отличных библиотеки. Это Formik, Redux Form и React Hook Form. На сайте последнего представлены плюсы перед конкурентами.


Для начала


Для начала нам нужно создать React приложение. Сделаем это через Create React App. Если информации по ссылке будет недостаточно, то github.


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


npx create-react-app react-custom-forms-article  --template typescript

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


Приступая к реализации хука


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


// App.tsxconst formInputs = {  firstName: {},  lastName: '',}

В компоненте инициализируем форму.


// App.tsxconst App = () => {  const { fields, handleSubmit } = useForm(formInputs);  const { firstName } = fields;  return <></>;}

handleSubmit метод, принимающий callback-функцию и возвращающий значение всех полей.
fields структура вида "ключ-значение", в ней будут храниться все описанные ранее поля формы с дополнительной информацией, например: свойство с текстом ошибки, значение поля, флаг валидности поля и те свойства, что будут переданы в объекте при создании формы.


Компонент вернет примерно такую верстку.


// App.tsx// Метод выполнения формы при срабатывание onSubmitconst onSubmit = ({ values }: { values: IValues }) => {  console.log(values, 'submit');}return (  <div className="App">    <form onSubmit={handleSubmit(onSubmit)}>      <input type="text" value={firstName.value} onChange={firstName.setState}/>    </form>  </div>);

useForm


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


// hooks/useForm.tsexport const useForm = (initialFields: any = {}) => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        value: (isString && value) || ((!isString && value.value) || ''),        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      }    }    return { ...fields, ...field };  }, {});

В этом примере кода для удобства итераций используется метод объектов entries, который возвращает массив вида [[propName, propValue], [propName, propValue]], и метод для работы с массивами reduce, который помогает собрать объект заново. В целом все выглядит неплохо, но не хватает методов для обновления значения полей. Добавим состояний с использованием React Hook.


// hooks/useForm.ts...const [fields, setState] = useState<any>(form);const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {  const input = fields[name];  const value = element.target.value;  const field = { ...input,  value };  setState(prevState => ({ ...prevState, [name]: field });}

Здесь заводится состояние для полей формы, и в качестве начального значения используется готовая структура формы. Функция handleInput будет необходима для редактирования данных. Как видно из кода, стейт будет обновляться полностью. Это специфика хука useState и текущей реализации. Если бы для работы с состояниями использовалась библиотека RxJs вместо хука useState, то была бы возможность обновлять состояние частично, не провоцируя повторный рендер компонента. В setState в данном примере состояние обновляется также через функцию обратного вызова. При использовании записи вида setState({ ...fields, [name]: field }) изменение другого поля провоцировало бы возврат остальных полей к исходным значениям.


Следующий пример кода проиллюстрирует применение формы.


// hooks/useForm.ts...const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const values = Object.entries(fields).reduce(((prev: any, [name, { value }]: any) => ({ ...prev, [name]: value })), {});    onSubmit({ values });  }

При помощи [каррирования] (https://learn.javascript.ru/currying-partials) принимается переданная из компонента функция и далее при сабмите вызывается с аргументами из хука. Каррирование в примере выше используется для того, чтобы иметь возможность вызвать метод в верстке, не выполняя его при рендере компонента.


Таким образом у нас получился минимальный хук для обычных форм.


Весь код хука
import { ChangeEvent, FormEvent,, useState } from 'react';export const useForm = (initialFields = {}) => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        value: (isString && value) || ((!isString && value.value) || ''),        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      },    };    return { ...fields, ...field };  }, {});  const [fields, setState] = useState<any>(form);  const handleInput = useCallback(    (element: ChangeEvent<HTMLInputElement>, name: string) => {      const input = fields[name];      const value = element.target.value;      const field = { ...input, value };      setState(prevState => ({ ...prevState, [name]: field });    }, [fields, setState]);  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const values = Object.entries(fields).reduce(((prev: any, [name, {value}]: any) => ({ ...prev, [name]: value })), {});    onSubmit({ values });  }  return {    handleSubmit,    fields,  }}

Типизация


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


Валидация


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


Итак, немного перепишем схему. Здесь указан массив validators для обоих полей и добавлен флаг обязательности поля.


// App.tsx...const formInputs = {  firstName: {    required: true,    validators: [      (s: string) => !s.length && 'Поле обязательно для заполнения',      (s: string) => s.length < 2 && 'Минимальная длина строки 2',      (s: string) => s.length <= 2 && 'А теперь 3',      (s: string) => parseInt(s) < 2 && 'Должна быть цифра больше 1',    ]  },  datetime: {    validators: [      (s: string) => new Date(s).getUTCFullYear() > new Date().getUTCFullYear() && 'Год рождения не может быть больше текущего',    ],  },}

Помимо обновленной схемы добавим в хук переменную isValid, которая будет запрещать/разрешать отправку формы по кнопке. Рядом с полями также будет выводиться ошибка. К слову, ошибку будем выводить только для тронутых полей.


// App.tsx...const { fields, isValid, handleSubmit } = useForm(formInputs);const { firstName, datetime }: Form = fields;return (    <div className="App">      <form onSubmit={handleSubmit(onSubmit)}>        <input type="text" value={firstName.value} onChange={firstName.setState}/>        <span>{firstName.touched && firstName.error}</span>        <input type="date" value={datetime.value} onChange={datetime.setState}/>        <span>{datetime.touched && datetime.error}</span>        <button disabled={!isValid}>Send form</button>      </form>    </div>);

Разберем этот код. В показанной функции ожидается 2 аргумента, field поле инпута, второй опциональный, в нем будут храниться дополнительные свойства для поля. Далее при помощи деструктуризации объекта заводятся переменные value, required и массив валидаций. Чтобы не менять свойства аргумента, заводятся новые переменные error и valid. Я объявил их через let, так как меняю их в процессе. В коде до обхода массива валидаторов стоит проверка на required, в теле условия проверяется значение поля, и там же прокидывается ошибка.


Мы подошли к условию, где проверяем переменную validators. Она должна быть массивом. Далее по коду создаем массив результатов выполнения функций валидации при помощи метода массива map. validateFn здесь хранится функция, в которую передается значение поля из свойства value. Результат выполнения функции валидации должен быть строкой, так как выводить мы будем именно текст ошибки. Если результат не строка, то возвращаться будет что-то другое. Конкретно здесь пустая строка, но там может быть и другое значение, например false. В любом случае далее происходит фильтрация массива результатов для удаления пустых значений. Таким образом, поле ошибки могло бы быть и массивом ошибок. Но здесь я решил выводить лишь одну ошибку, поэтому далее стоит условие проверки массива result, где присваивается ошибка и меняется состояние valid. В конце выполнения функция fieldValidation возвращает новый объект поля, где записаны все переданные ранее значения + новые, модифицированные.


Далее этот метод будет использоваться в функции handleInput и handleSubmit. В обоих случаях будет прогоняться валидация.


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


// hooks/useForm.tsconst [isValid, setFormValid] = useState<boolean>(true);const getFormValidationState = (fields) => Object.entries(fields).reduce((isValid: any, [_, value]: any) => Boolean(isValid * value.isValid), true);const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {    const input = fields[name];    const value = element.target.value;    const field = {      ...input,      value,      touched: true,      isValid: true,    };    const validatedField = fieldValidation(field);    setState((prevState) => {      const items = {...prevState, [name]: validatedField};      setFormValid(getFormValidationState(items));      return items;    });  }  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const fieldsArray = Object.entries(fields);    // Забираем только значения    const values = fieldsArray.reduce(((prev: any, [name, {value}]: any) => ({ ...prev, [name]: value })), {});    // Валидируем каждый инпут    const validatedInputs = fieldsArray.reduce((prev: any, [name, value]: any) => ({ ...prev, [name]: fieldValidation(value, { touched: true }) }), {});    // Изменяем значение стейта isValid    setFormValid(getFormValidationState(validatedInputs));    setState(validatedInputs);    onSubmit({ values });  }

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


useForm.ts
import { ChangeEvent, FormEvent, useState } from 'react';type IValidatorFN = (s: string) => {};export interface IField {  value?: string;  type?: string;  label?: string;  error?: string;  isValid?: boolean;  required?: boolean;  touched?: boolean;  setState?: (event: ChangeEvent<HTMLInputElement>) => {};  validators?: IValidatorFN[];}export type ICustomField<T = {}> = IField & T;export type ICustomObject<T = {}> = {  [key: string]: ICustomField & T;}export type IValues = {  [key: string]: string | number;}export type IForm = {  fields: ICustomObject;  isValid: boolean;  handleSubmit: (onSubmit: Function) => (e: FormEvent) => void;}type IOptions = {  [key: string]: any;}export const useForm = (initialFields: ICustomObject): IForm => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        type: 'text',        value: (isString && value) || ((!isString && value.value) || ''),        error: (!isString && value.error) || null,        validators: (!isString && value.validators) || null,        isValid: (!isString && value.isValid) || true,        required: (!isString && value.required) || false,        touched: false,        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      },    };    return {...fields, ...field};  }, {});  const [fields, setState] = useState<ICustomObject>(form);  const [isValid, setFormValid] = useState<boolean>(true);  const getFormValidationState = (fields: ICustomObject): boolean =>    Object.entries(fields).reduce((isValid: boolean, [_, value]: any) => Boolean(Number(isValid) * Number(value.isValid)), true);  const fieldValidation = (field: ICustomField, options: IOptions = {}) => {    const { value, required, validators } = field;    let isValid = true, error;    if (required) {      isValid = !!value;      error = isValid ? '' : 'field is required';    }    if (validators && Array.isArray(validators)) {      const results = validators.map(validateFn => {        if (typeof validateFn === 'string') return validateFn;        const validationResult = validateFn(value || '');        return typeof validationResult === 'string' ? validationResult : '';      }).filter(message => message !== '');      if (results.length) {        isValid = false;        error = results[0];      }    }    return { ...field, isValid, error, ...options };  };  const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {    const input = fields[name];    const value = element.target.value;    const field = {      ...input,      value,      touched: true,      isValid: true,    };    const validatedField = fieldValidation(field);    setState((prevState: ICustomObject) => {      const items = {...prevState, [name]: validatedField};      setFormValid(getFormValidationState(items));      return items;    });  }  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const fieldsArray = Object.entries(fields);    const values = fieldsArray.reduce(((prev: ICustomObject, [name, { value }]: any) => ({ ...prev, [name]: value })), {});    const validatedInputs = fieldsArray.reduce((prev: ICustomObject, [name, value]: any) => ({ ...prev, [name]: fieldValidation(value, { touched: true }) }), {});    setFormValid(getFormValidationState(validatedInputs));    setState(validatedInputs);    onSubmit({ values });  }  return {    fields,    isValid,    handleSubmit,  }}

App.tsx
import React from 'react';import { useForm, IValues } from './hooks/useForm';const formInputs = {  firstName: {    required: true,    validators: [      (s: string) => !s.length && 'Поле обязательно для заполнения',      (s: string) => s.length < 2 && 'Минимальная длина строки 2',      (s: string) => s.length <= 2 && 'А теперь 3',      (s: string) => parseInt(s) < 2 && 'Должна быть цифра, больше 1',    ],    label: 'First Name',  },  datetime: {    type: 'date',    label: 'Birth Date',    validators: [      (s: string) => new Date(s).getUTCFullYear() > new Date().getUTCFullYear() && 'Год рождения не может быть больше текущего',    ],  },  lastName: {    label: 'Last Name',  },}const App = () => {  const { fields, isValid, handleSubmit } = useForm(formInputs);  const { firstName, datetime, lastName } = fields;  const onSubmit = ({ values }: { values: IValues }) => {    console.log(values, 'submit');  }  const formFields = [firstName, lastName, datetime];  return (    <div className="App">      <form onSubmit={handleSubmit(onSubmit)}>        {formFields.map((field, index) => (          <div key={index}>            <input              type={field.type}              placeholder={field.label}              value={field.value}              onChange={field.setState}            />            <span>{field.touched && field.error}</span>          </div>        ))}        <div>          <button disabled={!isValid}>Send form</button>        </div>      </form>    </div>  );}export default App;

Итог


В данной статье мне хотелось привести пример создания собственного хука для работы с формами. Необязательно для работы с ними ограничиваться локальными состояниями или реакт-хуками. Как я упоминал выше, есть возможности и способы оптимизации текущего примера. Основная проблема в этом коде в том, что он вызывает рендер всего компонента при взаимодействии с полями. От этого можно избавиться, если написать реализацию без контроля состояния полей. Однако не всегда неуправляемый вариант подходит для решения той или иной задачи. На помощь может прийти библиотека rxjs или другие, использующие ее возможности, например, такие как focal или rixio. Но это тема для другой статьи.
Спасибо, что дочитали до конца. Я надеюсь, что после ознакомления с этими примерами у вас улучшилось понимание темы кастомных хуков, в частности, работа с формами.
Проект на github

Подробнее..

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

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


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


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


habr Разработка PWA с поддержкой распознавания лица и голоса
habr Фронтендер пишет нейронки. Уровень сложности хочу на ручки
en Искусство создания компонентов из реальной жизни
en Что не так с Web Assembly?
en Front-end прогнозы на 2021 год
en Создайте клон Google Doc с помощью HTML, CSS и JavaScript
video en Прогрессивное улучшение мертво?




CSS


en Варианты стилизации веб-компонентов
en Финальный отчет State Of CSS 2020
en Кастомные свойства в качестве состояния
en Действительно ли Tailwind того стоит?
en Изучение нового CSS Houdini Painting API


JavaScript


Что не так с классами в JavaScript?
en Что такое JavaScriptLandia?
en В чем разница между DOM Node и Element?
en Стек и очередь в JavaScript
en 7 ошибок, которые я сделал (и сожалею) как разработчик JavaScript
en Самая свежая информация о JavaScript классах
en Использование компонентов Bootstrap с кастомным JavaScript





Браузеры


В Microsoft Edge наконец-то появилась синхронизация истории и открытых вкладок
В адресной строке Chrome по умолчанию начнёт применяться HTTPS
Прощай, молодость: Google Chrome стал терять пользовательскую аудиторию
Mozilla отключила клавишу Backspace в браузере Firefox для защиты от случайной потери данных
Firefox 85 перейдёт на ECH для скрытия домена в HTTPS-трафике
en Интересный список браузеров, о которых вы никогда не слышали
en Чем браузер Vivaldi отличается от Google Chrome

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

Electron Разработка Desktop-приложений, используя HTML, CSS и JavaScript

11.01.2021 14:11:20 | Автор: admin
О чем вы узнаете из статьи?
Что такое Electron
Возможности и ограничения при разработке
Как работать с Electron
Плюсы и минусы
Известные проблемы
Вывод

Electron это библиотека, которую можно использовать для написания десктопных приложений с помощью html, css, js. Эти приложения могут быть упакованы под windows, mac, linux.

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

Список некоторых приложений, написанных на Electron
Visual Studio Code
Atom
Skype
Discord
Slack
GitHub Desktop
Figma
Zeplin
Postman


Что можно создать?
приложение по работе с файлами
скрытые приложение ( Tray )
приложения для менеджмента ( tracker, pomodoro )
видео/аудио проигрыватели
социальные чаты
календарь

Любые ваши идеи, которые будут полезны вам или компании

Когда вы работаете с Electron вы должны помнить о некоторых ограничениях старые ОС:
а) Windows 7+
b) Mac OS 10.10+
c) Ubuntu 12.04+

Electron состоит из трех компонентов:


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

Прицип работы в схеме


Взаимодействие процессов
Эти процессы полностью изолированы друг от друга и отвечают за различные задачи, но они должны как-то взаимодействовать и с этим нам помогает IPC (inter-process communication) модуль, который позволяет взаимодействовать между этими процессами.

Взаимодействие процессов в схеме


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

В примере показано как событие click, описанное в процессе рендеринга (Renderer) взаимодействует с главным (main) процессом.

Хранение данных: FileSystem
Обычно для хранения каких-либо данных используют БД.
Но в десктоп приложениях у нас есть доступ к файловой системе, поэтому мы можем хранить данные прямо на компьютере пользователя (если их не очень много).

Плюсы Electron приложения
WEB как UI ( HTML, CSS, JS )
Разработка только под Chrome ( Safari, IE )
Chrome Devtools
Скорость разработки
Современный стек ( React, TypeScript, ...)
Кроссплатформенная разработка ( Windows, Mac OS, Linux )

Минусы Electron приложений
Вес проекта
Время старта ( если большое приложение )
Не все платформы имеют одинаковый интерфейс ( Н-р: Tray )
Linux имеет большое количество дистрибутивов

Вывод: достаточно знать html, css, js, чтобы разрабатывать на Desktop.
Подробнее..
Категории: Html , Javascript , Css , Electron , Desktop , Desktop apps

Свободно стилизируемы outline DOM элементов

13.01.2021 16:08:51 | Автор: admin

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

Я пришел к своему решению, которое изложено в этой статье.

Большой gif

Работая над своим pet проектом мне нужно было сделать один и тот же стиль обводки (при наведении и фокусе) для элементов визуализаций и всех фокусируемых элементов DOM.


Моё решение

Вставляемdivповерх всего остального контента вdocument.body, и отключаем ему обработку событий черезpointer-events: none, растягиваем в размер документа,z-indexдолжен быть больше всех остальных на странице.

Добавляем еще 4-edivс абсолютными позициями в ранее добавленный родительский:

их стили (scss):

Добавляем подписку на события дляdocument:pointerenter,pointerleave,focus,blur:

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

В функциях слушателей фильтруем все события поtabIndex > -1уevent.target. При этом также проверяем что у ссылок естьhref, и кроме того что у элементов нет атрибутаdisabled. Когда происходитblurможет оказаться, что элемент оказался в контейнере, который тоже может иметь фокус (тут конечно можно задаться вопросом семантики, но такое бывает почему вbuttonнаходитсяa):

В методеshowполучаем размерыtargetс помощьюgetBoundingClientRect. А затем перемещаем, наши 4-ediv, каждый в свой угол:

Собственно, всё!

Описанный выше код вы можете найти здесь.

Заключение

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

Кроме того,overflow: hiddenне влияет на нашoutline, но иногда нам нужно следить за формой элемента и размерами (к примеру ResizeObserver) , поэтому вы можете улучшить этот подход, все в ваших руках.

Спасибо за прочтение!

Если вам нужна дополнительная информация, дайте мне знать об этом DMartzub.online.

P.S.: английская версия моей статьи здесь

Подробнее..
Категории: Html , Javascript , Css , Usability , Веб-дизайн , Accessibility , Ui , Ux , Ux/ui

Как собрать паука в Godot, Unigine или PlayCanvas

04.01.2021 22:21:38 | Автор: admin
С наступившим 21-м годом 21-го века.
В данной статье пробегусь по особенностям работы в трёх игровых движках, на примере написания кода для паукообразного средства передвижения.




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



Godot


Здесь у меня уже был готов небольшой проект с машинками и паука я решил добавить в одну из сцен (префабов), содержащую в себе подкласс машинок, которые не имеют колёс.
Сама сцена specific_base устроена таким образом, что в основе узел-пустышка, который просто висит где-то в мире, без движения, а по миру перемещается kinematic body внутри него. Камера находится внутри сцены, но вне body, просто следуя за ним.



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



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


Код внутри редактора Godot

Пишем код. Я использую GDScript, потому как особого смысла писать именно на C# в Годо не вижу (не настолько фанат фигурных скобочек):

extends Spatialexport var distance = 2.5#максимальная дистанция, после которой случится перерасчётexport var step = 1#переменная для дополнительного смещения лап (вещь необязательная)#ссылки на центр паука и одну из позиций лап, а также элементы для их храненияexport (NodePath) var spidercenter = nullvar trg_centerexport (NodePath) var spiderleg = nullvar trg_leg#переменные для расстояний по осям x и zvar x_dis = 0.0var z_dis = 0.0#переменная-таймер, а также флагvar time_lag = -1.0# инициализацияfunc _ready():self.hide()#скрыть лапуtrg_center = get_node(spidercenter)#запомнить объектыtrg_leg = get_node(spiderleg)LegPlace()#один раз вызвать установку лапки на позицию# основной циклfunc _process(delta):        #развернуть лапу в направлении центра паука. можно ввести таймер, чтобы делать это через малые интервалыself.look_at(trg_center.global_transform.origin, Vector3(0,1,0))        #включить видимость, если лапа была невидимой. это делалось для того, чтобы показывать её снова, после того как она скрывается в момент перестановки (чтобы перестановка выглядела как появление лапы в новой позиции уже развёрнутой в нужную сторону, а не перенесением с последующим разворачиванием). на самом деле можно было вынести внеочередной разворот и последующий показ лапы в LegPlaceif self.visible == false: self.show()if time_lag>=0:#если флаг-таймер запущен, то наращивать его значениеtime_lag +=1*delta if time_lag>0.06:#при истечении задержки сбросить флаг и вызвать перерисовкуtime_lag = -1.0LegPlace()else:#пока флаг неактивен считать дистанции от лапы до позиции лапы по двум осямx_dis = abs(trg_leg.global_transform.origin.x - self.global_transform.origin.x)z_dis = abs(trg_leg.global_transform.origin.z - self.global_transform.origin.z)if (x_dis + z_dis) > distance:#если дистанция больше лимита, запустить флагtime_lag = 0.0passfunc LegPlace():#собственно, сама функция перестановки лапыself.hide()step = step*(-1)self.global_transform.origin = trg_leg.global_transform.origin+Vector3(0,0,0.5*step)



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



Unigine


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



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



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


Unigine для редактирования кода запускает внешнюю среду

Код:

using System;//стандартная "шапка"using System.Collections;using System.Collections.Generic;using Unigine;//уникальный идентификатор компонента, генерируемый при создании скрипта[Component(PropertyGuid = "5a8dd6f85781adf7567432eae578c5414581ddac")]public class theLegBehavior : Component{[ShowInEditor][Parameter(Tooltip = "CenterSpider")]//указатель на центр паукаprivate Node spiderCenter = null;[ShowInEditor][Parameter(Tooltip = "Target Leg Point")]//указатель на точку крепленияprivate Node legPoint = null;//переменные для вычислений дистанций по осямprivate float x_dis= 0.0f;private float z_dis= 0.0f;private float ifps;//переменная для дельтатаймprivate float time_lag = -1.0f;//таймер-флагprivate void Init()//инициализация{node.Enabled = false;//скрыть лапуLegPlace();//вызвать перестановку лапы}private void Update()//основной цикл{ifps = Game.IFps;//сохранить дельтатаймif (time_lag>=0.0f){//далее уже знакомая конструкцияtime_lag += 1.0f*ifps;if (time_lag>=0.6f) {time_lag = -1.0f;LegPlace();}}else{x_dis = MathLib.Abs(legPoint.WorldPosition.x - node.WorldPosition.x);z_dis = MathLib.Abs(legPoint.WorldPosition.z - node.WorldPosition.z);            if (x_dis + z_dis > 0.8f){time_lag = 0.0f;}}}        //функция перерасчёта положения лапы. здесь уже финальный показ лапы встроен внутрь функции. также тут происходит единичный разворот в сторону центра паука. а постоянный разворот считается вне этого скрипта, а в отдельном, наброшенном на лапу скрипте, хотя я по сути уже это вынес оттуда и можно включить в Update этого скрипта.private void LegPlace(){node.Enabled = false;vec3 targetDirection = vec3.ZERO;targetDirection = (legPoint.WorldPosition - node.WorldPosition);quat targetRot = new quat(MathLib.LookAt(vec3.ZERO, targetDirection, vec3.UP, MathLib.AXIS.Y));quat delta = MathLib.Inverse(targetRot);delta.z = 0;delta.Normalize();node.WorldPosition = legPoint.WorldPosition;        targetDirection = (spiderCenter.WorldPosition - node.WorldPosition);node.SetWorldDirection(targetDirection, vec3.UP, MathLib.AXIS.Y);node.Enabled = true;}}



Видеонарезка паучьего теста в Unigine



PlayCanvas


PlayCanvas игровой движок под webGL, использующий javascript. Недавно начал в нём разбираться. Напоминает нечто среднее между Unity и Godot, но с разработкой онлайн редактор открывается в браузере.

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

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



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



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


В playcanvas редактор кода запускается вновой вкладке браузера

Код:
var TheLegBehavior = pc.createScript('theLegBehavior');//ссылка на центр паукаTheLegBehavior.attributes.add('N_spiderCenter', { type: 'entity' });//ссылка на точку постановки этой лапкиTheLegBehavior.attributes.add('N_legPoint', { type: 'entity' });this.x_dis = 0.0;this.z_dis = 0.0;this.time_lag = -1.0;// initialize code called once per entityTheLegBehavior.prototype.initialize = function() {        };// update code called every frameTheLegBehavior.prototype.update = function(dt) {    if (this.N_spiderCenter) {        this.entity.lookAt(this.N_spiderCenter.getPosition());    }};// постапдейтTheLegBehavior.prototype.postUpdate = function(dt) {    //    if (this.N_spiderCenter) {//        this.entity.lookAt(this.N_spiderCenter.getPosition());//    }    if (time_lag>=0.0){        time_lag+=1.0*dt;        if (time_lag>=0.06){            time_lag=-1.0;            this.LegUpdate();        }            } else {                x_dis = Math.abs(this.entity.getPosition().x-this.N_legPoint.getPosition().x);        z_dis = Math.abs(this.entity.getPosition().z-this.N_legPoint.getPosition().z);                if ((x_dis+z_dis)>3.0){         time_lag=0.0;        }                    }};TheLegBehavior.prototype.LegUpdate = function() {        if (this.N_legPoint) {        this.entity.setPosition(this.N_legPoint.getPosition());    }    //    if (this.N_spiderCenter.enabled === false) {//        this.entity.enabled = false;//    }//    if (this.N_spiderCenter.enabled === true) {//        this.entity.enabled = true;//    }    };


В целом, пока получилась заготовка паучка с четырьмя лапками и не совсем оптимальными расчётами.

Потестить получившегося на текущий момент кадавра можно здесь:
https://playcanv.as/p/rOebDLem/

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

На ПК, в отличие от смартфонов в этой демке работает прыжок (по кнопке пробел), заготовка стрейфа (Q и E) и перезагрузка уровня (на R).

Итог



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

Фронтендер пишет нейронки. Уровень сложности хочу на ручки

05.01.2021 22:19:51 | Автор: admin

Рано или поздно это должно произойти

Рано или поздно, фронтенд - разработчик устает играть со своими фреймворками, устает докучать коллегам - бэкендерам, устает играть в девопс и начинает смотреть в сторону машинного обучения, дата - саенс и вот это вот все. Благо, каждый второй курс для тех кто хочет войти вайти способствует этому, крича на всех платформах, как это легко. Я тоже, насытившись перекладыванием данных из базы в API, а потом из API в таблицы и формы, решил взять небольшой отпуск и попробовать применить свои скилы фронтендера в машинном обучении. Благо, существуют такие люди какDaniel ShiffmanиCharlie Gerard, которые своим примером помогают не бросить начатое, увидев первые страницы с математическими формулами.

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

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

Схема нейронной сетиСхема нейронной сети

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

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

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

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

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

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

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

Веса нейронной сетиВеса нейронной сети

Допустим, как мы уже сказали, нам не страшен лед если мы в ботинках. Тогда важность этого факта будет минимальной, мы можем учитывать только0.1*X1(W1== 0.1)от любого значенияX1. (Да, забыл сказать, так как у нас искусственная нейронная сеть, каждый факт должен быть выражен каким либо числом, но лучше, конечно, нормированным от -1 до 1). В этом случае какое бы значение мы не получили, мы будем принижать его важность в 10 раз. И наоборот, важность падения сосулек для нашей жизни максимальная, поэтому вес для такой связи будет1*X2(W2== 1).

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

Теперь, когда мы немного ближе рассмотрели нашу картинку, напоминает ли она еще что-то? По мне, так это самая настоящая функция (или более правильное название - функция активации).f(Х)=У. ГдеХ- это матрица всех входных данных или input слой,У- это матрица всевозможных вариантов исхода или output слой, и некоторый алгоритмf, который по какому-то паттерну преобразовывает входной слой в выходной.

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

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

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

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

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

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

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

Пример с погодой, который мы рассмотрели выше, относится к группе методов обучения с подкреплением (без учителя). Данная группа считается наиболее естесственной и близкой к настоящим нейронным сетям.

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

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

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

Итак, собрав эту небольшую информацию, как и зачем работают нейронки, давайте немного отдохнем и поиграемв raid shadow legends. Играть мы будем вdino gameот создателейgoogle chrome. Но нажимать пробел было бы очень просто, давайте напишем игру с нуля и нейронную сеть, которая сама будет играть в эту игру?

Dino game

В написании игры нам будет помогать такой редактор какp5.js. Данный инструмент уже заточен на реализацию подобных задач, когда необходимо реализовывать игровой цикл, работу с канвасом и обработкой событий во время самой игры. Любой скетч на р5 имеет две функции:setup, где мы инициализируем все наши переменные, рисуем канвас определенного размера и прочие вещи; иdraw- функция, которая вызывается на каждой итерации игрового цикла, здесь мы можем обновлять наши анимации и прочее.

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

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

class Cactus {  constructor() {}}class Dino {  constructor() {}}

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

...onTick(cb = () => { }) {  cb(this);}...

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

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

switch (this.state) {  case DinoStateEnum.run: break;  case DinoStateEnum.jump: {    this.currH += this.jumpSpeed;    if (this.currH == this.maxH) {      this.state = DinoStateEnum.fall;    }    break;  }  case DinoStateEnum.fall: {    this.currH -= this.jumpSpeed;    if (this.currH == 0) {      this.state = DinoStateEnum.run;    }    break;  }}

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

function updateCactuses() {  const copy = cactuses.slice();  for (let i = 0; i < copy.length; i++) {    let c = copy[i];    c.onTick(cactus => {      drawCactus(cactus)      cactus.currDistance -= dinoVelocitySlider.value();      if (cactus.currDistance + cactusW < initialDinoW && !cactus.passDinoPosition) {        updateDinoScore()        cactus.passDinoPosition = true;      }      if (cactus.currDistance < 0) {        cactuses.splice(i, 1);      }    })  }}

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

Нейроэволюция

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

<script  src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script><script>  tf.setBackend('cpu') // tf глобальная переменная</script>

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

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

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

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

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

В чем идея? Мы генерируем популяцию рандомно - настроенных дино размером 200-300 особей, и смотрим насколько они способны выживать. Далее мы выбираем несколько особей, у которых продолжительность жизни (best score) наибольшая, пытаемся немного их мутировать, как если бы это делала настоящая эволюция и создаем новое поколение. То есть имитируем настоящую эволюцию, поощряя продолжительность жизни. В итоге через несколько поколений преобладающим качеством наших дино должна стать долгая жизнь. Это как выводить пшеницу с наибольшими зернами (селекция).

Теперь когда с теорией, наконец, закончили, давайте перейдем к имплементации.

Мы будем реализовывать простуюнейронную сеть из трех слоев.

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

  • Cкрытый слой - наш черный ящик.

  • Выходной слой - решения, которые принимает дино.

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

  • Tекущее положение дино по осиУ.

  • Скорость дино.

  • Расстояние до ближайшего кактуса.

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

Всего 4 входных узла или нейрона.

Настройку скрытого слоя доверим великому рандому и просто выберем 8 нейронов. Выходных нейронов будет 2, два решения:прыгать или не прыгать.

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

createModel() {  const model = tf.sequential();  const hiddenLayer = tf.layers.dense({    units: this.hidden_nodes, // кол-во нейронов в скрытом слое (8)    inputShape: [this.input_nodes], // кол-во нейронов во входном слое (4)    activation: "sigmoid" // функция активации  });  model.add(hiddenLayer);  const outputLayer = tf.layers.dense({    units: this.output_nodes, // кол-во нейронов в выходном слое (2)    activation: "sigmoid"  });  model.add(outputLayer);  return model;}

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

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

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

predict(inputs) {  return tf.tidy(() => {    const xs = tf.tensor([inputs]); // создание тензора из массива (входной слой)    const ys = this.model.predict(xs); // предсказание сети    const output = ys.dataSync(); // превращение тензора в массив    return output;  });}

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

Как уже сказано,tensorflowработает только с тензорами, поэтому в нашу функцию предсказания мы посылаем тензор, полученный путем преобразования одномерного массива. После обработки входящих данных мы также получаем тензор и, чтобы превратить его в удобочитаемый формат, вызываем специальный метод. (Можем читать эти данные, как синхронно так и асинхронно). На выходе мы также получим одномерный массив длиной 2, поскольку мы указали 2 выходных нейрона. И, если мы вспомним начало, то поймем, что массив заполнен вероятностями того или иного решения, то есть прыгать или нет. Нам теперь достаточно проверить, чтоoutput[0] > output[1], чтобы дино прыгнул.

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

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

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

function drawDino(dino) {  if (dino.isDead) return;  if (dino.state != DinoStateEnum.run) {    // если дино прыгает, то рисуем его на текущей высоте    image(dino2, initialDinoW, initialDinoH - dino.currH, dinoW, dinoH); // р5 специальный метод добавления изображения на канвас  } else if (iteration % 7 == 0)    // иначе имитируем бег и перебирание ножками    image(dino1, initialDinoW, initialDinoH, dinoW, dinoH);  else    image(dino2, initialDinoW, initialDinoH, dinoW, dinoH);}

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

function updateGenerationIfNeeded() {  if (dinos.every(d => d.isDead)) {    cactuses = [];    dinoVelocitySlider.value(initDinoVelocity);    dinos = newGeneration(dinos)  }}

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

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

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

mutate(rate) {  tf.tidy(() => {    const weights = this.model.getWeights(); // берем веса модели    const mutatedWeights = [];    for (let i = 0; i < weights.length; i++) {      let tensor = weights[i]; // каждый вес - это тензор      let shape = weights[i].shape;      let values = tensor.dataSync().slice();      for (let j = 0; j < values.length; j++) {        if (Math.random() < rate) { // мутируем если нам повезло          let w = values[j];          values[j] = w + this.gaussianRandom(); // рандомное нормальное изменение в интервале от -1 до 1        }      }      let newTensor = tf.tensor(values, shape);      mutatedWeights[i] = newTensor;    }    this.model.setWeights(mutatedWeights); // ставим мутировавшие веса  });}

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

const calculateFitness = (dinos) => {  let sum = 0;  dinos.map(d => sum += d.score)  dinos.map(d => d.fitness = d.score / sum)}

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

Функция для такого действия может выглядеть так:

const pickOne = (dinos) => { // на входе дино отсортированные по убыванию fitness  let index = 0;  let r = Math.random();  while (r > 0) {    r = r - dinos[index].fitness;    index++;  }  index--;  let dino = dinos[index] // берем дино где-то из начала списка, как повезет с rate  const dinoBrain = dino.brain.copy();  dinoBrain.mutate(0.2) // делаем мутировавшую копию  let newDino = new Dino(dinoBrain) // дино для нового поколения  return newDino;}

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

for (let i = 0; i < TOTAL; i++) {  newDinos.push(pickOne(oldDinos));}console.log(currentGeneration++);return newDinos;

Запустив симуляцию мы можем увидеть несколько стратегий, которые появляются в первом поколении, дино либо прыгает постоянно, либо прыгает далеко перед кактусом и приземляется сразу за ним, либо прыгает прямо перед кактусом. К десятому поколению эти стратегии усредняются и дино способен прожить 50+ кактусов. По ссылке можно проверить симуляцию самому.

Исходники проекта на гитхабе.

P.S. Если возникли вопросы к материалу или заметили ошибку, welcome to PR's. Или напишите мне в твиттерv_hadoocken

Подробнее..

Перевод Пугающие эксперименты с PDF запускаем Арканоид в документе

06.01.2021 12:20:45 | Автор: admin

Подробнее об этом хаке и особенностях его работы можно узнать из доклада на !!con 2020 Playing Breakout inside a PDF!!

Если вы его не смотрели, то попробуйте открыть файл breakout.pdf в Chrome.

Как и многие из вас, я всегда считал PDF довольно безопасным форматом: автор создаёт текст и графику, после чего он открывается в программе просмотра PDF, больше ничего не делая. Несколько лет назад я мимоходом слышал об уязвимостях Adobe Reader, но особо не задумывался о том, как они могут возникать.

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


но самое интересное для нас


Разумеется, большинство программ для чтения PDF (кроме Adobe Reader) не реализует основную часть этих возможностей. Однако Chrome реализует JavaScript! Если вы откроете подобный файл PDF в Chrome, то он запустит скрипт. Я выяснил, повторив действия из этого поста о создании PDF с JS.

Однако здесь есть хитрость. Chrome реализует только крошечное подмножество огромной поверхности Acrobat JavaScript API. Реализация API в PDFium браузера Chrome в основном состоит из подобных заглушек:

FX_BOOL Document::addAnnot(IJS_Context* cc,                           const CJS_Parameters& params,                           CJS_Value& vRet,                           CFX_WideString& sError) {  // Not supported.  return TRUE;}FX_BOOL Document::addField(IJS_Context* cc,                           const CJS_Parameters& params,                           CJS_Value& vRet,                           CFX_WideString& sError) {  // Not supported.  return TRUE;}FX_BOOL Document::exportAsText(IJS_Context* cc,                               const CJS_Parameters& params,                               CJS_Value& vRet,                               CFX_WideString& sError) {  // Unsafe, not supported.  return TRUE;}

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

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

Вероятно, можно встроить в PDF компилятор C, скомпилировав его в JS, например, с помощью Emscripten, но тогда компилятор C должен будет получать ввод из формы простого текста (plain text), а вывод выполнять снова в поле формы.

На самом деле, я заинтересовался PDF пару недель назад из-за PostScript; я читал посты Дона Хопкинса о NeWS напоминающей AJAX системе, но реализованной в 80-х на PostScript.

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

Ещё один любопытный момент: как любой долгоживущий цифровой формат (лично я испытываю нежные чувства к файловой системе FAT), PDF сам по себе является своего рода историческим документом. Мы можем отследить, как поколения инженеров добавляли нужные им в своё время функции, пытаясь при этом не поломать уже существующие.

Я не совсем понимаю, зачем разработчики Chrome вообще заморачивались поддержкой JS. Они взяли код программы чтения PDF из Foxit; возможно, у Foxit был какой-то клиент, использовавший валидацию форм через JavaScript?

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

Breakout


Так что же мы можем сделать с предоставляемой нам Chrome поверхностью API?

Кстати, должен извиниться за неидеальное распознавание коллизий и непостоянную скорость игры. БОльшую часть игры я содрал с туториала.

Первые доступные пользователю точки ввода-вывода, которые я смог найти в реализации PDF API браузера Chrome, находились в Field.cpp.

Мы не можем менять цвет заливки текстового поля во время выполнения, зато можем менять прямоугольник его границ и задавать стиль границ. Мы не можем считывать точное положение мыши, однако можем при создании PDF привязать к полям скрипты mouse-enter и mouse-leave. Также во время выполнения нельзя добавлять поля: придётся ограничиться тем, что мы поместили в PDF в момент создания. Любопытно, почему разработчики выбрали именно эти методы? Это похоже на какой-то стереотип о программировании на олдскульном FORTRAN: необходимо объявлять все переменные заранее, чтобы компилятор мог статически выделить под них память.

Итак, файл PDF генерируется скриптом, заранее создающим набор текстовых полей, в том числе и игровых элементов:

  • Ракетку
  • Кирпичи
  • Мяч
  • Очки
  • Жизни

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

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

Во-вторых, мы создаём поле под названием whole, закрывающее всю верхнюю половину экрана. Chrome не ожидает, что отображение PDF будет меняться, поэтому если перемещать поля в JS, то получатся довольно сильные артефакты. Это поле whole решает данную проблему мы включаем/отключаем его во время рендеринга кадра. Этот трюк заставляет Chrome подчищать артефакты.

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

Полезные ресурсы






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


Закажите сервер и сразу начинайте работать! Создание VDS любой конфигурации в течение минуты, в том числе серверов для хранения большого объёма данных до 4000 ГБ. Эпичненько :)

Подробнее..

Мониторинг бизнес-процессов Camunda

07.01.2021 18:21:18 | Автор: admin

Привет, Хабр.

Меня зовут Антон и я техлид в компании ДомКлик. Создаю и поддерживаю микросервисы позволяющие обмениваться данными инфраструктуре ДомКлик с внутренними сервисами Сбербанка.

Это продолжение цикла статей о нашем опыте использования движка для работы с диаграммами бизнес-процессов Camunda. Предыдущая статья была посвящена разработке плагина для Bitbucket позволяющего просматривать изменения BPMN-схем. Сегодня я расскажу о мониторинге проектов, в которых используется Camunda, как с помощью сторонних инструментов (в нашем случае это стек Elasticsearch из Kibana и Grafana), так и родного для Camunda Cockpit. Опишу сложности, возникшие при использовании Cockpit, и наши решения.

Когда у тебя много микросервисов, то хочется знать об их работе и текущем статусе всё: чем больше мониторинга, тем увереннее ты себя чувствуешь как в штатных, так и внештатных ситуациях, во время релиза и так далее. В качестве средств мониторинга мы используем стек Elasticsearch: Kibana и Grafana. В Kibana смотрим логи, а в Grafana метрики. Также в БД имеются исторические данные по процессам Camunda. Казалось бы, этого должно хватать для понимания, работает ли сервис штатно, и если нет, то почему. Загвоздка в том, что данные приходится смотреть в трёх разных местах, и они далеко не всегда имеют четкую связь друг с другом. На разбор и анализ инцидента может уходить много времени. В частности, на анализ данных из БД: Camunda имеет далеко не очевидную схему данных, некоторые переменные хранит в сериализованном виде. По идее, облегчить задачу может Cockpit инструмент Camunda для мониторинга бизнес-процессов.


Интерфейс Cockpit.

Главная проблема в том, что Cockpit не может работать по кастомному URL. Об этом на их форуме есть множество реквестов, но пока такой функциональности из коробки нет. Единственный выход: сделать это самим. У Cockpit есть Sring Boot-автоконфигурация CamundaBpmWebappAutoConfiguration, вот её-то и надо заменить на свою. Нас интересует CamundaBpmWebappInitializer основной бин, который инициализирует веб-фильтры и сервлеты Cockpit.

Нам необходимо передать в основной фильтр (LazyProcessEnginesFilter) информацию об URL, по которому он будет работать, а в ResourceLoadingProcessEnginesFilter информацию о том, по каким URL он будет отдавать JS- и CSS-ресурсы.

Для этого в нашей реализации CamundaBpmWebappInitializer меняем строчку:

registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")

на:

registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)

servicePath это наш кастомный URL. В самом же CustomLazyProcessEnginesFilter указываем нашу реализацию ResourceLoadingProcessEnginesFilter:

class CustomLazyProcessEnginesFilter:       LazyDelegateFilter<ResourceLoaderDependingFilter>       (CustomResourceLoadingProcessEnginesFilter::class.java)

В CustomResourceLoadingProcessEnginesFilter добавляем servicePath ко всем ссылкам на ресурсы, которые мы планируем отдавать клиентской стороне:

override fun replacePlaceholder(       data: String,       appName: String,       engineName: String,       contextPath: String,       request: HttpServletRequest,       response: HttpServletResponse) = data.replace(APP_ROOT_PLACEHOLDER, "$contextPath$servicePath")           .replace(BASE_PLACEHOLDER,                   String.format("%s$servicePath/app/%s/%s/", contextPath, appName, engineName))           .replace(PLUGIN_PACKAGES_PLACEHOLDER,                   createPluginPackagesString(appName, contextPath))           .replace(PLUGIN_DEPENDENCIES_PLACEHOLDER,                   createPluginDependenciesString(appName))

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

Но ведь не может быть всё так просто? В нашем случае Cockpit не способен работать из коробки на нескольких экземплярах приложения (например, в подах Kubernetes), так как вместо OAuth2 и JWT используется старый добрый jsessionid, который хранится в локальном кэше. Это значит, что если попытаться залогиниться в Cockpit, подключенный к Camunda, запущенной сразу в нескольких экземплярах, имея на руках ей же выданный jsessionid, то при каждом запросе ресурсов от клиента можно получить ошибку 401 с вероятностью х, где х = (1 1/количество_под). Что с этим можно сделать? У Cockpit во всё том же CamundaBpmWebappInitializer объявлен свой Authentication Filter, в котором и происходит вся работа с токенами; надо заменить его на свой. В нём из кеша сессии берём jsessionid, сохраняем его в базу данных, если это запрос на авторизацию, либо проверяем его валидность по базе данных в остальных случаях. Готово, теперь мы можем смотреть инциденты по бизнес-процессам через удобный графический интерфейс Cockpit, где сразу видно stacktrace-ошибки и переменные, которые были у процесса на момент инцидента.

И в тех случаях, когда причина инцидента ясна по stacktrace исключения, Cockpit позволяет сократить время разбора инцидента до 3-5 минут: зашел, посмотрел, какие есть инциденты по процессу, глянул stacktrace, переменные, и вуаля инцидент разобран, заводим баг в JIRA и погнали дальше. Но что если ситуация немного сложнее, stacktrace является лишь следствием более ранней ошибки или процесс вообще завершился без создания инцидента (то есть технически всё прошло хорошо, но, с точки зрения бизнес-логики, передались не те данные, либо процесс пошел не по той ветке схемы). В этом случае надо снова идти в Kibana, смотреть логи и пытаться связать их с процессами Camunda, на что опять-таки уходит много времени. Конечно, можно добавлять к каждому логу UUID текущего процесса и ID текущего элемента BPMN-схемы (activityId), но это требует много ручной работы, захламляет кодовую базу, усложняет рецензирование кода. Весь этот процесс можно автоматизировать.

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

Во-первых, необходимо зарегистрировать customPreBPMNParseListeners в текущем processEngine Camunda. В слушателе переопределить методы parseStartEvent (добавление слушателя на событие запуска верхнеуровневого процесса) и parseServiceTask (добавление слушателя на событие запуска ServiceTask).

В первом случае мы создаем Sleuth-контекст:

customContext[X_B_3_TRACE_ID] = businessKeycustomContext[X_B_3_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_PARENT_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_SAMPLED] = "0" val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(customContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

и сохраняем его в переменную бизнес-процесса:

execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)

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

val storedContext = execution       .getVariableTyped<ObjectValue>(TRACING_CONTEXT)       .getValue(HashMap::class.java) as HashMap<String?, String?>val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(storedContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

Нам нужно трейсить логи вместе с дополнительными параметрами, такими как activityId (ID текущего BPMN-элемента), activityName (его бизнес-название) и scenarioId (ID схемы бизнес-процесса). Такая возможность появилась только с выходом Sleuth 3.

Для каждого параметра нужно объявить BaggageField:

companion object {   val HEADER_BUSINESS_KEY = BaggageField.create("HEADER_BUSINESS_KEY")   val HEADER_SCENARIO_ID = BaggageField.create("HEADER_SCENARIO_ID")   val HEADER_ACTIVITY_NAME = BaggageField.create("HEADER_ACTIVITY_NAME")   val HEADER_ACTIVITY_ID = BaggageField.create("HEADER_ACTIVITY_ID")}

Затем объявить три бина для обработки этих полей:

@Beanopen fun propagateBusinessProcessLocally(): BaggagePropagationCustomizer =       BaggagePropagationCustomizer { fb ->           fb.add(SingleBaggageField.local(HEADER_BUSINESS_KEY))           fb.add(SingleBaggageField.local(HEADER_SCENARIO_ID))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_NAME))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_ID))       }/** [BaggageField.updateValue] now flushes to MDC  */@Beanopen fun flushBusinessProcessToMDCOnUpdate(): CorrelationScopeCustomizer =       CorrelationScopeCustomizer { builder ->           builder.add(SingleCorrelationField.newBuilder(HEADER_BUSINESS_KEY).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_SCENARIO_ID).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_NAME).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_ID).flushOnUpdate().build())       }/** [.BUSINESS_PROCESS] is added as a tag only in the first span.  */@Beanopen fun tagBusinessProcessOncePerProcess(): SpanHandler =       object : SpanHandler() {           override fun end(context: TraceContext, span: MutableSpan, cause: Cause): Boolean {               if (context.isLocalRoot && cause == Cause.FINISHED) {                   Tags.BAGGAGE_FIELD.tag(HEADER_BUSINESS_KEY, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_SCENARIO_ID, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_NAME, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_ID, context, span)               }               return true           }       }

После чего мы можем сохранять дополнительные поля в контекст Sleuth:

HEADER_BUSINESS_KEY.updateValue(businessKey)HEADER_SCENARIO_ID.updateValue(scenarioId)HEADER_ACTIVITY_NAME.updateValue(activityName)HEADER_ACTIVITY_ID.updateValue(activityId)

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

И такая возможность имеется. Cockpit поддерживает пользовательские расширения плагины, в Kibana есть Rest API и две клиентские библиотеки для работы с ним: elasticsearch-rest-low-level-client и elasticsearch-rest-high-level-client.

Плагин представляет из себя проект на Maven, наследуемый от артефакта camunda-release-parent, с бэкендом на Jax-RS и фронтендом на AngularJS. Да-да, AngularJS, не Angular.

У Cockpit есть подробная документация о том, как писать для него плагины.

Уточню лишь, что для вывода логов на фронтенде нас интересует tab-панель на странице просмотра информации о Process Definition (cockpit.processDefinition.runtime.tab) и странице просмотра Process Instance (cockpit.processInstance.runtime.tab). Для них регистрируем наши компоненты:

ViewsProvider.registerDefaultView('cockpit.processDefinition.runtime.tab', {   id: 'process-definition-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-definition/processDefinitionTabView.html'});ViewsProvider.registerDefaultView('cockpit.processInstance.runtime.tab', {   id: 'process-instance-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-instance/processInstanceTabView.html'});

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

<div cam-searchable-area (1)    config="searchConfig" (2)    on-search-change="onSearchChange(query, pages)" (3)    loading-state="Loading..." (4)    text-empty="Not found"(5)    storage-group="'ANU'"    blocked="blocked">   <div class="col-lg-12 col-md-12 col-sm-12">       <table class="table table-hover cam-table">           <thead cam-sortable-table-header (6)                  default-sort-by="time"                  default-sort-order="asc" (7)                  sorting-id="admin-sorting-logs"                  on-sort-change="onSortChanged(sorting)"                  on-sort-initialized="onSortInitialized(sorting)" (8)>           <tr>               <!-- headers -->           </tr>           </thead>           <tbody>           <!-- table content -->           </tbody>       </table>   </div></div>

  1. Атрибут для объявления компонента поиска.
  2. Конфигурация компонента. Здесь имеем такую структуру:

    tooltips = { //здесь мы объявляем плейсхолдеры и сообщения,                    //которые будут выводиться в поле поиска в зависимости от результата   'inputPlaceholder': 'Add criteria',   'invalid': 'This search query is not valid',   'deleteSearch': 'Remove search',   'type': 'Type',   'name': 'Property',   'operator': 'Operator',   'value': 'Value'},operators =  { //операторы, используемые для поиска, нас интересует сравнение строк     'string': [       {'key': 'eq',  'value': '='},       {'key': 'like','value': 'like'}   ]},types = [// поля, по которым будет производится поиск, нас интересует поле businessKey   {       'id': {           'key': 'businessKey',           'value': 'Business Key'       },       'operators': [           {'key': 'eq', 'value': '='}       ],       enforceString: true   }]
    

  3. Функция поиска данных используется как при изменении параметров поиска, так и при первоначальной загрузке.
  4. Какое сообщение отображать во время загрузки данных.
  5. Какое сообщение отображать, если ничего не найдено.
  6. Атрибут для объявления таблицы отображения данных поиска.
  7. Поле и тип сортировки по умолчанию.
  8. Функции сортировок.

На бэкенде нужно настроить клиент для работы с Kibana API. Для этого достаточно воспользоваться RestHighLevelClient из библиотеки elasticsearch-rest-high-level-client. Там указать путь до Kibana, данные для аутентификации: логин и пароль, а если используется протокол шифрования, то надо указать подходящую реализацию X509TrustManager.

Для формирования запроса поиска используем QueryBuilders.boolQuery(), он позволяет составлять сложные запросы вида:

val boolQueryBuilder = QueryBuilders.boolQuery();KibanaConfiguration.ADDITIONAL_QUERY_PARAMS.forEach((key, value) ->       boolQueryBuilder.filter()               .add(QueryBuilders.matchPhraseQuery(key, value)));if (!StringUtils.isEmpty(businessKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.BUSINESS_KEY, businessKey));}if (!StringUtils.isEmpty(procDefKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.SCENARIO_ID, procDefKey));}if (!StringUtils.isEmpty(activityId)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.ACTIVITY_ID, activityId));}

Теперь мы прямо из Cockpit можем просматривать логи отдельно по каждому процессу и по каждой activity. Выглядит это так:


Таб для просмотра логов в интерфейсе Cockpit.

Но нельзя останавливаться на достигнутом, в планах идеи о развитии проекта. Во-первых, расширить возможности поиска. Зачастую в начале разбора инцидента business key процесса на руках отсутствует, но имеется информация о других ключевых параметрах, и было бы неплохо добавить возможность настройки поиска по ним. Также таблица, в которую выводится информация о логах, не интерактивна: нет возможности перехода в нужный Process Instance по клику в соответствующей ему строке таблицы. Словом, развиваться есть куда. (Как только закончатся выходные, я опубликую ссылку на Github проекта, и приглашаю туда всех заинтересовавшихся.)
Подробнее..

Перевод Как я реализовал MVC в JavaScript

08.01.2021 02:22:10 | Автор: admin

для лучшей разделяемости кода

Для будущих студентов курса "Архитектура и шаблоны проектирования" и всех интересующихся подготовили перевод полезного материала.

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


Что из себя представляет архитектурный паттерн Model, View, Controller (MVC)?

Источник: документация Rails

Архитектура MVC разделяет ваш код на три (3) уровня: модели (Models), представления (Views) и контроллеры (Controllers), выполняющие различные задачи внутри программы.

Изображение взято из ВикипедииИзображение взято из Википедии

Уровень модели

В Ruby on Rails этот уровень содержит модель предметной области, которая обычно представляет определенный класс объектов (например, Человек, Животное, Книги). Обычно именно здесь обрабатывается бизнес-логика, поскольку модель связана с базой данных, и данные для нее извлекаются из строк соответствующей таблицы.

Уровень представления

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

Уровень контроллера

В Rails этот уровень отвечает за взаимодействие с моделью, манипулирование ее данными и предоставление соответствующих ответов на различные HTTP-запросы.

Как бы паттерн MVC выглядел в JavaScript?

Источник: документация MDN

Поскольку JavaScript обычно не предполагает использования баз данных (хотя и может) или обработки HTTP-запросов (опять же, может), паттерн MVC придется немного подкорректировать, чтобы он соответствовал специфике языка.

Изображение взято с MDNИзображение взято с MDN

Уровень модели

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

Возьмем, к примеру, приложение Classroom, которое отслеживает, какие классы посещает человек. В этом случае уровень модели можно разделить на классы, такие как Classroom, Person и модель на основе массива под названием Subjects.

Базовые классы модели

class Classroom {  constructor(id, subject = 'Homeroom') {    this.id = id;    this.persons = [];    this.subject = subject;  }}

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

class Person {  constructor(id, firstN = 'John', lastN = 'Doe') {    this.id = id;    this.firstName = firstN;    this.lastName = lastN;    this.subjects = [];    this.classrooms = [];  }}

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

const subjects = [  "English",  "Math",  "Computer Science",  "Business",  "Finance",  "Home Economics"];

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

Уровень контроллера

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

Например, в приложении Classroom контроллер получает данные, вводимые пользователем, от элементов представления, таких как ввод текста (text input) или выбор из списка опций (select options), а также нажатия кнопок, которые используются для изменения модели.

import classroomModel from "../models/classroom";class ClassroomController {  constructor() {    this.lastID = 0;    this.classrooms = [];    this.selectedClass = null;  }  selectClassroom(classroomID) {    this.selectedClass = this.classrooms    .filter(c => c.id === parseInt(classroomID, 10))[0];  }  addClassroom(subject) {    this.classrooms.push(      new classroomModel(this.lastID, subject)      );    this.lastID += 1;  }  removeClassroom(classroomID) {    this.classrooms = this.classrooms      .filter(c => c.id !== parseInt(classroomID, 10));  }  setSubject(subject, classroomID) {    const classroom = this.classrooms      .filter(c => c.id === parseInt(classroomID, 10))[0];    classroom.subject = subject;  }  addPerson(person, classroom) {    // const classroom = this.classrooms    // .filter(c => c.id === parseInt(classroomID, 10))[0];    if (!person) return;    classroom.addPerson(person);  }  removePerson(person, classroomID) {    const classroom = this.classrooms    .filter(c => c.id === parseInt(classroomID, 10))[0];    classroom.removePerson(person);  }}

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

Этот контроллер имеет три собственные переменные: lastID (каждый раз, когда объект класса создается и добавляется к массиву классов, значение этой переменной инкрементируется), classrooms (массив всех созданных объектов класса) и selectedClass.

Уровень представления

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

Например, в приложении Classroom представление будет предоставлять элементы DOM (объектной модели документа), такие как кнопки, инпуты и контейнеры (<div/>, <span/ >, <p/> и т. д.) для отображения различных людей и классов, и связанных с ними данных.

import classroomController from "../controllers/classroom";import subjects from "../models/subjects";class ClassroomView {  constructor(appDiv) {    this.classroomController = new classroomController();    this.classroomSectionDiv = document.createElement('div');    this.classroomsDiv = document.createElement('div');    this.addclassBtn = document.createElement('button');    this.selectSubjectInput = document.createElement('select');    this.classroomSectionDiv.classList.add('classroom-section');    this.classroomsDiv.classList.add('classroom-container');    this.selectSubjectInput.innerHTML = subjects.map((option, index) => (      `<option key=${index} value=${option}>${option.toUpperCase()}</option>`    ));    this.addclassBtn.textContent = 'New Class';    this.addclassBtn.addEventListener('click', () => this.addClassroom());    this.classroomSectionDiv.append(      this.classroomsDiv, this.selectSubjectInput,      this.addclassBtn,      );    appDiv.appendChild(this.classroomSectionDiv);  }  updateView() {    const { classroomController, classroomsDiv } = this;    const allClassrooms = classroomController.classrooms.map(      c => {        const removeBtn = document.createElement('button');        const classDiv = document.createElement('div');        classDiv.classList.add('classroom');        if (classroomController.selectedClass === c) {          classDiv.classList.add('selected');        }        classDiv.addEventListener('click', () => this.selectClassroom(classDiv.getAttribute('data-classroom-id')));        classDiv.setAttribute('data-classroom-id', c.id);        removeBtn.addEventListener('click', () => this.removeClassroom(removeBtn.getAttribute('data-classroom-id')));        removeBtn.setAttribute('data-classroom-id', c.id);        removeBtn.classList.add('remove-btn');        removeBtn.textContent= 'remove';        const allPersons = c.persons.map(p => (          `<div class="person-inline">            <span class="fname">${p.firstName}</span>            <span class="lname">${p.lastName}</span>            <span class="${p.occupation}">${p.occupation}</span>          </div>`        ));        classDiv.innerHTML = `<div class="m-b">            <span class="id">${c.id}</span>            <span class="subject">${c.subject}</span></div>            <div class="all-persons">${allPersons.join('')}</div>`;        classDiv.appendChild(removeBtn);        return classDiv;      }    );    classroomsDiv.innerHTML='';    allClassrooms.map(div => classroomsDiv.append(div));  }    selectClassroom(classroomID) {    const { classroomController } = this;    classroomController.selectClassroom(classroomID);     this.updateView();  }  addClassroom() {    const {      classroomController,      selectSubjectInput,    } = this;    const subjectChosen = selectSubjectInput.value;    classroomController.addClassroom(subjectChosen);    this.updateView();  }  removeClassroom(classroomID) {    const { classroomController } = this;    classroomController.removeClassroom(classroomID);    this.updateView();  }  addPerson(person, classroomID) {    const { classroomController } = this;    classroomController.addPerson(person, classroomID);    this.updateView();  }}

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

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

Все функции в представлении просто захватывают значения из UI элементов DOM и передают их как переменные функциям контроллера. Функции selectClassroom(), addClassroom() и removeClassroom() добавляются к элементам DOM через функцию updateView() как события через функцию addEventListener().

Доступ ко всем контроллерам и представлениям с помощью одного представления

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

import classroomView from './classroom';import personView from './person';class AppView {  constructor(appDiv) {    this.classroomView = new classroomView(appDiv);    this.personView = new personView(appDiv);    this.addPersonToClassBtn = document.createElement('button');    this.addPersonToClassBtn.textContent = 'Add selected Person to Selected Class';    this.addPersonToClassBtn.addEventListener('click', () => this.addPersonToClass());    appDiv.appendChild(this.addPersonToClassBtn);  }  addPersonToClass() {    const { classroomView, personView } = this;    const { classroomController } = classroomView;    const { personController } = personView;    const selectedClassroom = classroomController.selectedClass;    const selectedPerson = personController.selectedPerson;    classroomView.addPerson(selectedPerson, selectedClassroom);    personView.updateView();  }}

Класс AppView будет иметь собственные переменные, которые будут связываться как с ClassroomView, так и с PersonView. Поскольку он имеет доступ к этим двум представлениям, он также имеет доступ и к их контроллерам.

Кнопка выше создается AppView. Оно получает значения selectedClassroom и selectedPerson из соответствующих контроллеров и при взаимодействии запускает функцию addPerson() в ClassroomView.

Чтобы полностью посмотреть приложение Classroom, переходите в CodeSandBox по этой ссылке.

Некоторые преимущества использования структуры MVC

Источники: Brainvire, c-sharpcorner, StackOverflow, Wikipedia

1. Разделение обязанностей

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

2. Одновременная разработка

Поскольку модель MVC четко разделяет проект на три (3) уровня, становится намного проще поделить и распределить задачи между несколькими разработчиками.

3. Простота модификации

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

4. Разработка через тестирование (TDD)

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


Узнать подробнее о курсе "Архитектура и шаблоны проектирования".

Зарегистрироваться на открытый вебинар на тему "Интерпретатор".

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

Кстати, о "красивой упаковке" онлайн-сертификатов мырассказываем в этой статье.

ЗАБРАТЬ СКИДКУ

Подробнее..

Библиотека Frontend-разработчика, часть 3 Литература уровня Middle и выше

09.01.2021 14:22:36 | Автор: admin

Предисловие

Явление деления разработчиков на уровни очень распространено. Даже в вакансиях чаще всего пишут не просто "Frontend-разработчик", а более развернуто - "Junior/Middle/Senior/${место для вашей должности} Frontend-разработчик". Для чего? С помощью такого деления легче делегировать задачи в команде. У каждого разработчика своя особая матрица компетенций, свои навыки, которые он оттачивал месяцами, а то и годами. С помощью такого деления процесс разработки ускоряется в разы.
Вообще на рынке (я смотрю на рынок стран СНГ) по состоянию на начало 2021 года среди Frontend-разработчиков имеют место быть такие должности (от низшего уровня, к наивысшему и без привязки к инструментам/библиотекам)

  • Intern Frontend-developer - другими словами - стажер

  • Junior Frontend-developer - уровень выше начального, уже более менее самостоятельная единица

  • Middle Frontend-developer - самостоятельная единица, командный игрок, много умеет, но чаще всего в одной сфере / направлении. Через тернии к звездам или стремится к Senior

  • Senior Frontend-developer* - старший, чаще всего в компаниях самый опытный. Человек, который попробовал многие инструменты, много может, много пробует.

  • Architector Frontend-developer** - человек, который часто занимается вопросами выбора технологий, решений в вопросах архитектуры будущего приложения

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

* - он может быть также ведущим разработчиком, который выполняет все те же функци, что и выполнял senior, но при этом отчасти может выполнять функции архитектора. Я видел компании, где понятия "ведущий разработчик" и "Senior developer" отождествлялись.

** - Архитектор - достаточно размытое понятие. Иногда, в командах нет архитектора, но при этом есть techlead. Если загуглить, то он решает все те же задачи, что и архитектор.

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

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

Эта статья как раз о том сборнике книг для разработчиков уровня "Middle и выше".

Секреты Javascript Ниндзя. Джон Резиг, Беэр Бибо, Иосип Марас

У каждой книги есть своя цель, и у этой книги она тоже есть. Цель - напомнить вам о том, что есть в чистом js, что есть в браузере, что такое DOM и какие есть возможности у него.
Она буквально вам говорит: "Отвлекитесь от фреймворков и библиотек - посмотрите что умеет язык без этого!".
На страницах есть краткая информация для повторения основных понятий в js, некоторых стандартных приемов работы, например, с событиями. Все это приправлено от души хорошими примерами кода.
Книга хороша своим грамотным делением на главы и простым языком изложения. Тот самый случай, когда на ночь можно почитать не только с удовольствием, но и с пользой.

Рефакторинг кода на Javascript. Мартин Фаулер

Мы поняли как работают события, что такое замыкания, как использовать генераторы, как убежать от адской пирамиды вызовов и самое главное - с помощью чего. Теперь время писать код. Вот только мы не знаем самого главного в любом коде - способов его организации.
На просторах интернета и в книгах, о которых я напишу ниже вы сможете увидеть такие понятия как DRY, SOLID, KISS, YAGNI - это все общие положения, немного размытые, о построении архитектуры кода, приложения.
В Книге Фаулера идет полное описание каждого действия в момент рефакторинга кода. И да, Мартин Фаулер описал все способы - поверьте, Фаулер докопался и описал даже способ рефакторинга "Вынос в функцию". По факту - книга полноценный справочник или очень хорошая настольная энциклопедия, которая служит на благо архитектуры вашего кода.

Javascript для профессионалов. Джон Резиг.

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

Погружение в Паттерны Проектирования от RefactoringGuru

А что такое паттерны?

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

Взято с RefactoringGuru

Я уже говорил об DRY, SOLID, KISS, YAGNI. В электронной книге или на официальном сайте вы сможете ознакомиться с данными понятиями более подробно.
Авторов можно поддержать - купить электронную версию, что я советую всем сделать. Труд был проделан огромадный. Я всегда был сторонником идеи "Если можешь объяснить ребенку что-то - значит ты владеешь этим на все 100". Книга "Погружение в Паттерны Проектирования" будет понятна наверное даже ребенку, потому что все очень подробно описано как в главах об архитектуре кода, так и в главах про устройство и построение архитектуры всего приложения.

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

Чистая архитектура. Искусство разработки программного обеспечения. Роберт Мартин

Да, последние книги про архитектуру, но иначе никак - разработчик не должен писать код ради кода, он должен думать о пригодности этого кода в будущем. Даже не так - разработчик должен писать код ради бизнеса. А чтобы он был ради бизнеса, он должен быть поддерживаемым в будущем.
Роберт Мартин писал книгу не для Frontend-developer`ов, а для всех разработчиков и им сочувствующих. Мартин объяснил почему стоит уделять внимание архитектуре, как проводить архитектурный рефакторинг, с основными принципами, о которых также написано в refactoringGuru. (в refactoringGuru, как по мне, более подробно раскрыты некоторые моменты).

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

Заключение

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

Спасибо за внимание!

Подробнее..

Перевод Компилируем Svelte в уме. Часть 13

09.01.2021 14:22:36 | Автор: admin

Введение

Давайте вспомним как мы пишем веб-приложения без фреймворков:

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

// создаем элемент h1const h1 = document.createElement('h1');h1.textContent = 'Hello World';// ...и добавляем его в bodydocument.body.appendChild(h1);

Обновляем элемент

// обновляем текст элемента h1h1.textContent = 'Bye World';

Удаляем элемент

// наконец, мы удаляем элемент h1document.body.removeChild(h1);

Добавляем стили к элементу

const h1 = document.createElement('h1');h1.textContent = 'Hello World';// добавляем класс к элементу h1h1.setAttribute('class', 'abc');// ...и добавляем тег <style> в headconst style = document.createElement('style');style.textContent = '.abc { color: blue; }';document.head.appendChild(style);document.body.appendChild(h1);

Слушаем события click на элементе

const button = document.createElement('button');button.textContent = 'Click Me!';// слушаем событие clickbutton.addEventListener('click', () => {  console.log('Hi!');});document.body.appendChild(button);

На чистом JavaScript нам нужно написать что-то подобное.

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

Синтаксис Svelte

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

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

Итак, вот простейший компонент Svelte:

<h1>Hello World</h1>

Svelte REPL

Для добавления стилей, нужно добавить тег <style>:

<style>  h1 {    color: rebeccapurple;  }</style><h1>Hello World</h1>

Svelte REPL

На этом этапе написание Svelte компонента ощущается аналогично тому как мы пишем обычный HTML, потому что синтаксис Svelte является надмножеством HTML синтаксиса.

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

<script>  let name = 'World';</script><h1>Hello {name}</h1>

Svelte REPL

Мы помещаем переменную JavaScript в фигурные скобки.

Чтобы добавить обработчик клика, мы используем директиву on:

<script>  let count = 0;  function onClickButton(event) {    console.log(count);  }</script><button on:click={onClickButton}>Clicked {count}</button>

Svelte REPL

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

<script>  let count = 0;  function onClickButton(event) {    count += 1;  }</script><button on:click={onClickButton}>Clicked {count}</button>

Svelte REPL

Давайте посмотрим как синтаксис Svelte компилируется в JavaScript, который мы видели ранее.

Компилируем Svelte в уме

Компилятор Svelte анализирует код, который вы пишете и генерирует оптимизированный JavaScript.

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

Первый пример на который мы посмотрим:

<h1>Hello World</h1>

Svelte REPL

Код который получится на выходе:

function create_fragment(ctx) {  let h1;  return {    c() {      h1 = element('h1');      h1.textContent = 'Hello world';    },    m(target, anchor) {      insert(target, h1, anchor);    },    d(detaching) {      if (detaching) detach(h1);    },  };}export default class App extends SvelteComponent {  constructor(options) {    super();    init(this, options, null, create_fragment, safe_not_equal, {});  }}

Мы можем разделить данный код на 2 части:

  • create_fragment

  • class App extends SvelteComponent

create_fragment

Компоненты Svelte - это строительные блоки приложения Svelte. Каждый компонент Svelte фокусируется на построении своей части или фрагменте финального DOM дерева.

Функция create_fragment дает компоненту Svelte руководство по созданию фрагмента DOM дерева.

Посмотрите на возвращаемый объект функции create_fragment. В нем есть такие методы, как:

  • c()

Сокращенно от create. Содержит инструкции по созданию всех элементов во фрагменте.

В этом примере метод содержит инструкции по созданию элемента h1:

h1 = element('h1');h1.textContent = 'Hello World';
  • m(target, anchor)

Сокращенно от mount. Содержит инструкции для вставки элементов в указанную цель.

В этом примере метод содержит инструкции по вставке элемента h1 в target:

insert(target, h1, anchor);// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.tsexport function insert(target, node, anchor) {  target.insertBefore(node, anchor || null);}
  • d(detaching)

Сокращенно от destroy. Содержит инструкции по удалению элементов из указанной цели.

В этом примере мы удаляем элемент h1 из DOM дерева:

detach(h1);// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.tsfunction detach(node) {  node.parentNode.removeChild(node);}

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

export default class App extends SvelteComponent

Каждый компонент - это класс, который вы можете импортировать и создать экземпляр через этот API.

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

Попробуйте удалить тег <h1> и посмотрите, что произойдет с выводом:

<!-- empty -->

Svelte REPL

class App extends SvelteComponent {  constructor(options) {    super();    init(this, options, null, null, safe_not_equal, {});  }}

Svelte передаст null вместо create_fragment!

Функция init - это то место, где Svelte настраивает большинство внутренних частей, таких как:

  • входные параметры компонента, ctx и контекст

  • события жизненного цикла

  • механизм обновления компонента

в самом конце Svelte вызывает create_fragment для создания и монтирования элементов в DOM.

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

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

Добавление данных

Теперь когда мы рассмотрели минимальный Svelte компонент, давайте посмотрим как добавление данных изменит скомпилированный код:

<script>let name = 'World';</script><h1>Hello {name}</h1>

Svelte REPL

Обратите внимание на изменение вывода:

function create_fragment(ctx) {  // ...  return {    c() {      h1 = element('h1');      h1.textContent = `Hello ${name}`;},    // ...  };}let name = 'World';class App extends SvelteComponent {  // ...}

Некоторые наблюдения:

  • то, что мы написали в теге <script>, перемещается на верхний уровень кода

  • текстовое содержимое элемента h1 теперь является шаблонной строкой

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

Обновление данных

Давайте добавим функцию для обновления имени:

<script>let name = 'World';function update() {name = 'Svelte';}</script><h1>Hello {name}</h1>

Svelte REPL

и посмотрим на изменение скомпилированного кода:

function create_fragment(ctx) {  return {    c() {      h1 = element('h1');      t0 = text('Hello ');      t1 = text(/*name*/ ctx[0]);    },    m(target, anchor) {      insert(target, h1, anchor);      append(h1, t0);      append(h1, t1);    },    p(ctx, [dirty]) {      if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);    },    d(detaching) {      if (detaching) detach(h1);    },  };}function instance($$self, $$props, $$invalidate) {  let name = 'World';  function update() {    $$invalidate(0, (name = 'Svelte'));  }  return [name];}export default class App extends SvelteComponent {  constructor(options) {    super();    init(this, options, instance, create_fragment, safe_not_equal, {});  }}

Некоторые наблюдения:

  • текстовое содержимое элемента <h1> теперь разбито на 2 текстовых узла, созданных функцией text (...)

  • объект возвращаемый функцией create_fragment получил новый метод p(ctx, dirty)

  • появилась новая функция instance

  • то что мы написали в теге script было перенесено в функцию instance

  • имя переменной, которое использовалось в create_fragment, теперь заменено на ctx[0]

Почему произошли такие изменения?

Компилятор Svelte отслеживает все переменные, объявленные в теге <script>.

Он отслеживает следующие факторы переменной:

  • может быть изменена? например: count++

  • может быть переназначена? например: name = 'Svelte'

  • на переменную ссылаются в шаблоне? например <h1>Hello {name}</h1>

  • доступна для записи? например const i = 1; или let i = 1;

  • ... и многое другое

Когда компилятор Svelte понимает, что имя переменной можно переназначить (из-за name = 'Svelte'; при обновлении), он разбивает текстовое содержимое h1 на части, чтобы он мог динамически обновлять часть текста.

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

  • p(ctx, dirty)

Сокращенно от u_p_date

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

Функция instance

Компилятор понимает, что имя переменной не может использоваться в разных экземплярах компонента App.

Вот почему он перемещает объявление имени переменной в функцию с именем instance.

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

<App /><App /><App /><!-- выведет --><h1>Hello world</h1><h1>Hello world</h1><h1>Hello world</h1>

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

<App /><App /><App /><!-- может быть --><h1>Hello world</h1><h1>Hello Svelte</h1><h1>Hello world</h1><!-- в зависимости от внутреннего состояния компонента -->

instance($$self, $$props, $$invalidate)

Функция instance возвращает список переменных компонента:

  • на которые ссылаются в шаблоне

  • которые могут быть изменены или переназначены в рамках экземпляра компонента

В Svelte мы называем этот список переменных ctx.

В функции init Svelte вызывает функцию instance для создания ctx и использует его при создания фрагмента для компонента:

// концептуально,const ctx = instance(/*...*/);const fragment = create_fragment(ctx);// создаем фрагментfragment.c();// монтируем фрагмент в DOM деревоfragment.m(target);

Теперь вместо доступа к переменной name вне компонента мы ссылаемся на name, переданную через ctx:

t1 = text(/* name */ ctx[0]);

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

$$invalidate

Секрет системы реактивности в Svelte - кроется в функции $$invalidate.

Для каждой переменной, которая была

  • переназначена или изменена

  • упомянута в шаблоне

будет вставлена функция $$invalidate сразу после присвоения или изменения:

name = 'Svelte';count++;foo.a = 1;// скомпилируется в примерно такой кодname = 'Svelte';$$invalidate(/* name */, name);count++;$$invalidate(/* count */, count);foo.a = 1;$$invalidate(/* foo */, foo);

Функция $$invalidate отмечает переменную как грязную и планирует обновление для компонента:

// концептуально...const ctx = instance(/*...*/);const fragment = create_fragment(ctx);// чтобы отслеживать, какая переменная измениласьconst dirty = new Set();const $$invalidate = (variable, newValue) => {  // обновляем ctx  ctx[variable] = newValue;  // помечаем переменную как грязную  dirty.add(variable);  // планируем обновление для компонента  scheduleUpdate(component);};// вызывается, когда запланировано обновлениеfunction flushUpdate() {  // обновить фрагмент  fragment.p(ctx, dirty);  // очистить список помеченных переменных  dirty.clear();}

Добавляем слушатели событий

Теперь добавим слушателя событий

<script>let name = 'world';function update() {name = 'Svelte';}</script><h1 on:click={update}>Hello {name}</h1>

Svelte REPL

И обратите внимание на разницу:

function create_fragment(ctx) {  // ...  return {    c() {      h1 = element('h1');      t0 = text('Hello ');      t1 = text(/*name*/ ctx[0]);    },    m(target, anchor) {      insert(target, h1, anchor);      append(h1, t0);      append(h1, t1);      dispose = listen(h1, 'click', /*update*/ ctx[1]);},    p(ctx, [dirty]) {      if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);    },    d(detaching) {      if (detaching) detach(h1);      dispose();},  };}function instance($$self, $$props, $$invalidate) {  let name = 'world';  function update() {    $$invalidate(0, (name = 'Svelte'));  }  return [name, update];}// ...

Некоторые наблюдения:

  • функция instance теперь возвращает 2 переменных вместо одной

  • добавлен вызов listen в функции mount и dispose в функции destroy

Как я упоминал ранее, функция instance возвращает переменные, на которые есть ссылка в шаблоне и которые изменены или переназначены.

Поскольку мы только что сослались на функцию update в шаблоне, теперь она возвращается в функции instance как часть ctx.

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

listen и dispose

Каждый раз, когда вы добавляете слушатель событий в шаблоне, Svelte добавляет соответствующий слушатель и удаляет его, когда фрагмент удаляется из DOM.

Попробуем добавить больше слушателей событий,

<h1on:click={update}on:mousedown={update}on:touchstart={update}>  Hello {name}!</h1>

Svelte REPL

и посмотрим на вывод компилятора:

// ...dispose = [  listen(h1, 'click', /*update*/ ctx[1]),  listen(h1, 'mousedown', /*update*/ ctx[1]),  listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true }),];// ...run_all(dispose);

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

// вместо вот такогоdispose1 = listen(h1, 'click', /*update*/ ctx[1]);dispose2 = listen(h1, 'mousedown', /*update*/ ctx[1]);dispose2 = listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true });// ...dispose1();dispose2();dispose3();

Минификация может сжать имя переменной, но скобки убрать нельзя.

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

Итого

Синтаксис Svelte - это надмножество HTML.

Когда вы пишете компонент Svelte, компилятор анализирует ваш код и генерирует оптимизированный JavaScript код.

Который можно разделить на 3 сегмента:

1. create_fragment

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

2. instance

  • Большая часть кода, написанного в теге <script>, находится здесь.

  • Возвращает список переменных экземпляра, на которые есть ссылка в шаблоне.

  • $$invalidate вставляется после каждого присваивания и изменения переменной экземпляра

3. class App extends SvelteComponent

  • Инициализирует компонент с помощью create_fragment и instance

  • Устанавливает внутренние части компонента

  • Предоставляет API компонента

Svelte стремится создать как можно более компактный JavaScript, например:

  • Разбиение текстового содержимого h1 на отдельные текстовые узлы только тогда, когда часть текста может быть обновлена

  • Не определяет create_fragment или instance, когда это не нужно

  • Генерирует dispose как массив или функцию, в зависимости от количества слушателей событий.

  • ...

Заключение

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


Надеюсь данный материал был для вас полезен!

Подробнее..

Перевод Фронтендеры герои. Yehuda Katz объясняет почему

11.01.2021 08:05:18 | Автор: admin

Идея что фронтенд это "для джунов", расстраивает меня тем, что никто не скажет так про другие специализации.

Кто-то может сказать, что неплохо, если б автор компилятора был более "фуллстековым".

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

Это перевод треда Yehuda Katz из твиттера. Под фронтедом здесь подразумеваются именно браузерные приложения на JS (и, отчасти, вся JS-экосистема).

По сути, когда люди говорят фронт для джунов, они делают несколько больших ошибок. Вот две из них:

  1. Они недооценивают сложность работы.

  2. Они переносят мемы про тулинг фронта на самих фронтендеров.

Фронтенд - это сложно в самой своей сути

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

На самом базовом уровне это просто трудно. Эквивалент пользователь закрыл свой ноутбук - это что, если случайные 5% моих SQL-запросов в Rails фейлятся.

Начинающие фронтендеры не думают об этом, но более опытные - точно учитывают такие кейсы.

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

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

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

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

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

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

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

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

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

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

На бэкэнде нет ничего похожего.

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

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

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

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

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

Я видел это во многих, многих компаниях, включая Tilde. Опытные фронтендеры наставляют и направляют новых разработчиков.

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

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

Лично я работаю над ядром Ember, бэкэнд-кодом Rails и бэкэнд-кодом Rust, когда работаю над Skylight.

Я считаю, что это хорошо.

Но никто никогда не скажет: Специализация на Rust-бэкэнде у нас имеет смысл только для джунов. По сути, сказать так про фронт - значит сильно недооценить сложность задач, которые стоят перед фронтендерами.

Итак, это была первая часть.

Мемы про node_modules

Вторая часть - это (откровенно комичное) высмеивание инструментов фронта.

Скажу как человек, который в свое время создал много тулингов (bundler, cargo, инспектор Ember): тулинг фронта часто на голову лучше, чем тулинги, которые кто-то может самодовольно превозносить.

Не только JavaScript может получать пользу от транспайлеров. Разве плохо было бы, если б большинство фич C++ 20 работали с более старыми (и все еще широко использующимися) версиями компиляторов через транспиляцию?

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

И дело не только в том, что вы можете транспилировать код: инструменты вроде eslint, IDE language servers, syntax highlighters и TypeScript обычно поддерживают большую часть самой последней версии ECMAScript задолго до того, как она будет завершена.

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

И всё это довольно легко работает с популярным расширением JS (JSX), которое даже не входит в спецификацию ECMAScript.

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

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

Стандартный способ указать, какие фичи необходимо транслировать - это browserslist, который позволяет вам сказать последняя версия Chrome или браузеры с долей рынка в 1%.

Таким образом, вы просто используете ES2020 и TypeScript, указываете браузеры, которые вам нужно поддерживать (используя гибкое описание, основанное на данных, поддерживаемых caniuse.com), и легко получаете сборку, которая работает в поддерживаемых браузерах.

Если надо поддерживать Node - всё то же самое.

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

Насмехайтесь над npm сколько угодно, но отличия npm от Bundler сильно повлияли на мой дизайн Cargo, а экосистема Rust стала лучше из-за отсутствия ограничения только одна копия вашей зависимости, даже если она используется только внутри пакета.

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

Workspaces из Yarn перенимаются в npm и pnpm, что ставит фронт в высшую лигу языков с хорошей поддержкой монорепозиториев (поддержка не идеальна, но точно в высшей лиге).

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

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

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

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

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

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

По моему опыту, легче полностью погрузиться во фронтенд, нежели во что-то вроде Rails, и при этом всё ещё приносить огромную пользу по мере развития.

Это, кстати, вызвано (отчасти) тем фактом, что фронт достаточно прост для начинающих разработчиков.

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

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

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

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

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

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

Мои друзья-фронтендеры: мне нравится быть с вами в одном сообществе.

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

Мы круты! Мы зажигаем!

Подробнее..

Taiga UI библиотека компонентов под Angular, которую вам стоит попробовать

12.01.2021 14:15:46 | Автор: admin

Привет!

Саша Инкин и я регулярно пишем на Хабр статьи по Angular. Почти все они основаны на нашем опыте разработки большой библиотеки компонентов.

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

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

Как развивался наш UI Kit и как он организован

Если вам интересно узнать, зачем нам понадобилась отдельная библиотека компонентов и как она исторически развивалась, то сначала рекомендую прочитать крутейшую статью Юли Царевой Как организовать работу над библиотекой общих компонентов.

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

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

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

Эта часть обзавелась собственным дизайном и полной независимостью от контекста, а недавно оправдала свое название переездом в опенсорс. Давайте познакомимся с нашим UI Kit поближе!

Полностью модульный

Начнем с того, как организован проект. Taiga UI состоит из нескольких слоев, которые являются отдельными пакетами.

@taiga-ui/cdk

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

Пример сущностей:

  • TuiDestroyService для избавления от постоянного создания сабжектов destroy$ в компонентах.

  • TuiFilterPipe и TuiMapperPipe для обработки значений в шаблонах без лишних вызовов ChangeDetection.

  • Декоратор tuiPure для мемоизации значений геттеров и методов класса.

@taiga-ui/core

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

@taiga-ui/kit

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

@taiga-ui/addon-*

Ряд тематических пакетов, которые основаны на первых трех. Например, есть пакет charts для графиков, commerce для работы с валютами, деньгами и вводом карт или даже отдельный пакет doc для построения собственной витрины аналогично нашей (ссылка на нее будет дальше).

Получается вот такая иерархия, при которой пакеты более высокого уровня строятся на базовых пакетах:

Вопрос: зачем тянуть в зависимости несколько пакетов, если я хочу лишь пару компонентов? Сколько они весят?

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

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

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

Такой подход к работе с Secondary Entry Points дает целый ряд плюсов в организации библиотек:

  • Бандл-приложений меньше, библиотека становится максимально tree shakable.

  • Любые циклические зависимости отлавливаются на этапе сборки.

  • Больше структурности в проекте, нет лишних связей между сущностями.

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

Кастомизируемый

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

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

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

Агностичный

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

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

Тем не менее мы контролируем базовый UX, чтобы вам не приходилось о нем думать: например, при фокусировке инпута с клавиатуры тултип покажет подсказку через секунду автоматически, чтобы screen reader прочитал ее:

Технологичный

Мы очень чтим девелоперские практики и стараемся придерживаться близкого к идеям самого фреймворка Angular-подхода.

Мы не боимся работать с DI, все наши компоненты в OnPush, а на всем проекте включен strict-режим TypeScriptа к типизации мы тоже относимся трепетно. Одним днем вы решите перейти на SSR, и наши компоненты в нем не сломаются.

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

Многообразный

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

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

Как начать использовать

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

taiga-ui.dev

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

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

Хотите узнать больше?

В ближайший четверг, 14 января, мы проведем стрим на нашем новом Twich канале. Начнем в 19-00 по МСК.

Расскажем больше про проект, презентуем его структуру, основные части и особенности. Постараемся ответить на любые ваши вопросы про Taiga UI, Angular или разработку библиотек компонентов.

Не прощаемся

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

Поделитесь мнением о Taiga UI и расскажите, о каких компонентах, инструментах или процессах вам хотелось бы почитать в первую очередь?

Подробнее..

OpenCart popup, модальные окна

13.01.2021 16:08:51 | Автор: admin

Статья нашего сотрудника из его личного блога.

Разрабатывая модуль, в админке мне понадобилось использоватьмодальные окна OpenCartдля вывода определенной информации, а так же для показа формы. Мой опыт frontend на тот момент былтак себе, однако коллега подсказал что в OpenCart используется jquery(2.1.1), а у этой библиотеки есть поддержка popup окон. Но не все так просто

Уточним: модальное окно == всплывающее окно == popup.

Библиотека модальных окон

Кнопка при клике по которой показывается модальное окноКнопка при клике по которой показывается модальное окно

Для того чтобы использоватьмодальное окно OpenCart, надо определитькакая библиотекапредоставляет для этого функционал, используемый в этом движке. Разбирая админку OpenCart 3.0(в 2.3 такой кнопки нет), в разделеПанель состояниябыла найдена кнопка, при клике по которой показалосьвсплывающее окно.

То что нужно, начинаем разбор :)

Модальное окно в админке OpenCart Настройки разработчикаМодальное окно в админке OpenCart Настройки разработчика

Заходим на страницу "Панель состояния", открываем ее исходный код и смотрим в конце скрипт:

$('#button-setting').on('click', function() {    $.ajax({        url: 'index.php?route=common/developer&user_token=D9aTD65JQVdyOY9pcVxcRUx0M3eTefnr',        dataType: 'html',        beforeSend: function() {            $('#button-setting').button('loading');        },        complete: function() {            $('#button-setting').button('reset');        },        success: function(html) {            $('#modal-developer').remove();                         $('body').prepend('<div id="modal-developer" class="modal">' + html + '</div>');                         $('#modal-developer').modal('show');        },        error: function(xhr, ajaxOptions, thrownError) {            alert(thrownError + "\r\n" + xhr.statusText + "\r\n" + xhr.responseText);        }    }); }); 

Как видно, на кнопку с idbutton-settingвешается обработчик на клик, в которомajax запрос, успешный результат которого показывается вpopup окне. А для показа этого окна используется:

$('#modal-developer').modal('show');

Похоже на библиотеку jquerymodal. Однако в bootstrap тоже есть поддержка модальных окон. Пробуем выяснить через отладчик какая библиотека все-таки используется,ставим брекпоинтв вышеприведенном скрипте на методеmodal.

Брекпоинт на методе modalБрекпоинт на методе modal

Нажимаем на кнопку(на первом скрине), попадаем на брекпоинт, шагаем внутрь и попадаем вbootstrap.min.js.

Стек привел в bootstrap.min.jsСтек привел в bootstrap.min.js

Всплывающие окна в OpenCartреализуются через bootstrap.

Использование popup окон в OpenCart

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

  • divс классомmodal-headerв которомh4, который и есть заголовок окна

  • divс классомmodal-bodyC11Cвнутри содержит контент окна

Просмотр html кода модального окна Настройки разработчикаПросмотр html кода модального окна Настройки разработчика

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

<div id="modal-window" class="modal">    <div class="modal-dialog">        <div class="modal-content">                       <!--заголовок и кнопка закрытия-->            <div class="modal-header">                <button type="button" class="close" data-dismiss="modal" aria-hidden="true"></button>                <h4 class="modal-title"></h4>            </div>                         <!--контентная часть окна-->            <div class="modal-body"></div>                     </div>    </div></div>

Для показа окна с idmodal-windowбудем использовать:

$('#modal-window').modal('show');

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

$this->model_extension_event->addEvent('modal_window', 'admin/view/sale/order_form/after', 'extension/module/modal_window/eventSaleOrderFormAfter');

Теперь обработчик:

public function eventSaleOrderFormAfter(&$route, &$args, &$output){    $idOrder = $args["order_id"];     $this->load->model('sale/order');    $this->load->model('catalog/product');     //загрузка списка продуктов заказа    $aOrderProducts = $this->model_sale_order->getOrderProducts($args["order_id"]);         //строка верстки списка товаров    $sOrderProducts = "";     //формируем список товаров    for($i=0; $i<count($aOrderProducts); ++$i)    {        $aProduct = $this->model_catalog_product->getProduct($aOrderProducts[$i]["product_id"]);        $sOrderProducts .= "<li>".$aProduct["name"]." - ".$aProduct["model"]." (".$aOrderProducts[$i]["quantity"]." шт.): ".round($aOrderProducts[$i]["total"], 2)."р. </li>";    }         $sOrderProducts = "<ul>$sOrderProducts</ul>";     //верстка модального окна и скрипт вызова    $sModal = '    <div id="modal_window" class="modal">        <div class="modal-dialog">            <div class="modal-content">                <div class="modal-header">                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true"></button>                    <h4 class="modal-title">Список товаров</h4>                </div>                <div class="modal-body">                    '.$sOrderProducts.'                </div>            </div>        </div>    </div>    <script>$("#modal_window").modal("show");</script>    ';     //находим закрывающий тег body и перед ним вставляем модальное окно и скрипт его показа    $iPos = strripos($output, "</body>");    $output = substr($output, 0, $iPos).$sModal.substr($output, $iPos);}

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

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

Автор: Виталий Бутурлин

Подробнее..

Автоматическая виртуализация рендеринга произвольной вёрстки

14.01.2021 20:16:39 | Автор: admin

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

Это - текстовая расшифровка выступления на HolyJS'20 Moscow. Вы можете либо посмотреть видео запись, либо открыть в интерфейсе проведения презентаций, либо читать как статью Короче, где угодно лучше, чем на Хабре, ибо новый редактор плохо импортирует статьи..

Тяжёлое прошлое: Огромные списки задач

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

Рекламная пауза: Богатый редактор

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

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

Альтернативная линия времени: $mol

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

mol.hyoo.ru

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

Типичный цикл разработки

Типичный цикл разработки выглядит так:

  • Написали код.

  • Проверили в тепличных условиях.

  • Пришли пользователи и всё заспамили.

  • Оптимизировали.

  • Пользователи всё не унимаются.

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

Наивный рендеринг: Скорость загрузки и Отзывчивость

Этот порочный круг приводит к таким вот последствиям:

Сейчас вы видите timeline загрузки и рендера двух с половиной тысяч комментариев. Скрипту требуется 50 секунд на формирование DOM, после чего ещё 5 секунд нужно браузеру чтобы рассчитать стили, лейаут и дерево слоёв.

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

Наивный рендеринг: Потребление памяти

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

На моем телефоне (потряхивает своим тапком) её меньше 3 гигов. И я думаю не надо объяснять откроется ли она у меня вообще. Тут мы плавно переходим к следующему риску..

Наивный рендеринг: Риск неработоспособности

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

  • Не влезли по памяти - приложение закрывается.

  • Обрыв соединения - страница обрывается.

  • Браузер может заглючить на больших объёмах.

Наивный рендеринг: Резюме

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

  • Медленная загрузка.

  • Плохая отзывчивость.

  • Высокое потребление памяти.

  • Риск неработоспособности.

Первый подопытный: Статья на Хабре

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

Вырезаем SSR и ускоряем Хабр в 10 раз

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

https://nin-jin.github.io/habrcomment/#article=423889

Второй подопытный: Ченьжлог на GitLab

На сей раз мы разберём новый кейс - страница коммита на GitLab.

Я просто взял второй попавшийся коммит в первом попавшемся репозитории. Это всего порядка 5 тысяч строк в 100 файлах. Казалось бы - совсем не много. Однако, грузится всё довольно долго. Сначала 10 секунд ждём ответа сервера, потом ещё пол минуты любуемся мёртвыми кусками кода без подсветки синтаксиса. Короче, приходится ждать почти минуту, прежде чем приложением становится возможно пользоваться.

Перенос рендеринга HTML на сервер

Что ж, давайте откроем таймлайн и увидим там следующую картину.

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

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

Страдания Ильи Климова по GitLab-у

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

Год назад Илья Климов как раз рассказывал в своём выступлении страшные истории про работу как раз над этой страницей. Например, прикручивание спинера заняло 3 месяца работы не самого дешёвого разработчика. А сворачивание небольшого файла вешало вкладку на 10 секунд. Но это, правда, уже оптимизировали - теперь на это требуется всего лишь пара секунд!

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

На мой вопрос "что ж вы на $mol всё это не перепишите, чтобы у вас всё летало?" Илья ответил, что не знает как его продать в компании. Что ж, надеюсь дальнейший разбор техник виртуализации поможет ему в этом нелёгком деле.

Оптимизации вёрстки

Первое что приходит на ум - а давайте упростим вёрстку и DOM уменьшится. В качестве примера - кейс от Альфа-банка, который я разбирал в своей статье об истории $mol.

$mol: 4 года спустя

Там был компонент вывода денежных сумм, который рендерился в 8 DOM элементов вместо 3.

<div class="amount">    <h3 class="heading ...">        <span>            <span class="amount__major">1233</span>            <div class="amount__minor-container">                <span class="amount__separator">,</span>                <span class="amount__minor">43</span>            </div>            <span class="amount__currency"></span>        </span>    </h3></div>
<h3 class="amount">    <span class="amount__major">1233</span>    <span class="amount__minor">,43</span></h3>

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

Если так поработать и над остальными компонентами, думаю можно уменьшить размер DOM раза в 2. Однако

Достоинства оптимизации вёрстки

Важно понимать, что асимптотика таким образом не меняется. Допустим страница грузилась 30 секунд. Вы оптимизировали, и она сала грузиться 10 секунд. Но пользователи сгенерируют в 3 раза больше контента и страница снова грузится 30 секунд. А оптимизировать вёрстку уже не куда.

  • Кратное ускорение

  • Асимптотика не меняется

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

Прикладная оптимизация

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

  • Пагинация

  • Экспандеры

  • Бесконечный скролл

  • Виртуальный скролл

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

Первое, что приходит на ум - это пагинация.

Не буду рассказывать что это такое - все и так с ней прекрасно знакомы. Поэтому сразу к достоинствам

Достоинства пагинации

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

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

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

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

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

  • Много кликать

  • Ожидание загрузки каждой страницы

  • Теряется контекст

  • Элементы скачут между страницами

  • Вероятность пропустить элемент

  • Применимо лишь для плоских списков

  • Большой элемент возвращает тормоза

  • Слепые вас ненавидят

  • Работает быстрее, чем всё скопом рендерить

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

Популярные анти паттерны: пагинация

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

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

https://nin-jin.github.io/my_gitlab/#collapse=true

Достоинства экспандеров

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

  • Очень много кликать

  • Ожидание загрузки каждой ветки

  • Если не закрывать, то снова тормоза

  • Слепые вас проклинают

  • Открывается быстро

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

Прикладная оптимизация: Бесконечный скролл

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

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

Достоинства бесконечного скролла

  • Применимо лишь для плоских списков

  • Ожидание загрузки каждой ветки

  • Увеличение тормозов по мере прокрутки

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

Прикладная оптимизация: Виртуальный скролл

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

https://bvaughn.github.io/react-virtualized/#/components/WindowScroller

Достоинства виртуального скролла

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

  • Применимо лишь для плоских списков

  • Размеры элементов должны быть предсказуемы

  • Работает быстро

Прикладная оптимизация: Резюме

  • Ухудшение пользовательского опыта

  • Не решают проблему полностью

  • Ограниченная применимость

  • Полный контроль где какую применять

  • Нужно не забыть

  • Нужно продавить

  • Нужно реализовать

  • Нужно оттестировать

  • Нужно поддерживать

Оптимизация инструментов

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

  • Тайм слайсинг

  • Прогрессивный рендеринг

  • Ленивый рендеринг

  • Виртуальный рендеринг

Оптимизация инструментов: Тайм слайсинг

Можно попробовать так называемый time slicing. На одной из прошлых Holy JS я рассказывал про технику квантования вычислений, где долгое вычисление разбивается на кванты по 16 миллисекунд, и между этими квантами браузер может обрабатывать какие-то события делать плавную анимацию и так далее. То есть вы не блокируете поток на долгое время, получая хорошую отзывчивость..

Квантовая механика вычислений в JS

Достоинства тайм слайсинга

Звучит вроде бы не плохо. Но тут есть такая проблема, как замедление общего времени работы всего вычисления. Например, если просто взять и выполнить вычисление без квантования занимает полсекунды, то если его постоянно прерывать каждые 16 мс, позволяя браузеру делать свои дела, то до завершения может пройти и пара секунд. Для пользователя это может выглядеть как замедление работы. Ну и другой аспект заключается в том что javascript не поддерживает файберы, то есть такие потоки исполнения, которые можно останавливать и возобновлять в любое время. Их приходится эмулировать тем или иным способом, а это всегда костыли, замедление работы и некоторые ограничения на то, как пишется код. В общем, с этим подходом всё не очень хорошо, поэтому в $mol мы и отказались от квантования.

  • Хорошая отзывчивость

  • Замедленность работы

  • Эмуляция файберов в JS

Оптимизация инструментов: Прогрессивный рендеринг

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

catberry.github.io

Достоинства прогрессивного рендеринга

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

  • Хорошая отзывчивость в процессе появления

  • Эмуляция файберов в JS

  • На больших объёмах всё встаёт колом

Оптимизация инструментов: Ленивый рендеринг

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

https://nin-jin.github.io/my_gitlab/#lazy=true

Достоинства ленивого рендеринга

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

  • Размеры элементов должны быть предсказуемы

  • Увеличение тормозов по мере прокрутки

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

Оптимизация инструментов: Виртуальный рендеринг

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

https://nin-jin.github.io/my_gitlab/

Достоинства виртуального рендеринга

  • Размеры элементов должны быть предсказуемы

  • Работает, наконец, быстро

Оптимизация инструментов: Резюме

На уровне инструментов поддержка сейчас есть лишь в полутора фреймворках: time slicing в React, прогрессивный рендер в catberry и виртуальный рендер в $mol. Зато, такую оптимизацию инструмента достатоxно реализовать один раз? и далее наслаждаться ею во всех своих приложениях не тратя дополнительное время на оптимизацию.

  • Поддерживает полтора фреймворка

  • Работает само по себе

Оптимизации: Резюме

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

Оптимизация

Стоит того?

Вёрстка

Прикладной код

Инструментарий

Виртуализация браузера

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

content-visibility: auto;contain-intrinsic-size: 1000px;

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

Логика рендеринга

Для каждого компонента нам нужно получить следующие данные.

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

Далее мы пройдёмся по этим шагам подробнее.

Оценка размеров

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

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

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

  • Последняя

  • Усреднённая

  • Минимальная

Типы компонент: Атомарный

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

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

Типы компонент: Стек наложения

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

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

Типы компонент: Вертикальный список

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

Типы компонент: Горизонтальный список

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

Типы компонент: Горизонтальный список с переносами

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

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

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

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

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

Типы компонент: Грид и Буклет

Такие компоненты, как гриды и буклеты, - это просто композиция упомянутых ранее.

Грид - это вертикальный список из горизонтальных списков. А буклет - это горизонтальный список из вертикальных списков.

Типы компонент: Резюме

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

  • Атомарный

  • Стек наложения

  • Вертикальный список

  • Горизонтальный список

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

Отслеживание положения: onScroll

Теперь перейдем к вопросу о том, когда производить обновление DOM. Самое очевидное - это реагировать на событие scroll..

document.addEventListener( 'scroll', event => {    // few times per frame}, { capture: true } )

Достоинства отслеживания onScroll

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

  • Слишком часто

  • Изменения DOM

  • Изменения стилей

  • Изменения состояния элементов

  • Изменения состояния браузера

Отслеживание положения: IntersectionObserver

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

const observer = new IntersectionObserver(    event => {        // calls on change of visibility percentage        // doesn't call when visibility percentage doesn't changed    },    { root: document.body  })observer.observe( watched_element )

Достоинства отслеживания IntersectionObserver

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

  • Облом, если степень наложения не меняется

Отслеживание положения: requestAnimationFrame

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

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

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

function tick() {    requestAnimationFrame( tick )    for( const element of watched_elements ) {        element.getBoundingClientRect()    }    render()   }

Достоинства отслеживания requestAnimationFrame

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

  • Постоянная фоновая нагрузка

  • Просто и надёжно

Обновление: Резюме

  • onScroll

  • IntersectionObserver

  • requestAnimationFrame

Скачки при прокрутке

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

https://nin-jin.github.io/my_gitlab/#anchoring=false

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

Привязка скролла: Предотвращает скачки

Есть классический пример демонстрирующий проблему..

https://codepen.io/chriscoyier/embed/oWgENp?theme-id=dark&default-tab=result

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

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

Привязка скролла: Выбор точки привязки

Чтобы выбрать элемент для привязки скролла, браузер идёт по DOM от корня в поисках первого же элемента, попадающего в видимую область. Заходит в него и снова ищет первый попавшийся. И так далее. При этом совершенно не важно как элементы друг относительно друга расположены визуально. Важно лишь расположение их в доме и попадают ли они в видимую область. Разберём этот процесс на простом примере..

Элемент 1 не видим, поэтому пропускаем. 2 видим, так что заходим в него. Тут и 2.2, и 2.3 видимы, поэтому заходим в первый. Далее ближайший видимый 2.2.2, внутри которых видимых больше нет, так что именно этот элемент становится якорем для привязки скролла. Браузер запоминает смещение верхней границы якорного элемента относительно вьюпорта и старается его сохранить постоянным.

Привязка скролла: Подавление привязки

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

  • overflow-anchor: none

  • top, left, right, bottom

  • margin, padding

  • Any width or height-related properties

  • transform

Виртуализация: Распорки

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

Виртуализация: Прокрутка вниз

Рассмотрим несколько сценариев работы виртуализации с привязкой скролла. Для примера, возьмём компонент вертикального списка. Изначально видны элементы 3, 4 и 5, а сверху и снизу - распорки.

Зелёная фаза - пользователь скроллит вниз. Как видно третий блок вышел из видимой области, а снизу образовалась дырка. Но пользователь это ещ1 не видит.

Синяя фаза - срабатывает обработчик requestAnimationFrame и мы обновляем DOM: удаляем третий узел и добавляем шестой. Как видно, контент уехал вверх относительно видимой области, но пользователь это ещё не видит.

Красная фаза - мы отдаём управление браузеру, а тот выбирает 4 элемент в качестве якоря и отскролливает чуть-назад. Так что пользователь видит лишь добавившийся снизу шестой элемент.

Виртуализация: Прокрутка вверх

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

Виртуализация: Расширение

Может так оказаться, что контент не достигает краёв видимой области с обоих концов.

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

Виртуализация: Превышение

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

Виртуализация: Скачок кенгуру

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

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

Привязка скролла в действии

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

https://nin-jin.github.io/my_gitlab/

Привязка скролла: Поддержка

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

Браузер

overflow-anchor

Chrome

Firefox

Safari

Привязка скролла: Запасный выход

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

const anchoring_support = CSS.supports( 'overflow-anchor:auto' )if( anchoring_support ) {    virtual render} else {    lazy render}

Проблема: Долгая раскладка

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

https://nin-jin.github.io/habrcomment/#article=423889

Минимизация расчётов лейаута

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

[mol_scroll] {    contain: content;}

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

Прокрутка в отдельном потоке

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

[mol_scroll] > * {    transform: translateZ(0);}

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

Плавная прокрутка (или нет)

Применив все оптимизации мы получаем плавную прокрутку..

https://nin-jin.github.io/habrcomment/#article=423889

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

Логика поиска

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

*find_path(    check: ( view : View )=> boolean,    path = [] as View[],): Generator&lt; View[] > {    path = [ ... path, this ]    if( check( view ) ) return yield path    for( const item of this.kids ) {        yield* item.find_path( check, path )    }}
  • Рекурсивно спускаемся по компонентам.

  • Отбираем соответствующие запросу.

  • Рисуем интерфейс перехода между найденным.

Логика прокрутки к компоненту

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

scroll_to_view( view: View ) {    const path = this.find_path( v => v === view ).next().value    this.force_render( new Set( path ) )    view.dom_node.scrollIntoView()}

Логика форсирование рендеринга видимого

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

force_render( path : Set< View > ): number {    const items = this.rows    const index = items.findIndex( item => path.has( item ) )    if( index >= 0 ) {        this.visible_range = [ index, index + 1 ]        items[ index ].force_render( path )    }    return index}

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

Работающий поиск

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

https://nin-jin.github.io/habrcomment/#article=423889/search=vin

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

Доступность

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

https://nin-jin.github.io/my_gitlab/

Можете сами попробовать, просто включив NVDA и закрыв глаза.

Решаемые проблемы виртуализации

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

  • Оценка будущих размеров.

  • Скачки контента.

  • Тормоза при скроллинге.

  • Прокрутка к элементу.

  • Поиск по странице.

  • Доступность.

Фундаментальные особенности

Тем не менее у нас остался и ряд фундаментальных особенностей, с которыми придётся смириться..

  • Скачки скроллбара при неточной оценке размеров.

  • Scroll Anchoring может не работать в некоторых контекстах.

  • Копирование выделенного текста не работает.

Бенчмарки: Скорость открытия и Отзывчивость

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

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

Итого: ускорение открытия не менее чем в 4 раза даже на сравнительно небольшом объёме данных.

Бенчмарки: Отзывчивость

Можем погонять и какие-нибудь синтетические бенчмарки. Например, dbmon.

https://mol.js.org/perf/dbmon/-/

Пока все реализации топчутся у меня вкруг 20 фпс, наивная реализация на $mol со встроенной виртуализацией показывает стабильные 60.

Бенчмарки: Потребление памяти

Нельзя забывать и про потребление оперативной памяти. Та реализация Хабра на VueJS на 170 комментариях отжирает 40 мегабайт хипа JS. Но если посмотреть понять вкладки, то это будет уже в 3 раза больше, так как самому браузеру нужно весь этот дом показывать. Если же открыть реализацию на $mol, где выводится статья, да ещё и две с половиной тысячи комментариев к ней, то мы получаем те же 40 мегабайт JS хипа. Но вкладка при этом кушает в два раза меньше, ибо браузеру показывать всего ничего - меньше тысячи DOM элементов.

Вариант

Память JS

Память вкладки

VueJS: 170 комментариев

40 MB

150 MB

$mol: статья + 2500 комментариев

40 MB

90 MB

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

Бенчмарки: Гулять так гулять!

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

https://showcase.hyoo.ru/

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

ООП против ФП

Короче, виртуализация - классная тема. И давайте посмотрим какие у нас есть перспективы, что она появится в ныне популярных инструментах. Но сперва определимся с вопросом чем объект отличается от функции..

  • Объект: одно состояние - много действий.

  • Функция: много состояний - одно действие.

Условный Angular использует концепцию объектов: каждый компонент - объект умеющий много разных действий. А вот в React популярна тема функциональных компонент - тут компонент имеет лишь одно действие - отрендерить своё содержимое в виртуальное дерево.

Ортогональные действия

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

  • Узнать минимальные размеры без полного рендера.

  • Частично отрендерить содержимое.

  • Проверить соответствие поисковому запросу.

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

Композиция против вёрстки

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

Column    Row        Search        Icon    Scroll        Column            Task            Task            Task

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

<main class="panel">    <div class="header">        <input class="search">        <img src="..." class="icon">    </div>    <div class="scroll">        <div class="card">        <div class="card">        <div class="card">    </div></main>

Перспективы во фреймворках

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

Чуть ближе к виртуализации React Native, где нет никакого сырого HTML и всё строится из компонент. Но подражание html тут как собаке пятая нога.

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

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

Инструмент

ООП

КОП

React

React Native

Vue

Angular

Svelte

$mol

Выбери виртуализацию

Но я не предлагаю вам использовать $mol. Потому что, ну знаете, "использовать" - это какое-то потребительское отношение. Я вам предлагаю присоединиться к его разработке и тем самым получить максимальный профит.

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

Ссылочки

Обратная связь

Превосходно: 34%

  • Вроде все знакомо, но нашёл для себя пару интересных нюансов.

  • Узнал кое что новое.

  • Все ок.

  • По моему мощнейший доклад, очень круто и интересно. Один момент. У Дмитрия сквозь доклад использовалась шутка: недостатки преподносились как преимущества (в духе контент прыгает - пользователь тренируется концентрировать внимание). Она удачная и смешная (правда так считаю, не из вежливости это пишу), но ее было слишком много. Например, был слайд, где было сразу много "преимуществ", и Дмитрий проговаривал по шутке на каждый пункт. Ломало темп. Сначала весело, а потом уже как-то хочется чтобы доклад дальше двигался. Но это минорное замечание, доклад ?

  • Изначально я слышал про $mol из комментариев на Хабре, многие из которых были неуместны / подавались в странной манере. В докладе я увидел, что автор - "видел некоторое дерьмо" (простите за мем) - понимает причины - предлагает здравые рассуждения по тому, как это чинить. Совершенно точно посмотрю на эту библиотеку, чтобы увидеть воплощение принципов из доклада на практике. Но даже если не буду использовать эту библиотеку в проде, разочарован не буду - настолько ценной и качественной я считаю информацию в этом докладе.

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

  • На удивление было мало $mol и много полезных вещей)

Хорошо: 42%

  • Интересный доклад, но к сожалению не все было понятно, начиналось вроде все просто, но потом быстро вышло за моё понимание).

  • Карловский как всегда жжёт.

  • Очень круто и глубоко, но не очень понравилась подача. Хочется побольше "огонька".

Нормально: 18%

  • Слишком узкая специализация

  • Что смотрел помню, а про что нет.

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

Плохо: 6%

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

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

Подробнее..

Приглашаем на DINS JS EVENING разбираем Chrome DevTools и Cypress

15.01.2021 16:07:39 | Автор: admin

На митапе Андрей Соколов из DINS расскажет, как разработчикам облегчить отладку с помощью Chrome DevTools. Константин Поздникин из Usetech покажет, как Cypress помогает бороться с багами в проектах со сложной бизнес-логикой. Во время прямого эфира вы сможете задать вопросы спикерам. Участие бесплатное, но нужно зарегистрироваться. Подробная программа и информация о спикерах под катом.


Программа

19:00-19:40 Инструменты отладки в Chrome DevTools (Андрей Соколов, DINS)

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

Вместе со спикером рассмотрим главные вкладки для отладки фронтенда: elements, console, source и network. Андрей расскажет о кнопках и функциях, которые вы видели, но не использовали.

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

Андрей Соколов Frontend Developer в DINS. Помимо работы увлекается написанием странных Telegram-ботов.

19:40-20:30 Cypress как ядерное оружие для багов на проекте со сложной бизнес-логикой (Константин Поздникин, Usetech)

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

В такой ситуации важно использовать интеграционное тестирование, тестирование API и unit-тесты для frontend, backend и их связки в окружении. Константин расскажет, как с этой задачей поможет справиться Cypress настоящее ядерное оружие в войне против багов. Вы узнаете, как с помощью Cypress заменить Selenium, экономить время и проводить комплексное эффективное тестирование.

Доклад будет интересен специалистам уровня Middle и выше.

Константин Поздникин ведущий разработчик компании Usetech. Преподаватель и ментор в Корпоративном университете компании.

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

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

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

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

О нас

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

Подробнее..

Конструктор плейлистов для Spotify

17.01.2021 16:22:22 | Автор: admin

Статья о том, как Spotify Web API (SWA) и платформа Google Apps Script (GAS) позволили превратить библиотеку в гибкий конструктор плейлистов с бесплатным исполнением по расписанию.

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

Ссылки на исходный код и документацию в конце статьи.

Проблема

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

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

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

Решение

В результате была разработана библиотека для GAS на JavaScript - Goofy. Конструктором её делает возможность запуска на платформе в редакторе или по расписанию (триггерам) без стороннего ПО.

Естественно GAS накладывает свои ограничения, но другого порядка. Например, время исполнения не более 6 минут; количество запросов к внешним API не более 20 тысяч в день. С другой стороны, круг возможностей существенно расширился. Реализованы все функции Smarter Playlists, устранены его недостатки. Появились новые функции.

Алгоритм

Несколько простых примеров, которые можно реализовать с помощью Goofy

  • Объединение личных миксов с удалением ранее прослушанных треков

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

  • Сбор новинок отслеживаемых исполнителей

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

  • Рекомендации Last.fm любого пользователя, топ за период и прочее

Новые возможности

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

  • SWA дает возможность получить только 50 последних прослушанных треков. Появление 51-ого делает недоступным 1-й.

    GAS имеет выход к Google Drive. Библиотека создает повторяющийся триггер и опрашивает SWA на предмет новых прослушиваний. Если они есть, сохраняет в файл. Таким образом, появляется локальная полная история прослушиваний.

  • Есть другая проблема. Трек считается прослушанным только после 30 секунд. Причем зачастую недавние треки обновляются не сразу или вовсе теряются. Списываю это на "качество" SWA.

    Решение есть и здесь - использовать Last.fm. Реализовано два подхода. Во первых, можно просто запросить недавние прослушивания и удалять совпадения по названию трека. Во вторых, можно выполнить поиск трека в Spotify и сохранить его в файл на Google Drive. Тем самым получая историю прослушиваний от Last.fm готовую к работе со Spotify.

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

Пример

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

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

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

function createHelloPlaylist() {    let tracks = Source.getSavedTracks();    Selector.keepRandom(tracks, 5);    Playlist.saveWithReplace({        name: 'Hello, playlist',        tracks: tracks,    });}

2) Выявить любимые треки, которые не были прослушаны более чем месяц.

function templateSavedAndForgot(){    let recentTracks = Source.getRecentTracks(2500);    let savedTracks = Source.getSavedTracks();     Filter.removeTracks(savedTracks, recentTracks);    let startDate = new Date('2006-01-01');    let endDate = Filter.getDateRel(30, 'endDay');    Filter.rangeDateAbs(savedTracks, startDate, endDate);    Selector.keepRandom(savedTracks, 20);        Order.sort(savedTracks, 'meta.added_at', 'asc');    Playlist.saveWithReplace({        name: 'Любимо и забыто',        tracks: savedTracks,        randomCover: 'update', // установить случайную обложку плейлиста    });}

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

function createNewRelease() {    const playlistId = 'abc';    let tracks = Source.getArtistsTracks({        artist: {           followed_include: true         },        album: {           groups: 'album,single',           release_date: { sinceDays: 7, beforeDays: 0 }         },    });    Order.shuffle(tracks);    Combiner.push(tracks, Source.getPlaylistTracks('name', playlistId));    Filter.removeTracks(tracks, RecentTracks.get(3000));    Filter.matchOriginalOnly(tracks);        Playlist.saveWithReplace({        id: playlistId,        name: 'Новые релизы',        tracks: tracks,        randomCover: 'update',    });}

Ссылки

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

Исходный код на GitHub, включая форум.

Подробнее..
Категории: Javascript , Api , Музыка , Spotify , Google apps script , Last.fm , Gas

Категории

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

© 2006-2021, personeltest.ru