目录
首发于:
最近更新于:
分类: archived

前言

blockcode是一个html5网页项目,其演示地址在这里: Block Code (dethe.github.io) 。项目代码在这里:dethe/bloc 。代码和文章略有不同,主要是拖动方面进一步根据dragula项目简化了代码,其他大体是一样的。

这个小项目主要演示了一种拖动可视化编程,下面主要是从html5编程的角度来学习这个项目,同时进行一些思考。

首先是基本的HTML页面,这个就不用多说了,下面主要分析那些js模块。这些js模块中看到:

(function () {})()

这种写法的都要优先分析,这些匿名函数在js加载之后会马上执行。其中util.js提供了一些便捷的函数供其他js文件调用,优先分析。

util.js

这个js文件给window上挂了几个函数作为全局变量,供其他js文件使用。

elem函数

看到这样的调用:

elem("input", { type: "number", value: value })
elem(
      "div",
      { class: "block", draggable: true, "data-name": name },
      [name]
    );

这是一个创建HTML DOM元素的便捷函数。第三个参数用于插入一个文本节点或者其他子节点。

trigger函数

  global.trigger = function trigger(name, target) {
    target.dispatchEvent(
      new CustomEvent(name, { bubbles: true, cancelable: false })
    );
  };

这个函数新建一个事件,并将这个事件绑定到target元素上,然后触发target的这个事件。下面继续分析整个项目的事件相关内容。

事件分析

blocks.js文件里面有:

  window.addEventListener("unload", file.saveLocal);
  window.addEventListener("load", file.restoreLocal);

window load事件之后将会调用 file.restoreLocal ,全局变量file来自file.js,其内的restoreLocal函数等下再细分析。

window unload 事件将会调用 file.saveLocal ,其内的saveLocal函数等下再细分析。

全局变量Block.run对应里面的runBlocks函数,其将触发传过来的各个block元素的run事件。

trigger("run", block);

在menu.js文件里面有:

  script.addEventListener("run", runEach);

script是:

  var script = document.querySelector(".script");

也就是中间的div元素。现在block触发run事件之后将会冒泡到script那里,然后script触发run事件之后将会调用这里的runEach函数。runEach函数做了一些什么事情后面再细讲。

在drag.js那里有:

drake.on("drop", () => trigger("scriptChanged", document.body));
drake.on("remove", () => trigger("scriptChanged", document.body));

这个drake元素和dragula这个第三方js模块有关,其用于处理窗体的拖动事件的。这里是将drop和remove事件都触发了scriptChanged事件。

在menu.js那里有:

  document.addEventListener("scriptChanged", runSoon);

所有最后将执行这里的runSoon函数,具体细节后面再将。

在file.js那里有:

  document
    .querySelector(".clear-action")
    .addEventListener("click", clearScript);
  document
    .querySelector(".save-action")
    .addEventListener("click", saveFile);
  document
    .querySelector(".load-action")
    .addEventListener("click", loadFile);
  document
    .querySelector(".choose-example")
    .addEventListener("change", loadExample);

这个很简单,基本的按钮事件和下选菜单事件触发这里的几个函数,具体动作内容后面再细分析。

在menu.js那里除了前面谈到的还有:

  script.addEventListener("change", runSoon);
  script.addEventListener("keyup", runSoon);

keyup在中间那个脚本区块那里鼠标松开之后触发,change事件应该是脚本区块里面的input里面的值发生了变动然后冒泡出来的事件。这些都将执行runSoon函数,具体什么动作后面再细分析。

在menu.js的run函数那里有:

  function run() {
    if (scriptDirty) {
      scriptDirty = false;
      trigger("beforeRun", script);
      var blocks = Array.from(document.querySelectorAll(".script > .block"));
      Block.run(blocks);
      trigger("afterRun", script);
    } else {
      trigger("everyFrame", script);
    }
    requestAnimationFrame(run);
  }

其将触发beforeRun和afterRun事件,那个everyFrame事件暂时项目里面还没有对应的动作,应该是没影响了。

在turtle.js那里有:

  script.addEventListener("beforeRun", clear); 
  script.addEventListener("afterRun", drawTurtle); 

上面提到的beforeRun和afterRun的调用函数是这里的clear和drawTurtle函数。

此外turtle.js还有一句:

  window.addEventListener("resize", onResize);

这个是额外的尺寸调整处理了。

有了上面的基本分析,那么接下来分析window的load事件绑定的restoreLocal函数就是第一要务了,这很合情理,先看看浏览器刚加载网页做了一些什么事情。

刚加载时动作分析

  function restoreLocal() {
    jsonToScript(localStorage[title] || "[]");
  }

  function jsonToScript(json) {
    clearScript();
    JSON.parse(json).forEach(function (block) {
      scriptElem.appendChild(Block.create.apply(null, block));
    });
    Menu.runSoon();
  }

  function clearScript() {
    Array.from(document.querySelectorAll(".script > .block")).forEach(function (
      block
    ) {
      block.parentElement.removeChild(block);
    });
    Menu.runSoon();
  }

clearScript移除了script下的所有子节点。然后执行 Menu.runSoon() 。runSoon函数仅仅设置了一个变量:

  function runSoon() {
    scriptDirty = true;
  }

将localStorage里面保存的json值读取出来之后下面开始根据这个json值来重载各个div block元素。这其中的关键语句是:

Block.create.apply(null, block)

Block.create实际是createBlock函数:

function createBlock(name, value, contents) {
    var item = elem(
      "div",
      { class: "block", draggable: true, "data-name": name },
      [name]
    );
    if (value !== undefined && value !== null) {
      item.appendChild(elem("input", { type: "number", value: value }));
    }
    if (Array.isArray(contents)) {
      item.appendChild(
        elem(
          "div",
          { class: "container" },
          contents.map(function (block) {
            return createBlock.apply(null, block);
          })
        )
      );
    } else if (typeof contents === "string") {
      // Add units specifier
      item.appendChild(document.createTextNode(" " + contents));
    }
    return item;
  }

具体上面就是创建各个Block的过程,并不是太难懂。所以加载时的动作就是将原script里面的各个block重载进去,但我经过测试发现实际上run函数还是执行了,然后发现这个run函数是一直不停的在执行:

  function run() {
    if (scriptDirty) {
      scriptDirty = false;
      trigger("beforeRun", script);
      var blocks = Array.from(document.querySelectorAll(".script > .block"));
      Block.run(blocks);
      trigger("afterRun", script);
    } else {
      trigger("everyFrame", script);
    }
    requestAnimationFrame(run);
  }

  requestAnimationFrame(run);

如下这种写法:

function repeatOften() {
  // Do whatever
  requestAnimationFrame(repeatOften);
}
requestAnimationFrame(repeatOften);

目标函数将会在浏览器每次重新渲染前执行一次。

所以runSoon虽然只改了一个变量,但这个变量将会导致上面那个区块的代码被执行。

首先触发了beforeRun事件,然后运行了 Block.run(blocks) ,然后触发了afterRun事件。

  function runBlocks(blocks) {
    blocks.forEach(function (block) {
      trigger("run", block);
    });
  }

这将继续触发各个block的run事件。继而再触发runEach函数:

  function runEach(evt) {
    var elem = evt.target;
    if (!elem.matches(".script .block")) return;
    if (elem.dataset.name === "Define block") return;

    elem.classList.add("running");
    scriptRegistry[elem.dataset.name](elem);

    elem.classList.remove("running");
  }

beforeRun和afterRun只是绘图的一些额外动作,最关键的是runBlocks这里。然后最关键的是这一句:

scriptRegistry[elem.dataset.name](elem);

这就是调用实际函数的那一句。而这些函数都是在turtle.js那里定义的:

  function menuItem(name, fn, value, units) {
    var item = Block.create(name, value, units);
    scriptRegistry[name] = fn;
    menu.appendChild(item);
    return item;
  }

    Menu.item("Forward", forward, 10, "steps");

这个一方面是在绘制左边的菜单,另一方面将函数注册到了 scriptRegistry那里。最后turtle.js具体和canvas相关的绘图细节这里就忽略讨论了。

repeat语句支持

  function repeat(block) {
    var count = Block.value(block);
    var children = Block.contents(block);
    for (var i = 0; i < count; i++) {
      Block.run(children);
    }
  }
  menuItem("Repeat", repeat, 10, []);

具体是假设顺序的话则依次触发各个block的run事件,如果遇到repeat区块,则依次触发各个子区块的run事件。

我担心repeat区块里面再夹个repeat区块有问题,经过试探发现确实解析上是有问题的。如下图所示这个例子:

img

他解析的顺序是:

f 50    f50

r 2    l50 f30

f50  f50

r2  l50 f30

f50 f50

r2  l50 f30

但是按照程序语法应该是 f50 l50 f30 l50 l30 f50 l50 f30 l50 l30 f50 l50 f30 l50 l30 。