Seneca — это набор инструментов для быстрого создания систем микросервисов на основе сообщений. Вам не требуется знать, где именно расположены различные сервисы, сколько их существует или что они конкретно выполняют. Любые службы вне области вашей бизнес-логики (например, базы данных, кэширование или интеграция с третьими сторонами) скрыты за микросервисами.
Эта декомпозиция позволяет легко строить и обновлять ваши системы непрерывно. Seneca обеспечивает это благодаря своим трём ключевым функциям:
В Seneca сообщение представляет собой JSON-объект с любой внутренней структурой, который может быть передан через HTTP/HTTPS, TCP, очереди сообщений, публикацию/подписку или любым другим способом передачи данных. Как производитель сообщений, вы просто отправляете его, не заботясь о том, какие службы принимают его.Затем вы хотите сообщить миру, что вы хотите получать некоторые сообщения. Это также очень просто: достаточно выполнить некоторую конфигурацию шаблонного соответствия в Seneca. Шаблоны просты: это список пар ключ-значение, используемых для соответствия определенным атрибутам JSON-сообщений.
В следующих разделах мы будем создавать несколько микросервисов с использованием Seneca.
Давайте начнем с очень простого примера кода. Мы создадим два микросервиса: один будет выполнять математическое вычисление, другой же вызовет этот первый.
const seneca = require('seneca')();
seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: (msg.left + msg.right)});
});
seneca.act({
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});
Сохраните вышеупомянутый код в файл .js, затем запустите его. Вы должны увидеть что-то подобное в консоли:
{"kind":"notice","notice":"hello seneca 4y8daxnikuxp/1483577040151/58922/3.2.2/-","level":"info","when":1483577040175}
(node:58922) DeprecationWarning: 'root' is deprecated, use 'global'
{ answer: 3 }
До сих пор всё происходило в одном процессе без генерации сетевой активности, а вызовы функций внутри процесса осуществлялись через передачу сообщений.
Метод seneca.add
добавляет новый шаблон действия (Action Pattern) к экземпляру Seneca
. Он имеет два параметра:
pattern
: шаблон для совпадения JSON-тела сообщений в экземпляре Seneca
;
action
: действие, которое выполняется при совпадении шаблона.Метод seneca.act
также принимает два параметра:
msg
: входящее сообщение, представленное как чистый объект;
respond
: обратный вызов для получения и обработки ответа.
Представим, что мы пройдемся еще раз по всему коду:
seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: (msg.left + msg.right)});
});
Функция Action
в вышестоящем коде суммирует значения двух свойств left
и right
в теле совпадающего сообщения. Нет необходимости создавать ответ для всех сообщений, но в большинстве случаев это требуется. Для ответа сообщению предоставляется обратный вызов.
Шаблон совпадения role:math, cmd:sum
совпадает с следующим телом сообщения:
{
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}
И получает результат:
{
answer: 3
}
Свойства role
и cmd
ничего особенного собой не представляют; они просто используются для совпадения шаблонов.
Затем метод seneca.act
отправляет сообщение, имеющее два параметра:
msg
: основной контент сообщения;response_callback
: обратный вызов, который будет выполнен, если сообщение имеет ответ.Обратный вызов для ответа может принимать два параметра: error
и result
. В случае возникновения ошибки (например, если отправленное сообщение не совпалось ни с одним шаблоном), первый параметр будет объектом типа Error
. Если программа работает так, как нам это требуется, второй параметр будет содержать результат ответа. В нашем примере мы просто выводим этот результат в консоль.```javascript
seneca.act({
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});
Пример файла [sum.js](https://github.com/pantao/getting-started-seneca/blob/master/sum.js) демонстрирует, как определить и создать действие и как его вызвать, но всё это происходит в рамках одного процесса. В ближайшее время мы покажем, как разделить это на несколько частей и процессов.
# Как работают шаблоны?
Шаблон позволяет вам расширять или усиливать вашу систему легче, чем с использованием сетевых адресов или сессий. Это делает добавление новых микросервисов проще.
Теперь давайте добавим новую функциональность в нашу систему — вычисление произведения двух чисел.
Мы хотим отправлять сообщение такого типа:
```javascript
{
role: 'math',
cmd: 'product',
left: 3,
right: 4
}
И получить ответ такого типа:
{
answer: 12
}
Понятно? Вы можете создать операцию role: math, cmd: product
, аналогичную role: math, cmd: sum
:
seneca.add('role:math, cmd:product', (msg, reply) => {
reply(null, { answer: (msg.left * msg.right)});
});
Затем вызвать эту операцию:
seneca.act({
role: 'math',
cmd: 'product',
left: 3,
right: 4
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});
Выполните скрипт product.js, чтобы получить нужный результат.
Объединив эти два метода, код будет выглядеть так:
const seneca = require('seneca')();
```seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: msg.left + msg.right });
});
seneca.add('role:math, cmd:product', (msg, reply) => {
reply(null, { answer: msg.left * msg.right });
});
seneca.act({ role: 'math', cmd: 'sum', left: 1, right: 2 }, console.log)
.act({ role: 'math', cmd: 'product', left: 3, right: 4 }, console.log);
Выполнив [sum-product.js](https://github.com/pantao/getting-started-seneca/blob/master/sum-product.js), вы получите следующий вывод:
```bash
null { answer: 3 }
null { answer: 12 }
В объединённом выше коде мы видим, что seneca.act
можно использовать в цепочке вызовов. API Seneca предоставляет возможность последовательного выполнения запросов, но они могут выполняться параллельно, поэтому порядок получения результатов может отличаться от порядка вызова.
Шаблоны позволяют легко расширять функциональность программы. В отличие от использования конструкций if...else...
, вы можете добавлять новые шаблоны для достижения аналогичной функциональности.
Допустим, мы хотим расширить операцию role: math, cmd: sum
, которая принимает только целые числа. Как это сделать?
seneca.add({ role: 'math', cmd: 'sum', integer: true }, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right);
respond(null, { answer: sum });
});
Теперь рассмотрим следующее сообщение:
{ role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true }
Это сообщение вернет следующий результат:
{ answer: 3 } // == 1 + 2, десятичные части были удалены
```Код доступен в файле [sum-integer.js](https://github.com/pantao/getting-started-seneca/blob/master/sum-integer.js).
Теперь ваши два шаблона существуют в системе и имеют пересечение. Какой шаблон будет выбран Senecой в конечном итоге? Принцип таков: шаблон с большим количеством совпадающих атрибутов имеет более высокий приоритет.
[pattern-priority-testing.js](https://github.com/pantao/getting-started-seneca/blob/master/pattern-priority-testing.js) предоставляет более наглядное тестирование:
```javascript
const seneca = require('seneca')();
seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right;
respond(null, {answer: sum});
});
// Эти два сообщения совпадают с ролью: math, командой: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log);
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log);
setTimeout(() => {
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right);
respond(null, {answer: sum});
});
// Это сообщение также совпадает с ролью: math, командой: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log);
// Но также совпадает с ролью: math, командой: sum, целочисленной частью: true
// Однако, поскольку больше атрибутов совпадают, его приоритет выше
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log);
}, 100);
Результат вывода должен выглядеть следующим образом:
null {answer: 4}
null {answer: 4}
null {answer: 4}
null {answer: 3}
```В приведённом выше коде, поскольку в системе существует только один шаблон с ролью: math, командой: sum, все сообщения совпадают с ним. Однако после того как через 100 миллисекунд мы добавляем в систему новый шаблон с ролью: math, командой: sum, целочисленной частью: true, результаты становятся другими, и шаблон с большим количеством совпадающих атрибутов имеет более высокий приоритет. Эта архитектура позволяет нашей системе легко добавлять новые функции как в среде разработки, так и в среде производства. Вы можете обновлять новые службы без изменения существующего кода. Вам просто нужно подготовить новую службу и запустить её.## Кодовая переиспользование на основе шаблонов
Шаблоны могут вызывать другие операции, что позволяет нам достичь цели переиспользования кода:
```javascript
const seneca = require('seneca')()
seneca.add('role: math, cmd: sum', function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
senega.add('role: math, cmd: sum, integer: true', function (msg, respond) {
// Переиспользует role:math, cmd:sum
this.act({
role: 'math',
cmd: 'sum',
left: Math.floor(msg.left),
right: Math.floor(msg.right)
}, respond)
})
// Соответствует role:math,cmd:sum
senega.act('role: math, cmd: sum, left: 1.5, right: 2.5', console.log)
// Соответствует role:math,cmd:sum,integer:true
senega.act('role: math, cmd: sum, left: 1.5, right: 2.5, integer: true', console.log)
В приведенном выше примере кода мы используем this.act
, а не senega.act
. Это потому, что контекстный объект this
внутри функции действия ссылается на текущий экземпляр senega
, позволяя вам получить доступ ко всему контексту вызова действия из любой функции действия.
В этом коде используется сокращенная форма JSON для описания шаблонов и сообщений. Например, это объект-литерал:
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5}
Сокращённый шаблон выглядит следующим образом:
'role: math, cmd: sum, left: 1.5, right: 2.5'
Формат jsonic предоставляет удобный способ представления объектов через строковые литералы, что позволяет создавать более простые шаблоны и сообщения.
Вышеупомянутый код хранится в файле sum-reuse.js.## Уникальность шаблонов
Каждый определённый вами шаблон действий уникален и может триггерировать только одну функцию. Правила парсинга шаблонов следующие:
Эти правила были спроектированы максимально просто, чтобы вы могли легко понять, какой именно шаблон был выбран. Ниже приведены примеры, чтобы вам было легче понять:
a: 1, b: 2
имеет приоритет перед a: 1
, так как он содержит больше свойств;a: 1, b: 2
имеет приоритет перед a: 1, c: 3
, так как буква b
стоит раньше буквы c
;a: 1, b: 2, d: 4
имеет приоритет перед a: 1, c: 3, d: 4
, так как буква b
стоит раньше буквы c
;a: 1, b: 2, c: 3
имеет приоритет перед a: 1, b: 2
, так как он содержит больше свойств;a: 1, b: 2, c: 3
имеет приоритет перед a: 1, c: 3
, так как он содержит больше свойств.Часто полезно иметь возможность расширить функциональность существующего действия без необходимости полностью переписывать его. Например, вы можете захотеть добавить более сложную проверку атрибутов сообщения, отслеживать статистику сообщений, добавлять дополнительные данные из базы данных или контролировать скорость потока сообщений.В следующем примере кода операция сложения ожидает, что атрибуты left
и right
будут окончательными числами. Кроме того, полезно добавлять проверки и информацию для отладки, используя следующий код:```javascript
const seneca = require('seneca')()
seneca .add( 'role:math,cmd:sum', function(msg, respond) { var sum = msg.left + msg.right respond(null, { answer: sum }) })
// Переопределяем role:math,cmd:sum с добавлением дополнительной функциональности .add( 'role:math,cmd:sum', function(msg, respond) {
// Возвращаем ошибку, если есть проблема
if (!Number.isFinite(msg.left) ||
!Number.isFinite(msg.right)) {
return respond(new Error("Значения left и right должны быть числами."))
}
// Вызываем старую версию действия role:math,cmd:sum
this.prior({
role: 'math',
cmd: 'sum',
left: msg.left,
right: msg.right,
}, function(err, result) {
if (err) return respond(err)
result.info = msg.left + '+' + msg.right
respond(null, result)
})
})
// Расширенная версия role:math,cmd:sum .act('role:math,cmd:sum,left:1.5,right:2.5', console.log // выводит { answer: 4, info: '1.5+2.5' } )
Инстанс `seneca` предоставляет метод `prior`, который позволяет вызвать старое действие внутри текущего действия.
Метод `prior` принимает два параметра:
1. `msg`: тело сообщения
2. `response_callback`: обратный вызов
В приведенном выше примере показано, как можно модифицировать входные и выходные параметры; эти изменения являются необязательными, и вы можете добавить новые переопределения для внесения дополнительных изменений, таких как логирование. Приведённый выше пример также демонстрирует лучшие способы обработки ошибок. Мы проверяем корректность данных до выполнения действий, чтобы сразу вернуть сообщение об ошибке при некорректных входных данных.
> Сообщения об ошибках следует использовать только для описания неправильных входных данных или внутренних сбоев системы. Например, если вы выполнили запрос в базу данных и получили пустой ответ, это не является ошибкой, а просто отражает текущее состояние базы данных. Однако, если соединение с базой данных было установлено неверно, то это уже будет являться ошибкой.
Вышеупомянутый код можно найти в файле [sum-valid.js](https://github.com/pantao/getting-started-seneca/blob/master/sum-valid.js).
## Организация паттернов действия с помощью плагинов
Инстанс `seneca` представляет собой набор различных паттернов действий. Вы можете организовать эти паттерны с использованием пространства имён, как показано в предыдущих примерах, где мы использовали `role: math`. Для помощи в логгировании и отладке, `Seneca` предоставляет возможность использования минималистичных плагинов.
Аналогично, Seneca-плагины представляют собой набор паттернов действий, который может иметь имя для аннотации логгирования и набор опций для управления поведением. Плагины предоставляют механизм для правильной последовательности выполнения методов инициализации, например, установка соединения с базой данных перед попыткой чтения данных из неё.
Кратко говоря, Seneca-плагин — это всего лишь функция с одним параметром опций, которую вы передаёте методу `seneca.use`, вот минимальный пример Seneca-плагина:
```javascript
function plugin(options) {
// Логика плагина
}
Параметр options
обычно содержит конфигурационные данные, такие как пути к файлам, адреса серверов и другие настройки, необходимые для работы плагина.```javascript
function минимальный_плugин(опции) {
console.log(опции);
}
require('seneca')() .use(минимальный_плugин, {foo: 'bar'});
Метод `seneca.use` принимает два параметра:
1. `plugin`: функция определения плагина или название плагина;
2. `options`: конфигурационные опции плагина;
После выполнения приведённого выше кода, вывод в консоли будет следующим:
```bash
{"kind":"notice","notice":"hello seneca 3qk0ij5t2bta/1483584697034/62768/3.2.2/-","level":"info","when":1483584697057}
(node:62768) DeprecationWarning: 'root' is deprecated, use 'global'
{ foo: 'bar' }
Seneca также предлагает расширенные возможности логгирования, которые могут обеспечивать больше информации для разработки или производства. Обычно уровень логгирования устанавливается на INFO
, что ограничивает количество записываемых сообщений. Чтобы видеть все сообщения логгирования, попробуйте запустить ваш сервис следующим образом:
node minimal-plugin.js --seneca.log.all
Неожиданно? Конечно, вы можете также фильтровать логи:
node minimal-plugin.js --seneca.log.all | grep plugin:define
Из логов видно, что Seneca загружает множество встроенных плагинов, таких как basic
, transport
, web
и mem-store
. Эти плагины предоставляют базовые возможности для создания микросервисов. Также вы должны заметить плагин minimal_plugin
.
Теперь давайте добавим несколько режимов работы этому плагину:
function math(опции) {
this.add('role:math,cmd:sum', function (msg, respond) {
respond(null, { answer: msg.left + msg.right })
});
``` этот.add('роль:math,cmd:произведение', function (msg, respond) {
respond(null, { ответ: msg.left * msg.right })
})
}
require('seneca')()
.use(математика)
.act('роль:math,cmd:сумма,left:1,right:2', console.log)
Запустите файл math-plugin.js, чтобы получить следующий вывод:
null { ответ: 3 }
Посмотрите на запись в логах:
{
"actid": "7ubgm65mcnfl/uatuklury90r",
"msg": {
"роль": "math",
"cmd": "сумма",
"left": 1,
"right": 2,
"meta$": {
"id": "7ubgm65mcnfl/uatuklury90r",
"tx": "uatuklury90r",
"pattern": "cmd:сумма,роль:math",
"action": "(bjx5u38uwyse)",
"plugin_name": "math",
"plugin_tag": "-",
"prior": {
"chain": [],
"entry": true,
"depth": 0
},
"start": 1483587274794,
"sync": true
},
"plugin$": {
"name": "math",
"tag": "-"
},
"tx$": "uatuklury90r"
},
"entry": true,
"prior": [],
"meta": {
"plugin_name": "math",
"plugin_tag": "-",
"plugin_fullname": "math",
"raw": {
"роль": "math",
"cmd": "сумма"
},
"sub": false,
"client": false,
"args": {
"роль": "math",
"cmd": "сумма"
},
"rules": {},
"id": "(bjx5u38uwyse)",
"pattern": "cmd:сумма,роль:math",
"msgcanon": {
"cmd": "сумма",
"роль": "math"
},
"priorpath": ""
},
"client": false,
"listen": false,
"transport": {},
"kind": "act",
"case": "OUT",
"duration": 35,
"result": {
"ответ": 3
},
"level": "debug",
"plugin_name": "math",
"plugin_tag": "-",
"pattern": "cmd:сумма,роль:math",
"when": 1483587274829
}
Все логи данного плагина автоматически получают атрибут plugin
.
В мире Seneca мы организуем различные режимы работы с помощью плагинов, что делает логирование и отладку проще, затем вы можете объединять несколько плагинов в различные микросервисы. В следующих разделах мы создадим сервис math
.Плагины выполняют некоторые начальные операции, такие как подключение к базе данных, но вам не нужно выполнять эти операции внутри основной функции плагина. Основная функция предназначена для синхронного выполнения, так как она просто определяет плагин. На самом деле, вы не должны вызывать метод seneca.act
внутри основной функции; следует использовать только метод seneca.add
.
Для инициализации плагина вам нужно определить специальный шаблон init: <plugin-name>
, который будет последовательно вызываться для каждого плагина. Функция init
должна вызвать её callback-функцию без ошибок, если инициализация плагина завершается неудачей, то процесс Node.js немедленно завершится. Все операции инициализации плагина должны быть завершены до выполнения любых действий.
Чтобы продемонстрировать инициализацию, давайте добавим простое пользовательское логирование в плагин math
. Когда плагин запускается, он открывает файл логов и записывает все логи действий в этот файл. Файл должен успешно открыться и быть доступен для записи; если это невозможно, запуск микросервиса должен завершиться неудачей.
const fs = require('fs')
function math(options) {
// Логирующая функция, созданная через init функцию
var log
// Соединяем все режимы вместе для удобства поиска
this.add('role:math,cmd:sum', sum)
this.add('role:math,cmd:product', product)
}
``` // Это специальное инициализационное действие
this.add('init:math', init)
function init(msg, respond) {
// Логируем в специфический файл
fs.open(options.logfile, 'a', function (err, fd) {
// Если файл недоступен для чтения или записи, возвращаем ошибку, что приведёт к неудачному запуску Seneca
if (err) return respond(err)
log = makeLog(fd)
respond()
})
}
function sum(msg, respond) {
var out = { answer: msg.left + msg.right }
log('сложение ' + msg.left + '+' + msg.right + '=' + out.answer + '\n')
respond(null, out)
}
function product(msg, respond) {
var out = { answer: msg.left * msg.right }
log('умножение ' + msg.left + '*' + msg.right + '=' + out.answer + '\n')
respond(null, out)
}
}
функция makeLog(fd) { return function (entry) { fs.write(fd, new Date().toISOString() + ' ' + entry, null, 'utf8', function (err) { if (err) return console.log(err)
// Убедитесь, что запись лога была синхронизирована
fs.fsync(fd, function (err) {
if (err) return console.log(err)
})
})
}
} }
require('seneca')() .use(math, { logfile: './math.log' }) .act('role:math,cmd:sum,left:1,right:2', console.log)
В приведённом выше плагине код организован таким образом, чтобы шаблоны были видны сверху, а функции определены чуть ниже. Также обратите внимание, как можно указывать путь к пользовательскому файлу журнала через опции (не забывайте, это не производственный журнал!).Инициализирующая функция `init` выполняет некоторые асинхронные операции системы файлов, поэтому она должна завершиться до выполнения любых действий. В случае ошибки весь сервис может не запуститься. Чтобы увидеть поведение при возникновении ошибки, попробуйте указать недействительный путь к файлу журнала, например `/invalid.log`.Вышеуказанный код находится в файле [math-plugin-init.js](https://github.com/pantao/getting-started-seneca/blob/master/math-plugin-init.js).
# Создание микросервиса
Теперь давайте сделаем из плагина `math` настоящий микросервис. Для начала вам потребуется организовать ваш плагин. Бизнес-логика плагина `math`, то есть те функции, которые он предоставляет, отделена от способа его взаимодействия с внешним миром. Возможно, вы захотите экспонировать веб-сервис или слушать сообщения на мессенджинговой бирже.
Размещение бизнес-логики (то есть определения плагина) в отдельном файле имеет смысл. Это идеально подходит для модулей Node.js. Создайте файл под названием [math.js](https://github.com/pantao/getting-started-seneca/blob/master/math.js) со следующим содержимым:
```javascript
module.exports = function math(options) {
this.add('role:math,cmd:sum', function sum(msg, respond) {
respond(null, { answer: msg.left + msg.right })
})
this.add('role:math,cmd:product', function product(msg, respond) {
respond(null, { answer: msg.left * msg.right })
})
this.wrap('role:math', function (msg, respond) {
msg.left = Number(msg.left).valueOf()
msg.right = Number(msg.right).valueOf()
this.prior(msg, respond)
})
}
Затем вы можете добавить этот микросервис в систему, где требуется обращаться к нему, используя следующую конструкцию:
// Ниже приведены два эквивалентных способа (помните ли вы два аргумента метода `seneca.use`, который мы рассматривали ранее?)
require('seneca')()
.use(require('./math.js'))
.act('role:math,cmd:sum,left:1,right:2', console.log)
``````markdown
require('seneca')()
.use('math') // Найдёт './math.js' в текущей директории
.act('role:math,cmd:sum,left:1,right:2', console.log)
Метод seneca.wrap
может совпадать с набором шаблонов и использовать одинаковый расширяемый функционал действий для всех совпавших шаблонов, что аналогично эффекту, который можно достичь путём ручной вызова seneca.add
для каждого шаблона. Этот метод требует двух параметров:
pin
: Шаблон для совпаденияaction
: Расширяющая функция действияШаблон pin
может совпадать с множеством других шаблонов, например, role:math
совпадает как с role:math, cmd:sum
, так и с role:math, cmd:product
.
В примерах выше, в последнем вызове wrap
функции, мы гарантируем, что любое сообщение, переданное в role:math
, будет иметь значения left
и right
в виде чисел, даже если были переданы строки, они будут автоматически преобразованы в числа.
Иногда полезно знать, какие операции были переопределены в экземпляре Seneca. Вы можете добавить параметр --seneca.print.tree
при запуске приложения. Создайте файл math-tree.js со следующим содержанием:
require('seneca')()
.use('math')
Затем выполните его:
❯ node math-tree.js --seneca.print.tree
{"kind":"notice","notice":"hello seneca abs0eg4hu04h/1483589278500/65316/3.2.2/-","level":"info","when":1483589278522}
(node:65316) DeprecationWarning: 'root' is deprecated, use 'global'
Seneca action patterns for instance: abs0eg4hu04h/1483589278500/65316/3.2.2/-
├─┬ cmd:sum
│ └─┬ role:math
│ └── # math, (15fqzd54pnsp),
│ # math, (qqrze3ub5vhl), sum
└─┬ cmd:product
└─┬ role:math
└── # math, (qnh86mgin4r6),
# math, (4nrxi5f6sp69), product
```Вы видите множество пар ключ/значение, отображённых в виде дерева, где все функции действий имеют формат `#plugin, (action-id), function-name`.
Однако до настоящего времени все операции находятся в одном процессе. Давайте создадим файл [math-service.js](https://github.com/pantao/getting-started-seneca/blob/master/math-service.js) со следующим содержанием:
```javascript
require('seneca')()
.use('math')
.listen()
Запустив этот скрипт, вы запустите наш микросервис, который начнёт прослушивание HTTP-запросов через порт 10101
. Это не веб-сервер; в данном случае HTTP используется как механизм передачи сообщений.
Вы можете получить доступ к следующему адресу: http://localhost:1cq101/act?role=math&cmd=sum&left=1&right=2, чтобы увидеть результат, либо использовать команду curl
:
curl -d '{"role":"math","cmd":"sum","left":1,"right":2}' http://localhost:10101/act
Оба способа вернут вам результат:
{"answer":3}
Далее вам потребуется клиент для микросервиса math-client.js:
require('seneca')()
.client()
.act('role:math,cmd:sum,left:1,right:2', console.log)
Откройте новый терминал и выполните этот скрипт:
null { answer: 3 } { id: '7uuptvpf8iff/9wfb26kbqx55',
accept: '043di4pxswq7/1483589685164/65429/3.2.2/-',
track: undefined,
time:
{ client_sent: '0',
listen_recv: '0',
listen_sent: '0',
client_recv: 1483589898390 } }
```В Seneca мы создаем микросервис с помощью метода `seneca.listen`, а затем используем метод `seneca.client` для взаимодействия с этим микросервисом. В приведенном выше примере используются значения по умолчанию конфигурации Seneca, такие как использование протокола HTTP для прослушивания порта `10101`. Однако методы `seneca.listen` и `seneca.client` принимают следующие параметры для настройки:
- **address**: Адрес сервера.
- **port**: Номер порта.
- **protocol**: Протокол связи (например, `http`, `https`).
Метод `seneca.listen` также может принимать объект конфигурации, который позволяет настроить различные параметры прослушивания. Метод `seneca.client` используется для создания клиента, который будет взаимодействовать с микросервисом, и также принимает параметры для настройки соединения.- `port`: опциональное число, представляющее номер порта;
- `host`: опциональная строка, представляющая имя хоста или IP-адрес;
- `spec`: опциональный объект, содержащий полную спецификацию;
> **Примечание**: На системах Windows, если значение `host` не указано, по умолчанию будет использоваться `0.0.0.0`, что не имеет практического применения. Вы можете установить `host` равным `localhost`.
Если порты и хосты совпадают, то `client` может общаться с `listen`:
- seneca.client(8080) → seneca.listen( Yöntem:8080)
- seneca.client(8080, '192.168.0.2') → seneca.listen(8080, '192.168.0.2')
- seneca.client({ port: 8080, host: '192.168.0.2' }) → seneca.listen({ port: 8080, host: '192.168.0.2' })
Функция **безопасной передачи данных**, предоставляемая Seneca, позволяет вам сосредоточиться на бизнес-логике без необходимости знать детали передачи сообщений или какие сервисы получают эти сообщения. Эти детали можно указывать в коде или конфигурации сервиса, например, код плагина `math.js` никогда не требует изменения при изменении способа передачи данных. Хотя протокол `HTTP` очень удобен, но он не всегда является лучшим выбором. Другой часто используемый протокол — это `TCP`. Мы можем легко использовать протокол `TCP`, чтобы передавать данные. Примеры двух файлов:
[math-service-tcp.js](https://github.com/pantao/getting-started-seneca/blob/master/math-service-tcp.js):
```javascript
require('seneca')()
.use('math')
.listen({type: 'tcp'})
Исправлено:
Yöntem:8080
заменено на `seneca.listen(8080)````javascript
require('seneca')()
.client({type: 'tcp'})
.act('role:math,cmd:sum,left:1,right:2', console.log)
По умолчанию, `client/listen` не указывают, какие сообщения отправляются куда. Если локально определены шаблоны, они будут отправлены в локальные шаблоны; в противном случае все сообщения будут отправлены на сервер. Вы можете использовать конфигурацию для определения, какие сообщения отправляются на конкретные службы. Это можно сделать с помощью параметра `pin`.
Давайте создадим приложение, которое будет отправлять все сообщения с ролью `role:math` через `TCP` на службу, а остальные сообщения — на локальную машину.
[math-pin-service.js](https://github.com/pantao/getting-started-seneca/blob/master/math-pin-service.js):
```javascript
require('seneca')()
.use('math')
// слушаем сообщения с ролью math
// важно: должно совпадать с клиентом
.listen({ type: 'tcp', pin: 'role:math' })
require('seneca')()
// локальный шаблон
.add('say:hello', function (msg, respond) { respond(null, { text: "Привет!" }) })
// отправляем шаблон role:math на сервис
// обратите внимание: должно совпадать с сервером
.client({ type: 'tcp', pin: 'role:math' })
// удалённая операция
.act('role:math,cmd:sum,left:1,right:2', console.log)
// локальная операция
.act('say:hello', console.log)
Вы можете настроить вывод журнала с использованием различных фильтров для отслеживания потока сообщений. Поддерживаемые конфигурации включают следующие параметры:- date-time
: время создания записи журнала;
seneca-id
: идентификатор процесса Seneca;level
: уровень логгирования (DEBUG
, INFO
, WARNING
, ERROR
, FATAL
);type
: тип записи, например act
, plugin
;plugin
: имя плагина, если действие не относится к плагину, то используется root$
;case
: событие записи: IN
, ADD
, OUT
и т.д.;action-id/transaction-id
: идентификатор трассировки, который остаётся постоянным в сети;pin
: шаблон действия;message
: входящий/выходящий контент сообщения. Если вы запустите указанный выше процесс с использованием --seneca.log.all
, то будут отображены все логи. Если же вам требуется видеть только логи плагина math
, можно использовать следующий способ запуска сервиса:node math-pin-service.js --seneca.log=plugin:math
Seneca не является веб-фреймворком. Однако, вам всё равно потребуется соединить его с вашими веб-службами API. Важно помнить, что не следует открывать внутренние модели поведения внешним пользователям — это плохая практика безопасности. Вместо этого следует определять набор моделей API, таких как использование атрибута role: api
. Это позволит вам связывать эти модели с вашими внутренними микросервисами.
Ниже представлен пример нашего определения файла api.js:
module.exports = function api(options) {
var validOps = { sum: 'sum', product: 'product' };
``` этот.add('роль:api,path:calculate', функция (msg, respond) {
var операция = msg.args.params.operation
var левый = msg.args.query.left
var правый = msg.args.query.right
этот.act('роль:математика', {
cmd: validOps[операция],
левый: левый,
правый: правый,
}, respond)
})
этот.add('init:api', функция (msg, respond) {
этот.act('роль:веб',{routes:{
префикс: '/api',
пин: 'роль:api,path:*',
карта: {
calculate: { GET:true, суффикс:'/{operation}' }
}
}}, respond)
})
}
Затем мы используем hapi
в качестве веб-фреймворка и создаем hapi-app.js приложение:
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const конфиг = {
адаптер: require('seneca-web-adapter-hapi'),
контекст: (() => {
const сервер = новый Hapi.Server();
сервер.connection({
порт: 3000
});
сервер.route({
путь: '/routes',
метод: 'get',
хэндлер: (запрос, ответ) => {
const маршруты = сервер.table()[0].table.map(маршрут => {
return {
путь: маршрут.path,
метод: маршрут.method.toUpperCase(),
описание: маршрут.settings.description,
тэги: маршрут.settings.tags,
vhost: маршрут.settings.vhost,
cors: маршрут.settings.cors,
jsonp: маршрут.settings.jsonp,
}
})
ответ(маршруты)
}
});
``` вернуть сервер;
})();
};
const сенека = Seneca()
.use(SenecaWeb, конфиг)
.use('математика')
.use('апи')
.готов(() => {
const сервер = сенека.export('web/контекст')();
сервер.start(() => {
сервер.log('сервер запущен по адресу:' + сервер.info.uri);
});
});
---
После запуска `hapi-app.js`, обратитесь к <http://localhost:3000/routes>, чтобы увидеть следующую информацию:
```json
[
{
"путь": "/routes",
"метод": "GET",
"cors": false
},
{
"путь": "/api/calculate/{operation}",
"метод": "GET",
"cors": false
}
]
```Это указывает на то, что мы успешно обновили маршруты в приложении `hapi`. Обращение к <http://localhost:3000/api/calculate/sum?left=1&right=2> приведёт к получению ответа:
```json
{"response":3}
В примере выше мы непосредственно загружаем плагин math
в экземпляр seneca
. Однако более целесообразно использовать метод client
, как показано в файле hapi-app-client.js:
...
const seneca = Seneca()
.use(SenecaWeb, config)
.use('api')
.client({type: 'tcp', pin: 'role:math'})
.ready(() => {
const server = seneca.export('web/context')();
server.start(() => {
server.log('server started at address:' + server.info.uri);
});
});
Мы не регистрируем плагин math
, а используем метод client
, отправляющий запрос role:math
сервису math-pin-service.js
через соединение tcp
.
Примечание: никогда не создавайте тела сообщений на основе внешних входных данных; всегда создавайте их явно внутри системы, это поможет защититься от атак внедрения.
В методе инициализации используется операция с ролью role:web
, которая определяет правила соответствия URL-адреса и модели. У этого есть следующие параметры:
prefix
: префикс URLpin
: набор моделей, которые требуется отобразитьmap
: список свойств шаблонов pin
, которые будут использоваться в качестве конечной точки URLВаш URL будет начинаться с /api/
.роль: api, путь: *
этот пин
указывает на соответствие любых моделей с ключом роль="api"
, а также с определенным значением path
. В данном случае единственная модель, удовлетворяющая этому условию — роль: api, путь: расчёт
. map
свойство является объектом, который имеет свойство calculate
, соответствующий URL начинается с: /api/calculate
.
При этом, значение calculate
представляет собой объект, указывающий, что метод HTTP GET
разрешён, и URL должен иметь параметризованный суффикс (как в правилах маршрутизации Hapi).
Поэтому ваш полный адрес будет выглядеть как /api/calculate/{operation}
.
Значения других сообщений будут получены из объекта запроса URL или JSON-тела запроса. В данном примере, так как используется метод GET
, то нет тела запроса.
SenecaWeb
будет использовать msg.args
для описания запроса, который включает:
body
: часть payload
HTTP-запроса;query
: строки запроса;params
: параметры пути запроса.Теперь запустите микрослужбу, которую мы создали ранее:
node math-pin-service.js --seneca.log=plugin:math
И затем запустите наше приложение:
node hapi-app.js --seneca.log=plugin:web,plugin:api
Перейдите по следующим адресам:
{"answer":6}
{"answer":5}
seneca-entity предоставляет простое абстрактное представление данных (ORM), основанное на следующих действиях:
load
: загрузка сущности по её идентификатору;save
: создание или обновление сущности (если предоставлен идентификатор);list
: вывод всех сущностей, удовлетворяющих условиям запроса;remove
: удаление сущности по её идентификатору.Соответствующие им шаблоны команд:
load
: role:entity,cmd:load,name:<entity-name>
;save
: role:entity,cmd:save,name:<entity-name>
;list
: role:entity,cmd:list,name:<entity-name>
;remove
: role:entity,cmd:remove,name:<entity-name>
.Любой плагин, реализующий эти паттерны, может использоваться для предоставления доступа к базе данных (например, MySQL).
Когда сохранение данных осуществляется с помощью одного и того же механизма, что и все остальное, разработка микросервисов становится проще, а этот механизм — это паттерн совпадения сообщений.Поскольку прямое использование паттерна сохранения данных может стать однообразным, объекты seneca
также предоставляют более знакомый интерфейс в стиле ActiveRecord
. Чтобы создать объект записи, вызовите метод seneca.make()
. Объект записи имеет методы load$
, save$
, list$
и remove$
(все методы имеют суффикс $
, чтобы избежать конфликтов с полями данных), а сами поля данных являются просто свойствами объекта.Установите seneca-entity
через npm
, затем используйте метод seneca.use()
для загрузки его в ваш экземпляр seneca
.
Теперь давайте создадим простую сущность данных, которая будет хранить детали книги.
Файл book.js
const seneca = require('seneca')();
seneca.use('basic').use('entity');
const book = seneca.make('book');
book.title = 'Действие в Seneca';
book.price = 9.99;
// Отправка сообщения role:entity, cmd:save, name:book
book.save$(console.log);
В примере выше мы также использовали seneca-basic, который является зависимостью для seneca-entity
.
После выполнения указанного выше кода вы можете видеть следующий лог:
❯ node book.js
null $-/-/book;id=byo81d;{title:Действие в Seneca, price:9.99}
Внутри Seneca есть mem-store, что позволяет нам выполнять полную операцию сохранения данных без использования каких-либо других баз данных (хотя, это не является истинной персистентностью).
Поскольку сохранение данных всегда происходит с использованием одного и того же набора шаблонов сообщений, можно очень легко взаимодействовать с базой данных, например, вы можете использовать MongoDB во время разработки, а после завершения разработки использовать PostgreSQL в продакшене.Теперь давайте создадим простой интернет-магазин книг, где мы можем быстро добавлять новые книги, получать информацию о книге и совершать её покупку.book-store.js```javascript module.exports = function(options) {
// Из базы данных выбирается книга с ID msg.id
, мы используем метод load$
this.add('role:store, get:book', function(msg, respond) {
this.make('book').load$(msg.id, respond);
});
// В базу данных добавляется новая книга со значениями msg.data
, мы используем метод data$
this.add('role:store, add:book', function(msg, respond) {
this.make('book').data$(msg.data).save$(respond);
});
// Создается новый заказ на покупку (в реальных системах это часто происходит при нажатии кнопки "Купить" на странице товара),
// сначала выбирается книга с ID msg.id
. Если выбор завершается ошибкой, то она возвращается сразу же,
// если же все хорошо, то информация книги копируется в сущность purchase
и сохраняется как новый заказ.
// После этого отправляется сообщение role:store,info:purchase
(хотя ответ на него нам не требуется),
// которое информирует всю систему о том, что был создан новый заказ, но нас не интересует, кто его получит.
this.add('role:store, cmd:purchase', function(msg, respond) {
this.make('book').load$(msg.id, function(err, book) {
if (err) return respond(err);
this
.make('purchase')
.data$({
когда: Date.now(),
bookId: book.id,
название: book.title,
цена: book.price,
})
.save$(function(err, purchase) {
if (err) return respond(err);
this.act('role:store,info:purchase', {
заказ: purchase
});
respond(null, purchase);
});
});
});
}
``` // Наконец, реализуется режим role:store, info:purchase
, который просто выводит информацию,
// объект `seneca.log` предоставляет методы `debug`, `info`, `warn`, `error`, `fatal` для записи логов различных уровней.
this.add('role:store, info:purchase', function(msg, respond) {
this.log.info('заказ:', msg.заказ);
respond();
});
};Далее можно создать простой тестовый скрипт для проверки работы программы:
boot-store-test.js```javascript
// Используйте встроенный модуль assert
Node
const assert = require('assert');
const seneca = require('seneca')() .use('basic') .use('entity') .use('book-store') .error(assert.fail);
// Добавьте книгу addBook();
function addBook() { seneca.act( 'role:store,add:book,data:{title:Action in Seneca,price:9.99}', function(err, savedBook) {
this.act(
'role:store,get:book', {
id: savedBook.id
},
function(err, loadedBook) {
assert.equal(loadedBook.title, savedBook.title);
покупка(loadedBook);
}
);
}
); }
function покупка(book) { seneca.act( 'role:store,cmd:purchase', { id: book.id }, function(err, purchase) { assert.equal(purchase.bookId, book.id); } ); }
Выполнение этого теста:
```bash
❯ node book-store-test.js
["purchase",{"entity$":"-/-/purchase","when":1483607360925,"bookId":"a2mlev","title":"Action in Seneca","price":9.99,"id":"i28xoc"}]
В производственной системе мы можем иметь отдельный сервис для мониторинга заказов данных, а не просто выводить логи, как это было сделано выше. Теперь создадим новый сервис для сбора данных о заказах:
const stats = {};
require('seneca')()
.add('role:store,info:purchase', function(msg, respond) {
const id = msg.purchase.bookId;
stats[id] = stats[id] || 0;
stats[id]++;
console.log(stats);
respond();
})
.listen({
port: 9003,
host: 'localhost',
pin: 'role:store,info:purchase'
});
Затем обновите файл book-store-test.js
:
const seneca = require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.client({port:9003,host: 'localhost', pin:'role:store,info:purchase'})
.error(assert.fail);
Функция покупка
переименована в purchase
.
function purchase(book) {
seneca.act(
'role:store,cmd:purchase', {
id: book.id
},
function(err, purchase) {
assert.equal(purchase.bookId, book.id);
}
);
}
```Теперь при появлении нового заказа он будет отправлен в сервис мониторинга заказов.
## Интеграция всех служб вместе
После выполнения всех этих шагов у нас уже есть четыре службы:
- [book-store-stats.js](https://github.com/pantao/getting-started-seneca/blob/master/book-store-stats.js) : собирает информацию о заказах в книжном магазине;
- [book-store-service.js](https://github.com/pantao/getting-started-seneca/blob/master/book-store-service.js) : предоставляет функциональность, связанную с книжным магазином;
- [math-pin-service.js](https://github.com/pantao/getting-started-seneca/blob/master/math-pin-service.js) : предоставляет некоторые математические службы;
- [app-all.js](https://github.com/pantao/getting-started-seneca/blob/master/app-all.js) : веб-сервис `book-store-stats` и `math-pin-service` уже существуют, поэтому можно сразу запустить их:
```bash
node math-pin-service.js --seneca.log.all
node book-store-stats.js --seneca.log.all
Теперь нам нужен сервис book-store-service
:
require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.listen({
port: 9002,
host: 'localhost',
pin: 'role:store'
})
.client({
port: 9003,
host: 'localhost',
pin: 'role:store,info:purchase'
});
Этот сервис принимает любое сообщение с ролью role:store
, но одновременно отправляет все сообщения с ролью role:store,info:purchase
в сеть, всегда помните, что конфигурация пинов для клиентов и прослушивания должна быть полностью одинаковой.
Теперь мы можем запустить этот сервис:
node book-store-service.js --seneca.log.all
Затем создайте наш app-all.js
. В первую очередь скопируйте файл api.js
в [api-all.js](_url_)
, это будет наш API.```javascript
module.exports = function api(options) {
var validOps = { sum: 'sum', product: 'product' }
this.add('role:api,path:calculate', function(msg, respond) { var operation = msg.args.params.operation; var left = msg.args.query.left; var right = msg.args.query.right; this.act('role:math', { cmd: validOps[operation], left: left, right: right }, respond); });
this.add('role:api,path:store', function(msg, respond) { let id = null; if (msg.args.query.id) id = msg.args.query.id; if (msg.args.body.id) id = msg.args.body.id;
const operation = msg.args.params.operation;
const storeMsg = {
role: 'store',
id: id
};
if ('get' === operation) storeMsg.get = 'book';
if ('purchase' === operation) storeMsg.cmd = 'purchase';
this.act(storeMsg, respond);
});
this.add('init:api', function(msg, respond) { this.act('role:web', { routes: { prefix: '/api', pin: 'role:api,path:*', map: { calculate: { GET: true, suffix: '/{operation}' }, store: { GET: true, POST: true, suffix: '/{operation}' } } } }, respond); }); };
## Последнее
Последней частью является [app-all.js](https://github.com/pantao/getting-started-seneca/blob/master/app-all.js):
```javascript
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() => {
const server = new Hapi.Server();
server.connection({
port: 3000
});
server.route({
path: '/routes',
method: 'GET',
handler: (request, reply) => {
const routes = server.table()[0].table.map(route => {
return {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp
};
});
reply(routes);
}
});
})();
``` return сервер;
})()
};
const seneca = Seneca()
.use(SenecaWeb, config)
.use('basic')
.use('entity')
.use('math')
.use('api-all')
.client({
тип: 'tcp',
пин: 'роль:math'
})
.client({
порт: 9002,
хост: 'localhost',
пин: 'роль:store'
})
.готов(() => {
const сервер = senica.export('web/context')();
сервер.start(() => {
сервер.log('сервер запущен на: ' + сервер.info.uri);
});
});
// Создание примерной книги
seneca.act(
'роль:store,добавить:книгу', {
данные: {
название: 'Действие в Seneca',
цена: 9.99
}
},
console.log
)
Запустите сервис:
node app-all.js --seneca.log.all
Из консоли вы можете видеть следующее сообщение:
null $-/-/книга;id=0r7mg7;{название:Действие в Seneca,цена:9.99}
Это указывает на успешное создание книги с ID 0r7mg7
. Теперь можно получить детали этой книги по её ID, переходя по адресу http://localhost:3000/api/store/get?id=0r7mg7 (ID случайный, поэтому ваш ID может отличаться).
Ссылка http://localhost:3000/routes позволяет просмотреть все маршруты.
Далее можно создать новый заказ покупки:
curl -d '{"id":"0r7mg7"}' -H "content-type:application/json" http://localhost:3000/api/store/purchase
{"когда":1483609872715,"книгаИд":"0r7mg7","название":"Действие в Seneca","цена":9.99,"ид":"8suhf4"}
При доступе к http://localhost:3000/api/calculate/sum?left=2&right=3 можно получить {"ответ":5}
.
require('seneca')({ некоторые_опции: 123 })
// Уже существующие плагины Seneca .use('сообщественный-плагин-0') .use('сообщественный-плагин-1', { некая_конфигурация: НЕКОТАННАЯ_КОНФИГУРАЦИЯ }) .use('сообщественный-плагин-2')
// Плагины бизнес-логики .use('проектный-модуль-плагина') .use('../репозиторий-плагинов') .use('./lib/локальный-плагин')
.слушает( ... ) .клиент( ... )
.готов( function() { // Самостоятельный скрипт после успешной инициализации Seneca });
- Порядок загрузки плагинов важен, это хорошо, поскольку позволяет иметь полный контроль над сообщениями.
### Не рекомендовано делать так
- Объединять запуск и инициализацию Seneca-приложений с запуском и инициализацией других фреймворков; всегда помните, что следует поддерживать простоту транзакций;
- Передавать экземпляр Seneca как переменную повсюду.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )