Подробнее о JSON RPC.
Не хочу разводить очередной холивар на эту тему. Поэтому, если вкратце, то GraphQL — это сложно, RPC — быстро, REST — некий медиум, но не хватает batch-запросов. И если у вас небольшое приложение или микросервис, то rpc, он же “вызов удаленной процедуры”, может оказаться гораздо лучше и экономичнее для вашей архитектуры, особенно если она основана на микросервисном подходе.
Итак, давайте подробнее разберем JSON RPC v2.0
Спецификация достаточно
Подробнее о JSON RPC...
Не хочу разводить очередной холивар на эту тему. Поэтому, если вкратце, то GraphQL — это сложно, RPC — быстро, REST — некий медиум, но не хватает batch-запросов. И если у вас небольшое приложение или микросервис, то rpc, он же “вызов удаленной процедуры”, может оказаться гораздо лучше и экономичнее для вашей архитектуры, особенно если она основана на микросервисном подходе.
Итак, давайте подробнее разберем JSON RPC v2.0
Спецификация достаточно проста и лаконична.
На моей практике это решение оказалось достаточно удобным и простым, но при этом мощным и расширяемым. И меня сильно удивило, что в мире node.js оно применяется крайне редко.
Суть подхода достаточно примитивна
Запрос включает в себя 4 поля:
- jsonrpc — всегда будет “2.0”, указывает версию протокола.
- method — название метода (функции), который нужно вызвать.
- params — опциональное поле, нагрузка к вызову (аргументы функции).
- id — опциональное поле, уникальный идентификатор вызова. Если вы хотите получить значение от вызванной функции, то вы должны сгенерировать id на стороне клиента и при ответе вы сможете понять, на какой именно вызов пришел ответ, сопоставив id ответа.
Если вы не отправили id, то это означает, что ответ вас не интересует и от сервера вы ничего не получите. Такой вызов называется нотификацией.
Ответ может иметь следующие поля:
- jsonrpc — всегда будет “2.0”, указывает версию протокола.
- result — тело ответа (возвращаемое значение функции).
- id — уникальный идентификатор ответа. Он нужен для того, чтобы клиент мог сопоставить, на какой запрос он получил ответ.
- error — в случае ошибки вместо result, вы получите поле error, содержащее в себе code (код ответа: по протоколу их может быть шесть) и message (человекопонятное описание ошибки).
Вишенка на торте — это Batch-запрос!
Это означает, что мы можем слепить несколько отдельных ajax запросов в один и отдать его серверу в виде массива запросов. А при получении по заданным id мы поймем, на какой запрос получили ответ.
Небольшая тонкость!
Учтите, что по протоколу сервер не гарантирует последовательность элементов в ответе, например:
> [{id:1}, {id:2}, {id:3}]
<-[{id:2}, {id:1}, {id:3},]
Будет считаться вполне нормальным ответом, т.к. время выполнения функций разное, и сервер заполняет массив ответами по мере их асинхронного выполнения.
Поэтому клиент сам должен промапить ответ и сопоставить id каждого элемента в ответе с запросом.
Давайте уже писать код!
Поскольку в последнее время я пишу на node.js и готового решения, которое бы полностью меня устроило, я не нашел, то я решил запилить простой JSON-RPC роутер для express.js без каких либо зависимостей.
https://www.npmjs.com/package/express-json-rpc-router
В документации к пакету базовые примеры уже рассмотрены. Давайте придумаем вариант посложнее.
Например, создадим сервер с тремя методами, которые будут получать данные по http от другого ресурса. Каждый метод будет проверять, что пользователь ввел валидные параметры, затем выполнять тело метода и логировать результат выполнения в файл.
Сначала мы создадим папку с файлами, инициализируем проект и подключим к нему зависимости.
cd /path/to/your/proj
mkdir medium-json-rpc
cd medium-json-rpc
yarn init -y
touch index.js
yarn add express axios body-parser express-json-rpc-router
const axios = require('axios')
const { appendFile } = require('fs')
const app = require('express')()
const bodyParser = require('body-parser')
const jsonRouter = require('../json-rpc')
function formatData(data, id = '') {
return${id}: ${Date.now()} : ${JSON.stringify(data)}\n
}
const controller = {
async getUser({ id }) {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
return response.data
},
async getPost({ id }) {
const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
return response.data
},
async getPhoto({ id }) {
const response = await axios.get(`https://jsonplaceholder.typicode.com/photos/${id}`)
return response.data
}
}
const beforeController = {
getUser({ id }) {
if (id <= 0 || id > 10) {
throw new Error('ERROR: getUser id should be between 1 and 10')
}
},
getPost({ id }) {
if (id <= 0 || id > 100) {
throw new Error('ERROR: getUserPosts id should be between 1 and 100')
}
},
getPhoto({ id }) {
if (id <= 0 || id > 5000) {
throw new Error('ERROR: getUserPhotos id should be between 1 and 5000')
}
}
}
const afterController = {
getUser({ id }, execResult) {
appendFile("users.log", formatData(execResult, id))
},
getPost({ id }, execResult) {
appendFile("posts.log", formatData(execResult, id))
},
getPhoto({ id }, execResult) {
appendFile("photos.log", formatData(execResult, id))
}
}
app.use(bodyParser.json())
app.use(jsonRouter({
methods: controller,
beforeMethods: beforeController,
afterMethods: afterController,
onError(e) {
console.log('Omg error occurred!', e)
}
}))
app.listen(3000, () => console.log('Example app listening on port 3000'))
У нас имеется три основных объекта:
methods — наши JSON-RPC методы, функции которые реализуют основную логику. Это могут быть ваши контроллеры.
beforeMethods (опциональные) — хук, который будет вызван перед выполнением метода. Здесь вы можете валидировать переданные параметры и выкидывать ошибки, которые попадут в вышестоящий обработчик. Название хука должно совпадать с названием метода, к которому этот хук относится, иначе будет вызвано исключение с сообщением, содержащим информацию о том, какой хук был назван неправильно.
afterMethods (опционально) — хук, который по своей логике похож на предыдущий. Разница только в том, что он будет вызван после выполнения метода и в качестве второго параметра получит результат выполнения. Может быть удобен для логгирования запросов.
Также предоставлен callback (onError) — функция, в которую будет передана ошибка, если она случилась. Тут можно удобно подключить логгер ошибок вроде sentry.io.
Обратите внимание, что bodyParser вызван перед роутером. Можете использовать другой вариант парсинга тела запроса, главное, чтобы в req.body мы получили параметры запроса в формате JSONRPC.
Библиотека в стадии разработки, так что если у вас есть пожелания или идеи, смело пишите в комментариях или открывайте github issues.
Всем хорошего настроения!
Статья Валерия Кузиванова: Подробнее о JSON RPC