Protobuf- oder JSON-strukturiertes Front-End-Kommunikationsprotokoll?

Bild

In dem neuen Projekt in unserem Team haben wir das Frontend-Framework VUE für das neue Produkt ausgewählt, das Backend ist in PHP geschrieben und arbeitet seit 17 Jahren erfolgreich.

Als der Code zu wachsen begann, musste ich darüber nachdenken, den Datenaustausch mit dem Server zu vereinfachen, worüber ich sprechen werde.

Über das Backend


Das Projekt ist groß genug und die Funktionalität ist sehr verwirrt. Daher hatte der in DDD geschriebene Code bestimmte Datenstrukturen, sie waren komplex und umfangreich für eine gewisse Universalität im gesamten Projekt.

Über das Frontend


4 Monate Bei der Entwicklung der Front haben wir JSON als Antwort vom Server verwendet und State Vuex in einem für uns geeigneten Format zugeordnet. Für die Rückkehr zum Server mussten wir jedoch in die entgegengesetzte Richtung konvertieren, damit der Server seine DTO-Objekte lesen und zuordnen konnte (es mag seltsam erscheinen, sollte es aber sein :)).

Probleme


Es scheint nichts zu sein, sie haben mit dem gearbeitet, was ist, der Staat ist zu großen Objekten gewachsen. Sie begannen, sich in noch kleinere Module aufzuteilen, von denen jedes seine eigenen Zustände, Mutationen usw. hatte. Die API begann sich nach neuen Aufgaben von Managern zu ändern, und es wurde immer schwieriger, all dies zu verwalten, dann wurde es falsch zugeordnet, dann änderten sich die Felder ...

Und hier sind wir begann über universelle Datenstrukturen auf dem Server und auf der Vorderseite nachzudenken, um Fehler beim Parsen, Zuordnen usw. zu beseitigen.

Nach einiger Suche kamen wir zu zwei Optionen:

  1. Protokollpuffer
  2. Serverseitige automatische JS-DTO-Generierung für die Front mit weiterer JSON-Verarbeitung in diesen DTOs.

Nach dem Ausprobieren des Stifts war es üblich, Protobuf von Google zu verwenden.

Und deshalb:

  1. Es gibt bereits eine Funktion, die die beschriebenen Strukturen für viele Plattformen kompiliert, einschließlich für PHP und für JS.
  2. Es gibt einen Dokumentationsgenerator für erstellte Strukturen .proto
  3. Sie können leicht eine Versionierung für Strukturen schrauben.
  4. Die Suche nach Objekten wird beim Refactoring in PHP und JS erleichtert.
  5. Und andere Chips wie gRPC usw., falls erforderlich.

Hör auf zu reden, mal sehen, wie das alles aussieht


Ich werde nicht beschreiben, wie es auf der PHP-Seite aussieht, dort ist alles ziemlich gleich, die Objekte sind gleich.

Ich werde Ihnen ein Beispiel eines einfachen Client-JS und eines Mini-Servers auf Node.js zeigen.

Zunächst beschreiben wir die Datenstrukturen, die wir benötigen. 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;
}

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

Ich werde ein wenig über den Service erklären, warum er benötigt wird, wenn er nicht einmal genutzt wird. Der Service wird in unserem Fall nur zur Dokumentation beschrieben, was er akzeptiert und was er gibt, damit wir die erforderlichen Objekte ersetzen können. Es wird nur für gRPC benötigt.

Als nächstes wird der Codegenerator basierend auf den Strukturen heruntergeladen .

Der Generierungsbefehl wird unter JS ausgeführt.

./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

Weitere Details im Dock .

Nach der Generierung werden 3 JS-Dateien angezeigt, in denen alles auf Objekte reduziert ist, mit der Funktionalität der Serialisierung in den Puffer und der Deserialisierung aus dem Puffer.

price_pb.js
product_pb.js
service_pb.js

Als nächstes beschreiben wir den JS-Code.

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());

Grundsätzlich ist der Kunde bereit.

Wir drücken auf dem Server aus

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

Was haben wir insgesamt


  1. Ein einziger Wahrheitspunkt in Form von generierten Objekten, die auf Strukturen basieren, die für viele Plattformen einmal beschrieben wurden.
  2. Es gibt keine Verwirrung, es gibt eine klare Dokumentation sowohl in Form von automatisch generiertem HTML als auch nur zum Anzeigen von .proto-Dateien.
  3. Überall wird mit bestimmten Entitäten gearbeitet, ohne deren Modifikationen usw. (und wir alle wissen, dass das Frontend Gag liebt :))
  4. Die sehr bequeme Bedienung dieses Protokolls wird über Web-Sockets ausgetauscht.

Es gibt natürlich ein kleines Minus, dies ist die Geschwindigkeit der Serialisierung und Deserialisierung, hier ein Beispiel.

Ich habe Lorem Ipsum für 10 Absätze verwendet, es stellte sich heraus, dass 5,5 KB Daten unter Berücksichtigung der gefüllten Objekte Preis, Produkt. Und ich habe Daten zu Protobuf und JSON gefahren (trotzdem nur JSON-Schemata anstelle von Protobuf-Objekten ausgefüllt)

   

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

Vielen Dank für Ihre Aufmerksamkeit :)

All Articles