¿Protocolo de comunicación front-end estructurado Protobuf o JSON?

imagen

En el nuevo proyecto en nuestro equipo, elegimos el marco de interfaz de usuario VUE para el nuevo producto, el backend está escrito en PHP y ha estado funcionando con éxito durante 17 años.

Cuando el código comenzó a crecer, tuve que pensar en simplificar el intercambio de datos con el servidor, del que hablaré.

Sobre el backend


El proyecto es lo suficientemente grande y la funcionalidad es muy confusa, por lo tanto, el código escrito en DDD tenía ciertas estructuras de datos, eran complejas y voluminosas para cierta universalidad en el proyecto en su conjunto.

Sobre frontend


4 meses En el desarrollo del frente, utilizamos JSON como respuesta del servidor, cargado en State Vuex en un formato conveniente para nosotros. Pero para el regreso al servidor, necesitábamos convertir en la dirección opuesta, para que el servidor pudiera leer y mapear sus objetos DTO (puede parecer extraño, pero debería ser así :))

Problemas


Parece que no es nada, trabajaron con lo que es, el estado se convirtió en grandes objetos. Comenzaron a dividirse en módulos aún más pequeños, cada uno de los cuales tenía sus propios estados, mutaciones, etc. ... La API comenzó a cambiar después de nuevas tareas de los gerentes, y se hizo cada vez más difícil administrar todo esto, luego se mapeó mal, luego los campos cambiaron ...

Y aquí estamos comenzó a pensar en estructuras de datos universales en el servidor y en el frente para eliminar errores en los análisis, las asignaciones, etc.

Después de algunas búsquedas, llegamos a dos opciones:

  1. tampones de protocolo
  2. Generación automática de JS DTO del lado del servidor para el frente, con más procesamiento JSON en estos DTO.

Después de probar el lápiz, era costumbre usar Protobuf de google.

Y es por eso:

  1. Ya existe una función funcional que compila las estructuras descritas para muchas plataformas, incluidas PHP y JS.
  2. Hay un generador de documentación para estructuras creadas .proto
  3. Puede atornillar fácilmente algunas versiones para estructuras.
  4. La búsqueda de objetos se facilita al refactorizar tanto en PHP como en JS.
  5. Y otros chips como gRPC, etc. si es necesario.

Deja de hablar, veamos cómo se ve todo


No describiré cómo se ve en el lado de PHP, allí todo es prácticamente igual, los objetos son los mismos.

Le mostraré un ejemplo de un cliente simple JS y un mini servidor en Node.js.

Primero, describimos las estructuras de datos que necesitamos. Doca .

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;
}

precio.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);
}

Explicaré un poco sobre el servicio, por qué es necesario, incluso si no se usa. El servicio se describe solo por el bien de la documentación en nuestro caso, lo que acepta y lo que brinda, para que podamos sustituir los objetos necesarios. Solo es necesario para gRPC.

A continuación, el generador de código se descarga en función de las estructuras.

Y el comando de generación se ejecuta bajo 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

Más detalles en el muelle .

Después de la generación, aparecen 3 archivos JS, en los que todo se reduce a objetos, con la funcionalidad de serialización al búfer y deserialización del búfer.

price_pb.js
product_pb.js
service_pb.js

A continuación describimos el código 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());

En principio, el cliente está listo.

Expresamos en el servidor

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!');
});

¿Qué tenemos en total?


  1. Un único punto de verdad en forma de objetos generados basados ​​en estructuras que se describen una vez para muchas plataformas.
  2. No hay confusión, hay una documentación clara tanto en forma de HTML generado automáticamente como simplemente viendo archivos .proto.
  3. En todas partes, se está trabajando con entidades específicas, sin sus modificaciones, etc. (y todos sabemos que el frontend ama la mordaza :))
  4. La operación muy conveniente de este protocolo se intercambia a través de sockets web.

Por supuesto, hay un pequeño inconveniente, esta es la velocidad de serialización y deserialización, aquí hay un ejemplo.

Tomé lorem ipsum durante 10 párrafos, resultó en 5,5 kb de datos, teniendo en cuenta los objetos llenos Precio, Producto. Y manejé datos en Protobuf y JSON (de todos modos, solo llené los esquemas JSON, en lugar de los 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

Gracias a todos por su atención :)

All Articles