前言
最近玩了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:
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元素,直接扩展下:
namespace JSX {
interface IntrinsicElements {
xml: any
}
}
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
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后的实例。
对刚注册的组件定义语句:
Blockly. JavaScript[ 'myblock' ] = function ( block : any) {
let repeats
if ( block. getField ( 'mycount' ) ) {
repeats = String ( Number ( block. getFieldValue ( 'mycount' ) ) ) ;
} else {
repeats = Blockly. JavaScript. valueToCode ( block, 'mycount' , '0' )
}
let branch = Blockly. JavaScript. statementToCode ( block, 'do' ) ;
branch = Blockly. JavaScript. addLoopTrap ( branch, block) ;
let code = '' ;
let loopVar = Blockly. JavaScript. nameDB_. getDistinctName (
'count' , Blockly. VARIABLE_CATEGORY_NAME ) ;
var endVar = repeats;
if ( ! repeats. match ( /^\w+$/ ) && ! Blockly. isNumber ( repeats) ) {
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也可以自定义,我们以自定义时间选择为例:
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' ) ;
}
} ;
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 ;
Blockly. JavaScript. ORDER_NEW = 1.1 ;
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 ;
Blockly. JavaScript. ORDER_VOID = 4.6 ;
Blockly. JavaScript. ORDER_DELETE = 4.7 ;
Blockly. JavaScript. ORDER_AWAIT = 4.8 ;
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 ;
Blockly. JavaScript. ORDER_INSTANCEOF = 8 ;
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 ;
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去做加减乘除,其他组合块拼接时生成的有优先级,最简单方式就是参数里面都使用最高优先级,生成的语句都使用最低优先级。