阅读视图

发现新文章,点击刷新页面。

救命!这个低代码工具太香了 ——TinyEngine 物料自动导入上手

本文由TinyEngine低代码物料导入功能贡献者张筠同学原创。

引言:低代码物料接入的痛点与解决方案

在低代码平台开发中,物料接入是核心环节之一。传统物料接入依赖开发者手动编写符合平台协议的 JSON 配置,不仅效率低下,还容易因人为失误导致兼容性问题 —— 尤其是面对海量 UI 组件时,重复的人工操作会大幅拖慢开发进度。

为解决这一痛点,我们开发了 TinyEngine 低代码物料自动导入工具,支持通过 URL 爬取、NPM 包解析、源码上传三种方式,自动提取组件的 Props/Events/Slots 等 API 信息,并转换为符合 TinyEngine 协议的标准物料格式。配套的可视化前端实现了 "导入 - 预览 - 编辑 - 保存" 全流程闭环,将物料接入效率大幅度提升。

本文将从项目设计、核心模块实现、项目部署与使用指南等方面,带大家全面了解这个前后端一体化的物料处理方案。

一、项目概览:技术栈与核心架构

1. 技术栈选型

后端技术栈

  • 运行环境:Node.js v20.19.5+(原生支持 fetch 与 ES6 + 语法)
  • 核心依赖:Express(接口服务)、MySQL(物料存储)、Puppeteer(URL 爬取)、LLM SDK(API 提取)
  • 核心能力:多源数据解析、物料协议转换、异步任务管理

前端技术栈

  • 框架:Vue 3.2+(<script setup>语法)
  • 构建工具:Vite 4.0+(高效热更新与跨域代理)
  • UI 组件:OpenTiny Vue(轻量化企业级组件库)
  • 核心能力:动态表单、任务进度可视化、表格编辑、批量数据管理

2. 整体架构设计

1.png

项目采用前后端分离架构,核心交互流程如下:

  1. 前端发起导入请求(携带 URL/NPM 信息 / 源码文件);
  2. 后端创建异步任务,根据导入类型执行对应解析逻辑;
  3. 调用 LLM 接口提取结构化 API 信息,转换为标准物料格式;
  4. 前端通过轮询获取任务进度,实时展示处理状态;
  5. 处理完成后,前端提供物料预览与编辑功能,支持保存到数据库。

二、核心功能拆解:从解析到可视化

1. 多源物料解析(后端核心能力)

依托四大核心模块协同工作,实现URL、NPM、源码三种导入方式的标准化解析,全程自动化完成从原始数据到TinyEngine标准物料的转化。

(1)URL 导入

基于 API生成模块 的URL表格驱动流程,通过Puppeteer模拟浏览器访问目标URL,根据用户指定的CSS选择器精准定位API表格数据,搭配重试机制保障爬取稳定性;获取表格数据后,传递给LLM模型进行结构化处理,生成包含Props/Events/Slots的原始API JSON;随后经 物料转换模块 转化为符合TinyEngine规范的物料格式,最后由 后处理模块 完成组件名标准化(如统一为PascalCase格式)及规则化优化,确保物料一致性。

(2)NPM 导入

接收用户输入的NPM包名与组件名后,后端自动下载对应包资源,通过 文件筛选模块 的NPM类型规则(强制校验index入口文件,提取含组件关键词的核心文件,自动跳过styleutils等非API相关目录及.map文件)完成文件筛选;筛选后的核心文件进入 API生成模块 的文件驱动流程,经LLM解析生成原始API JSON;再通过 物料转换模块 补充组件基本信息、规范Props/Events/Slots定义,最终由 后处理模块 按预设规则优化(无需处理的组件直接保留、表格组件合并列定义等),输出标准物料。

(3)源码导入

用户可上传Vue组件源码文件或ZIP压缩包,后端解压后触发 文件筛选模块 的源码类型处理逻辑——自动识别index.js/ts入口文件及Props/Events定义文件,过滤非API相关内容;筛选后的有效文件进入 API生成模块 执行文件驱动流程,经LLM解析生成原始API JSON;后续通过 物料转换模块 转化为TinyEngine标准格式,再由 后处理模块 清理子项组件冗余片段、统一组件名格式,最终生成可直接使用的标准化物料。

2. 可视化操作闭环(前端核心能力)

(1)动态导入表单

根据用户选择的导入类型(URL/NPM/ 源码),自动切换对应表单:

  • URL 导入:展示 URL 输入框与表格 CSS 选择器输入框;
  • NPM 导入:展示包名与组件名输入框;
  • 源码导入:展示文件上传组件(支持单个文件与 ZIP 包)。

(2)任务进度可视化

  • 提交导入请求后,展示进度条实时更新处理进度(0-100%);
  • 处理中任务支持最小化为右侧悬浮卡片,不影响其他操作,点击可重新打开模态框查看详情;
  • 任务状态实时反馈(处理中 / 成功 / 失败),失败时显示具体错误信息。

(3)物料预览与编辑

任务处理成功后,通过表格展示生成的物料列表,支持:

  • 主表展示基础信息(组件名、导入类型、导入时间等);
  • 展开行查看子表(属性 / 事件 / 插槽),支持编辑字段值、删除无效项;
  • 编辑后实时提交更新,确保数据同步到后端。

(4)物料库管理

提供完整的物料管理功能:

  • 筛选与搜索:按组件名精确筛选、关键词模糊搜索;
  • 批量操作:批量导出选中物料为 JSON 文件、批量删除无用物料;
  • 分页控制:默认 10 条 / 页,支持自定义每页显示数量。

三、项目部署

1. 环境要求

环境/工具 版本要求 说明
Node.js v20.19.5 及以上 支持fetch、ES6+语法,前后端通用,建议使用官网长期支持版确保兼容性
前端框架 Vue 3.2+ 前端采用<script setup>语法开发,需确保依赖版本符合要求
构建工具 Vite 4.0+ 负责前端项目构建、热更新及跨域代理配置,当前项目实际使用v7.1.7版本
数据库 MySQL 8.0 及以上 用于存储物料数据,需提前本地安装并启动
依赖服务 LLM接口(如DeepSeek/Qwen/OpenAI) 后端核心依赖,需准备支持JSON输出的大模型接口及对应API密钥、接口地址

2. 安装与配置

2.1 克隆仓库

首先将项目代码克隆到本地,执行以下命令:

git clone <仓库地址>  # 替换为实际的项目仓库地址

2.2 安装依赖

进入项目根目录后,分别安装后端和前端的依赖包,确保环境一致性:

# 1. 安装后端依赖
cd backend
npm install  # 若使用yarn/pnpm,可替换为yarn install/pnpm install

# 2. 安装前端依赖(需新开终端或返回根目录)
cd frontend
npm install  # 同理可替换为对应包管理工具的安装命令

2.3 环境配置

(1)后端配置
  1. 复制环境变量模板:进入后端目录,将 .env.example 模板文件复制为实际使用的 .env 文件:

    cd backend  # 若当前不在后端目录需执行此命令
    cp .env.example .env  # Windows系统可手动复制文件并重命名为.env
    
  2. 编辑.env文件参数:用文本编辑器打开 .env 文件,根据本地环境和实际资源信息填写以下关键配置(替换占位符内容):

    # 服务器配置
    SERVER_PORT=3001                  # 后端服务端口,默认3001,可按需修改
    CORS_ALLOW_ORIGIN=http://localhost:8080 # 前端地址,需与前端端口保持一致,解决跨域问题
    
    # 数据库配置(需与本地MySQL环境匹配)
    MYSQL_HOST=localhost       # MySQL服务地址,本地默认localhost
    MYSQL_PORT=3306            # MySQL端口,默认3306
    MYSQL_USER=root            # MySQL用户名,替换为你的实际用户名
    MYSQL_PASSWORD=your_password    # MySQL密码,替换为你的实际密码(无密码则留空)
    MYSQL_DATABASE=lowcode_material # 数据库名,需后续手动创建该库
    
    # LLM模型配置(必填,替换为实际可用的大模型信息)
    OPENAI_MODEL=deepseek-reasoner            # 模型名称,如deepseek-reasoner、Qwen3-32B等
    OPENAI_API_KEY=your_api_key_here          # 模型API密钥,从对应平台获取
    OPENAI_BASE_URL=https://api.deepseek.com/v1 # 模型接口地址,按实际平台填写
    
    # 默认路径配置(系统自动创建,无需手动操作)
    DEFAULT_OUTPUT_DIR=output-log       # 最终物料JSON输出目录
    DEFAULT_SCHEMA_LOG_DIR=schema-log   # 转换过程日志目录
    DEFAULT_API_LOG_DIR=raw-api-log     # 原始API JSON日志目录
    
(2)前端配置(跨域与端口)

前端需配置代理对接后端服务,确保接口请求正常,步骤如下:

  1. 进入前端目录,打开 vite.config.js 文件(路径:frontend/vite.config.js);
  2. 确认或修改以下配置(需与后端 .env 中的配置保持一致):
    import { defineConfig } from 'vite'; 
    import vue from '@vitejs/plugin-vue'; 
    
    export default defineConfig({
      plugins: [vue()],
      server: {
        port: 8080, // 前端端口,默认8080,需与后端CORS_ALLOW_ORIGIN中的端口一致
        proxy: {
          // 代理所有/api前缀的请求到后端服务
          '/api': {
            target: 'http://localhost:3001', // 后端服务地址,与SERVER_PORT一致
            changeOrigin: true, // 开启跨域适配
          }
        }
      }
    });
    
    • 若修改了前端端口或后端服务端口,需同步更新对应配置。

3. 快速启动

前后端服务需按特定顺序启动,核心顺序:启动MySQL服务 → 创建项目数据库 → 启动后端服务 → 启动前端服务,具体步骤如下:

3.1 启动MySQL服务并创建项目数据库

(1)启动MySQL服务

根据本地操作系统,执行对应启动命令:

  • Windows:通过“服务”管理器找到“MySQL”服务,右键点击“启动”;
  • macOS(Homebrew安装):打开终端执行 brew services start mysql
  • Linux(系统服务,以Ubuntu为例):执行 sudo systemctl start mysql(其他发行版按对应命令操作)。
(2)手动创建项目数据库

项目需使用预先创建的 lowcode_material 数据库,执行以下步骤:

  1. 打开终端/命令提示符,登录MySQL:
    mysql -u root -p  # 替换root为你的MySQL用户名,回车后输入密码(无密码直接回车)
    
  2. 执行SQL命令创建数据库(指定字符集避免中文乱码):
    CREATE DATABASE IF NOT EXISTS lowcode_material 
    CHARACTER SET utf8mb4 
    COLLATE utf8mb4_unicode_ci;
    
  3. (可选)验证数据库创建成功:
    SHOW DATABASES;  # 执行后查看输出列表,确认包含lowcode_material
    
  4. 退出MySQL命令行:
    exit;
    

3.2 启动后端服务

# 进入后端目录(若当前不在该目录)
cd backend

# 启动开发环境服务(使用配置的npm脚本)
npm run serve
  • 启动成功标识:终端显示服务监听信息(如 后端服务启动成功,端口:3001);
  • 验证接口可用性:可通过浏览器访问 http://localhost:3001/api/material/docs,查看简易接口文档。

3.3 启动前端服务

需新开一个终端(避免与后端服务冲突),执行以下命令:

# 进入前端目录
cd frontend

# 启动开发环境(支持热更新,修改代码后自动刷新)
npm run dev
  • 启动成功标识:终端输出 VITE v7.1.7 ready in 300 ms 及访问地址;
  • 访问前端:打开浏览器输入默认地址 http://localhost:8080,即可进入物料管理首页。

4. 启动常见问题排查

  • MySQL连接失败:检查 .env 中数据库配置(地址、端口、用户名、密码)是否与本地环境一致,确保MySQL服务已启动;
  • LLM接口报错:检查模型配置(API密钥、接口地址、模型名称)是否正确,确保接口可正常访问且有剩余调用额度。

四、核心使用流程

  1. 访问前端地址(默认 http://localhost:8080),进入物料管理首页;

2.png

  1. 选择导入方式(URL / NPM / 源码),填写对应信息(如 URL 地址、NPM 包名、上传源码文件);

    • URL 导入:输入URL地址和API表格CSS选择器; 3.gif
    • NPM 导入:输入NPM包名和组件名称; 4.gif
    • 源码导入:上传源码文件(支持单个文件或zip)。 5.gif
  2. 提交后等待任务处理,实时查看进度条(0-100%),进度条支持最小化;

6.gif

  1. 任务成功后,预览生成的物料数据(属性 / 事件 / 插槽),可直接编辑修改或删除;
  • 编辑属性/事件/插槽

7.gif

  • 删除属性/事件/插槽

8.gif

  • 删除组件物料

9.gif

  1. 点击 “保存到物料库”,将物料同步至 MySQL 数据库;

10.gif

  1. 在首页通过筛选、搜索功能管理已保存的物料,支持批量导出或删除。
  • 通过组件名称筛选组件物料

11.gif

  • 通过关键词筛选组件物料

12.gif

  • 批量删除组件物料

13.gif

  • 批量导出组件物料

14.gif

导出文件如下(以element-plus的Breadcrumb为例):

[
  {
    "npm": {
      "package": "element-plus",
      "exportName": "ElBreadcrumb"
    },
    "icon": "breadcrumb",
    "name": {
      "zh_CN": "面包屑"
    },
    "tags": [
      "导航",
      "面包屑"
    ],
    "group": "element-plus",
    "schema": {
      "slots": {
        "default": {
          "label": {
            "zh_CN": "默认内容"
          },
          "description": {
            "zh_CN": "自定义默认内容"
          }
        }
      },
      "events": {},
      "properties": [
        {
          "name": "0",
          "label": {
            "zh_CN": "基础属性"
          },
          "content": [
            {
              "cols": 12,
              "type": "ArrayItemConfigurator-test",
              "label": {
                "text": {
                  "zh_CN": "分隔符"
                }
              },
              "widget": {
                "props": {
                  "placeholder": "请输入分隔符"
                },
                "component": "InputConfigurator"
              },
              "disabled": false,
              "property": "separator",
              "readOnly": false,
              "required": false,
              "description": {
                "zh_CN": "分隔符"
              },
              "defaultValue": "/",
              "labelPosition": "left"
            },
            {
              "cols": 12,
              "type": "unknown",
              "label": {
                "text": {
                  "zh_CN": "图标分隔符"
                }
              },
              "widget": {
                "props": {
                  "placeholder": "请输入图标名称"
                },
                "component": "InputConfigurator"
              },
              "disabled": false,
              "property": "separatorIcon",
              "readOnly": false,
              "required": false,
              "description": "图标分隔符组件",
              "defaultValue": null,
              "labelPosition": "left"
            }
          ],
          "description": {
            "zh_CN": "组件核心功能相关的配置,包括 separator、 separatorIcon 等核心属性"
          }
        }
      ]
    },
    "devMode": "proCode",
    "doc_url": "",
    "version": "",
    "category": "element-plus",
    "keywords": [
      "Breadcrumb",
      "面包屑",
      "导航"
    ],
    "snippets": [
      {
        "icon": "breadcrumb",
        "name": {
          "zh_CN": "面包屑"
        },
        "schema": {
          "children": [
            {
              "props": {
                "to": "/home"
              },
              "children": [
                {
                  "props": {
                    "text": "首页"
                  },
                  "componentName": "Text"
                }
              ],
              "componentName": "ElBreadcrumbItem"
            },
            {
              "props": {
                "to": "/list"
              },
              "children": [
                {
                  "props": {
                    "text": "列表页"
                  },
                  "componentName": "Text"
                }
              ],
              "componentName": "ElBreadcrumbItem"
            },
            {
              "props": {},
              "children": [
                {
                  "props": {
                    "text": "详情页"
                  },
                  "componentName": "Text"
                }
              ],
              "componentName": "ElBreadcrumbItem"
            }
          ]
        },
        "category": "element-plus",
        "screenshot": "",
        "snippetName": "ElBreadcrumb"
      }
    ],
    "component": "ElBreadcrumb",
    "configure": {
      "loop": true,
      "styles": true,
      "isModal": false,
      "isLayout": false,
      "isPopper": false,
      "condition": true,
      "framework": "Vue",
      "shortcuts": {
        "properties": [
          "separator"
        ]
      },
      "isNullNode": false,
      "contextMenu": {
        "actions": [
          "copy",
          "remove",
          "insert",
          "updateAttr",
          "bindEvent"
        ],
        "disable": []
      },
      "isContainer": true,
      "nestingRule": {
        "childWhitelist": "ElBreadcrumbItem",
        "parentWhitelist": "",
        "ancestorWhitelist": "",
        "descendantBlacklist": ""
      },
      "clickCapture": false,
      "rootSelector": ""
    },
    "description": "面包屑导航组件,用于显示当前页面在系统层级 结构中的位置"
  }
]

总结

TinyEngine 低代码物料自动导入工具的核心目标,是通过「自动化解析 + 可视化操作」的闭环设计,解决了低代码物料接入的效率瓶颈与兼容性难题。无论是 URL 爬取、NPM 解析还是源码上传,工具都力求简化全流程操作,让开发者无需关注底层协议细节与格式转换逻辑,即可快速将各类 UI 组件无缝转化为符合 TinyEngine 标准的可用物料,让低代码物料接入从繁琐的手动配置,转变为高效、省心的一站式操作。

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网:opentiny.design
OpenTiny 代码仓库:github.com/opentiny
TinyVue 源码:github.com/opentiny/ti…
TinyEngine 源码: github.com/opentiny/ti…
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~ 如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~

从零实现一个低代码编辑器:揭秘可视化搭建的核心原理

引言:当编程遇上「拖拽」

还记得第一次接触编程时的兴奋吗?在黑色的终端里输入几行神秘的代码,就能让计算机按照我们的意愿工作。但随着编程经验的增长,我们也会发现:很多重复性的页面开发工作,其实并不需要每次都从头开始写代码。

这就是低代码/零代码平台诞生的背景。作为一名开发者,我最初对这类平台是抱有怀疑态度的——「拖拽就能生成应用?肯定很鸡肋吧!」直到我真正深入使用并实现了一个低代码编辑器,才发现其中的技术内涵远比想象中丰富。

今天,就让我带你一起揭开低代码编辑器的神秘面纱,看看这看似简单的「拖拽」背后,到底藏着怎样的技术奥秘。

低代码与零代码:概念辨析

什么是低代码/零代码?

低代码(Low-Code)和零代码(No-Code)都是通过可视化界面和配置化方式,减少或替代传统手写代码的应用开发方法。

  • 低代码:主要面向开发者,提供可视化开发工具提升开发效率
  • 零代码:让非技术人员也能搭建简单应用,如表单、审批流程、数据看板等

实际应用场景

在一个企业内部管理系统项目中,开发者使用低代码平台快速搭建了:

  1. 员工请假审批流程
  2. 数据报表展示看板
  3. 客户信息管理表单

原本需要2周开发的功能,在低代码平台上只用了2天就完成了配置和测试,效率提升惊人!

低代码编辑器核心架构

三大核心区域

任何低代码编辑器都包含三个基本区域:

  1. 物料区域:提供可拖拽的组件
  2. 编辑区域:组件组合和布局的区域
  3. 属性设置区域:配置组件属性的面板
// 编辑器布局组件示例
import { Allotment } from 'allotment';

export default function LowcodeEditor() {
  return (
    <div className="h-[100vh] flex flex-col">
      <Header />
      <Allotment>
        <Allotment.Pane preferredSize={240}>
          <Material /> {/* 物料区域 */}
        </Allotment.Pane>
        <Allotment.Pane>
          <EditArea /> {/* 编辑区域 */}
        </Allotment.Pane>
        <Allotment.Pane preferredSize={300}>
          <Setting /> {/* 属性设置区域 */}
        </Allotment.Pane>
      </Allotment>
    </div>
  )
}

核心技术栈

在实现低代码编辑器时,我们选择了以下技术栈:

  • React + TypeScript:组件化开发和类型安全
  • react-dnd:实现拖拽功能
  • allotment:可调整大小的分栏布局
  • zustand:轻量级状态管理
  • tailwindcss:原子化CSS样式

实现细节深度解析

状态管理:编辑器的「大脑」

低代码编辑器的核心是一个表示组件树结构的状态管理。我们使用 zustand 来管理这个状态:

// 组件数据结构定义
export interface Component {
  id: number;
  name: string;      // 组件类型,如 'Button', 'Container'
  props: any;        // 组件属性
  children?: Component[]; // 子组件
  parentId?: number;     // 父组件ID
}

// 状态管理store
export const useComponentsStore = create<State & Action>((set, get) => ({
  components: [
    {
      id: 1,
      name: 'Page',
      props: {},
      desc: '页面'
    },
  ],
  // 添加组件
  addComponent: (component, parentId) => set((state) => {
    if (parentId) {
      // 找到父组件并添加子组件
      const parentComponent = getComponentById(parentId, state.components);
      if (parentComponent) {
        if (parentComponent.children) {
          parentComponent.children.push(component);
        } else {
          parentComponent.children = [component];
        }
      }
      component.parentId = parentId;
      return {
        components: [...state.components],
      }
    }
    return {
      components: [...state.components, component],
    }
  }),
  // 其他操作...
}));

这个数据结构虽然简单,但却是整个编辑器的核心。它本质上是一棵树,通过 parentIdchildren 属性构建出完整的组件层级关系。

组件配置管理:编辑器的「组件库」

为了让编辑器知道有哪些组件可用,我们需要一个组件配置管理系统:

export interface ComponentConfig {
  name: string;
  defaultProps: Record<string, any>; // 默认属性
  component: any; // React组件
}

export const useComponentConfigStore = create<State & Actions>((set) => ({
  componentConfig: {
    Container: {
      name: "Container",
      defaultProps: {},
      component: Container
    },
    Button: {
      name: "Button",
      defaultProps: {
        type: "primary",
        text: "按钮",
      },
      component: Button
    },
    Page: {
      name: "Page",
      defaultProps: {},
      component: Page
    },
  },
  // 注册新组件
  registerComponent: (name, componentConfig) => set((state) => {
    return {
      ...state,
      componentConfig: {
        ...state.componentConfig,
        [name]: componentConfig,
      }
    }
  }),
}));

拖拽实现:编辑器的「交互灵魂」

拖拽功能是低代码编辑器最核心的交互方式。我们使用 react-dnd 来实现:

// 物料项 - 可拖拽的组件
export function MaterialItem(props: MaterialItemProps) {
  const { name } = props;
  const [_, drag] = useDrag({
    type: name,
    item: {
      type: name,
    }
  });
  
  return (
    <div
      ref={drag}
      className="border-dashed border-[1px] border-[#000] py-[8px] px-[10px] m-[10px] cursor-move inline-block"
    >
      {name}
    </div>
  )
}

// 放置区域hook
export function useMaterialDrop(accept: string[], id: number) {
  const { addComponent } = useComponentsStore();
  const { componentConfig } = useComponentConfigStore();
  
  const [{ canDrop }, drop] = useDrop(() => ({
    accept,
    drop: (item: { type: string }, monitor) => {
      const didDrop = monitor.didDrop();
      if (didDrop) return; // 防止重复触发
      
      const props = componentConfig[item.type].defaultProps;
      addComponent({
        id: new Date().getTime(),
        name: item.type,
        props
      }, id);
    },
    collect: (monitor) => ({
      canDrop: monitor.canDrop(),
    }),
  }));
  
  return { canDrop, drop };
}

组件渲染:从数据到UI

编辑区域需要将组件树数据渲染为实际的UI:

export function EditArea() {
  const { components } = useComponentsStore();
  const { componentConfig } = useComponentConfigStore();

  function renderComponents(components: Component[]): React.ReactNode {
    return components.map((component: Component) => {
      const config = componentConfig?.[component.name];
      if (!config?.component) return null;
      
      // 递归渲染子组件
      return React.createElement(
        config.component,
        {
          key: component.id,
          id: component.id,
          ...config.defaultProps,
          ...component.props,
        },
        renderComponents(component.children || [])
      );
    })
  }

  return <>{renderComponents(components)}</>;
}

实际组件示例

基础容器组件

import type { CommonComponentProps } from "../../interface";
import { useMaterialDrop } from '../../hooks/useMaterialDrop';

const Container = ({ id, name, children }: CommonComponentProps) => {
  // 容器可以接受 Button 和 Container 类型的拖拽
  const { canDrop, drop } = useMaterialDrop(['Button', 'Container'], id);
  
  return (
    <div
      ref={drop}
      className="border-[1px] border-[#000] min-h-[100px] p-[20px]"
    >
      {children}
    </div>
  )
};

export default Container;

按钮组件

import { Button as AntdButton } from "antd";
import type { ButtonType } from "antd/es/button";

export interface ButtonProps {
  type: ButtonType;
  text: string;
}

const Button = ({ type, text }: ButtonProps) => {
  return <AntdButton type={type}>{text}</AntdButton>;
}

export default Button;

技术难点与解决方案

问题:useDrop 重复触发

在实现拖拽功能时,我们遇到了一个常见问题:当在嵌套的容器中拖拽组件时,useDrop 会被多次触发,导致同一个组件被重复添加。

解决方案:通过 monitor.didDrop() 检查是否已经在子元素中处理了 drop 事件:

drop: (item: { type: string }, monitor) => {
  const didDrop = monitor.didDrop();
  if (didDrop) return; // 如果已经在子元素处理过,则不再处理
  
  // 正常的添加组件逻辑...
}

问题:组件树操作复杂性

对组件树进行增删改查操作时,需要考虑嵌套结构带来的复杂性。

解决方案:实现递归工具函数来处理组件树:

export function getComponentById(
  id: number | null,
  components: Component[]
): Component | null {
  if (!id) return null;
  
  for (const component of components) {
    if (component.id === id) return component;
    
    if (component.children && component.children.length > 0) {
      const found = getComponentById(id, component.children);
      if (found) return found;
    }
  }
  return null;
}

低代码编辑器的价值思考

对开发者的价值

  1. 提升开发效率:重复性页面可以快速搭建
  2. 降低维护成本:可视化配置比代码更直观易懂
  3. 促进团队协作:产品、设计也能参与页面搭建

对企业的价值

  1. 降低技术门槛:业务人员也能搭建简单应用
  2. 快速响应需求:业务变化时能快速调整
  3. 成本控制:减少对高级开发人员的依赖

扩展思路:让编辑器更强大

基础的低代码编辑器实现后,我们可以考虑添加更多高级功能:

1. 撤销重做功能

interface HistoryState {
  past: Component[][];
  present: Component[];
  future: Component[][];
}

// 在每次状态变更时记录历史

2. 组件数据绑定

// 支持将组件属性绑定到数据源
{
  "type": "bind",
  "value": "{{user.name}}"
}

3. 条件渲染和循环渲染

// 支持根据条件显示/隐藏组件
{
  "condition": "{{user.isAdmin}}",
  "component": "AdminPanel"
}

// 支持循环渲染
{
  "loop": "{{userList}}",
  "component": "UserItem"
}

4. 事件处理系统

// 配置按钮点击事件
{
  "onClick": {
    "action": "navigate",
    "params": {
      "url": "/detail"
    }
  }
}

总结

通过这个简单的低代码编辑器实现,我们可以看到:

  1. 低代码的核心是数据结构:一个精心设计的组件树结构是基础
  2. 拖拽交互是关键体验:流畅的拖拽体验决定编辑器的易用性
  3. 组件化思维是桥梁:将UI拆分为可配置的组件是实现可视化的前提
  4. 扩展性是生命力:良好的架构设计让后续功能扩展成为可能

低代码并不是要取代传统开发,而是为特定场景提供更高效的解决方案。作为开发者,理解低代码背后的原理,不仅能让我们更好地使用这类平台,还能在适当时机自己构建适合业务的可视化工具。

技术的本质不是堆砌复杂度,而是在理解原理的基础上做出恰当的简化。希望这篇笔记能帮助你理解低代码编辑器的核心原理,在可视化开发的道路上走得更远。

❌