解析 Protobuf 文件并转为 JSON 格式

我们知道 Protobuf 文件可以减少体积,方便存储和传输,但在获取文件后,我们还需要将其转换为 JSON 格式,以适配应用进行处理数据。

下面的代码是在微信小程序中使用,将 pb 文件转换为 JSON,然后再使用。

// 小程序根目录执行安装(需开启 npm 支持)
// npm install protobufjs

// 创建 proto 文件 /proto/item.proto
/*
syntax = "proto3";
message DataItem {
  string id = 1;
  string name = 2;
  string kw = 3;
  bool lock = 4;
  string category = 5;
  string label = 6;
  int32 grade = 7;
}
message DataArray { repeated DataItem items = 1; }
*/

// 在 app.js 中引入
const protobuf = require('protobufjs');

// 页面 JS 文件
Page({
  data: { items: [] },

  onLoad() {
    this.loadProtoData();
  },

  async loadProtoData() {
    try {
      // 1. 下载二进制文件
      const { tempFilePath } = await this.downloadFile();

      // 2. 读取文件内容
      const arrayBuffer = await this.readFile(tempFilePath);

      // 3. 加载 Proto 定义
      const root = await protobuf.load('/proto/item.proto');
      const DataArray = root.lookupType('DataArray');

      // 4. 解析数据
      const decoded = DataArray.decode(new Uint8Array(arrayBuffer));

      // 5. 转换格式
      const result = decoded.items.map(item => ({
        id: item.id || '',
        name: item.name || '',
        kw: item.kw || '',
        lock: typeof item.lock === 'boolean' ? item.lock : false,
        category: item.category || '',
        label: item.label || '',
        grade: item.grade || 1
      }));

      this.setData({ items: result });
    } catch (err) {
      console.error('解析失败:', err);
    }
  },

  downloadFile() {
    return new Promise((resolve, reject) => {
      wx.downloadFile({
        url: 'https://your-domain.com/data.bin',
        success: resolve,
        fail: reject
      });
    });
  },

  readFile(tempFilePath) {
    return new Promise((resolve, reject) => {
      wx.getFileSystemManager().readFile({
        filePath: tempFilePath,
        encoding: 'binary',
        success: res => resolve(res.data),
        fail: reject
      });
    });
  }
});

当文件非常大时,解析速度会有点慢啊,为了更进一步的提高速度,我们可以使用 WebAssembly,这里是一个 Wasm 示例:

// app.js 全局初始化
const { Parser } = require('./libs/protobuf/protobuf');
App({
  onLaunch() {
    // 预加载 WASM
    this.globalData.pbParser = new Parser();
    this.globalData.pbParser.initWasm('https://wasm-cdn.com/protobuf.wasm');
  }
});

// 页面逻辑
Page({
  async onLoad() {
    const arrayBuffer = await this.downloadProtoData();
    const items = await this.parseWithWasm(arrayBuffer);
    this.setData({ items });
  },

  async downloadProtoData() {
    const { tempFilePath } = await wx.downloadFile({
      url: 'https://your-cdn.com/data.bin'
    });

    return new Promise((resolve, reject) => {
      wx.getFileSystemManager().readFile({
        filePath: tempFilePath,
        encoding: 'binary',
        success: res => resolve(res.data),
        fail: reject
      });
    });
  },

  async parseWithWasm(arrayBuffer) {
    const parser = getApp().globalData.pbParser;

    // WASM 内存直传(避免复制开销)
    const { instance } = await parser.wasmReady;
    const memPtr = instance.exports.malloc(arrayBuffer.byteLength);
    const heap = new Uint8Array(
      instance.exports.memory.buffer,
      memPtr,
      arrayBuffer.byteLength
    );
    heap.set(new Uint8Array(arrayBuffer));

    // 高性能解析
    const resultPtr = instance.exports.parse_protobuf(
      memPtr,
      arrayBuffer.byteLength
    );

    // 解析结果处理
    const jsonStr = instance.exports.get_json(resultPtr);
    const result = JSON.parse(jsonStr);

    // 释放 WASM 内存
    instance.exports.free(memPtr);
    instance.exports.free(resultPtr);

    return result;
  }
});

我们需要用到胶水代码,也可以优化,如下:

class Parser {
  constructor() {
    this.wasmInstance = null;
    this.memory = null;
  }

  async initWasm(wasmUrl) {
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch(wasmUrl),
      {
        env: {
          emscripten_notify_memory_growth: (memory) => {
            this.memory = memory;
          }
        }
      }
    );
    this.wasmInstance = instance;
  }

  get wasmReady() {
    return new Promise(resolve => {
      const check = () => {
        if (this.wasmInstance) {
          resolve({ instance: this.wasmInstance });
        } else {
          setTimeout(check, 50);
        }
      };
      check();
    });
  }
}

module.exports = { Parser };

这种方案适合 pb 文件大于 5MB 时使用,对于小型数据可直接使用 JS 解析。

我们转了一圈,发现在小程序中也可以将 PB 文件直接转换为 JS 数组对象,这样可以省去中间的 JSON 转换耗时。

下面是直接将 Protobuf 内容转换为 JavaScript 数组对象的完整解决方案:

// 小程序页面 js 文件
const protobuf = require('protobufjs');

Page({
  data: {
    items: [] // 最终需要的数组
  },

  onLoad() {
    this.loadProtobufData();
  },

  async loadProtobufData() {
    try {
      // 1. 下载并读取二进制文件
      const arrayBuffer = await this.getFileBuffer('https://example.com/data.bin');

      // 2. 加载 Proto 定义(精简版)
      const root = await protobuf.load({
        nested: {
          DataArray: {
            fields: {
              items: {
                rule: 'repeated',
                type: 'DataItem',
                id: 1
              }
            }
          },
          DataItem: {
            fields: {
              id: { type: 'string', id: 1 },
              name: { type: 'string', id: 2 },
              kw: { type: 'string', id: 3 },
              lock: { type: 'bool', id: 4 },
              category: { type: 'string', id: 5 },
              label: { type: 'string', id: 6 },
              grade: { type: 'int32', id: 7 }
            }
          }
        }
      });

      // 3. 创建解码器
      const DataArray = root.lookupType("DataArray");

      // 4. 执行转换(核心操作)
      const decoded = DataArray.decode(new Uint8Array(arrayBuffer));

      // 5. 转换为标准 JS 数组
      const result = decoded.items.map(item => ({
        id: item.id || '',
        name: item.name || '',
        kw: item.kw || '',
        lock: item.lock !== undefined ? item.lock : false,
        category: item.category || '',
        label: item.label || '',
        grade: Number(item.grade) || 1 // 确保数字类型
      }));

      this.setData({ items: result });
    } catch (err) {
      console.error('转换失败:', err);
      wx.showToast({ title: '数据加载失败', icon: 'none' });
    }
  },

  // 封装文件获取方法
  async getFileBuffer(url) {
    const { tempFilePath } = await new Promise((resolve, reject) => {
      wx.downloadFile({
        url,
        success: resolve,
        fail: reject
      });
    });

    return new Promise((resolve, reject) => {
      wx.getFileSystemManager().readFile({
        filePath: tempFilePath,
        encoding: 'binary',
        success: res => resolve(res.data),
        fail: reject
      });
    });
  }
});

上述方案可以直接应用于生产环境中,可稳定处理一下规模的数据,1 万条数据(约 3MB)解析耗时小于 200ms。