从开篇介绍的 bpmn-js 依赖图中,我们可以发现 bpmn-js 在 图元素 的操作上依赖 daigram-js,而与 XML 之间的转换则是需要依赖 bpmn-moddle,但 bpmn-moddle 的依赖之一 moddle-xml 和它本身其实都有一个依赖包 ------ moddle。
所以我们对 bpmn-js 解析处理 xml 的过程就从 moddle 开始。
从入口开始
关于 Moddle,它的介绍是 A library for importing meta-model based file formats into JS,即"一个用于导入元模型文件的js库"。核心文件都在 lib 文件夹中,入口是 index.js。
Moddle 的最大作用就是,提供了一种通过 json 文件来定义 XML 元模型的方式,并且实现 XML 字符串与 JavaScript 对象之间的转换。
进入到 index.js 入口文件中,会发现这里只有几个默认导出的方法:
js
// 默认导出 Moddle 类
export {
default as Moddle
} from './moddle.js';
// 导出一个 parseNameNS 的方法
export {
parseName as parseNameNS
} from './ns.js';
// 导出三个类型判断的方法
export {
isBuiltIn as isBuiltInType,
isSimple as isSimpleType,
coerceType
} from './types.js';
当然,由于 Moddle.js 本身也需要引用下面的几个方法,所以我们先从下面的方法开始。
ParseNameNS - 命名空间解析
这个函数的作用很简单,就是 从一个字符串中解析出来命名空间前缀和属性(标签)名称,然后组合成一个标准对象;如果字符串格式不对,则抛出异常。
js
export function parseName(name, defaultPrefix) {
var parts = name.split(/:/),
localName, prefix;
// no prefix (i.e. only local name)
if (parts.length === 1) {
localName = name;
prefix = defaultPrefix;
} else
// prefix + local name
if (parts.length === 2) {
localName = parts[1];
prefix = parts[0];
} else {
throw new Error('expected <prefix:localName> or <localName>, got ' + name);
}
name = (prefix ? prefix + ':' : '') + localName;
return {
name: name, // 完整名称
prefix: prefix, // 命名空间
localName: localName // 属性(标签)名
};
}
代码其实也很简单:
- 将
name参数按照:进行拆分,得到数组parts- 如果
parts长度为 1,则默认传递的是一个没有命名空间的属性,那么设置命名空间是defaultPrefix - 如果长度是 2,则默认冒号前半截是命名空间
prefix,后半截是属性名 - 如果长度大于 3,则抛出异常
- 如果
- 如果 此时
prefix没有确定值的话,则会直接保留属性名localName
最终,会根据上面的结果组装成一个包含 name,prefix,localName 的对象。例如:
这个方法 通常用来解析 XML 中的标签名或者属性名,验证是否是已经注册的合法属性,如果是没有注册的属性或者标签的话,则会在后面的过程中进行其他处理。
Types - 内置的类型校验
整个 types.js 其实只有一个功能:预设几种属性类型并提供几个类型判断的方法。
这些方法,主要目的是 将从 XML 中解析出来的属性,转换成符合预设的类型定义的值。
源码如下:
js
// 预设的 moddle 类型
var BUILTINS = {
String: true,
Boolean: true,
Integer: true,
Real: true,
Element: true
};
// 上面的内置类型对应的实际值转换方法
var TYPE_CONVERTERS = {
String: function(s) { return s; },
Boolean: function(s) { return s === 'true'; },
Integer: function(s) { return parseInt(s, 10); },
Real: function(s) { return parseFloat(s); }
};
// 将解析值与其类型转换成实际值,Element 类型则不处理
export function coerceType(type, value) {
var converter = TYPE_CONVERTERS[type];
if (converter) {
return converter(value);
} else {
return value;
}
}
// 判断是否是内置类型
export function isBuiltIn(type) {
return !!BUILTINS[type];
}
// 判断是否是简单类型
export function isSimple(type) {
return !!TYPE_CONVERTERS[type];
}
其中 moddle 可以支持两种大类型的数据:简单类型(布尔、字符串、整数和小数)与对象类型(Element,在这里一般代指对象);其中引用类型基本上只适用于 Object 对象,而不支持 Array 数组或者 Map、Set 之类的数据结构。
并且,由于 XMl 在解析时是当做字符串来处理的,所以解析出来的属性值一般都是字符串,这里就需要用 Converter 转换器来进行对应的转换。而默认的 Converter 只有简单类型对应的 4 个转换器:
-
String:直接返回当前值(因为最初解析出来就是字符串格式) -
Boolean:只有'true'字符串才会当成true -
Integer:整数类型,通过parseInt转化为 十进制 整数 -
Real:小数类型,通过parseFloat进行转换
当我们记住了这几个工具方法之后,就可以开始研究 moddle.js 了。
Moddle - 用于构建特定类型的元素的类
既然是作为可以 创建 特定类型元素的模块,肯定需要知道 有哪些特定的类型以及每个类型的具体属性 ,以及 提供创建方法。
所以 Moddle 的构造函数本身会接收一个参数 packages,用来声明有哪些特定类型的元素或属性;另外还接收一个 config 配置,用来确认是否需要使用严格模式创建元素。
strict开启时,会抛出一个异常中断代码执行,否则只会在控制台打印警告。
Moddle 函数内部,依赖 Properties,Factory,Registry 三个构造函数,其中:
Properties是用于获取和设置模型元素的属性的实用程序Factory是用于创建指定类型元素的实例,并且内部的示例创建方法,实际上是构建一个新的ModdleElement类型的对象实例Registry则是一个注册表程序,会解析packages数组中的所有元素与属性定义,并生成一个typeMap的对象
在 Moddle 实例创建的过程中,会为 moddle 实例创建以上三个类型对应的实例属性,并且这四个实例之间还存在互相引用的关系。
js
export default function Moddle(packages, config = {}) {
this.properties = new Properties(this);
this.factory = new Factory(this, this.properties);
this.registry = new Registry(packages, this.properties);
this.typeCache = {};
this.config = config;
}
首先,我们先分析一下这四个类型分别提供了哪些方法。
Moddle.prototype
在 Moddle 的原型上,一共定义了 9 个方法:
js
// 传入一个类型定义 descriptor 和 属性定义,创建一个指定类型的元素实例
Moddle.prototype.create = function(descriptor: string | object, attrs?: object) {}
// 根据传入的类型定义,返回该类型定义对应的 元素创建构造函数
Moddle.prototype.getType = function(descriptor: string | object) {}
// 直接根据 name 与 nsUri 创建一个具有 properties 中定义属性的对象
Moddle.prototype.createAny = function(name: string, nsUri: string, properties?: object) {}
// 通过uri或前缀返回已注册的类型定义 json 对象
Moddle.prototype.getPackage = function(uriOrPrefix: string) {}
// 返回所有已注册的 json 数组
Moddle.prototype.getPackages = function() {}
// 获取一个对象实例对应的具体类型定义
Moddle.prototype.getElementDescriptor = function(element: ModdleElement) {}
// 判断一个类型是否是属于指定元素定义的,省略元素实例时会查询所有已注册类型
Moddle.prototype.hasType = function(element: ModdleElement, type: string) {}
Moddle.prototype.hasType = function(type: string) {}
// 返回 property 指定的属性对应的属性定义
Moddle.prototype.getPropertyDescriptor = function(element: ModdleElement, property: unkown) {}
// 返回指定类型 type 对应的类型定义
Moddle.prototype.getTypeDescriptor = function(type: string) {}
从方法说明上不难看出,这几个方法主要是用来 创建元素实例或者读取/判断元素属性 的。
假设我们此时有这样一个 json 文件,用来进行元素类型定义:
json
{
"name": "Properties",
"uri": "http://properties",
"prefix": "props",
"types": [
{
"name": "Complex",
"properties": [
{ "name": "id", "type": "String", "isAttr": true, "isId": true }
]
},
{
"name": "ComplexAttrs",
"superClass": [ "Complex" ],
"properties": [
{ "name": "attrs", "type": "Attributes", "serialize": "xsi:type" }
]
},
{
"name": "ComplexAttrsCol",
"superClass": [ "Complex" ],
"properties": [
{ "name": "attrs", "type": "Attributes", "isMany": true, "serialize": "xsi:type" }
]
},
{
"name": "ComplexCount",
"superClass": [ "Complex" ],
"properties": [
{ "name": "count", "type": "Integer", "isAttr": true }
]
},
{
"name": "ComplexNesting",
"superClass": [ "Complex" ],
"properties": [
{ "name": "nested", "type": "Complex", "isMany": true }
]
},
{
"name": "SimpleBody",
"superClass": [ "Base" ],
"properties": [
{ "name": "body", "type": "String", "isBody": true }
]
},
{
"name": "SimpleBodyProperties",
"superClass": [ "Base" ],
"properties": [
{ "name": "intValue", "type": "Integer" },
{ "name": "boolValue", "type": "Boolean" },
{ "name": "str", "type": "String", "isMany": true }
]
},
{
"name": "Base"
},
{
"name": "BaseWithId",
"superClass": [ "Base" ],
"properties": [
{ "name": "id", "type": "String", "isAttr": true, "isId": true }
]
},
{
"name": "BaseWithNumericId",
"superClass": [ "BaseWithId" ],
"properties": [
{ "name": "idNumeric", "type": "Integer", "isAttr": true, "redefines": "BaseWithId#id", "isId": true }
]
},
{
"name": "Attributes",
"superClass": [ "BaseWithId" ],
"properties": [
{ "name": "realValue", "type": "Real", "isAttr": true },
{ "name": "integerValue", "type": "Integer", "isAttr": true },
{ "name": "booleanValue", "type": "Boolean", "isAttr": true },
{ "name": "defaultBooleanValue", "type": "Boolean", "isAttr": true, "default": true }
]
},
{
"name": "SubAttributes",
"superClass": [ "Attributes" ]
},
{
"name": "Root",
"properties": [
{ "name": "any", "type": "Base", "isMany": true }
]
},
{
"name": "Embedding",
"superClass": [ "BaseWithId" ],
"properties": [
{ "name": "embeddedComplex", "type": "Complex" }
]
},
{
"name": "ReferencingSingle",
"superClass": [ "BaseWithId" ],
"properties": [
{ "name": "referencedComplex", "type": "Complex", "isReference": true, "isAttr": true }
]
},
{
"name": "ReferencingCollection",
"superClass": [ "BaseWithId" ],
"properties": [
{ "name": "references", "type": "Complex", "isReference": true, "isMany": true }
]
},
{
"name": "ContainedCollection",
"superClass": [ "BaseWithId" ],
"properties": [
{ "name": "children", "type": "Complex", "isMany": true }
]
},
{
"name": "MultipleSuper",
"superClass": ["Base","BaseWithId","SimpleBody"]
}
]
}
当我们使用这个 Properties.json 进行元素与属性的注册时,返回的 model 实例会包含以下内容:
js
import { Moddle } from 'moddle'
import Properties from '../../model/properties.json'
const model = new Moddle([Properties])
其中 factory 与 properties 属性由于互相依赖的关系,与 model 实例存在一个循环引用;而 registry 中则是保存了传递进去的 descriptor json 定义与每种元素/属性的具体配置。
model.create 实例创建
当我们通过 model.create 方法创建一个实例时,实际上返回的实例类型是 ModdleElement:
js
const complexTypeInst = model.create('props:Complex', { name: 'complex', id: 'complex', idx: 1 })
const simpleBodyInst = model.create('props:SimpleBody', { body: 'simple', idx: 2 })
const attributesInst = model.create('props:Attributes', { attrs: [complexTypeInst, simpleBodyInst], idx: 3 })
console.log(complexTypeInst, simpleBodyInst, attributesInst)
为什么不是 Moddle 或者 Factory 等构造函数中的一种呢?这需要从源码中一步一步来理解。
首先,我们需要先了解一下 Properties 与 Registry。
Properties - 模型实例的属性管理与配置
从 Moddle 构造函数那里可以得知,Properties 需要依赖 Moddle 的实例对象 model。
js
export default function Properties(model) {
this.model = model;
}
然后, Properties 提供了实例属性 改与查 两个类型的方法:
- 查:
get与getProperty - 改:
set与define、defineDescriptor、defineModel
set - 修改属性
接收三个参数 target, name, value,即 接收一个目标对象 taeget,将 name 属性值设置为 value 。但是与 Object.defineProperty 和 Reflect.set 两个方法不同的是,这里会 校验 name 与 value 两个参数在属性定义中的合法性。
js
import { assign, isString } from 'min-dash';
Properties.prototype.set = function(target, name, value) {
if (!isString(name) || !name.length) {
throw new TypeError('property name must be a non-empty string');
}
var property = this.getProperty(target, name);
var propertyName = property && property.name;
if (isUndefined(value)) {
if (property) {
delete target[propertyName];
} else {
delete target.$attrs[stripGlobal(name)];
}
} else {
if (property) {
if (propertyName in target) {
target[propertyName] = value;
} else {
defineProperty(target, property, value);
}
} else {
target.$attrs[stripGlobal(name)] = value;
}
}
};
function isUndefined(val) {
return typeof val === 'undefined';
}
function defineProperty(target, property, value) {
Object.defineProperty(target, property.name, {
enumerable: !property.isReference,
writable: true,
value: value,
configurable: true
});
}
function stripGlobal(name) {
return name.replace(/^:/, '');
}
简述一下就是:首先获取到 name 对应的属性定义 property;如果传入的值 value 是 undefined 的话,合法属性(存在 property 定义)则从 target 中直接删除,否则则从 target.$attrs 里面删除;如果不是 undefined 的话,合法属性则修改 target 中的对应字段属性值,不合法属性则直接赋值到 target.$attrs 里面。
这也是
bpmn-js中如何删除属性以及读取其他非法属性的原理。
当然,此时写入的属性 都是可读可编辑的 ,但是 define 方法则与之不同。
define - 为元素实例添加属性
define 方法,在一定层度上,就是 Object.defineProperty 方法:
js
Properties.prototype.define = function(target, name, options) {
if (!options.writable) {
var value = options.value;
options = assign({}, options, {
get: function() { return value; }
});
delete options.value;
}
Object.defineProperty(target, name, options);
};
当然,这里表示 除了显示设置属性是可编辑属性 ,都会删除对象上的 value 属性,并重新设置 get 方法返回指定的 value 值。
在这个方法的基础上,设置了 defineDescriptor 与 defineModel 方法:
js
Properties.prototype.defineDescriptor = function(target, descriptor) {
this.define(target, '$descriptor', { value: descriptor });
};
Properties.prototype.defineModel = function(target, model) {
this.define(target, '$model', { value: model });
};
即为目标元素 target 设置两个 只读属性 $descriptor 与 $model。
get - 读取属性
与 set 方法正好对应,get 方法就是获取元素对应的属性值。如果属于合法属性,则直接返回对应属性;如果不是合法属性,则返回 $attrs 里面的对应属性。
合法属性如果定义是
isMany,即表示为数组格式,如果当前没有设置该属性值的话,会初始化为一个空数组再返回。
js
Properties.prototype.get = function(target, name) {
var property = this.getProperty(target, name);
if (!property) {
return target.$attrs[stripGlobal(name)];
}
var propertyName = property.name;
if (!target[propertyName] && property.isMany) {
defineProperty(target, property, []);
}
return target[propertyName];
};
getProperty - 获取属性的定义对象
这里的方法名叫 getProperty,但是实际上称为 getPropertyDescriptor 更为恰当,即 获取指定属性的定义的描述对象。
本身这个方法是依赖了 model 实例上的 getPropertyDescriptor 方法,返回的是 这个指定实例 target 的指定属性 name 对应的 $$descriptor 属性 。当然,如果这个 name 是一个未定义的非法属性,或者包含非法符号 :, 则都返回的是 null。
js
Properties.prototype.getProperty = function(target, name) {
var model = this.model;
var property = model.getPropertyDescriptor(target, name);
if (property) {
return property;
}
if (name.includes(':')) {
return null;
}
const strict = model.config.strict;
if (typeof strict !== 'undefined') {
const error = new TypeError(`unknown property <${ name }> on <${ target.$type }>`);
if (strict) {
throw error;
} else {
typeof console !== 'undefined' && console.warn(error);
}
}
return null;
};
这里还会根据
new Moddle时传递的config对象中的strict属性,来确定是否需要抛出错误或者异常;当strict布尔值为true时,会抛出异常直接中断执行;反之则是通过控制台打印一个警告。这也是为什么我们在使用
bpmn-js时,直接绑定未声明属性会保留在$attrs中;虽然能正常导出xml字符串,但是再次导入控制台就会报这个警告。
Factory - 模型元素实例的创建工厂
整个 Factory 实际上只做一件事,就是 创建模型元素实例 。只是这个实例除了需要 符合模型定义之外 ,还要 具有统一的类型与属性。
上文 model.create 的示例中创建的三个元素实例,打印出来的内容中就体现了这一特点 ------ 类型都是 ModdleElement, 并且继承自 Base 类型,都具有 $type, $parent, $attrs, $descriptor, $model 等属性。
base.js:
js
export default function Base() { }
Base.prototype.get = function(name) {
return this.$model.properties.get(this, name);
};
Base.prototype.set = function(name, value) {
this.$model.properties.set(this, name, value);
};
factory.js:
js
import { forEach, bind } from 'min-dash';
import Base from './base.js';
export default function Factory(model, properties) {
this.model = model;
this.properties = properties;
}
Factory.prototype.createType = function(descriptor) {
var model = this.model;
var props = this.properties,
prototype = Object.create(Base.prototype);
forEach(descriptor.properties, function(p) {
if (!p.isMany && p.default !== undefined) {
prototype[p.name] = p.default;
}
});
props.defineModel(prototype, model);
props.defineDescriptor(prototype, descriptor);
var name = descriptor.ns.name;
function ModdleElement(attrs) {
props.define(this, '$type', { value: name, enumerable: true });
props.define(this, '$attrs', { value: {} });
props.define(this, '$parent', { writable: true });
forEach(attrs, bind(function(val, key) {
this.set(key, val);
}, this));
}
ModdleElement.prototype = prototype;
ModdleElement.hasType = prototype.$instanceOf = this.model.hasType;
props.defineModel(ModdleElement, model);
props.defineDescriptor(ModdleElement, descriptor);
return ModdleElement;
};
从上面的源码中,可以看出 Factory 本身只有一个方法 createType,用来创建一个 ModdleElement 类型的构造函数并返回;ModdleElement 继承自 Base 类型,提供了 get 与 set 方法,借用 Properties 模块的 get 和 set 方法来验证属性合法性与修改查询。
在 createType(descriptor) 执行时,会根据 Base 创建一个"原型"对象 proptotype,将 model 与传入参数 descriptor 作为只读属性绑定到 $model 与 $descriptor 上,并读取该类型的名称 name。
然后定义 ModdleElement,当后期执行 new ModdleElement 时,将会把 name 绑定到实例的 $type 属性上,并初始化 $attrs 与 $parent,然后借用 Base 的 set 方法按元素定义设置对应的属性值。
最后则是绑定原型指向与定义 $instanceOf 方法来判断属性继承关系(这个方法其实就是从 descriptor 定义中解析属性的继承关系来判断,详细内容与 Registry 有关联)。
并且还会为 ModdleElement 这个构造函数绑定 $model 与 $descriptor 两个静态属性以及 hasType 的静态方法。
此时就剩下 Registry 函数还没有分析,但是在分析 Registry 之前我们先来看一下它的另一个依赖构造函数 DescriptorBuilder。
DescriptorBuilder - 构建元素的描述对象
该构造函数执行时需要一个参数 nameNS,而这个参数就是 parseNameNS 函数返回的 name namespace 标准对象。
在初始化时,会生成一个对象,保存后续 descriptor 中会注册的类型与属性,并生成对应的 map 对象:
js
export default function DescriptorBuilder(nameNs) {
this.ns = nameNs;
this.name = nameNs.name;
this.allTypes = [];
this.allTypesByName = {};
this.properties = [];
this.propertiesByName = {};
}
回到 model.create 实例创建 那里的截图,如果我们展开 $descriptor 对象,就可以发现这个对象的格式与这里的构造函数定义如出一辙。
但是与其构造函数不同的是,这里还多了 IdProperty 属性,并且 propertiesByName 中还有两种对 id 属性的定义 id 与 props:id。
整个 DescriptorBuilder 类里面,最重要的就是 addTrait 方法。在 model.create()/model.getType() 执行时,会调用 registry.getEffectiveDescriptor(name),此时就需要创建一个 DescriptorBuilder 实例,并且通过 addTrait 来构建一个属性构建对象。
addTrait 方法接收两个参数:t 和 inherited。其中 t 表示 typeDescriptor,在实际执行过程中会是一个 经过扩充之后的一个元素表述对象 ,包含我们在 json 文件中定义的该类型对应的对象,具有 superClass、properties、meta、extends 等多个属性;而 inherited 是一个布尔值,表示该属性是否 存在继承关系。
如果存在继承关系的话,还会校验这个定义是否是 对原有元素定义的补充 ,即 验证这个元素对象描述是否存在 extends 属性,如果存在则会抛出异常。
这里的 继承 通过
superClass来指定继承对象,通过extends来表示对某个原对象描述的补充;两者不能同时存在,校验顺序为superClass => extends。
报错如下:
如果继承与补充校验成功之后,则是 遍历元素描述对象中的元素属性定义数组 properties,根据每一个 property 来将其定义进行整理和标准化。
js
DescriptorBuilder.prototype.addTrait = function(t, inherited) {
if (inherited) {
this.assertNotTrait(t);
}
var typesByName = this.allTypesByName,
types = this.allTypes;
var typeName = t.name;
if (typeName in typesByName) {
return;
}
forEach(t.properties, bind(function(p) {
p = assign({}, p, {
name: p.ns.localName,
inherited: inherited
});
Object.defineProperty(p, 'definedBy', {
value: t
});
var replaces = p.replaces,
redefines = p.redefines;
if (replaces || redefines) {
this.redefineProperty(p, replaces || redefines, replaces);
} else {
if (p.isBody) {
this.setBodyProperty(p);
}
if (p.isId) {
this.setIdProperty(p);
}
this.addProperty(p);
}
}, this));
types.push(t);
typesByName[typeName] = t;
};
在后面的
forEach循环中,函数体里面的p变量指代的就是描述对象的properties数组中的每一项
在循环中,首先就是像 p 对象中合并进去 localName 和 inherited 两个字段;并设置一个只读属性 definedBy,标识是被那个元素定义的。
然后,需要注意四个定义:p.replaces, p.redefines, p.isBody, p.isId。
当 p.replaces 或者 p.redefines 为 truth 时,会通过 redefineProperty() 方法, 将原继承到的这个属性定义(即 superClass 中的指定类型已经定义了一个同名属性)进行重新定义。当然,如果原来的类型不存在该定义,则会抛出异常。
js
DescriptorBuilder.prototype.redefineProperty = function(p, targetPropertyName, replace) {
var nsPrefix = p.ns.prefix;
var parts = targetPropertyName.split('#');
var name = parseNameNs(parts[0], nsPrefix);
var attrName = parseNameNs(parts[1], name.prefix).name;
var redefinedProperty = this.propertiesByName[attrName];
if (!redefinedProperty) {
throw new Error('refined property <' + attrName + '> not found');
} else {
this.replaceProperty(redefinedProperty, p, replace);
}
delete p.redefines;
};
从上面的代码来看,如果要定义 replaces 或者 redefines 来覆盖父级的属性定义,需要在描述对象中定义 replaces 或者 redefines 属性并设置需要覆盖的父级元素属性。
例如我们对上面的例子进行修改,会得到以下结果:
一定程度上来说,
replaces与redefines的效果基本上差不多,只是replaces会调整properties中该属性的位置:
replaces:将新定义移动到properties数组的最后一位redefines:在原地替换属性定义
至于 isId 与 isBody,则更多的时候是一个 标识作用 ,用来确定这个属性是一个 id 属性或者 body 属性。
最后,通过 addProperty 方法,将该定义插入到 builder 实例中的 properties 数组中,并根据属性名绑定两种命名方式到 builder 的 propertiesByName 属性上。
现在,让我们回到 Registry 模块上~
Registry - 元素/属性与描述对象的注册表
在 new Moddle() 的初始化中,除了会实例化上述对象之外,还会实例化一个 Registry 注册表对象。
Registry 对象一共有四个属性:packageMap、typeMap、packages、properties;其中 properties 就是上文所讲的 Properties 类的对应实例,而 packages 则是 new Moddle 是传递进来的 descriptor json 对象数组。
js
export default function Registry(packages, properties) {
this.packageMap = {};
this.typeMap = {};
this.packages = [];
this.properties = properties;
forEach(packages, bind(this.registerPackage, this));
}
当 new Registry 执行时,会初始化上述的 4 个属性,然后遍历 packages 数组,分别对每个 descriptor json 的内容进行解析和注册。
registerPackage 函数,主要负责 验证每个属性描述对象的 prefix 与 uri 属性是否与其他定义重复 ,并 将该描述对象 package 保存到 Registry 实例的 packageMap 与 packages 属性上 。然后 遍历每个定义中的 types 数组(具体的单个元素/属性定义组成的集合)调用 registerType 来注册到 Registry 实例的 typeMap 上面。
js
Registry.prototype.registerPackage = function(pkg) {
pkg = assign({}, pkg);
var pkgMap = this.packageMap;
ensureAvailable(pkgMap, pkg, 'prefix');
ensureAvailable(pkgMap, pkg, 'uri');
forEach(pkg.types, bind(function(descriptor) {
this.registerType(descriptor, pkg);
}, this));
pkgMap[pkg.uri] = pkgMap[pkg.prefix] = pkg;
this.packages.push(pkg);
};
Registry.prototype.registerType = function(type, pkg) {
type = assign({}, type, {
superClass: (type.superClass || []).slice(),
extends: (type.extends || []).slice(),
properties: (type.properties || []).slice(),
meta: assign(({}, type.meta || {}))
});
var ns = parseNameNs(type.name, pkg.prefix),
name = ns.name,
propertiesByName = {};
forEach(type.properties, bind(function(p) {
var propertyNs = parseNameNs(p.name, ns.prefix),
propertyName = propertyNs.name;
if (!isBuiltInType(p.type)) {
p.type = parseNameNs(p.type, propertyNs.prefix).name;
}
assign(p, { ns: propertyNs, name: propertyName });
propertiesByName[propertyName] = p;
}, this));
assign(type, { ns, name, propertiesByName });
forEach(type.extends, bind(function(extendsName) {
var extendsNameNs = parseNameNs(extendsName, ns.prefix);
var extended = this.typeMap[extendsNameNs.name];
extended.traits = extended.traits || [];
extended.traits.push(name);
}, this));
this.definePackage(type, pkg);
this.typeMap[name] = type;
};
function ensureAvailable(packageMap, pkg, identifierKey) {
var value = pkg[identifierKey];
if (value in packageMap) {
throw new Error('package with ' + identifierKey + ' <' + value + '> already defined');
}
}
当然,在 registrerType 的过程中,除了最后将这个元素/属性的类型声明绑定到 typeMap 上之外,还会处理声明中的 properties 属性配置。
这个处理过程中,会 解析每一个 property 的属性名 name 字段,并且将 properties 中的所有属性组成一个 propertiesByName 对象合并到每一个元素/属性类型定义中。
最终,Registry 实例会含有如下结构:
new Moddle() - 实例创建之后
在 上述四个依赖模块都了解清楚之后,我们在回到之前的 Moddle 原型方法与实例创建。
已知 Moddle 原型上定义了 9 个方法,差不多可以分为以下几种:
- 元素实例创建:
create与createAny - 元素构造函数生成:
getType - 已注册的
package描述对象查询:getPackage与getPackages - 某个属性的具体定义描述
descriptor查询:getElementDescriptor、getPropertyDescriptor与getTypeDescriptor - 判断元素实例或者
model实例上是否具有某个属性:hasType
并且这些方法,本质上还是对上述 Factory、Registry、Properties 这几个模块提供的方法的一个封装。
回到 model.create 实例创建
首先是 create 方法:
js
Moddle.prototype.create = function(descriptor, attrs) {
var Type = this.getType(descriptor);
if (!Type) {
throw new Error('unknown type <' + descriptor + '>');
}
return new Type(attrs);
};
这个方法的作用就是 创建一个 getType 返回的类型构造函数对应的实例。
而 getType 方法内部就是 通过 registry.getEffectDescriptor 获取到我们已注册的某个指定类型的描述对象,然后通过 factory.createType 根据这个描述对象生成一个 ModdleElement 构造函数。
js
Moddle.prototype.getType = function(descriptor) {
var cache = this.typeCache;
var name = isString(descriptor) ? descriptor : descriptor.ns.name;
var type = cache[name];
if (!type) {
descriptor = this.registry.getEffectiveDescriptor(name);
type = cache[name] = this.factory.createType(descriptor);
}
return type;
};
例如:
js
const model = new Moddle([Properties])
const ComplexType = model.getType('props:Complex')
const SimpleBody = model.getType('props:SimpleBody')
const Attributes = model.getType('props:Attributes')
console.log(ComplexType.toString(), '\n', SimpleBody.toString(), '\n', Attributes.toString())
会得到这样三个函数:
所以 通过 model.create() 创建的对象实例,类型都是 ModdleElement。
另一个方法 ------ createAny
在 moddle.prototype 中,还有一个方法 createAny,其描述也是创建一个实例。但是该方法与 create 的创建却有很大的区别。
js
Moddle.prototype.createAny = function(name, nsUri, properties) {
var nameNs = parseName(name);
var element = {
$type: name,
$instanceOf: function(type) {
return type === this.$type;
},
get: function(key) {
return this[key];
},
set: function(key, value) {
set(this, [ key ], value);
}
};
var descriptor = {
name: name,
isGeneric: true,
ns: {
prefix: nameNs.prefix,
localName: nameNs.localName,
uri: nsUri
}
};
this.properties.defineDescriptor(element, descriptor);
this.properties.defineModel(element, this);
this.properties.define(element, 'get', { enumerable: false, writable: true });
this.properties.define(element, 'set', { enumerable: false, writable: true });
this.properties.define(element, '$parent', { enumerable: false, writable: true });
this.properties.define(element, '$instanceOf', { enumerable: false, writable: true });
forEach(properties, function(a, key) {
if (isObject(a) && a.value !== undefined) {
element[a.name] = a.value;
} else {
element[key] = a;
}
});
return element;
};
该方法不会像 create 一样借助 Factory 来创建一个 ModdleElement 构造函数,而是直接创建一个对象,并为这个对象创建一个私有的 ns 命名空间对象,然后设置其定义属性。
例如:
js
model.createAny('other:Foo', 'http://other', {
bar: 'BAR'
});
最终得到如下对象:
并且,也只有这种情况下,
isGeneric才会为true。这个标识字段在xml解析和转换的时候也会使用。
小节
moddle 仓库,本身在 bpmn-js 或者 dmn-js 等基于 diagram-js 开发的图形绘制库中,相当于 对元素及元素扩展属性的一个标准化处理模块 ,它 规定了如何注册元素类型与属性更新、绑定的规则 ,也是最后实现 xml 与 JavaScript 对象之间互相转化的底层依赖。