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

Last Edited Time
Oct 30, 2023 06:37 AM
date
Oct 27, 2023
slug
tango-leaning-note
status
Published
tags
Lowcode
出码
In-browser compiler
summary
基于源代码 AST 实现可视化搭建操作,支持实时出码,不受私有 DSL 和协议限制
type
Post
notion image

一、实现原理

1. 初始化渲染逻辑

实例化工作区

相关代码
// 1. 实例化工作区
const workspace = new Workspace({
  entry: '/src/index.js',
  files: sampleFiles,
});
window.__workspace__ = workspace;
https://github.com/NetEase/tango/blob/d63517ecb936e4227e70c33e610664316625f4f4/apps/playground/src/pages/index.tsx#L29
Workspace(工作区状态)功能
  • 管理当前工作区的文件列表,并且进行转义
  • 管理页面路由、store、service 配置文件
  • 管理当前的路由、当前选中的视图文件与选中的文件
  • 创建通信机制
  • 管理 Workspace 历史记录

初始化引擎

引擎包括两部分
  • Workspace - 工作区状态
  • Designer - 设置器状态
// 2. 引擎初始化
const engine = createEngine({
  workspace,
});
window.__workspace__ = workspace;
https://github.com/NetEase/tango/blob/d63517ecb936e4227e70c33e610664316625f4f4/apps/playground/src/pages/index.tsx#L35

初始化变量配置

自定义配置数据
{
  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',
      },
    ],
  },
];
https://github.com/NetEase/tango/blob/d63517ecb936e4227e70c33e610664316625f4f4/apps/playground/src/pages/share.tsx#L9

初始化沙箱 Query 工具

一个 DOM Query 工具,通过引用的方式来与 Iframe 沙箱进行通信
const sandboxQuery = new DndQuery({
  context: 'iframe',
});
https://github.com/NetEase/tango/blob/d63517ecb936e4227e70c33e610664316625f4f4/apps/playground/src/pages/index.tsx#L42

渲染布局

布局很好理解,包括以下部分
  • DesignerPanel:壳子
  • Sidebar:左侧工具栏面板
  • WorkspacePanel
    • WorkspaceView design 模式
    • WorkspaceView code 模式
  • SettingPanel:右侧设置器

初始化沙箱

  • 这里的沙箱指的是 WorkspaceView design 模式用到的 CodeSandbox 核心功能,是整个低码编辑器的核心,这里需要引入 CodeSandbox 的 bundler。
    • <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)
<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 注入的

二、框架、物料设计

notion image

1. 物料设计

目录结构

+ src
  + button
    - view.tsx     // 默认视图文件
    - index.ts     // 渲染视图入口文件
    - designer.ts  // 设计器视图入口文件
    - prototype.ts // 组件描述文件
  + date-picker
  - index.ts       // 组件包默认入口文件
  - designer.ts    // 组件包设计器视图入口文件
物料核心包(TangoAntd)是应用必带的基础包,NPM 地址见 @music163/antd,主要有以下功能:
暴露物料的 MenuData
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'],
    }
  ],
};
https://github.com/NetEase/tango-components/blob/main/packages/antd/src/designer.ts
暴露物料的组件,包括 withDnD 的逻辑
默认情况下 withDnd 会在组件外层包裹一层 dnd 容器,以便于组件能够在设计器中被拖拽:
  • draggable 属性表示该区域可以被拖拽
  • data-dnd 用来追踪渲染的 dom 元素
export const Section = withDnd({
  name: 'Section',
  hasWrapper: false,
})(({ isRender = true, children = <Placeholder />, ...rest }: SectionProps) =>
  React.createElement(
    BaseSection,
    {
      opacity: isRender ? undefined : 0.4,
      ...rest,
    },
    children,
  ),
);
https://github.com/NetEase/tango-components/blob/main/packages/antd/src/designers/section.tsx
暴露物料组件的基本描述和设置器设置
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',
  },
};
https://github.com/NetEase/tango-components/blob/main/packages/antd/src/prototypes/section.ts

2. 应用框架设计

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

目录结构

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

在 Workspace 中定义的文件类型

/**
 * 文件类型枚举
 */
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',
}
https://github.com/NetEase/tango/blob/15542d9eb2f8959597b81cae457091ee71710c83/packages/core/src/types.ts#L8

路由、状态管理(略)

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

Reference