| 类型 | 谁在用 | 能力边界 | 典型产品 |
|---|---|---|---|
| 无代码 No-Code | 业务人员 | 固定模板 + 拖拽,不能写代码 | 明道云、简道云 |
| 低代码 Low-Code | 开发 + 业务 | 可视化搭建 + 可写代码扩展(JS 表达式/函数) | 阿里宜搭、腾讯微搭、自研中台 |
| ProCode | 纯开发 | 传统编码,可被低代码平台出码 | — |
④ 渲染引擎 + 出码(核心底座):把 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 描述。描述里告诉平台:这个组件叫什么、有哪些属性、属性是文本/数字/选择、默认值、图标、如何渲染。
// 物料注册表 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': { /* …同理 */ }, };
// 物料项:拖拽源(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>; }
onDragOver 里 e.preventDefault(),否则 drop 不触发;② 拖到已有组件的"内部"还是"后面"要区分(判断 drop 目标与偏移);③ 移动端拖拽要换成 Pointer Events 或长按方案。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> ); }
// 拖动移动:记录开始位置,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); }} /> ); }
拖动时把组件边缘/中心与画布网格线、其他组件边缘对比,距离小于阈值(如 8px)时"吸"上去并显示参考线:遍历其他节点,比较 left/top/right/bottom/center 六个值,找最小差值。
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 / 事件绑定 …同理 }
{{ form.username }},渲染引擎解析表达式取值;支持简单 JS 表达式(安全沙箱求值)属性面板绑的"事件",在渲染引擎里被解释成行为。两种主流设计:
// 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); }, }; // 触发时:按事件类型找到动作链,依次执行,动作间可传结果
用节点图(开始 → 条件 → 动作 → 结束)编辑逻辑,导出为 JSON 流程,引擎用状态机/工作流解释器执行。适合"表单校验、分支跳转、多接口联动"等复杂页面。
核心输入:Schema + 物料注册表 + 全局上下文(状态/请求/路由),输出:真实页面。特性:
{{ form.username }} 从上下文取值把 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 构建 → 产出可独立部署的静态页面
{{ form.phone }},请求体取 form{
"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" } } } },
]
}