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

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天是請到阿里的狼叔給大家分享最近他們團隊內部開源項目IMove基於數據可視化的一些思考。本文詳細闡述了iMove的可視化編排是如何實現的,以及iMove基於 X6+form-render的思考,整體內容詳細且豐富,建議先收藏再看。在今年的5月28-30日舉辦的 "},{"type":"link","attrs":{"href":"https:\/\/qcon.infoq.cn\/2021\/beijing\/?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"QCon全球軟件開發大會(北京站)"}]},{"type":"text","text":"上我們也設置了“低代碼探索與實踐“專題,目前議題徵集中,歡迎大家來QCon分享。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近,我們的項目 "},{"type":"link","attrs":{"href":"https:\/\/github.com\/imgcook\/imove?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 在 "},{"type":"codeinline","content":[{"type":"text","text":"github"}]},{"type":"text","text":" 上的 "},{"type":"codeinline","content":[{"type":"text","text":"star"}]},{"type":"text","text":" 數增長較快,想必是有值得大家肯定的地方的。設計這款工具的初衷是爲了提高開發者的開發效率,通過面向業務邏輯的可視化編排提高邏輯元件(如 "},{"type":"codeinline","content":[{"type":"text","text":"ui"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"api"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"function"}]},{"type":"text","text":")的複用能力。我們在雙11業務中投入使用 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 進行開發,不僅提高了開發的速度,還積累了許多的邏輯元件,如下圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ac\/acf9bdb9dc02fdeadcf7605e3fd333b1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家可能會很好奇,我們按照一直習慣的前端開發模式寫代碼不好嗎?根據產品給的需求文檔,根據UI劃分一個個的組件,再一起把UI和邏輯實現了不是大功告成嗎?爲什麼要額外引入流程圖的繪製,會不會增加工作量?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文會講2個要點"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 是如何進行開發的,我們爲什麼要打破以往的開發模式。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相比於繪製流程圖, "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 更吸引人的是它可以將流程圖編譯成業務項目中可實際運行的代碼。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"imove的可視化編排是如何實現的?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 的核心就是基於 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 協議實現的。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有節點:利用 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 的可視化界面,便於複用和編排。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有指向邊:即流程可視化,簡單直觀,邊上還可以帶參數。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有 "},{"type":"codeinline","content":[{"type":"text","text":"function"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"schema2form"}]},{"type":"text","text":",支持函數定義,這是面向開發者的。支持 "},{"type":"codeinline","content":[{"type":"text","text":"form"}]},{"type":"text","text":",讓每個函數都可以配置入參,這部分是基於阿里開源的 "},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":" 實現的。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整個項目難度不大,基於 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":" 進一步整合,將寫法規範化,將編排工具化,這樣剋制的設計使得 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 具備小而美的特點,便於開發使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"基於imove的開發方式"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"繪製完流程圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據你的業務邏輯繪製好流程圖,這裏的節點包括開始節點(圓形)、分支節點(菱形)和行爲節點(矩形)。如下圖所示,可以繪製多條執行鏈,每條執行鏈都從一個開始節點出發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c8\/c834dfc26dc01f314b16775ccdd90f8d.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"完成每個節點的函數編寫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依次雙擊節點,打開代碼編輯框,爲每個節點編寫代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7e\/7e02c342f94e68816c3e54aa5d9ce10b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端js代碼實現包含了同步邏輯和異步邏輯,大多數可以使用同步邏輯實現,但是涉及到接口請求、定時任務等場景時就需要使用到異步邏輯。因此,在 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 中我們支持書寫同步代碼和異步代碼,並在編譯中考慮了這兩種情況。具體寫法如下所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 同步代碼\nexport default function(ctx) {\n const data = ctx.getPipe();\n return doSomething(data);\n}\n\n\/\/ 異步代碼\n\/\/ 寫法1 使用promise\nexport default function(ctx) {\n return new Promise(resolve => {\n setTimeout(() => resolve(), 2000);\n });\n}\n\/\/ 寫法2 使用async await\nexport default async function(ctx) {\n const data = await fetchData();\n return data;\n}"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"在項目中使用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上步驟只涉及到繪製流程圖、編寫節點代碼,但是我們需要把這些js邏輯代碼加入到自己的項目中,才能實現一個完整的項目。爲了方便把這些編寫的 "},{"type":"codeinline","content":[{"type":"text","text":"js"}]},{"type":"text","text":" 邏輯加入到項目中,我們可以選擇以下兩種方式引入:1)將 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 編寫的代碼在線打包,將打包文件引入到項目中;2)直接在本地啓動開發模式 "},{"type":"codeinline","content":[{"type":"text","text":"imove -d"}]},{"type":"text","text":",可以結合項目進行實時調試,邊在 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 修改邊看項目效果。以下介紹兩種引入方式的具體步驟:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"(1)本地打包出碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"點擊頁面右上方的\"導出\"按鈕後,可以在彈窗內選擇“導出代碼”,此時流程圖編譯後的代碼將以"},{"type":"codeinline","content":[{"type":"text","text":"zip"}]},{"type":"text","text":"包的形式下載到本地,你可以解壓後再引入項目中使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/17\/170862645f14c0ef220e1dc795aa54b6.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"引入後使用方法如下:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"通過 "},{"type":"codeinline","content":[{"type":"text","text":"logic.on"}]},{"type":"text","text":" 方法監聽事件,事件名和參數與流程圖中節點代碼的 "},{"type":"text","marks":[{"type":"strong"}],"text":"ctx.emit"},{"type":"text","text":" 相對應"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"通過 "},{"type":"codeinline","content":[{"type":"text","text":"logic.invoke"}]},{"type":"text","text":" 方法調用邏輯,事件名與流程圖中的開始節點的 "},{"type":"text","marks":[{"type":"strong"}],"text":"邏輯觸發名稱"},{"type":"text","text":" 相對應,否則會調用失敗"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import React, {useEffect} from 'react';\nimport logic from '.\/logic';\n\nconst App = () => {\n \/\/ 引入方法\n useEffect(() => {\n \/\/ 事件監聽——在節點代碼中,通過`ctx.emit('a')`執行a事件,以下是監聽此事件的函數\n logic.on('a', (data) => {\n \n });\n \/\/ 執行一條流程——觸發執行“開始節點”邏輯觸發名稱爲b的那條流程\n logic.invoke('b');\n }, []);\n \n return
xxx\n};\n\nexport default App;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"2)本地啓動開發模式"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"安裝 "},{"type":"codeinline","content":[{"type":"text","text":"@imove\/cli"}]},{"type":"text","text":" "}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"$ npm install -g @imove\/cli"}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"進入項目根目錄, "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 初始化"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"$ cd yourProject \n$ imove --init # 或 imove -i"}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"本地啓動開發模式"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"$ imove --dev # 或 imove -d"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本地啓動成功之後,可以看到原來的頁面右上角會顯示連接成功。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d5\/d5ab92015cfe49edd960e2bb154fe72f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時頁面上觸發“保存快捷鍵 Ctrl + S”時,就可以看到當前項目的 "},{"type":"codeinline","content":[{"type":"text","text":"src"}]},{"type":"text","text":" 目錄下會多出一個 "},{"type":"codeinline","content":[{"type":"text","text":"logic"}]},{"type":"text","text":" 目錄,這就是 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 編譯生成的代碼,此時你只要在你的組件中調用它即可。調用的方法仍然如"},{"type":"text","marks":[{"type":"strong"}],"text":"本地打包出碼"},{"type":"text","text":"中演示的一致。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"爲什麼需要使用流程編排"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瞭解了基於 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 的開發方式,接下來具體討論一下爲什麼需要這麼做。試想一下,你有沒有遇到過以下場景:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"UI"}]},{"type":"text","text":" 經常發生變化,但是我想複用以往實現過的邏輯,卻不知道代碼在哪裏,又要重新寫一遍"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"產品的需求文檔都是文字,我們只好在腦中構思邏輯,邊寫邊想還容易遺漏邏輯,只好寫完了代碼反覆檢查"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"以前做的項目很久沒做了,但是最近我想改,但是項目對我來說如此陌生,不知道代碼是什麼意思,無法快速入手"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"我要接手一個老項目,但是裏面的代碼邏輯很複雜,又沒有什麼註釋,不知道如何是好"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"我是個新人,在做新業務時可能有很多不太清楚的實現邏輯,如關注店鋪、判斷登錄、發送埋點……,沒有什麼文檔參考,師兄又太忙,問起來很花時間"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"實現某個邏輯a時,想參考下以前別人實現的代碼,但是大串大串的業務邏輯耦合在一起,不知道哪一部分纔是邏輯a,於是開始自己鑽研……"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上這些種種問題,其實是前端開發中或多或少會遇到的痛點。其實,我們可以採用邏輯編排的開發方式,把業務中的一個個功能點按照邏輯順序組織起來,不僅能夠及時檢查邏輯漏洞,還能沉澱非常多的業務邏輯,達到參考和複用的效果。以下是總結的採用可視化編排方式進行編程的好處:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/23\/23abaaeec91997098b7c9d90b250cf25.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(1)需求可視化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"把需求按照流程圖的方式展示出來,可視化的流程對於新人以及非開發同學來說是非常容易理解的,對了解業務有很大的幫助。特別是在判別條件非常多、業務身份非常多、業務流程冗長而複雜的場景中,能梳理好全部的需求邏輯,能更好地檢查代碼是否考慮了全部的需求場景。這使得交付、交流、接手會更容易。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(2)邏輯複用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲代碼是針對於節點粒度的,在做需求時,可以參考和複用已有的節點代碼(如判斷登錄、關注店鋪等等),不僅對新人上手非常友好,也節約了反覆編寫重複代碼的時間,能縮短開發週期。隨着邏輯節點的不斷沉澱,這種優勢也會越發明顯。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(3)提高代碼規範"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在現有的開發方式下,隨着需求不斷迭代,可能有會有越來越多的業務邏輯被分散在各個模塊中,很可能會違反職責單一、高內聚低耦合的編程原則。而通過邏輯編排的方式開發,每次只針對一個節點編寫代碼,能保證各個能力職責單一,最後通過聚合的方式串聯起整個業務邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在思考一下,使用到邏輯編排後,上述列舉出的種種問題,是不是會迎刃而解呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實邏輯編排不是一種新出現的思想,其實像我們生活中,都有邏輯編排的影子。它只是按照行爲邏輯,把一個個單一的業務行爲(如限流、限購、加購等等)有序編織爲一個完整流程,聚合成一條具有特定業務含義的執行鏈,每一個完整的流程都會有一個觸發點。如下圖,就是在業務場景中整理的流程圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/14\/149361ea45f70a84c99ef93989e30362.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,在 "},{"type":"codeinline","content":[{"type":"text","text":"coding"}]},{"type":"text","text":" 中引入邏輯編排的概念是有價值的,我們也正在嘗試着使用新的開發模式去提高生產效率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於x6的流程編排"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上文中詳細展示了爲什麼需要使用到流程編排,下面詳細介紹下流程圖繪製的原理。 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 底層採用了螞蟻團隊提供的 "},{"type":"link","attrs":{"href":"https:\/\/X6.antv.vision\/zh\/?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"antv-X6 圖編輯引擎"}]},{"type":"text","text":" ,從而實現了流程圖繪製的能力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼要選用 "},{"type":"codeinline","content":[{"type":"text","text":"antv-x6"}]},{"type":"text","text":" 作爲底層繪圖引擎呢?因爲它基本能覆蓋到流程圖繪製的全部需求,內置了圖編輯場景的常規交互和設計,能幫助我們快速創建畫布、節點和邊,能夠做到開箱即用,使用是非常方便的。它開放了豐富的定製能力給到開發者,只需簡單的配置就能實現想要的效果,其不僅僅支持流程圖,還支持 "},{"type":"codeinline","content":[{"type":"text","text":"DAG"}]},{"type":"text","text":" 圖、 "},{"type":"codeinline","content":[{"type":"text","text":"ER"}]},{"type":"text","text":" 圖、組織架構圖等等。在 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 中我們僅僅使用到了流程圖繪製的能力,其實 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 還有更多的能力值得大家去使用去探索。下面結合 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 框架的實現,詳細介紹下我們實現流程圖繪製的具體思路,如何從 0 到 1 實現可用的較爲完備的流程圖繪製能力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"畫布設計"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 創建畫布非常容易,實例化一個 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 暴露的 "},{"type":"codeinline","content":[{"type":"text","text":"Graph"}]},{"type":"text","text":" 對象即可。以下是畫布的具體配置項,包括了一下特性。"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"節點是否可旋轉"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"節點是否可調整大小"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"跨畫布的複製\/剪切\/粘貼"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"節點連線規則配置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"畫布背景配置(支持顏色\/圖片\/水印等)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"網格配置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"點選\/框選配置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":8,"align":null,"origin":null},"content":[{"type":"text","text":"對齊線配置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":9,"align":null,"origin":null},"content":[{"type":"text","text":"鍵盤快捷鍵配置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":10,"align":null,"origin":null},"content":[{"type":"text","text":"撤銷\/重做能力"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":11,"align":null,"origin":null},"content":[{"type":"text","text":"畫布滾動、平移、居中、縮放等能力"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":12,"align":null,"origin":null},"content":[{"type":"text","text":"鼠標滾輪縮放配置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":13,"align":null,"origin":null},"content":[{"type":"text","text":"……"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體的代碼實現如下,註釋了各類能力對應的API文檔,大家有興趣可以試試~"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import {Graph} from '@antv\/X6';\nconst flowChart = new Graph({\n \/\/ 渲染指定 dom 節點\n container: document.getElementById('flowChart'),\n \/\/ 節點是否可旋轉\n rotating: false,\n \/\/ 節點是否可調整大小\n resizing: true,\n \/\/ 剪切板,支持跨畫布的複製\/粘貼(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/clipboard)\n clipboard: {\n enabled: true,\n useLocalStorage: true,\n },\n \/\/ 節點連線規則配置(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/api\/graph\/interaction#connecting)\n connecting: {\n snap: true,\n dangling: true,\n highlight: true,\n anchor: 'center',\n connectionPoint: 'anchor',\n router: { name: 'manhattan' }\n },\n \/\/ 畫布背景,支持顏色\/圖片\/水印等(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/background)\n background: {\n color: '#f8f9fa',\n },\n \/\/ 網格配置(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/grid)\n grid: {\n visible: true,\n },\n \/\/ 點選\/框選配置(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/selection)\n selecting: {\n enabled: true,\n multiple: true,\n rubberband: true,\n movable: true,\n strict: true,\n showNodeSelectionBox: true\n },\n \/\/ 對齊線配置,輔助移動節點排版(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/snapline)\n snapline: {\n enabled: true,\n clean: 100,\n },\n \/\/ 撤銷\/重做能力(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/history)\n history: {\n enabled: true,\n },\n \/\/ 使畫布具備滾動、平移、居中、縮放等能力(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/scroller)\n scroller: {\n enabled: true,\n },\n \/\/ 鼠標滾輪縮放(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/mousewheel)\n mousewheel: {\n enabled: true,\n minScale: MIN_ZOOM,\n maxScale: MAX_ZOOM,\n modifiers: ['ctrl', 'meta'],\n },\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成了畫布的開發,接下來在主界面中引入即可。這裏我們發現還是存在很多問題:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無法使用鍵盤快捷鍵怎麼辦,比如複製粘貼保存撤銷?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想監聽一些畫布方法怎麼辦,例如雙擊節點直接打開編輯框、右鍵節點打開菜單、右鍵畫布打開另一個菜單?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想實現小地圖怎麼辦?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想把畫布信息導出並儲存怎麼辦?"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"……"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"僅僅依賴於以上 "},{"type":"codeinline","content":[{"type":"text","text":"new Graph"}]},{"type":"text","text":" 配置的信息是遠遠不夠的,還需要一步步完善起來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"快捷鍵設置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據 "},{"type":"link","attrs":{"href":"https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/keyboard#graphbindkey?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"鍵盤快捷鍵 Keyboard"}]},{"type":"text","text":" 介紹,"},{"type":"codeinline","content":[{"type":"text","text":"Graph"}]},{"type":"text","text":" 實例會暴露一個 "},{"type":"codeinline","content":[{"type":"text","text":"bindKey"}]},{"type":"text","text":" 方法,我們可以用它來綁定快捷鍵。以使用頻率最高的 "},{"type":"codeinline","content":[{"type":"text","text":"ctrl + c"}]},{"type":"text","text":" \/ "},{"type":"codeinline","content":[{"type":"text","text":"ctrl + v"}]},{"type":"text","text":" 舉例,這裏定義了一系列的鍵盤快捷鍵和handler監聽函數,如 "},{"type":"codeinline","content":[{"type":"text","text":"ctrl + c"}]},{"type":"text","text":"時獲取當前選中的元素,利用 "},{"type":"codeinline","content":[{"type":"text","text":"copy()"}]},{"type":"text","text":" 方法完成複製操作,"},{"type":"codeinline","content":[{"type":"text","text":"ctrl + v"}]},{"type":"text","text":" 時直接使用 "},{"type":"codeinline","content":[{"type":"text","text":"paste({offset:xx})"}]},{"type":"text","text":" 方法完成粘貼操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首選需要在 "},{"type":"codeinline","content":[{"type":"text","text":"new Graph"}]},{"type":"text","text":" 的參數中加入 "},{"type":"codeinline","content":[{"type":"text","text":"keyboard"}]},{"type":"text","text":" 配置項,這裏的 "},{"type":"codeinline","content":[{"type":"text","text":"global"}]},{"type":"text","text":" 代表是否爲全局鍵盤事件,設置爲 "},{"type":"codeinline","content":[{"type":"text","text":"true"}]},{"type":"text","text":" 時綁定在 "},{"type":"codeinline","content":[{"type":"text","text":"Document"}]},{"type":"text","text":" 上,否則綁定在畫布容器上。在這裏我們設置爲 "},{"type":"codeinline","content":[{"type":"text","text":"false"}]},{"type":"text","text":",只在畫布獲得焦點才觸發鍵盤事件。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const flowChart = new Graph({\n \/\/ 鍵盤快捷鍵能力(詳細文檔:https:\/\/X6.antv.vision\/zh\/docs\/tutorial\/basic\/keyboard)\n keyboard: {\n enabled: true,\n global: false,\n }\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來要配置支持的快捷鍵,如複製、粘貼、保存、撤銷、縮放、全選……需要指定對應的按鍵以及監聽函數。感興趣的朋友可以看下"},{"type":"link","attrs":{"href":"https:\/\/github.com\/imgcook\/imove\/blob\/master\/packages\/core\/src\/common\/shortcuts.ts?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"shortcuts.ts"}]},{"type":"text","text":"。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import { Cell, Edge, Graph, Node } from '@antv\/X6';\ninterface Shortcut {\n keys: string | string[];\n handler: (flowChart: Graph) => void;\n}\nconst shortcuts: { [key: string]: Shortcut } = {\n \/\/ 複製\n copy: {\n keys: 'meta + c',\n handler(flowChart: Graph) {\n const cells = flowChart.getSelectedCells();\n if (cells.length > 0) {\n flowChart.copy(cells);\n message.success('複製成功');\n }\n return false;\n },\n },\n \/\/ 粘貼\n paste: {\n keys: 'meta + v',\n handler(flowChart: Graph) {\n if (!flowChart.isClipboardEmpty()) {\n const cells = flowChart.paste({ offset: 32 });\n flowChart.cleanSelection();\n flowChart.select(cells);\n }\n return false;\n },\n },\n \/\/ many funcions can be defined here\n save: {}, \/\/ 保存\n undo: {}, \/\/ 撤銷\n redo: {}, \/\/ 重做\n zoomIn: {}, \/\/ 放大\n zoomOut: {}, \/\/ 縮小\n delete: {}, \/\/ 刪除\n selectAll: {}, \/\/ 全選\n bold: {}, \/\/ 加粗\n italic: {}, \/\/ 斜體\n underline: {}, \/\/ 下劃線\n bringToTop: {}, \/\/ 置於頂層\n bringToBack: {} \/\/ 置於底層\n};\nexport default shortcuts;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們也可以實現快捷鍵或滾輪放大、縮小的能力,需要自定義每次縮放尺度的改變量,根據當前的縮放尺度去增減這個改變量即可。爲了避免用戶無限放大或無限縮小帶來不好的視覺體驗,最好定義一個最大和最小的縮放尺度。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const shortcuts: { [key: string]: Shortcut } = {\n zoomIn: {\n keys: 'meta + shift + +',\n handler(flowChart: Graph) {\n const nextZoom = (flowChart.zoom() + ZOOM_STEP).toPrecision(2);\n flowChart.zoomTo(Number(nextZoom), { maxScale: MAX_ZOOM });\n return false;\n },\n },\n zoomOut: {\n keys: 'meta + shift + -',\n handler(flowChart: Graph) {\n const nextZoom = (flowChart.zoom() - ZOOM_STEP).toPrecision(2);\n flowChart.zoomTo(Number(nextZoom), { minScale: MIN_ZOOM });\n return false;\n },\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後我們把所有的快捷鍵綁定在 "},{"type":"codeinline","content":[{"type":"text","text":"Graph"}]},{"type":"text","text":" 上,即可完成全部快捷點的配置。"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"shortcuts.forEach(shortcur => {\n const { key, handler } = shortcut;\n graph.bindKey(key, () => handler(graph));\n});"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"畫布方法註冊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在當前畫布上,我們需要監聽一系列常用的方法,如雙擊節點、畫布右鍵、節點右鍵等等。在imove中,完成了以下的操作:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"雙擊節點打開代碼編輯框,想提高交互體驗"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"右鍵節點打開菜單欄,支持節點複製、刪除、編輯文本、置於頂層\/底層、編輯代碼、執行代碼"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"右鍵畫布打開菜單欄,支持全選和粘貼"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ca\/cab784b0d0c8bc3181b8a5f0942be99f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是具體的實現方法:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const registerEvents = (flowChart: Graph): void => {\n \/\/ 監聽節點雙擊事件,用於打開代碼編輯界面\n flowChart.on('node:dblclick', () => {\n });\n \n \/\/ 監聽畫布右鍵菜單\n flowChart.on('blank:contextmenu', (args) => {\n });\n \n \/\/ 監聽節點右鍵菜單\n flowChart.on('node:contextmenu', (args) => {\n });\n};"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"創建畫布實例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前已經完成了畫布通用配置、快捷鍵設置、事件綁定,接下來實現一個 "},{"type":"codeinline","content":[{"type":"text","text":"createFlowChart"}]},{"type":"text","text":" 的工廠函數。在工廠函數中,我們創建了畫布實例,併爲其註冊綁定的事件、註冊快捷鍵、註冊服務端存儲。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 註冊快捷鍵\nconst registerShortcuts = (flowChart: Graph): void => {\n Object.values(shortcuts).forEach((shortcut) => {\n const { keys, handler } = shortcut;\n flowChart.bindKey(keys, () => handler(flowChart));\n });\n};\nconst createFlowChart = (container: HTMLDivElement, miniMapContainer: HTMLDivElement): Graph => {\n const flowChart = new Graph({\n \/\/ many configuration\n })\n \n registerEvents(flowChart); \/\/ 註冊綁定事件\n registerShortcuts(flowChart); \/\/ 註冊快捷鍵\n registerServerStorage(flowChart); \/\/ 註冊服務端存儲\n \n return flowChart;\n};\n\nexport default createFlowChart;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,畫布的功能越來越完善了,已經支持繪製流程圖了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"導出模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 中,需要支持繪製的流程圖以 "},{"type":"codeinline","content":[{"type":"text","text":"DSL"}]},{"type":"text","text":" 、代碼、流程圖的導出,這樣在真實業務開發中,就可以最大程度的複用節點和流程。爲了實現這個功能,我們實現的相關函數如下。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 導出DSL\nconst onExportDSL = () => {\n const dsl = JSON.stringify(flowChart.toJSON(), null, 2);\n const blob = new Blob([dsl], { type: 'text\/plain' });\n DataUri.downloadBlob(blob, 'imove.dsl.json');\n};\n\n\/\/ 導出代碼\nconst onExportCode = () => {\n const zip = new JSZip();\n const dsl = flowChart.toJSON();\n const output = compileForProject(dsl);\n Helper.recursiveZip(zip, output);\n\n zip.generateAsync({ type: 'blob' }).then((blob) => {\n DataUri.downloadBlob(blob, 'logic.zip');\n });\n};\n\n\/\/ 導出流程圖\nconst onExportFlowChart = () => {\n flowChart.toPNG((dataUri: string) => {\n DataUri.downloadDataUri(dataUri, 'flowChart.png');\n }, { padding: 50, ratio: '3.0' });\n};"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"節點設計"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 中,我們設計了以下三種節點類型:事件節點(開始節點)、行爲節點、分支節點。節點負責處理具體的邏輯流程,以下是三種節點的具體描述:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"開始節點:邏輯起始,是所有流程的開始,可以是一次生命週期初始化\/一次點擊"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"行爲節點:邏輯執行,可以是一次網絡請求\/一次改變狀態\/發送埋點等"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"分支節點:邏輯路由,根據不同的邏輯執行結果跳轉到不同的節點(注:一條邏輯流程必須以"},{"type":"text","marks":[{"type":"strong"}],"text":"開始節點"},{"type":"text","text":"爲起始)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8b\/8bb955bbf2d6cc7226259232d51834e5.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據上述的規範描述,我們可以繪製出各種各樣的邏輯流程圖,例如 "},{"type":"text","marks":[{"type":"strong"}],"text":"進入首頁"},{"type":"text","text":" 的流程圖如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/55\/557bcea40796985d9df29644c262cc8a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"節點屬性結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 的流程圖節點需要承載了多種屬性,如節點文字、代碼、投放配置模型、投放配置數據、依賴包等等,但是在 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 中,節點的基本屬性爲 "},{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"shape"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"position"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"size"}]},{"type":"text","text":",主要包括與 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 圖形展示相關的基本數據,簡稱爲圖形化數據。因此我們需要擴展節點屬性,這些擴展的屬性簡稱爲自定義數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"圖形化數據(主要包括與X6圖形展示相關的基本數據)"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"id: 32-bit 唯一標識符"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"shape: 形狀"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"position: {x, y}: 橫向\/縱向位移"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"size: {width, height}: 寬高大小"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"..."}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"自定義數據(擴展的屬性)"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"type: 節點類型"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"label: 節點展示文案"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"code: 節點存儲的代碼"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"dependencies: js 代碼的依賴包"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"trigger: 觸發邏輯開始的事件名(開始節點纔有)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ports: 節點出口配置(跳轉邏輯,分支節點纔有)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"configSchema: 投放配置模型"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"configData: 投放配置數據"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"version: 版本號"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"forkId: 複製來源"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"referId: 引用來源"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義完這些節點屬性後,就可以實現節點信息的存儲,不至於丟失信息。通常來說,信息配置使用頻率最高的有:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯示名稱:更改節點名稱"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"邏輯觸發名稱:開始節點類型專屬配置,項目代碼使用時根據這個值觸發邏輯調用"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"投放配置 "},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":":修改投放配置的表單結構"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏編輯的每一條信息,都會保存在節點的屬性裏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/0f\/0f55f82351095dc55e6ca854636154f3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"節點拖拽"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單純有畫布是不夠的,我們還需支持在畫布上添加節點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/35\/35da8171a3b7b7b928acb8185e7aa772.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以使用 addNode 和 addEdge 方法來動態添加節點和邊:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 添加起點\nconst source = graph.addNode({\n id: 'node1',\n x: 40,\n y: 40,\n width: 80,\n height: 40,\n label: 'Hello',\n});\n\/\/ 添加終點\nconst target = graph.addNode({\n id: 'node2',\n x: 160,\n y: 180,\n width: 80,\n height: 40,\n label: 'World',\n});\n\/\/ 連線\ngraph.addEdge({ source, target });"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而這種方式並不能達到通過拖拽來生成流程圖的要求,不過也不用擔心, "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 已經考慮到了這點,封裝了 "},{"type":"codeinline","content":[{"type":"text","text":"Dnd"}]},{"type":"text","text":" 類(drag and drop)來解決這個問題。代碼如下:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import { Addon, Graph } from '@antv\/X6';\nconst { Dnd } = Addon;\n\/\/ 創建主畫布\nconst graph = new Graph({\n id: document.getElementById('flowchart'),\n grid: true,\n snapline: { enabled: true }\n});\n\/\/ 創建 Dnd 實例\nconst dnd = new Dnd({\n target: graph,\n scaled: false,\n animation: true\n});\n\/\/ 創建側邊欄\nconst sideBar = new Graph({\n id: document.getElementById('sideBar'),\n interacting: false\n});\n\/\/ 側邊欄添加內置節點1\nsideBar.addNode({\n id: 'node1',\n x: 80,\n y: 80,\n width: 80,\n height: 40,\n label: 'Hello',\n});\n\/\/ 側邊欄添加內置節點2\nsideBar.addNode({\n id: 'node2',\n x: 80,\n y: 140,\n width: 80,\n height: 40,\n label: 'iMove',\n});\n\/\/ 監聽 mousedown 事件,調用 dnd.start 處理拖拽\nsideBar.on(\"cell:mousedown\", (args) => {\n const { node, e } = args;\n dnd.start(node.clone(), e);\n});"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"節點樣式設置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了不限制繪製流程圖的體驗, "},{"type":"codeinline","content":[{"type":"text","text":"iMove"}]},{"type":"text","text":" 工具欄提供了繪製流程圖常用到的一些功能(例如修改字號、加粗、斜體、文字顏色、背景顏色、對齊等等),這主要也是得益於 "},{"type":"codeinline","content":[{"type":"text","text":"X6"}]},{"type":"text","text":" 提供了統一修改節點樣式的方法。工具欄如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/54\/549e7bebac1a30f6a8e0098c44dced55.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 "},{"type":"codeinline","content":[{"type":"text","text":"setAttrs"}]},{"type":"text","text":" 方法可以配置指定的樣式:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 設置字號\ncell.setAttrs({ label: { fontSize: 14 } };\n\/\/ 設置字重\ncell.setAttrs({ label: { fontWeight: 'bold' } });\n\/\/ 設置斜體\ncell.setAttrs({ label: { fontStyle: 'italic' } });\n\/\/ 設置文字顏色\ncell.setAttrs({ label: { fill: 'red' } });\n\/\/ 設置背景顏色\ncell.setAttrs({ body: { fill: 'green' } });\n\/\/ …………"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"節點代碼編寫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每個節點的代碼等價於一個 "},{"type":"codeinline","content":[{"type":"text","text":"js"}]},{"type":"text","text":" 模塊,因此你不用擔心全局變量的命名污染問題,甚至可以 "},{"type":"codeinline","content":[{"type":"text","text":"import"}]},{"type":"text","text":" 現有的 "},{"type":"codeinline","content":[{"type":"text","text":"npm"}]},{"type":"text","text":" 包,但最後必須 "},{"type":"codeinline","content":[{"type":"text","text":"export"}]},{"type":"text","text":" 出一個函數。需要注意的是,由於 "},{"type":"codeinline","content":[{"type":"text","text":"iMove"}]},{"type":"text","text":" 天生支持節點代碼的異步調用,因此 "},{"type":"codeinline","content":[{"type":"text","text":"export"}]},{"type":"text","text":" 出的函數默認是一個 "},{"type":"codeinline","content":[{"type":"text","text":"promise"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c1\/c1f1bd8580d1d907a2db5785d6ef76b8.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就以 "},{"type":"text","marks":[{"type":"strong"}],"text":"是否登錄"},{"type":"text","text":" 這個分支節點爲例,我們來看下節點代碼應該如何編寫:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"export default async function() {\n return fetch('\/api\/isLogin')\n .then(res => res.json())\n .then(res => {\n const {success, data: {isLogin} = {}} = res;\n return success && isLogin;\n }).catch(err => {\n console.log('fetch \/api\/isLogin failed, the err is:', err);\n return false;\n });\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於該節點是分支節點,因此其 "},{"type":"codeinline","content":[{"type":"text","text":"boolean"}]},{"type":"text","text":" 返回值決定了整個流程的走向。如果是非分支節點,直接流向下一個連接的節點即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"節點間數據通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成節點代碼編寫之後,我們再來看下節點之間是如何進行數據通信的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"iMove"}]},{"type":"text","text":" 中,數據是以流(pipe)的形式從前往後進行流動的,也就是說前一個節點的返回值會是下一個節點的輸入。不過也有一個例外,由於 "},{"type":"text","marks":[{"type":"strong"}],"text":"分支節點"},{"type":"text","text":" 的返回值會是 "},{"type":"codeinline","content":[{"type":"text","text":"boolean"}]},{"type":"text","text":" 類型,因此它的下游節點拿到的輸入必將是一個 "},{"type":"codeinline","content":[{"type":"text","text":"boolean"}]},{"type":"text","text":" 類型值,從而造成數據流的中斷。爲此,我們進行了一定的改造,分支節點的作用只負責數據流的轉發,就像一個開關一樣,只決定數據流的走向,但不改變流向下游的數據——流入分支節點後一個節點的數據依然是分支節點前一個節點輸出的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,以下例子中“請求profile接口”和“返回數據“兩個節點會成爲數據流的上下游關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/03\/03d2e6868e9f178d1f5523b341555a78.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們再來看下他們之間是如何進行數據通信的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"節點: 請求profile接口"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"export default async function() {\n return fetch('\/api\/profile')\n .then(res => res.json())\n .then(res => {\n const {success, data} = res;\n return {success, data};\n }).catch(err => {\n console.log('fetch \/api\/isLogin failed, the err is:', err);\n return {success: false};\n });\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"節點: 接口成功"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"export default async function(ctx) {\n \/\/ 獲取上游數據\n const pipe = ctx.getPipe() || {};\n return pipe.success;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"節點: 返回數據"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const processData = (data) => {\n \/\/ TODO: 數據加工處理\n return data;\n};\nexport default async function(ctx) {\n \/\/ 這裏獲取到的上游數據,不是\"接口成功\"這個分支節點的返回值,而是\"請求profile接口\"這個節點的返回值\n const pipe = ctx.getPipe() || {};\n \n \/\/ 觸發updateUI這個方法更新界面,傳入的值爲profileData\n ctx.emit('updateUI', {profileData: processData(pipe.data)});\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上代碼所述,每個下游節點可以調用 "},{"type":"codeinline","content":[{"type":"text","text":"ctx.getPipe"}]},{"type":"text","text":" 方法獲取上游節點返回的數據流。另外,需要注意的是 "},{"type":"text","marks":[{"type":"strong"}],"text":"返回數據"},{"type":"text","text":" 節點的最後一行代碼 "},{"type":"codeinline","content":[{"type":"text","text":"ctx.emit('updateUI', data)"}]},{"type":"text","text":" 需要和項目中的代碼配合使用,項目中想監聽這個事件,需要執行 "},{"type":"codeinline","content":[{"type":"text","text":"logic.on('updateUI',data=>{ \/\/handler })"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"邊設計"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 中,邊的作用被弱化,僅表示圖形上的節點連接關係,主要控制流程的走向,使用過程中僅用於連線。因此我們沒有額外設計邊屬性,只採用了X6默認的邊的屬性:id(唯一標識符)、shape(形狀)、source(起點)、target(終點)。如下所示:"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"{\n \"id\": \"5d034984-e0d5-4636-a5ab-862f1270d9e0\",\n \"shape\": \"edge\",\n \"source\": {\n \"cell\": \"1b44f69a-1463-4f0e-b8fc-7de848517b4e\",\n \"port\": \"bottom\"\n },\n \"target\": {\n \"cell\": \"c18fa75c-2aad-40e9-b2d2-f3c408933d53\",\n \"port\": \"top\"\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了邊屬性結構描述外,我們還需要關注邊連線實現、樣式定製化和代碼編譯,下面會分別講一下。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"邊連線實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"邊連線是通過在畫布上綁定 "},{"type":"codeinline","content":[{"type":"text","text":"edge:connected"}]},{"type":"text","text":" 事件實現的:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"flowChart.on('edge:connected', (args) => {\n const edge = args.edge as Edge;\n const sourceNode = edge.getSourceNode() as Node;\n\n if (sourceNode && sourceNode.shape === 'imove-branch') {\n const portId = edge.getSourcePortId();\n\n if (portId === 'right' || portId === 'bottom') {\n edge.setLabelAt(0, sourceNode.getPortProp(portId, 'attrs\/text\/text'));\n sourceNode.setPortProp(portId, 'attrs\/text\/text', '');\n }\n }\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 都已經提供好了,開發和定製都是非常簡單。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"邊選中樣式定製化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"默認的邊是沒有選中高亮的樣式的,這裏我們可以直接在畫布上綁定邊選中的事件,改變邊的樣式即可:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"flowChart.on('edge:selected', (args) => {\n args.edge.attr('line\/stroke', '#feb663');\n args.edge.attr('line\/strokeWidth', '3px');\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果大家想按照自己的喜好來定製流程圖上的邊展示,可以非常簡單的實現。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"基於流程圖的代碼編譯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流程圖畫好了,我們如何實現完整流程的代碼運行呢?其實,流程圖對應是的一個 "},{"type":"codeinline","content":[{"type":"text","text":"JSON Schema"}]},{"type":"text","text":",這樣我們就可以根據節點的連接順序進行編譯了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/0f\/0f5319d0161196937453e64356bfad41.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流程圖編譯的 "},{"type":"codeinline","content":[{"type":"text","text":"Schema"}]},{"type":"text","text":" 如下所示:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"分支節點\n{\n \"shape\": \"imove-branch\",\n \"data\": {\n \"ports\": {\n \"right\": {\n \"condition\": \"true\"\n },\n \"bottom\": {\n \"condition\": \"false\"\n }\n },\n \"code\": \"export default async function(ctx) {\\n return true;\\n}\"\n },\n \"id\": \"a6da6684-96c8-4595-bec6-a94122b61e30\"\n}\n連線\n{\n \"shape\": \"edge\",\n \"id\": \"cbcbd0ea-4a2a-4d2a-8135-7b4b7d7ec50d\",\n \"source\": {\n \"cell\": \"de868d18-9ec0-4dac-abbe-5cb9e3c20e2f\",\n \"port\": \"right\"\n },\n \"target\": {\n \"cell\": \"a6da6684-96c8-4595-bec6-a94122b61e30\",\n \"port\": \"left\"\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過流程圖 "},{"type":"codeinline","content":[{"type":"text","text":"Schema"}]},{"type":"text","text":",可以解析到節點屬性和節點關係,如開始節點、行爲節點如何流向下一個節點,分支節點如何根據運行結果分別流向兩條邊。具體的編譯過程如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/45\/45af56f0f6b708fe2865c22e909faf4c.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先根據 "},{"type":"codeinline","content":[{"type":"text","text":"DSL"}]},{"type":"text","text":" 可以獲取全部的邊(Edge),邊的屬性上存儲了 "},{"type":"codeinline","content":[{"type":"text","text":"source"}]},{"type":"text","text":" 節點和 "},{"type":"codeinline","content":[{"type":"text","text":"target"}]},{"type":"text","text":" 節點的 "},{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":" 和方向,即起始節點和終點節點的 "},{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":" 和方向,再根據節點的id可以找到全部的節點,於是可以串聯起全部的節點和邊。這裏有幾點要注意的是:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"節點(Node)和邊(Edge)的 "},{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":" 永遠都是唯一的,可以根據id找到對應的節點\/邊。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"方向永遠都是從邊(Edge)的 "},{"type":"codeinline","content":[{"type":"text","text":"source"}]},{"type":"text","text":" 節點流向 "},{"type":"codeinline","content":[{"type":"text","text":"target"}]},{"type":"text","text":" 節點的,因此節點之間的流動關係也是固定的。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"判斷節點有兩個輸出方向,需要根據節點輸出的boolean值去判斷走向。其中判斷節點記錄了兩條邊對應的方向和 "},{"type":"codeinline","content":[{"type":"text","text":"boolean"}]},{"type":"text","text":" 值,在編譯時需要考慮。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照以上分析的思路,找到當前節點的下一個節點代碼如下:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 找到下一個節點\nconst getNextNode = (curNode: Cell.Properties, dsl: DSL) => {\n const nodes = dsl.cells.filter((cell) => cell.shape !== 'edge');\n const edges = dsl.cells.filter((cell) => cell.shape === 'edge');\n const foundEdge = edges.find((edge) => edge.source.cell === curNode.id);\n \n if (foundEdge) {\n return nodes.find((node) => node.id === foundEdge.target.cell);\n }\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於form-render的可視化搭建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在電商領域,業務場景會時常涉及到營銷表單的配置。因此代碼中會暴露出一些字段供運營配置,這些字段需要以合適的表單形式呈現,因此涉及到表單 "},{"type":"codeinline","content":[{"type":"text","text":"Schema"}]},{"type":"text","text":" 結構的定義。 "},{"type":"codeinline","content":[{"type":"text","text":"iMove"}]},{"type":"text","text":" 在定義表單 "},{"type":"codeinline","content":[{"type":"text","text":"Schema"}]},{"type":"text","text":" 結構時,是可以使用可視化的方式設計表單結構的,這裏得益於 "},{"type":"link","attrs":{"href":"https:\/\/x-render.gitee.io\/form-render\/","title":null,"type":null},"content":[{"type":"text","text":"form-render"}]},{"type":"text","text":" 這個開源項目的優秀設計,我們可以使用其提供的"},{"type":"codeinline","content":[{"type":"text","text":"fr-generator"}]},{"type":"text","text":"庫,通過可視化拖拽修改的形式快速生成投放表單結構,方便後續進行數據投放。具體效果如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ef\/ef086bf8922417a96da6e74cd83b1974.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"如何做到Schema to Form"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上提到的"},{"type":"codeinline","content":[{"type":"text","text":"fr-generator"}]},{"type":"text","text":"表單設計器是如何快速地進行表單搭建的呢?這不得不先說一下如何根據規範化 "},{"type":"codeinline","content":[{"type":"text","text":"Schema"}]},{"type":"text","text":" 轉化爲 "},{"type":"codeinline","content":[{"type":"text","text":"Form"}]},{"type":"text","text":"。知其然知其所以然,接下來我們看看 "},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":" 是如何實現這一步的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/40\/400b8ac8d3f78c7e264ed59282460831.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"入口是"},{"type":"codeinline","content":[{"type":"text","text":"AntdForm\/FusionForm"}]},{"type":"text","text":"組件(分別兼容"},{"type":"codeinline","content":[{"type":"text","text":"antd"}]},{"type":"text","text":"組件庫和"},{"type":"codeinline","content":[{"type":"text","text":"fusion"}]},{"type":"text","text":"組件庫)。其中傳入"},{"type":"codeinline","content":[{"type":"text","text":"widgets"}]},{"type":"text","text":"參數包含了暴露的所有組件,如"},{"type":"codeinline","content":[{"type":"text","text":"checkbox"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"input"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"radio"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"select"}]},{"type":"text","text":"等,"},{"type":"codeinline","content":[{"type":"text","text":"mapping"}]},{"type":"text","text":"參數包含了"},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"type"}]},{"type":"text","text":"字段與 "},{"type":"codeinline","content":[{"type":"text","text":"widgetName"}]},{"type":"text","text":"(組件名)的映射關係。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"AntdForm\/FusionForm"}]},{"type":"text","text":"組件是由"},{"type":"codeinline","content":[{"type":"text","text":"RenderField"}]},{"type":"text","text":"組件實現的,在這裏組合了全部的 "},{"type":"codeinline","content":[{"type":"text","text":"Widget"}]},{"type":"text","text":" 生成了 "},{"type":"codeinline","content":[{"type":"text","text":"Field"}]},{"type":"text","text":" 組件(即純展示的表單組件)。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"RenderField"}]},{"type":"text","text":"是由"},{"type":"codeinline","content":[{"type":"text","text":"Field"}]},{"type":"text","text":"組件實現的,在這裏將純展示表單組件和"},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":"的屬性結合在一起,轉化爲真正帶有屬性的表單組件。轉化函數詳見"},{"type":"link","attrs":{"href":"https:\/\/github.com\/alibaba\/form-render\/blob\/master\/packages\/form-render\/src\/base\/parser.js","title":null,"type":null},"content":[{"type":"text","text":"parser.js"}]},{"type":"text","text":",這裏的轉化函數是基於"},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":"設計的json規範實現的,使用"},{"type":"codeinline","content":[{"type":"text","text":"'ui:className'"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"'ui:hidden'"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"'ui:width'"}]},{"type":"text","text":"等一致的屬性命名規範去承載樣式數據。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來討論下"},{"type":"codeinline","content":[{"type":"text","text":"fr-generator"}]},{"type":"text","text":"表單設計器的核心操作是什麼。點擊左側表單組件,就可以將其加入到表格中:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b4\/b45112b17ab829338104cc797e8e2fe6.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下的 "},{"type":"codeinline","content":[{"type":"text","text":"Element"}]},{"type":"text","text":" 組件即代表左側每一個表單選項,點擊左側表單組件,會調用"},{"type":"codeinline","content":[{"type":"text","text":"handleElementClick"}]},{"type":"text","text":"方法,實際上是調用了 "},{"type":"codeinline","content":[{"type":"text","text":"addItem"}]},{"type":"text","text":" 方法:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const Element = ({ text, name, schema, icon }) => {\n \/\/ ......\n \n const { selected, flatten, onFlattenChange } = useStore();\n \n const handleElementClick = () => {\n const { newId, newFlatten } = addItem({ selected, name, schema, flatten });\n onFlattenChange(newFlatten);\n setGlobal({ selected: newId });\n };\n return (\n
\n \n \n );\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"addItem"}]},{"type":"text","text":" 對應的代碼實現如下,其實是改變了原有的 "},{"type":"codeinline","content":[{"type":"text","text":"JSON Schema"}]},{"type":"text","text":" 數據, "},{"type":"codeinline","content":[{"type":"text","text":"JSON Schema"}]},{"type":"text","text":" 改變了,中間渲染的表單就會跟隨發生變化。核心步驟如下:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"獲取畫布中選中的節點(表單項)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"獲取此節點的父節點children屬性"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"找到此節點在children數組中的位置"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"在選中節點後插入新的表單項(其實是在操作父節點的children數組)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"JSON Schema結構改變"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"畫布更新"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 點擊左側菜單添加表單結構項\nexport const addItem = ({ selected, name, schema, flatten }) => {\n \/\/ ......\n let _name = name + '_' + nanoid(6);\n const idArr = selected.split('\/');\n idArr.pop();\n idArr.push(_name);\n newId = idArr.join('\/');\n const newFlatten = { ...flatten };\n \n try {\n \/\/ 拿到選中的節點\n const item = newFlatten[selected];\n \/\/ 拿到選中節點的父節點children屬性\n const siblings = newFlatten[item.parent].children;\n \/\/ 找到選中節點在children數組中的位置\n const idx = siblings.findIndex(x => x === selected);\n \/\/ 將選中節點後插入新的表單項\n siblings.splice(idx + 1, 0, newId);\n const newItem = {\n parent: item.parent,\n schema: { ...schema, $id: newId },\n data: undefined,\n children: [],\n };\n newFlatten[newId] = newItem;\n } catch (error) {\n console.error(error);\n }\n return { newId, newFlatten };\n};"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"數據驅動帶來的便利性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實,類似於 "},{"type":"codeinline","content":[{"type":"text","text":"form render"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"fomily"}]},{"type":"text","text":" 這樣的庫,可以通過簡單的 "},{"type":"codeinline","content":[{"type":"text","text":"JSON Schema"}]},{"type":"text","text":" 生成表單,都是基於數據驅動的思想實現的。維護自身的一套 "},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":" 規範,按照規範解析 "},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":" 文件可以直接完成表單的渲染。數據驅動爲何讓開發者如此熱衷? "},{"type":"codeinline","content":[{"type":"text","text":"react"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"vue"}]},{"type":"text","text":" 等流行框架的底層也是數據驅動的思維,解放了程序員繁瑣重複的工作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就拿 "},{"type":"codeinline","content":[{"type":"text","text":"vue"}]},{"type":"text","text":" 框架來說,以下數據驅動帶來的便利性非常受人歡迎:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"模板渲染:根據模板生成 "},{"type":"codeinline","content":[{"type":"text","text":"AST"}]},{"type":"text","text":",最後根據 "},{"type":"codeinline","content":[{"type":"text","text":"AST"}]},{"type":"text","text":" 樹填充數據生成真實 "},{"type":"codeinline","content":[{"type":"text","text":"DOM"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"數據綁定:可以監聽 交互輸入\/http請求響應\/定時器觸發 等行爲,當數據發生變化時,做 "},{"type":"codeinline","content":[{"type":"text","text":"diff"}]},{"type":"text","text":" 操作,完成 "},{"type":"codeinline","content":[{"type":"text","text":"DOM"}]},{"type":"text","text":" 的更新"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"路由引擎:根據 host\/path\/params 等數據,解析對應頁面"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在數據驅動的場景下,我們只需要完成兩步:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"將產品、業務、設計進行抽象化,將UI、交互抽象爲數據"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"將數據用邏輯處理連接起來,通過數據去直接影響結果"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據驅動的思想能夠給前端帶來很多的便利。以前開發時,我們處理頁面元素就會處理 "},{"type":"codeinline","content":[{"type":"text","text":"DOM"}]},{"type":"text","text":",處理事件邏輯就會處理 "},{"type":"codeinline","content":[{"type":"text","text":"JavaScript"}]},{"type":"text","text":",處理樣式就會處理 "},{"type":"codeinline","content":[{"type":"text","text":"CSS"}]},{"type":"text","text":"。切換爲數據驅動的思想之後,我們可以把頁面元素、事件邏輯、樣式都視爲數據,設計好數據與狀態之間的轉換關係,通過改變數據直接去改變狀態。以上介紹的 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":" 都是可以通過已有的規範 "},{"type":"codeinline","content":[{"type":"text","text":"JSON"}]},{"type":"text","text":" 數據,直接生成對應的流程圖和表單,其實數據和UI的轉換關係已經隱藏在了框架的內部實現中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這篇文章主要討論了 "},{"type":"codeinline","content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 基於"},{"type":"codeinline","content":[{"type":"text","text":"X6"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":"背後的思考以及相關的實現原理,主要是在已有開源庫的基礎上不斷去完善,直到滿足項目的需求,這樣才能做到 "},{"type":"codeinline","content":[{"type":"text","text":"ROI"}]},{"type":"text","text":" 最大化,相互成就。 "},{"type":"codeinline","content":[{"type":"text","text":"iMove"}]},{"type":"text","text":" 做的比較好的是定位,繼而將寫法規範化,將編排工具化,這樣剋制的設計使得 "},{"type":"codeinline","content":[{"type":"text","text":"iMove"}]},{"type":"text","text":" 具備小而美的特點,便於開發使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在初次使用某個開源框架或庫時可以根據官方文檔提供的示例實現 "},{"type":"codeinline","content":[{"type":"text","text":"Demo"}]},{"type":"text","text":" 效果,由於每個人業務需求都不盡相同,可能需要進行不同的配置甚至進行二次開發才能實現。這時候,可以恰當地使用現有的 "},{"type":"codeinline","content":[{"type":"text","text":"API"}]},{"type":"text","text":" 能力去儘量實現需求,如果沒有提供相應的能力,也可以看看有沒有提供自定義插件、自定義函數或組件能滿足需求。如果還是沒有合適的方案,或許可以嘗試一下自己實現?如果框架體積非常大但是你只需要使用到其中很小一部分,這時候可以考慮下看看對應的源碼,學習下原理嘗試自己實現,其實 "},{"type":"codeinline","content":[{"type":"text","text":"x6"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"form-render"}]},{"type":"text","text":" 這種開源基礎庫在很多場景下都是非常實用的。未來 "},{"type":"link","attrs":{"href":"https:\/\/github.com\/imgcook\/imove?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"imove"}]},{"type":"text","text":" 還會持續迭代,喜歡的朋友們歡迎來踩踩哦~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"活動推薦"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在今年的5月28-30日舉辦的 "},{"type":"link","attrs":{"href":"https:\/\/qcon.infoq.cn\/2021\/beijing\/?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"QCon全球軟件開發大會(北京站)"}]},{"type":"text","text":"我們也設置了“低代碼探索與實踐“專題,目前議題徵集中,歡迎大家來QCon分享。此外,QCon還設置有前端工程化、業務架構、大數據實時計算等技術專場,感興趣的同學點擊"},{"type":"link","attrs":{"href":"https:\/\/qcon.infoq.cn\/2021\/beijing\/?utm_source=web&utm_campaign=7&utm_medium=infoq&utm_content=langshu","title":"xxx","type":null},"content":[{"type":"text","text":"官網"}]},{"type":"text","text":"搶先了解吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章