Protobuf还是JSON结构的前端通信协议?

图片

在我们团队的新项目中,我们为新产品选择了前端框架VUE,后端是用PHP编写的,并且已经成功运行了17年。

当代码开始增长时,我不得不考虑简化与服务器的数据交换,这将在后面讨论。

关于后端


该项目足够大且功能非常混乱,因此,用DDD编写的代码具有某些数据结构,对于整个项目中的某些通用性而言,它们既复杂又庞大。

关于前端


4个月 在前端开发中,我们使用JSON作为服务器的响应,我们以方便我们的格式映射到State Vuex。但是,要返回服务器,我们需要朝相反的方向进行转换,以便服务器可以读取和映射其DTO对象(这看起来很奇怪,但应该如此:)

问题


似乎什么都没有,他们与之共事,国家成长为大物体。他们开始分解成甚至更小的模块,每个模块都有自己的状态,突变等。...在经理提出新任务之后,API开始发生变化,管理所有这些模块变得越来越困难,然后将其映射错误,然后更改字段...

在这里,我们开始考虑服务器和前端的通用数据结构以消除解析,映射等错误。

经过一番搜索,我们得出了两种选择:

  1. 协议缓冲区
  2. 服务器端JS DTO会自动生成前端,并在这些DTO中进行进一步的JSON处理。

试过笔后,习惯使用Google的Protobuf。

这就是为什么:

  1. 已经有一个功能可以为许多平台(包括PHP和JS)编译所描述的结构。
  2. 有一个用于生成结构的文档生成器.proto
  3. 您可以轻松地为结构添加一些版本控制。
  4. 在PHP和JS中重构时,都有助于对象的搜索。
  5. 如有必要,还可以使用其他芯片,例如gRPC等。

别说话了,让我们看看一切


我不会描述它在PHP方面的外观,那里的内容几乎相同,对象也相同。

我将向您展示Node.js上的简单客户端JS和小型服务器的示例。

首先,我们描述所需的数据结构。多卡

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才需要。

接下来,根据结构下载代码生成器

生成命令在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

码头上有更多细节

生成后,将出现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());

原则上,客户已准备就绪。

我们在服务器上快递

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

我们总共有什么


  1. 基于许多平台曾经描述过的结构的生成对象形式的单一事实点。
  2. 没有混乱,有清晰的文档,既有自动生成的HTML形式,也有仅查看.proto文件的形式。
  3. 到处都在与特定实体进行工作,而无需对其进行修改等(并且我们都知道前端喜欢gag :))
  4. 可通过Web套接字交换此协议的非常方便的操作。

当然有一个小的减号,这是序列化和反序列化的速度,这里是一个示例。

我用lorem ipsum进行了10段,考虑到填充的对象Price,Product,结果得出5.5kb的数据。我将数据驱动在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

谢谢大家的关注:)

All Articles