邏輯編排在優酷可視化搭建中的實踐 - 邏輯與 Runtime
從可視化搭建說起
頁面可視化搭建系統從 16 年開始如雨後春筍般湧現而出,從活動頁搭建到中後臺搭建,有開源有僅公司內部使用的,都致力於將前端從繁複的體力勞動中解脫出來,提高頁面生產效率。優酷內部也有一套營銷活動搭建系統,每年生產 2K + 活動頁;能夠滿足這麼多頁面的需求,除了沉澱了大量可複用的組件外,圍繞着搭建系統的前端研發每天都在不停地維護升級老的組件,同時生產新的組件。
痛點
頁面生產能力上去了,研發還是一直埋頭在組件開發需求中。這些需要都是從哪裏來的呢?其實上面也有提到,就是兩點:
-
老的組件需要添加新的能力,可能是 UI 改動,可能是邏輯變更
-
新的業務組件開發
需求是永遠也做不完的,爲了提高開發效率,研發側不斷地沉澱通用的基礎庫,與服務端商定標準化的接口,以此來減少維護成本,但基於現有模式錦上添花的優化遠遠不夠。說白了,現有的可視化搭建效率和研發效率都已經達到瓶頸了,我們急需一種新的生產模式,給我們帶來生產效率的突破性提升。
解法
我們思考一下,頁面可視化搭建是如何解放生產效率的。它將完整的頁面進行拆解,拆分爲可以複用的組件,研發負責組件生產,產品運營負責組件配置,形成了一種簡單的流水線作業的模式,這種模式的好處在於:
-
業務組件複用,避免重複開發,研發只需要專注於單個組件
-
產品運營可以介入頁面生產過程,減少溝通損耗的同時分擔了部分先前研發承擔的工作
現在的瓶頸已從頁面開發效率轉變到組件開發效率上,我們做一個設想,讓非研發角色也能介入到組件生產過程中,進一步提高生產效率。在此之前,我們需要先基於現有模式進一步拆分組件結構:
組件可以拆解爲 UI + 邏輯。UI 可以通過細粒度的文字、圖片、slider 等組件搭建出來;邏輯主要涉及到接口請求、數據處理、能力調用,我們將一些常用的 api 調用比如跳轉、常用的接口請求比如查詢登錄態等進行封裝沉澱,加上語義化的描述,非研發人員可以將他們拖拽繪製成流程圖,完成業務邏輯的編排。兩者結合,就可以生產出完整的業務組件。而對於業務邏輯的組合編排,我們稱之爲邏輯編排。
我現在有個需求,需要實現一個簡單的抽獎活動。頁面分爲兩塊,第一塊是獎池,五個獎品橫鋪開,第二塊是抽獎按鈕,點擊按鈕,如果用戶沒登錄,拉起登錄面板,如果用戶已登錄,則進行抽獎並高亮展示對應的獎品。
我現在把 UI 撘出來了,就差邏輯來讓對應的坑位展示了,我們把欠缺的邏輯部分拆成兩段:
-
進入頁面,也就是 componentDidMount 階段,查詢獎池
-
點擊抽獎按鈕時,查詢是否登錄,未登錄拉起登錄面板,已登錄則調用抽獎接口
除了開始和結束以外的這種邏輯片段都是我們已經沉澱下來的,可以直接拖拽進來,繪製流程圖。繪製完了後,我們選擇模塊的 頁面主動觸發
(設計給非研發用的,他們肯定不理解 didMount),將它和 查詢獎品
關聯上;選擇抽獎按鈕的 點擊事件
, 關聯上 抽獎
邏輯,這樣模塊就能在不同的時機做正確的事情了。可能你還有點小疑惑,我查詢了獎品後,數據是如何映射到 UI 展示上的呢?要想知道如何映射,你得先知道邏輯是怎麼運行的。
下圖是我們在寒假戰役中的一個應用場景:
Logic is Core
扯一點遠的,不願意看可以直接跳到 “邏輯如何運行” 小節。前端的邏輯編排相比服務編排要更復雜一點,並不是說前端邏輯編排更難,而是因爲:場景更加多樣化,C 端模塊搭建和中後臺搭建的編排差異就很大;人員更加多樣化,除了服務研發人員還要服務非研發人員;除此以外,還需要和 UI 進行結合,可能性就更多了。
這段時間比較火的 iMove ,初始被設計服務優酷活動搭建平臺的邏輯編排,後面服務 imgcook 後,與優酷的邏輯編排在形態上已經截然不同,所以在前端比較難像服務編排一樣,平臺化、中心化然後去服務多種多樣的業務。如果你真的很想去服務多樣化的場景及業務,提供一個輕量級的庫,讓它足夠靈活、可定製,這也是 iMove 正在走的路。
前面講了很多,都是在說,邏輯編排需要 follow 不同的平臺做對應的定製,但是不管形態怎麼變,它的核心是不會變的,我們一定是在圍繞着 “邏輯” 去做包裝,將之打造爲不同的產品形態。所以,邏輯纔是核心!
邏輯如何運行
說了半天邏輯多重要,假使我現在抽離了一個函數出來,把它發佈了,我又用它拖了個流程圖,它怎麼才能執行呢?
邏輯最後想要運行,無論你是出碼 (to code) 還是在線執行,你都需要一個 “邏輯編排解釋器”,這個解釋器可以讀懂編排後的邏輯並且去執行邏輯,這個解釋器我們稱之爲 Runtime。代碼是冰冷的,解釋器是沒有智商的,它並不能真的讀懂邏輯,只是因爲我們約定了一個規則,定好了編排後的邏輯可能有幾種情況,每種情況應該如何做,Runtime 只是在依章辦事,而這個規則就是 DSL 。所以邏輯想要運行,它依賴於 DSL + Runtime ,要做一個邏輯編排平臺,也一定是先定好 DSL,實現 runtime ,後面纔是去做平臺。
每個沉澱下來的邏輯,我們將其定義爲邏輯元件,加上語義化的元件名稱及詳細的描述,然後發佈到元件市場。使用者只需要根據元件名稱來挑選需要的邏輯,通過連接線將他們連接起來,就可以組合成一個流程圖,這個流程圖也就是一段完整的邏輯。
可是流程圖導出來的 graph json 都是點和線的集合:
這種 json 如果直接用 runtime 執行有兩個問題:
-
流程走向不直觀,每個元件執行完後需要浪費時間去查找下一個元件是哪個
-
無效信息太多導致 json 體積太大進而影響加載性能,比如座標信息 (x/y)、標籤信息 (label) ...
所以我們需要先去約定 DSL ,保證足夠直觀且只包含必要信息,所以我們設計了一個轉換器將 graph json 轉換爲 DSL 。每個邏輯流程圖對應一個 DSL 產物,藉助 runtime 的 interpret
方法,它可能會運行在 useEffect 中,也有可能運行在某個元素的點擊事件中,或者是頁面的滾動事件中。
邏輯元件生產與消費的分工
邏輯元件從被編排到被運行的過程,也是它被消費的過程。文章到這裏,相信大家可以感受到這個消費的過程,非研發同學確實是可以進入的,因爲我們設計的足夠簡單。我們把整個頁面的生產當做一條流水線的話,以前是 模塊生產 --> 模塊市場 --> 頁面搭建,有了邏輯編排後,我們的流水線比之前劃分的更細了。
這張圖是產品進入到中期的一個形態,前期的時候產品的角色更多是由研發來承擔的,然後逐漸過渡到產品,到了後期呢,產研聯合給運營同學做培訓,中間這部分會逐漸的過渡到運營同學那邊。
因爲整條線上大家需要關注的事情越來越細了,頁面生產線出錯的幾率也會低一些。
邏輯編排
上面一直在從業務角度去聊,接下來就要深入到邏輯編排裏去了,講一講邏輯編排的設計思想。邏輯編排最主要是分爲三塊,元件、編排器、runtime,我將它們稱作邏輯編排三板斧。那問題就轉換成了:
-
元件該怎麼做?
-
編排器該怎麼做?
-
runtime 該怎麼做?
這三塊各自拆解出基本要素,用基本要素來描述它們,互相之間建立起連接關係,這些構成了邏輯編排的規範協議。這樣不管什麼業務進來,只要遵循這套規範,它們的底層就是一致的,各業務也不會顯得散亂。
DSL
在探討我們的 DSL 之前,一定要先聊一聊 DAG(有向無環圖),因爲我們的 DSL 設計本質上就是 DAG 。
DAG
DAG 的 G 指 Graph(圖),圖是數據結構中最爲複雜的一種,我們在瞭解 DAG 之前,回顧圖的幾個要點:
-
頂點 (vertices):圖中的一個點
-
邊 (edge): 連接兩個頂點的線段
-
度數 (degree): 從一個頂點出發有幾條邊,這個頂點的度數就是幾
圖就是由一些頂點和邊組成,邊就是頂點間的關聯關係。DAG 中的 D 是 Directed,代表是有方向的,就是說頂點之間的邊帶箭頭,常見的比如食物鏈,就是有向的;A 是 Acyclic,代表無環,從某個頂點出發,無論走那條路,都不會回到那個頂點。
基於有向無環圖來約定我們的 DSL 正合適不過:
-
我們需要有向邊來告訴我們邏輯的走向
-
流程從開始節點出發一定要遇到結束節點才結束
-
我們的邏輯元件可能有多個出口,就像頂點可能會有好幾個度,比如說判斷是否登錄,就算是 2 度
-
邏輯編排中我們沒法要求流程圖一定繪製成樹(一個頂點到另一個頂點,只有一條路徑)那樣
基於 DAG 的 DSL
每一個頂點都是邏輯元件的實例,繼承自邏輯元件,也可以修改自身屬性。邏輯元件分了兩大類 - 基礎元件和業務元件。基礎元件目前只有開始和結束,其他所有需要研發開發的都是業務元件,這麼設計是爲了降低編排使用門檻,你不需要有任何編程基礎。
-
type: 目前只有三種類型,
Start
|End
|Custom
-
func: Custom 類型專用,可以是函數體,可以是函數名,如果是函數名,需要提前在 runtime 中註冊
-
payload: 元件會開放配置項給運營配置,form 表單數據會作爲 payload 傳入給頂點對應的函數
-
next: 每個頂點都會有一或多個出度(出口),next 指向目標頂點的 uuid
跟 Flow-based programming 不一樣的是,我們移除了頂點之間的值的傳遞,運營不是研發,他們很難理解計算值如何流動以及如何操作它們。
Runtime
前面說到數據如何映射到 UI 時,不是故意賣關子,實在是要配合着 runtime 一起給您講解一下。邏輯與 UI 的結合這裏也只是粗略提一下,要留到後面的文章細講。
Runtime 與 UI
React 自身定位是用於構建 UI 的 Javascript 庫,它做的就是通過 data 驅動 UI 展示,react 的 data 通常都存儲在 state 中,我們剛纔已經通過邏輯編排拿到了獎品數據,對此 runtime 唯一要做的就是在內部 data 與外部 react 中間架一座橋。
將邏輯內部生成的數據全部存儲在內部 context 中,利用發佈訂閱模式,每當 context 有更新時,通知 UI 組件執行 setState,state 更新後,React 自動更新 UI。
runtime.subscribe('context', (val) => {
// handle data
this.setState({ data });
})
輕量的 Runtime
YOHO 的 runtime 很小,不到 10k,麻雀雖小,五臟俱全,接下里我們看看這小麻雀是如何支撐起流程編排的最後一公里 - “執行邏輯” ,又是如何和業務打交道的😊。
邏輯元件本質上就是基於一段代碼,給它加了一些描述信息,可是我的頁面中是不會內置這一段段代碼的,runtime 也不可能內置這些代碼,也就是說我們的執行上下文中是沒有元件對應的函數的;我們的業務是 No Code 形式,不出碼,所以 Runtime 內部實現了一個元件管理器,你可以通過他進行元件的註冊,在 DSL 執行過程中,它也會幫你進行元件檢查。
Runtime 對每一次編排實例(流程)的調用都是在沙箱中進行,實例的執行是互不干擾的,而執行過程中每個元件輸出的結果我們也做了隔離;其實我們還給元件之間互相通信提供了方法,但是目前不建議使用,因爲元件之間隨意通信有很大的副作用,我們目前還沒有足夠的產品形態去約束它。
在 runtime 設計過程中,我們預見業務會不斷地有各種需求進來,可是我們不希望 runtime 過於業務定製化,導致將來積重難返,所以設計了生命週期。在編排實例執行的各個階段,業務都可以進行干預。
使用發佈訂閱模式,而不是觀察者模式的原因,也是我們希望 runtime 足夠靈活。舉例來說,上下文因爲數據隔離,我們把它拆解爲各個子上下文,你可以把每一個 childContext 當做一個 topic 來訂閱,其他 childContext 更新並不會附帶影響當前的。childContext 。
除了以上,runtime 只做了一個事情 —— 邏輯走向的調度,就是有向無環圖中的 “有向”。正是基於以上的設計,我們的 runtime 足夠小,但是又足夠靈活,便於定製。
三板斧在本文中就先講這第一斧😄
後話
在做邏輯編排平臺的過程中,方案做過好幾版——從一開始兩週搞出 iMove 初版給老闆去展示,然後想要擁抱集團,和優秀的編排平臺 Logic Force 想着共建前端編排,因爲我們最終的產品形態差異過大,LF 想要支持也需要投入很大的精力,同時我們的業務想要快速驗證,所以又重新自研了一個輕量的邏輯編排平臺。期間有很多感悟,我們也都在邏輯編排的設計中添加進去了。
邏輯編排講到這裏還沒有結束,我們後面還會有系列文章,元件和編排器是如何設計的呀,和 UI 進行聯動,UI 側又是怎麼設計的呀,我們在邏輯編排方面又做了哪些前端特色的東西呀?如果你也感到好奇,敬請關注~
關注「Alibaba F2E」微信公衆號把握阿里巴巴前端新動向
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9fv4719rsxwrr5oimoUnZg