頭條資訊 - 為您提供最新最全的新聞資訊,每日實時更新

iMove 基於 X6+form-render 背後的思考

科技數碼 InfoQ

作者 | 狼叔

策劃 | 葉蘭

今天是請到阿裡的狼叔給大家分享最近他們團隊內部開源項目 iMove 基於數據可視化的一些思考。本文詳細闡述了 iMove 的可視化編排是如何實現的,以及 iMove 基於 X6+form-render 的思考,整體內容詳細且豐富,建議先收藏再看。在今年的 5 月 28-30 日舉辦的 QCon 全球軟件開發大會(北京站)上我們也設置了“低代碼探索與實踐“專題,目前議題徵集中,歡迎大家來 QCon 分享。

最近,我們的項目 imove 在 github 上的 star 數增長較快,想必是有值得大家肯定的地方的。設計這款工具的初衷是為了提高開發者的開發效率,通過面向業務邏輯的可視化編排提高邏輯元件(如 ui、 api、 function)的複用能力。我們在雙 11 業務中投入使用 imove 進行開發,不僅提高了開發的速度,還積累了許多的邏輯元件,如下圖所示:

iMove 基於 X6+form-render 背後的思考

大家可能會很好奇,我們按照一直習慣的前端開發模式寫代碼不好嗎?根據產品給的需求文檔,根據 UI 劃分一個個的組件,再一起把 UI 和邏輯實現了不是大功告成嗎?為什麼要額外引入流程圖的繪製,會不會增加工作量?

本文會講 2 個要點

imove 是如何進行開發的,我們為什麼要打破以往的開發模式。

相比於繪製流程圖, imove 更吸引人的是它可以將流程圖編譯成業務項目中可實際運行的代碼。

1imove 的可視化編排是如何實現的?

imove 的核心就是基於 x6 協議實現的。

有節點:利用 x6 的可視化界面,便於複用和編排。

有指向邊:即流程可視化,簡單直觀,邊上還可以帶參數。

有 function 和 schema2form,支持函數定義,這是面向開發者的。支持 form,讓每個函數都可以配置入參,這部分是基於阿裡開源的 form-render 實現的。

整個項目難度不大,基於 x6 和 form-render 進一步整合,將寫法規範化,將編排工具化,這樣剋制的設計使得 imove 具備小而美的特點,便於開發使用。

基於 imove 的開發方式

繪製完流程圖

根據你的業務邏輯繪製好流程圖,這裡的節點包括開始節點(圓形)、分支節點(菱形)和行為節點(矩形)。如下圖所示,可以繪製多條執行鏈,每條執行鏈都從一個開始節點出發。

iMove 基於 X6+form-render 背後的思考

完成每個節點的函數編寫

依次雙擊節點,打開代碼編輯框,為每個節點編寫代碼。

iMove 基於 X6+form-render 背後的思考

前端 js 代碼實現包含了同步邏輯和異步邏輯,大多數可以使用同步邏輯實現,但是涉及到接口請求、定時任務等場景時就需要使用到異步邏輯。因此,在 imove 中我們支持書寫同步代碼和異步代碼,並在編譯中考慮了這兩種情況。具體寫法如下所示。

// 同步代碼export default function(ctx) { const data = ctx.getPipe(); return doSomething(data);}

// 異步代碼// 寫法1 使用promiseexport default function(ctx) { return new Promise(resolve => { setTimeout(() => resolve(), 2000); });}// 寫法2 使用async awaitexport default async function(ctx) { const data = await fetchData(); return data;}

在項目中使用

以上步驟只涉及到繪製流程圖、編寫節點代碼,但是我們需要把這些 js 邏輯代碼加入到自己的項目中,才能實現一個完整的項目。為了方便把這些編寫的 js 邏輯加入到項目中,我們可以選擇以下兩種方式引入:1)將 imove 編寫的代碼在線打包,將打包文件引入到項目中;2)直接在本地啟動開發模式 imove -d,可以結合項目進行實時調試,邊在 imove 修改邊看項目效果。以下介紹兩種引入方式的具體步驟:

(1)本地打包出碼

點擊頁面右上方的"導出"按鈕後,可以在彈窗內選擇“導出代碼”,此時流程圖編譯後的代碼將以 zip 包的形式下載到本地,你可以解壓後再引入項目中使用。

iMove 基於 X6+form-render 背後的思考

引入後使用方法如下:

通過 logic.on 方法監聽事件,事件名和參數與流程圖中節點代碼的 ctx.emit 相對應

通過 logic.invoke 方法調用邏輯,事件名與流程圖中的開始節點的 邏輯觸發名稱 相對應,否則會調用失敗

import React, {useEffect} from 'react';import logic from './logic';

const App = () => { // 引入方法 useEffect(() => { // 事件監聽——在節點代碼中,通過`ctx.emit('a')`執行a事件,以下是監聽此事件的函數 logic.on('a', (data) => { }); // 執行一條流程——觸發執行“開始節點”邏輯觸發名稱為b的那條流程 logic.invoke('b'); }, []); return xxx};

export default App;

2)本地啟動開發模式

1.安裝 @imove/cli

$ npm install -g @imove/cli

2.進入項目根目錄, imove 初始化

$ cd yourProject$ imove --init # 或 imove -i

3.本地啟動開發模式

$ imove --dev # 或 imove -d

本地啟動成功之後,可以看到原來的頁面右上角會顯示連接成功。

iMove 基於 X6+form-render 背後的思考

此時頁面上觸發“保存快捷鍵 Ctrl + S”時,就可以看到當前項目的 src 目錄下會多出一個 logic 目錄,這就是 imove 編譯生成的代碼,此時你只要在你的組件中調用它即可。調用的方法仍然如本地打包出碼中演示的一致。

為什麼需要使用流程編排

瞭解了基於 imove 的開發方式,接下來具體討論一下為什麼需要這麼做。試想一下,你有沒有遇到過以下場景:

UI 經常發生變化,但是我想複用以往實現過的邏輯,卻不知道代碼在哪裡,又要重新寫一遍

產品的需求文檔都是文字,我們只好在腦中構思邏輯,邊寫邊想還容易遺漏邏輯,只好寫完了代碼反覆檢查

以前做的項目很久沒做了,但是最近我想改,但是項目對我來說如此陌生,不知道代碼是什麼意思,無法快速入手

我要接手一個老項目,但是裡面的代碼邏輯很複雜,又沒有什麼註釋,不知道如何是好

我是個新人,在做新業務時可能有很多不太清楚的實現邏輯,如關注店鋪、判斷登錄、發送埋點……,沒有什麼文檔參考,師兄又太忙,問起來很花時間

實現某個邏輯 a 時,想參考下以前別人實現的代碼,但是大串大串的業務邏輯耦合在一起,不知道哪一部分才是邏輯 a,於是開始自己鑽研……

以上這些種種問題,其實是前端開發中或多或少會遇到的痛點。其實,我們可以採用邏輯編排的開發方式,把業務中的一個個功能點按照邏輯順序組織起來,不僅能夠及時檢查邏輯漏洞,還能沉澱非常多的業務邏輯,達到參考和複用的效果。以下是總結的採用可視化編排方式進行編程的好處:

iMove 基於 X6+form-render 背後的思考

(1)需求可視化

把需求按照流程圖的方式展示出來,可視化的流程對於新人以及非開發同學來說是非常容易理解的,對了解業務有很大的幫助。特別是在判別條件非常多、業務身份非常多、業務流程冗長而複雜的場景中,能梳理好全部的需求邏輯,能更好地檢查代碼是否考慮了全部的需求場景。這使得交付、交流、接手會更容易。

(2)邏輯複用

因為代碼是針對於節點粒度的,在做需求時,可以參考和複用已有的節點代碼(如判斷登錄、關注店鋪等等),不僅對新人上手非常友好,也節約了反覆編寫重複代碼的時間,能縮短開發週期。隨著邏輯節點的不斷沉澱,這種優勢也會越發明顯。

(3)提高代碼規範

在現有的開發方式下,隨著需求不斷迭代,可能有會有越來越多的業務邏輯被分散在各個模塊中,很可能會違反職責單一、高內聚低耦合的編程原則。而通過邏輯編排的方式開發,每次只針對一個節點編寫代碼,能保證各個能力職責單一,最後通過聚合的方式串聯起整個業務邏輯。

現在思考一下,使用到邏輯編排後,上述列舉出的種種問題,是不是會迎刃而解呢?

其實邏輯編排不是一種新出現的思想,其實像我們生活中,都有邏輯編排的影子。它只是按照行為邏輯,把一個個單一的業務行為(如限流、限購、加購等等)有序編織為一個完整流程,聚合成一條具有特定業務含義的執行鏈,每一個完整的流程都會有一個觸發點。如下圖,就是在業務場景中整理的流程圖:

iMove 基於 X6+form-render 背後的思考

因此,在 coding 中引入邏輯編排的概念是有價值的,我們也正在嘗試著使用新的開發模式去提高生產效率。

2

基於 x6 的流程編排

在上文中詳細展示了為什麼需要使用到流程編排,下面詳細介紹下流程圖繪製的原理。imove 底層採用了螞蟻團隊提供的 antv-X6 圖編輯引擎 ,從而實現了流程圖繪製的能力。

為什麼要選用 antv-x6 作為底層繪圖引擎呢?因為它基本能覆蓋到流程圖繪製的全部需求,內置了圖編輯場景的常規交互和設計,能幫助我們快速創建畫布、節點和邊,能夠做到開箱即用,使用是非常方便的。它開放了豐富的定製能力給到開發者,只需簡單的配置就能實現想要的效果,其不僅僅支持流程圖,還支持 DAG 圖、 ER 圖、組織架構圖等等。在 imove 中我們僅僅使用到了流程圖繪製的能力,其實 x6 還有更多的能力值得大家去使用去探索。下面結合 imove 框架的實現,詳細介紹下我們實現流程圖繪製的具體思路,如何從 0 到 1 實現可用的較為完備的流程圖繪製能力。

畫布設計

使用 x6 創建畫布非常容易,實例化一個 x6 暴露的 Graph 對象即可。以下是畫布的具體配置項,包括了一下特性。

節點是否可旋轉

節點是否可調整大小

跨畫布的複製 / 剪切 / 粘貼

節點連線規則配置

畫布背景配置(支持顏色 / 圖片 / 水印等)

網格配置

點選 / 框選配置

對齊線配置

鍵盤快捷鍵配置

撤銷 / 重做能力

畫布滾動、平移、居中、縮放等能力

鼠標滾輪縮放配置

……

具體的代碼實現如下,註釋了各類能力對應的 API 文檔,大家有興趣可以試試~

import {Graph} from '@antv/X6';const flowChart = new Graph({ // 渲染指定 dom 節點 container: document.getElementById('flowChart'), // 節點是否可旋轉 rotating: false, // 節點是否可調整大小 resizing: true, // 剪切板,支持跨畫布的複製/粘貼(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/clipboard) clipboard: { enabled: true, useLocalStorage: true, }, // 節點連線規則配置(詳細文檔:https://X6.antv.vision/zh/docs/api/graph/interaction#connecting) connecting: { snap: true, dangling: true, highlight: true, anchor: 'center', connectionPoint: 'anchor', router: { name: 'manhattan' } }, // 畫布背景,支持顏色/圖片/水印等(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/background) background: { color: '#f8f9fa', }, // 網格配置(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/grid) grid: { visible: true, }, // 點選/框選配置(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/selection) selecting: { enabled: true, multiple: true, rubberband: true, movable: true, strict: true, showNodeSelectionBox: true }, // 對齊線配置,輔助移動節點排版(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/snapline) snapline: { enabled: true, clean: 100, }, // 撤銷/重做能力(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/history) history: { enabled: true, }, // 使畫布具備滾動、平移、居中、縮放等能力(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/scroller) scroller: { enabled: true, }, // 鼠標滾輪縮放(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/mousewheel) mousewheel: { enabled: true, minScale: MIN_ZOOM, maxScale: MAX_ZOOM, modifiers: ['ctrl', 'meta'], },});

完成了畫布的開發,接下來在主界面中引入即可。這裡我們發現還是存在很多問題:

無法使用鍵盤快捷鍵怎麼辦,比如複製粘貼保存撤銷?

想監聽一些畫布方法怎麼辦,例如雙擊節點直接打開編輯框、右鍵節點打開菜單、右鍵畫布打開另一個菜單?

想實現小地圖怎麼辦?

想把畫布信息導出並儲存怎麼辦?

……

僅僅依賴於以上 new Graph 配置的信息是遠遠不夠的,還需要一步步完善起來。

快捷鍵設置

根據 鍵盤快捷鍵 Keyboard 介紹,Graph 實例會暴露一個 bindKey 方法,我們可以用它來綁定快捷鍵。以使用頻率最高的 ctrl + c / ctrl + v 舉例,這裡定義了一系列的鍵盤快捷鍵和 handler 監聽函數,如 ctrl + c 時獲取當前選中的元素,利用 copy() 方法完成複製操作,ctrl + v 時直接使用 paste({offset:xx}) 方法完成粘貼操作。

首選需要在 new Graph 的參數中加入 keyboard 配置項,這裡的 global 代表是否為全局鍵盤事件,設置為 true 時綁定在 Document 上,否則綁定在畫布容器上。在這裡我們設置為 false,只在畫布獲得焦點才觸發鍵盤事件。

const flowChart = new Graph({ // 鍵盤快捷鍵能力(詳細文檔:https://X6.antv.vision/zh/docs/tutorial/basic/keyboard) keyboard: { enabled: true, global: false, }});

接下來要配置支持的快捷鍵,如複製、粘貼、保存、撤銷、縮放、全選……需要指定對應的按鍵以及監聽函數。感興趣的朋友可以看下 shortcuts.ts。

import { Cell, Edge, Graph, Node } from '@antv/X6';interface Shortcut { keys: string | string[]; handler: (flowChart: Graph) => void;}const shortcuts: { [key: string]: Shortcut } = { // 複製 copy: { keys: 'meta + c', handler(flowChart: Graph) { const cells = flowChart.getSelectedCells(); if (cells.length > 0) { flowChart.copy(cells); message.success('複製成功'); } return false; }, }, // 粘貼 paste: { keys: 'meta + v', handler(flowChart: Graph) { if (!flowChart.isClipboardEmpty()) { const cells = flowChart.paste({ offset: 32 }); flowChart.cleanSelection(); flowChart.select(cells); } return false; }, }, // many funcions can be defined here save: {}, // 保存 undo: {}, // 撤銷 redo: {}, // 重做 zoomIn: {}, // 放大 zoomOut: {}, // 縮小 delete: {}, // 刪除 selectAll: {}, // 全選 bold: {}, // 加粗 italic: {}, // 斜體 underline: {}, // 下劃線 bringToTop: {}, // 置於頂層 bringToBack: {} // 置於底層};export default shortcuts;

我們也可以實現快捷鍵或滾輪放大、縮小的能力,需要自定義每次縮放尺度的改變量,根據當前的縮放尺度去增減這個改變量即可。為了避免用戶無限放大或無限縮小帶來不好的視覺體驗,最好定義一個最大和最小的縮放尺度。

const shortcuts: { [key: string]: Shortcut } = { zoomIn: { keys: 'meta + shift + +', handler(flowChart: Graph) { const nextZoom = (flowChart.zoom() + ZOOM_STEP).toPrecision(2); flowChart.zoomTo(Number(nextZoom), { maxScale: MAX_ZOOM }); return false; }, }, zoomOut: { keys: 'meta + shift + -', handler(flowChart: Graph) { const nextZoom = (flowChart.zoom() - ZOOM_STEP).toPrecision(2); flowChart.zoomTo(Number(nextZoom), { minScale: MIN_ZOOM }); return false; }, }}

最後我們把所有的快捷鍵綁定在 Graph 上,即可完成全部快捷點的配置。

shortcuts.forEach(shortcur => { const { key, handler } = shortcut; graph.bindKey(key, () => handler(graph));});

畫布方法註冊

在當前畫布上,我們需要監聽一系列常用的方法,如雙擊節點、畫布右鍵、節點右鍵等等。在 imove 中,完成了以下的操作:

雙擊節點打開代碼編輯框,想提高交互體驗

右鍵節點打開菜單欄,支持節點複製、刪除、編輯文本、置於頂層 / 底層、編輯代碼、執行代碼

右鍵畫布打開菜單欄,支持全選和粘貼

iMove 基於 X6+form-render 背後的思考

以下是具體的實現方法:

const registerEvents = (flowChart: Graph): void => { // 監聽節點雙擊事件,用於打開代碼編輯界面 flowChart.on('node:dblclick', () => { }); // 監聽畫布右鍵菜單 flowChart.on('blank:contextmenu', (args) => { }); // 監聽節點右鍵菜單 flowChart.on('node:contextmenu', (args) => { });};

創建畫布實例

目前已經完成了畫布通用配置、快捷鍵設置、事件綁定,接下來實現一個 createFlowChart 的工廠函數。在工廠函數中,我們創建了畫布實例,併為其註冊綁定的事件、註冊快捷鍵、註冊服務端存儲。

// 註冊快捷鍵const registerShortcuts = (flowChart: Graph): void => { Object.values(shortcuts).forEach((shortcut) => { const { keys, handler } = shortcut; flowChart.bindKey(keys, () => handler(flowChart)); });};const createFlowChart = (container: HTMLDivElement, miniMapContainer: HTMLDivElement): Graph => { const flowChart = new Graph({ // many configuration }) registerEvents(flowChart); // 註冊綁定事件 registerShortcuts(flowChart); // 註冊快捷鍵 registerServerStorage(flowChart); // 註冊服務端存儲 return flowChart;};

export default createFlowChart;

這樣,畫布的功能越來越完善了,已經支持繪製流程圖了。

導出模型

在 imove 中,需要支持繪製的流程圖以 DSL 、代碼、流程圖的導出,這樣在真實業務開發中,就可以最大程度的複用節點和流程。為了實現這個功能,我們實現的相關函數如下。

// 導出DSLconst onExportDSL = () => { const dsl = JSON.stringify(flowChart.toJSON(), null, 2); const blob = new Blob([dsl], { type: 'text/plain' }); DataUri.downloadBlob(blob, 'imove.dsl.json');};

// 導出代碼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'); });};

// 導出流程圖const onExportFlowChart = () => { flowChart.toPNG((dataUri: string) => { DataUri.downloadDataUri(dataUri, 'flowChart.png'); }, { padding: 50, ratio: '3.0' });};

節點設計

在 imove 中,我們設計了以下三種節點類型:事件節點(開始節點)、行為節點、分支節點。節點負責處理具體的邏輯流程,以下是三種節點的具體描述:

開始節點:邏輯起始,是所有流程的開始,可以是一次生命週期初始化 / 一次點擊

行為節點:邏輯執行,可以是一次網絡請求 / 一次改變狀態 / 發送埋點等

分支節點:邏輯路由,根據不同的邏輯執行結果跳轉到不同的節點(注:一條邏輯流程必須以開始節點為起始)

iMove 基於 X6+form-render 背後的思考

根據上述的規範描述,我們可以繪製出各種各樣的邏輯流程圖,例如 進入首頁 的流程圖如下所示:

iMove 基於 X6+form-render 背後的思考

節點屬性結構

imove 的流程圖節點需要承載了多種屬性,如節點文字、代碼、投放配置模型、投放配置數據、依賴包等等,但是在 x6 中,節點的基本屬性為 id、 shape、 position、 size,主要包括與 x6 圖形展示相關的基本數據,簡稱為圖形化數據。因此我們需要擴展節點屬性,這些擴展的屬性簡稱為自定義數據。

1.圖形化數據(主要包括與 X6 圖形展示相關的基本數據)

id: 32-bit 唯一標識符

shape: 形狀

position: {x, y}: 橫向 / 縱向位移

size: {width, height}: 寬高大小

...

2.自定義數據(擴展的屬性)

type: 節點類型

label: 節點展示文案

code: 節點存儲的代碼

dependencies: js 代碼的依賴包

trigger: 觸發邏輯開始的事件名(開始節點才有)

ports: 節點出口配置(跳轉邏輯,分支節點才有)

configSchema: 投放配置模型

configData: 投放配置數據

version: 版本號

forkId: 複製來源

referId: 引用來源

定義完這些節點屬性後,就可以實現節點信息的存儲,不至於丟失信息。通常來說,信息配置使用頻率最高的有:

顯示名稱:更改節點名稱

邏輯觸發名稱:開始節點類型專屬配置,項目代碼使用時根據這個值觸發邏輯調用

投放配置 schema:修改投放配置的表單結構

這裡編輯的每一條信息,都會保存在節點的屬性裡。

iMove 基於 X6+form-render 背後的思考

節點拖拽

單純有畫布是不夠的,我們還需支持在畫布上添加節點:

iMove 基於 X6+form-render 背後的思考

可以使用 addNode 和 addEdge 方法來動態添加節點和邊:

// 添加起點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 });

然而這種方式並不能達到通過拖拽來生成流程圖的要求,不過也不用擔心, x6 已經考慮到了這點,封裝了 Dnd 類(drag and drop)來解決這個問題。代碼如下:

import { Addon, Graph } from '@antv/X6';const { Dnd } = Addon;// 創建主畫布const graph = new Graph({ id: document.getElementById('flowchart'), grid: true, snapline: { enabled: true }});// 創建 Dnd 實例const dnd = new Dnd({ target: graph, scaled: false, animation: true});// 創建側邊欄const sideBar = new Graph({ id: document.getElementById('sideBar'), interacting: false});// 側邊欄添加內置節點1sideBar.addNode({ id: 'node1', x: 80, y: 80, width: 80, height: 40, label: 'Hello',});// 側邊欄添加內置節點2sideBar.addNode({ id: 'node2', x: 80, y: 140, width: 80, height: 40, label: 'iMove',});// 監聽 mousedown 事件,調用 dnd.start 處理拖拽sideBar.on("cell:mousedown", (args) => { const { node, e } = args; dnd.start(node.clone(), e);});

節點樣式設置

為了不限制繪製流程圖的體驗, iMove 工具欄提供了繪製流程圖常用到的一些功能(例如修改字號、加粗、斜體、文字顏色、背景顏色、對齊等等),這主要也是得益於 X6 提供了統一修改節點樣式的方法。工具欄如下所示:

iMove 基於 X6+form-render 背後的思考

使用 setAttrs 方法可以配置指定的樣式:

// 設置字號cell.setAttrs({ label: { fontSize: 14 } };// 設置字重cell.setAttrs({ label: { fontWeight: 'bold' } });// 設置斜體cell.setAttrs({ label: { fontStyle: 'italic' } });// 設置文字顏色cell.setAttrs({ label: { fill: 'red' } });// 設置背景顏色cell.setAttrs({ body: { fill: 'green' } });// …………

節點代碼編寫

每個節點的代碼等價於一個 js 模塊,因此你不用擔心全局變量的命名汙染問題,甚至可以 import 現有的 npm 包,但最後必須 export 出一個函數。需要注意的是,由於 iMove 天生支持節點代碼的異步調用,因此 export 出的函數默認是一個 promise。

iMove 基於 X6+form-render 背後的思考

就以 是否登錄 這個分支節點為例,我們來看下節點代碼應該如何編寫:

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; });}

由於該節點是分支節點,因此其 boolean 返回值決定了整個流程的走向。如果是非分支節點,直接流向下一個連接的節點即可。

節點間數據通信

完成節點代碼編寫之後,我們再來看下節點之間是如何進行數據通信的。

在 iMove 中,數據是以流(pipe)的形式從前往後進行流動的,也就是說前一個節點的返回值會是下一個節點的輸入。不過也有一個例外,由於 分支節點 的返回值會是 boolean 類型,因此它的下遊節點拿到的輸入必將是一個 boolean 類型值,從而造成數據流的中斷。為此,我們進行了一定的改造,分支節點的作用只負責數據流的轉發,就像一個開關一樣,只決定數據流的走向,但不改變流向下遊的數據——流入分支節點後一個節點的數據依然是分支節點前一個節點輸出的數據。

因此,以下例子中“請求 profile 接口”和“返回數據“兩個節點會成為數據流的上下遊關係。

iMove 基於 X6+form-render 背後的思考

我們再來看下他們之間是如何進行數據通信的:

節點: 請求 profile 接口

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/isLogin failed, the err is:', err); return {success: false}; });}

節點: 接口成功

export default async function(ctx) { // 獲取上遊數據 const pipe = ctx.getPipe() || {}; return pipe.success;}

節點: 返回數據

const processData = (data) => { // TODO: 數據加工處理 return data;};export default async function(ctx) { // 這裡獲取到的上遊數據,不是"接口成功"這個分支節點的返回值,而是"請求profile接口"這個節點的返回值 const pipe = ctx.getPipe() || {}; // 觸發updateUI這個方法更新界面,傳入的值為profileData ctx.emit('updateUI', {profileData: processData(pipe.data)});}

如上代碼所述,每個下遊節點可以調用 ctx.getPipe 方法獲取上遊節點返回的數據流。另外,需要注意的是 返回數據 節點的最後一行代碼 ctx.emit('updateUI', data) 需要和項目中的代碼配合使用,項目中想監聽這個事件,需要執行 logic.on('updateUI',data=>{ //handler })。

邊設計

在 imove 中,邊的作用被弱化,僅表示圖形上的節點連接關係,主要控制流程的走向,使用過程中僅用於連線。因此我們沒有額外設計邊屬性,只採用了 X6 默認的邊的屬性:id(唯一標識符)、shape(形狀)、source(起點)、target(終點)。如下所示:

{ "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" }}

除了邊屬性結構描述外,我們還需要關注邊連線實現、樣式定製化和代碼編譯,下面會分別講一下。

邊連線實現

邊連線是通過在畫布上綁定 edge:connected 事件實現的:

flowChart.on('edge:connected', (args) => { const edge = args.edge as Edge; const sourceNode = edge.getSourceNode() as Node;

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', ''); } }});

這些 x6 都已經提供好了,開發和定製都是非常簡單。

邊選中樣式定製化

默認的邊是沒有選中高亮的樣式的,這裡我們可以直接在畫布上綁定邊選中的事件,改變邊的樣式即可:

flowChart.on('edge:selected', (args) => { args.edge.attr('line/stroke', '#feb663'); args.edge.attr('line/strokeWidth', '3px');});

如果大家想按照自己的喜好來定製流程圖上的邊展示,可以非常簡單的實現。

基於流程圖的代碼編譯

流程圖畫好了,我們如何實現完整流程的代碼運行呢?其實,流程圖對應是的一個 JSON Schema,這樣我們就可以根據節點的連接順序進行編譯了:

iMove 基於 X6+form-render 背後的思考

流程圖編譯的 Schema 如下所示:

分支節點{ "shape": "imove-branch", "data": { "ports": { "right": { "condition": "true" }, "bottom": { "condition": "false" } }, "code": "export default async function(ctx) {\n return true;\n}" }, "id": "a6da6684-96c8-4595-bec6-a94122b61e30"}連線{ "shape": "edge", "id": "cbcbd0ea-4a2a-4d2a-8135-7b4b7d7ec50d", "source": { "cell": "de868d18-9ec0-4dac-abbe-5cb9e3c20e2f", "port": "right" }, "target": { "cell": "a6da6684-96c8-4595-bec6-a94122b61e30", "port": "left" }}

通過流程圖 Schema,可以解析到節點屬性和節點關係,如開始節點、行為節點如何流向下一個節點,分支節點如何根據運行結果分別流向兩條邊。具體的編譯過程如下所示:

iMove 基於 X6+form-render 背後的思考

首先根據 DSL 可以獲取全部的邊(Edge),邊的屬性上存儲了 source 節點和 target 節點的 id 和方向,即起始節點和終點節點的 id 和方向,再根據節點的 id 可以找到全部的節點,於是可以串聯起全部的節點和邊。這裡有幾點要注意的是:

節點(Node)和邊(Edge) 的 id 永遠都是唯一的, 可以根據 id 找到對應的節點 / 邊。

方向永遠都是從邊(Edge)的 source 節點流向 target 節點的,因此節點之間的流動關係也是固定的。

判斷節點有兩個輸出方向,需要根據節點輸出的 boolean 值去判斷走向。其中判斷節點記錄了兩條邊對應的方向和 boolean 值,在編譯時需要考慮。

按照以上分析的思路,找到當前節點的下一個節點代碼如下:

// 找到下一個節點const getNextNode = (curNode: Cell.Properties, dsl: 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); }};

3基於 form-render 的可視化搭建

在電商領域,業務場景會時常涉及到營銷表單的配置。因此代碼中會暴露出一些字段供運營配置,這些字段需要以合適的表單形式呈現,因此涉及到表單 Schema 結構的定義。iMove 在定義表單 Schema 結構時,是可以使用可視化的方式設計表單結構的,這裡得益於 form-render 這個開源項目的優秀設計,我們可以使用其提供的 fr-generator 庫,通過可視化拖拽修改的形式快速生成投放表單結構,方便後續進行數據投放。具體效果如下:

iMove 基於 X6+form-render 背後的思考

如何做到 Schema to Form

以上提到的 fr-generator 表單設計器是如何快速地進行表單搭建的呢?這不得不先說一下如何根據規範化 Schema 轉化為 Form。知其然知其所以然,接下來我們看看 form-render 是如何實現這一步的:

iMove 基於 X6+form-render 背後的思考

入口是 AntdForm/FusionForm 組件(分別兼容 antd 組件庫和 fusion 組件庫)。其中傳入 widgets 參數包含了暴露的所有組件,如 checkbox、input、radio、select 等,mapping 參數包含了 schema 的 type 字段與 widgetName(組件名)的映射關係。

AntdForm/FusionForm 組件是由 RenderField 組件實現的,在這裡組合了全部的 Widget 生成了 Field 組件(即純展示的表單組件)。

RenderField 是由 Field 組件實現的,在這裡將純展示表單組件和 schema 的屬性結合在一起,轉化為真正帶有屬性的表單組件。轉化函數詳見 parser.js,這裡的轉化函數是基於 form-render 設計的 json 規範實現的,使用'ui:className'、'ui:hidden'、'ui:width'等一致的屬性命名規範去承載樣式數據。

接下來討論下 fr-generator 表單設計器的核心操作是什麼。點擊左側表單組件,就可以將其加入到表格中:

iMove 基於 X6+form-render 背後的思考

以下的 Element 組件即代表左側每一個表單選項,點擊左側表單組件,會調用 handleElementClick 方法,實際上是調用了 addItem 方法:

const Element = ({ text, name, schema, icon }) => { // ...... const { selected, flatten, onFlattenChange } = useStore(); const handleElementClick = () => { const { newId, newFlatten } = addItem({ selected, name, schema, flatten }); onFlattenChange(newFlatten); setGlobal({ selected: newId }); }; return ( );};

addItem 對應的代碼實現如下,其實是改變了原有的 JSON Schema 數據, JSON Schema 改變了,中間渲染的表單就會跟隨發生變化。核心步驟如下:

獲取畫布中選中的節點(表單項)

獲取此節點的父節點 children 屬性

找到此節點在 children 數組中的位置

在選中節點後插入新的表單項(其實是在操作父節點的 children 數組)

JSON Schema 結構改變

畫布更新

// 點擊左側菜單添加表單結構項export const addItem = ({ selected, name, schema, flatten }) => { // ...... let _name = name + '_' + nanoid(6); const idArr = selected.split('/'); idArr.pop(); idArr.push(_name); newId = idArr.join('/'); const newFlatten = { ...flatten }; try { // 拿到選中的節點 const item = newFlatten[selected]; // 拿到選中節點的父節點children屬性 const siblings = newFlatten[item.parent].children; // 找到選中節點在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 };};

數據驅動帶來的便利性

其實,類似於 form render、 fomily 這樣的庫,可以通過簡單的 JSON Schema 生成表單,都是基於數據驅動的思想實現的。維護自身的一套 schema 規範,按照規範解析 schema 文件可以直接完成表單的渲染。數據驅動為何讓開發者如此熱衷?react、 vue 等流行框架的底層也是數據驅動的思維,解放了程序員繁瑣重複的工作。

就拿 vue 框架來說,以下數據驅動帶來的便利性非常受人歡迎:

模板渲染:根據模板生成 AST,最後根據 AST 樹填充數據生成真實 DOM

數據綁定:可以監聽 交互輸入 /http 請求響應 / 定時器觸發 等行為,當數據發生變化時,做 diff 操作,完成 DOM 的更新

路由引擎:根據 host/path/params 等數據,解析對應頁面

在數據驅動的場景下,我們只需要完成兩步:

將產品、業務、設計進行抽象化,將 UI、交互抽象為數據

將數據用邏輯處理連接起來,通過數據去直接影響結果

數據驅動的思想能夠給前端帶來很多的便利。以前開發時,我們處理頁面元素就會處理 DOM,處理事件邏輯就會處理 JavaScript,處理樣式就會處理 CSS。切換為數據驅動的思想之後,我們可以把頁面元素、事件邏輯、樣式都視為數據,設計好數據與狀態之間的轉換關係,通過改變數據直接去改變狀態。以上介紹的 x6 和 form-render 都是可以通過已有的規範 JSON 數據,直接生成對應的流程圖和表單,其實數據和 UI 的轉換關係已經隱藏在了框架的內部實現中。

4總結

這篇文章主要討論了 imove 基於 X6 和 form-render 背後的思考以及相關的實現原理,主要是在已有開源庫的基礎上不斷去完善,直到滿足項目的需求,這樣才能做到 ROI 最大化,相互成就。iMove 做的比較好的是定位,繼而將寫法規範化,將編排工具化,這樣剋制的設計使得 iMove 具備小而美的特點,便於開發使用。

我們在初次使用某個開源框架或庫時可以根據官方文檔提供的示例實現 Demo 效果,由於每個人業務需求都不盡相同,可能需要進行不同的配置甚至進行二次開發才能實現。這時候,可以恰當地使用現有的 API 能力去儘量實現需求,如果沒有提供相應的能力,也可以看看有沒有提供自定義插件、自定義函數或組件能滿足需求。如果還是沒有合適的方案,或許可以嘗試一下自己實現?如果框架體積非常大但是你只需要使用到其中很小一部分,這時候可以考慮下看看對應的源碼,學習下原理嘗試自己實現,其實 x6 和 form-render 這種開源基礎庫在很多場景下都是非常實用的。未來 imove 還會持續迭代,喜歡的朋友們歡迎來踩踩哦~

活動推薦

在今年的 5 月 28-30 日舉辦的 QCon 全球軟件開發大會(北京站)我們也設置了“低代碼探索與實踐“專題,目前議題徵集中,歡迎大家來 QCon 分享。此外,QCon 還設置有前端工程化、業務架構、大數據實時計算等技術專場,感興趣的同學點擊【閱讀原文】搶先了解吧。

iMove 基於 X6+form-render 背後的思考

點個在看少個 bug

轉載請超鏈接註明:頭條資訊 » iMove 基於 X6+form-render 背後的思考
免責聲明
    :非本網註明原創的信息,皆為程序自動獲取互聯網,目的在於傳遞更多信息,並不代表本網贊同其觀點和對其真實性負責;如此頁面有侵犯到您的權益,請給站長發送郵件,並提供相關證明(版權證明、身份證正反面、侵權鏈接),站長將在收到郵件24小時內刪除。
加載中...