- PVSM.RU - https://www.pvsm.ru -
В новом проекте в нашей команде мы выбрали frontend framework VUE для нового продукта, бэкенд написан на PHP, и уже как 17 лет успешно работает.
Когда код начал разрастаться, нужно было думать над упрощением обмена данных с сервером, об этом я и расскажу.
Проект достаточно большой, и функционал очень замороченный, следовательно код написанный на DDD имел определенные структуры данных, они были сложные и объемные для некой универсальности в проекте в целом.
4мес. разработки фронта мы использовать JSON в качестве ответа от сервера, мапили в State Vuex в удобном нам формате. Но для отдачи на сервер нам требовалось преобразовывать в обратную сторону, чтобы сервер смог прочитать и замапить свои DTO объекты (может показаться странным, но так надо :) )
Вроде бы ничего, работали с тем что есть, состояние разрасталось до объектов больших размеров. Начали разбивать на еще меньшие модули в каждом из которых были свои состояния, мутации и т.п… API стало меняться вслед новым задачам от менеджеров, и все сложнее стало управлять всем этим, то там замапили не так, то поля изменились…
И тут мы начали думать об универсальных структурах данных на сервере и фронте чтобы исключить ошибки в парсингах, мапингах и т.п.
После некоторых поисков, мы пришли к двум вариантам:
После пробы пера, было принято использовать Protobuf от google.
И вот почему:
Как это выглядит на стороне PHP я не буду описывать, там примерно все тоже самое, объекты те же.
Покажу на примере простого клиентского JS и мини сервера на Node.js.
Для начала описываем структуры данных которые нам потребуется. Дока [2].
product.proto
syntax = "proto3";
package api;
import "price.proto";
message Product {
message Id {
uint32 value = 1;
}
Id id = 1;
string name = 2;
string text = 3;
string url = 4;
Price price = 5;
}
price.proto
syntax = "proto3";
package api;
message Price {
float value = 1;
uint32 tax = 2;
}
service.proto
syntax = "proto3";
package api;
import "product.proto";
service ApiService {
rpc getById (Product.Id) returns (Product);
}
Поясню немного про сервис, зачем он нужен, если даже не используется. Сервис описывается только ради документации в нашем случае, что принимает и что отдает, чтобы мы могли подставлять нужные объекты. Он нужен только для gRPC.
Далее скачивается генератор [3] кода на основании структур.
И запускается команда генерации под JS.
./protoc --proto_path=/Users/user/dev/habr_protobuf/public/proto --js_out=import_style=commonjs,binary:/Users/user/dev/habr_protobuf/src/proto/ /Users/user/dev/habr_protobuf/public/proto/*.proto
Подробнее в доке [4].
После генерации появляется 3 JS файла, в которых уже все приведено к объектам, с функционалом сериализации в буфер и десериализации из буфера.
price_pb.js
product_pb.js
service_pb.js
Далее описываем уже JS код.
import { Product } from '../proto/product_pb';
// В структуре мы описали что ждем от клиента Product.Id
const instance = new Product.Id().setValue(12345);
let message = instance.serializeBinary();
let response = await fetch('http://localhost:3008/api/getById', {
method: 'POST',
body: message
});
let result = await response.arrayBuffer();
// Ждем от сервера объект Product, его и подставляем, он сам все сделает,
// распарсит в нужные структуры и сабструктуры.
const data = Product.deserializeBinary(result);
console.log(data.toObject());
В принципе клиент готов.
На сервере заюзаем Express
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
// подключаем все нужные объекты для их заполнения и отправки на клиент.
const Product = require('./src/proto/product_pb').Product;
const Price = require('./src/proto/price_pb').Price;
// обрабатываем буфер с клиента, т.к данные обмениваются только через него.
app.use (function(req, res, next) {
let data = [];
req.on('data', function(chunk) {
data.push(chunk);
});
req.on('end', function() {
if (data.length <= 0 ) return next();
data = Buffer.concat(data);
console.log('Received buffer', data);
req.raw = data;
next();
})
});
app.post('/api/getById', function (req, res) {
// От клиента мы ожидаем Product.Id, его и подставляем
const prId = Product.Id.deserializeBinary(req.raw);
const id = prId.toObject().value;
// Далее куча "бизнес логики" и мы красивыми объектами оформляем ответ
const product = new Product();
product.setId(new Product.Id().setValue(id));
product.setName('Sony PSP');
product.setUrl('http://mysite.ru/product/psp/');
const price = new Price();
price.setValue(35500.00);
price.setTax(20);
product.setPrice(price);
// и отправляем обратно клиенту сериализованный объект Product
res.send(Buffer.from(product.serializeBinary()));
});
app.listen(3008, function () {
console.log('Example app listening on port 3008!');
});
Есть конечно небольшой минус, это скорость сериалиации и десериализации, вот пример.
Я взял lorem ipsum на 10 абзацев, получилось 5.5кб данных с учетом заполненных объектов Price, Product. И погонял данные по Protobuf и JSON (все тоже самое только заполненные JSON схемы, вместо Protobuf объектов)
без учета самих запросов
Protobuf parsing
client
2.804999ms
1.8150000ms
0.744999ms
server
1.993ms
0.495ms
0.412ms
JSON
client
0.654999ms
0.770000ms
0.819999ms
server
0.441ms
0.307ms
0.242ms
Всем спасибо за внимание :)
Автор: bagzon
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/347743
Ссылки в тексте:
[1] protocol-buffers: https://developers.google.com/protocol-buffers
[2] Дока: https://developers.google.com/protocol-buffers/docs/proto3
[3] скачивается генератор: https://github.com/protocolbuffers/protobuf/releases
[4] доке: https://developers.google.com/protocol-buffers/docs/reference/javascript-generated
[5] Источник: https://habr.com/ru/post/489916/?utm_campaign=489916&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.