Blockly 二次封装
基础功能封装
对 Blockly 进行二次封装,提供如下方法:
- loadCustomBlocks: 用来动态加载自定义 block
- buildContainer: 确保 workspace 的 container 和 toolbox 被创建
- initSpace:在指定位置创建一个画布
- clearSpace: 清除画布上的内容
- loadSpace: 将画布存档加载到指定的 workspace 中去
- saveSpace:保存画布内容
1. 创建 Blcokly 文件 – @/externals/blockly.js
// @/externals/blockly.js
import Blockly from 'blockly'; // version = "blockly": "8",
// import { javascriptGenerator, Order } from 'blockly/javascript';
/**
* 用来动态加载自定义 block
*/
export const loadCustomBlocks = () => {
Blockly.Blocks['my_block'] = {
init: function () {
this.appendDummyInput()
.appendField("My Block");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(230);
this.setTooltip('');
this.setHelpUrl('');
}
};
Blockly.JavaScript['my_block'] = function (block) {
console.log('block:', block)
var code = 'console.log("Hello, World!");\n';
return code;
};
Blockly.Blocks['my_custom_block'] = {
init: function () {
this.appendDummyInput()
.appendField("自定义块");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(230);
this.setTooltip('');
this.setHelpUrl('');
}
};
Blockly.Blocks['combined_block'] = {
init: function () {
this.appendValueInput("TEXT_INPUT")
.setCheck(null)
.appendField("输入文本:");
this.appendDummyInput()
.appendField(new Blockly.FieldDropdown([["选项1", "1"], ["选项2", "2"]]), "DROPDOWN");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(230);
this.setTooltip('111');
this.setHelpUrl('');
}
};
Blockly.defineBlocksWithJsonArray([
{
"type": "my_custom_block",
"message0": "自定义块",
"previousStatement": null,
"nextStatement": null,
"colour": 230,
"tooltip": "",
"helpUrl": ""
},
{
"type": "combined_block",
"message0": "输入文本: %1 选择选项: %2",
"args0": [
{
"type": "input_value",
"name": "TEXT_INPUT"
},
{
"type": "field_dropdown",
"name": "DROPDOWN",
"options": [["选项1", "1"], ["选项2", "2"]]
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 230,
"tooltip": "",
"helpUrl": ""
}
]);
const C_Block = {
init: function () {
this.appendValueInput('VAR1')
.setCheck('Number');
this.setInputsInline(false)
this.setNextStatement(true, null);
this.setTooltip('这是自定义的模块');
this.setHelpUrl('这只是一个测试用例');
this.setColour(255);
}
};
Blockly.Blocks['C_Block'] = C_Block;
}
/**
* 确保 workspace 的 container 和 toolbox 被创建
* @param {*} root 新建 toolbox 和 container 插入锚点
*/
export const buildContainer = (containerId = 'blocklyDiv', toolboxId = 'toolbox') => {
let toolbox = document.getElementById(toolboxId);
if (toolbox) {
toolbox.innerHTML = null;
} else {
toolbox = document.createElement('xml');
toolbox.id = toolboxId;
toolbox.style = "display: none;";
}
toolbox.innerHTML = `
<category name="custom" colour="#230">
<block type="my_custom_block"></block>
<block type="combined_block"></block>
<block type="C_Block"></block>
<block type="my_block"></block>
</category>
<category name="Logic" colour="#5b80a5">
<block type="controls_if"></block>
<block type="logic_compare">
<field name="OP">EQ</field>
</block>
<block type="logic_operation">
<field name="OP">AND</field>
</block>
<block type="logic_negate"></block>
<block type="logic_boolean">
<field name="BOOL">TRUE</field>
</block>
<block type="logic_null"></block>
<block type="logic_ternary"></block>
</category>
<category name="Loops" colour="#5ba55b">
<block type="controls_repeat_ext">
<value name="TIMES">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
</block>
<block type="controls_for">
<field name="VAR" id="W4T~@+9%MGU@6@Bmao1)">i</field>
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
<value name="BY">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="controls_forEach">
<field name="VAR" id="8Ic*U$XC+I3a7UGQ5NCS">j</field>
</block>
<block type="controls_flow_statements">
<field name="FLOW">BREAK</field>
</block>
</category>
<category name="Math" colour="#5b67a5">
<block type="math_number">
<field name="NUM">0</field>
</block>
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="B">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_single">
<field name="OP">ROOT</field>
<value name="NUM">
<shadow type="math_number">
<field name="NUM">9</field>
</shadow>
</value>
</block>
<block type="math_trig">
<field name="OP">SIN</field>
<value name="NUM">
<shadow type="math_number">
<field name="NUM">45</field>
</shadow>
</value>
</block>
<block type="math_constant">
<field name="CONSTANT">PI</field>
</block>
<block type="math_number_property">
<mutation divisor_input="false"></mutation>
<field name="PROPERTY">EVEN</field>
<value name="NUMBER_TO_CHECK">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="math_round">
<field name="OP">ROUND</field>
<value name="NUM">
<shadow type="math_number">
<field name="NUM">3.1</field>
</shadow>
</value>
</block>
<block type="math_on_list">
<mutation op="SUM"></mutation>
<field name="OP">SUM</field>
</block>
<block type="math_modulo">
<value name="DIVIDEND">
<shadow type="math_number">
<field name="NUM">64</field>
</shadow>
</value>
<value name="DIVISOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="math_constrain">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="LOW">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="HIGH">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_int">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_float"></block>
</category>
<category name="Text" colour="#5ba58c">
<block type="text">
<field name="TEXT"></field>
</block>
<block type="text_join">
<mutation items="2"></mutation>
</block>
<block type="text_append">
<field name="VAR" id="t=e01]I(kFE;GF^|L[xE">item</field>
<value name="TEXT">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="text_length">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_isEmpty">
<value name="VALUE">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="text_indexOf">
<field name="END">FIRST</field>
<value name="VALUE">
<block type="variables_get">
<field name="VAR" id="]i+*soCH@O.?{:Ho$po">text</field>
</block>
</value>
<value name="FIND">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_charAt">
<mutation at="true"></mutation>
<field name="WHERE">FROM_START</field>
<value name="VALUE">
<block type="variables_get">
<field name="VAR" id="]i+*soCH@O.?{:Ho$po">text</field>
</block>
</value>
</block>
<block type="text_getSubstring">
<mutation at1="true" at2="true"></mutation>
<field name="WHERE1">FROM_START</field>
<field name="WHERE2">FROM_START</field>
<value name="STRING">
<block type="variables_get">
<field name="VAR" id="]i+*soCH@O.?{:Ho$po">text</field>
</block>
</value>
</block>
<block type="text_changeCase">
<field name="CASE">UPPERCASE</field>
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_trim">
<field name="MODE">BOTH</field>
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_print">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_prompt_ext">
<mutation type="TEXT"></mutation>
<field name="TYPE">TEXT</field>
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
</category>
<category name="Lists" colour="#745ba5">
<block type="lists_create_with">
<mutation items="0"></mutation>
</block>
<block type="lists_create_with">
<mutation items="3"></mutation>
</block>
<block type="lists_repeat">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
</block>
<block type="lists_length"></block>
<block type="lists_isEmpty"></block>
<block type="lists_indexOf">
<field name="END">FIRST</field>
<value name="VALUE">
<block type="variables_get">
<field name="VAR" id="dB-;(eN*HU{H);~5[4WK">list</field>
</block>
</value>
</block>
<block type="lists_getIndex">
<mutation statement="false" at="true"></mutation>
<field name="MODE">GET</field>
<field name="WHERE">FROM_START</field>
<value name="VALUE">
<block type="variables_get">
<field name="VAR" id="dB-;(eN*HU{H);~5[4WK">list</field>
</block>
</value>
</block>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="LIST">
<block type="variables_get">
<field name="VAR" id="dB-;(eN*HU{H);~5[4WK">list</field>
</block>
</value>
</block>
<block type="lists_getSublist">
<mutation at1="true" at2="true"></mutation>
<field name="WHERE1">FROM_START</field>
<field name="WHERE2">FROM_START</field>
<value name="LIST">
<block type="variables_get">
<field name="VAR" id="dB-;(eN*HU{H);~5[4WK">list</field>
</block>
</value>
</block>
<block type="lists_split">
<mutation mode="SPLIT"></mutation>
<field name="MODE">SPLIT</field>
<value name="DELIM">
<shadow type="text">
<field name="TEXT">,</field>
</shadow>
</value>
</block>
<block type="lists_sort">
<field name="TYPE">NUMERIC</field>
<field name="DIRECTION">1</field>
</block>
</category>
<category name="Colour" colour="#a5745b">
<block type="colour_picker">
<field name="COLOUR">#ff0000</field>
</block>
<block type="colour_random"></block>
<block type="colour_rgb">
<value name="RED">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
<value name="GREEN">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="BLUE">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="colour_blend">
<value name="COLOUR1">
<shadow type="colour_picker">
<field name="COLOUR">#ff0000</field>
</shadow>
</value>
<value name="COLOUR2">
<shadow type="colour_picker">
<field name="COLOUR">#3333ff</field>
</shadow>
</value>
<value name="RATIO">
<shadow type="math_number">
<field name="NUM">0.5</field>
</shadow>
</value>
</block>
</category>
<sep></sep>
<category name="Variables" colour="#a55b80" custom="VARIABLE"></category>
<category name="Functions" colour="#995ba5" custom="PROCEDURE"></category>
`
// document.body.insertBefore(toolbox, root);
document.body.appendChild(toolbox);
let container = document.getElementById(containerId);
if (container) {
container.innerHTML = null;
} else {
container = document.createElement('div');
container.id = containerId;
container.style = "height: 100vh; width: 100%;";
document.body.appendChild(container);
// document.body.insertBefore(container, root);
}
}
/**
* 在指定位置创建一个画布
* @param {string} containerId container id 默认值为 'blocklyDiv'
* @param {string} toolboxId toolbox id 默认值为 'toolbox'
* @returns workspace 绘图空间/画布
*/
export const initSpace = (containerId = 'blocklyDiv', toolboxId = 'toolbox') => {
// 将 workspace 注入到 id 为 blocklyDiv 的容器中
const workspace = Blockly.inject(containerId, {
toolbox: document.getElementById(toolboxId) // 指定 toolbox xml 格式
})
workspace.registerToolboxCategoryCallback('MY_CATEGORY', function () {
return [
{ kind: 'block', type: 'my_block' }
];
});
// 返回 workspace
return workspace;
}
/**
* 清除画布上的内容
* @param {*} workspace 清除目标
*/
export const clearSpace = (workspace) => {
// 清空工作区的内容
workspace.clear();
}
/**
* 将画布存档加载到指定的 workspace 中去
* @param {*} workspace 目标画布
* @param {string} xmlText 加载内容
*/
export const loadSpace = (workspace, xmlText) => {
xmlText ??= `<xml xmlns="https://developers.google.com/blockly/xml"><block type="text_print" id="%nkw(EK0gme?jpRyFt6c" x="417" y="120"><value name="TEXT"><shadow type="text" id="f]JRs#?(A]75a8|~b~}c"><field name="TEXT">abc</field></shadow></value></block></xml>`;
// 需要将保存的 xmlText 转为 xml dom 对象
const xml = Blockly.Xml.textToDom(xmlText);
// 回显数据
Blockly.Xml.domToWorkspace(xml, workspace);
}
/**
* 保存画布内容
* @param {*} workspace
* @returns code: 生成出来的代码; xml:工作区目前编辑的 xml dom 对象; xmlText: xml dom 对象转 text
*/
export const saveSpace = (workspace) => {
// code: 生成出来的代码
// xml:工作区目前编辑的 xml dom 对象
// xmlText: xml dom 对象转 text
const code = Blockly.JavaScript.workspaceToCode(workspace);
const xml = Blockly.Xml.workspaceToDom(workspace);
const xmlText = Blockly.Xml.domToText(xml);
return {
code,
xml,
xmlText,
}
}
2. Vue2 项目中使用示例
<div
id="blocklyDiv2"
style="height: 480px; width: 100%; border: none; overflow: hidden; margin-top: 24px;"
></div>
import {
loadCustomBlocks,
buildContainer,
initSpace,
clearSpace,
loadSpace,
} from "@/externals/blockly.js";
import * as Blockly from "@/externals/blockly.js";
window.blc = Blockly;
export default {
mounted(){
loadCustomBlocks()
buildContainer('blocklyDiv2', 'toolbox')
const workspace = window.workspace = initSpace('blocklyDiv2', 'toolbox')
clearSpace(workspace);
loadSpace(workspace)
},
}
上面的代码很容易封装成一个 mixin 如果在 vue3 中,那么自然可以封装成一个 hook 就像下面的 React 示例一样。
3. React 项目中使用示例
创建一个 hook:
import { useEffect } from "react";
import {
loadCustomBlocks,
buildContainer,
initSpace,
clearSpace,
loadSpace,
} from "./utils/blockly";
import * as Blockly from "./utils/blockly";
window.blc = Blockly;
export const useBlockly = (blockId = 'blocklyDiv', toolboxId = 'toolbox') => {
useEffect(() => {
loadCustomBlocks()
buildContainer(blockId, toolboxId)
const workspace = window.workspace = initSpace(blockId, toolboxId)
clearSpace(workspace);
loadSpace(workspace)
return () => {
document.getElementById(blockId)?.remove();
document.getElementById(toolboxId)?.remove();
}
}, [])
}
在相应的视图组件中使用这个 hook:
// App.js
import './App.css';
import {useBlockly} from "./hooks";
function App() {
useBlockly();
return (
<div className="App">
<div id="blocklyDiv" style={{height: "100vh", width: "100%"}}></div>
</div>
);
}
export default App;