Frontend Development 36 min read

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.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
Boost Frontend Efficiency with imove: Visual Workflow and Code Generation Using X6

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

addNode

and

addEdge

or 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:connected

and

edge:selected

are 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-generator

to 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/FusionForm

receives

widgets

(component library) and a

mapping

from schema

type

to widget name.

RenderField

combines 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.

frontendcode generationX6form-renderiMovevisual workflow
Taobao Frontend Technology
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.