当前位置: 首页 > 工具软件 > Blockly > 使用案例 >

【react】blockly结合react快速上手

潘泳
2023-12-01

前言

  • 最近玩了blockly,感觉太强大了,原来scratch也是blockly做的。

文档

  • blockly文档:https://developers.google.com/blockly/guides/overview?hl=en
  • blockly开发工具:https://blockly-demo.appspot.com/static/demos/blockfactory/index.html

快速上手

  • 首先安装blockly
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
    }
}
  • 一般,block有以下几种:
/*
 * @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');
  }
};
  • 然后对该block注册语句:
//@ts-ignore
Blockly.JavaScript['test_react_date_field'] = function (block :any) {
    console.log(block,'bbz')
    return 'console.log(' + block.getField('DATE').getText() + ');\n';
};
  • 放置block:
       <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去做加减乘除,其他组合块拼接时生成的有优先级,最简单方式就是参数里面都使用最高优先级,生成的语句都使用最低优先级。
 类似资料: