首先,安装 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 的形式只是引入了单个文件,解析可能不会成功。