No novo projeto de nossa equipe, escolhemos a estrutura de front-end VUE para o novo produto, o back-end é escrito em PHP e trabalha com sucesso há 17 anos.Quando o código começou a crescer, tive que pensar em simplificar a troca de dados com o servidor, sobre o qual falarei.Sobre back-end
O projeto é grande o suficiente e a funcionalidade é muito confusa; portanto, o código escrito em DDD tinha certas estruturas de dados, elas eram complexas e volumosas para alguma universalidade do projeto como um todo.Sobre o frontend
4 meses No desenvolvimento da frente, usamos JSON como resposta do servidor, mapeamos no State Vuex em um formato conveniente para nós. Mas, para o retorno ao servidor, precisávamos converter na direção oposta, para que o servidor pudesse ler e mapear seus objetos DTO (pode parecer estranho, mas deve ser assim :))Problemas
Parece não ser nada, eles trabalharam com o que é, o estado cresceu para objetos grandes. Eles começaram a dividir-se em módulos ainda menores, cada qual com seus próprios estados, mutações, etc ... A API começou a mudar depois de novas tarefas de gerentes, e tornou-se mais e mais difícil de gerir tudo isso, errado, então ele foi mapeado, os campos mudou ...E aqui estamos nós começou a pensar em estruturas de dados universais no servidor e na frente para eliminar erros em análises, mapeamentos etc.Após algumas pesquisas, chegamos a duas opções:- buffers de protocolo
- Geração automática de JS DTO do lado do servidor para a frente, com processamento JSON adicional nesses DTOs.
Depois de experimentar a caneta, era costume usar o Protobuf do google.E é por causa disso:- Já existe um funcional que compila as estruturas descritas para muitas plataformas, inclusive para PHP e JS.
- Existe um gerador de documentação para estruturas criadas .proto
- Você pode facilmente danificar algumas versões para estruturas.
- A pesquisa de objetos é facilitada ao refatorar em PHP e JS.
- E outros chips como gRPC, etc., se necessário.
Pare de falar, vamos ver como tudo fica
Não vou descrever como fica do lado do PHP, tudo fica igual, os objetos são iguais.Vou mostrar um exemplo de um cliente JS simples e um mini servidor no Node.js.Primeiro, descrevemos as estruturas de dados que precisamos. Doca .product.protosyntax = "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.protosyntax = "proto3";
package api;
message Price {
float value = 1;
uint32 tax = 2;
}
service.protosyntax = "proto3";
package api;
import "product.proto";
service ApiService {
rpc getById (Product.Id) returns (Product);
}
Vou explicar um pouco sobre o serviço, por que ele é necessário, se não usado. O serviço é descrito apenas para fins de documentação em nosso caso, o que ele aceita e o que oferece, para que possamos substituir os objetos necessários. Só é necessário para o gRPC.Em seguida, o gerador de código é baixado com base nas estruturas.E o comando de geração é executado em 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
Mais detalhes no banco dos réus .Após a geração, aparecem 3 arquivos JS, nos quais tudo é reduzido a objetos, com a funcionalidade de serialização para o buffer e desserialização do buffer.price_pb.jsproduct_pb.jsservice_pb.jsEm seguida, descrevemos o código JS.import { Product } from '../proto/product_pb';
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();
const data = Product.deserializeBinary(result);
console.log(data.toObject());
Em princípio, o cliente está pronto.Expressamos no servidorconst 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) {
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);
res.send(Buffer.from(product.serializeBinary()));
});
app.listen(3008, function () {
console.log('Example app listening on port 3008!');
});
O que temos no total
- Um único ponto de verdade na forma de objetos gerados com base em estruturas que são descritas uma vez para muitas plataformas.
- Não há confusão, há documentação clara, tanto na forma de HTML gerado automaticamente quanto na visualização de arquivos .proto.
- Em todos os lugares, o trabalho está em andamento com entidades específicas, sem suas modificações, etc. (e todos sabemos que o frontend adora vomitar :))
- A operação muito conveniente desse protocolo é trocada por soquetes da web.
É claro que há um pequeno sinal de menos, essa é a velocidade de serialização e desserialização, aqui está um exemplo.Tomei lorem ipsum por 10 parágrafos, resultou em 5,5kb de dados, levando em consideração os objetos preenchidos Preço, Produto. E eu dirigi dados sobre Protobuf e JSON (todos os mesmos, apenas preenchi esquemas JSON, em vez de objetos 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
Obrigado a todos pela atenção :)