首页  |  学习总览  |  ← 返回专题总览 进阶专题 05 · 平台化

低代码平台从 0 到 1 🔥 重点

物料区怎么弄、画布区怎么渲染、属性区怎么操作 —— 亲手搭一个能拖拽、能配置、能渲染的 mini 低代码平台
学习时长:约 18 小时 | 前置:阶段 01/02/05/06 | 产出:可拖拽的 mini 低代码平台

一、概念辨析:低代码 / 无代码 / ProCode

类型谁在用能力边界典型产品
无代码 No-Code业务人员固定模板 + 拖拽,不能写代码明道云、简道云
低代码 Low-Code开发 + 业务可视化搭建 + 可写代码扩展(JS 表达式/函数)阿里宜搭、腾讯微搭、自研中台
ProCode纯开发传统编码,可被低代码平台出码
核心认知:低代码平台本质是「把页面描述成数据(Schema)+ 一个解释器(渲染引擎)」。会写 Schema 和渲染引擎,你就拥有平台。

二、平台架构总览:四大模块

① 物料区(左)组件面板:按钮/输入框/表格…每个物料 = 组件 + Schema 描述

拖到画布使用
② 画布区(中)可视化编辑:渲染组件树、选中、拖拽移动、缩放、对齐

所见即所得
③ 属性区(右)属性面板:Schema 驱动生成表单,改属性、绑事件、绑数据

配置驱动

④ 渲染引擎 + 出码(核心底座):把 Schema 数据在运行时渲染成真实页面;或把 Schema 编译成前端代码(出码),脱离平台运行。

三、页面 Schema 设计(一切的起点)

一个页面的完整描述 = 组件树(树形 JSON)+ 每个组件的属性/事件/样式

{
  "page": { "title": "用户登录", "layout": "flex-column" },
  "components": [
    {
      "id": "c1",                       // 唯一 id(树操作锚点)
      "type": "Input",                 // 组件类型 → 对应物料
      "props": { "placeholder": "请输入账号", "size": "large" },
      "style": { "width": "300px", "margin": "8px 0" },
      "events": { "onChange": { "type": "setState", "target": "form.username" } },
      "children": []
    },
    {
      "id": "c2",
      "type": "Button",
      "props": { "text": "登录", "type": "primary" },
      "events": { "onClick": { "type": "request", "url": "/api/login", "data": "form" } },
      "children": []
    }
  ]
}
Schema 设计三原则:① 组件树与真实 DOM 树同构(渲染时递归即可);② 属性与样式分离(属性面板分组展示);③ 事件是"声明式的"(描述做什么,不写死代码,保证可序列化、可出码)。

四、物料区:物料注册 + 拖拽

1. 什么是物料(Material)?

物料 = 组件 + 它的 Schema 描述。描述里告诉平台:这个组件叫什么、有哪些属性、属性是文本/数字/选择、默认值、图标、如何渲染。

// 物料注册表 material-registry.ts(以 React 为例)
export const MATERIALS = {
  'Input': {
    name: '输入框',
    icon: '📝',
    component: Input,                    // 实际渲染组件
    defaultProps: { placeholder: '', size: 'default' },
    propsSchema: [                       // 属性面板由它自动生成
      { key: 'placeholder', label: '占位文本', type: 'text' },
      { key: 'size', label: '尺寸', type: 'select', options: [{v:'default',l:'默认'},{v:'large',l:'大'}] },
      { key: 'disabled', label: '禁用', type: 'switch' },
    ],
  },
  'Button': { /* …同理 */ },
  'Table':  { /* …同理 */ },
};

2. 拖拽实现(HTML5 Drag & Drop 最简方案)

// 物料项:拖拽源(dragstart 时把物料 type 放进 dataTransfer)
function MaterialItem({ type }) {
  return (
    <div
      draggable
      onDragStart={e => { e.dataTransfer.setData('material', type); e.dataTransfer.effectAllowed = 'copy'; }}
    >
      {MATERIALS[type].icon} {MATERIALS[type].name}
    </div>
  );
}

// 画布容器:放置目标(drop 时读取物料 type,插入组件树)
function Canvas({ tree, addNode }) {
  const onDrop = e => {
    e.preventDefault();
    const type = e.dataTransfer.getData('material');
    if (type) addNode({ id: genId(), type, props: MATERIALS[type].defaultProps, style: {}, events: {}, children: [] });
  };
  return <div onDragOver={e => e.preventDefault()} onDrop={onDrop}>…</div>;
}
拖拽细节坑:① 容器必须 onDragOvere.preventDefault(),否则 drop 不触发;② 拖到已有组件的"内部"还是"后面"要区分(判断 drop 目标与偏移);③ 移动端拖拽要换成 Pointer Events 或长按方案。

五、画布区:Schema → 组件树 → 渲染

1. 递归渲染:Schema 即组件树

function Renderer({ node, selectedId, onSelect }) {
  const M = MATERIALS[node.type];
  const Comp = M.component;
  const selected = node.id === selectedId;
  return (
    <div
      className={`render-node${selected ? ' selected' : ''}`}
      onClick={e => { e.stopPropagation(); onSelect(node.id); }}   // 点击选中
      style={node.style}                                          // 注入样式
    >
      <Comp {...node.props} {...bindEvents(node)} />   // 渲染物料组件
      {node.children?.map(child => <Renderer key={child.id} node={child} … />)}
    </div>
  );
}

2. 选中与拖动:编辑器态 ≠ 运行态

// 拖动移动:记录开始位置,mousemove 更新 style.left/top,mouseup 提交
function DragLayer({ node, onMove }) {
  const start = useRef();
  return (
    <div className="drag-handle"
      onMouseDown={e => {
        start.current = { x: e.clientX, y: e.clientY };
        const move = ev => onMove(node.id, { dx: ev.clientX - start.current.x, dy: ev.clientY - start.current.y });
        const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
        document.addEventListener('mousemove', move);
        document.addEventListener('mouseup', up);
      }}
    />
  );
}

3. 对齐与吸附(体验加分项)

拖动时把组件边缘/中心与画布网格线、其他组件边缘对比,距离小于阈值(如 8px)时"吸"上去并显示参考线:遍历其他节点,比较 left/top/right/bottom/center 六个值,找最小差值。

六、属性区:Schema 驱动属性面板

原理:选中节点 → 读它的物料 propsSchema → 自动生成表单 → 修改写回 Schema

function PropertyPanel({ node, onChange }) {
  if (!node) return <div>未选中组件</div>;
  const schema = MATERIALS[node.type].propsSchema;
  return (
    <div>
      <h4>{MATERIALS[node.type].name} 属性</h4>
      {schema.map(field => (
        <FieldEditor key={field.key} field={field}
          value={node.props[field.key]}
          onChange={v => onChange({ ...node.props, [field.key]: v })}   // 写回组件树(immutable)
        />
      ))}
    </div>
  );
}

// FieldEditor 根据 field.type 渲染不同控件
function FieldEditor({ field, value, onChange }) {
  if (field.type === 'text') return <input value={value} onChange={e => onChange(e.target.value)} />;
  if (field.type === 'select') return (
    <select value={value} onChange={e => onChange(e.target.value)}>
      {field.options.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
    </select>
  );
  // switch / number / color / 事件绑定 …同理
}

事件绑定与数据绑定(属性区的进阶)

七、逻辑编排:从"静态配置"到"页面行为"

属性面板绑的"事件",在渲染引擎里被解释成行为。两种主流设计:

方案 A:声明式动作(简单、可出码)

// Schema 中的事件 → 引擎内置动作解释器
"events": {
  "onClick": { "type": "request", "url": "/api/login", "data": "form", "success": { "type": "navigate", "to": "/home" } }
}

const actions = {
  request: async (ctx, cfg) => { const res = await fetch(cfg.url, { method: 'POST', body: JSON.stringify(ctx.state[cfg.data]) }); return res.json(); },
  navigate: (ctx, cfg) => { history.push(cfg.to); },
  setState: (ctx, cfg) => { ctx.setState(cfg.target, cfg.value); },
};
// 触发时:按事件类型找到动作链,依次执行,动作间可传结果

方案 B:可视化流程编排(复杂场景)

用节点图(开始 → 条件 → 动作 → 结束)编辑逻辑,导出为 JSON 流程,引擎用状态机/工作流解释器执行。适合"表单校验、分支跳转、多接口联动"等复杂页面。

选型:80% 页面用声明式动作就够;只有审批流、复杂联动才需要流程编排。先做声明式,验证需求后再加编排器。

八、渲染引擎 + 出码

1. 渲染引擎(运行时解释 Schema)

核心输入:Schema + 物料注册表 + 全局上下文(状态/请求/路由),输出:真实页面。特性:

2. 出码(Schema → 工程代码)

把 Schema 编译成标准 React/Vue 代码,生成可独立部署的工程:

// 简化出码器:Schema → JSX 字符串
function codegen(node, indent = '') {
  const props = Object.entries(node.props).map(([k, v]) => `${k}="${v}"`).join(' ');
  const children = node.children.map(c => codegen(c, indent + '  ')).join('\n');
  return `<${node.type} ${props}>\n${children}\n</${node.type}>`;
}
// 生成 .tsx 文件 → 交给 Vite 构建 → 产出可独立部署的静态页面
运行态 vs 出码:运行态灵活(改 Schema 即时生效,适合后台配置类页面);出码性能最好(无引擎运行时,适合 C 端页面)。大平台通常两种都支持。

九、完整案例:拖拽搭建「用户登录页」全流程

场景:运营要用低代码平台 5 分钟搭一个带校验的登录页

  1. 拖物料:从物料区拖入「容器」→「输入框×2」→「按钮×1」到画布,树变为 4 个节点
  2. 改属性:选中输入框 1,属性区把 placeholder 改为"请输入手机号",校验规则加"11 位数字";按钮文字改"登录"
  3. 绑事件:按钮 onClick → 动作"校验表单 → 发请求 /api/login → 成功后跳转 /home"
  4. 绑数据:输入框 value 绑定 {{ form.phone }},请求体取 form
  5. 保存:Schema 存后端,生成页面链接;同时一键出码 → 下载 React 工程

最终 Schema(简化)

{
  "type": "Container", "style": { "width": "400px", "margin": "100px auto" },
  "children": [
    { "id": "n2", "type": "Input", "props": { "placeholder": "手机号" },
      "events": { "onChange": { "type": "setState", "target": "form.phone" } } },
    { "id": "n3", "type": "Input", "props": { "placeholder": "密码", "type": "password" },
      "events": { "onChange": { "type": "setState", "target": "form.password" } } },
    { "id": "n4", "type": "Button", "props": { "text": "登录" },
      "events": { "onClick": { "type": "request", "url": "/api/login", "data": "form",
        "success": { "type": "navigate", "to": "/home" } } } },
  ]
}

复盘:这套流程里平台自动完成的事

十、坑清单(一线浓缩)

坑 1:Schema 版本兼容 —— 物料升级后老页面炸。解法:Schema 带 version,渲染引擎做迁移(migrate 函数链)。
坑 2:表达式注入 —— 用 eval 执行 {{ }} 被 XSS。解法:白名单解析器,禁止任意代码。
坑 3:拖拽性能 —— 大页面 mousemove 每帧全树重渲染卡顿。解法:拖拽用 transform 定位 + requestAnimationFrame 节流,drop 后才写回树。
坑 4:物料"能用但不好用" —— 平台 100 个组件没人用。解法:物料必须带场景化模板(表格+弹窗+表单一键组合),降低使用门槛。
坑 5:出码不可维护 —— 生成的代码人看不了。解法:出码模板工程化(组件名/注释/格式规范),支持"出码后继续手改再同步"。
坑 6:事件链无调试 —— 页面行为错了一头雾水。解法:运行态 + 编辑态日志面板,动作链逐步执行可视化。
坑 7:权限缺失 —— 运营能改线上页面。解法:发布审核流(草稿 → 预览 → 审核 → 发布)+ 版本回滚。

🏆 实战项目:实现 mini 低代码编辑器

要求(React + 300 行内完成)

  1. 左侧物料面板:Input / Button / Container 三个物料(含 propsSchema)
  2. 中间画布:拖入组件、点击选中、显示选中框;组件可拖动移动(transform 定位)
  3. 右侧属性面板:按选中节点的 propsSchema 自动生成表单,修改即时反映到画布
  4. 顶部导出:点按钮打印当前 Schema JSON;再点"运行"切到运行态(无选中框、可交互)

通关标准

十一、通关自检题

Q1:低代码平台的本质是什么?
展开答案
把页面描述成可序列化的数据(Schema),用渲染引擎解释执行;编辑器只是 Schema 的"可视化编辑器"。
Q2:物料注册表(propsSchema)为什么能同时驱动物料区和属性区?
展开答案
一份 Schema 描述:物料区用它显示名称/图标/默认值,画布用它渲染组件,属性区遍历它自动生成编辑表单 —— 一处定义,三处复用。
Q3:编辑态和运行态为什么必须分开?
展开答案
编辑态需要选中框/拖拽手柄/事件拦截,运行态要干净渲染真实交互;混在一起会污染真实 DOM 与交互行为。
Q4:拖拽为什么必须 preventDefault dragOver?
展开答案
浏览器默认不允许元素作为放置目标,不阻止默认行为 drop 事件永远不会触发。
Q5:数据绑定表达式怎么安全执行?
展开答案
不用 eval/new Function(可注入);用表达式解析器(如 jsep)+ 白名单上下文对象(只暴露 form/api 等)求值,或仅支持点路径取值。