前言
- 最近玩了blockly,感觉太强大了,原来scratch也是blockly做的。
文档
- blockly文档:https://developers.google.com/blockly/guides/overview?hl=en
- blockly开发工具:https://blockly-demo.appspot.com/static/demos/blockfactory/index.html
快速上手
npm i blockly
- blockly 主要靠xml注入和获取块,可以建造个blocklyComponent制作该playground:
/*
* @Author: yehuozhili
* @Date: 2021-07-15 15:45:35
* @LastEditors: yehuozhili
* @LastEditTime: 2021-07-15 21:34:15
* @FilePath: \blockdemo\src\Blockly\BlocklyComponent.tsx
*/
import { PropsWithChildren, RefObject, useEffect } from 'react';
import './BlocklyComponent.css';
import Blockly, { BlocklyOptions } from 'blockly/core';
import locale from 'blockly/msg/zh-hans';
import 'blockly/blocks';
Blockly.setLocale(locale);
interface BlocklyProps extends BlocklyOptions{
initialXml:string,
blocklyDiv:RefObject<HTMLDivElement>
toolboxDiv:RefObject<HTMLElement>
}
function BlocklyComponent(props:PropsWithChildren<BlocklyProps>){
const { initialXml, children,blocklyDiv,toolboxDiv, ...rest } = props;
useEffect(()=>{
if(blocklyDiv.current && toolboxDiv.current){
const primaryWorkspace = Blockly.inject(
blocklyDiv.current,{
toolbox:toolboxDiv.current,...rest
}
)
if (initialXml) {
Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(initialXml), primaryWorkspace);
}
}
},[blocklyDiv, initialXml, rest, toolboxDiv])
return <>
<div ref={blocklyDiv} id="blocklyDiv" />
<xml xmlns="https://developers.google.com/blockly/xml" is="blockly" style={{ display: 'none' }} ref={toolboxDiv}>
{props.children}
</xml>
</>
}
export default BlocklyComponent;
- 由于我们需要在该组件外调用拖拽的代码生成,所以将workspce的ref给提到父组件。
- 这个组件的children里放block后,即可渲染workspace块以及操作区了。
- 有可能jsx不认识xml元素,直接扩展下:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace JSX {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface IntrinsicElements {
xml:any
}
}
/*
* @Author: yehuozhili
* @Date: 2021-07-15 15:58:54
* @LastEditors: yehuozhili
* @LastEditTime: 2021-07-15 16:04:57
* @FilePath: \blockdemo\src\Blockly\index.ts
*/
import React, { PropsWithChildren } from 'react';
import BlocklyComponent from './BlocklyComponent';
export default BlocklyComponent;
const Block = (p:PropsWithChildren<any>) => {
const { children, ...props } = p;
props.is = "blockly";
return React.createElement("block", props, children);
};
const Category = (p:PropsWithChildren<any>) => {
const { children, ...props } = p;
props.is = "blockly";
return React.createElement("category", props, children);
};
const Value = (p:PropsWithChildren<any>) => {
const { children, ...props } = p;
props.is = "blockly";
return React.createElement("value", props, children);
};
const Field = (p:PropsWithChildren<any>) => {
const { children, ...props } = p;
props.is = "blockly";
return React.createElement("field", props, children);
};
const Shadow = (p:PropsWithChildren<any>) => {
const { children, ...props } = p;
props.is = "blockly";
return React.createElement("shadow", props, children);
};
export { Block, Category, Value, Field, Shadow }
- 这些组件的文档说明在这:https://developers.google.com/blockly/guides/configure/web/toolbox
// All standard block types in provided in Blockly core.
StandardCategories.coreBlockTypes = ["controls_if", "logic_compare",
"logic_operation", "logic_negate", "logic_boolean", "logic_null",
"logic_ternary", "controls_repeat_ext", "controls_whileUntil",
"controls_for", "controls_forEach", "controls_flow_statements",
"math_number", "math_arithmetic", "math_single", "math_trig",
"math_constant", "math_number_property", "math_change", "math_round",
"math_on_list", "math_modulo", "math_constrain", "math_random_int",
"math_random_float", "text", "text_join", "text_append", "text_length",
"text_isEmpty", "text_indexOf", "variables_get", "text_charAt",
"text_getSubstring", "text_changeCase", "text_trim", "text_print",
"text_prompt_ext", "colour_picker", "colour_random", "colour_rgb",
"colour_blend", "lists_create_with", "lists_repeat", "lists_length",
"lists_isEmpty", "lists_indexOf", "lists_getIndex", "lists_setIndex",
"lists_getSublist", "lists_split", "lists_sort", "variables_set",
"procedures_defreturn", "procedures_ifreturn", "procedures_defnoreturn",
"procedures_callreturn"];
- blockly一下载完内置了很多组件,所以你也需要对其内置的组件有一定了解,否则不知道type是啥,里面插值的Value的name填啥。内置组件地址:https://github.com/google/blockly/tree/master/blocks
自定义块
- 我们模仿官方循环做个循环块。
- 首先需要注册块,可以使用json进行注册:
Blockly.Blocks['myblock']= {
init : function(this: Blockly.Block__Class){
this.jsonInit( {
"type": "myblock",
"message0": "我的重复 %1 次 %2 执行 %3",
"args0": [
{
"type": "input_value",
"name": "mycount",
"check": "Number"
},
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "do"
}
],
"inputsInline": true,
"previousStatement": null,
"nextStatement": null,
"colour": 330,
"tooltip": "",
"helpUrl": ""
})
}
}
- 接收的值即为mycount,语句为do。
- 组件里国际化使用的语法是icu语法,具体可以在我博客里搜索。
- 再使用前面react.createelement的组件创建下:
<Block type="myblock">
<Value name="mycount">
<Shadow type="math_number">
<Field name="NUM">10</Field>
</Shadow>
</Value>
</Block>
- 就能显示出来了。
- 下面,还需要对myblock制作生成语句。
- 语句使用blockly对应的语言包,我们以生成javascript为例,生成方式主要是这2句:
import BlocklyJS from 'blockly/javascript';
const generateCode = ()=>{
var code = BlocklyJS.workspaceToCode(
(blocklyDiv as any).current.workspace
);
console.log(code);
}
- blocklyDiv是被 Blockly.inject后的实例。
- 对刚注册的组件定义语句:
//@ts-ignore
Blockly.JavaScript['myblock'] = function (block :any) {
// Repeat n times.
let repeats
if (block.getField('mycount')) {
// Internal number.
repeats = String(Number(block.getFieldValue('mycount')));
} else {
//@ts-ignore
repeats = Blockly.JavaScript.valueToCode(block, 'mycount','0')
}//@ts-ignore
let branch = Blockly.JavaScript.statementToCode(block, 'do');
//@ts-ignore
branch = Blockly.JavaScript.addLoopTrap(branch, block);
let code = '';
//@ts-ignore
let loopVar = Blockly.JavaScript.nameDB_.getDistinctName(
'count', Blockly.VARIABLE_CATEGORY_NAME);
var endVar = repeats;
if (!repeats.match(/^\w+$/) && !Blockly.isNumber(repeats)) {
//@ts-ignore
endVar = Blockly.JavaScript.nameDB_.getDistinctName(
'repeat_end', Blockly.VARIABLE_CATEGORY_NAME);
code += 'var ' + endVar + ' = ' + repeats + ';\n';
}
code += 'for (var ' + loopVar + ' = 0; ' +
loopVar + ' < ' + endVar + '; ' +
loopVar + '++) {\n' +
branch + '}\n';
return code;
};
- 其中使用
Blockly.JavaScript.nameDB_.getDistinctName
可以避免上下文中有重复的定义。 - 这样就可以在生成循环语句时生成code类似:
for (var count = 0; count < 10; count++) {
}
自定义field
- 上面自定义组件里每个args的type是个field,field也可以自定义,我们以自定义时间选择为例:
/*
* @Author: yehuozhili
* @Date: 2021-07-15 16:18:56
* @LastEditors: yehuozhili
* @LastEditTime: 2021-07-15 16:25:06
* @FilePath: \blockdemo\src\fields\DateField.tsx
*/
import * as Blockly from 'blockly/core';
import BlocklyReactField from './BlocklyReactField';
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
class ReactDateField extends BlocklyReactField {
static fromJson(options:any) {
return new ReactDateField(new Date(options['date']));
}
onDateSelected_ = (date:Date) => {
this.setValue(new Date(date));
Blockly.DropDownDiv.hideIfOwner(this, true);
}
getText_() {
return this.value_.toLocaleDateString();
};
fromXml(fieldElement :Element) {
this.setValue(new Date(fieldElement.textContent as unknown as Date));
}
render() {
return <DatePicker
selected={this.value_}
onChange={this.onDateSelected_}
inline />
}
}
Blockly.fieldRegistry.register('field_react_date', ReactDateField);
export default ReactDateField;
- 最下面注册的field_react_date即为组件里field使用的名字。
- 我们注册个自定义组件试试field:
var reactDateField = {
"type": "test_react_date_field",
"message0": "date field %1",
"args0": [
{
"type": "field_react_date",
"name": "DATE",
"date": "01/01/2020"
},
],
"previousStatement": null,
"nextStatement": null,
};
Blockly.Blocks['test_react_date_field'] = {
init: function(this: Blockly.Block__Class) {
this.jsonInit(reactDateField);
this.setStyle('loop_blocks');
}
};
//@ts-ignore
Blockly.JavaScript['test_react_date_field'] = function (block :any) {
console.log(block,'bbz')
return 'console.log(' + block.getField('DATE').getText() + ');\n';
};
<Block type="test_react_date_field" />
console.log(2020/1/10);
优先级
- blockly拼出的优先级和正常的语法优先级不一样,你可以使用加括号解决,但如果代码不展示还好,展示的话就不容易读,所以谷歌整了个优先级概念。
- 优先级字段:
Blockly.JavaScript.ORDER_ATOMIC = 0; // 0 "" ...
Blockly.JavaScript.ORDER_NEW = 1.1; // new
Blockly.JavaScript.ORDER_MEMBER = 1.2; // . []
Blockly.JavaScript.ORDER_FUNCTION_CALL = 2; // ()
Blockly.JavaScript.ORDER_INCREMENT = 3; // ++
Blockly.JavaScript.ORDER_DECREMENT = 3; // --
Blockly.JavaScript.ORDER_BITWISE_NOT = 4.1; // ~
Blockly.JavaScript.ORDER_UNARY_PLUS = 4.2; // +
Blockly.JavaScript.ORDER_UNARY_NEGATION = 4.3; // -
Blockly.JavaScript.ORDER_LOGICAL_NOT = 4.4; // !
Blockly.JavaScript.ORDER_TYPEOF = 4.5; // typeof
Blockly.JavaScript.ORDER_VOID = 4.6; // void
Blockly.JavaScript.ORDER_DELETE = 4.7; // delete
Blockly.JavaScript.ORDER_AWAIT = 4.8; // await
Blockly.JavaScript.ORDER_EXPONENTIATION = 5.0; // **
Blockly.JavaScript.ORDER_MULTIPLICATION = 5.1; // *
Blockly.JavaScript.ORDER_DIVISION = 5.2; // /
Blockly.JavaScript.ORDER_MODULUS = 5.3; // %
Blockly.JavaScript.ORDER_SUBTRACTION = 6.1; // -
Blockly.JavaScript.ORDER_ADDITION = 6.2; // +
Blockly.JavaScript.ORDER_BITWISE_SHIFT = 7; // << >> >>>
Blockly.JavaScript.ORDER_RELATIONAL = 8; // < <= > >=
Blockly.JavaScript.ORDER_IN = 8; // in
Blockly.JavaScript.ORDER_INSTANCEOF = 8; // instanceof
Blockly.JavaScript.ORDER_EQUALITY = 9; // == != === !==
Blockly.JavaScript.ORDER_BITWISE_AND = 10; // &
Blockly.JavaScript.ORDER_BITWISE_XOR = 11; // ^
Blockly.JavaScript.ORDER_BITWISE_OR = 12; // |
Blockly.JavaScript.ORDER_LOGICAL_AND = 13; // &&
Blockly.JavaScript.ORDER_LOGICAL_OR = 14; // ||
Blockly.JavaScript.ORDER_CONDITIONAL = 15; // ?:
Blockly.JavaScript.ORDER_ASSIGNMENT = 16; // = += -= **= *= /= %= <<= >>= ...
Blockly.JavaScript.ORDER_YIELD = 16.5; // yield
Blockly.JavaScript.ORDER_COMMA = 17; // ,
Blockly.JavaScript.ORDER_NONE = 99; // (...)
- 一般来说,需要将参数结合的地方使用最高优先级,返回语句的地方使用最低优先级,即可确保生成正确的代码:
var arg0 = Blockly.JavaScript.valueToCode(this, 'NUM1', Blockly.JavaScript.ORDER_DIVISION);
return [arg0 + ' / ' + arg1, Blockly.JavaScript.ORDER_DIVISION];
- 再简单说就是你的参数可能是由其他组合块拼接而成,而你不能简单的使用js去做加减乘除,其他组合块拼接时生成的有优先级,最简单方式就是参数里面都使用最高优先级,生成的语句都使用最低优先级。