扩展程序和转变器

扩展是在创建指定类型的块时在其上运行的函数。这些配置或行为通常会向块中添加一些自定义配置或行为

更改器是一种特殊的扩展,它向块添加自定义序列化(有时是界面)。

扩展程序

扩展是在创建指定类型的块时在其上运行的函数。他们可以添加自定义配置(例如设置块的提示)或自定义行为(例如向块添加事件监听器)。

// This extension sets the block's tooltip to be a function which displays
// the parent block's tooltip (if it exists).
Blockly.Extensions.register(
    'parent_tooltip_extension',
    function() { // this refers to the block that the extension is being run on
      var thisBlock = this;
      this.setTooltip(function() {
        var parent = thisBlock.getParent();
        return (parent && parent.getInputsInline() && parent.tooltip) ||
            Blockly.Msg.MATH_NUMBER_TOOLTIP;
      });
    });

扩展必须“注册”,以便与字符串键相关联。然后,您可以将此字符串键分配给分块类型的 JSON 定义extensions 属性,以将扩展应用于分块。

{
 //...,
 "extensions": ["parent_tooltip_extension",]
}

您还可以一次添加多项附加信息。请注意,extensions 属性必须是数组,即使您只应用一个扩展也是如此。

{
  //...,
  "extensions": ["parent_tooltip_extension", "break_warning_extension"],
}

混合音乐

如果您想向块中添加一些属性/辅助函数,但不立即运行它们,Blockly 还提供了一种便捷方法。通过这种方式,您可以注册一个包含所有附加属性/方法的 mixin 对象。然后,mixin 对象会封装在一个函数中,该函数会在每次创建指定块类型的实例时应用 mixin。

Blockly.Extensions.registerMixin('my_mixin', {
  someProperty: 'a cool value',

  someMethod: function() {
    // Do something cool!
  }
))`

与任何其他扩展程序一样,可以在 JSON 中引用与 mixin 关联的字符串键。

{
 //...,
 "extensions": ["my_mixin"],
}

变更器

更改器是一种特殊类型的扩展程序,它向块添加额外的序列化(保存和加载的额外状态)。例如,内置 controls_iflist_create_with 块需要进行额外的序列化,以便保存它们的输入数量。

请注意,更改块的形状并不一定意味着您需要额外的序列化。例如,math_number_property 块会更改形状,但它会根据下拉菜单字段进行更改,该字段的值已序列化。因此,它只能使用字段验证器,不需要更改器。

如需详细了解何时需要赋值函数以及何时不需要,请参阅序列化页面

如果您提供一些可选方法,则 Mutator 还会提供一个内置界面,以便用户更改块的形状。

序列化钩子

Mutator 有两对可使用的序列化钩子。一对钩子与新的 JSON 序列化系统配合使用,而另一对钩子则与旧的 XML 序列化系统配合使用。您必须至少提供其中一个键值对。

saveExtraState 和 loadExtraState

saveExtraStateloadExtraState 是可与新 JSON 序列化系统配合使用的序列化钩子。saveExtraState 会返回一个 JSON 可序列化值,该值表示块的额外状态,loadExtraState 接受相同的 JSON 可序列化值,并将其应用于块。

// These are the serialization hooks for the lists_create_with block.
saveExtraState: function() {
  return {
    'itemCount': this.itemCount_,
  };
},

loadExtraState: function(state) {
  this.itemCount_ = state['itemCount'];
  // This is a helper function which adds or removes inputs from the block.
  this.updateShape_();
},

生成的 JSON 将如下所示:

{
  "type": "lists_create_with",
  "extraState": {
    "itemCount": 3 // or whatever the count is
  }
}
无状态

如果您的块在序列化时处于默认状态,则您的 saveExtraState 方法可能会返回 null 来指明这一点。如果您的 saveExtraState 方法返回 null,则不会向 JSON 添加任何 extraState 属性。这样可以使保存的文件保持较小的大小。

完全序列化和后备数据

saveExtraState 还会接收可选的 doFullSerialization 参数。引用由其他序列化器(如后备数据模型)序列化的状态的块会用到该类。该参数会指示在对块进行反序列化时引用的状态将不可用,因此块应序列化所有后备状态本身。例如,当单个块序列化或复制粘贴块时,结果为 true。

两种常见用例是:

  • 当单个块加载到不存在后备数据模型的工作区时,它自身的状态将有足够的信息来创建新的数据模型。
  • 复制粘贴块时,它始终会创建新的后备数据模型,而不是引用现有数据模型。

用到它的一些块是 @blockly/block-shareable-procedures 块。通常,它们会序列化对后备数据模型的引用,以存储其状态。但如果 doFullSerialization 参数为 true,则它们会序列化所有状态。可共享过程块使用此方法可确保在复制粘贴时创建新的后备数据模型,而不是引用现有模型。

mutateToDom 和 domToMutation

mutationToDomdomToMutation 是与旧的 XML 序列化系统配合使用的序列化钩子。仅在必要时使用这些钩子(例如,您在使用尚未迁移的旧代码库),否则请使用 saveExtraStateloadExtraState

mutationToDom 会返回一个 XML 节点,它表示块的额外状态,domToMutation 接受该 XML 节点并将状态应用于块。

// These are the old XML serialization hooks for the lists_create_with block.
mutationToDom: function() {
  // You *must* create a <mutation></mutation> element.
  // This element can have children.
  var container = Blockly.utils.xml.createElement('mutation');
  container.setAttribute('items', this.itemCount_);
  return container;
},

domToMutation: function(xmlElement) {
  this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
  // This is a helper function which adds or removes inputs from the block.
  this.updateShape_();
},

生成的 XML 应如下所示:

<block type="lists_create_with">
  <mutation items="3"></mutation>
</block>

如果您的 mutationToDom 函数返回 null,则系统不会向 XML 中添加任何额外的元素。

界面钩子

如果您在更改器中提供某些功能,Blockly 会向您的块添加默认的“更改器”界面。

如果您想添加额外的序列化,无需使用此界面。您可以使用自定义界面(如 blocks-plus-minus 插件提供的),也可以根本不使用界面!

组合和分解

默认界面依赖于 composedecompose 函数。

decompose 将代码块“分解”为更小的子块,以便四处移动、添加和删除这些子块。此函数应返回一个“顶部块”,这是子块连接到的更改器工作区中的主块。

然后,compose 会解释子块的配置,并使用它们来修改主块。此函数应接受 decompose 作为参数返回的“顶部块”。

请注意,这些函数会“混合”到要“更改”的块中,因此 this 可用于引用该块。

// These are the decompose and compose functions for the lists_create_with block.
decompose: function(workspace) {
  // This is a special sub-block that only gets created in the mutator UI.
  // It acts as our "top block"
  var topBlock = workspace.newBlock('lists_create_with_container');
  topBlock.initSvg();

  // Then we add one sub-block for each item in the list.
  var connection = topBlock.getInput('STACK').connection;
  for (var i = 0; i < this.itemCount_; i++) {
    var itemBlock = workspace.newBlock('lists_create_with_item');
    itemBlock.initSvg();
    connection.connect(itemBlock.previousConnection);
    connection = itemBlock.nextConnection;
  }

  // And finally we have to return the top-block.
  return topBlock;
},

// The container block is the top-block returned by decompose.
compose: function(topBlock) {
  // First we get the first sub-block (which represents an input on our main block).
  var itemBlock = topBlock.getInputTargetBlock('STACK');

  // Then we collect up all of the connections of on our main block that are
  // referenced by our sub-blocks.
  // This relates to the saveConnections hook (explained below).
  var connections = [];
  while (itemBlock && !itemBlock.isInsertionMarker()) {  // Ignore insertion markers!
    connections.push(itemBlock.valueConnection_);
    itemBlock = itemBlock.nextConnection &&
        itemBlock.nextConnection.targetBlock();
  }

  // Then we disconnect any children where the sub-block associated with that
  // child has been deleted/removed from the stack.
  for (var i = 0; i < this.itemCount_; i++) {
    var connection = this.getInput('ADD' + i).connection.targetConnection;
    if (connection && connections.indexOf(connection) == -1) {
      connection.disconnect();
    }
  }

  // Then we update the shape of our block (removing or adding iputs as necessary).
  // `this` refers to the main block.
  this.itemCount_ = connections.length;
  this.updateShape_();

  // And finally we reconnect any child blocks.
  for (var i = 0; i < this.itemCount_; i++) {
    connections[i].reconnect(this, 'ADD' + i);
  }
},

saveConnections

或者,您还可以定义一个可与默认界面搭配使用的 saveConnections 函数。此函数让您有机会将主代码块(位于主工作区中)的子块与赋值器工作区中存在的子块相关联。然后,您可以使用这些数据来确保在重新组织子块时,compose 函数能够正确地重新连接主块的子块。

saveConnections 应接受 decompose 函数返回的“顶部块”作为参数。如果定义了 saveConnections 函数,Blockly 会在调用 compose 之前调用该函数。

saveConnections: function(topBlock) {
  // First we get the first sub-block (which represents an input on our main block).
  var itemBlock = topBlock.getInputTargetBlock('STACK');

  // Then we go through and assign references to connections on our main block
  // (input.connection.targetConnection) to properties on our sub blocks
  // (itemBlock.valueConnection_).
  var i = 0;
  while (itemBlock) {
    // `this` refers to the main block (which is being "mutated").
    var input = this.getInput('ADD' + i);
    // This is the important line of this function!
    itemBlock.valueConnection_ = input && input.connection.targetConnection;
    i++;
    itemBlock = itemBlock.nextConnection &&
        itemBlock.nextConnection.targetBlock();
  }
},

正在注册

变更器是一种特殊的扩展,因此,您必须先注册变更器,然后才能在块类型的 JSON 定义中使用它们。

// Function signature.
Blockly.Extensions.registerMutator(name, mixinObj, opt_helperFn, opt_blockList);

// Example call.
Blockly.Extensions.registerMutator(
    'controls_if_mutator',
    { /* mutator methods */ },
    undefined,
    ['controls_if_elseif', 'controls_if_else']);
  • name:要与更改器关联的字符串,以便在 JSON 中使用。
  • mixinObj:包含各种更改方法的对象。例如,saveExtraStateloadExtraState
  • opt_helperFn:可选的辅助函数,混合了 mixin 后在块上运行。
  • opt_blockList:可选的块类型数组(以字符串形式),如果还定义了界面方法,该数组将添加到默认更改器界面中的浮出控件。

请注意,与扩展不同,每个块类型只能有一个更改器。

{
  //...
  "mutator": "controls_if_mutator"
}

辅助函数

更改器可以随 mixin 一起注册辅助函数。在创建此属性并添加 mixinObj 后,在指定类型的每个块上运行此函数。它可用于向变更添加其他触发器或效果。

例如,您可以向列表类代码块添加一个帮助程序来设置初始项数:

var helper = function() {
  this.itemCount_ = 5;
  this.updateShape();
}