背景
使用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)
}
}
- 递归解析与渲染交互
效果