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

Node.js

Перевод Использование ECMAScript-модулей в Node.js

23.05.2021 14:09:40 | Автор: admin
ECMAScript-модули (кратко их называют ES-модулями) это модули, формат которых описан в стандарте ECMAScript, при работе с которыми используются инструкции import и export:

// ECMAScript-модуль// инструкция importimport myFunc from './my-func';//инструкция exportexport myOtherFunc(param) {const result = myFunc(param);// ....return otherResult;}

В Node.js, начиная с версии 13.2.0, имеется стабильная поддержка ES-модулей.



Этот материал посвящён особенностям работы с ES-модулями в Node.js.

1. Условия, необходимые для работы с ES-модулями в Node.js


На платформе Node.js по умолчанию используются модули формата CommonJS. Для того чтобы платформа смогла бы использовать ES-модули, нужно кое-что сделать.

А именно, Node.js сможет пользоваться ES-модулями в следующих случаях:

  • Если файл модуля имеет расширение .mjs.
  • Или если в package.json ближайшей родительской папки модуля имеется конструкция { type: module }.
  • Или если при запуске Node.js используется флаг --input-type=module, и при этом код модуля передаётся платформе в виде строки с использованием аргумента --eval="<module-code>", или поступает из STDIN.

Рассмотрим первые два способа работы с модулями (применение .mjs-файлов и использование { type: module } в package.json).

1.1. Расширение файлов .mjs


Легче всего сообщить Node.js о том, что некий файл надо воспринимать как ES-модуль, можно, дав этому файлу расширение .mjs.

Код ES-модуля, приведённый ниже, хранится в файле month-from-date.mjs (обратите внимание на расширение .mjs). Модуль экспортирует функцию monthFromDate(), которая определяет название месяца по произвольной дате, переданной ей.

// month-from-date.mjs (ES-модуль)const MONTHS = ['January', 'February', 'March','April', 'May', 'June','July', 'August', 'September', 'October', 'November', 'December'];export function monthFromDate(date) {if (!(date instanceof Date)) {date = new Date(date);}return MONTHS[date.getMonth()];}

Другой ES-модуль, month.mjs, похожим образом использует инструкцию import для импорта функции monthFromDate() из модуля month-from-date.mjs. Этот модуль, кроме того, поддерживает приём аргументов из командной строки, выводя название месяца после обработки переданной ему даты:

// month.mjs (ES-модуль)import { monthFromDate } from './month-from-date.mjs';const dateString = process.argv[2] ?? null;console.log(monthFromDate(dateString));

Это всё что нужно для того чтобы пользоваться ES-модулями в Node.js!

Попробуем запустить month.mjs в командной строке:

node ./month.mjs "2022-02-01"

В ответ будет выведено название месяца February.

Поэкспериментировать с этим скриптом можно здесь.

1.2. Использование { type: module } в package.json


По умолчанию Node.js воспринимает файлы с расширением .js как CommonJS-модули. Для того чтобы такие файлы выглядели бы для Node.js как ES-модули, нужно просто записать в поле type файла package.json значение module:

{"name": "my-app","version": "1.0.0","type": "module",// ...}

Теперь все .js-файлы в папке, содержащей такой package.json, будут восприниматься как ES-модули.

Переработаем наш пример. А именно переименуем month-from-date.mjs в month-from-date.js, а month.mjs в month.js (не трогая инструкции import и export). Затем, в файл package.json, который находится в той же папке, что и эти файлы, внесём запись type: module. После этого Node.js будет воспринимать наши .js-файлы как ES-модули.

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

node ./month.js "2022-03-01"

Система выдаст March.

Вот ссылка на страницу с интерактивным примером.

2. Импорт ES-модулей


Спецификатор это строковой литерал, представляющий путь к тому месту, откуда нужно импортировать модуль.

В следующем примере кода спецификатором является строка path:

// 'path' - это спецификаторimport module from 'path';

В Node.js существует три вида спецификаторов: относительные, простые и абсолютные

2.1. Относительные спецификаторы


Импорт модуля с использованием относительного спецификатора приведёт к разрешению пути к импортируемому модулю относительно расположения текущего (импортирующего) модуля. Относительные спецификаторы обычно начинаются с символов '.', '..' или './':

// Относительные спецификаторы:import module1 from './module1.js';import module2 from '../folder/module2.mjs';

При использовании относительных спецификаторов нужно обязательно указывать расширение файла (.js, .mjs и так далее).

2.2. Простые спецификаторы


Простой спецификатор начинается с имени модуля (то есть у него в начале нет символов '.', './', '..', '/') и используется для импорта встроенных модулей Node.js или модулей из папки node_modules.

Например, если в node_modules установлен пакет lodash-es, то импортировать его можно, воспользовавшись простым спецификатором:

// Простые спецификаторы:import lodash from 'lodash-es';import intersection from 'lodash-es/intersection';

Простые спецификаторы применяются и при импорте встроенных модулей Node.js:

import fs from 'fs';

2.3. Абсолютные спецификаторы


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

// Абсолютный спецификатор:import module from 'file:///usr/opt/module.js';

Обратите внимание на то, что в абсолютном спецификаторе присутствует префикс file://.

3. Динамический импорт модулей


Стандартный механизм импорта ES-модулей всегда выполняет код модуля, упомянутого в команде вида import module from 'path', и импортирует этот модуль. Делается это вне зависимости от того, используется ли в коде этот модуль или нет.

Иногда бывает так, что нужно импортировать модуль динамически. В таких случаях можно воспользоваться асинхронной функцией вида import('./path-to-module'):

async function loadModule() {const {default: defaultComponent,component1} = await import('./path-to-module');// ...}loadModule();

Команда import('./path-to-module') асинхронно загружает модуль и возвращает промис, результатом успешного разрешения которого являются компоненты импортированного модуля. Свойство default представляет собой результаты импорта, выполняемого по умолчанию, а именованные импорты оказываются в свойствах с соответствующими именами.

Например, давайте сделаем так, чтобы модуль month-from-date.js загружался бы в скрипте month.js только в том случае, если пользователь, при запуске скрипта, передал ему дату:

// month.js (ES-модуль)const dateString = process.argv[2] ?? null;if (dateString === null) {console.log('Please indicate date argument');} else {(async function() {const { monthFromDate } = await import('./month-from-date.js');console.log(monthFromDate(dateString));})();}

Команда const { monthFromDate } = await import('./month-from-date.mjs') выполняет динамическую загрузку модуля и присваивает результат именованного экспорта константе с тем же именем, которое имеет экспортированная функция.

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

node ./month.js "2022-04-01"

Скрипт выведет April.

Поэкспериментировать с кодом можно здесь.

4. Совместное использование модулей разных форматов


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

К счастью, Node.js позволяет, используя механизмы импорта по умолчанию, включать в состав ES-модулей CommonJS-модули:

// ES-модульimport defaultComponent from './module.commonjs.js';// используется `defaultComponent`...

При импорте CommonJS-модуля в ES-модуле то, что экспортировано в CommonJS-модуле с использованием команды module.exports, превращается в импорт по умолчанию. Правда, надо отметить, что именованные импорты из CommonJS-модулей не поддерживаются.

Однако, функция require(), используемая для импорта CommonJS-модулей, не умеет импортировать ES-модули. Вместо неё в CommonJS-модулях можно, для импорта ES-модулей, использовать асинхронную функцию import():

// CommonJS-модульasync function loadESModule() {const {default: defaultComponent,component1} = await import('./module.es.mjs');// ...}loadESModule();

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

5. ES-модули и окружение Node.js


В области видимости ES-модуля недоступны сущности, специфичные для CommonJS-модулей. Среди них можно отметить следующие:

  • require()
  • exports
  • module.exports
  • __dirname
  • __filename

Но для определения абсолютного пути к текущему модулю можно пользоваться свойством import.meta.url:

// ES-модуль, путь к которому выглядит как "/usr/opt/module.mjs"console.log(import.meta.url); // "file:///usr/opt/module.mjs"

6. Итоги


Node.js позволяет работать с ES-модулями при условии, что расширением файла модуля является .mjs, или в том случае, если в ближайшей родительской папке модуля имеется файл package.json, содержащий конструкцию { type: module }. Если эти условия соблюдены это значит, что у нас имеются следующие возможности по импорту модулей:

  • Можно воспользоваться относительным путём к модулю: import module from './module.js'.
  • Можно применить абсолютный путь к модулю: import module from 'file:///abs/path/module.js'.
  • Можно импортировать модули, которые имеются в папке node_modules: import lodash from 'lodash-es'.
  • Можно импортировать встроенные модули Node.js: import fs from 'fs'.
  • Модули можно импортировать и динамически, пользуясь конструкцией вида import('./path-to-module').

Хотя делать этого и не рекомендуется, но, если нужно, CommonJS-модули можно импортировать в ES-модули, пользуясь выражением вида import defaultImport from './common.js'. При этом то, что было экспортировано из CommonJS-модуля с использованием команды module.exports, превращается в ES-модуле в импорт по умолчанию.

Планируете ли вы полностью перейти на ES-модули в своих Node.js-проектах?


Подробнее..

Перевод Управление зависимостями в Node.js

04.06.2021 16:20:58 | Автор: admin
Управление зависимостями это часть повседневной работы Node.js-программиста. Сегодня мы поговорим о разных подходах к работе с зависимостями в Node.js, и о том, как система загружает и обрабатывает зависимости.

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



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

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

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

Я исхожу из предположения о том, что вы уже владеете основами Node.js. Если нужно можете, прежде чем продолжать читать этот материал, посмотреть мою статью, посвящённую основам Node.js.

Подготовка приложения и эксперименты по экспорту и импорту


Начнём с простых вещей. Я создал директорию для проекта и, используя команду npm init, инициализировал проект. Затем я создал два JavaScript-файла: app.js и appMsgs.js. Ниже показан внешний вид структуры проекта в VS Code. Этот проект мы будем использовать в роли отправной точки наших экспериментов. Вы можете, прорабатывая этот материал, делать всё сами, а можете упростить себе работу, воспользовавшись готовым кодом. Его можно найти в репозитории, ссылку на который я приведу в конце статьи.

Структура базового проекта

В данный момент оба .js-файла пусты. Внесём в файл appMsgs.js следующий код:


Экспорт значений простых типов и объектов в appMsgs.js

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

В app.js воспользоваться тем, что экспортировано из appMsgs.js, можно, прибегнув к команде require:


Импорт модуля appMsgs.js в app.js

Система, выполнив команду require, вернёт объект, который будет представлять обособленный фрагмент кода, описанный в файле appMsgs.js. Мы назначаем этот объект переменной appMsgs, а затем просто пользуемся свойствами этого объекта в вызовах console.log. Ниже показан результат выполнения кода app.js.


Выполнение app.js

Команда require выполняет код файла appMsgs.js и конструирует объект, дающий нам доступ к функционалу, экспортируемому файлом appMsgs.js.

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

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

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

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


Экспорт функции из appMsgs.js

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

Попробуем воспользоваться этой функцией в app.js, приведя код этого файла к следующему виду:


Использование импортированной функции в app.js

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

Вот результат запуска этого кода:


Выполнение app.js

Мы рассмотрели два подхода к использованию module.exports. Ещё одним способом применения module.exports является экспорт функций-конструкторов, используемых, с ключевым словом new, для создания объектов. Рассмотрим пример:


Экспорт функции-конструктора из appMsgs.js

А вот обновлённый код app.js, в котором используется импортированная функция-конструктор:

Использование в app.js функции-конструктора, импортированной из appMsgs.js

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

Вот что получится, если выполнить новый вариант app.js:


Выполнение app.js

Я добавил в проект файл userRepo.js и внёс в него следующий код:


Файл userRepo.js

Вот файл app.js, в котором используется то, что экспортировано из userRepo.js:


Использование в app.js того, что экспортировано из userRepo.js

Запустим app.js:


Выполнение app.js

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

Импорт директорий


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

var appMsgs = require("./appMsgs")

Node.js, выполняя эту команду, будет искать файл appMsgs.js, но систему будет интересовать и директория appMsgs. То, что она найдёт первым, она и импортирует.

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

Я создал папку logger, а в ней файл index.js. В этот файл я поместил следующий код:


Код файла index.js из папки logger

А вот файл app.js, в котором команда require используется для импорта этого модуля:


Файл app.js, в котором require передаётся не имя файла, а имя папки

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

var logger = require("./logger/index.js")

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

var logger = require("./logger")

Так как система не может обнаружить файл logger.js, она ищет соответствующую папку. По умолчанию импортируется файл index.js, являющейся точкой входа в модуль. Именно поэтому я и дал .js-файлу, находящемуся в папке, имя index.js.

Попробуем теперь выполнить код app.js:


Выполнение app.js

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

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

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

Npm


Мы кратко поговорим и о ещё одном аспекте работы с зависимостями в Node.js. Это npm (Node Package Manager, менеджер пакетов Node.js). Вы, вероятно, уже знакомы с npm. Если кратко описать его суть, то окажется, что он даёт разработчикам простой механизм для включения в их проекты необходимого им функционала, оформленного в виде npm-пакетов.

Установить нужную зависимость с помощью npm (в данном случае библиотеку underscore) можно так:

npm install underscore

Потом эту библиотеку можно подключить в коде с помощью require:


Импорт библиотеки, установленной с помощью npm

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


Пример использования underscore в app.js

Выполним этот код.


Выполнение app.js

Итоги


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

Применяете ли вы модульный подход при работе над своими Node.js-проектами?


Подробнее..

Меняем промежуточное представление кода на лету в Ghidra

30.04.2021 14:04:59 | Автор: admin

Когда мы разрабатывали модуль ghidra nodejs для инструмента Ghidra, мы поняли, что не всегда получается корректно реализовать опкод V8 (движка JavaScript, используемого Node.js) на языке описания ассемблерных инструкций SLEIGH. В таких средах исполнения, как V8, JVM и прочие, один опкод может выполнять достаточно сложные действия. Для решения этой проблемы в Ghidra предусмотрен механизм динамической инъекции конструкций P-code языка промежуточного представления Ghidra. Используя этот механизм, нам удалось превратить вывод декомпилятора из такого:

В такой:

Рассмотрим пример с опкодом CallRuntime. Он вызывает одну функцию из списка т.н. Runtime-функций V8 по индексу (kRuntimeId). Также данная инструкция имеет переменное число аргументов (range номер начального регистра-аргумента, rangedst число аргументов). Описание инструкции на языке SLEIGH, который Ghidra использует для определения ассемблерных инструкций, выглядит так:

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

  1. Поиск нужного названия функции в массиве Runtime-функций по индексу kRuntimeId.

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

  3. Передача в функцию переменного количества аргументов.

  4. Вызов функции и сохранение результата вызова в аккумулятор.

  5. Восстановление предыдущего состояния регистров.

Если вы знаете, как сделать такое на SLEIGH, пожалуйста, напишите комментарий. А мы решили, что все это (а особенно работу с переменным количеством аргументов-регистров) не очень удобно (если возможно) реализовывать на языке описания процессорных инструкций, и применили механизм динамических инъекций p-code, который как раз для таких случаев реализовали разработчики Ghidra. Что это за механизм?

Можно создать в файле описания ассемблерных инструкций (slaspec) специальную пользовательскую операцию, например CallRuntimeCallOther. Далее, изменив конфигурацию вашего модуля (подробнее об этом ниже), вы можете сделать так, чтобы при нахождении в коде данной инструкции Ghidra передавала бы обработку в Java динамически, и уже на языке Java написать обработчик, который будет динамически формировать p-code для инструкции, пользуясь всей гибкостью Java.

Рассмотрим подробно, как это сделать.

Создание служебной операции SLEIGH

Опишем опкод CallRuntime следующим образом. Подробнее об описании процессорных инструкций на языке SLEIGH все можете узнать из статьи Создаем процессорный модуль под Ghidra на примере байткода v8.

Определим служебную операцию:

define pcodeop CallRuntimeCallOther;

И опишем саму инструкцию:

:CallRuntime [kRuntimeId], range^rangedst is op = 0x53; kRuntimeId; range;       rangedst {CallRuntimeCallOther(2, 0);}

Таким образом, любой опкод, начинающийся с байта 0x53, будет расшифрован как CallRuntime При попытке его декомпиляции будет вызываться обработчик операции CallRuntimeCallOtherс аргументами 2 и 0. Эти аргументы описывают тип инструкции (CallRuntime) и позволят нам написать один обработчик для нескольких похожих инструкций (CallWithSpread, CallUndefinedReceiverи т.п.).

Подготовительная работа

Добавим класс, через который будет проходить инъекция кода: V8_PcodeInjectLibrary. Этот класс мы унаследуем от ghidra.program.model.lang.PcodeInjectLibrary который реализует большую часть необходимых для инъекции p-code методов.

Начнем написание класса V8_PcodeInjectLibraryс такого шаблона:

package v8_bytecode;import public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {public V8_PcodeInjectLibrary(SleighLanguage l) {}}

V8_PcodeInjectLibraryбудет использоваться не пользовательским кодом, а движком Ghidra, поэтому нам необходимо задать значение параметра pcodeInjectLibraryClassв файле pspec, чтобы движок Ghidra знал, какой класс задействовать для инъекции p-code.

<?xml version="1.0" encoding="UTF-8"?><processor_spec>  <programcounter register="pc"/>  <properties>  <property key="pcodeInjectLibraryClass" value="v8_bytecode.V8_PcodeInjectLibrary"/>  </properties></processor_spec>

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

<callotherfixup targetop="CallRuntimeCallOther"><pcode dynamic="true"><input name=outsize"/> </pcode></callotherfixup>

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

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

public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {private Set<String> implementedOps;private SleighLanguage language;public V8_PcodeInjectLibrary(SleighLanguage l) {super(l);language = l;String translateSpec = language.buildTranslatorTag(language.getAddressFactory(),getUniqueBase(), language.getSymbolTable());PcodeParser parser = null;try {parser = new PcodeParser(translateSpec);}catch (JDOMException e1) {e1.printStackTrace();}implementedOps = new HashSet<>();implementedOps.add("CallRuntimeCallOther");}}

Благодаря внесенным нами изменениям Ghidra будет вызывать метод getPayloadнашего класса V8_PcodeInjectLibraryкаждый раз при попытке декомпиляции инструкции CallRuntimeCallOther Создадим данный метод, который при наличии инструкции в списке реализованных операций будет создавать объект класса V8_InjectCallVariadic(этот класс мы реализуем чуть позже) и возвращать его.

@Override/*** This method is called by DecompileCallback.getPcodeInject.*/public InjectPayload getPayload(int type, String name, Program program, String context) {if (type == InjectPayload.CALLMECHANISM_TYPE) {return null;}if (!implementedOps.contains(name)) {return super.getPayload(type, name, program, context);}V8_InjectPayload payload = null; switch (name) {case ("CallRuntimeCallOther"):payload = new V8_InjectCallVariadic("", language, 0);break;default:return super.getPayload(type, name, program, context);}return payload;}

Генерация p-code

Основная работа по динамическому созданию p-code будет происходить в классе V8_InjectCallVariadic. Давайте его создадим и опишем типы операций.

package v8_bytecode;import public class V8_InjectCallVariadic extends V8_InjectPayload {public V8_InjectCallVariadic(String sourceName, SleighLanguage language, long uniqBase) {super(sourceName, language, uniqBase);}// Типы операций. В данном примере мы рассматриваем RUNTIMETYPEint INTRINSICTYPE = 1;int RUNTIMETYPE = 2;int PROPERTYTYPE = 3;@Overridepublic PcodeOp[] getPcode(Program program, InjectContext context) {}@Overridepublic String getName() {return "InjectCallVariadic";}}

Как нетрудно догадаться, нам необходимо разработать нашу реализацию метода getPcode Для начала создадим объект pCode класса V8_PcodeOpEmitter Этот класс будет помогать нам создавать инструкции pCode (позже мы ознакомимся с ним подробнее).

V8_PcodeOpEmitter pCode = new V8_PcodeOpEmitter(language, context.baseAddr, uniqueBase); 

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

Address opAddr = context.baseAddr;

С помощью данного адреса мы получим объект текущей инструкции:

Instruction instruction = program.getListing().getInstructionAt(opAddr);

Также с помощью аргумента contextмы получим значения аргументов, которые ранее описывали на языке SLEIGH.

Integer funcType = (int) context.inputlist.get(0).getOffset();Integer receiver = (int) context.inputlist.get(1).getOffset();

Реализуем обработку инструкции и генерации Pcode.

// проверка типа инструкцииif (funcType != PROPERTYTYPE) {// получаем kRuntimeId  индекс вызываемой функцииInteger index = (int) instruction.getScalar(0).getValue();// сгенерируем Pcode для вызова инструкции cpool с помощью объекта pCode класса V8_PcodeOpEmitter. Подробнее остановимся на нем ниже.pCode.emitAssignVarnodeFromPcodeOpCall("call_target", 4, "cpool", "0", "0x" + opAddr.toString(), index.toString(), funcType.toString());}// получаем аргумент диапазон регистровObject[] tOpObjects = instruction.getOpObjects(2);// get caller args count to save only necessary onesObject[] opObjects;Register recvOp = null;if (receiver == 1) {}else {opObjects = new Object[tOpObjects.length];System.arraycopy(tOpObjects, 0, opObjects, 0, tOpObjects.length);}// получаем количество аргументов вызываемой функцииtry {callerParamsCount = program.getListing().getFunctionContaining(opAddr).getParameterCount();}catch(Exception e) {callerParamsCount = 0;}// сохраняем старые значения регистров вида aN на стеке. Это необходимо для того, чтобы Ghidra лучше распознавала количество аргументов вызываемой функцииInteger callerArgIndex = 0;for (; callerArgIndex < callerParamsCount; callerArgIndex++) {pCode.emitPushCat1Value("a" + callerArgIndex);}// сохраняем аргументы вызываемой функции в регистры вида aNInteger argIndex = opObjects.length;for (Object o: opObjects) {argIndex--;Register currentOp = (Register)o;pCode.emitAssignVarnodeFromVarnode("a" + argIndex, currentOp.toString(), 4);}// вызов функцииpCode.emitVarnodeCall("call_target", 4);// восстанавливаем старые значения регистров со стекаwhile (callerArgIndex > 0) {callerArgIndex--;pCode.emitPopCat1Value("a" + callerArgIndex);}// возвращаем массив P-Code операцийreturn pCode.getPcodeOps();

Теперь рассмотрим логику работы класса V8_PcodeOpEmitter (https://github.com/PositiveTechnologies/ghidra_nodejs/blob/main/src/main/java/v8_bytecode/V8_PcodeOpEmitter.java), который во многом основан на аналогичном классе модуля для JVM. Данный класс генерирует p-code операции с помощью ряда методов. Рассмотрим их в порядке обращения к ним в нашем коде.

emitAssignVarnodeFromPcodeOpCall(String varnodeName, int size, String pcodeop, String... args)

Для понимания работы данного метода сначала рассмотрим понятие Varnodeодин из основных элементов p-code, по сути представляющий собой любую переменную, задействованную в p-code. Регистры, локальные переменные всё это Varnode.

Вернемся к методу. Данный метод генерирует p-code для вызова функции pcodeopс аргументами argsи сохраняет результат работы функции в varnodeName То есть в итоге получается такая конструкция:

varnodeName = pcodeop(args[0], args[1], );

emitPushCat1Value(String valueName) и emitPopCat1Value (String valueName)

Генерирует p-code для аналогов ассемблерных операций push и pop соответственно с Varnode valueName.

emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size)

Генерирует p-code для операции присвоения значения varnodeOutName = varnodeInName

emitVarnodeCall (String target, int size)

Генерирует P-Code для вызова функции target.

Заключение

Благодаря вышеизложенному механизму у нас получилось значительно улучшить вывод декомплилятора Ghidra. В итоге динамическая генерация p-code стала еще одним кирпичиком в нашем большом инструменте модуле для анализа скомпилированного bytenode скриптов Node.JS. Исходный код модуля доступен в нашем репозитории на github.com. Пользуйтесь, и удачного вам реверс-инжиниринга!

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

Большое спасибо за исследование особенностей Node.js и разработку модуля моим коллегам: Владимиру Кононовичу, Наталье Тляповой, Сергею Федонину.

Подробнее..

Поиск коллизий в SHA-256 на платформе Node.js при помощи Bitcoin Hasher

09.06.2021 14:20:05 | Автор: admin

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

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

Для понимания работы приложения Bitcoin Hasher содержание статьи было поделено на небольшие разделы:

  1. Немного теории

  2. Немного о SHA-2

  3. Немного о Blockchain

  4. Bitcoin Hasher

  5. Полезные материалы

1. Немного теории

Алгоритмом шифрования называется некая функция принимающая на вход строку произвольной длины, которая посредством определённого математического алгоритма преобразует INPUT (строку произвольной длины) в OUTPUT (строку фиксированной длины). Данный процесс конвертации называется хешированием или шифрованием. Конечным результатом шифрования любой подающей на вход информации называется цифровым отпечатком (дайджестом). Цель любого алгоритма заключается в сжатии, рассеивании и перемешивании входной информации. Оттого насколько сильно она преобразована будет зависеть не только безопасность этого самого отпечатка, но и алгоритма хеширования посредством которого был сформирован отпечаток. Наглядный процесс конвертации показан на схеме:

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

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

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

  3. Отсутствие какой-либо зависимости между входной и выходной информацией

  4. Сложность или невозможность подбора входного значения для цифрового отпечатка

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

2. Немного о SHA-2

На момент написания этой статьи одним из наиболее эффективных алгоритмов хеширования является семейство криптографических систем защиты информации SHA-2 (Secure Hash Algorithm Version 2 - безопасный алгоритм хеширования, версия 2).

Все функции, которые входят в данное "семейство", а именно: SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/256 и SHA-512/224 построены на основе структуры Меркла-Дамгарда, что сказывается на их реальной стойкости к различным видам атак. Принцип работы абсолютно всех вышеприведённых алгоритмов заключается в разбивке входящей информации на части одинакового размера, каждая из которых подвергается обработке выбранной односторонней функцией сжатия. Ключевым преимуществом при таком подходе является алгоритмическая односторонность, то бишь невозможность восстановления каких-либо исходных данных на основе полученного выходного результата без наличия сформированного ключа. Данное элегантное решение было представлено взамен устаревшему SHA-1 Агентством Национальной Безопасности США в 2002 году для более надёжного шифрования конфиденциальных данных. Одним из наиболее применимых на сегодняшний день алгоритмов является SHA-256, свою популярность по внедрению его в различные системы он завоевал благодаря таким масштабным проектам как: Bitcoin и Blockchain (о Blockchain далее остановимся чуть подробнее). Все представленные функции благополучно работают и применяются по сегодняшний день.

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

SHA-224:       Hello World ! --> 2c8abaa6a94a76fe9c6005994567d67a1631bc90dfca267099dc750fSHA-256:       Hello World ! --> 07f2bdef34ed16e3a1ba0dbb7e47b8fd981ce0ccb3e1bfe564d82c423cba7e47SHA-384:       Hello World ! --> 67e60f9ce837caa3ca82550f0dfcbde1b8b8a7c1605fa8d115bcc2314204fd95f5f607306622c38c0205de7df6d426d8SHA-512:       Hello World ! --> feab0028f1142d420a1425d1dd5b518225b4523aa1cff63385ece3411318819f5ec83042ccb79d81f20e4a243866886ca3ae3026153acff8e126c0e89631502eSHA-512/256:   Hello World ! --> a70e1d1268e729e90db4c0834214f449c8e7b652777f40a8a0d26f2372e39ca7SHA-512/224:   Hello World ! --> 7cc0d174b7ce522eff7d7ee59789e420d75d0244f006ef8ce0f4efb7

3. Немного о Blockchain

Для понимания работы приложения я обязан написать пару слов о Blockchain сети так как именно она базируется на работе с алгоритмом SHA-256 а, также является важным "поставщиком входной информации" для Bitcoin Hasher.

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

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

  1. (1991 год): Размышления о Blockchain, как о защищённом хранилище цифровых документов без возможности их подделки или возврата были описаны в работах Стюарта Хаббера и У. Скотта Шторнетта в 1991 году. Столь гениальная и стойкая идея была сформулирована задолго до появления Blockchain-сети.

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

  3. (2008 год): Некий человек или организация под псевдонимом Сатоши Накамото публикует документ под названием: "Bitcoin: a peer-to-peer electronic cash system". Данный документ впоследствии станет отправной точкой создания нынешнего Blockchain для валюты Bitcoin.

  4. (2009 год): В альтернативу нынешней финансовой системе Сатоши Накамото реализовывает децентрализованную, не подконтрольную не одной государственной единице, сеть Blockchain для работы с первой в мире цифровую валюту Bitcoin.

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

Вся информация о переводах в Blockchain хранится в виде блоков, каждый из которых представляет собой объект и имеет следующий вид (Пример блока под номером 685466, созданный 2021-05-30 08:05):

{      "hash": "00000000000000000009fde417c010d7ec9ffb25a268f4b0667681ed9b74cf65",       (уникальный идентификатор созданного блока)      "ver": 536870916,                                                                 (версия блока)      "prev_block": "00000000000000000007b7241ee4748769266870bdab4e5306379739db07c466"  (уникальный идентификатор предыдущего блока),      "mrkl_root": "8d620000ab7ba942a165ed49be563a31c33269ce8f2d40b8317784475a543fe7"   (хеш всех транзакций в текущем блоке),      "time": 1622351111                                                                (время за которое был создан текущий блок),      "bits": 386752379                                                                 (суб-единица BTC),      "nonce": 3069945434                                                               (случайное значение которое можно скоректировать для подтверждения работы),      "n_tx": 996                                                                       (колличество подтвержденных транзакций в текущем блоке),      "size": 1602081,                                                                  (размер текущего блока)      "block_index": 685466                                                             (индекс текущего блока),      "height": 685466                                                                  (высота текущего блока),      "tx": [         "--Array of Transactions--"                                                    (Массив транзакций содержащих информацию)      ]   }

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

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

4. Bitcoin Hasher

Bitcoin Hasher представляет собой небольшое приложение для поиска коллизий в алгоритме шифрования SHA-256.

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

Алгоритм работы сводился к следующему:

  1. На клиенте JavaScript делал новый XHR-запрос к Blockchain API следующего вида: https://blockchain.info/rawblock/ (уникальный идентификатор блока вводимый в input приложения).

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

  3. Из поля "tx" приложение "забирало" массив дайджестов подтвержденных транзакций в конкретном блоке и на их основе Node.js генерировал точно такой же цифровой отпечаток каждой из транзакций.

  4. Параллельно работы генерации из поля "prev_block" (в которое входит значение идентификатора предыдущего блока) на клиенте JavaScript создавал новый XHR-запрос следующего вида: https://blockchain.info/rawblock/ (уникальный идентификатор предыдущего блока). Данный процесс был зациклен до тех пор пока все блоки и транзакции не будут обработаны.

  5. При параллельной работе клиент-серверного приложения все INPUT-OUTPUT данные записываются в папку db_blocks/block-NUMBER_BLOCK.txt

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

Полезные материалы для ознакомления с приложением:

Repository Bitcoin Hasher

Пример формирования "двойного шифрования", для блока с высотой 665862 в Blockchain

Процесс работы Bitcoin Hasher:

5. Полезные материалы

  1. Алферов А. П., Зубов А.Ю., Кузьмин А.С., Черемушкин А. В. Основы криптографии. М.: Гелиос АРВ, 2001. 479 с.

  2. Децентрализованные приложения. Технология Blockchain в действии. С. Равала

  3. Практическая криптография. Нильс Фергюсон и Брюс Шнайер

Подробнее..

Как я сделал свою сборку Gulp для быстрой, лёгкой и приятной вёрстки

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

Серьёзно и профессионально я начал заниматься вёрсткой в 2019 году, хотя до этого ещё со школы интересовался данной темой как любитель. Поэтому новичком мне себя назвать сложно, но и профессионалом с опытом 5+ лет я тоже не являюсь. Тем не менее, я успел познакомиться со сборщиком Gulp, его плагинами и сделал для себя хорошую, как по мне, сборку для работы. О её возможностях сегодня и расскажу.

ВАЖНО! В этой статье речь пойдёт о самой последней версии сборки. Если вы пользуетесь версиями сборки, вышедшими до публикации этой статьи, информация будет для вас не релевантна, но полезна.

Какие задачи решает эта сборка?

  • вёрстка компонентами (вам не нужно в каждую страницу копировать head, header, footer и другие повторяющиеся элементы, вплоть до кнопок или кастомных чекбоксов);

  • вёрстка с препроцессорами (SASS/SCSS);

  • конвертация шрифтов из ttf в eot, woff, woff2;

  • лёгкое (почти автоматическое) подключение шрифтов;

  • лёгкое (почти автоматическое) создание псевдоэлементов-иконок;

  • обработка изображений "на лету";

  • минификация html/css/js файлов;

  • возможность вёрстки с использованием php;

  • выгрузка файлов на хостинг по FTP;

  • несколько мелких задач с помощью миксинов.

Для тех, кому лень читать и делать всё руками - сразу ссылка на сборку.

Собственно создание сборки

Начнём собирать нашу сборку (простите за тавтологию). Предварительно нам потребуется уже установленная на компьютере LTS-версия Node.js и NPM (входит в пакет Node.js) либо Yarn. Для нашей задачи не имеет значения, какой из этих пакетных менеджеров использовать, однако я буду объяснять на примере NPM, соответственно, для Yarn вам потребуется нагуглить аналоги NPM-команд.

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

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

Далее будет намного удобнее работать через Visual Studio Code (поскольку у него есть встроенный терминал) или любой другой удобный вам редактор + терминал.

Прежде всего, нам нужно установить сам Gulp. Делается это двумя командами npm i gulp -global - устанавливаем Gulp глобально на систему и npm i gulp --save-dev - устанавливаем Gulp локально в проект. Ключ --save здесь отвечает за сохранение версии плагина при дальнейшей установке (без него вам может установить более новую, несовместимую с другими плагинами версию), а ключ -dev указывает на то, что этот пакет необходим только во время разработки проекта, а не во время его выполнения. Например, если мы устанавливаем в проект пакет Swiper, который содержит скрипты слайдера и будет отображаться на странице, мы будем устанавливать его без ключа -dev, поскольку он нужен для выполнения, а не для разработки.

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

После этого нам нужно подключить Gulp в нашем файле, для того чтобы он исполнялся. Это делается с помощью require:

const gulp = require('gulp');

Далее, для каждой задачи будем использовать модули в отдельных файлах. Для того, чтобы не подключать каждый модуль отдельно, нужно установить и подключить плагин require-dir. Устанавливается он всё той же командой (как и все последующие плагины, поэтому далее повторяться не буду, просто знайте, что установить - это npm i $PLUGIN-NAME$ --save-dev). После установки подключаем его и прописываем путь к директории, в которую будем складывать модули (у меня это директория tasks):

const gulp = require('gulp');const requireDir = require('require-dir');const tasks = requireDir('./tasks');

Первая задача

Давайте проверим, всё ли мы правильно сделали. Создадим в директории tasks файл модуля с именем hello.js. В созданном файле напишем простейшую функцию, которая будет выводить в консоль строку "Hello Gulp!" (можете придумать что-то менее банальное, если хотите).

module.exports = function hello () {console.log("Hello Gulp!");}

Теперь вернёмся в gulpfile.js и зададим там задачу hello:

const gulp = require('gulp');const requireDir = require('require-dir');const tasks = requireDir('./tasks');exports.hello = tasks.hello;

Теперь командой gulp hello в терминале запустим нашу задачу. Если всё сделано правильно - в терминал должно вывестись приблизительно такое сообщение:

[13:17:15] Using gulpfile D:\Web projects\Easy-webdev-startpack-new\gulpfile.js[13:17:15] Starting 'hello'...Hello Gulp![13:17:15] The following tasks did not complete: hello[13:17:15] Did you forget to signal async completion?

Так же, можно получить список всех заданных задач командой gulp --tasks.

Файловая структура

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

В директории src/ нам понадобятся следующие поддиректории:
  • components/ - директория для компонентов

  • components/bem-blocks/ - директория для БЭМ-блоков

  • components/page-blocks/ - директория для типовых блоков страницы, таких как хедер, футер и т.п.

  • fonts/ - директория для шрифтов

  • img/ - директория для изображений

  • js/ - директория для файлов JavaScript

  • scss/ - директория для файлов стилей

  • scss/base/ - директория для базовых стилей, которые мы изменять не будем

  • svg/ - директория для файлов SVG

  • svg/css/ - директория для SVG-файлов, которые будут интегрироваться в CSS

Получиться в итоге должно приблизительно следующее:

 project/  build/ 
Подробнее..

Yarn 2 Устанавливаем и разбираемся

28.04.2021 22:20:56 | Автор: admin

Знакомство

Yarn 2 (Berry) это новый выпуск революционного и хорошо зарекомендовавшего себя менеджера пакетов Yarn, включающий в себя такие особенности, как: PlugnPlay, возможность расширения модульного API, оффлайн-кэш и улучшенную поддержку рабочих пространств.

PlugnPlay

Yarn PnP это новая функция, которая по умолчанию включена в Yarn 2. PnP избавляет проекты от папки node_modules в пользу файла.pnp.js.

Файл.pnp.js сопоставляет все пакеты, установленные в проекте, с тем местом, где Yarn разместил их на вашем диске. Это избавляет от большого количества операций ввода-вывода при генерации node_modules, обеспечивая более быструю и надёжную установку.

В новой документации Yarn подробно рассказываетсяо недостатках node_modules,как структуры папок, и объясняется, почему необходим новый взгляд на управление зависимостями.

Монорепозитории

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

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

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

Модульная архитектура, плагины

Сделав важный шаг вперед, Yarn 2 был переработан в пользу нового модульного API, расширяемого при помощью плагинов. В настоящее время большинство функций уже реализовано с их помощью дажеyarn addиyarn installявляются предустановленными плагинами!

Вы можете сами написать плагин для Yarn, а чтобы дать вам представление об этом процессе, разработчики Yarn создалиплагин TypeScript, который будет автоматически добавлять соответствующие@types/packagesкаждый раз, когда вы запускаетеyarn add.

Как начать работу?

Установка

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

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

 npm install -g yarn

Выполнив данную инструкцию (запускyarn --versionдолжен вывести что-то вроде1.22.x), перейдём к созданию каталога для запуска нового проекта:

 mkdir my-app cd my-app

Berry кодовое имя релизной ветки Yarn 2.
Изменим версию Yarn конкретно для каталогаmy-app:

 yarn set version berry

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

Добавление зависимостей

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

  • yarn init инициализация проекта

  • yarn add <package> [--dev] добавление пакета

  • yarn remove <package> удаление пакета

  • yarn up <package> обновление пакета

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

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

  • почти все сообщения имеют собственные коды ошибок, которые можно найти вдокументации;

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

Установка React.js с Yarn-плагином TypeScript

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

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

Инициализируем package.json и установим плагин TypeScript:

 yarn init yarn plugin import typescript

Проведем установку библиотеки React:

 yarn add react react-dom YN0000:  Resolution step YN0000:  Completed in 1s 932ms YN0000:  Fetch step YN0013:  loose-envify@npm:1.4.0 YN0013:  object-assign@npm:4.1.1 YN0013:  react-dom@npm:17.0.2 YN0013:  react@npm:17.0.2 YN0013:  scheduler@npm:0.20.2 YN0000:  Completed in 0s 502ms YN0000:  Link step YN0000:  Completed YN0000: Done in 2s 503ms

Зависимости @types/ были успешно установлены!

package.jsonpackage.json

Что в итоге

Ветка Yarn 1.x (Classic) уже официально перешла в статус поддержки, предполагающей только исправление уязвимостей.

Все новые функции будут разрабатываться исключительно для Yarn 2, версия которого будет распространяться черезyarn set version.

Если Yarn не подружится с вашей IDE, нужно будет кое-что установить. Не скучайте!

Подробнее..
Категории: Javascript , Typescript , Node.js , Yarn , Package manager

Запускаемприложение наExpress.js вYandexCloudFunctions

01.05.2021 10:11:55 | Автор: admin

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

Еслиидтипо пути упрощенияадминистрирования, возникает желаниезагрузитьприложение вYandexCloudFunctionsи вызывать его из облака.Ксожалению,пока нельзяпросто так взять и запуститьв облакеприложение, написанное на любом популярномnode.js-фреймворке.Фреймворкипишут ответвсокетHTTP(S).Рантаймфункций ожидает получить от пользовательского кода функции объект определенного содержания.

{         "statusCode": <HTTP код ответа>,    "headers": <словарь со строковыми значениями HTTP-заголовков>,    "multiValueHeaders": <словарь со списками значений HTTP-заголовков>,    "body": "<содержимое ответа>",    "isBase64Encoded": <true или false> }

Из коробкиэто работать не будет, но можно научить приложение возвращать ответ в ожидаемом формате.Разберем,как это сделать,на примереприложенияExpress.jsс двумяэндпоинтами.

Создаем и запускаем новый проект

Создаемновую директорию и инициируем в ней новый проект:

mkdir sample-app && cd sample-appnpm init -ynpm install expresstouch index.js

Далее вindex.jsдобавляем следующий код:

const express = require('express');const app = express();app.use(express.urlencoded({ extended: true }));app.use(express.json());app.get('/api/info', (req, res) => {    res.send({ application: 'sample-app', version: '1.0' });});app.post('/api/v1/getback', (req, res) => {    res.send({ ...req.body });});app.listen(3000, () => console.log(`Listening on: 3000`)); 

Запускаем проект ипроверяем, чтоприходятожидаемые ответы:

$ curl 'http://localhost:3000/api/info'{"application":"sample-app","version":"1"}

АдаптируемпроектподServerless

Интегрируеммодуль serverless-http:

npm i --save serverless-http

Это универсальныйвраппер, онподдерживает не толькоExpress, но иConnect,Koa,restana, а также экспериментально другиефреймворки:Sails,Hapi,Fastify,Restify,PolkaиLoopBack.

Затеммодифицируем наш пример.Заменяемзапуск сервера напорте3000экспортом функции-обработчика, которая будет вызыватьсяserverless-рантаймомоблака:

const express = require('express');const app = express();const serverless = require('serverless-http');app.use(express.urlencoded({ extended: true }));app.use(express.json());app.get('/api/info', (req, res) => {    res.send({ application: 'sample-app', version: '1.0' });});app.post('/api/v1/getback', (req, res) => {    res.send({ ...req.body });});//app.listen(3000, () => console.log(`Listening on: 3000`)); module.exports.handler = serverless(app);

Теперь наше приложение готово к запуску воблаке.

Развертываем приложение в облаке

Для того чтобы развернуть код в облаке,проще всего воспользоваться утилитойserverless. УYandex.Cloudестьсвойплагин,который позволяетдеплоитьфункции.Из него пока нельзя развернуть еще один ключевой компонент системы YandexAPIGateway,мычуть позже сделаем это вручнуючерез консоль.

УстанавливаемServerlessFrameworkи плагин к нему:

npm i -g serverless serverless-yandex-cloud 

Далее создаем в проектефайлserverless.yamlс содержимым:

service: sample-appframeworkVersion: ">=1.1.0"configValidationMode: offprovider:  name: yandex-cloud  runtime: nodejs12-previewplugins:  - serverless-yandex-cloudpackage:  exclude:    - ./**  include:    - ./package.json    - ./**/*.jsfunctions:  express:    # this is formatted as <FILENAME>.<HANDLER>    handler: index.handler    memory: 128    timeout: 5

Деплоимфункцию командой:

serverlessdeploy

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

$ curl 'https://functions.yandexcloud.net/%function-id%/api/info'{"errorCode":400,"errorMessage":"Invalid functionID: /%function-id%/api/info","errorType":"ProxyIntegrationError"}

необходима настройка APIGateway.

Создание APIGateway

Спецификация должна соответствоватьстандартуOpenAPI3.0, для нашего простого APIееможнонаписатьруками:

openapi: 3.0.0info:  title: Sample API  version: 1.0.0paths:  /api/info:    get:      responses:        '200':          description: Ok      x-yc-apigateway-integration:        type: cloud_functions        function_id: %function_id%        tag: $latest        service_account_id: %service_account_id%  /api/v1/getback:    post:      responses:        '200':          description: Ok          content:            application/json:              schema:                $ref: '#/components/schemas/Test'      requestBody:        required: false        content:          application/json:            schema:              $ref: '#/components/schemas/Test'      x-yc-apigateway-integration:        type: cloud_functions        function_id: %function_id%        tag: $latest        service_account_id: %service_account_id%components:  schemas:    Test:      type: object

Не забудьтепоменять%function_id%и%service_account_id%на ваши значения. У сервисного аккаунта должна быть рольserverless.functions.invokerиливыше, если вы оставили функцию без публичного доступа.

Вболее сложныхслучаяхможно попробовать сгенерироватьспецификациюOpenAPIна основе уже имеющегося кода API. Для этогоподойдетexpress-oas-generator.

Теперь наше приложение работает идоступно по URL.

$ curl 'https://%api-gw-id%.apigw.yandexcloud.net/api/info'{"application":"sample-app","version":"1"}

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

Новый параметрAPIGateway

Совсем недавно вAPIGatewayпоявилась возможность указать параметр вида{param+}.Вэтом случае будутматчитьсяи вложенные пути.

paths:  /api/{proxy+}:    get:      x-yc-apigateway-integration:        type: cloud_functions        function_id: d4e***        tag: $latest        service_account_id: aje***      responses:        200:          description: Ok      parameters:        - explode: true          in: path          name: proxy          required: true          schema:            type: string          style: simple

Впервом параметре функцииeventвпропертиpathбудет лежатьзначениевида/api/%7Bproxy+%7Dи роутерExpress.jsбудет ломаться.

Решения как минимум два:

  • написать честныйproviderдляYandex.Cloudпо образу того,что сейчас есть дляAWS;

  • пропатчитьобъектevent, положив вpathзначение изurl(строки1319в примере ниже).

Пример готового скриптаможноскачать.

const express = require('express');const serverless = require('serverless-http');const app = express();app.use(express.urlencoded({ extended: true }));app.use(express.json());app.get('/api/info', (req, res) => {    res.send({ application: 'sample-app', version: '1.0' });});app.get('/api/pet/:name?', (req, res) => {    res.send({ ...req.params });});module.exports.handler = (event, context) => {    const patchedEvent = {        ...event,        path: event.url,        originalPath: event.path,    }    return serverless(app)(patchedEvent, context);}

Вы можете бесплатнопопробовать запустить приложений Express.js на YandexCloudFunctionsпо программеfreetier:сервис не тарифицируетпервыймиллионвызовов функций и первые 10ГБчасвыполнения функций.А любые вопросыоработесервисов можно обсудить как с их пользователями, так ис ихсоздателямивчате Yandex Serverless Ecosystem.

Подробнее..
Категории: Javascript , Node.js , Serverless

Оплата в телеграм боте Платежи 2.0 Сбербанк Telegraf Node.js

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

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



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


Платежи 2.0


Платежи 2.0


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


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


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


Платежи 2.0


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


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


Создаём бота в Telegram


Бот в Telegram создается при помощи другого бота под названием @BotFather. Отправляем ему команду /newbot, выбираем имя, которое будет отображаться в списке контактов, и адрес. Например, Оплата в Telegram боте с адресом sber_pay_test_bot.


newbot


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


ВНИМАНИЕ! Его нужно сохранить и никому не показывать.


Создаем проект Node.js


Далее создадим новый проект. Создаем папку.


Вводим в консоле:


mkdir sber_pay_test_bot && cd sber_pay_test_bot

Затем:


npm init 

Программа задаёт вам разные вопросы и создает package.json, который определяет настройки проекта, зависимости, скрипты, название и прочее. Для примера можно везде нажать enter


и добавим файл index.js в котором будет разрабатываться наш бот.


touch index.js    

Telegraf.js


Cтавим telegraf.js это один из популярных фреймворков для создания телеграм бота.


npm install telegraf@3.38 

Ставим библиотеку dotenv это модуль, который загружает переменные среды из файла .env в process.env., а также заодно поставим nodemon инструмент, который помогает разрабатывать приложения на основе node.js путем автоматического перезапуска приложения node при обнаружении изменений файлов в каталоге.


npm install dotenv nodemon

Добавляем скрипт запуска в package.json


"scripts": {    "start": "nodemon index"}

Из документации telegraf.js, копируем в наш проект первоначальную настройку бота.


const { Telegraf } = require('telegraf')require('dotenv').config()const bot = new Telegraf(process.env.BOT_TOKEN) //сюда помещается токен, который дал botFatherbot.start((ctx) => ctx.reply('Welcome')) //ответ бота на команду /startbot.help((ctx) => ctx.reply('Send me a sticker')) //ответ бота на команду /helpbot.on('sticker', (ctx) => ctx.reply('')) //bot.on это обработчик введенного юзером сообщения, в данном случае он отслеживает стикер, можно использовать обработчик текста или голосового сообщенияbot.hears('hi', (ctx) => ctx.reply('Hey there')) // bot.hears это обработчик конкретного текста, данном случае это - "hi"bot.launch() // запуск бота

Создаем файл .env куда в переменную BOT_TOKEN кладем токен, который ранее нам выдал @BotFather


BOT_TOKEN='сюда'

Запускаем бот командой


npm run start

Проверяем работу бота


check bot


Получаем PROVIDER_TOKEN от @SberbankPaymentBot


Для получения PROVIDER_TOKEN вам необходимо получить merchantLogin в Сбербанке. Для этого необходимо подключить услугу интерент-эквайринг в Сбербанке.


После того как вы его получили переходим в @BotFather и вызываем команду /mybots, где выбираем вашего бота.


Далее Payments


Payments


Где выбираем Сбербанк


Payments


Выбираем Connect Сбербанк Live


Payments


После этого вас перекинет на @SberbankPaymentBot, где нужно ввести ваш merchantLogin, который необходимо вводить без всяких префиксов -api или -operator. Например так: P71XXXXXXX21. Из-за того что я этого не знал, у меня ушло на переписку с техподдержкой Сбербанка неделя времени.


SberbankPaymentBot


После @BotFather выдаст вам токен, который нужно вставить в переменную PROVIDER_TOKEN файла .env


PROVIDER_TOKEN='41018XXXX:LIVE:XXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

SberbankPaymentBot


Подключаем оплату в приложении


Пишем в index.js следующий код:


const { Telegraf } = require('telegraf')require('dotenv').config()const bot = new Telegraf(process.env.BOT_TOKEN) //сюда помещается токен, который дал botFatherconst getInvoice = (id) => {  const invoice = {    chat_id: id, // Уникальный идентификатор целевого чата или имя пользователя целевого канала    provider_token: process.env.PROVIDER_TOKEN, // токен выданный через бот @SberbankPaymentBot     start_parameter: 'get_access', //Уникальный параметр глубинных ссылок. Если оставить поле пустым, переадресованные копии отправленного сообщения будут иметь кнопку Оплатить, позволяющую нескольким пользователям производить оплату непосредственно из пересылаемого сообщения, используя один и тот же счет. Если не пусто, перенаправленные копии отправленного сообщения будут иметь кнопку URL с глубокой ссылкой на бота (вместо кнопки оплаты) со значением, используемым в качестве начального параметра.    title: 'InvoiceTitle', // Название продукта, 1-32 символа    description: 'InvoiceDescription', // Описание продукта, 1-255 знаков    currency: 'RUB', // Трехбуквенный код валюты ISO 4217    prices: [{ label: 'Invoice Title', amount: 100 * 100 }], // Разбивка цен, сериализованный список компонентов в формате JSON 100 копеек * 100 = 100 рублей    payload: { // Полезные данные счета-фактуры, определенные ботом, 1128 байт. Это не будет отображаться пользователю, используйте его для своих внутренних процессов.      unique_id: `${id}_${Number(new Date())}`,      provider_token: process.env.PROVIDER_TOKEN     }  }  return invoice}bot.use(Telegraf.log())bot.hears('pay', (ctx) => { . // это обработчик конкретного текста, данном случае это - "pay"  return ctx.replyWithInvoice(getInvoice(ctx.from.id)) //  метод replyWithInvoice для выставления счета  })bot.on('pre_checkout_query', (ctx) => ctx.answerPreCheckoutQuery(true)) // ответ на предварительный запрос по оплатеbot.on('successful_payment', async (ctx, next) => { // ответ в случае положительной оплаты  await ctx.reply('SuccessfulPayment')})bot.launch()

Метод Telegraf replyWithInvoice это метод telegram.sendInvoice.


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


Запускаем бот командой yarn start и проверяем проходит ли оплата.


Проверить как работает оплата можно в наших телеграм ботах JavaScript Bot это бот с тестовыми вопросами по нашим курсам JavaScript, React Native, TypeScript, а также проверить платежи можно боте по изучению английских слов по эмодзи Englishmoji


Проблемы или вопросы?


Задавайте их в телеграм сообществе Боты на Telegraf


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


JavaScript Camp

Подробнее..

Fastify.js не только самый быстрый веб-фреймворк для node.js

04.05.2021 18:23:50 | Автор: admin
Последние 10 лет среди веб-фреймворков для node.js самой большой популярностью пользуется Express.js. Всем, кто с ним работал, известно, что сложные приложения на Express.js бывает сложно структурировать. Но, как говорится, привычка вторая натура. От Express.js бывает сложно отказаться. Как, например, сложно бросить курить. Кажется, что нам непременно нужна эта бесконечная цепь middleware, и если у нас забрать возможность создавать их по любому поводу и без повода проект остановится.

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

Таблица. Показатели популярности пакетов по данным npmjs.org, github.com
Пакет Количество загрузок Количество звезд
1 connect 4 373 963 9 100
2 express 16 492 569 52 900
3 koa 844 877 31 100
4 nestjs 624 603 36 700
5 hapi 389 530 13 200
6 fastify 216 240 18 600
7 restify 93 665 10 100
8 polka 71 394 4 700


Express.js по-прежнему работает в более чем в 2/3 веб-приложений для node.js. Более того, 2/3 наиболее популярных веб-фреймворков для node.js используют подходы Express.js. (Точнее было бы сказать, подходы библиотеки Connect.js, на которой до версии 4 базировался Express.js).

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


Критика фреймворков, основаных на синхронных middleware



Что же плохого может быть в таком коде?

app.get('/', (req, res) => {  res.send('Hello World!')})


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

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

app.get('/', async (req, res, next) => {   try {      ...   } catch (ex) {      next(ex);   }})


или так:

app.get('/', (req, res, next) => {   doAcync().catch(next)})


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

app.get('/', (req, res) => {  res.send('Hello World!')})


4. Ну и наконец, последнее но немаловажное. В большинстве Express.js приложений работет примерно такой код:

app.use(someFuction);app.use(anotherFunction());app.use((req, res, nexn) => ..., next());app.get('/', (req, res) => {  res.send('Hello World!')})


Когда Вы разрабатываете свою часть приложения, то можете быть уверенным что до вашего кода уже успели отработать 10-20 middleware, которые вешают на объект req всевозможные свойства, и, даже, могут модифицировать исходный запрос, ровно как и в том что столько же если не больше middleware может бтоь добавлено после того, как вы разработаете свою часть приложения. Хотя, к слову сказать, в документации Express.js для навешивания дополнительных свойств неоднозначно рекомендуется объект res.locals:

// из документации Express.jsapp.use(function (req, res, next) {  res.locals.user = req.user  res.locals.authenticated = !req.user.anonymous  next()})


Исторические попытки преодоления недостатков Express.js



Не удивительно, что основной автор Express.js и Connect.js TJ Holowaychuk оставил проект, чтобы начать разработку нового фреймворка Koa.js. Koa.js добавляет асинхронность в Express.js. Например, такой код избавляет от необходимости перехватывать асинхронные ошибки в коде каждого роута и выносит обработчик в один middleware:

app.use(async (ctx, next) => {  try {    await next();  } catch (err) {    // will only respond with JSON    ctx.status = err.statusCode || err.status || 500;    ctx.body = {      message: err.message    };  }})


Первые версии Koa.js имели замысел внедрить генераторы для обработки асинхронных вызовов:

// from http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/var request = Q.denodeify(require('request')); // Example of calling library code that returns a promisefunction doHttpRequest(url) {    return request(url).then(function(resultParams) {        // Extract just the response object        return resultParams[];    });}app.use(function *() {    // Example with a return value    var response = yield doHttpRequest('http://example.com/');    this.body = "Response length is " + response.body.length;});


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

Почти ровесник Express.js фреймворк Hapi.js. Контроллеры в Hapi.js уже возвращают значение, что является шагом вперед, по сравнению с Express.js. Не получив популярность сравнимую с Express.js, мега-успешной стала составная часть проекта Hapi.js библиотека Joi, которая имеет количество загрузок с npmjs.org 3 388 762, и сейчас используется как на бэкенде, так и на фронтенде. Поняв, что валидация входящих объектов это не какой-то особый случай, а необходимый атрибут каждого приложения валидация в Hapi.js была включена как составляющая часть фреймворка, и как параметр в определении роута:

server.route({    method: 'GET',    path: '/hello/{name}',    handler: function (request, h) {        return `Hello ${request.params.name}!`;    },    options: {        validate: {            params: Joi.object({                name: Joi.string().min(3).max(10)            })        }    }});


В настоящее время, библиотека Joi выделена в самостоятельный проект.

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

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

Fastify.js



Подитожу те требования, который мне кажутся существенными при выборе веб-фреймворка:

1. Наличие полноценных контроллеров (возвращаемое значение функции возвращется клиенту в теле ответа).
2. Удобная обработка синхронных и асинхронных ошибок.
3. Валидация входных параметров.
4. Самодокуметирование на основании определений роутов и схем валидации входных/выходных параметров.
5. Инстанциирование асинхронных сервисов.
6. Расширяемость.

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

Поэтому альтернативой может стать фреймворк Fastify.js, особенности применения которого я сейчас разберу.

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

// Require the framework and instantiate itconst fastify = require('fastify')({  logger: true})// Declare a routefastify.get('/', (request, reply) => {  reply.send({ hello: 'world' })})// Run the server!fastify.listen(3000, (err, address) => {  if (err) throw err  // Server is now listening on ${address}})


const fastify = require('fastify')({  logger: true})fastify.get('/',  (request, reply) => {  reply.type('application/json').code(200)  return { hello: 'world' }})fastify.listen(3000, (err, address) => {  if (err) throw err  // Server is now listening on ${address}})


Обработка ошибок может быть встроенной (из коробки) и кастомной.

const createError = require('fastify-error');const CustomError = createError('403_ERROR', 'Message: ', 403);function raiseAsyncError() {  return new Promise((resolve, reject) => {    setTimeout(() => reject(new CustomError('Async Error')), 5000);  });}async function routes(fastify) {  fastify.get('/sync-error', async () => {    if (true) {      throw new CustomError('Sync Error');    }    return { hello: 'world' };  });  fastify.get('/async-error', async () => {    await raiseAsyncError();    return { hello: 'world' };  });}


Оба варианта и синхронный и асинхронный отрабатывают одинаково встроенным обработчиком ошибок. Конечно, встроенных возможностей всегда мало. Кастомизируем обработчик ошибок:

fastify.setErrorHandler((error, request, reply) => {  console.log(error);  reply.status(error.status || 500).send(error);});  fastify.get('/custom-error', () => {    if (true) {      throw { status: 419, data: { a: 1, b: 2} };    }    return { hello: 'world' };  });


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

Для валидации Fastify.js использует библиотеку Ajv.js, которая реализует интерфенйс swagger/openAPI. Этот факт делает возможным интеграцию Fastify.js со swagger/openAPI и самодокументирвоание API.

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

const fastify = require('fastify')({  logger: true,  ajv: {    customOptions: {      removeAdditional: false,      useDefaults: true,      coerceTypes: true,      allErrors: true,      strictTypes: true,      nullable: true,      strictRequired: true,    },    plugins: [],  },});  const opts = {    httpStatus: 201,    schema: {      description: 'post some data',      tags: ['test'],      summary: 'qwerty',      additionalProperties: false,      body: {        additionalProperties: false,        type: 'object',        required: ['someKey'],        properties: {          someKey: { type: 'string' },          someOtherKey: { type: 'number', minimum: 10 },        },      },      response: {        200: {          type: 'object',          additionalProperties: false,          required: ['hello'],          properties: {            value: { type: 'string' },            otherValue: { type: 'boolean' },            hello: { type: 'string' },          },        },        201: {          type: 'object',          additionalProperties: false,          required: ['hello-test'],          properties: {            value: { type: 'string' },            otherValue: { type: 'boolean' },            'hello-test': { type: 'string' },          },        },      },    },  };  fastify.post('/test', opts, async (req, res) => {    res.status(201);    return { hello: 'world' };  });}


Поскольку схема входящих объектов уже определена, генерация документации swagger/openAPI сводится к инсталляции плагина:

fastify.register(require('fastify-swagger'), {  routePrefix: '/api-doc',  swagger: {    info: {      title: 'Test swagger',      description: 'testing the fastify swagger api',      version: '0.1.0',    },    securityDefinitions: {      apiKey: {        type: 'apiKey',        name: 'apiKey',        in: 'header',      },    },    host: 'localhost:3000',    schemes: ['http'],    consumes: ['application/json'],    produces: ['application/json'],  },  hideUntagged: true,  exposeRoute: true,});


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

fastify.register(require('fastify-response-validation'));


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

Код связанный с написание статьи можно найти здесь.

Дополнительные источники информации

1. blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators
2. habr.com/ru/company/dataart/blog/312638

apapacy@gmail.com
4 мая 2021 года
Подробнее..

Модуль для работы с sqlite3

07.05.2021 16:08:17 | Автор: admin

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

Концепция

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

Представим модуль в виде класса.

Всего будет 4 метода:

  1. getData() - для получения данных из таблицы.

  2. insertData() - для добавления данных в таблицу.

  3. updateData() - для обновления данных в таблице.

  4. deleteData() - для удаления данных из таблицы.

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

Кодим

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

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

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

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

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');        /**     *      * @param {String[]} keys      * @param {String} table      * @param {String} condition      * @param {Boolean} some      * @param {Function()} callback      */    static getData(keys, table, condition = '', some = true, callback = () => {}) {        let sql = 'SELECT ';        for (let i = 0; i < keys.length; i++) {            sql += keys[i] === '*' ? keys[i] : '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ' FROM `' + table + '` ' + condition;                if (some)            this.database.all(sql, (err, rows) => {                callback(err, rows);            });        else            this.database.get(sql, (err, row) => {                callback(err, row);            });    };    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

Напишем метод отвечающий за обновление данных.

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

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');        /**     *      * @param {String[]} keys      * @param {String} table      * @param {String} condition      * @param {Boolean} some      * @param {Function()} callback      */    static getData(keys, table, condition = '', some = true, callback = () => {}) {        let sql = 'SELECT ';        for (let i = 0; i < keys.length; i++) {            sql += keys[i] === '*' ? keys[i] : '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ' FROM `' + table + '` ' + condition;                if (some)            this.database.all(sql, (err, rows) => {                callback(err, rows);            });        else            this.database.get(sql, (err, row) => {                callback(err, row);            });    };        /**     *      * @param {String[]} keys      * @param {Values[]} values      * @param {String} table      * @param {String} condition      * @param {Function()} callback      */    static updateData(keys, values, table, condition, callback = () => {}) {        let sql = 'UPDATE `' + table + '` SET ';        for (let i = 0; i < keys.length; i++) {            sql += '`' + keys[i] + '` = ' + this.ToString(values[i]);            if (keys.length > i + 1)                sql += ', ';        }        sql += ' ' + condition;                this.database.run(sql, (err) => {            callback(err);        });    }    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

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

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

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');        /**     *      * @param {String[]} keys      * @param {String} table      * @param {String} condition      * @param {Boolean} some      * @param {Function()} callback      */    static getData(keys, table, condition = '', some = true, callback = () => {}) {        let sql = 'SELECT ';        for (let i = 0; i < keys.length; i++) {            sql += keys[i] === '*' ? keys[i] : '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ' FROM `' + table + '` ' + condition;                if (some)            this.database.all(sql, (err, rows) => {                callback(err, rows);            });        else            this.database.get(sql, (err, row) => {                callback(err, row);            });    };        /**     *      * @param {String[]} keys      * @param {Values[]} values      * @param {String} table      * @param {String} condition      * @param {Function()} callback      */    static updateData(keys, values, table, condition, callback = () => {}) {        let sql = 'UPDATE `' + table + '` SET ';        for (let i = 0; i < keys.length; i++) {            sql += '`' + keys[i] + '` = ' + this.ToString(values[i]);            if (keys.length > i + 1)                sql += ', ';        }        sql += ' ' + condition;                this.database.run(sql, (err) => {            callback(err);        });    }        /**     * @param {String[]} keys     * @param {String[]} values     * @param {String} table      * @param {Function()} callback      */    static insertData(keys, values, table, callback = () => {}) {        let sql = 'INSERT INTO `' + table + '` (';        for (let i = 0; i < keys.length; i++) {            sql += '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ') VALUES (';        for (let i = 0; i < values.length; i++) {            sql += this.ToString(values[i]);            if (values.length > i + 1)                sql += ', ';        }        sql += ')';        this.database.run(sql, (err) => {            callback(err);        });    };    /**     *      * @param {String} table      * @param {String} condition      * @param {Function()} callback      */    static deleteData(table, condition = '', callback = () => {}) {        this.database.run('DELETE FROM `' + table + '` ' + condition, (err) => {            callback(err);        });    }    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

На этом все, спасибо за внимание!
Проект на GitHub

Подробнее..

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

10.05.2021 20:18:52 | Автор: admin

Вот за что я люблю typescript, так это за то что он не даёт мне пороть ерунду. Померять длину числового значения и проч. Поначалу я конечно плевался, возмущался что ко мне пристают со всякими глупыми формальностями. Но потом втянулся, полюбил пожёстче. Ну в смысле a little bit more strict. Включил в проекте опцию strictNullCheck и три дня потратил на устранение возникших ошибок. А потом с удовлетворением радовался, отмечая как легко и непринуждённо проходит теперь рефакторинг.

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

Пример 1

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

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


import { createElement, FunctionComponent, ComponentClass } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

export class Rendered<P> extends String {
constructor(component: FunctionComponent<P> | ComponentClass<P>, props: P)
{
super('<!DOCTYPE html>' + renderToStaticMarkup(
createElement(component, props),
));
}
}

Теперь если мы попытаемся передать в компонент пользователя пропсы от заказа - нам незамедлительно укажут на это недоразумение. Круто? Круто.

Но это в момент генерации html. Как же дела обстоят с дальнейшим его использованием? Т.к. результатом инстацирования Rendered является просто строка, то typescript не будет ругаться например на такую конструкцию:

const html: Rendered<SomeProps> = 'Typescript cannot into space';

Соответственно, если мы напишем примерно такой контроллер:

@Get()
public index(): Rendered<IHelloWorld> {
return new Rendered(HelloWorldComponent, helloWorldProps);
}

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

Ну так давайте сделаем, чтобы это была не просто строка :)

export class Rendered<P> extends String {
_props: P;
constructor(component: FunctionComponent<P> | ComponentClass<P>, props: P)
...

Тут уже 'cannot into space' не прокатит. Уже есть прогресс. Вероятность нечаянно вернуть что-то не то сильно уменьшается. Что так же мне нравится - мы никак не используем свойство _props, и соответственно оно не попадает в скомпиллированный js код и не перегружает ответ сервера, т.е. остаётся "виртуальным" усилителем проверки типов.

Но всё еще прокатит вариант

Object.assign('cannot into space', {_props: 42})

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

export class Rendered1<P> extends String {
// @ts-ignore - не случай если у вас включен noUnusedParameters
private readonly _props: P;
constructor(component: FunctionComponent<P> | ComponentClass<P>, props: P)
...

Теперь даже результат вызова Object.assign нам не дадут вернуть из контроллера, т.к. в классе Rendered поле _props приватное, а в самодельном объекте публичное.

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

Пример 2

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

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

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

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

export interface IApiResponse {
readonly scenarioSuccess: boolean;
readonly systemSuccess: boolean;
readonly result: string | null;
readonly error: string | null;
readonly payload: string | null;
}

export class ApiResponse implements IApiResponse {
constructor(
public readonly scenarioSuccess: boolean,
public readonly systemSuccess: boolean,
public readonly result: string | null = null,
public readonly error: string | null = null,
public readonly payload: string | null = null,
) {}
}

При успешном выполнении операции будем ставить scenarioSuccess в true. Если наша логика отработала корректно, но пользователю нужно отказать (например введённый пароль неверен) - будем ставить scenarioSuccess в false. А если мы не смогли проверить пароль например потому что база отвалилась - будем ставить systemSuccess в false. Сообщения об успехе/неудаче будем отдавать в полях result/error. Интерфейс конечно подраздут. Зато хорошо видно, что можно например выставить scenarioSuccess true и непустое значение error.

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

export class ScenarioSuccessResponse extends ApiResponse {
constructor(result: string, payload: string | null = null) {
super(true, true, result, null, payload);
}
}

и так далее.

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

const SECRET_SYMBOL = Symbol('SECRET_SYMBOL');

expoet abstract class ApiResponse implements IApiResponse {
// @ts-ignore
private readonly [SECRET_SYMBOL]: unknown;


constructor(
public readonly scenarioSuccess: boolean,
public readonly systemSuccess: boolean,
public readonly result: string | null = null,
public readonly error: string | null = null,
public readonly payload: string | null = null,
) {}
}

Если для обхода класса Rendered можно было создать новый класс с приватным полем _props, то теперь приватное свойство является вычисляемым, а обратиться к нему можно через символ, который мы не экспортируем из модуля. И соответственно "повторить" его не получится. По крайней мере в другом файле. (Вам тоже кажется, что это попахивает паранойей?)

Ну что ж. Такое обойти можно, пожалуй, только через any. Но против лома нет приёма.

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

Well...

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

Благодарю за уделённое время. Буду рад конструктивной критике.

Подробнее..
Категории: Javascript , Typescript , Node.js , Nodejs , Типизация

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

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

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

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

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

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

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

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

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

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

node src/ws_server/echo_server.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Хочу middleware, но не хочу ExpressJS

14.05.2021 06:11:23 | Автор: admin
Middleware в случае с HTTP-сервером в Node.JS это промежуточный код, который выполняется до того, как начнёт выполняться ваш основной код. Это, чаще всего, нужно для того, чтобы сделать какой-то дополнительный тюнинг или проверку входящего запроса. Например, чтобы превратить данные из POST-запроса в формате JSON-строки в обычный объект, или получить доступ к кукам в виде объета, и т.п.

Стандартный модуль http из Node.JS не поддерживает такие вещи. Самый очевидный путь: установить ExpressJS и не париться. Но, на мой взгляд, если есть возможность самому написать немного кода и не добавлять ещё 50 пакетов-зависимостей в проект, архитектура станет проще, скорость работы будет выше, будет меньше точек отказа, и ещё не будет нужно постоянно пастись на гитхабе и уговаривать разработчиков обновить версии зависимостей в package.json (или просто принять пулл-реквест, где другой человек за него это сделал), чтобы код был постоянно свежим и актуальным. Я пару раз так делал, и мне не очень нравится тратить время на такие вещи. Очень часто, если ты самостоятельно воспроизводишь какую-то технологию, времени на поддержку тратится меньше, чем если ты устанавливаешь сторонний модуль с такой технологией как раз из-за таких моментов, когда ты тратишь время на то, чтобы напоминать другим разработчикам, что нужно следить за обновлениями зависимостей и реагировать на них своевременно.

Суть middleware довольно-таки проста: это функция, которая принимает три параметра: request, response и next:



Middleware делает все нужные телодвижения с request и response, после чего вызывает функцию next это сигнал, что оно закончило работу и можно работать дальше (например, запустить в обработку следующее middleware, или просто перейти к основному коду). Если next вызывается без параметров, то всё нормально. Если в вызов передать ошибку, то обработка списка middleware останавливается.

Пример простейшего middleware:

function myMiddleware(request, response, next) {    if (typeof next !== 'function') {        next = () => {};    }    console.log('Incoming request');    next();}


Если честно, я даже не смотрел, как это реализовано в ExpressJS, но, навскидку, я понимаю этот процесс так: когда вызывается server.use(myMiddleware), моя функция myMiddleware добавляется в какой-то массив, а при каждом входящем запросе вызываются все функции из этого массиа в порядке очерёдности их добавления, после чего начинает работать остальной код. Очевидно, раз используется функция next, то подразумевается асинхронность кода: middleware-функции не просто выполняются одна за другой перед тем как выполнить следующую функцию из списка, нужно дождаться окончания работы предыдущей.

Получается, вначале мне нужно создать функцию server.use, которая будет регистрировать все middleware.

MyHttpServer.js:

const http = require('http');const middlewares = [];/** * Основной обработчик HTTP-запросов *  * Пока что тут только заглушка *  * @param {IncomingMessage} request * @param {ServerResponse} response * @return {Promise<void>} */async function requestListener(request, response) {    throw new Error('Not implemented');}/* * Функция-регистратор middleware-кода */function registerMiddleware(callback) {    if (typeof callback !== 'function') {        return;    }    middlewares.push(callback);}// Создаётся сервер и регистрируется осноной обработчикconst server = http.createServer(requestListener);// К серверу добавляется регистратор middleware-функцийserver.use = registerMiddleware;


Осталась самая малость: нужно каким-то образом выполнять все эти middleware в асинхронном режиме. Лично я, если мне нужно обойти массив в асинхронном режиме, пользуюсь функцией Array.prototype.reduce(). Она, в определённых условиях, может делать как раз то, что мне нужно. Самое время доработать функцию requestListener.

/* * Это просто служебная функция  вставлена здесь для примера  * и сама по себе обычно находится в другом модуле */function isError(error) {    switch (Object.prototype.toString.call(error)) {        case '[object Error]':            return true;        case '[object Object]':            return (                error.message                && typeof error.message === 'string'                && error.stack                && typeof error.stack === 'string'            );    }    return false;}/** * Основной обработчик HTTP-запросов *  * @param {IncomingMessage} request * @param {ServerResponse} response * @return {Promise<void>} */async function requestListener(request, response) {    response.isFinished = false;    response.on('finish', () => response.isFinished = true);    response.on('close', () => response.isFinished = true);    let result;    try {        result = await middlewares.reduce(            // Редусер. Первый параметр  предыдущее значение,             // второй  текущее. Чтобы обеспечить асинхронность,             // всё оборачивается в Promise.            (/**Promise*/promise, middleware) => promise.then(result => {                // Если в предыдущем middleware был вызов                 // next(new Error('Some message')), текущий middleware                 // игнорируется и сразу возвращается ошибка                 // из предыдущего кода                if (isError(result)) {                    return Promise.reject(result);                }                // Возвращается новый Promise, который, кроме прочего,                 // реагирует на какую-то ошибку в рамках не только                 // вызова next, но и в рамках всего кода.                // То есть, если middleware вызывает внутри JSON.parse                 // без try-catch, то, в случае ошибки парсинга, реакция                 // будет такая же, как и при вызове next с передачей                 // ошибки в качестве параметра                return new Promise((next, reject) => {                    Promise.resolve(middleware(request, response, next)).catch(reject);                });            }),            Promise.resolve()        );        if (isError(result)) {            throw result;        }    } catch (error) {        response.statusCode = 500;        result = 'Error';    }    if (response.isFinished) {        return;    }    response.end(result);}


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

const cookieParser = require('cookie-parser');server.use(cookieParser());// Мой основной кодserver.use((request, response, next) => {    if (request.cookies['SSID']) {        response.end('Your session id is ' + request.cookies['SSID']);    } else {        response.end('No session detected');    }    next();});


Под спойлером простейший пример стандартного HTTP-сервера Node.JS с поддержкой экспрессовких middleware для тех, кто предпочитает copy/paste.

MyHttpServer.js
const http = require('http');const middlewares = [];function isError(error) {    switch (Object.prototype.toString.call(error)) {        case '[object Error]':            return true;        case '[object Object]':            return (                error.message                && typeof error.message === 'string'                && error.stack                && typeof error.stack === 'string'            );    }    return false;}/** * Основной обработчик HTTP-запросов *  * @param {IncomingMessage} request * @param {ServerResponse} response * @return {Promise<void>} */async function requestListener(request, response) {    response.isFinished = false;    response.on('finish', () => response.isFinished = true);    response.on('close', () => response.isFinished = true);    let result;    try {        result = await middlewares.reduce(            (/**Promise*/promise, middleware) => promise.then(result => {                if (isError(result)) {                    return Promise.reject(result);                }                return new Promise((next, reject) => {                    Promise.resolve(middleware(request, response, next)).catch(reject);                });            }),            Promise.resolve()        );        if (isError(result)) {            throw result;        }    } catch (e) {        response.statusCode = 500;        result = 'Error';    }    if (response.isFinished) {        return;    }    response.end(result);}/* * Функция-регистратор middleware-кода */function registerMiddleware(callback) {    if (typeof callback !== 'function') {        return;    }    middlewares.push(callback);}const server = http.createServer(requestListener);server.use = registerMiddleware;const cookieParser = require('cookie-parser');server.use(cookieParser());server.use((request, response) => {    if (request.cookies['SSID']) {        return 'Your session id is ' + request.cookies['SSID'];    }    return 'No session detected';});server.listen(12345, 'localhost', () => {    console.log('Started http');});



Нашли ошибку в тексте? Выделите текст, содержащий ошибку и нажмите Alt-F4 (если у вас мак, то -Q). Шутка, конечно же. Если нашли ошибку, пишите в личные сообщения или в комментарии постараюсь исправить.
Подробнее..
Категории: Javascript , Node.js , Nodejs , Middleware

Как написать пассивный доход Пишем качественного трейд бота на JS (часть 1)

12.06.2021 20:19:26 | Автор: admin

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

  1. торговать самостоятельно, принося какой-то доход

  2. должен быть удобен для создания и обкатывания различных стратегий торговли

  3. тестировать стратегию на исторических данных

Пожалуй, начнем с архитектуры

У нас есть биржа Binance, у которой есть шикарное api. Поэтому архитектура могла бы выглядеть так:

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

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

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

Сервис для логов

Простой класс, который принимает на вход префикс для логирования и имеет два метода log и error. Эти методы печатают лог с текущим временем и перфиксом:

class LoggerService {  constructor(prefix) {    this.logPrefix = prefix  }  log(...props) {    console.log(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)  }  error(...props) {    console.error(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)  }}

Теперь подключим биржу

yarn add node-binance-api

Добавим класс BaseApiService. Сделаем в нем инициализацию Binance SDK, а также применим сервис LoggerService. Учитывая мой опыт с Binance могу сразу сказать, что в зависимости от торговой пары мы должны слать цену и обьем с разным количеством знаков после запятой. Все эти настройки для каждой пары можно взять, сделав запрос futuresExchangeInfo(). И написать методы для получения количества знаков после запятой для цены getAssetPricePrecision и объема getAssetQuantityPrecision.

class BaseApiService {  constructor({ client, secret }) {    const { log, error } = new Logger('BaseApiService')    this.log = log    this.error = error    this.api = new NodeBinanceApi().options({      APIKEY: client,      APISECRET: secret,      hedgeMode: true,    })    this.exchangeInfo = {}  }  async init() {    try {      this.exchangeInfo = await this.api.futuresExchangeInfo()    } catch (e) {      this.error('init error', e)    }  }  getAssetQuantityPrecision(symbol) {    const { symbols = [] } = this.exchangeInfo    const s = symbols.find(s => s.symbol === symbol) || { quantityPrecision: 3 }    return s.quantityPrecision  }  getAssetPricePrecision(symbol) {    const { symbols = [] } = this.exchangeInfo    const s = symbols.find(s => s.symbol === symbol) || { pricePrecision: 2 }    return s.pricePrecision  }}

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

async futuresOrder(side, symbol, qty, price, params={}) {  try {    qty = Number(qty).toFixed(this.getAssetQuantityPrecision(symbol))    price = Number(price).toFixed(this.getAssetPricePrecision(symbol))    if (!params.type) {      params.type = ORDER.TYPE.MARKET    }    const res = await this.api.futuresOrder(side, symbol, qty, price || false, params)    this.log('futuresOrder', res)    return res  } catch (e) {    console.log('futuresOrder error', e)  }}

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

class TradeService {  constructor({client, secret}) {    const { log, error } = new LoggerService('TradeService')    this.log = log    this.error = error    this.api = new NodeBinanceApi().options({      APIKEY: client,      APISECRET: secret,      hedgeMode: true,    })    this.events = new EventEmitter()  }  marginCallCallback = (data) => this.log('marginCallCallback', data)  accountUpdateCallback = (data) => this.log('accountUpdateCallback', data)  orderUpdateCallback = (data) => this.emit(data)  subscribedCallback = (data) => this.log('subscribedCallback', data)  accountConfigUpdateCallback = (data) => this.log('accountConfigUpdateCallback', data)  startListening() {    this.api.websockets.userFutureData(      this.marginCallCallback,      this.accountUpdateCallback,      this.orderUpdateCallback,      this.subscribedCallback,      this.accountConfigUpdateCallback,    )  }  subscribe(cb) {    this.events.on('trade', cb)  }  emit = (data) => {    this.events.emit('trade', data)  }}

При помощи метода из SDK this.api.websockets.userFutureData подписываемся на события из биржы. Самой главный колбек для нас this.orderUpdateCallback . Он вызывается каждый раз когда меняется статус у ордера. Ловим это событие и прокидываем через EventEmitter тому, кто на это событие подписался, используя метод subscribe.

Перейдем к базе данных

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

yarn add sequelize-cli -Dyarn add sequelizenpx sequelize-cli init

Добавим docker-compose.yml файл для локальной базы:

version: '3.1'services:  db:    image: 'postgres:12'    restart: unless-stopped    volumes:      - ./volumes/postgresql/data:/var/lib/postgresql/data    environment:      POSTGRES_USER: root      POSTGRES_PASSWORD: example      POSTGRES_DB: bot    ports:      - 5432:5432    networks:      - postgresnetworks:  postgres:    driver: bridge

А также добавляю миграции и модели. User, Order

Продолжение следует.

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

Подробнее..

Как работает Middleware в Express?

16.06.2021 00:20:01 | Автор: admin

Статья переведена. Ссылка на оригинал

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

__________________

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

  • Где правильное место для добавления этого middleware в мое приложение?

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

  • Почему важен порядок использования middleware?

  • Как я могу написать свой собственный код для обработки ошибок?

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

В этой статье мы подробно рассмотрим паттерн промежуточного ПО (middleware). Мы также рассмотрим различные типы middleware Express и то, как эффективно сочетать их при создании приложений.

Шаблон Middleware

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

Когда вы определяете маршрут в Express, функция-обработчик маршрута, которую вы указываете для этого маршрута, является функцией Middleware:

app.get("/user", function routeHandlerMiddleware(request, response, next) {    // execute something});

(Пример 1.1)

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

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

Синтаксис Middleware

Вот синтаксис функции middleware:

/** * @param {Object} request - Express request object (commonly named `req`) * @param {Object} response - Express response object (commonly named `res`) * @param {Function} next - Express `next()` function */function middlewareFunction(request, response, next) {    // execute something}

(Пример 1.2)

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

Когда Express запускает функцию middleware, ей передаются три аргумента:

  • Объект запроса Express (обычно называемый req) - это расширенный экземпляр встроенного в Node.js класса http.IncomingMessage.

  • Объект ответа Express (обычно называемый res) - это расширенный экземпляр встроенного в Node.js класса http.ServerResponse.

  • Функция Express next() - После того как промежуточная функция выполнит свои задачи, она должна вызвать функцию next(), чтобы передать управление следующей промежуточной программе. Если вы передаете ей аргумент, Express принимает его за ошибку. Он пропустит все оставшиеся функции middleware, не обрабатывающие ошибки, и начнет выполнять middleware, которое обрабатывает ошибки.

  • Функции middleware не должны иметь значение return. Любое значение, возвращаемое промежуточным ПО, не будет использовано Express.

Два типа Middleware

Обычное промежуточное ПО (middleware)

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

Вот пример простой функции middleware:

function plainMiddlewareFunction(request, response, next) {    console.log(`The request method is ${request.method}`);    /**     * Ensure the next middleware function is called.     */    next();}

(Пример 1.3)

Middleware для обработки ошибок

  • Разница между middleware для обработки ошибок и обычным middleware заключается в том, что функции middleware для обработки ошибок задают четыре параметра вместо трех, т.е. (error, request, response, next).

Вот пример функции middleware для обработки ошибок:

function errorHandlingMiddlewareFunction(error, request, response, next) {    console.log(error.message);    /**     * Ensure the next error handling middleware is called.     */    next(error);}

(Пример 1.4)

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

function anotherMiddlewareFunction(request, response, next) {    const error = new Error("Something is wrong");    /**     * This will cause Express to start executing error     * handling middleware.     */    next(error);}

(Пример 1.5)

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

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

  • Уровень маршрута

  • Уровень маршрутизатора

  • Уровень приложения

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

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

На уровне маршрута

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

app.get("/", someMiddleware, routeHandlerMiddleware, errorHandlerMiddleware);

(Пример 1.6)

На уровне маршрутизатора

Express позволяет создавать объекты Router. Они позволяют вам ограничить использование middleware определенным набором маршрутов. Если вы хотите, чтобы одно и то же middleware выполнялось для нескольких маршрутов, а не для всех, то такие объекты могут быть очень полезны.

import express from "express";const router = express.Router();router.use(someMiddleware);router.post("/user", createUserRouteHandler);router.get("/user/:user_id", getUserRouteHandler);router.put("/user/:user_id", updateUserRouteHandler);router.delete("/user/:user_id", deleteUserRouteHandler);router.use(errorHandlerMiddleware);

(Пример 1.7)

На уровне приложения

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

app.use(someMiddleware);// define routesapp.use(errorHandlerMiddleware);

(Пример 1.8)

Технически вы можете определить несколько маршрутов, вызвать app.use(someMiddleware), затем определить несколько других маршрутов, для которых вы хотите запустить someMiddleware. Я не рекомендую такой подход, поскольку он приводит к запутанной и трудноотлаживаемой структуре приложения.

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

Подведение итогов

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

Если вы хотите прочитать больше о middleware, в документации Express есть несколько руководств:

__________________

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

__________________

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

Статья переведена в преддверии старта курса "Node.js Developer". Всех, кто желает подробнее узнать о курсе и процессе обучения, приглашаем записаться на Demo Day курса, который пройдет 28 июня.

- ЗАПИСАТЬСЯ НА DEMO DAY КУРСА

Статья переведена. Ссылка на оригинал

Подробнее..

Перевод Сервер в одну строку на 17 языках

16.05.2021 16:07:51 | Автор: admin
Каждая из этих команд будет запускать специальный статический http-сервер в вашем текущем (или указанном) каталоге, доступном по адресу http://localhost:8000. Используйте эту силу с умом.

Python 2.x


$ python -m SimpleHTTPServer 8000


Python 3.x


$ python -m http.server 8000


Twisted (Python)


$ twistd -n web -p 8000 --path .


Или:

$ python -c 'from twisted.web.server import Site; from twisted.web.static import File; from twisted.internet import reactor; reactor.listenTCP(8000, Site(File("."))); reactor.run()'

В зависимости от Twisted.

Ruby


$ ruby -rwebrick -e'WEBrick::HTTPServer.new(:Port => 8000, :DocumentRoot => Dir.pwd).start'


Ruby 1.9.2+


$ ruby -run -ehttpd . -p8000


adsf (Ruby)


$ gem install adsf   # install dependency$ adsf -p 8000


Sinatra (Ruby)


$ gem install sinatra   # install dependency$ ruby -rsinatra -e'set :public_folder, "."; set :port, 8000'


Perl


$ cpan HTTP::Server::Brick   # install dependency$ perl -MHTTP::Server::Brick -e '$s=HTTP::Server::Brick->new(port=>8000); $s->mount("/"=>{path=>"."}); $s->start
'

Plack (Perl)


$ cpan Plack   # install dependency$ plackup -MPlack::App::Directory -e 'Plack::App::Directory->new(root=>".");' -p 8000


Mojolicious (Perl)


$ cpan Mojolicious::Lite   # install dependency$ perl -MMojolicious::Lite -MCwd -e 'app->static->paths->[0]=getcwd; app->start' daemon -l http://*:8000


http-server (Node.js)


$ npm install -g http-server   # install dependency$ http-server -p 8000


Примечание: этот сервер делает забавные вещи с relative paths. Например, если у вас есть файл /tests/index.html, он загрузит index.html, если вы перейдете в /test, но будет обрабатывать относительные пути, как если бы они исходили из /.

node-static (Node.js)


$ npm install -g node-static   # install dependency$ static -p 8000


PHP (>= 5.4)


$ php -S 127.0.0.1:8000


Erlang


$ erl -s inets -eval 'inets:start(httpd,[{server_name,"NAME"},{document_root, "."},{server_root, "."},{port, 8000},{mime_types,[{"html","text/html"},{"htm","text/html"},{"js","text/javascript"},{"css","text/css"},{"gif","image/gif"},{"jpg","image/jpeg"},{"jpeg","image/jpeg"},{"png","image/png"}]}]).'


busybox httpd


$ busybox httpd -f -p 8000


webfs


$ webfsd -F -p 8000


В зависимости от webfs.

IIS Express


C:\> "C:\Program Files (x86)\IIS Express\iisexpress.exe" /path:C:\MyWeb /port:8000


В зависимости от IIS Express.

Подробнее..

Перевод Слабо поднять такой крошечный контейнер? Создаем контейнеризованный HTTP-сервер на 6kB

25.04.2021 16:15:44 | Автор: admin
TL;DR я решил создать самый маленький образ контейнера, при помощи которого все-таки можно сделать что-нибудь полезное. Опираясь на преимущества многоступенчатых сборок, базового образаscratchи крошечного http-сервера на основе этой сборки, я смог ужать результат до 6.32kB!





Если предпочитаете видео, вот ролик по статье, выложенный на YouTube!

Раздутые контейнеры


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

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

Задача


Правила довольно просты:

  • Контейнер должен выдавать содержимое файла по http на выбранный вами порт
  • Монтирование томов не допускается (так называемое Правило Марека )

Упрощенное решение


Чтобы узнать размер базового образа, можно воспользоваться node.js и создать простой серверindex.js:

const fs = require("fs");const http = require('http');const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'text/html' })fs.createReadStream('index.html').pipe(res)})server.listen(port, hostname, () => {console.log(`Server: http://0.0.0.0:8080/`);});

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

FROM node:14COPY . .CMD ["node", "index.js"]

Этот завесил на943MB!

Уменьшенный базовый образ


Один из простейших и наиболее очевидных тактических подходов к уменьшению размера образа выбрать более компактный базовый образ. Официальный базовый образ node существует в варианте slim(по-прежнему на основе debian, но с меньшим количеством предустановленных зависимостей) и вариантalpineна основеAlpine Linux.

С применениемnode:14-slimиnode:14-alpineв качестве базового удается уменьшить размер образа до167MBи116MBсоответственно.

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

Скомпилированные языки


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

Я создал простейший файловый серверserver.go:

package mainimport ("fmt""log""net/http")func main() {fileServer := http.FileServer(http.Dir("./"))http.Handle("/", fileServer)fmt.Printf("Starting server at port 8080\n")if err := http.ListenAndServe(":8080", nil); err != nil {log.Fatal(err)}}

И встроил его в контейнерный образ, воспользовавшись официальным базовым образом golang:

FROM golang:1.14COPY . .RUN go build -o server .CMD ["./server"]

Который завесил на818MB.

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

Многоступенчатые сборки


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

Это полезно по нескольким причинам, но одна из наиболее очевидных размер образа! Выполнив рефакторинг dockerfile вот так:

### этап сборки ###FROM golang:1.14-alpine AS builderCOPY . .RUN go build -o server .### этап запуска ###FROM alpine:3.12COPY --from=builder /go/server ./serverCOPY index.html index.htmlCMD ["./server"]

Размер полученного образа всего13.2MB!

Статическая компиляция + образ Scratch


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

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

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

### этап сборки ###FROM golang:1.14 as builderCOPY . .RUN go build \-ldflags "-linkmode external -extldflags -static" \-a server.go### этап запуска ###FROM scratchCOPY --from=builder /go/server ./serverCOPY index.html index.htmlCMD ["./server"]

В частности, мы задаем externalв качестве режима линковки и передаем флаг-staticвнешнему линковщику.

Благодаря двум этим изменениям удается довести размер образа до 8.65MB

ASM как залог победы!


Образ размером менее 10MB, написанный на языке вроде Go, отчетливо миниатюрен почти для любых обстоятельств но можно сделать еще меньше! Пользовательnemasuвыложил на Github полноценный http-сервер, написанный на ассемблере. Он называется assmttpd.

Все, что потребовалось для его контейнеризации установить несколько зависимостей сборки в базовый образ Ubuntu, прежде, чем запустить предоставленный рецепт make release:

### этап сборки ###FROM ubuntu:18.04 as builderRUN apt updateRUN apt install -y make yasm as31 nasm binutilsCOPY . .RUN make release### этап запуска ###FROM scratchCOPY --from=builder /asmttpd /asmttpdCOPY /web_root/index.html /web_root/index.htmlCMD ["/asmttpd", "/web_root", "8080"]

Затем полученный в результате исполняемый файлasmttpdкопируется в scratch-образ и вызывается через командную строку. Размер полученного образа всего 6,34kB!
Подробнее..

Монстрация-онлайнстрация

01.05.2021 12:14:42 | Автор: admin

Дело было вечером, делать было нечего.

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

По известным всем обстоятельствам в этом году Монстрацию не разрешили, о чём я немного взгрустнул и подумал: "А что я могу сделать, как разработчик?".

Короче, развернул сервер, сделал небольшое API для обработки запросов на NodeJS+Express+MongoDB, а затем в Unity 3D сварганил небольшое приложение, в котором создал простенькое окружение, логику взаимодействия с API и пару игровых персонажей: участников митинга и полицию, которая их "охраняет". С контентом мне помогли несколько ребят, сделав модели полицейского УАЗика, Новосибирского оперного театра, основу персонажей.

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

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

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

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

Подробнее..

Спасибо хабравчанам за участие в Онлайнстрации

02.05.2021 20:23:47 | Автор: admin

Ребята, спасибо всем, ктопоучаствовал в мероприятии. Зарегилось 424 человека, получился настоящий творческий первомайский митинг в онлайне.

В честь этого создал специального персонажа!

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

Подробнее..

Screeps, есть ли жизнь после туториала?

15.05.2021 10:13:08 | Автор: admin

Screeps это ММО для програмистов (платное). сделан хаброчанином @artch

Что у вас есть после туториала?

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

четвертый урок туториала, даёт вам все инструменты для апгрейда контроллера. для этого вам достаточно добавить этот код в класс main.js в 21 строку

else {        var upgraders = _.filter(Game.creeps, (creep) => creep.memory.role == 'upgrader');        if(upgraders.length < 2){            var newName = 'Upgrader' + Game.time;            console.log('Spawning new upgrader: ' + newName);            Game.spawns['Spawn1'].spawnCreep([WORK,CARRY,MOVE], newName,                {memory: {role: 'upgrader'}});        }    }

для визуальной картинки, добавляем в main.js (в самый конец функции, она там всего одна)

    var controller = Game.spawns['Spawn1'].room.controller;    controller.room.visual.text('Tick '+Game.time+'\nLevel'+(controller.level+1)+' '+(controller.progress*100/controller.progressTotal)+'% Complete',            controller.pos.x + 1,            controller.pos.y,            {align: 'left', opacity: 0.8});

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

Видео, добавляем правки

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

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

Подробнее..
Категории: Node.js , Nodejs , Tutorial , Screeps

Категории

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

© 2006-2021, personeltest.ru