bpmn-js 源码篇10:moddle-xml与bpmn-moddle - xml 与 js 对象之间的核心转换库

上一节 Moddle 的讲解中,已经清楚了 bpmn-js编辑图形的过程中 是通过什么方式来规定每个元素与属性的合法性的,知道了我们所说的 属性描述对象 descriptor json 到底是一个什么结构。

而从关系图中,bpmn-js 是依赖 bpmn-moddle 来处理 xml 的,而 bpmn-moddle 又依赖 moddlemoddle-xml。既然上一节 moddle 掌握了,那么这一节就直接进入 moddle-xmlbpmn-moddle

moddle-xml - Read and write XML documents described with moddle

官方给这个仓库的定义就是 "一个用来根据 moddle 定义读写 xml 文档" 的库。

moddle-xml 对外提供了两个工具:ReaderWriter,分别用来解析 xml 和生成 xml。

Reader 的解析,则是依赖了另外一个库 ------ saxen

Saxen - A tiny, super fast, namespace aware sax-style XML parser written in plain JavaScript.

saxen,是由 bpmn-io 团队核心成员 nikku 根据 vflash/easysax 修改之后的一个 xml 解析库,其特点是:可解析 namespace 命名空间、代码量小、解析速度快等。

当然,由于相对于其他解析库,saxen 在代码量以及解析速度上得到了提升,但是也缺少了 text trimming 文本修剪、entity decoding 实体解码等;但是这些在 BPMN 关联业务中没有特别明显的使用需求,所以影响不大。

saxen 对外提供了两个模块:Parser 构造函数与 decode 工具函数。

decode 函数 - 解析实体编码返回原始字符

decode 解码函数,主要作用就是 通过 string 字符串的 repalce 函数,将字符串中的 HTML 实体字符串转换为原来的字符

在这个文件中,先定义了 一个实体字符串的匹配正则 ENTITY_PATTERN 和一个实体与原字符的对应关系 ENTITY_MAPPING

js 复制代码
var ENTITY_PATTERN = /&#(\d+);|&#x([0-9a-f]+);|&(\w+);/ig;

var ENTITY_MAPPING = {
  'amp': '&',
  'apos': '\'',
  'gt': '>',
  'lt': '<',
  'quot': '"'
};

// map UPPERCASE variants of supported special chars
Object.keys(ENTITY_MAPPING).forEach(function(k) {
  ENTITY_MAPPING[k.toUpperCase()] = ENTITY_MAPPING[k];
});

也就是说,在转换后的 xml 字符串中,遇到 &amp;、 &AMP;、 &gt; 等字符串,都会转换成 &, > 等对应字符(正则表达式确认的匹配规则是必须以 & 作为开头)。

当然,具体的替换规则还是在 decode 相关的方法:

js 复制代码
var fromCharCode = String.fromCharCode;
var hasOwnProperty = Object.prototype.hasOwnProperty;

function replaceEntities(_, d, x, z) {
  if (z) {
    if (hasOwnProperty.call(ENTITY_MAPPING, z)) {
      return ENTITY_MAPPING[z];
    } else {
      return '&' + z + ';';
    }
  }

  if (d) {
    return fromCharCode(d);
  }
  
  return fromCharCode(parseInt(x, 16));
}

export default function decodeEntities(s) {
  if (s.length > 3 && s.indexOf('&') !== -1) {
    return s.replace(ENTITY_PATTERN, replaceEntities);
  }

  return s;
}

在默认导出的 decodeEntities 方法中,本质就是判断字符串是否需要进行解析,然后在需要解析的时候返回解析后的结果,否则返回原字符串,核心还是在 replaceEntities 方法。

根据 MDN/String.prototype.replace() 的描述,replaceEntities 的函数参数与正则表达式结构有关。而原来的 ENTITY_PATTERN 经过分解,会分为三个分组:

  • 第一组为 匹配 &# 开头且以分号 ; 结尾,中间包含一个数字以上 ,将内部数字作为替换方法中的第二个参数 d
  • 第二组为 匹配 &#x 开头且以分号 ; 结尾,中间包含一个或以上任意数字字母 ,将内部数字 + 字母作为替换方法中的第三个参数 x
  • 第三组为 匹配 & 开头且以 ; 结尾,中间包含一个或及以上英文单词 ,将内部字符串作为替换方法中的第三个参数 z

然后,replaceEntities 会区分以上三种情况分别进行处理:

  • 第三组匹配时,z 不为空,则判断是否是 ENTITY_MAPPING 中的某个实体,是则返回对应的字符,不是则按初始状态返回(拼接回 & 与分号)
  • 第一组匹配时,则表示是 10 进制的数据,通过 fromCharCode 将其转换为 UTF-16 码元序列字符串
  • 以上两组都不匹配时,默认为 16 进制数据,则通过 parseInt 将其转换回 16 进制之后再通过 fromCharCode 转换为 UTF-16 码元序列字符串

例如,我们有这样一个字符串:const encoded = "&amp;'&gt;&lt;&quot;&Quot;&QUOT;&#39;&#38;&#0123;&#x0130;>>&constructor;&#NaN;"

通过 decodeEntities(encoded),我们会得到这样一个结果:"&'><"&Quot;"'&{İ>>&constructor;&#NaN;"

对应如下:

text 复制代码
&amp;          => &
'         => '
&gt;           => >
&lt;           => <
&quot;         => "
&Quot;         => &Quot;
&QUOT;         => "
&#39;          => '
&#38;          => &
&#0123;        => {
&#x0130;       => İ
>>        => >> 
&constructor;  => &constructor;
&#NaN;         => &#NaN;

Parser - 解析器

该构造函数作为解析 xml 字符串的核心,主要就是提供一个 parse 方法,接收一个 字符串 ,开始遍历(并非一个字符一个字符的遍历)字符串的并 触发相关钩子函数

该函数接收一个 包含 proxy 布尔值的对象参数,用来确定是否开启 代理模式 ;在 Parser 的设计中,parse 方法 主要用于开启 xml 字符串解析,但没有返回值 ,解析结果 需要通过注册 openTag 之类的钩子函数 类自行处理。

Parser 中,支持通过 on 方法注册一下钩子函数:

  • openTag(elementName, attrGetter, decodeEntities, selfClosing, contextGetter)
  • closeTag(elementName, decodeEntities, selfClosing, contextGetter)
  • error(err, contextGetter)
  • warn(warning, contextGetter)
  • text(value, decodeEntities, contextGetter)
  • cdata(value, contextGetter)
  • comment(value, decodeEntities, contextGetter)
  • attention(str, decodeEntities, contextGetter)
  • question(str, contextGetter)

当然,如果开启了代理模式的话,openTagcloseTag 会修改为一下形式:

  • openTag(element, decodeEntities, selfClosing, contextGetter)
  • closeTag(element, selfClosing, contextGetter)

代理模式下,第一个参数是一个对象,包含 name, originalName, attrs, ns 四个属性。

另外:selfClosing 是一个自闭合标签的标识字段,contextGetter 则是 当前解析内容在 xml 中所处的位置和原始文本信息,这个常常用来进行错误信息提示,后面会省略这部分的说明。

Parser 构造函数,默认只提供了 4 个实例方法

  1. ns:接收一个命名空间与前缀的对应关系对象 nsMap,将其保存在构造函数的闭包对象 nsUriToPrefix 中,以供后续解析使用
  2. parse:接收一个 xml 字符串,然后开启解析
  3. on:接收一个生命周期钩子的名称与对应的回调函数,注册对应的生命周期钩子
  4. stop:停止解析

在实际的解析过程中,我们一般会通过 的形式来保存和处理当前解析到的元素,通过 openTag 入栈,通过 closeTag 出栈,这样能保证在解析一个合法的 xml 时能正确的保存解析结果。

warnerror 则是在解析出现异常时,用来保存解析到的错误信息并抛出错误;或者在严重时终止执行。

textcdata 大部分情况下用来解析标签属性或者特殊标识,将其进行正确的编码解码;commentquestion 等在正常的业务中,通常只是用来进行说明或者标注,并不会影响 xml 的实际业务,所以不用处理也不会有太大影响。

Parser.parse - 开始 xml 解析

parse 方法,整个代码的长度大约有 700 行,已经算是一个比较复杂的函数了。

但是该方法的核心还是比较容易理解的:通过定义两个下标 ij,通过 匹配第一个尖括号 < 来开始遍历 ,然后根据 尖括号后面的第一个字符 w 来进行分类

w, q 等字符都已经通过 String.proptotype.charCodeAt 进行了转码。

  • w === 33,代表是 <!,此时会区分 w 后一个字符 q

    • q === 91 且 前几位是 CDATA[,则调用 CDATA 对应的钩子函数 on('cdata', cb) 将参数进行解析后调用 cb(如果末尾没有 ]] 的结束符,则会抛出异常)
    • q === 45q 后一位也是 45,表示 <-- 注释,此时触发 on('comment', cb) 对应的钩子函数
  • w === 63,代表 <?,作为一个问题标签进行解析,此时触发 on('question', cb) 对应的钩子函数

以上几种情况,都会在判断里面 首先查找对应的结束标志,例如 CDATA 对应 ]] , 注释节点 <-- 对应 --> 等,找到之后会记录结束标志的下标来更新之前的位置记录变量 j ,然后跳出当前循环。

如果都不满足以上情况,则会开始循环 i 后的剩余字符,从第一个字符 v = xml.charCodeAt(i+1) 开始(也就是尖括号 < 之后的第一个字符),开始解析引号的匹配关系,作为后续的 标签属性 来处理;直到解析到反括号 > 停止循环,记录反括号下标 j

  • w === 33,代表 <!,但是这时已经是非 CDATA 或者注释节点的情况,此时触发 on('attention', cb) 对应的钩子函数

  • w === 47,则表示 </,意思是解析到了 结束标签 ,此时会设置一个标志位 tagEnd = true,并且将 栈顶 元素取出与当前解析到的标签名进行匹配,确定是否是同一个标签

  • 如果以上都不满足,则进入 else 部分,这里也会区分几种情况

    1. 解析出来结尾是 /> ,则是自闭合标签,设置标志位 tagStart = tagEnd = true
    2. 否则设置标志位 tagStart = true; tagEnd = false

    并且会记录下标签名,然后会 验证标签名的合法性(只能以字母、下划线、英文冒号作为开头)

    然后,则是 按空格或者特殊标识(\f\n\r\t\v)划分,找到真正的标签名 ,当 tagEndtrue 时,还会将这个标签名 插入到栈中

然后,则会根据 namespace 对属性进行 前缀补充 等操作。

最后,会根据 tagStart、tagEnd 的情况分别触发 on('openTag', cb)on('endTag', cb) 对应的回调函数。

这个过程就是 对单个标签内容的匹配和解析 ,结束之后,还会继续解析 xml 字符串的剩余内容。

每次循环开始的时候 ,都会 判断剩余内容是否还有 < 尖括号 ,如果 没有尖括号,但是栈内还有元素的话,则说明 xml 结构有问题 ;如果 存在尖括号标识的起始标签,但是又存在外部的文本的话 ,则会抛出异常 non-whitespace outside of root node;最后则是 标签内部的文本内容 ,会解析出来通过 on('text', cb) 触发文本对应的回调。

大致的一些解析情况如下:

js 复制代码
parser.parse(`<div></div>`) ✅
parser.parse(`<doc><element id="sample>error"></element></doc>`) ✅
parser.parse(`<doc> \n<element id="sample>error" > \n </element></doc>`) ✅
parser.parse(`\n\x01asdasd`) // Error: missing start tag
parser.parse(`</a>`) // Error: missing open tag
parser.parse(`<!-- HELLO`) // Error: unclosed comment
parser.parse(`<open /`) // Error: unclosed tag
parser.parse(`<=div></=div>`) // Error: illegal first char nodeName
parser.parse(`<div=></div=>`) // Error: illegal first char nodeName
parser.parse(`<a><b></c></b></a>`) // Error: closing tag mismatch
parser.parse(`<root></rof</root>`) // Error: closing tag mismatch
parser.parse(`<root><foo>`) // Error: unexpected end of file
parser.parse(`a<root />`) // Error: non-whitespace outside of root node
parser.parse(`<root />a`) // Error: non-whitespace outside of root node
parser.parse(`<a$uri:foo xmlns:a$uri="http://not-atom" />`) // Error: invalid nodeName

尝试解析一个 xml

在了解了 parse 方法如何解析之后了,我们就可以尝试来解析一个 xml 字符串了。

在上文中,说过 parse 方法没有返回值,全靠遍历过程中触发对应的钩子函数来解析字符串,所以我们也需要通过这种方式来实现一个 xml 的基础解析工具。

假设现在有如下的 xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 
  xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" 
  xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" 
  xmlns:di="http://www.omg.org/spec/DD/20100524/DI" 
  xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  id="Definitions_1" 
  targetNamespace="http://bpmn.io/schema/bpmn" 
  exporter="Camunda Modeler" 
  exporterVersion="1.5.0-nightly">
  <bpmn:process id="Process_1" isExecutable="false">
    <bpmn:exclusiveGateway id="ExclusiveGateway_0loxwm5">
      <bpmn:outgoing>SequenceFlow_06jpd22</bpmn:outgoing>
    </bpmn:exclusiveGateway>
    <bpmn:task id="Task_1bs98ro">
      <bpmn:incoming>SequenceFlow_06jpd22</bpmn:incoming>
      <bpmn:multiInstanceLoopCharacteristics>
        <bpmn:loopCardinality>10</bpmn:loopCardinality>
        <bpmn:completionCondition>${true}</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:task>
    <bpmn:sequenceFlow id="SequenceFlow_06jpd22" name="" sourceRef="ExclusiveGateway_0loxwm5" targetRef="Task_1bs98ro">
      <bpmn:conditionExpression>${foo}</bpmn:conditionExpression>
    </bpmn:sequenceFlow>
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
      <bpmndi:BPMNShape id="ExclusiveGateway_0loxwm5_di" bpmnElement="ExclusiveGateway_0loxwm5" isMarkerVisible="true">
        <dc:Bounds x="127" y="152" width="50" height="50" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="152" y="207" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Task_1bs98ro_di" bpmnElement="Task_1bs98ro">
        <dc:Bounds x="281" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_06jpd22_di" bpmnElement="SequenceFlow_06jpd22">
        <di:waypoint xsi:type="dc:Point" x="177" y="177" />
        <di:waypoint xsi:type="dc:Point" x="216" y="177" />
        <di:waypoint xsi:type="dc:Point" x="216" y="177" />
        <di:waypoint xsi:type="dc:Point" x="281" y="177" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="231" y="177" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

这是导出的一个简易的流程图文件。

那么第一步,我们需要定义一个 ,用来保存和匹配解析到的元素。

js 复制代码
export function createStack() {
  const stack = []

  Object.defineProperty(stack, 'peek', {
    value: function () {
      return this[this.length - 1]
    }
  })

  return stack
}

这里设置的一个属性 peek,用来获取 栈顶 元素。

然后,我们可以通过 createStack 方法创建一个栈。但是此时需要注意的是,我们 xml 元素是 存在父子关系 的,所以我们可以 通过对象变量赋值时共享引用地址 的方式,将栈元素都设置为对象,并且 通过设置 children 对象数组属性,将栈内的元素按顺序进行关联

代码如下:

js 复制代码
const stack = createStack()
// 解析到开始类标签 
function handleOpen(node) {
  try {
    let handler = stack.peek()
    if (!handler) {
      stack[0] = node // 为 undefined 时说明栈内为空,直接赋值第一个元素
    } else {
      // 再次解析到开始标签时,说明这个标签是上一个标签的子节点,则直接插入到上一个元素的 children 数组中
      !handler.children && (handler.children = [])
      handler.children.push(node)
    }
    // 向 栈中 插入该元素
    stack.push(node)
  } catch (err) {
    console.log(err)
  }
}
// 解析到结束标签时,直接出栈
function handleClose() {
  stack.pop()
}
// 解析到文字时,说明是标签内文本,直接设置为栈顶元素的 body 属性
function handleText(text) {
  // 去除空格
  if (!text.trim()) {
    return
  }
  const element = stack.peek()
  element.body = text
}

然后,就可以初始化一个 Parser 实例并注册相应的解析钩子函数了。

js 复制代码
const proxy = false
const xmlParser = new Parser({ proxy })

// 默认的命名空间
xmlParser.ns({
  'http://www.omg.org/spec/BPMN/20100524/MODEL': 'bpmn',
  'http://www.omg.org/spec/BPMN/20100524/DI': 'bpmndi',
  'http://www.omg.org/spec/DD/20100524/DI': 'di',
  'http://www.omg.org/spec/DD/20100524/DC': 'dc',
  'http://www.w3.org/2001/XMLSchema-instance': 'xsi'
})

xmlParser
  .on('openTag', onOpenTag)
  .on('closeTag', handleClose)
  .on('text', function (text, decodeEntities, getContext) {
  handleText(decodeEntities(text), getContext)
})

值得注意的是,这里的 openTag 对应的钩子函数,其实需要区分 proxy 来设置不同的参数处理:

js 复制代码
const onOpenTag = proxy
  ? function (obj, decodeStr, selfClosing, getContext) {
    const attrs = obj.attrs || {}
    const decodedAttrs = Object.keys(attrs).reduce(function (d, key) {
      d[key] = decodeStr(attrs[key])
      return d
    }, {})
    const node = {
      name: obj.name,
      originalName: obj.originalName,
      attributes: decodedAttrs,
      ns: obj.ns
    }
    handleOpen(node, getContext)
  }
  : function (elementName, attrGetter, decodeEntities, selfClosing, getContext) {
    const attrs = attrGetter()
    const decodedAttrs = Object.keys(attrs).reduce(function (d, key) {
      d[key] = decodeEntities(attrs[key])
      return d
    }, {})
    const node = {
      elementName,
      attributes: decodedAttrs
    }
    handleOpen(node, getContext)
  }

两种方式对应的处理结果也会有部分区别:

这里因为只是 示例,所以两侧的结果看起来只是属性多少的问题,差异并不是很大。

但是在 moddle-xml 中,针对不同的命名空间有不同的操作,所以基本上都会使用代理模式,读取到对应的命名空间对象来进行二次处理。

moddle-xml

作为专为 bpmn-io 团队内部的一些项目做支撑的库,moddle-xmlsaxen 的基础上,通过 moddle 来实现 xml 字符串内容的标准化解析

所以官方给这个仓库的定义就是:"XML import/export for documents described with moddle",意为 "根据 moddle 类型描述来实现 xml解析和生成"。

既然支持 解析和生成 ,那么 moddle-xml 肯定就需要提供不同的方法来分别实现 xml 解析与生成 xml,所以它导出了两个模块(类):ReaderWriter。其中 Reader 负责解析,Writer 负责生成。

Reader - A reader for a meta-model

Reader 类本身依赖于 Moddle 模块,提供一个 fromXML 方法来解析 xml 字符串。

在初始化时,Reader 接受一个包含 Moddle 实例的配置参数 options:

typescript 复制代码
type Options = Moddle | { model: Moddle, lax?: boolean }

export function Reader(options: Options) {
  if (options instanceof Moddle) {
    options = {
      model: options
    };
  }

  assign(this, { lax: false }, options);
}

当然,在开始解析 formXML 方法之前,我们做一些准备工作。

common.js ------ 常量与工具函数

js 复制代码
export function hasLowerCaseAlias(pkg) {
  return pkg.xml && pkg.xml.tagAlias === 'lowerCase';
}

export var DEFAULT_NS_MAP = {
  'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
  'xml': 'http://www.w3.org/XML/1998/namespace'
};

export var SERIALIZE_PROPERTY = 'property';

export function getSerialization(element) {
  return element.xml && element.xml.serialize;
}

export function getSerializationType(element) {
  const type = getSerialization(element);

  return type !== SERIALIZE_PROPERTY && (type || null);
}

在这个文件中,定义了一个默认的命名空间对象 DEFAULT_NS_MAP 和一个 序列化属性标识 SERIALIZE_PROPERTY,以及一个 小写驼峰的判断方法 hasLowerCaseAlias 和元素序列化类型判断的方法 getSerializationType

Handlers ------ 不同的元素/属性的处理程序

在开始解析之前,Reader 定义了很多的 Handler 构造函数,用来处理不同的元素/属性,以及一个 Context 构造函数,用来记录解析过程中的上下文:

js 复制代码
export function Context(options) {
  assign(this, options);

  this.elementsById = {};
  this.references = [];
  this.warnings = [];

	// 添加未解析到的引用
  this.addReference = function(reference) {
    this.references.push(reference);
  };

	// 添加已解析的元素
  this.addElement = function(element) {
    if (!element) {
      throw error('expected element');
    }

    var elementsById = this.elementsById;
    var descriptor = getModdleDescriptor(element);
    var idProperty = descriptor.idProperty,
        id;

    if (idProperty) {
      id = element.get(idProperty.name);
      if (id) {
        if (!/^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i.test(id)) {
          throw new Error('illegal ID <' + id + '>');
        }

        if (elementsById[id]) {
          throw error('duplicate ID <' + id + '>');
        }

        elementsById[id] = element;
      }
    }
  };
  
  // 插入警告信息
  this.addWarning = function(warning) {
    this.warnings.push(warning);
  };
}

// 基础的抽象 Handler
function BaseHandler() {}
BaseHandler.prototype.handleEnd = function() {};
BaseHandler.prototype.handleText = function() {};
BaseHandler.prototype.handleNode = function() {};

// 一个简单的传递处理程序,除了忽略它接收的所有输入之外什么也不做。用于忽略未知的元素和属性
function NoopHandler() { }
NoopHandler.prototype = Object.create(BaseHandler.prototype);
NoopHandler.prototype.handleNode = function() {
  return this;
};

// 标签内部的文本处理
function BodyHandler() {}
BodyHandler.prototype = Object.create(BaseHandler.prototype);
BodyHandler.prototype.handleText = function(text) {
  this.body = (this.body || '') + text;
};

// 元素引用类型属性的处理程序
function ReferenceHandler(property, context) {
  this.property = property;
  this.context = context;
}
ReferenceHandler.prototype = Object.create(BodyHandler.prototype);
ReferenceHandler.prototype.handleNode = function(node) {
  if (this.element) {
    throw error('expected no sub nodes');
  } else {
    this.element = this.createReference(node);
  }
  return this;
};
ReferenceHandler.prototype.handleEnd = function() {
  this.element.id = this.body;
};
ReferenceHandler.prototype.createReference = function(node) {
  return { property: this.property.ns.name, id: ''  };
};

// 最简单的属性值处理程序
function ValueHandler(propertyDesc, element) {
  this.element = element;
  this.propertyDesc = propertyDesc;
}
ValueHandler.prototype = Object.create(BodyHandler.prototype);
ValueHandler.prototype.handleEnd = function() {
  var value = this.body || '',
      element = this.element,
      propertyDesc = this.propertyDesc;

  value = coerceType(propertyDesc.type, value);

  if (propertyDesc.isMany) {
    element.get(propertyDesc.name).push(value);
  } else {
    element.set(propertyDesc.name, value);
  }
};

// 基础的元素创建程序
function BaseElementHandler() {}
BaseElementHandler.prototype = Object.create(BodyHandler.prototype);
BaseElementHandler.prototype.handleNode = function(node) {
  var parser = this,
      element = this.element;

  if (!element) {
    element = this.element = this.createElement(node);
    this.context.addElement(element);
  } else {
    parser = this.handleChild(node);
  }

  return parser;
};

// 普通的元素处理程序
export function ElementHandler(model, typeName, context) {
  this.model = model;
  this.type = model.getType(typeName);
  this.context = context;
}
ElementHandler.prototype = Object.create(BaseElementHandler.prototype);
ElementHandler.prototype.addReference = function(reference) {
  this.context.addReference(reference);
};
ElementHandler.prototype.handleText = function(text) {
  var element = this.element,
      descriptor = getModdleDescriptor(element),
      bodyProperty = descriptor.bodyProperty;

  if (!bodyProperty) {
    throw error('unexpected body text <' + text + '>');
  }

  BodyHandler.prototype.handleText.call(this, text);
};
ElementHandler.prototype.handleEnd = function() {
  var value = this.body,
      element = this.element,
      descriptor = getModdleDescriptor(element),
      bodyProperty = descriptor.bodyProperty;

  if (bodyProperty && value !== undefined) {
    value = coerceType(bodyProperty.type, value);
    element.set(bodyProperty.name, value);
  }
};
ElementHandler.prototype.createElement = function(node) {
  var attributes = node.attributes,
      Type = this.type,
      descriptor = getModdleDescriptor(Type),
      context = this.context,
      instance = new Type({}),
      model = this.model,
      propNameNs;

  forEach(attributes, function(value, name) {
    var prop = descriptor.propertiesByName[name],
        values;

    if (prop && prop.isReference) {
      if (!prop.isMany) {
        context.addReference({
          element: instance,
          property: prop.ns.name,
          id: value
        });
      } else {
        values = value.split(' ');
        forEach(values, function(v) {
          context.addReference({
            element: instance,
            property: prop.ns.name,
            id: v
          });
        });
      }
    } else {
      if (prop) {
        value = coerceType(prop.type, value);
      } 
      else if (name === 'xmlns') {
        name = ':' + name;
      } else {
        propNameNs = parseNameNS(name, descriptor.ns.prefix);
        if (model.getPackage(propNameNs.prefix)) {
          context.addWarning({
            message: 'unknown attribute <' + name + '>',
            element: instance,
            property: name,
            value: value
          });
        }
      }
      instance.set(name, value);
    }
  });

  return instance;
};
ElementHandler.prototype.getPropertyForNode = function(node) {
  var name = node.name;
  var nameNs = parseNameNS(name);
  var type = this.type,
      model = this.model,
      descriptor = getModdleDescriptor(type);
  var propertyName = nameNs.name,
      property = descriptor.propertiesByName[propertyName];

  if (property && !property.isAttr) {
    const serializationType = getSerializationType(property);

    if (serializationType) {
      const elementTypeName = node.attributes[serializationType];
      if (elementTypeName) {
        const normalizedTypeName = normalizeTypeName(elementTypeName, node.ns, model);
        const elementType = model.getType(normalizedTypeName);
        return assign({}, property, {
          effectiveType: getModdleDescriptor(elementType).name
        });
      }
    }

    return property;
  }

  var pkg = model.getPackage(nameNs.prefix);

  if (pkg) {
    const elementTypeName = aliasToName(nameNs, pkg);
    const elementType = model.getType(elementTypeName);
    
    property = find(descriptor.properties, function(p) {
      return !p.isVirtual && !p.isReference && !p.isAttribute && elementType.hasType(p.type);
    });

    if (property) {
      return assign({}, property, {
        effectiveType: getModdleDescriptor(elementType).name
      });
    }
  } else {
    property = find(descriptor.properties, function(p) {
      return !p.isReference && !p.isAttribute && p.type === 'Element';
    });

    if (property) {
      return property;
    }
  }

  throw error('unrecognized element <' + nameNs.name + '>');
};
ElementHandler.prototype.toString = function() {
  return 'ElementDescriptor[' + getModdleDescriptor(this.type).name + ']';
};
ElementHandler.prototype.valueHandler = function(propertyDesc, element) {
  return new ValueHandler(propertyDesc, element);
};
ElementHandler.prototype.referenceHandler = function(propertyDesc) {
  return new ReferenceHandler(propertyDesc, this.context);
};
ElementHandler.prototype.handler = function(type) {
  if (type === 'Element') {
    return new GenericElementHandler(this.model, type, this.context);
  } else {
    return new ElementHandler(this.model, type, this.context);
  }
};
ElementHandler.prototype.handleChild = function(node) {
  var propertyDesc, type, element, childHandler;

  propertyDesc = this.getPropertyForNode(node);
  element = this.element;
  type = propertyDesc.effectiveType || propertyDesc.type;

  if (isSimpleType(type)) {
    return this.valueHandler(propertyDesc, element);
  }

  if (propertyDesc.isReference) {
    childHandler = this.referenceHandler(propertyDesc).handleNode(node);
  } else {
    childHandler = this.handler(type).handleNode(node);
  }

  var newElement = childHandler.element;
  
  if (newElement !== undefined) {
    if (propertyDesc.isMany) {
      element.get(propertyDesc.name).push(newElement);
    } else {
      element.set(propertyDesc.name, newElement);
    }

    if (propertyDesc.isReference) {
      assign(newElement, { element: element });
      this.context.addReference(newElement);
    } else {
      newElement.$parent = element;
    }
  }

  return childHandler;
};

// 需要执行特殊验证的元素的处理程序,以确保它初始化的节点与处理程序类型 (命名空间) 匹配
function RootElementHandler(model, typeName, context) {
  ElementHandler.call(this, model, typeName, context);
}
RootElementHandler.prototype = Object.create(ElementHandler.prototype);
RootElementHandler.prototype.createElement = function(node) {
  var name = node.name,
      nameNs = parseNameNS(name),
      model = this.model,
      type = this.type,
      pkg = model.getPackage(nameNs.prefix),
      typeName = pkg && aliasToName(nameNs, pkg) || name;
      
  if (!type.hasType(typeName)) {
    throw error('unexpected element <' + node.originalName + '>');
  }

  return ElementHandler.prototype.createElement.call(this, node);
};

// 通用的元素处理程序
function GenericElementHandler(model, typeName, context) {
  this.model = model;
  this.context = context;
}
GenericElementHandler.prototype = Object.create(BaseElementHandler.prototype);
GenericElementHandler.prototype.createElement = function(node) {
  var name = node.name,
      ns = parseNameNS(name),
      prefix = ns.prefix,
      uri = node.ns[prefix + '$uri'],
      attributes = node.attributes;

  return this.model.createAny(name, uri, attributes);
};
GenericElementHandler.prototype.handleChild = function(node) {
  var handler = new GenericElementHandler(this.model, 'Element', this.context).handleNode(node),
      element = this.element;
  var newElement = handler.element,
      children;

  if (newElement !== undefined) {
    children = element.$children = element.$children || [];
    children.push(newElement);
    newElement.$parent = element;
  }

  return handler;
};
GenericElementHandler.prototype.handleEnd = function() {
  if (this.body) {
    this.element.$body = this.body;
  }
};

他们之间的继承关系如下:

js 复制代码
BaseHandler
├── NoopHandler
└── BodyHandler
		├── ReferenceHandler
		├── ValueHandler
    └── BaseElementHandler
      ├── ElementHandler
      │   └── RootElementHandler
      └── GenericElementHandler

BaseHandler 作为最基础的处理程序构造函数,定义了三个方法:handleEnd,handleText,handleNode,基于此衍生出去的 Handlers 都根据自身的需要对某一个或者多个函数进行了实现或者重写,但是大家都实现了一个方法,那就是 handleNode,并且返回值都是该 Handler 对应的实例本身(BodyHandler 是个例外,只需要处理标签内部的内容,所以只有一个 handleText 方法)。

ElementHandler 则是最复杂的一个处理器,需要同时处理属性、body 文本或者子元素等内容,并且需要确保标签元素解析之后的关系正确。

至于每个 Handler 的具体作用,我们可以结合 formXML 解析方法来介绍。

reader.formXML - 解析 XML 为 JS Object

fromXML 方法,逻辑上就是通过 saxenparse 方法来解析 xml 字符串,通过对 parse 方法不同的钩子注册相应的钩子函数,并通过一个栈来存储和匹配已解析的结果,最后返回解析结果。

当然,这个过程中 栈 并没有作为保存解析标签内容的角色,所以还需要其他的角色来负责这部分工作。这时就引入了上文所说的 HandlerContext 了。

js 复制代码
Reader.prototype.fromXML = function(xml, options, done) {
  var rootHandler = options.rootHandler;

  if (options instanceof ElementHandler) {
    rootHandler = options;
    options = {};
  } else {
    if (typeof options === 'string') {
      rootHandler = this.handler(options);
      options = {};
    } else if (typeof rootHandler === 'string') {
      rootHandler = this.handler(rootHandler);
    }
  }

  var model = this.model,
      lax = this.lax;
  var context = new Context(assign({}, options, { rootHandler: rootHandler })),
      parser = new SaxParser({ proxy: true }),
      stack = createStack();

  rootHandler.context = context;
  stack.push(rootHandler);
  
  // 。。。 后续解析过程
}
Reader.prototype.handler = function(name) {
  return new RootElementHandler(this.model, name);
};

function createStack() {
  var stack = [];
  Object.defineProperty(stack, 'peek', {
    value: function() {
      return this[this.length - 1];
    }
  });
  return stack;
}

formXML 方法的开头,if 部分主要是用来 校验参数 ,确保 入栈的第一个元素是 ElementHandler 元素处理器 ;然后,则是将这个 rootHandler 对象作为参数初始化一个 context 对象实例,用来 作为解析过程中的上下文内容保存 ,这样在解析完成之后 context 的内容就是所有的解析结果。

最后,就是 初始化一个 stack ,并将 rootHandler 插入栈中。

然后,需要注册 saxen/Parser 的钩子函数,来处理不同的解析内容

js 复制代码
function handleError(err, getContext, lax) {
  var ctx = getContext();
  var line = ctx.line,
      column = ctx.column,
      data = ctx.data;

  if (data.charAt(0) === '<' && data.indexOf(' ') !== -1) {
    data = data.slice(0, data.indexOf(' ')) + '>';
  }
  var message = '' // 这里省略了错误信息,主要内容包含 err 对象以及 ctx 中的代码位置
  if (lax) {
    context.addWarning({ message, error: err });
    return true;
  } else {
    throw error(message);
  }
}

function handleWarning(err, getContext) {
  return handleError(err, getContext, true);
}

// 引用收集
function resolveReferences() {
  var elementsById = context.elementsById;
  var references = context.references;
  var i, r;

  for (i = 0; (r = references[i]); i++) {
    var element = r.element;
    var reference = elementsById[r.id];
    var property = getModdleDescriptor(element).propertiesByName[r.property];

    if (!reference) {
      context.addWarning({
        message: 'unresolved reference <' + r.id + '>',
        element: r.element,
        property: r.property,
        value: r.id
      });
    }

    if (property.isMany) {
      var collection = element.get(property.name),
          idx = collection.indexOf(r);
      if (idx === -1) {
        idx = collection.length;
      }

      if (!reference) {
        collection.splice(idx, 1); // 无法找到引用时移除
      } else {
        collection[idx] = reference;
      }
    } else {
      element.set(property.name, reference);
    }
  }
}

function handleClose() {
  stack.pop().handleEnd();
}

var PREAMBLE_START_PATTERN = /^<\?xml /i;
var ENCODING_PATTERN = / encoding="([^"]+)"/i;
var UTF_8_PATTERN = /^utf-8$/i;

function handleQuestion(question) {
  if (!PREAMBLE_START_PATTERN.test(question)) {
    return;
  }
  var match = ENCODING_PATTERN.exec(question);
  var encoding = match && match[1];
  if (!encoding || UTF_8_PATTERN.test(encoding)) {
    return;
  }

  context.addWarning({
    message:
    'unsupported document encoding <' + encoding + '>, ' +
    'falling back to UTF-8'
  });
}

function handleOpen(node, getContext) {
  var handler = stack.peek();
  try {
    stack.push(handler.handleNode(node));
  } catch (err) {
    if (handleError(err, getContext, lax)) {
      stack.push(new NoopHandler());
    }
  }
}

function handleCData(text, getContext) {
  try {
    stack.peek().handleText(text);
  } catch (err) {
    handleWarning(err, getContext);
  }
}

function handleText(text, getContext) {
  if (!text.trim()) {
    return;
  }
  handleCData(text, getContext);
}

这里除了 resolveReferences 是用来 解析"引用类型"属性 之外,其他几个方法就分别对应 ParseropenTag,question 等几个解析钩子。但是这些 handle 函数参数很明显与钩子对应的回调函数声明有区别,所以钩子回调函数还需要再次处理参数。不过在此之前,还需要注册相应的 namespace map

js 复制代码
// common.js
//export var DEFAULT_NS_MAP = {
//  'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
//  'xml': 'http://www.w3.org/XML/1998/namespace'
//};

var uriMap = model.getPackages().reduce(function(uriMap, p) {
  uriMap[p.uri] = p.prefix;
  return uriMap;
}, Object.entries(DEFAULT_NS_MAP).reduce(function(map, [ prefix, url ]) {
  map[url] = prefix;
  return map;
}, model.config && model.config.nsMap || {}));

parser
  .ns(uriMap)
  .on('openTag', function(obj, decodeStr, selfClosing, getContext) {
    var attrs = obj.attrs || {};
    var decodedAttrs = Object.keys(attrs).reduce(function(d, key) {
      var value = decodeStr(attrs[key]);
      d[key] = value;
      return d;
    }, {});

    var node = {
      name: obj.name,
      originalName: obj.originalName,
      attributes: decodedAttrs,
      ns: obj.ns
    };

    handleOpen(node, getContext);
  })
  .on('question', handleQuestion)
  .on('closeTag', handleClose)
  .on('cdata', handleCData)
  .on('text', function(text, decodeEntities, getContext) {
    handleText(decodeEntities(text), getContext);
  })
  .on('error', handleError)
  .on('warn', handleWarning);

这里的 uriMap,对应的就是 common.js 中的 DEFAULT_NS_MAP 以及上一篇文章中通过 Moddle 初始化时传递的 descriptor json 组成的对象。这里我们以 bpmn-moddle 中的默认声明对象为例:

最终会得到一个这样的 uriMap 对象:

js 复制代码
{
  "http://www.w3.org/2001/XMLSchema-instance": "xsi",
  "http://www.w3.org/XML/1998/namespace": "xml",
  "http://www.omg.org/spec/BPMN/20100524/MODEL": "bpmn",
  "http://www.omg.org/spec/BPMN/20100524/DI": "bpmndi",
  "http://www.omg.org/spec/DD/20100524/DC": "dc",
  "http://www.omg.org/spec/DD/20100524/DI": "di",
  "http://bpmn.io/schema/bpmn/biocolor/1.0": "bioc",
  "http://www.omg.org/spec/BPMN/non-normative/color/1.0": "color"
}

然后,就是通过 parser.on 方法来注册上文定义好的处理函数;不过这里在 openTag 这个钩子回调函数中,会进行参数处理:

js 复制代码
parser.on('openTag', function(obj, decodeStr, selfClosing, getContext) {
  var attrs = obj.attrs || {};

  var decodedAttrs = Object.keys(attrs).reduce(function(d, key) {
    var value = decodeStr(attrs[key]);
    d[key] = value;
    return d;
  }, {});

  var node = {
    name: obj.name,
    originalName: obj.originalName,
    attributes: decodedAttrs,
    ns: obj.ns
  };

  handleOpen(node, getContext);
})

因为 parser 是一个 代理解析器 ,所以回调函数中第一个参数是一个对象,包含 attrs 标签属性对象、nameoriginalName 标签名,ns 命名空间标识。

这里的 attrs 属性对象,解析出来的值是 没有经过 decode 处理的原始值 ,所以这里会 遍历原始 attrs 对象,分别调用 decode 方法解析属性值

然后,才会重新组装好 node 对象,进入 handleOpen 函数。

handleQuestion,则是用来 校验文件格式与编码格式,非 utf-8 格式的 xml 标识文件 ,抛出 unsupported document encoding 的错误;至于另外几个处理函数,相对而言比较基础,用到时会说明。

parse(xml) 解析过程

当前面的准备工作完成之后,就会通过 parser.parse 方法开始解析 xml 字符串了。我们假设此时有这样的 xml 与对应的 descriptor json

js 复制代码
var xml = '<props:root xmlns:props="http://properties">' +
            '<props:containedCollection id="C_5">' +
              '<props:complex id="C_1" />' +
              '<props:complex id="C_2" />' +
              '<props:complex id="C_3" />' +
            '</props:containedCollection>' +
            '<props:attributeReferenceCollection id="C_4" refs="C_2 C_3 C_5" />' +
            '<props:complexAttrsCol xmlns:props="http://properties">' +
              '<props:attrs integerValue="10" />' +
              '<props:attrs booleanValue="true" />' +
            '</props:complexAttrsCol>' +
          '</props:root>';
// properties.json
{
  "name": "Properties",
  "uri": "http://properties",
  "prefix": "props",
  "xml" : {
    "tagAlias": "lowerCase"
  },
  "types": [
    {
      "name": "Complex",
      "properties": [
        { "name": "id", "type": "String", "isAttr": true, "isId": true },
        { "name": "body", "type": "String", "isBody": true }
      ]
    },
    {
      "name": "ComplexAttrs",
      "superClass": [ "Complex" ],
      "properties": [
        { "name": "attrs", "type": "Attributes", "xml": { "serialize" : "xsi:type" } }
      ]
    },
    {
      "name": "ComplexAttrsCol",
      "superClass": [ "BaseWithId" ],
      "properties": [
        { "name": "attrs", "type": "Attributes", "isMany": true, "xml": { "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": "Body"
    },
    {
      "name": "SimpleBody",
      "superClass": [ "Base", "Body" ],
      "properties": [
        {
          "name": "body",
          "type": "String",
          "isBody": true
        }
      ]
    },
    {
      "name": "WithBody",
      "superClass": [ "Base" ],
      "properties": [
        {
          "name": "someBody",
          "type": "Body",
          "xml": { "serialize" : "xsi:type" }
        }
      ]
    },
    {
      "name": "SimpleBodyProperties",
      "superClass": [ "Base" ],
      "properties": [
        {
          "name": "intValue",
          "type": "Integer"
        },
        {
          "name": "boolValue",
          "type": "Boolean"
        },
        {
          "name": "str",
          "type": "String",
          "isMany": true
        }
      ]
    },
    {
      "name": "WithProperty",
      "superClass": [ "Base" ],
      "properties": [
        { "name": "propertyName", "type": "Base", "xml": { "serialize" : "property" }  }
      ]
    },
    {
      "name": "Base"
    },
    {
      "name": "BaseWithId",
      "superClass": [ "Base" ],
      "properties": [
        { "name": "id", "type": "String", "isAttr": true, "isId": true }
      ]
    },
    {
      "name": "BaseWithNumericId",
      "superClass": [ "BaseWithId" ],
      "properties": [
        { "name": "idNumeric", "type": "String", "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": "id",
          "type": "String",
          "isAttr": true,
          "isId": 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": "AttributeReferenceCollection",
      "superClass": [ "BaseWithId" ],
      "properties": [
        {
          "name": "refs",
          "type": "Complex",
          "isReference": true,
          "isMany": true,
          "isAttr": true
        }
      ]
    }
  ]
}
// properties-extended.json
{
  "name": "Extended",
  "uri": "http://extended",
  "prefix": "ext",
  "xml" : {
    "tagAlias": "lowerCase"
  },
  "types": [
    {
      "name": "ExtendedComplex",
      "superClass": [ "props:ComplexCount" ],
      "properties": [
        { "name": "numCount", "type": "Integer", "isAttr": true, "redefines": "props:Complex#count" }
      ]
    },
    {
      "name": "Root",
      "superClass": [ "props:Root" ],
      "properties": [
        { "name": "elements", "type": "Base", "isMany": true }
      ]
    },
    {
      "name": "Base"
    },
    {
      "name": "CABSBase"
    }
  ]
}

在解析时,首先需要创建一个 RootElementHandler

js 复制代码
const rootHandler = reader.handler('props:Root')

这里的 handler() 方法接受一个参数,用来创建一个 RootElementHandler

js 复制代码
Reader.prototype.handler = function(name) {
  return new RootElementHandler(this.model, name);
};

打印如下:

此时的 handler 实例 context 属性是空的,但是绑定了我们的 new Moddle 的实例,所以可以 访问到我们声明的所有类型和属性的具体定义

但是在 formXML 方法执行过程中,会创建一个 Context 实例绑定到该 handler 实例上:

js 复制代码
var context = new Context(assign({}, options, { rootHandler: rootHandler })),
    parser = new SaxParser({ proxy: true }),
    stack = createStack();
rootHandler.context = context;
stack.push(rootHandler);

会得到一个这样的 handler 对象:

然后,在 parse(xml) 解析我们示例中的 xml 字符串时,会首先进入到 openTag 对应的钩子回调函数中,此时调用 handleOpen 传递的参数是:

js 复制代码
node: {
  "name": "props:root",
  "originalName": "props:root",
  "attributes": {  "xmlns:props": "http://properties" },
  "ns": {
    "xml": "xml",
    "xml$uri": "http://www.w3.org/XML/1998/namespace",
    "props": "props",
    "props$uri": "http://properties",
    "ext": "ext",
    "ext$uri": "http://extended",
    "xsi": "xsi",
    "xsi$uri": "http://www.w3.org/2001/XMLSchema-instance"
  }
}

在进入 handleOpen 方法之后,就会 提取栈中的栈顶 handler 实例 ,通过该实例的 handleNode 方法处理当前节点的内容。

RootElementHandler 对应的 handleNode 方法由上文的源码中可知,是继承的 BaseElementHandler 中的方法。该方法会调用 this.createElement(noed) 来创建一个元素实例,并绑定到 this 上作为 this.element 属性。

这里的 this 就是上文的 RootElementHandler 的对应实例。

不过我们需要注意,createElement 方法只在三个地方有定义:

  1. ElementHandler:实际上的实例创建逻辑,借助 Moddle 模块的实例创建方式,构造特定的 ModdleElement 构造函数来创建,然后通过 set 方法实现实例属性按照 descriptor json 的声明来设置的实例的正确位置(不过,需要注意的一点是,这里针对 reference 引用定义的属性,还是会等到 xml 解析结束之后再设置真实的引用关系)

    也就是说,createElement 方法,创建的是 node 对应的 描述对象 propertyDesc.effectiveType 类型的一个实例对象,下文会说明。

  2. RootElementHandler:会借助 Moddle 里面的所有类型声明,来校验当前传入的 node 是否符合我们的定义;符合则会调用 ElementHandlercreateElement 来创建一个实例,否则则会直接抛出一个 unexpected element 的错误

  3. GenericElementHandler:一样是借助 Moddle 来创建,但是这个方法 只需要命名空间前缀和 uri 属性 ,然后就会通过 model.createAny 创建一个带有 attrs 中所有属性的对象实例

所以当我们解析到 xml 的第一个正常标签,也就是 <props:root> 标签时,会第一次调用 RootElementHandler.prototype.handleNode 方法,并将该方法返回值插入栈中,得到这样的结果:

并且此时栈中的两个元素实际上都是同一个实例 ------ rootHandler

root 标签继续往下解析,会进入 props:containedCollection 标签,此时栈顶元素仍然是 rootHandler,所以进入 handleNode 方法时,this.element 属性存在,会进入 handleChild 方法,并将该方法的返回值直接返回,并作为新的栈元素插入栈顶。

根据上文源码中的 Handler 的相关定义,这几个方法都在 ElementHandler 原型上。

handleChild 方法中,会根据我们解析到的该节点的内容 node,借助 Moddle 获取到这个 node.name 对应的元素或者属性描述 propertyDesc,比如这时的 node 对象对应的描述对象就是:

js 复制代码
propertyDesc = {
  "name": "any",
  "type": "props:Base",
  "isMany": true,
  "ns": { "name": "props:any", "prefix": "props", "localName": "any" },
  "inherited": true,
  "effectiveType": "props:ContainedCollection"
}

这个对象包含了 这个标签对应的在上级标签中作为什么属性、以及该标签对应的完整标签名

然后,会根据 propertyDesc.effectiveType || propertyDesc.type 的值,以及 propertyDesc.isReference ,创建对应的 ValueHandlerReferenceHandlerElementHandler 或者 GenericElementHandler 的实例 childHandler

当实例创建完成,会调用 childHandler.handleNode(node) 来借助 Moddle 实例创建一个 node 对象对应的 element 实例作为 childHandler.element 属性(与 rootHandler.handleNode 类似)来指向该标签的解析结果,当然如果是 ValueHandler 的实例的话,element 这里是 undefined

childHandler.element 存在时,则根据 propertyDesc 中的配置,将其设置为 上级节点对应属性的内容,并设置 $parent 属性指向上级节点实例 ,这里就会设置为 root 标签实例的 any 数组中的一项。然后开始继续向下解析。

当解析到 <props:complex id="C_1" /> 时,一样会进入 BaseElementHandler.prototype.handleNode,然后进入 handleChild,然后与上文一样继续后面的逻辑;但是,这个标签是一个 自闭合标签 ,会立即进入 closeTag 对应的钩子函数,将这个 handler 从栈中移除,并执行 handler.handleEnd():

这时 栈 会恢复到三个元素的状态,栈顶元素指向 <props:containedCollection> 标签对应的 ElementHandler 实例。

但是,假设我们将 xml 字符串适当进行修改,将 <props:complex id="C_2" />修改为 <props:complex id="C_2">complex body string</props:complex>,就会进入另外一个处理函数。

上文 <props:complex id="C_1" /> 自闭合标签时虽然会入栈,然后后面就会立即出栈,但是解析 <props:complex id="C_2">complex body string</props:complex> 时,由于不是自闭合标签,所以解析到 <props:complex id="C_2"> 这里时就会执行 openTag 对应的钩子函数,然后进行上文提到的类似处理方式,得到这样的一个栈:

然后,则会一直匹配到下一个 < 尖括号,从 <props:complex id="C_2"> 到这个尖括号之间的内容,就会触发 text 钩子对应的 handleText 方法。

最终会将这个 text 字符串作为 element.body 属性挂载到 <props:complex id="C_2"> 对应的实例上;然后会解析到 </props:complex> 结束标签,触发 handleClose 执行出栈操作。

不过,虽然出栈之后 stack 栈中没有了这个元素,但是在 context 对象中,依然会记录所有的已解析内容:

然后,会继续如上文类似的处理过程,直到解析到 <props:attributeReferenceCollection id="C_4" refs="C_2 C_3 C_5" /> 标签

在定义中,我们知道 AttributeReferenceCollection 类型的元素,会有一个 refs 数组,里面包含多个 Complex 类型的实例,并且该属性设置的 isReference,即 xml 中体现为 Complex 实例的 id 组成的字符串(isAttrtrue,只能是数字、布尔或者字符串)。

当解析到该标签时,一样会通过 stack.push(handler.handleNode(node)) 进入到 handleChild 方法,然后创建一个新的 ElementHandler 实例再次执行 handleNode(node),最后进入新的 handlercreateElement(node) 方法。

但是,在 createElement 创建的 childHandler 执行 handleNode(node) 时,创建的 attributeReferenceCollection 实例,属性 refs引用类型属性 ,即 prop.isReferencetrue,这时 并不会直接处理实例引用,而是将当前的 attributeReferenceCollection 实例与解析出来的属性值 refs 的对应关系插入到 context

<props:attributeReferenceCollection id="C_4" refs="C_2 C_3 C_5" /> 标签解析结束之后,状态结果如下:

最后,当 parser.parse(xml) 执行结束之后,才会立即执行 resolveReferences() 方法,遍历 context.references,将解析到的 element 实例与引用关系中指定的 id 对应的实例进行重新关联。当然,如果最后 找不到引用的实例 ,则会抛出一个警告,插入到 contextwarnings 数组中。

当引用关系处理完成之后,xml 的解析过程也就基本结束了,最后就是整理输出结果并返回:

js 复制代码
var rootElement = rootHandler.element;

if (!err && !rootElement) {
  err = error('failed to parse document as <' + rootHandler.type.$descriptor.name + '>');
}

var warnings = context.warnings;
var references = context.references;
var elementsById = context.elementsById;

if (err) {
  err.warnings = warnings;
  return reject(err);
} else {
  return resolve({
    rootElement: rootElement,
    elementsById: elementsById,
    references: references,
    warnings: warnings
  });
}

例如我们上文的 xml,最终得到的结果如下:

Writer - A writer for meta-model backed document trees

既然 Reader解析 xml 得到 JavaScript 对象数据 ,那么肯定就需要有一个模块来实现 JavaScript 数据转换为 xml 字符串 的功能,而这部分工作就是 Writer 来完成的。

Reader 类似,Writer 也是一个 构造函数 ,并且也有一个方法 toXML 来实现 js 对象到 xml 的转换。

js 复制代码
export function Writer(options) {
  options = assign({ format: false, preamble: true }, options || {});

  function toXML(tree, writer) {
    // ...
  }

  return {
    toXML
  };
}

由源码可见,Writer 初始化时接受一个可选对象参数 options,包含两个可选配置项:formatpreamble,并且这两个参数具有默认值。

分别用来 处理标签换行的格式化以及 xml 文件头(文件说明和编码格式)

如果 formattruth 值的话,遇到新标签会 换行并插入两个空格字符 ;如果 preambletruth 值的话,会在 xml 头部插入声明部分 <?xml version="1.0" encoding="UTF-8"?> 并换行。

writer.toXML - 将 jS Object 转换为 xml

js 复制代码
function toXML(tree, writer) {
  var internalWriter = writer || new SavingWriter();
  var formatingWriter = new FormatingWriter(internalWriter, options.format);

  if (options.preamble) {
    formatingWriter.append(XML_PREAMBLE);
  }

  var serializer = new ElementSerializer();

  var model = tree.$model;

  serializer.getNamespaces().mapDefaultPrefixes(getDefaultPrefixMappings(model));

  serializer.build(tree).serializeTo(formatingWriter);

  if (!writer) {
    return internalWriter.value;
  }
}

作为转换方法,toXML 接收一个参数 tree 和一个可选参数 writertree 代表我们解析的 js 对象,writer 则是解析器实例,如果为空的话会重新创建一个 SavingWriter

那么我们分别来看一下 toXML 里面用到的几个构造函数。

1. SavingWriter - 结果保存与更新

该函数(构造函数)只有一个作用,生成一个带有 value 属性与 write 方法的对象,value 是记录的 xml 字符串,write 负责接收一个新的子串 str 拼接到 value 上。

js 复制代码
function SavingWriter() {
  this.value = '';

  this.write = function(str) {
    this.value += str;
  };
}

2. FormatingWriter - xml 的格式化写入

负责格式化输出的部分,需要接收一个 SavingWriter 实例(或者一个具有 write 方法的对象),以及配置参数 format

这个构造函数会创建一个 闭包,并创建一个具有 5 个方法的对象。

js 复制代码
function FormatingWriter(out, format) {
  var indent = [ '' ];

  this.append = function(str) {
    out.write(str);
    return this;
  };
  this.appendNewLine = function() {
    if (format) {
      out.write('\n');
    }
    return this;
  };
  this.appendIndent = function() {
    if (format) {
      out.write(indent.join('  '));
    }
    return this;
  };
  this.indent = function() {
    indent.push('');
    return this;
  };
  this.unindent = function() {
    indent.pop();
    return this;
  };
}

其中 indent 数组用来标识 新换行的标签应该具有几个空格 ,也就是我们常说的 缩进 ,这里的缩进设置是 两个空格

indentunindent 两个方法,分别用来向 indent 数组中添加或者移除元素,通过数组元素个数来确定缩进范围;而 appendNewLineappendIndent 则分别用来插入换行标识与新行缩进;append 则是直接调用 writer 对象的 write 方法,直接写入数据。

如果是自定义 writer 的话,write 方法就不一定是拼接了。

ElementSerializer 之前,我们先了解一下其他的 Serializer 和相关配置

3. ESCAPE MAPs - 转码匹配与对应关系

ESCAPE,这里指的是 转码 的意思。

Reader 中,我们知道 Reader 通过 saxen 进行 xml 解析的时候,会 匹配特殊字符进行解码 ,那么同样的,在转换成 xml 的时候,一样需要将特殊字符进行转换。

saxen 的解码规则,主要是依靠公共函数 decode(见上文 [decode 函数 - 解析实体编码返回原始字符](/#decode 函数 - 解析实体编码返回原始字符)),里面处理了 尖括号 < >& 符号和单双英文引号,所以这里会有对应的反向的转换关系;只是这里还 增加了 \n, \r 换行符的特殊处理saxen 默认不处理换行符等内容)。

js 复制代码
var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g;
var ESCAPE_CHARS = /<|>|&/g;

var ESCAPE_ATTR_MAP = {
  '\n': '#10',
  '\n\r': '#10',
  '"': '#34',
  '\'': '#39',
  '<': '#60',
  '>': '#62',
  '&': '#38'
};

var ESCAPE_MAP = {
  '<': 'lt',
  '>': 'gt',
  '&': 'amp'
};

4. Namespaces - 命名空间与前缀管理

这个模块主要就是 管理和查询注册的命名空间与对应前缀,提供了以下几个属性和方法:

属性

  • prefixMap:一个存储前缀到URI映射的对象。
  • uriMap:一个存储URI到命名空间对象映射的对象。
  • used:一个存储被使用的命名空间的对象。
  • wellknown:一个存储被标记为"wellknown"的命名空间的数组。
  • custom:一个存储自定义命名空间的数组。
  • parent:一个指向父命名空间的引用。
  • defaultPrefixMap:一个存储默认前缀到URI映射的对象。

其中 new Namespaces(parent) 时就需要传递一个 Namespaces 对象 作为父级(也可以为空);而 customwellknown 正好相反,保存不同的命名空间内容。

方法

  • mapDefaultPrefixes(defaultPrefixMap):将传入的默认前缀映射对象设置为当前命名空间的默认前缀映射。
  • defaultUriByPrefix(prefix):根据给定的前缀返回默认的URI。
  • byUri(uri):根据给定的URI返回对应的命名空间对象。
  • add(ns, isWellknown):将给定的命名空间对象添加到命名空间中,可以选择将其标记为"wellknown"(isWellknowntruth 进入 wellknown 数组,反之进入 custom 数组)。
  • uriByPrefix(prefix):根据给定的前缀返回对应的 URI
  • mapPrefix(prefix, uri):将给定的前缀和 URI 映射关系添加到命名空间中。
  • getNSKey(ns):根据给定的命名空间对象返回一个唯一的键。
  • logUsed(ns):记录给定的命名空间对象被使用过。
  • getUsed():返回所有被使用过的命名空间对象。

5. Utils - 工具函数

在开始解析 JavaScript 对象的时候,还需要一些工具函数来处理属性或者标签名:

  • lower(string):将字符串的第一个字符转换为小写。
  • nameToAlias(name, pkg):根据给定的名称和包名,返回一个别名。如果包名有小写别名,则将名称转换为小写形式,否则返回原始名称。
  • inherits(ctor, superCtor):实现继承关系,让 ctor 构造函数继承 superCtor 构造函数。
  • nsName(ns):根据给定的命名空间对象 ns,返回命名空间的字符串表示。如果命名空间是字符串类型,则直接返回;否则返回带有前缀的命名空间字符串。
  • getNsAttrs(namespaces):根据给定的命名空间集合 namespaces,获取已使用的命名空间,并将其转换为属性对象数组。过滤掉内置的 xml 命名空间。
  • getElementNs(ns, descriptor):根据给定的命名空间对象 ns 和描述对象 descriptor,获取元素命名空间。如果描述对象中这个类型是通用类型(即 isGenerictrue),则返回原来的 localName 组成的命名空间对象;否则使用 nameToAlias 函数将 localName 转换为别名之后再返回。
  • getPropertyNs(ns, descriptor):根据给定的命名空间对象 ns 和该属性的描述对象 descriptor,获取属性命名空间。
  • getSerializableProperties(element):根据给定的元素对象,获取该元素对应的描述对象 descriptor,拿到 properties 属性数组。过滤掉虚拟属性、默认值和空值属性,返回剩余属性组成的属性对象数组。
  • escape(str, charPattern, replaceMap):将给定的字符串 str 中的特定字符替换为转义字符。使用正则表达式 charPattern 匹配需要替换的字符,并使用 replaceMap 对应的转义字符进行替换。返回替换后的字符串。
  • escapeAttr(str):将给定的字符串 str 中的特殊字符进行转义,以便在属性值中使用。使用 ESCAPE_ATTR_CHARS 正则表达式匹配需要转义的字符,并使用 ESCAPE_ATTR_MAP 对应的转义字符进行替换。返回转义后的字符串。
  • escapeBody(str):将给定的字符串 str 中的特殊字符进行转义,以便在元素内容中使用。使用 ESCAPE_CHARS 正则表达式匹配需要转义的字符,并使用 ESCAPE_MAP 对应的转义字符进行替换。返回转义后的字符串。
  • filterAttributes(props):根据给定的属性数组 props,过滤出所有是属性(即作为 xml 标签的标签属性)的属性对象,并返回过滤后的属性数组。
  • filterContained(props):根据给定的属性数组 props,过滤出所有不是属性的属性对象(即包含在元素内容中的属性),并返回过滤后的属性数组,与上一个方法正好相反。

当这些准备工作做完之后,就可以进入解析和生成阶段了。

解析 JavaScript 对象树生成对应 xml 字符串的部分,在 Writer 中被称为 Serializer,也就是 序列化程序 ,与 JSON.stringify 类似,作用都是 将一个值序列化为一个字符串 ,只是 Serializers 会有特殊的序列化规则。

6. Serializers - 序列化程序

Reader 中的 Handlers 一样,Serializers 也有多个,并且部分也有继承关系:

  • ReferenceSerializer:引用类型序列化处理器
  • BodySerializerbody 属性序列化处理器
  • ValueSerializer:属性值序列化处理器
  • ElementSerializer:通用的标签元素生成器
  • TypeSerializer:需要处理特殊数据类型的序列化处理器

其继承关系如下:

markdown 复制代码
├── ReferenceSerializer
├── BodySerializer
│		└── ValueSerializer
└── ElementSerializer
      └── TypeSerializer

这些 Serializers 都有两个核心方法:

  • build:接收一个元素对象,绑定到 Serializers 的实例上
  • serializeTo:接受一个 Writer 实例,将绑定元素序列化后的结果通过 Writer 插入到序列化结果中

其中 ElementSerializer 是最复杂的 Serializer,它会包含和创建多个 BodySerializerValueSerializerReferenceSerializer 的实例;而它也是负责构建和序列化 xml 最核心的部分,可以解析元素的属性、命名空间和包含的子元素;TypeSerializer 则通过继承 ElementSerializer 重写 parseNsAttributesisLocalNs 方法,通过 检查属性描述对象中是否具有独立的 xml.serialize 序列化配置 ,来更新 xml 中的写入内容。

7. 从序列化过程开始

大致了解完相关内容之后,我们就可以回到 toXML 这个方法了。

该方法在初始化了 formatingWriter 之后,紧接着就是实例化一个 ElementSerializer,然后通过 getNamespacesmapDefaultPrefixes 区分出来 customwellknown 两种不同的 nsMap 命名空间对象(这部分暂时还未发布正式版,在 v10.1.0 中只有下面一步);然后就是通过 build 方法根据 descriptor json 描述对象解析传入的 tree 对象树,最后将解析结果通过 serializeTo 写入最终结果中。

即:

js 复制代码
var serializer = new ElementSerializer();
var model = tree.$model;

// 未发布内容
serializer.getNamespaces().mapDefaultPrefixes(getDefaultPrefixMappings(model));

serializer.build(tree).serializeTo(formatingWriter);

首先, new ElementSerializer 创建了一个包含四个空值属性的对象 serializer

getNamespaces 方法,主要是 serializer.parentserializer.namespaces 来组合一个新的 Namespaces 对象实例,以 serializer.namespaces 为准,但因为此时这里两个属性都是空值,所以会根据 parent?.namespaces 创建一个新的 Namespaces 实例并挂在到 serializer 上然后返回(这里 parent?.namespaces 其实也是空的)。

然后,则是从传入的对象树上拿到 $model (也就是 Moddle 实例),来设置 serializer.namespaces 里面的内容。我们以 Reader 中用到的那个 Moddle 实例为例,解析时会得到这样的内容:

js 复制代码
// Moddle 引用的属性描述对象文件,和实例化结果
import properties from '../../model/properties.json'
import propertiesExtended from '../../model/properties-extended.json'

const model = new Moddle([properties, propertiesExtended])

其中红色标注是我们后续引用的描述对应对应的命名空间前缀和 uri,蓝色标注则是 moddle-xml/common.js 中预设的两个命名空间前缀及 uri

然后,就会进入 build 函数,进行解析和序列化。

js 复制代码
ElementSerializer.prototype.build = function (element) {
  this.element = element

  var elementDescriptor = element.$descriptor,
    propertyDescriptor = this.propertyDescriptor
  var otherAttrs, properties
  var isGeneric = elementDescriptor.isGeneric

  if (isGeneric) {
    otherAttrs = this.parseGenericNsAttributes(element)
  } else {
    otherAttrs = this.parseNsAttributes(element)
  }

  if (propertyDescriptor) {
    this.ns = this.nsPropertyTagName(propertyDescriptor)
  } else {
    this.ns = this.nsTagName(elementDescriptor)
  }

  this.tagName = this.addTagName(this.ns)
  if (isGeneric) {
    this.parseGenericContainments(element)
  } else {
    properties = getSerializableProperties(element)
    this.parseAttributes(filterAttributes(properties))
    this.parseContainments(filterContained(properties))
  }

  this.parseGenericAttributes(element, otherAttrs)

  return this
}

根据 ModdlecreateAny 方法的介绍,我们可以知道 isGeneric 只有在使用 model.createAny() 创建实例的时候才会为 true,而大部分情况下,我们都不会使用这个方法,所以基本上都默认为 false

这种情况下,解析过程就分为以下步骤:

  1. this.parseNsAttributes(element)
  2. this.ns = this.propertyDescriptor ? this.nsPropertyTagName(this.propertyDescriptor) : this.nsTagName(this.elementDescriptor)
  3. this.tagName = this.addTagName(this.ns)
  4. this.parseAttributes(filterAttributes(element.properties))
  5. this.parseContainments(filterContained(element.properties))
  6. this.parseGenericAttributes(element, otherAttrs)

这里我们创建一个与 Reader 解析结果类似的一个 实例树:

js 复制代码
const complex1 = model.create('props:Complex', { id: 'C_1', body: 'this is complex c_1' })
const complex2 = model.create('props:Complex', { id: 'C_2', body: 'this is complex c_2' })
const complex3 = model.create('props:Complex', { id: 'C_3', body: 'this is complex c_3' })

const containedCollection = model.create('props:ContainedCollection', {
  id: 'C_5',
  children: [complex1, complex2, complex3]
})
complex1.$parent = complex2.$parent = complex3.$parent = containedCollection

const attributeReferenceCollection = model.create('props:AttributeReferenceCollection', {
  id: 'C_4',
  refs: [complex1, complex2, complex3]
})

const simpleBody1 = model.create('props:SimpleBody', { body: 'foo', otherAttr: 'other foo' })
const simpleBody2 = model.create('props:SimpleBody', { body: 'bar', anotherAttr: 'another bar' })
const simpleBody3 = model.create('props:SimpleBody', { body: 'baz', anotherAttr: 'another baz' })

const colAttr1 = model.create('props:Attributes', { name: 'integerValue', value: 10 })
const colAttr2 = model.create('props:Attributes', { name: 'booleanValue', value: true })
const complexAttrsCol = model.create('props:ComplexAttrsCol', {
  attrs: [colAttr1, colAttr2]
})
colAttr1.$parent = colAttr2.$parent = complexAttrsCol

const root = model.create('props:Root', {
  any: [simpleBody1, simpleBody2, simpleBody3, containedCollection, attributeReferenceCollection, complexAttrsCol],
  otherAttr: 'other root',
  anotherAttr: 'another root'
})
simpleBody1.$parent = simpleBody2.$parent = simpleBody3.$parent = root
containedCollection.$parent = attributeReferenceCollection.$parent = complexAttrsCol.$parent = root

会得到这样一个对象:

需要设置每个子对象的 $parent 指向,保证树的正确层级。为了测试所有的方法,增加了 otherAttr,anotherAttr 两个未定义属性。

1) 进入 parseGenericNsAttributes/parseNsAttributes 阶段。

顾名思义,这两个方法是用来 根据命名空间对象解析对象属性 的,并且两个方法都是通过 parseNsAttribute 来处理单个属性的,只是 parseGenericNsAttributes 会提取 element 元素上的 $ 符号开头的所有可枚举属性 ,通过 parseNsAttribute 进行判断和处理之后再组合成一个 [{ name: key, value: value }] 格式的对象;而 parseNsAttributes 则是 遍历 element.$attrs ,通过 parseNsAttribute 处理之后的返回值直接组合成一个数组(格式也是 [{ name: key, value: value }])。

那么 root 对象中,$attrs 就有我们自定义的两个属性,这时就会进入 parseNsAttribute(element, name, value) 方法。

该方法会通过 Moddle 提供的 parseNameNS 来将属性名 name 转换为一个 nameNs 对象,然后判断 nameNs.prefix 前缀或者 nameNs.localName 属性原名是否是 xmlns,如果是的话会将 value 作为 uri 属性创建一个 { uri: value } 的对象作为 ns 变量。

如果最后 ns 变量不存在的话,则返回 { name, value },否则会根据 value 的值(也就是 ns.uri)从 model.getPackages() 方法中读取该命名空间对应的属性描述对象是否存在,存在则调用 logNamespace 将其记录到 serializer.namespaceswellknown 此时为 true) 中;如果不是已注册的,则会通过 logNamespaceUsed 创建一个新的命名控件对象再记录到 serializer.namespaceswellknown 此时为 false)。

当然,我们注册的这两个属性并不会进入后面这部分逻辑之中,而是直接返回原属性名与属性值对象。

2) 然后进入下一步 this.ns = this.propertyDescriptor ? this.nsPropertyTagName(this.propertyDescriptor) : this.nsTagName(this.elementDescriptor)

这一步会区分 当前 serializer 的描述对象存在情况 ,来判断调用哪个方法。而 propertyDescriptor 属性的赋值只有在 new ElementSerializer 创建实例时才会发生,所以 首个标签的解析propertyDescriptor 属性值都是 undefined;只有在后续的 parseContainments 阶段,才会有进入 nsPropertyTagName() 的情况。

而两个方法的逻辑也很简单:

  • nsTagName:根据 当前元素对应的描述对象 elementDescriptor ,返回一个 prefix, uri, localName 组成的命名空间对象 ns 并绑定到当前 serializerlocalName 对应当前解析元素的定义名称
  • nsPropertyTagName:根据 上级元素定义中该属性的配置 propertyDescriptor ,返回一个 prefix, uri, localName 组成的命名空间对象 ns 并绑定到当前 serializerlocalName 对应 propertyDescriptor 中指定的属性名称
3) 然后生成标签名

上一步已经得到一个 ns 对象,这一步就是根据 ns 对象,生成一个 ${prefix}:${localName} 格式的标签名 tagName,绑定到 serializer

4) 然后,开始解析对象的属性

这一步,首先需要 找到需要序列化处理的属性 properties 定义数组,即 getSerializableProperties(element)

然后,根据 properties 中对每一项属性的定义,按照 isAttr 字段进行区分:

  • 如果isAttr === true,则是 标签的行内属性 ,通过 parseAttributes 处理
  • 反之,则是标签内部的字标签或者 body 文本,通过 parseContainments 处理

parseAttributes 方法在处理数据时,又分为几种情况:

  1. isReference 引用类型标识为 trueisManyfalse,则直接设置属性值为该引用实例的 id 属性值
  2. isReference 引用类型标识为 trueisManytrue,则需要遍历该属性,分别将所有引用实例的 id 读取出来,组成一个 由空格分开的 字符串
  3. isReferencefalse 时,则不做特殊处理,后面运算时会调用 toString 自动转为字符串(这也是为什么有些自定义属性设置成数组时,显示在 xml 上会变成 [object Object]

最后,会通过 serializer.addAttribute 将 属性名与属性值(属性值还会调用 escapeAttr 进行转码)组成 { name, value } 格式记录到 serializer.attrs 数组中,以供后续生成 xml 时使用。

parseContainments 方法,则是生成过程中 最核心的方法

由于这个方法处理的是 标签内部的内容 ,包含 文本(也就是 body)、子标签 两种情况;内部可以存放其他元素的引用,子标签也可能有 自定义的序列化类型 ,所以 在处理每一个属性时,都需要进行不同情况的判断,我们可以结合它的源码来分析:

js 复制代码
ElementSerializer.prototype.parseContainments = function (properties) {
  var self = this,
    body = this.body,
    element = this.element

  forEach(properties, function (p) {
    var value = element.get(p.name),
      isReference = p.isReference,
      isMany = p.isMany

    if (!isMany) {
      value = [value]
    }

    if (p.isBody) {
      body.push(new BodySerializer().build(p, value[0]))
    }
    else if (isSimpleType(p.type)) {
      forEach(value, function (v) {
        body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v))
      })
    } 
    else if (isReference) {
      forEach(value, function (v) {
        body.push(new ReferenceSerializer(self.addTagName(self.nsPropertyTagName(p))).build(v))
      })
    } 
    else {
      var serialization = getSerialization(p)

      forEach(value, function (v) {
        var serializer

        if (serialization) {
          if (serialization === SERIALIZE_PROPERTY) {
            serializer = new ElementSerializer(self, p)
          } else {
            serializer = new TypeSerializer(self, p, serialization)
          }
        } else {
          serializer = new ElementSerializer(self)
        }

        body.push(serializer.build(v))
      })
    }
  })
}

进入 parseContainments 方法,由上文可以知道 properties 参数是 当前元素对应的描述对象中,所有 isAttr 不等于 true 的属性组合成的数组 。以上文的根节点 root 为例,这里的 properties 就是:

js 复制代码
[{
  "name": "any",
  "type": "props:Base",
  "isMany": true,
  "ns": { "name": "props:any", "prefix": "props", "localName": "any"  },
  "inherited": true
}]

然后,开始遍历这个数组,依次读取当前实例中的所有对应属性值(获取方式参见上节 Moddle);并且,这里会对获取到的值进行 标准化处理,都转换为对象数组形式

下一步则是区分不同情况,分别进行属性值的解析:

  1. isBody == true:这种情况说明 这个属性需要显示在标签的内容;此时这部分内容通常会是字符串或者数字,那么直接创建一个 BodySerializer 将属性描述与属性值传递进 build 方法,并且将该实例插入到当前 serializerbody 数组中。
  2. isSimpleType(p.type) == trueisSimpleType 方法在 Moddle 一节中有说过,即验证这个属性定义的类型是不是字符串、数字(整数或小数)、布尔值;如果是这种情况,则需要针对该属性值的每一项分别创建一个 ValueSerializer 实例并添加到当前 serializerbody 数组中;还会调用每个 ValueSerializer 实例的 build 方法。
  3. isReference == true:即该属性是引用其他实例的 id 属性,在 xml 显示为一个字符串;一样会初始化一个 ReferenceSerializer 实例,调用其 build 方法然后插入到 serializerbody 数组中。
  4. 剩余情况,则是验证这个属性对应的描述对象 property 有没有定义 xml.serialize 属性(结果赋值给变量 serialization),根据这个属性的不同,又会分为以下三种情况:
    1. serialization === 'property':表示需要作为一个新的标签来处理属性,这种情况下会对当前属性,根据当前 serializer 实例与当前属性对应的描述对象,作为参数重新创建一个 ElementSerializer 实例,然后执行新实例的 build 方法。
    2. serialization != null:即存在 xml.serialize 配置,但是值不是 property;这种情况下,与上一种情况比较类型,是创建一个 TypeSerializer 实例,然后调用其 build 方法;但是 TypeSerializer 也是继承自 ElementSerializer,只是增加了 serialization 的自定义序列化规则。
    3. 最后,则是没有定义序列化的情况,这种情况下,一样会创建一个 ElementSerializer 实例然后执行 build 方法,只是此时只需要传入当前的 serializer 实例作为父级元素即可,然后按照该元素自己的定义进行解析。

最后,我们会得到一个 与原来的 root 实例结构类似的一个树形结构对象实例 serializer,大致结构如下:

最后,调用 serializeTo 将其转化为 字符串结果。

至于 serializeTo,其核心也是 借助 writer 实例创建一个标签头,然后通过 serializeAttributes 方法将当前实例的属性紧接着标签头进行写入,然后遍历 serializer 实例的 body 数组属性,依次执行为 body 中的每个 Serializer 执行 serializeTo 方法,直到解析完所有的 serializer 实例;原理上,就是一个 深度递归 的方式遍历对象树结构。

8. 不同 Serializer 序列化器及属性定义之间的区别

根据 6. Serializer 序列化处理程序 这部分的继承关系,可以知道 BodyValueElementType 互相有继承关系;除了 ElementSerializer 比较复杂之外,其他几个都比较简单。

  • ReferenceSerializer:生成一个 与属性定义中该属性名称对应的一个标签,并将该实例数组值对应的实例的 id 插入到这个标签内部作为纯文本
  • BodySerializer:将属性值转码后插入到结果中
  • ValueSerializer:与 BodySerializer 不同的是,ValueSerializer 实例化时需要传递一个 tagName,转字符串时会根据该 tagName 生成一个新的完整标签,并在标签内部插入转码后的结果
  • ElementSerializer:这种情况一般适用于非简单类型,也就是一个对象或者数组(当然数组最终解析时也会遍历内部的内容,最终始终是按对象的定义来执行);通常,都会根据这个对象的描述对象中定义的 name 字段来生成一个新的标签,然后再依次遍历其属性定义,如果某个属性也是对象或者数组的话,则会进行针对属性的序列化处理,直到所有属性解析处理完成
  • TypeSerializer:这个序列化器则是针对具有自定义序列化标识的属性,会按照其标识创建不同的 Serializer,并修改属性结果;通常这种情况下,会修改原属性对应的对象的标签名或者新建一个新的标签名;大部分情况下,描述对象中设置的自定义序列化标识只有两个值:propertyxsi:type

为了更好的体现不同的属性定义与解析结果之间的区别,我对上文的 property.json 进行一下修改,将其中的 WithPropertyComplexAttrs 改为以下形式:

json 复制代码
{
  "name": "ComplexAttrs",
  "superClass": [ "Complex" ],
  "properties": [
    { "name": "attrs", "type": "Attributes" }
  ]
},{
  "name": "WithProperty",
  "superClass": [ "Base" ],
  "properties": [
    { "name": "propertyName", "type": "Base", "xml": { "serialize" : "property" }  },
    { "name": "typeName", "type": "Base", "xml": { "serialize" : "xsi:type" }  },
    { "name": "defineName", "type": "Base", "xml": { "serialize" : "props:attributes" }  }
  ]
}

然后,创建不同情况下的实例树:

js 复制代码
const complex1 = model.create('props:Complex', { id: 'C_1', body: 'this is complex c_1' })
const complex2 = model.create('props:Complex', { id: 'C_2', body: 'this is complex c_2' })
const complex3 = model.create('props:Complex', { id: 'C_3', body: 'this is complex c_3' })

const containedCollection = model.create('props:ContainedCollection', {
  id: 'C_5',
  children: [complex1, complex2, complex3]
})
complex1.$parent = complex2.$parent = complex3.$parent = containedCollection

const attributeReferenceCollection = model.create('props:AttributeReferenceCollection', {
  id: 'C_4',
  refs: [complex1, complex2, complex3]
})

const complexCount = model.create('props:ComplexCount', { id: 'ComplexCount_1' })
const complexNesting = model.create('props:ComplexNesting', { id: 'ComplexNesting_1' })

const referencingCollection = model.create('props:ReferencingCollection', {
  references: [complexCount, complexNesting]
})
complexCount.$parent = complexNesting.$parent = referencingCollection

const simpleBody1 = model.create('props:SimpleBody', { body: 'foo', otherAttr: 'other foo' })
const simpleBody2 = model.create('props:SimpleBody', { body: 'bar', anotherAttr: 'another bar' })
const simpleBody3 = model.create('props:SimpleBody', { body: 'baz', anotherAttr: 'another baz' })

const colAttr1 = model.create('props:Attributes', { name: 'integerValue', value: 10, arr: [1, 2, 3] })
const colAttr2 = model.create('props:Attributes', { name: 'booleanValue', value: true, obj: { a: 1, b: 2, c: 3 } })
const complexAttrsCol = model.create('props:ComplexAttrsCol', {
  attrs: [colAttr1, colAttr2]
})
colAttr1.$parent = colAttr2.$parent = complexAttrsCol

const colAttr3 = model.create('props:Attributes', { name: 'integerValue', value: 10 })
const complexAttrs = model.create('props:ComplexAttrs', {
  attrs: colAttr3
})
colAttr3.$parent = complexAttrs

const propertyValue = model.create('props:BaseWithId', { id: 'PropertyValue' })
const typeValue = model.create('props:BaseWithId', { id: 'PropertyValue' })
const defineValue = model.create('props:BaseWithId', { id: 'PropertyValue' })
const withProperty = model.create('props:WithProperty', { propertyName: propertyValue, typeName: typeValue, defineName: defineValue })

const root = model.create('props:Root', {
  any: [simpleBody1, simpleBody2, simpleBody3, containedCollection, attributeReferenceCollection, referencingCollection, complexAttrs, complexAttrsCol, withProperty],
  otherAttr: 'other root',
  anotherAttr: 'another root'
})
simpleBody1.$parent = simpleBody2.$parent = simpleBody3.$parent = withProperty.$parent = root
containedCollection.$parent = attributeReferenceCollection.$parent = referencingCollection.$parent = complexAttrsCol.$parent = root

const xml1 = writer.toXML(root)

最终我们会得到这样一个结果:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<props:root xmlns:props="http://properties" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" otherAttr="other root" anotherAttr="another root">
  <props:simpleBody otherAttr="other foo">foo</props:simpleBody>
  <props:simpleBody anotherAttr="another bar">bar</props:simpleBody>
  <props:simpleBody anotherAttr="another baz">baz</props:simpleBody>
  <props:containedCollection id="C_5">
    <props:complex id="C_1">this is complex c_1</props:complex>
    <props:complex id="C_2">this is complex c_2</props:complex>
    <props:complex id="C_3">this is complex c_3</props:complex>
  </props:containedCollection>
  <props:attributeReferenceCollection id="C_4" refs="C_1 C_2 C_3" />
  <props:referencingCollection>
    <props:references>ComplexCount_1</props:references>
    <props:references>ComplexNesting_1</props:references>
  </props:referencingCollection>
  <props:complexAttrs>
    <props:attributes name="integerValue" value="10" />
  </props:complexAttrs>
  <props:complexAttrsCol>
    <props:attrs name="integerValue" value="10" arr="1,2,3" />
    <props:attrs name="booleanValue" value="true" obj="[object Object]" />
  </props:complexAttrsCol>
  <props:withProperty>
    <props:propertyName id="PropertyValue" />
    <props:typeName xsi:type="props:BaseWithId" id="PropertyValue" />
    <props:defineName attributes="props:BaseWithId" id="PropertyValue" />
  </props:withProperty>
</props:root>

其中:

  • <props:referencingCollection> 标签中的两个引用属性与 <props:attributeReferenceCollection> 标签中的引用属性有区别:

因为 referencingCollection 中未指定 isAttr,所以默认需要解析到 referencingCollection 标签内部;而为了区分 isBody 内容,需要创建属性名对应的标签来处理,所有有了 <props:references> 标签,这部分内容对应 ReferenceSerializer

  • <props:attributes><props:attrs> 标签,这两个标签对应 父元素 的属性类型定义,其实都指向 props:Attrubutes 声明:

但是,ComplexAttrsCol 中对其属性进行了序列化的指定,所以最终生成的标签名变成了 <props:attrs>,而 ComplexAttrs 没有处理,所以依然是 Attributes 对应的标签名 <props:attributes>

  • <props:withProperty> 中三种不同的序列化结果:

针对 withProperty,我们定义了三种对象类型都是 Base 的属性,但是分别有不同的序列化标识。这几种情况下都会按照配置的属性名生成一个新的子标签,但是,子标签的内容都有区别。

其中,propertyName 指定了作为 property 属性进行序列化,所以生成了一个新标签 <props:propertyName> 并且对应 id 属性为 propertyValue 设置在标签中;而 typeName 则指定为另一个常用序列化标识 xsi:type,这种情况与第三种情况一样,会执行 TypeSerializer 部分的逻辑,但是,xsi:type 并没有在我们已定义的所有命名空间中,所以,会生成一个新的属性 xsi:type 来指定我们的序列化方式,并设置其属性值为这个元素该属性对应的属性描述对象中的 name 字段,也就是 Props:BaseWithId;最后指定序列化为 props:Attributes 时,一样会生成一个属性来指定序列化方式,但是该属性对应的命令空间已经注册的,所以生成的属性名会自动去除对应的命名空间前缀,得到另外一个结果。

  • 未定义属性的处理

在创建不同的实例的组成 root 的时候,我们定义了一些 没有在 descriptor json 描述对象中定义的属性 ,例如 root 上的 otherAttr、anotherAttrattributes 上的 arr、obj 等。但在之前的 序列化过程 中,有解释过未声明的自定义属性,在解析时,会调用 toString 方法将其转为字符串,然后插入到标签中。

所以 otherAttr、otherAttr 这种字符串属性,在转换成 xml 后依然显示的一样的内容,而 arr 数组则转换为了以逗号分割的字符串形式,obj 则变成了 [object Object]。这种情况下,对于普通的字符串或者数字还比较好处理,遇到数组或者对象时,则很难再从 xml 转换为原来的格式了,所以这里建议 对所有的需要处理的 xml 上的属性,都需要通过 descriptor json 进行描述,并注册到 Moddle 之中

bpmn-moddle

在理解了 Moddlemoddle-xml 之后,再回到 bpmn-moddle 就很简单了。

bpmn-moddle 内部除了入口文件 index.js 之外,只有一个 simple.js 与一个 bpmn-moddle.js。其中,bpmn-moddle.js 导出了一个 继承自 Moddle 的构造函数 ,并且提供了 toXMLfromXML 两个方法。很明显,这两个方法来自于 moddle-xml

js 复制代码
function BpmnModdle(packages, options) {
  Moddle.call(this, packages, options);
}

BpmnModdle.prototype = Object.create(Moddle.prototype);

BpmnModdle.prototype.fromXML = function(xmlStr, typeName, options) {
  if (!isString(typeName)) {
    options = typeName;
    typeName = 'bpmn:Definitions';
  }

  var reader = new Reader(assign({ model: this, lax: true }, options));
  var rootHandler = reader.handler(typeName);

  return reader.fromXML(xmlStr, rootHandler);
};

BpmnModdle.prototype.toXML = function(element, options) {
  var writer = new Writer(options);

  return new Promise(function(resolve, reject) {
    try {
      var result = writer.toXML(element);

      return resolve({
        xml: result
      });
    } catch (err) {
      return reject(err);
    }
  });
};

在这两个方法中,核心就是 校验参数,调用 moddle-xmlReaderWriter,封装返回结果

simple.js,则是 引入 BPMN 2.0 规范对应的所有 descriptor json 文件,并创建一个默认注册所有描述文件的 Moddle 实例

js 复制代码
import {
  assign
} from 'min-dash';

import BpmnModdle from './bpmn-moddle.js';

import BpmnPackage from '../resources/bpmn/json/bpmn.json' assert { type: 'json' };
import BpmnDiPackage from '../resources/bpmn/json/bpmndi.json' assert { type: 'json' };
import DcPackage from '../resources/bpmn/json/dc.json' assert { type: 'json' };
import DiPackage from '../resources/bpmn/json/di.json' assert { type: 'json' };
import BiocPackage from '../resources/bpmn-io/json/bioc.json' assert { type: 'json' };
import BpmnInColorPackage from 'bpmn-in-color-moddle/resources/bpmn-in-color.json' assert { type: 'json' };

var packages = {
  bpmn: BpmnPackage,
  bpmndi: BpmnDiPackage,
  dc: DcPackage,
  di: DiPackage,
  bioc: BiocPackage,
  color: BpmnInColorPackage
};

export default function(additionalPackages, options) {
  var pks = assign({}, packages, additionalPackages);

  return new BpmnModdle(pks, options);
}

其中,dc.json 声明的是 文本样式与四个基础数据类型,以及坐标点、边界对象两个对象实例的数值类型di.json 则对应 所有的可见的图形元素实例对应的控制属性,例如连线类属性的路径 waypointbpmndi.json 则是 di.json 的扩展,定义 BPMN 元素的相关显示元素类型;bpmn-in-color.json 则是最近新增的一个用来合理化处理 setColor 方法导致标签多余样式属性的文件,控制哪些颜色属性可以在指定类型的元素上显示。

bpmn.json 则是 根据 BPMN 2.0 规范,按照规范中的元素以及属性定义,得到的完整的元素描述对象

相关推荐
獨枭1 小时前
使用 Spring Boot 快速构建企业微信 JS-SDK 权限签名后端服务
javascript·spring boot·企业微信
百万蹄蹄向前冲2 小时前
三句话四分钟,豆包出题把吾秀
前端·人工智能·豆包marscode
半旧5182 小时前
重构谷粒商城11:node快速入门
java·前端·重构
前端.火鸡3 小时前
认识vue中的install和使用场景
前端·javascript·vue.js
摆烂工程师3 小时前
Grok3 支持 DeeperSearch 的免费可用次数的查询了
前端·后端·程序员
pubuzhixing3 小时前
在线白板 - 如何从零实现一个自由画笔
前端·开源
hhw1991124 小时前
vue总结
前端·javascript·vue.js
学习2年半4 小时前
汇丰eee2
前端·spring
代码续发4 小时前
Vue进行前端开发流程
前端·vue.js
zpjing~.~4 小时前
CSS &符号
前端·css