返回专题

专题 2023

Tango 低码前端编辑器原理学习

基于源代码 AST 实现可视化搭建操作,支持实时出码,不受私有 DSL 和协议限制

In-browser compilerLowcode出码
本文目录21
  1. 一、实现原理
  2. 1. 初始化渲染逻辑
  3. 实例化工作区
  4. 初始化引擎
  5. 初始化变量配置
  6. 初始化沙箱 Query 工具
  7. 渲染布局
  8. 初始化沙箱
  9. 2. 沙箱原理
  10. CodeSandbox 对接原理
  11. 3. 插入、复制、删除、移动原理
  12. useDnd 的原理
  13. withDnd 的原理
  14. 二、框架、物料设计
  15. 1. 物料设计
  16. 目录结构
  17. 2. 应用框架设计
  18. 目录结构
  19. 在 Workspace 中定义的文件类型
  20. 路由、状态管理(略)
  21. Reference

一、实现原理

1. 初始化渲染逻辑

实例化工作区

相关代码

JavaScript
// 1. 实例化工作区
const workspace = new Workspace({
  entry: '/src/index.js',
  files: sampleFiles,
});
window.__workspace__ = workspace;

Workspace(工作区状态)功能

  • 管理当前工作区的文件列表,并且进行转义
  • 管理页面路由、store、service 配置文件
  • 管理当前的路由、当前选中的视图文件与选中的文件
  • 创建通信机制
  • 管理 Workspace 历史记录

初始化引擎

引擎包括两部分

  • Workspace - 工作区状态
  • Designer - 设置器状态
JavaScript
// 2. 引擎初始化
const engine = createEngine({
  workspace,
});
window.__workspace__ = workspace;

初始化变量配置

自定义配置数据

JavaScript
{
  customActionVariables: bootHelperVariables,
  customExpressionVariables: bootHelperVariables,
}

export const bootHelperVariables = [
  {
    key: '$helpers',
    title: '工具函数',
    children: [
      {
        title: 'setStoreValue',
        key: '() => tango.setStoreValue("variableName", "variableValue")',
        type: 'function',
      },
      {
        title: 'getStoreValue',
        key: '() => tango.getStoreValue("variableName")',
        type: 'function',
      },
      { title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
      { title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
      { title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
      { title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
      { title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
      { title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
      {
        title: 'copyToClipboard',
        key: '() => tango.copyToClipboard("hello")',
        type: 'function',
      },
    ],
  },
];

初始化沙箱 Query 工具

一个 DOM Query 工具,通过引用的方式来与 Iframe 沙箱进行通信

JavaScript
const sandboxQuery = new DndQuery({
  context: 'iframe',
});

渲染布局

布局很好理解,包括以下部分

  • DesignerPanel:壳子
  • Sidebar:左侧工具栏面板
  • WorkspacePanel
    • WorkspaceView design 模式
    • WorkspaceView code 模式
  • SettingPanel:右侧设置器

初始化沙箱

  • 这里的沙箱指的是 WorkspaceView design 模式用到的 CodeSandbox 核心功能,是整个低码编辑器的核心,这里需要引入 CodeSandbox 的 bundler。

    JavaScript
    <Sandbox
      bundlerURL="https://local.netease.com:8443"
      onMessage={(e) => {
        if (e.type === "done") {
          const sandboxWindow: any = sandboxQuery.window;
          if (sandboxWindow.TangoAntd) {
            if (sandboxWindow.TangoAntd.menuData) {
              setMenuData(sandboxWindow.TangoAntd.menuData);
            }
            if (sandboxWindow.TangoAntd.prototypes) {
              workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
            }
          }
          setMenuLoading(false);
        }
      }}
    />;
    
  • 在沙箱加载完成之后,会设置 MenuDataComponentPrototypes

  • 根据用户操作渲染左侧组件面板和组件的设置面板

2. 沙箱原理

Sandbox 包括 PreviewSandboxDesignSandbox 两种,其中

  • PreviewSandbox 直接渲染了 CodeSandbox 的预览模式
  • DesignSandbox 渲染了 CodeSandbox 的 Design 模式,添加 useDnD 逻辑,并且监听代码或边框更新的情况重新设置选中的边框

CodeSandbox 对接原理

  • 通过 iframe 加载并渲染 codesandbox-client 资源
  • 在加载完成后注册监听逻辑,并修改 domain,以便让外部页面和 iframe 页面在同一个域名下,主要目的是为了直接监听 iframe 页面的事件
  • 加载完成后实例化 Manager,即开始构建页面,其中Manager是一个管理者的角色,从大局上把控整个转译和执行的流程(通过 IFrameProtocol 来进行通信)
  • 将 iframe ref 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 iframe ref

3. 插入、复制、删除、移动原理

useDnd 的原理

useDnd 是在渲染沙箱的时候会使用到,宿主会有三个渲染元素,分包用于选中高亮(SelectionTools)、插入提示(InsertionPrompt)、拖拽占位图层(DraggingMask)

JavaScript
<div className="AuxTools">
  <SelectionTools actions={selectionTools} />
  <InsertionPrompt />
  <DraggingMask />
</div>

useDnd 本身会有以下功能:

  • 处理当前选中的元素、拖拽相关的元素,显示提示
  • 处理当前元素的拖拽逻辑,给出拖拽高亮提示
  • 操作完成后,执行 Workspace 的 dropNode 能力,对文件进行操作

withDnd 的原理

withDnd 是一个 HOC,每个物料都需要使用该 HOC 定义当前组件的布局属性,并渲染 tango-dndBox 壳子来配合实现 Dnd 逻辑,如:

  • 是否允许拖拽
  • 布局的基本样式 blockinline-blockinline
  • 注入DND 追踪标记(data-dnd)

其中 DND 追踪标记Workspace 创建 TangoViewModule 的时候调用 traverseViewFile 注入的

二、框架、物料设计

1. 物料设计

目录结构

JavaScript
+ src
  + button
    - view.tsx     // 默认视图文件
    - index.ts     // 渲染视图入口文件
    - designer.ts  // 设计器视图入口文件
    - prototype.ts // 组件描述文件
  + date-picker
  - index.ts       // 组件包默认入口文件
  - designer.ts    // 组件包设计器视图入口文件

物料核心包(TangoAntd)是应用必带的基础包,NPM 地址见 @music163/antd,主要有以下功能:

  • 暴露物料的 MenuData

    JavaScript
    export const menuData = {
      common: [
        {
          title: '基本',
          items: ['Button', 'Section', 'Box', 'Space', 'Typography', 'Title', 'Paragraph'],
        },
        {
          title: '输入',
          items: ['Input', 'InputNumber', 'Select'],
        },
        {
          title: 'Formily表单',
          items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
        }
      ],
    };
    
  • 暴露物料的组件,包括 withDnD 的逻辑

    默认情况下 withDnd 会在组件外层包裹一层 dnd 容器,以便于组件能够在设计器中被拖拽:

    • draggable 属性表示该区域可以被拖拽
    • data-dnd 用来追踪渲染的 dom 元素
    JavaScript
    export const Section = withDnd({
      name: 'Section',
      hasWrapper: false,
    })(({ isRender = true, children = <Placeholder />, ...rest }: SectionProps) =>
      React.createElement(
        BaseSection,
        {
          opacity: isRender ? undefined : 0.4,
          ...rest,
        },
        children,
      ),
    );
    
  • 暴露物料组件的基本描述和设置器设置

    JavaScript
    export const Section: ComponentPrototypeType = {
      name: 'Section',
      title: '布局区块',
      icon: 'icon-mianban',
      type: 'element',
      package: '@music163/antd',
      help: '区域容器,可以用来将页面划分成多个区域,每个区域放置具体的内容模块。',
      hasChildren: true,
      initChildren: '',
      props: [
        // ...IsRenderPrototypes,
        ...CommonSystemStylePrototypes,
        ...StylePrototypes,
        {
          name: 'title',
          title: '容器标题',
          setter: 'textSetter',
          initValue: '区块标题',
        },
        {
          name: 'shape',
          title: '容器外观',
          setter: 'choiceSetter',
          options: [
            { label: '卡片', value: 'panel' },
            { label: '正常', value: 'box' },
          ],
        },
        {
          name: 'span',
          title: '占据的列数',
          tip: '仅在容器为弹性容器时才生效',
          setter: 'numberSetter',
          setterProps: {
            max: 24,
            min: 1,
          },
        },
        {
          name: 'extra',
          title: '页头附加内容',
          setter: 'jsxSetter',
        },
      ],
      rules: {
        childrenContainerSelector: '.one-section-content',
      },
    };
    

2. 应用框架设计

应用框架核心包(TangoBoot) 指的是在该编辑开发的应用所要遵循的开发规范以及框架,方便后续解析和操作

目录结构

JavaScript
── src
│ +── assets
│ +── pages
│ +── components
│ +── services/index.js
│ +── stores
│ ├── routes.js // 路由配置
│ ├── global.less // 全局样式
| |-- index.js 应用启动配置
└── package.json
|---tango.config.json 设计器和 external 配置

在 Workspace 中定义的文件类型

JavaScript
/**
 * 文件类型枚举
 */
export enum FileType {
  // js 文件
  Module = 'module',
  StoreEntryModule = 'storeEntryModule',
  RouteModule = 'routeModule',
  BlockEntryModule = 'blockEntryModule',
  ServiceModule = 'serviceModule',
  StoreModule = 'storeModule',

  JsxViewModule = 'jsxViewModule',
  JsonViewModule = 'jsonViewModule',

  // 非 js 文件
  PackageJson = 'packageJson',
  TangoConfigJson = 'tangoConfigJson',
  AppJson = 'appJson',
  File = 'file',
  Json = 'json',
  Less = 'less',
  Scss = 'scss',
}

路由、状态管理(略)

这里有个遗留问题,assets 中允许存非文本文件吗?至少 CodeSandbox 是支持的

Reference

GitHub - NetEase/tango: A source code based low-code builder. Integrate low-code experience into your local development workflow seamlessly.

A source code based low-code builder.
https://github.com/NetEase/tango

GitHub - NetEase/tango-components: An UI library for tango low-code builder

An UI library for tango low-code builder.
https://github.com/NetEase/tango-components

返回首页
上一篇Rust 入坑下一篇Mometa 低码前端编辑器原理学习

Discussion

留言与讨论

想法、补充和不同意见都欢迎。