首先,安装 vite-plugin-proto 这个插件。
yarn add vite-plugin-proto
然后,配置 vite.config.ts
import { defineConfig } from 'vite' import proto from 'vite-plugin-proto' export default defineConfig(async () => { return { plugins: { proto({ basePath: './src/http/proto', parseOptions: { keepCase: true, }, }), } } })
其中 basePath 是 proto 文件目录。parseOptions 就是 protobufjs 的选项,keepCase 设为 true 是为了保持 protobuf 定义文件的命名方式,否则下划线会自动转为驼峰。
接着就可以在项目里直接导入 proto 文件了:
import def from '@/http/proto/test.proto' console.log(def)
这里拿到的就是前端可以使用的 protobuf 对象了(不是最终 JSON 结构)。
为了能够方便地使用,比如查找数据结构、序列化和反序列化,我还在 AI 的帮助下封装了一个服务。
import proto from 'protobufjs' export default class ProtoService { /** * 根对象 */ root: protobuf.Root /** * 指定的 service 对象,可以没有,并且很多 proto 文件就是没有 service */ service?: protobuf.Service /** * 缓存的方法 */ methodTypes: Map< string, { requestType: protobuf.Type responseType: protobuf.Type } > = new Map() constructor(json: protobuf.INamespace, serviceName?: string) { this.root = proto.Root.fromJSON(json) if (serviceName) { this.service = this.root.lookupService(serviceName) // 预加载所有方法的类型 Object.entries(this.service.methods).forEach(([methodName, method]) => { this.methodTypes.set(methodName, { requestType: this.root.lookupType(method.requestType), responseType: this.root.lookupType(method.responseType), }) }) } } /** * service 下指定方法的默认数据 * @param methodName * @returns */ getMethodDefaults(methodName: string) { if (!this.service) return const method = this.service?.methods[methodName] if (!method) { throw new Error(`Method ${methodName} not found`) } const requestType = this.root.lookupType(method.requestType) const responseType = this.root.lookupType(method.responseType) return { request: this.createCompleteDefault(requestType), response: this.createCompleteDefault(responseType), } } /** * service 下的所有方法对应的默认数据 * @returns */ getAllMethodDefaults() { const defaults: Record<string, any> = {} if (!this.service) return defaults Object.keys(this.service.methods).forEach((methodName) => { defaults[methodName] = this.getMethodDefaults(methodName) }) return defaults } // 获取方法的类型定义 getMethodTypes(methodName: string) { if (!this.service) return const method = this.service.methods[methodName] if (!method) { throw new Error(`Method ${methodName} not found`) } return { requestType: this.root.lookupType(method.requestType), responseType: this.root.lookupType(method.responseType), } } /** * 创建默认对象 * @param type * @returns */ createCompleteDefault(type: protobuf.Type | string) { if (typeof type === 'string') { type = this.findType(type) } return ProtoTypeHelper.createCompleteDefault(type) } // 查找类型 findType(type: string) { const res = this.root.lookupType(type) if (!res) { throw new Error(`Type ${type} not found`) } return res } // 查找枚举 findEnum(name: string) { const res = this.root.lookupEnum(name) if (!res) { throw new Error(`Enum ${name} not found`) } return res } /** * 获取枚举选项 * @param nameOrEnum * @returns */ getEnumOptions(nameOrEnum: string | protobuf.Enum) { if (typeof nameOrEnum === 'string') { nameOrEnum = this.findEnum(nameOrEnum) } const enumOptions = Object.entries(nameOrEnum.values).map((item) => ({ label: item[0], value: item[1], })) return enumOptions } // 序列化数据 serialize( type: protobuf.Type | string, data: any, options = { validateBeforeSerialize: true, }, ) { if (typeof type === 'string') { type = this.findType(type) } return ProtoMessageHandler.serialize(type, data, options) } // 反序列化数据 deserialize( type: protobuf.Type | string, data: Uint8Array, options = { verifyAfterDeserialize: true, enums: false, // 默认不设置 }, ) { if (typeof type === 'string') { type = this.findType(type) } return ProtoMessageHandler.deserialize(type, data, options) } // 规范化数据的方法 normalizeData( type: protobuf.Type | string, data: any, options = { validateBeforeSerialize: true, verifyAfterDeserialize: true, enums: false, }, ) { // 1. 序列化数据 const serializedData = this.serialize(type, data, { validateBeforeSerialize: options.validateBeforeSerialize, }) // 2. 反序列化数据 const deserializedData = this.deserialize(type, serializedData, { verifyAfterDeserialize: options.verifyAfterDeserialize, enums: options.enums, }) return deserializedData } // 序列化请求 serializeRequest( methodName: string, data: any, options = { validateBeforeSerialize: true, }, ): Uint8Array { const types = this.methodTypes.get(methodName) if (!types) { throw new Error(`Method ${methodName} not found`) } return this.serialize(types.requestType, data, options) } // 序列化响应 serializeResponse( methodName: string, data: any, options = { validateBeforeSerialize: true, }, ): Uint8Array { const types = this.methodTypes.get(methodName) if (!types) { throw new Error(`Method ${methodName} not found`) } return this.serialize(types.responseType, data, options) } // 反序列化请求 deserializeRequest( methodName: string, buffer: Uint8Array, options = { verifyAfterDeserialize: true, enums: false, }, ) { const types = this.methodTypes.get(methodName) if (!types) { throw new Error(`Method ${methodName} not found`) } return this.deserialize(types.requestType, buffer, options) } // 反序列化响应 deserializeResponse( methodName: string, buffer: Uint8Array, options = { verifyAfterDeserialize: true, enums: false, }, ) { const types = this.methodTypes.get(methodName) if (!types) { throw new Error(`Method ${methodName} not found`) } return this.deserialize(types.responseType, buffer, options) } } export class ProtoTypeHelper { static isEnum(resolvedType: any): boolean { return resolvedType instanceof proto.Enum } static isMessage(resolvedType: any): boolean { return resolvedType instanceof proto.Type } static getDefaultValue( field: protobuf.Field, typeChain: protobuf.Type[] = [], ) { if (!field.resolvedType) { // 原始类型 return field.defaultValue } if (this.isEnum(field.resolvedType)) { // 枚举类型返回第一个值或默认值 const enumValues = Object.values( (field.resolvedType as proto.Enum).values, ) return enumValues[0] || 0 } if (this.isMessage(field.resolvedType)) { // 如果是循环引用,直接返回默认值 if (typeChain.includes(field.resolvedType as proto.Type)) { return field.defaultValue } // 消息类型创建新实例 return this.createCompleteDefault( field.resolvedType as proto.Type, typeChain, ) } return undefined } static createCompleteDefault( type: protobuf.Type, typeChain: protobuf.Type[] = [], ) { const message: Record<string, any> = ProtoMessageHandler.toObject( type, type.create(), { defaults: true, }, ) const newTypeChain = [...typeChain, type] Object.entries(type.fields).forEach(([fieldName, field]) => { // 如果是 oneof 属性,直接忽略,因为 oneof 包含的属性只能存在一种 if (field.partOf) return if (field.resolvedType) { if (field.repeated) { // 数组对象默认是空数组 message[fieldName] = [] // 默认对象挂到另一个字段--需要处理循环引用 // message[`_${fieldName}Item`] = this.getDefaultValue( // field, // newTypeChain, // ) } else { message[fieldName] = this.getDefaultValue(field, newTypeChain) } } else { message[fieldName] = field.defaultValue } }) return message } static getEnumValues(enumType: protobuf.Enum) { return enumType.values } static getFieldInfo(field: protobuf.Field) { const info: any = { name: field.name, type: field.type, repeated: field.repeated, } if (field.resolvedType) { info.resolvedType = this.isEnum(field.resolvedType) ? 'enum' : 'message' if (this.isEnum(field.resolvedType)) { info.enumValues = this.getEnumValues(field.resolvedType as proto.Enum) } } return info } } export class ProtoMessageHandler { static serialize( messageType: protobuf.Type, data: any, options = { validateBeforeSerialize: true, }, ) { try { if (options.validateBeforeSerialize) { const error = messageType.verify(data) if (error) { throw new Error(`Validation error: ${error}`) } } // 创建一个新的消息实例 const message = messageType.create(data) // 编码为二进制 return messageType.encode(message).finish() } catch (error) { console.error('Serialization error:', error) throw error } } static deserialize( messageType: protobuf.Type, buffer: Uint8Array, options = { verifyAfterDeserialize: true, enums: false, }, ) { try { // 解码 let decoded = messageType.decode(buffer) if (options.verifyAfterDeserialize) { const error = messageType.verify(decoded) if (error) { throw new Error(`Verification error: ${error}`) } } // 枚举转换为 key const conversionOptions: protobuf.IConversionOptions = {} if (options.enums) { conversionOptions.enums = String } decoded = messageType.toObject( decoded, conversionOptions, ) as protobuf.Message return decoded } catch (error) { console.error('Deserialization error:', error) throw error } } static toObject( messageType: protobuf.Type, message: any, options: protobuf.IConversionOptions = { defaults: true, arrays: true, objects: true, longs: String, enums: String, bytes: String, }, ) { return messageType.toObject(message, options) } }
然后使用的时候就只需要几行代码就可以了。
const instance = new ProtoService(def, 'Test') console.log('instance: ', instance) const defaultReqObj = instance.getMethodDefaults('Update') defaultReqObj.request.uuid = 1 console.log('default request object: ', defaultReqObj) const buf = instance.serializeRequest('Update', defaultReqObj?.request) console.log('serialized buffer: ', buf) const desrializedObj = instance.deserializeRequest('Update', buf) // 枚举 const enumOptions = instance.getEnumOptions('Code') console.log('enum options: ', enumOptions)
好了,以上就是 vue3 前端使用 protobuf 的方式了。
等等,还有两点需要解释。
- 问:为什么不使用 protoc 命令行先将 protobuf 转为 js 文件,然后引入呢?网上有部分文章就是这种方式。
答:因为这种方式不方便,并且这种方式在项目引入 js 的时候可能报错。 - 为什么不直接用 ?raw 的形式引入 proto 文件,然后通过 protobufjs 解析呢?
答:虽然我没尝试,但考虑到 proto 文件自身会引用其他 proto 文件,?raw 的形式只是引入了单个文件,解析可能不会成功。