protobuf.js

TypeScriptJavaScriptProtocol BuffersSerializationBinary

protobuf.js

概要

protobuf.jsは、JavaScript/TypeScript向けのProtocol Buffers実装です。Googleが開発したProtocol Buffersという言語中立でプラットフォーム中立な構造化データのシリアライズ機構を、ブラウザとNode.jsの両方で利用可能にします。型安全性、高速なエンコード/デコード、コンパクトなバイナリ形式、スキーマの進化に対応した互換性を提供します。

特徴

  • 純粋なJavaScript実装: 外部依存ゼロ、Node.jsとモダンブラウザで動作
  • TypeScript完全サポート: 型定義の自動生成とファーストクラスのTS統合
  • 3つのビルドバリアント: フル版、軽量版、最小版から選択可能
  • 動的と静的なコード生成: 実行時の柔軟性と事前コンパイルのパフォーマンスを選択
  • 小さなバンドルサイズ: コアバンドルは約2KB (gzip圧縮後)
  • 高速なパフォーマンス: ネイティブJSONより高速なエンコード/デコード
  • ストリーミングAPI: 大きなメッセージの効率的な処理
  • gRPCサポート: サービス定義とRPC実装のサポート

インストール

# メインライブラリ
npm install protobufjs --save

# CLIツール(開発用)
npm install protobufjs-cli --save-dev

# TypeScript型定義(TypeScript 2.0以降は不要)
npm install @types/protobufjs --save-dev

使用例

基本的な使用方法(動的API)

import * as protobuf from 'protobufjs';

// .protoファイルの内容
const protoDefinition = `
  syntax = "proto3";
  
  package myapp;
  
  message User {
    string id = 1;
    string name = 2;
    int32 age = 3;
    repeated string tags = 4;
    Address address = 5;
  }
  
  message Address {
    string street = 1;
    string city = 2;
    string country = 3;
  }
`;

// .protoファイルを読み込む
protobuf.load("user.proto", (err, root) => {
  if (err) throw err;

  // メッセージタイプを取得
  const User = root.lookupType("myapp.User");
  const Address = root.lookupType("myapp.Address");

  // データを作成
  const payload = {
    id: "user123",
    name: "山田太郎",
    age: 30,
    tags: ["developer", "japan"],
    address: {
      street: "1-2-3 渋谷",
      city: "東京",
      country: "日本"
    }
  };

  // データを検証
  const errMsg = User.verify(payload);
  if (errMsg) throw Error(errMsg);

  // メッセージを作成
  const message = User.create(payload);

  // バイナリにエンコード
  const buffer = User.encode(message).finish();
  console.log("エンコード後のサイズ:", buffer.length, "bytes");

  // バイナリからデコード
  const decoded = User.decode(buffer);
  console.log("デコード結果:", decoded);

  // プレーンオブジェクトに変換
  const object = User.toObject(decoded, {
    longs: String,
    enums: String,
    bytes: String,
    defaults: true
  });
});

静的コード生成

# .protoファイルから静的JavaScriptコードを生成
pbjs -t static-module -w commonjs -o bundle.js user.proto

# TypeScript定義を生成
pbts -o bundle.d.ts bundle.js

生成されたコードの使用:

import { User, Address } from './bundle';

// 型安全なメッセージ作成
const user = User.create({
  id: "user456",
  name: "鈴木花子",
  age: 25,
  tags: ["designer", "tokyo"],
  address: {
    street: "4-5-6 新宿",
    city: "東京",
    country: "日本"
  }
});

// エンコード
const buffer = User.encode(user).finish();

// デコード
const decoded = User.decode(buffer);

JSONとの相互変換

// .protoファイルをJSONバンドルに変換
// $ pbjs -t json user.proto > bundle.json

import * as protobuf from 'protobufjs/light'; // 軽量版を使用

// JSONバンドルを読み込む
const root = protobuf.Root.fromJSON(require('./bundle.json'));
const User = root.lookupType('myapp.User');

// JSON ↔ Protobuf変換
const user = { id: "789", name: "田中次郎", age: 40 };
const message = User.fromObject(user);
const binary = User.encode(message).finish();
const decoded = User.decode(binary);
const json = User.toObject(decoded);

gRPCサービスの実装

// service.proto
const serviceProto = `
  syntax = "proto3";
  
  service UserService {
    rpc GetUser (GetUserRequest) returns (User);
    rpc ListUsers (ListUsersRequest) returns (UserList);
    rpc CreateUser (CreateUserRequest) returns (User);
  }
  
  message GetUserRequest {
    string id = 1;
  }
  
  message ListUsersRequest {
    int32 limit = 1;
    string cursor = 2;
  }
  
  message CreateUserRequest {
    User user = 1;
  }
  
  message UserList {
    repeated User users = 1;
    string next_cursor = 2;
  }
`;

// サービスの実装
protobuf.load("service.proto", (err, root) => {
  if (err) throw err;

  const UserService = root.lookupService("UserService");

  // RPC実装を定義
  const rpcImpl = (method, requestData, callback) => {
    // HTTP/WebSocketなどで実際のリクエストを送信
    if (method.name === "GetUser") {
      // サーバーにリクエスト
      fetch('/api/users/get', {
        method: 'POST',
        body: requestData,
        headers: { 'Content-Type': 'application/x-protobuf' }
      })
      .then(res => res.arrayBuffer())
      .then(buffer => callback(null, new Uint8Array(buffer)))
      .catch(err => callback(err));
    }
  };

  // サービスインスタンスを作成
  const userService = UserService.create(rpcImpl);

  // サービスメソッドを呼び出し
  userService.getUser({ id: "user123" }, (err, response) => {
    if (err) {
      console.error("エラー:", err);
      return;
    }
    console.log("ユーザー情報:", response);
  });

  // Promiseベースの使用
  userService.getUser({ id: "user456" })
    .then(user => console.log("ユーザー:", user))
    .catch(err => console.error("エラー:", err));
});

ストリーミングとlength-delimited形式

// 複数のメッセージをストリーミング
const messages = [
  { id: "1", name: "ユーザー1", age: 20 },
  { id: "2", name: "ユーザー2", age: 25 },
  { id: "3", name: "ユーザー3", age: 30 }
];

// Length-delimitedエンコード(複数メッセージの連結)
const writer = protobuf.Writer.create();
messages.forEach(msg => {
  const message = User.create(msg);
  User.encodeDelimited(message, writer);
});
const buffer = writer.finish();

// Length-delimitedデコード
const reader = protobuf.Reader.create(buffer);
const decodedMessages = [];
while (reader.pos < reader.len) {
  const message = User.decodeDelimited(reader);
  decodedMessages.push(message);
}

カスタム型とバリデーション

// TypeScriptでカスタムクラスを定義
class CustomUser {
  constructor(public id: string, public name: string, public age: number) {}
  
  get displayName(): string {
    return `${this.name} (${this.age}歳)`;
  }
  
  isAdult(): boolean {
    return this.age >= 20;
  }
}

// カスタムコンストラクタを登録
const User = root.lookupType("myapp.User");
User.ctor = CustomUser;

// デコード時にカスタムクラスのインスタンスが返される
const decoded = User.decode(buffer) as CustomUser;
console.log(decoded.displayName); // "山田太郎 (30歳)"
console.log(decoded.isAdult()); // true

// カスタムバリデーション
const validateUser = (user: any): string | null => {
  const baseError = User.verify(user);
  if (baseError) return baseError;
  
  // 追加のビジネスロジック検証
  if (user.age < 0 || user.age > 150) {
    return "年齢は0〜150の範囲で指定してください";
  }
  
  if (user.tags && user.tags.length > 10) {
    return "タグは最大10個までです";
  }
  
  return null;
};

ブラウザでの使用

<!-- CDN経由での読み込み -->
<script src="//cdn.jsdelivr.net/npm/[email protected]/dist/protobuf.min.js"></script>

<script>
  // グローバル変数protobufが利用可能
  protobuf.load("user.proto", function(err, root) {
    if (err) throw err;
    
    const User = root.lookupType("myapp.User");
    
    // フォームデータからProtobufメッセージを作成
    const formData = {
      id: document.getElementById('userId').value,
      name: document.getElementById('userName').value,
      age: parseInt(document.getElementById('userAge').value)
    };
    
    const message = User.create(formData);
    const buffer = User.encode(message).finish();
    
    // サーバーに送信
    fetch('/api/users', {
      method: 'POST',
      body: buffer,
      headers: { 'Content-Type': 'application/x-protobuf' }
    });
  });
</script>

環境別の最適化

// 最小版の使用(静的コードのみ)
import * as protobuf from 'protobufjs/minimal';

// 軽量版の使用(リフレクションあり、.protoパーサーなし)
import * as protobuf from 'protobufjs/light';

// フル版の使用(全機能)
import * as protobuf from 'protobufjs';

// Webpack設定での最適化
module.exports = {
  resolve: {
    alias: {
      // 本番環境では軽量版を使用
      'protobufjs': process.env.NODE_ENV === 'production' 
        ? 'protobufjs/light' 
        : 'protobufjs'
    }
  }
};

比較・代替手段

類似ライブラリとの比較

  • google-protobuf: Google公式実装だが、サイズが大きく使いにくい
  • protobuf-ts: TypeScript特化で型安全性が高いが、エコシステムが小さい
  • MessagePack: より単純だが、スキーマ定義や型安全性がない
  • Avro: スキーマ進化に優れるが、JavaScriptサポートが弱い
  • FlatBuffers: ゼロコピーで高速だが、APIが複雑

protobuf.jsを選ぶべき場合

  • Protocol Buffersとの互換性が必要
  • TypeScriptプロジェクトで型安全性を重視
  • ブラウザとNode.jsの両方で動作が必要
  • バイナリサイズとパフォーマンスが重要
  • gRPCとの統合が必要

学習リソース

まとめ

protobuf.jsは、JavaScript/TypeScriptエコシステムにおけるProtocol Buffersの標準的な実装です。優れたパフォーマンス、小さなバンドルサイズ、包括的な機能セット、そして優れたTypeScript統合により、バイナリデータのシリアライゼーションが必要なあらゆるプロジェクトで信頼性の高い選択肢となります。特に、マイクロサービス間の通信、リアルタイムアプリケーション、モバイルアプリとの通信など、効率的なデータ転送が重要な場面で威力を発揮します。