protobuf在低码中的应用

背景

使用protoBuf描述交互配置,转化生成容易被javascript 解析的结构------json schema,用于描述交互关联的组件与数据,根据配置生成交互界面。

概念

Protocol Buffers(简称:ProtoBuf)

  • 一种开源跨平台的序列化数据结构的协议。
  • 接口描述语言,描述一些数据结构,并提供程序工具根据这些描述产生代码,这些代码将用来生成或解析代表这些数据结构的字节流。

关键路径

  • 扩展语法描述

  • 解析protobuf文件

js 复制代码
import protobuf from 'protobufjs'
export class ProtoBuilder {
    public protoToJson ( filePath: string, content?: string ): IProtoJson {
        const proto = this.loadJson(filePath, content)
        return this.handleJson(filePath, proto)
    }

    private loadJson (filePath: string, content?: string): Record<string, any> {
        let proto = {}
        if (content) {
            // 远程文件,直接根据文件内容解proto
            proto = protobuf.parse(content, { keepCase: true }).root;
        } else {
            // 本地文件,根据文件路径解proto
            const root = new protobuf.Root;
            proto = root.loadSync(filePath, { keepCase: true });
        }
        return proto
    }

    private handleJson (filePath: string, proto: Record<string, any>): IProtoJson {
        const pathArr = filePath.replace(/\\/g, '/').split('/')
        // 文件名: sample_proto.proto
        const fileName = pathArr[pathArr.length - 1]
        const key: string = fileName.replace('.proto', '')
        let packageData: any = {}
        let rootMessage: any = {}
        let subMessages: any = {}
        // key -> sample_proto
        const nested = proto.nested || {}
        if (nested && Object.keys(nested).length > 0) {
            Object.keys(nested).some((v, i) => {
                if (i === 0) {
                    packageData = nested[v]
                    return true
                }
                return false
            })
            const rootMessageName = _.upperFirst(_.camelCase(key))
            rootMessage = packageData[rootMessageName];
            _.forEach(packageData.nested, (message, mn) => {
                if (mn !== rootMessageName) {
                    subMessages[mn] = message
                }
            });
        }
        return {
            filePath, key, package: packageData, rootMessage, subMessages
        }
    }
}
  • 解析注释
js 复制代码
import yaml from 'js-yaml'

function parseComment(comment?: string) {
    if (!comment) {
        return {}
    }
    comment = comment.replace(/^>{2}/mg, '')
    return yaml.load(comment)
}
  • 构造json schema,构造组件入参,包括类型、默认值等
js 复制代码
export default class SchemaBuilder {
    protected key: string = ''
    protected filePath: string = ''
    protected package: any = null
    protected rootMessage: any = {}
    protected subMessages: {[key: string]: any} = {}
    protected isRemoteRoot: boolean = false // 是否服务器拉取的 proto。开发设置的 proto 从 config_file/ 目录下读取,不需要从远程拉
    protected isDev: boolean = false // 开发菜单 devSettings 用的表单
    protected isIgnore: boolean = false // 忽略,不需要参与 schema 构建,返回是 null
    private protoBuilder: ProtoBuilder = new ProtoBuilder()

    constructor(options: { filePath: string, content?: string }) {
        const { filePath, content } = options
        const { key, package: packageData, rootMessage, subMessages } = this.protoBuilder.protoToJson(filePath, content)
        this.filePath = filePath
        this.key = key
        this.package = packageData
        this.rootMessage = rootMessage
        this.subMessages = subMessages
    }

    setRemoteRoot (isRemoteRoot: boolean) {
        this.isRemoteRoot = isRemoteRoot
        return this
    }
    
    setDev (isDev: boolean) {
        this.isDev = isDev
        return this
    }
    
    setIgnore (isIgnore: boolean) {
        this.isIgnore = isIgnore
        return this
    }

    end () {
        return this.build()
    }

    private build() {
        if (this.isIgnore) {
            console.warn(`${this.filePath} 不需要生成 Schema`)
            return null
        }
        if (!this.rootMessage) {
            throw new Error('未找到合法的入口Message')
        }
        const schema = this.buildMessage(this.rootMessage, true)
        const subSchema: IAnyKV = {}
        _.forEach(this.package.nested, (message, mn) => {
            if (mn === _.upperFirst(_.camelCase(this.key))) {
                return
            }
            subSchema[mn] = this.buildMessage(message)
        })
        schema.subSchema = subSchema
        // proto文件root message开放的接口协议为必填
        if ((!schema.protocol && 
            !schema.security && 
            this.isRemoteRoot && 
            !this.isDev)) {
            // throw new Error(`缺少要开放的协议配置,e.g. protocol: ['rpc', 'http']`)
            throw new Error(`缺少安全等级配置,e.g. security: 'internal'`)
        }
        this.handleSecurity(schema)
        return schema
    }

    private handleSecurity (schema: ISchema) {
        if (schema.security) {
            const { security } = schema
            security === 'internal' && (schema.protocol = ['rpc'])
            security === 'public' && (schema.protocol = ['http', 'rpc'])
        }
    }

    private buildMessage(m: any, isRoot?: boolean): ISchema {
        const messageOption: any = parseComment(m.comment);
        // parse sort [-order1, order2] => { order1: -1, order2: 1 }
        const sort: { [key: string]: number } = {};
        (messageOption.sort || []).forEach((f: string) => {
            let v = 1;
            if (f[0] === '-') {
                v = -1;
            }
            const k = _.trimStart(f, '+-');
            sort[k] = v;
        })
        const listColumns = messageOption.listColumns || [];

        const schema = {
            ...messageOption,
            name: messageOption.name,
            sort,
            listColumns,
            fields: []
        }
        if (isRoot) {
            schema.id = this.key
        } else {
            schema.id = m.name
        }

        _.forEach(m.fields, (f) => {
            schema.fields.push(this.parseField(f))
        })

        if (_.isEmpty(schema.listColumns)) {
            schema.listColumns = this.getDefaultListColumns(schema.fields)
        }

        // 格式化 fields 中的 opposite, exclusive
        schema.fields = this.parseFieldsAttr(schema.fields, 'opposite', 'exclusive')
        
        return schema
    }

    // 格式化 fields 中的 opposite | exclusive, 返回格式化后的 fields
    private parseClassify( fields: IField[], attr: ('opposite' | 'exclusive') ) {
        const map: any = {
            /*
            *   将 oppsite | 'exclusive' 根据标志进行分组 
            *   tag1: [{field1}, {field2}, ...]
            *   tag2: [...]
            */
        }
        fields.forEach((item: IField) => {
            if (item[attr] != null) {
                map[String(item[attr])] ? map[String(item[attr])].push(item) : map[String(item[attr])] = [item]
            }
        })
        fields = fields.map((item: IField) => {
            if (item[attr] == null) {
                return item
            }
            item[attr] = JSON.parse(JSON.stringify(map[String(item[attr])]))
            return item
        })

        return fields
    }

    // 格式化 fields 中的属性, 返回格式化后的 fields
    private parseFieldsAttr(fields: IField[], ...attrs: string[]): IField[] {
        const attrsSupported: any = {
            'opposite': this.parseClassify.bind(this, fields, 'opposite'),
            'exclusive': this.parseClassify.bind(this, fields, 'exclusive')
        }

        const attrsNonsupport: any[] = []

        attrs.forEach((attr: any) => {
            if(!attrsSupported[attr]) {
                attrsNonsupport.push(attr)
            }
        })

        if (attrsNonsupport.length) {
            throw new Error(`存在不支持的属性 [ ${attrsNonsupport} ],仅支持 [ ${attrsSupported} ]`)
        }

        attrs.forEach((attr: string) => {
            fields = attrsSupported[attr]()
        })

        return fields
    }

    private parseField(f: IReflectedField) {
        const fieldOption: any = parseComment(f.comment)
        let component = fieldOption.component
        if (component && _.isString(component)) {
            component = { type: component }
        }
        // 自定义 table 之前:table 的数据结构在之前是不设置 type
        // 自定义 table 之后:是需要指定 type, 为了修正自定义 table 的 schema 的结构,由此引入 isSubForm 属性 
        if (!component || !component.type || component.isSubForm) {
            component = this.getDefaultComponent(f.type, f.repeated)
            component = Object.assign(component, fieldOption.component || {})
        }
        
        component.type = this.handleRepeatedType(f, component)
        const option = {}
        Object.assign(option, _.omit(fieldOption, ['component']), { component });

        const field: IField = {
            ...fieldOption,
            id: f.name,
            type: f.type,
            name: fieldOption.name,
            required: !!fieldOption.required || !!fieldOption.uniq,
            repeated: !!f.repeated,
            component,
        }

        return field
    }

    private getDefaultComponent(type: string, repeated?: boolean) {
        const component: { type: string, schema?: string } = {
            type: ''
        }
        const subMessageNames = _.map(this.subMessages, (m) => {
            return m.name
        })
        if (_.indexOf(subMessageNames, type) >= 0) {
            component.type = 'Form'
            component.schema = type
        } else {
            component.type = COMPONENT_MAP[type]
        }

        return component
    }

    private getDefaultListColumns(fields: IField[]) {
        const cols: string[] = []
        _.every(fields, (f) => {
            if (cols.length === 4) {
                return false;
            }
            if (_.indexOf(LIST_COLUMN_TYPES, f.component.type) >= 0) {
                cols.push(f.id)
            }
            return true
        });
        return cols
    }

    private handleRepeatedType(f: IReflectedField, component: any) {
        return f.repeated ? `Repeated${_.upperFirst(component.type)}` : _.upperFirst(component.type)
    }
}
  • 递归解析与渲染交互

效果

相关推荐
吕彬-前端4 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱6 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai16 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓1 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb