low code平台建設的一些設計和思考


此篇主要來自我自己在分享內部lowcode項目時的一些資料,有些格式因為直接復制過來有些錯亂,待整理和完善ing。

前言

國內已經有不少的低代碼平台:例如墨刀、雲鳳蝶、愛速搭等等,低代碼平台面臨的差異性問題主要有:

  1. 場景不同,配運營活動與配流程表單,使用的組件幾乎完全不同,若組件庫不夠用,還需要自定義業務組件;
  2. 用戶不同,運營同學希望簡單拖拽完成,研發希望二次開發能力,所以既要考慮面向非研發 零代碼,再考慮面向非前端研發低代碼,還有面向前端的pro code;
  3. 開發者偏好不同,有人偏向react開發,有人偏向vue,使用組件庫也不盡相同;
  4. 設計器要求不同,不同系統對設計器界面要求不同,面板能力也不同;
  5. 根據場景不同,有些需要一個局部表單,有些需要整個頁面;

由於以上某些原因,導致一些開源工具不能很好的在實際業務落地,很多時候就只能自己開發,或基於開源二次改造。

設計器畫布

待補充ing

DSL

概念

DSL是一個比較大的概念,在low code這里,我們把它限定在對頁面配置描述的信息記錄,這種記錄通常是json結構,由前端設計器生產,在編譯/渲染時被還原。一個好的DSL設計至少得滿足一下特性:

    1. 全面性,dsl記錄的信息得能完整完全還原頁面理想使用時的排版,樣式和交互
    2. 要有原子性。數據足夠原子,結構足夠清晰。一個大的DSL可能由幾組元dsl組成,每組元dsl可能又有幾個元數據組,最底層的元dsl就是一個基礎組件/標簽,這樣對取數據很友好,最好能達到用多少取多少的目的。原子性除了對組件划分scope,也可以對功能職責划分。
    3. 擴展性強,如果想增加功能,新輸入的dsl描述能夠不影響現有功能下插入舊dsl,且不能增加冗余。

方案

基於以上,我提出一種MVC的DSL設計方案。view層存儲頁面組件的基本描述信息,比如包含這個組件的類型信息,樣式信息等,且描述是無“狀態”的,類似你可以認為就是視圖層。;至於這個組件運行時要發生的交互行為和動態數據等放在Model層;交互行為和組件之間的聯系映射關系數據放在Controller層;

如此,在view層我們可以詳盡用信息來描述一個組件的樣子,用嵌套結構來表達組件的層級關系和原子包含關系。舉一個場景,如果在平台上我們只想預覽實時的頁面排版效果,這時候就只用取view層的dsl數據即可,model組件運行時的交互表現數據我不關心所以就不用取,這樣就不會給其他模塊和步驟造成冗余的處理和計算。

 

例子

舉一個情景例子:

 

 

 

 

如圖一個表單,從視圖上看它位於一個頁面的某個位置,包含一個選擇框組件和一個輸入框組件,並排展示,從功能上看,下拉框只有在選擇id的時候id輸入框才會顯示。對於這樣一個功能,id輸入框在MVC三層記錄的dsl描述應該分別為:

  view層:描述當前這個邏輯規則的靜態信息,即描述當下拉選擇框的值===id的時候,id選擇框要顯示,注意這層數據是“靜態”的,只是對配置和規則的描述,在設計器中的表現可能就是給組件配置區域信息回填和展示。

  model層:放着這這個動態數據的運行時的過濾條件,可能是一個函數,這個會與狀態管理設計有關。

  controller層: 把計算后的結果/動態數據傳遞給view層

模型

上面說的還是比較抽象,具體怎么實現這個設計,比如如何傳遞,組件狀態如何管理,如何響應式等等;這里我們可以借鑒mobx/vueX的這種響應式狀態管理方式,和redux這種合成/傳遞規范。這里再引進幾個偏前端的字段概念,layout,store,mapper,來分別對應view,controller,model層的概念。

基於此,我們可以先設計確定store dsl的幾個字段,

  a. state,統一的存放組件動態數據的地方

  b. action,組件運行時調用的方法,由此可以擴展組件的交互能力

  c. mutation,對state的原子性操作,改變state的規范方式

  d.getters,對state操作的的組合/計算屬性方法。

  e.context,擴展屬性,聲明 Store 內部的各種常量、工具方法及三方庫的注入,以及運行時相關的變量方法。 

 

在上面選擇框輸入觸發輸入框顯示隱藏例子中,此模型下就可以描述為:

layout:

 

store:

 

mapper:

state是要改變的store的值的聯系

 

 

編譯能力

概念

  編譯過程做的就是將上圖中dsl編譯能用的代碼片段。編譯過程和渲染過程對於low code平台來說是是必須同時存在至少一個的。編譯如果是輸入代碼片段在瀏覽器里執行渲染過程,這時候的渲染過程邏輯相對少一些,可能做得更多的只是對業務的包裝和處理。如果沒有編譯過程,那么渲染過程就要處理dsl問題,渲染過程抽出來還能解決實時預覽的問題。

  對於編譯輸出代碼片段的過程,希望能夠分成兩個過程,第一個過程是把原始的dsl加工成框架組件代碼和數據代碼,第二個過程則是根據中間的某種聯系規則,能夠將二者合並成更成熟的代碼片段。

 

方案

編譯代碼片段

  針對視圖層的編譯,在dsl的廣搜和深搜中,重要的一個環節除了還原dsl 標簽tag數據到組件標簽,還有就是把每層dsl數據的關鍵信息key加在組件標簽上,后面傳遞數據時,就通過props的key一一對應。

  還有就是針對一些一些擴展的方法,統一掛在根組件上, 組件庫內部通過mixin調用

 

  針對數據層的編譯,因為數據都是通過props傳入,所以在編譯成目標數據代碼片段的時候,需要同時收集dsl的靜態數據和動態數據,最后通過動態數據優先級更高的方式覆蓋一起傳遞。

  針對映射層的編譯,我們要做的就是數據關系的合成和交互關系的合成,數據關系映射這里的key就是我們的組件key,對於value,先解構所有的靜態數據,后面再把動態數據插入覆蓋,最終通過所有的數據key和視圖層的props名一一傳遞對應。

dsl:

  編譯后代碼片段:

 

  中間還有一個比較有爭議的操作就是,dsl里的靜態數據是不是在編譯過程中就在視圖層合成了,比如樣式,在視圖層編譯的時候就把對應的樣式屬性還原成style樣式,而不是作為數據再編譯合並到數據代碼片段(比如store文件)里以props數據key形式一一傳遞。這樣看上去一個好處貌似是做了動靜的區分,但在數據流傳遞上不夠純粹(store)。

 

狀態合成

  編譯過程的重要的第二個操作,就是如何真正的利用這層映射關系把數據傳遞給組件內部。

  這里差不多就是要解決兩個問題,一個是store數據怎么傳遞,第二個就是action動作怎么引發rerender。

  第一個問題我們可以接收上述store片段的的state和action,將兩個js合成為一個HOC組件,根據映射關系,將state的數據保存在HOC的data里,再根據props傳遞。

  第二個問題是因為store只是一個靜態數據結構,需要我們包裝成有生命周期的響應式對象,我們可以給store包裝成發布訂閱模式的類,store的所有數據存在構造器里,實現一個訂閱函數,定義好action觸發的數據流,比如action只能由commit去觸發原子操作,每次操作時,依次執行訂閱器里存儲的函數。然后我們在HOC組件的掛載時期去訂閱該組件的setData操作,setData會重新根據映射關系讀取賦值HOC的data,如果有變化,HOC的子組件的props傳遞就會引起rerender。(當然中間就可以做componentShouldUpdate之類的優化操作)

 

store類

state: store.getState(),
commit: store.commit,
dispatch: store.dispatch,
getters: store.getters,

context: store.context

 

HOC內部:

  在HOC內部訂閱了hoc的setState方法,那么每次觸發交互事件commit的時候都會setHOC的data,就會引發響應式渲染。

action擴展

映射關系:

  要增加的action方法我們可以在設計器創建工具初始化schema的時候就創建和插進去。要插入的這些方法,默認帶有{ state, commit, dispatch, getters, context } 和config參數等。第一個對象參數是在HOC合成的時候,默認給所有的action方法注入的,第二個參數是組件庫中組件真正執行的時候傳遞的參數,這里面可以傳入組件庫的this等東西。

這塊既然講到擴展,其實能引起交互的變化,我們統稱為事件,這塊我們想到的有事件處理的幾種設計:

  • 彈窗的這種交互,在action里動態改變彈窗的顯隱相關屬性
  • 表單查詢,通過觸發targetID,傳遞給組件信息和type 
vm.form.handleActionBefore({ actionType: "formSearch", targetWidget: item.options.target })

 

  • request請求,外部的請求方法,要emit的方法等,這種場景就是編譯生成的組件外面如果還有渲染能力包裹了組件,那么在編譯HOC狀態合成的的時候就要注意把這層props再傳遞下去。(幻視當前缺失的,因為幻視輸出的是頁面,就沒有考慮編譯生成的組件外面還有這層組件的這個需求)

 

工程編譯

  上面一般編譯完了之后只會輸出個別幾個關鍵js片段或者文件。我們可以提前准備一個模板工程,每次編譯除了輸出上面信息,還拷貝這個模板工程並和輸入的js片段合並,形成一個真正的前端工程。在這個模板工程除了方便壓縮打包,還可以放一些公共樣式,版本信息,預覽信息(方便用戶下載工程代碼直接調試),存放node_modules(拷貝的工程的node_module都軟鏈到模板工程的node_module,實現node_module共享一份,節省空間)

 

 

渲染能力

  如果沒有編譯能力,不需要打包流程,那我們做的渲染包可能就是一個adapter適配器組件。

  上面編譯的第一步輸出的就是就可以輸createElement的那幾個參數信息,處理和第一步處理一樣

  狀態合成的操作還是得做,最終狀態合成后導出的就是一個組件,然后最后通過動態組件<component is > 來顯示這個組件。沒有編譯過程只要渲染過程其實就是沒有編譯能力中的工程編譯過程。無編譯流程的渲染過程其實就是把dsl->template->connect->組件變成了dsl->connect->組件。

  在現有dsl的這種設計下,這些處理避免不了,無法避免。業界formily的思路是一樣,基於MVVM模式,只是他們基於mobx自己實現了一個狀態管理庫。

   epage的渲染思路,就是每個widget 操作自己的schema就可以了。

{
  "key": "kuEXemrCZ",
  "widget": "grid",
  "hidden": false,
  "option": {
    "gutter": 0,
    "align": "top",
    "justify": "start"
  },
  "style": {
    "margin-right": "auto",
    "margin-left": "auto",
    "background": [],
    "container": {
      "background-color": "",
      "background": []
    }
  },
  "label": {
    "width": 80,
    "position": "right",
    "colon": false
  },
  "container": true,
  "children": [
    {
      "span": 24,
      "list": [
        {
          "key": "kab0Cd9ZP",
          "widget": "select",
          "hidden": false,
          "option": {
            "type": "static",
            "url": "",
            "adapter": "return data",
            "dynamicData": [],
            "data": [
              {
                "key": "A",
                "value": "A"
              },
              {
                "key": "B",
                "value": "B"
              }
            ],
            "multiple": false,
            "clearable": true
          },
          "style": {},
          "name": "kab0Cd9ZP",
          "type": "string",
          "label": "下拉框",
          "description": "",
          "help": "",
          "disabled": false,
          "rules": [
            {
              "required": false,
              "message": "必填",
              "trigger": "change",
              "type": "string"
            }
          ],
          "placeholder": "請選擇",
          "default": ""
        },
        {
          "key": "k5G7mDbx3",
          "widget": "button",
          "hidden": false,
          "option": {
            "text": "提交",
            "type": "primary",
            "icon": "",
            "long": false,
            "ghost": false,
            "shape": "square",
            "script": ""
          },
          "style": {},
          "disabled": false
        }
      ]
    }
  ],
  "title": "",
  "description": "",
  "size": "default",
  "logics": [
    {
      "type": "value",
      "key": "kab0Cd9ZP",
      "action": "=",
      "value": "1",
      "relation": "or",
      "trigger": "prop",
      "script": "",
      "effects": [
        {
          "key": "k5G7mDbx3",
          "properties": [
            {
              "key": "hidden",
              "value": true
            },
            {
              "key": "disabled",
              "value": false
            }
          ]
        }
      ]
    }
  ],
  "store": {
    "dicts": []
  }
}
View Code

  epage渲染還原創建組件的時候,會同時遍歷logic數組邏輯,logic記錄了動作組件和觸發組件的key,在還原觸發組件的時候,給觸發組件標簽上加disable和給這個組件加上相應的disable和v-if標簽,disable和v-if依賴的數據流存在vuex里,動作組件 都是通過onchange事件去連接vuex的數據流。

  一個簡單的遞歸渲染demo:

const render=(schema,params)=>{
  schema=Array.isArray(schema)?schema:[schema];
  const dom=schema.map((item,i)=>{
    let {type,props,children}=item;
    type=(type||'div').trim();
    const first=type.charAt(0);
    type=first.toUpperCase()===first?(components[type]||'div'):type;
    props={
      key:i,
      ...formatProps(props,params),
    };
    children=Array.isArray(children)?render(children,params):[formatChildren(children||props.children,params)??null];
    return createElement(type,props,...children);
  });
  return dom;
};
 

 

<template>
  <div class="opes-form">
    <component
      v-for="(formItem, index) in FormItems"
      :key="index + (formItem.gid ? formItem.gid : Math.random())"
      :is="formItem.type"
      :info="formItem"
      :taskId="taskId"
      :nodeId="nodeId"
      :processInstanceId="processInstanceId"
      :extraData="extraData"
      :commitInfoList="commitInfoList"
      @msg="$_msg(arguments[0], index, arguments[1])"
    ></component>
  </div>
</template>
View Code

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM