Boost Frontend Efficiency with imove: Visual Workflow and Code Generation Using X6
This article introduces imove, a visual workflow tool built on antv‑X6 and form‑render, explaining how it improves development speed, enables reusable logic components, supports synchronous and asynchronous JavaScript code, and provides detailed integration steps, shortcuts, and export mechanisms for modern frontend projects.
Recently the imove project on GitHub has seen rapid star growth, indicating its value. The tool was designed to increase developer efficiency by visualizing business logic and reusing logical components such as UI, API, and functions. In a Double‑11 business scenario, imove was used to speed up development and accumulate many logical components.
Readers may wonder why a new flowchart‑based approach is needed instead of the traditional frontend development model of writing code from UI specifications. This article covers two key points: how imove changes the development paradigm and why compiling flowcharts into runnable code is more attractive than merely drawing them.
How imove develops and why it breaks traditional development patterns.
How imove compiles flowcharts into actual business project code.
How is imove’s visual orchestration implemented?
The core of imove is based on the X6 protocol.
Nodes: visual reuse and orchestration via X6’s UI.
Edges: visual flow with optional parameters.
Functions and schema2form: support for function definitions and form configuration, built on Alibaba’s form‑render.
The project is not complex; it integrates X6 and form‑render, standardizes writing, and provides a small‑but‑beautiful tool for developers.
Development workflow based on imove
Draw the flowchart
Draw business logic flowcharts with start (circle), branch (diamond), and action (rectangle) nodes. Multiple execution chains can be created, each starting from a start node.
Write code for each node
Double‑click a node to open the code editor and write JavaScript. Both synchronous and asynchronous logic are supported. Example code:
<code>// 同步代码
export default function(ctx) {
const data = ctx.getPipe();
return doSomething(data);
}
// 异步代码 - 写法1 使用 promise
export default function(ctx) {
return new Promise(resolve => {
setTimeout(() => resolve(), 2000);
});
}
// 异步代码 - 写法2 使用 async/await
export default async function(ctx) {
const data = await fetchData();
return data;
}</code>Integrate into a project
After writing node code, you can import it into your project in two ways:
Export the compiled code as a zip file and import it manually.
Run imove in local development mode (
imove -d) for real‑time debugging.
Exporting code example:
<code>// (1) 本地打包出码
点击页面右上方的"导出"按钮后,选择“导出代码”,代码会以 <code>zip</code> 包形式下载。
// (2) 本地启动开发模式
$ npm install -g @imove/cli
$ cd yourProject
$ imove --init # or imove -i
$ imove --dev # or imove -d</code>Using the exported logic:
<code>import React, {useEffect} from 'react';
import logic from './logic';
const App = () => {
useEffect(() => {
// 监听事件 a
logic.on('a', data => {});
// 触发开始节点 b
logic.invoke('b');
}, []);
return <div>xxx</div>;
};
export default App;</code>Why use visual orchestration?
Common pain points in frontend development include frequent UI changes, difficulty reusing logic, lack of clear documentation, and onboarding challenges for new developers. Visual orchestration addresses these by:
Requirement visualization: Flowcharts make complex logic easy to understand for both developers and non‑technical stakeholders.
Logic reuse: Node‑level code can be reused across projects, reducing duplication.
Improved code standards: Each node handles a single responsibility, promoting high cohesion and low coupling.
These benefits help solve the listed problems and streamline development.
Based on X6 flow orchestration
imove uses antv‑X6 for graph editing, providing features such as node rotation, resizing, clipboard, connection rules, background, grid, selection, snaplines, keyboard shortcuts, undo/redo, scrolling, zoom, and more.
<code>import { Graph } from '@antv/X6';
const flowChart = new Graph({
container: document.getElementById('flowChart'),
rotating: false,
resizing: true,
clipboard: { enabled: true, useLocalStorage: true },
connecting: { snap: true, dangling: true, highlight: true, anchor: 'center', connectionPoint: 'anchor', router: { name: 'manhattan' } },
background: { color: '#f8f9fa' },
grid: { visible: true },
selecting: { enabled: true, multiple: true, rubberband: true, movable: true, strict: true, showNodeSelectionBox: true },
snapline: { enabled: true, clean: 100 },
history: { enabled: true },
scroller: { enabled: true },
mousewheel: { enabled: true, minScale: MIN_ZOOM, maxScale: MAX_ZOOM, modifiers: ['ctrl', 'meta'] },
});
</code>Additional features such as keyboard shortcuts, event registration, and server storage are added on top of the base Graph.
Keyboard shortcuts
<code>const shortcuts = {
copy: {
keys: 'meta + c',
handler(flowChart) {
const cells = flowChart.getSelectedCells();
if (cells.length > 0) {
flowChart.copy(cells);
message.success('复制成功');
}
return false;
},
},
paste: {
keys: 'meta + v',
handler(flowChart) {
if (!flowChart.isClipboardEmpty()) {
const cells = flowChart.paste({ offset: 32 });
flowChart.cleanSelection();
flowChart.select(cells);
}
return false;
},
},
// ... other shortcuts like save, undo, redo, zoomIn, zoomOut, delete, selectAll, etc.
};
</code>Binding shortcuts:
<code>shortcuts.forEach(shortcut => {
const { keys, handler } = shortcut;
graph.bindKey(keys, () => handler(graph));
});
</code>Event registration
<code>const registerEvents = (flowChart) => {
// Double‑click node to open code editor
flowChart.on('node:dblclick', () => {});
// Right‑click on blank area
flowChart.on('blank:contextmenu', args => {});
// Right‑click on node
flowChart.on('node:contextmenu', args => {});
};
</code>Factory to create the flowchart
<code>const registerShortcuts = (flowChart) => {
Object.values(shortcuts).forEach(shortcut => {
const { keys, handler } = shortcut;
flowChart.bindKey(keys, () => handler(flowChart));
});
};
const createFlowChart = (container, miniMapContainer) => {
const flowChart = new Graph({
// many configuration options
});
registerEvents(flowChart);
registerShortcuts(flowChart);
registerServerStorage(flowChart);
return flowChart;
};
export default createFlowChart;
</code>Exporting models
<code>// Export DSL
const onExportDSL = () => {
const dsl = JSON.stringify(flowChart.toJSON(), null, 2);
const blob = new Blob([dsl], { type: 'text/plain' });
DataUri.downloadBlob(blob, 'imove.dsl.json');
};
// Export code (zip)
const onExportCode = () => {
const zip = new JSZip();
const dsl = flowChart.toJSON();
const output = compileForProject(dsl);
Helper.recursiveZip(zip, output);
zip.generateAsync({ type: 'blob' }).then(blob => {
DataUri.downloadBlob(blob, 'logic.zip');
});
};
// Export flowchart image
const onExportFlowChart = () => {
flowChart.toPNG(dataUri => {
DataUri.downloadDataUri(dataUri, 'flowChart.png');
}, { padding: 50, ratio: '3.0' });
};
</code>Node design
Three node types are defined:
Start node – entry point of the logic.
Action node – performs operations such as API calls.
Branch node – routes based on a boolean result (must start with a start node).
Node attributes
Nodes store visual data (id, shape, position, size) and custom data (type, label, role, code, dependencies, trigger, ports, configSchema, configData, version, forkId, referId). Commonly edited fields include display name, trigger name, and form schema.
Node dragging
Nodes can be added via
addNodeand
addEdgeor by using X6’s DnD class.
<code>const source = graph.addNode({ id: 'node1', x: 40, y: 40, width: 80, height: 40, label: 'Hello' });
const target = graph.addNode({ id: 'node2', x: 160, y: 180, width: 80, height: 40, label: 'World' });
graph.addEdge({ source, target });
</code> <code>import { Addon, Graph } from '@antv/X6';
const { Dnd } = Addon;
const graph = new Graph({ id: document.getElementById('flowchart'), grid: true, snapline: { enabled: true } });
const dnd = new Dnd({ target: graph, scaled: false, animation: true });
const sideBar = new Graph({ id: document.getElementById('sideBar'), interacting: false });
sideBar.addNode({ id: 'node1', x: 80, y: 80, width: 80, height: 40, label: 'Hello' });
sideBar.addNode({ id: 'node2', x: 80, y: 140, width: 80, height: 40, label: 'iMove' });
sideBar.on('cell:mousedown', args => {
const { node, e } = args;
dnd.start(node.clone(), e);
});
</code>Node styling
Node appearance can be customized via
setAttrs(font size, weight, style, colors, etc.).
<code>// Set font size
cell.setAttrs({ label: { fontSize: 14 } });
// Set bold
cell.setAttrs({ label: { fontWeight: 'bold' } });
// Set italic
cell.setAttrs({ label: { fontStyle: 'italic' } });
// Set text color
cell.setAttrs({ label: { fill: 'red' } });
// Set background color
cell.setAttrs({ body: { fill: 'green' } });
</code>Node code writing
Each node’s code is a JS module exporting a function; async functions return a Promise.
<code>export default async function() {
return fetch('/api/isLogin')
.then(res => res.json())
.then(res => {
const { success, data: { isLogin } = {} } = res;
return success && isLogin;
})
.catch(err => {
console.log('fetch /api/isLogin failed, the err is:', err);
return false;
});
}
</code>Node data communication
Data flows through a pipe; each node receives upstream data via
ctx.getPipe(). Branch nodes forward data unchanged while routing based on a boolean result.
<code>// Request profile node
export default async function() {
return fetch('/api/profile')
.then(res => res.json())
.then(res => {
const { success, data } = res;
return { success, data };
})
.catch(err => {
console.log('fetch /api/profile failed, the err is:', err);
return { success: false };
});
}
// Interface success node
export default function(ctx) {
const pipe = ctx.getPipe() || {};
return pipe.success;
}
// Return data node
export default function(ctx) {
const pipe = ctx.getPipe() || {};
ctx.emit('updateUI', { profileData: processData(pipe.data) });
}
</code>Edge design
Edges only store connection information (id, shape, source, target). Example JSON:
<code>{
"id": "5d034984-e0d5-4636-a5ab-862f1270d9e0",
"shape": "edge",
"source": { "cell": "1b44f69a-1463-4f0e-b8fc-7de848517b4e", "port": "bottom" },
"target": { "cell": "c18fa75c-2aad-40e9-b2d2-f3c408933d53", "port": "top" }
}
</code>Edge events such as
edge:connectedand
edge:selectedare used to set labels and highlight styles.
<code>// Set label on branch node connection
flowChart.on('edge:connected', args => {
const edge = args.edge;
const sourceNode = edge.getSourceNode();
if (sourceNode && sourceNode.shape === 'imove-branch') {
const portId = edge.getSourcePortId();
if (portId === 'right' || portId === 'bottom') {
edge.setLabelAt(0, sourceNode.getPortProp(portId, 'attrs/text/text'));
sourceNode.setPortProp(portId, 'attrs/text/text', '');
}
}
});
// Highlight selected edge
flowChart.on('edge:selected', args => {
args.edge.attr('line/stroke', '#feb663');
args.edge.attr('line/strokeWidth', '3px');
});
</code>Compiling flowchart to code
The flowchart is serialized to a JSON schema (DSL). Nodes and edges are parsed to determine execution order. Branch nodes have two outgoing edges with conditions (true/false). Example snippet for finding the next node:
<code>const getNextNode = (curNode, dsl) => {
const nodes = dsl.cells.filter(cell => cell.shape !== 'edge');
const edges = dsl.cells.filter(cell => cell.shape === 'edge');
const foundEdge = edges.find(edge => edge.source.cell === curNode.id);
if (foundEdge) {
return nodes.find(node => node.id === foundEdge.target.cell);
}
};
</code>Form‑render based visual building
In e‑commerce, marketing forms are common. imove uses form‑render’s
fr-generatorto visually design form schemas. The generator maps JSON schema types to UI widgets (checkbox, input, radio, select, etc.).
How schema becomes a form
The entry component
AntdForm/FusionFormreceives
widgets(component library) and a
mappingfrom schema
typeto widget name.
RenderFieldcombines a pure field component with schema attributes to produce the final UI.
Adding form items
Clicking a left‑sidebar component calls
addItem, which inserts a new schema node into the JSON tree, causing the form to re‑render.
<code>export const addItem = ({ selected, name, schema, flatten }) => {
let _name = name + '_' + nanoid(6);
const idArr = selected.split('/');
idArr.pop();
idArr.push(_name);
const newId = idArr.join('/');
const newFlatten = { ...flatten };
try {
const item = newFlatten[selected];
const siblings = newFlatten[item.parent].children;
const idx = siblings.findIndex(x => x === selected);
siblings.splice(idx + 1, 0, newId);
const newItem = {
parent: item.parent,
schema: { ...schema, $id: newId },
data: undefined,
children: [],
};
newFlatten[newId] = newItem;
} catch (error) {
console.error(error);
}
return { newId, newFlatten };
};
</code>Conclusion
The article explains how imove leverages X6 and form‑render to provide a visual workflow and form building solution, emphasizing ROI‑driven use of open‑source libraries, standardization of writing, and the benefits of data‑driven development for frontend teams.
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.