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
一、实现原理1. 初始化渲染逻辑实例化工作区初始化引擎初始化变量配置初始化沙箱 Query 工具渲染布局初始化沙箱2. 沙箱原理CodeSandbox 对接原理3. 插入、复制、删除、移动原理useDnd 的原理withDnd 的原理二、框架、物料设计1. 物料设计目录结构2. 应用框架设计目录结构在 Workspace 中定义的文件类型路由、状态管理(略)Reference
一、实现原理
1. 初始化渲染逻辑
实例化工作区
相关代码
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
});
window.__workspace__ = workspace;
Workspace(工作区状态)功能
- 管理当前工作区的文件列表,并且进行转义
- 管理页面路由、store、service 配置文件
- 管理当前的路由、当前选中的视图文件与选中的文件
- 创建通信机制
- 管理 Workspace 历史记录
初始化引擎
引擎包括两部分
- Workspace - 工作区状态
- Designer - 设置器状态
// 2. 引擎初始化
const engine = createEngine({
workspace,
});
window.__workspace__ = workspace;
初始化变量配置
自定义配置数据
{
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 沙箱进行通信
const sandboxQuery = new DndQuery({
context: 'iframe',
});
渲染布局
布局很好理解,包括以下部分
- 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);
}
}}
/>;
- 在沙箱加载完成之后,会设置
MenuData
和ComponentPrototypes
- 根据用户操作渲染左侧组件面板和组件的设置面板
2. 沙箱原理
Sandbox
包括 PreviewSandbox
和 DesignSandbox
两种,其中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 逻辑,如:- 是否允许拖拽
- 布局的基本样式
block
、inline-block
、inline
- 注入
DND 追踪标记
(data-dnd)
其中
DND 追踪标记
是 Workspace
创建 TangoViewModule
的时候调用 traverseViewFile
注入的二、框架、物料设计
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'],
}
],
};
暴露物料的组件,包括 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,
),
);
暴露物料组件的基本描述和设置器设置
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) 指的是在该编辑开发的应用所要遵循的开发规范以及框架,方便后续解析和操作
目录结构
── src
│ +── assets
│ +── pages
│ +── components
│ +── services/index.js
│ +── stores
│ ├── routes.js // 路由配置
│ ├── global.less // 全局样式
| |-- index.js 应用启动配置
└── package.json
|---tango.config.json 设计器和 external 配置
在 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',
}
路由、状态管理(略)
这里有个遗留问题,assets 中允许存非文本文件吗?至少 CodeSandbox 是支持的