示例

英文原地址

使用.proto文件

可以使用完整版的库来加载现有的.proto文件,它会解析和编译定义以准备使用(基于反射的)消息类:

// awesome.proto
package awesomepackage;
syntax = "proto3";

message AwesomeMessage {
    string awesome_field = 1; // becomes awesomeField
}
1
2
3
4
5
6
7
protobuf.load("awesome.proto", function(err, root) {
    if (err)
        throw err;

    // 获得 message 类型
    var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");

    // 有效载荷
    var payload = { awesomeField: "AwesomeString" };

    // 验证有效负载(如可能不完整或无效)
    var errMsg = AwesomeMessage.verify(payload);
    if (errMsg)
        throw Error(errMsg);

    // 创建新 message
    var message = AwesomeMessage.create(payload); // 或使用 .fromObject 如果转换是必要的

    // 将 message 转换为 Uint8Array (浏览器) 或 Buffer (node)
    var buffer = AwesomeMessage.encode(message).finish();
    // 使用 buffer 搞事情...

    // 将 Uint8Array (浏览器) 或 Buffer (node) 解码为 message
    var message = AwesomeMessage.decode(buffer);
    // 使用 message 搞事情...

    // 如果应用程序使用了长度分隔的 buffer,那么也会有 encodeDelimited 的 buffer 和 decodeDelimited 的 buffer。

    // 可以将 message 转换回简单对象
    var object = AwesomeMessage.toObject(message, {
        longs: String,
        enums: String,
        bytes: String,
        // 参见 ConversionOptions
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

此外,可以使用期约(Promise)语法来替代回调:

protobuf.load("awesome.proto")
    .then(function(root) {
       // ...
    });
1
2
3
4

使用 JSON 格式

该库利用了与.proto定义相同的 JSON 格式。例如,下面的定义与上面看到的.proto定义相同:

{
  "nested": {
    "awesomepackage": {
      "nested": {
        "AwesomeMessage": {
          "fields": {
            "awesomeField": {
              "type": "string",
              "id": 1
            }
          }
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

JSON 格式非常类似于内部反射结构:

类型(T) 扩展 指定类型的属性
ReflectionObject options
Namespace ReflectionObject nested
Root Namespace nested
Type Namespace fields
Enum ReflectionObject values
Field ReflectionObject rule, type, id
MapField Field keyType
OneOf ReflectionObject oneof (字段名数组)
Service Namespace methods
Method ReflectionObject type, requestType, responseType, requestStream, responseStream
  • 上表中加粗的属性是必需属性,斜体是抽象的。
  • T.fromJSON(name, json)根据 JSON 格式创建相应的反射对象。
  • T#toJSON()根据相应的反射对象创建 JSON 格式(其名称用作父对象中的键)。

如果您只用 JSON 格式而并不使用.proto文件,那么使用light库即可(本例中不需要解析器)。

JSON 文件使用通常方法加载即可:

protobuf.load("awesome.json", function(err, root) {
    if (err) throw err;

    // 继续上面的"获取信息类型"
});
1
2
3
4
5

也可以内联加载:

var jsonDescriptor = require("./awesome.json"); // node 示例

var root = protobuf.Root.fromJSON(jsonDescriptor);

// 继续上面的"获取信息类型"
1
2
3
4
5

仅使用反射

完全版的库和 light 库都支持反射。举个例子,我们可以仅使用反射来定义上面例子中看到的.proto定义。

// ...
var Root  = protobuf.Root,
    Type  = protobuf.Type,
    Field = protobuf.Field;

var AwesomeMessage = new Type("AwesomeMessage").add(new Field("awesomeField", 1, "string"));

var root = new Root().define("awesomepackage").add(AwesomeMessage);

// 继续上面的 "创建一个新信息"
// ...
1
2
3
4
5
6
7
8
9
10
11

有关反射结构的详细信息可以在API文档中找到。

使用自定义类

Message 类也可以扩展自定义功能,还可以注册一个自定义构造函数来反射消息类型:

// ...
// 定义自定义构造器
function AwesomeMessage(properties) {
    // 自定义初始化代码
    // ...
}

// 使用反射类型注册自定义构造器
root.lookupType("awesomepackage.AwesomeMessage").ctor = AwesomeMessage;

// 定义自定义函数
AwesomeMessage.customStaticMethod = function() { ... };
AwesomeMessage.prototype.customInstanceMethod = function() { ... };

// 继续上面的 "创建一个新信息"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(*) 除了通过AwesomeMessage.$typeAwesomeMesage#$type引用其反射类型,相应的自定义类被自动填充为:

  • AwesomeMessage.create
  • AwesomeMessage.encodeAwesomeMessage.encodeDelimited
  • AwesomeMessage.decodeAwesomeMessage.decodeDelimited
  • AwesomeMessage.verify
  • AwesomeMessage.fromObjectAwesomeMessage.toObjectAwesomeMessage#toJSON

然后,这种类型的解码信息就成为instanceof AwesomeMessage

或者,如果不需要自定义初始化代码,也可以重用和扩展内部构造函数:

// ...

// 重用内部构造函数
var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage").ctor;

// 定义自定义函数
AwesomeMessage.customStaticMethod = function() { ... };
AwesomeMessage.prototype.customInstanceMethod = function() { ... };

// 继续上面的 "创建一个新信息"
1
2
3
4
5
6
7
8
9
10

使用服务

该库也支持使用服务但是不对实际的传输通道做任何假设。相反,用户必须提供一个合适的 RPC 实现。该实现是一个异步函数,以反射的服务方法、二进制请求和 node 式回调作为其参数:

function rpcImpl(method, requestData, callback) {
    // 例如,使用 HTTP 请求或 WebSocket 执行请求
    var responseData = ...;
    // 用二进制响应调用回调
    callback(null, responseData);
}
1
2
3
4
5
6

下面是使用 TypeScript 实现的使用 grpc npm 包的示例:

const grpc = require('grpc')

const Client = grpc.makeGenericClientConstructor({})
const client = new Client(
  grpcServerUrl,
  grpc.credentials.createInsecure()
)

const rpcImpl = function(method, requestData, callback) {
  client.makeUnaryRequest(
    method.name,
    arg => arg,
    arg => arg,
    requestData,
    callback
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

示例:

// greeter.proto
syntax = "proto3";

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
var Greeter = root.lookup("Greeter");
var greeter = Greeter.create(/* see above */ rpcImpl, /* request delimited? */ false, /* response delimited? */ false);

greeter.sayHello({ name: 'you' }, function(err, response) {
    console.log('Greeting:', response.message);
});
1
2
3
4
5
6
7

当然也支持期约(promise):

greeter.sayHello({ name: 'you' })
    .then(function(response) {
        console.log('Greeting:', response.message);
    });
1
2
3
4

这里有一个流式 RPC 的示例

请注意,服务 API 是为客户端设计的。实现一个服务器端接入点几乎总是需要特定的传输通道(如 http, websocket 等)代码,唯一的共同点是它对信息进行解码和编码。

搭配 TypeScript

该库附带了自己的类型定义文件,而VS Code这样的现代编辑器将自动检测并使用它们来完成代码。

由于Buffer的原因,该 npm 包依赖了@types/node,同样因为Long的原因,也依赖了@types/long。如果您并不是为 node 构建或/且不使用long.js,那么建议您手动排除它们。

使用 JS API

上面显示的 API 与 TypeScript 的工作原理基本相同。但是,因为所有内容都是有类型的,所以访问动态生成的message类实例上的字段需要使用括号符号(即message["awesomeField"])或显式数据类型转换。另外,也可以使用静态类型文件

import { load } from "protobufjs"; // respectively "./node_modules/protobufjs"

load("awesome.proto", function(err, root) {
  if (err)
    throw err;

  // example code
  const AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");

  let message = AwesomeMessage.create({ awesomeField: "hello" });
  console.log(`message = ${JSON.stringify(message)}`);

  let buffer = AwesomeMessage.encode(message).finish();
  console.log(`buffer = ${Array.prototype.toString.call(buffer)}`);

  let decoded = AwesomeMessage.decode(buffer);
  console.log(`decoded = ${JSON.stringify(decoded)}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用生成的静态代码

如果您使用 CLI 生成静态代码和类型定义文件到bundlejsbundle.d.ts,那么您可以做:

import { AwesomeMessage } from "./bundle.js";

// example code
let message = AwesomeMessage.create({ awesomeField: "hello" });
let buffer  = AwesomeMessage.encode(message).finish();
let decoded = AwesomeMessage.decode(buffer);
1
2
3
4
5
6

使用装饰器

该库还包括了装饰器的早期实现。

请注意,装饰器是 TypeScript 的一个实验性特性,声明顺序取决于 JS 目标。举个例子:@Field.d(2, AwesomeArrayMessage),这意味着AwesomeArrayMessage在之前针对ES5定义过。

import { Message, Type, Field, OneOf } from "protobufjs/light"; // respectively "./node_modules/protobufjs/light.js"

export class AwesomeSubMessage extends Message<AwesomeSubMessage> {

  @Field.d(1, "string")
  public awesomeString: string;

}

export enum AwesomeEnum {
  ONE = 1,
  TWO = 2
}

@Type.d("SuperAwesomeMessage")
export class AwesomeMessage extends Message<AwesomeMessage> {

  @Field.d(1, "string", "optional", "awesome default string")
  public awesomeField: string;

  @Field.d(2, AwesomeSubMessage)
  public awesomeSubMessage: AwesomeSubMessage;

  @Field.d(3, AwesomeEnum, "optional", AwesomeEnum.ONE)
  public awesomeEnum: AwesomeEnum;

  @OneOf.d("awesomeSubMessage", "awesomeEnum")
  public which: string;

}

// example code
let message = new AwesomeMessage({ awesomeField: "hello" });
let buffer  = AwesomeMessage.encode(message).finish();
let decoded = AwesomeMessage.decode(buffer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

支持的装饰器包括:

  • Type.d(typeName?: string) (可选)
    将类注释为protobuf信息类型。如果没有指定typeName,则会将构造函数的运行时函数名用于反射类型。

  • Field.d<T>(fieldId: number, fieldType: string | Constructor<T>, fieldRule?: "optional" | "required" | "repeated", defaultValue?: T) 将属性注释为具有指定idprotobuf类型的protobuf字段。

  • MapField.d<T extends { [key: string]: any }>(fieldId: number, fieldKeyType: string, fieldValueType. string | Constructor<{}>) 将属性注释为具有指定idprotobuf键和值类型的protobuf映射字段。

  • OneOf.d<T extends string>(...fieldNames: string[])
    将属性注释为覆盖指定字段的protobuf oneof

另外:

  • 修饰的类型使用平面结构放置在protobuf.roots["decorated"]中,因此不会有重复的名称。
  • Enum会在装饰器求值时被复制到带有泛型名称的反射枚举,因为引用的枚举对象并没有装饰器可以使用的运行时名称。
  • 默认值必须指定为装饰器的参数,而不是使用属性初始化来实现正确的原型行为。
  • 修饰类上的属性名不能在编译时重命名(例如通过一个小的修饰符),因为修饰符只接收字符串作为原始字段名。

那啥,虽然不太好看,不过在原生 JavaScript 中使用装饰器也是可以的。