解析 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。