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)
    }
}
  • 递归解析与渲染交互

效果

相关推荐
L耀早睡13 分钟前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer27 分钟前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿33 分钟前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年1 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net
爱分享的程序员2 小时前
全栈项目搭建指南:Nuxt.js + Node.js + MongoDB
前端
隐含3 小时前
webpack打包,把png,jpg等文件按照在src目录结构下的存储方式打包出来。解决同一命名的图片资源在打包之后,重复命名的图片就剩下一个图片了。
前端·webpack·node.js
lightYouUp3 小时前
windows系统中下载好node无法使用npm
前端·npm·node.js
Dontla3 小时前
npm cross-env工具包介绍(跨平台环境变量设置工具)
前端·npm·node.js