一種關于低代碼平臺(LCDP)建設實踐與設計思路

      后臺-系統設置-擴展變量-手機廣告位-內容正文頂部
       
       
      背景  
       
      負責菜鳥商業中心CRM系統開發已經有1年多時間,過程中發現有一個痛點:業務線特別多,每個業務線對同一個頁面都有個性化布局和不同的字段需求,而我所在的團隊就3個人,在資源有限的情況下如何支撐好呢?剛開始,我們是為各業務線單獨定制頁面和業務邏輯,1到2個業務線還能應付過來,目前已經發展有十幾業務線,且每個業務線下還有子業務線,這種個性化的開發多了,工作量就大了,系統維護壓力就巨大。所以就孕育而生了—— 銷售魔方類低代碼產品,與其說低代碼產品,還不如說是一種解決千人千面的個性化業務搭建的前后端一體的解決方案。
      本文就降本的情況下,我是如何低成本構建產品能力去支撐多條業務線、多租戶,我先以小實踐成果展示,再過度分享我后續升級的設計思路。
       
      什么是LCDP  

       

      低代碼開發平臺(Low-Code Development Platform)是無需編碼(0代碼)或通過少量代碼就可以快速生成應用程序的開發平臺。通過可視化進行應用程序開發的方法(參考可視編程語言),使具有不同經驗水平的開發人員可以通過圖形化的用戶界面,使用拖拽組件和模型驅動的邏輯來創建網頁和移動應用程序。低代碼開發平臺(LCDP)的正式名稱直到2014年6月才正式確定,整個低代碼開發領域卻可以追溯到更早前第四代編程語言和快速應用開發工具。

       

      魔方核心能力  
       

      產品能力

       
      上圖是魔方1.0 MVP版本基本運行原理,以及上線后降本增效的數據,業務開發從60人日縮短到20人日,年省成本180人日。  
      以上版本基本滿足了80%以上的業務個性化需求自閉環開發。
      還有一些小問題,基于這個版本,我們又不斷的升級,提升產品體驗、能力提升和業務覆蓋。
      后續我們可做到新頁面上線,只需5分鐘,新增字段無需模型變更和無需java代碼發布,復雜頁面前端也能做到0代碼。
      基于我們業務的訴求,所以銷售魔方需具有以下幾個核心能力

       

      • 頁面的千行千面(千人千面),包含同一個頁面不同布局、不同字段、不同樣式

      • 數據模塊的千行千面(千人千面),根據不同身份執行不同的業務技術邏輯和服務編排

      • page一鍵創建,在沒有新的業務組建和新的module情況無需開發接入,0代碼上線,運營同學自行配置頁面。

      • 前端組件復用,在沒有新前端組件,前端無需參與開發,后端只需編寫module對應的業務接口。

      • 實現module可復用,module數據渲染、數據寫入,查詢條件、浮層、半推頁面、頁面操作

      • 新增字段擴展0代碼,模型字段可以自定義,動態擴展,可定義來自本地數據庫、遠程HSF接口數據

      • 環境可隔離,測試、預發、生產

      • 平臺和業務代碼分離,業務上線只需關注業務邏輯本身的代碼。

      • DO DTO可定義,動態映射

      • 數據枚舉動態定義,動態綁定

       

      魔方的設計  

       

      產品界面

      先展現一個實例配置界面,有個體感
       
      ??
      ?  通過以上配置可以個性化配置頁面輸出的布局和字段,動態輸出個性化頁面
       

      用戶

       
      運營:  運營可以根據自己的業務身份,定義獨有的page實例,自由選擇頁面的版塊和需要展現和編輯的字段
      產品:  配置初始化頁面,module、以及全量字段池
      技術:  定義元數據,module,編寫module group邏輯執行單元代碼
       

      產品模塊

       
      ?
        
       

       

      核心邏輯

       
      一般低代碼平臺,主要分為兩部分,前端頁面的渲染和后端服務接口綁定(服務編排等)。
      大的邏輯差不多,因為我這個主要還是具有行業特色的類低代碼產品,所以是緊扣行業特殊性構建。

       

      前端渲染

       

      因為我負責的是后端,所以前端我就不過多敘述,大概的邏輯如下兩張圖,大致意思是前端的頁面渲染就如同做菜一樣,用戶根據自己的需求,可選擇菜譜、食材和烹飪方式,不用關心烹飪過程,也不用自己親自烹飪,都叫由廚師烹飪,廚師會根據你提供的菜譜和食材做好,最后將美食給你端上桌。
      同理,前端渲染引擎會根據數據協議、組件庫、渲染方式,動態渲染成頁面,如果有業務數據將會動態綁定。
      ?

       

      后端綁定

       

      我們這有個特殊性,頁面是通過后端給定schema,由前端根據這個schema進行頁面渲染。后端通過識別出用戶的身份,通過接口輸出給前端千人千面的個性化schema,前端就通過schema配置動態去渲染。
      這樣就能實現我們說的同一個功能頁面,不同業務身份展示不同的布局和字段。
      同時,還會會有一個業務數據接口,用于綁定前端頁面填充業務數據或提交表單數據。
      每一個組件綁定業務數據接口后,就不是單純的前端組件了,就具有行業業務屬性的組件和頁面,這樣在這個領域是可以被業務系統直接復用,無需重新編寫業務代碼。
      這里有一個難點,每次新增業務線(租戶)后就有新增字段需求,而且字段的差異還挺多,約占80%,在不修改模型的前提下,如何做到?
      帶著這個問題可以先思考下。
      一般解決方案有以下幾種:
      1、通過設計縱表,以擴展字段。缺點就是查詢復雜度高。
      2、元數據驅動。缺點就是成本高,架構更加復雜
      3、通過定義一個特殊feature字段,存儲類型為json,約定好協議,擴展字段按照不同行業存儲在json內。優點就是輕量。
      根據我們自身的定位和資源情況,最終選擇的是第3種方案。
      這里有個擔心就是json數據方便搜索嗎?性能如何?
      mysql5.7上,在相同數據量的情況下,虛擬索引和普通索引查詢效率基本一致 在大數據量情況下不推薦用聚合函數計算json數據,但是如果json key建立了虛擬列,可以對該虛擬列進行聚合操作,效率跟普通列一樣。  ?
       

      模型設計

      ?
      整個模型可以看出,從左往右:
      DO的定義、DTO的定義、module定義(等同VO定義)、原始頁面定義(originalPage)、實例化頁面配置(instancePage)  
      這個過程基本描述了一個頁面是如何定義出來的,以及動態模型的擴展(模型驅動),從模型到頁面組件的布局定義。
      行業個性化配置則通過對OriginalPage實例化成InstancePage而得。這樣就能動態生成千行千面。
      OriginalPage到InstancePage這里借鑒了java的思想,類似Class和實例對象。
      渲染頁面邏輯  
      頁面在渲染的時候,只需兩個接口:
      1、實例頁面數據結構 ;2、業務數據。  
      前端根據兩個接口分成兩步渲染:
      1、初始化頁面布局;2、填充業務數據。
      這個借鑒了Spring容器啟動時的設計思路,類似實例化、初始化,屬性數據填充。
       

       

      template定義

       
      即是對頁面類型的定義

       

      • 列表頁面
      • 詳情頁面
      • 半開頁面
      • 表單提交頁面

       

      ?
       

      page定義

       
      定義一張前端頁面,分成兩個階段:origin;instance。
      origin是定義的原始頁面,可以理解成java 的Class類,可以構建多個實例頁面。
      instance page是最終渲染的運行態。
      頁面結構:一個page 由N個module組成、一個module由N個field組成。如下圖
      一個實例page由以下維度定義  
      1、用戶傳入參數
      page_code
      custom_dimension
       
      2、系統獲取參數
      biz_code
      sub_biz_code
      enviroment
       
       

      module定義

       
       
      頁面的組成單元,一個頁面由多個module組成。代表頁面顯示區域單元,有多個前端組件組成,是頁面容器的布局單元。
      module_code 全局唯一,實例化后的module可被復用、重寫、實現多態。
      modules 數據結構為一個 B+tree,只有葉子節點是有具體的實體數據  ?  ?
       
      ?
      葉子節點是真正包含具體的字段和屬性配置

       

      module_type 定義

       

      對module類型的定義
      1、主列表查詢模塊 MAIN_LIST_MODULE
      2、導出模塊 EXPORT_MODULE
      3、彈出頁面模塊 FLOAT_PAGE_MODULE
      4、搜索條件區域 SEARCH_ARE_MODULE
      5、子列表查詢模塊 SUB_LIST_MODULE
      6、編輯表單模塊 EDIT_MODULE
      7、信息平鋪呈現 DISPLAY_FLAT_MODULE

       

       

      McubeContextAware

       

      module容器代碼定義
       
      @Component    public class McubeContextAware implements ApplicationContextAware {    
             private static volatile ApplicationContext alc;    
             @Resource        private ModuleBeanFactory moduleBeanFactory;    
             @Resource        private ModuleGroupBeanFactory moduleGroupBeanFactory;    
         
             @Override        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {            alc = applicationContext;        }    
             @PostConstruct        public void init(){            setModuleBeanMap();            setModuleGroupBeanMap();        }    
             private void setModuleBeanMap() {    
                 Map<String, McubeModuleExecutor> beanMap = alc.getBeansOfType(McubeModuleExecutor.class);            if (beanMap != null) {                beanMap.values().stream().forEach(m -> {                    McubeModule module = AnnotationUtils.findAnnotation(m.getClass(), McubeModule.class);                    if (module != null) {                        String code = module.code();                        String name = module.name();                        if (code != null) {                            moduleBeanFactory.getMcubeBeanMap().put(code, m);                        }                    }                });            }    
             }    
             private void setModuleGroupBeanMap() {    
                 Map<String, McubeModuleExecutor> beanMap = alc.getBeansOfType(McubeModuleExecutor.class);            if (beanMap != null) {                beanMap.values().stream().forEach(m -> {                    McubeModuleGroup module = AnnotationUtils.findAnnotation(m.getClass(), McubeModuleGroup.class);                    if (module != null) {                        String code = module.code();                        String name = module.name();                        moduleGroupBeanFactory.getMcubeBeanMap().put(code,m);                    }                });            }    
             }    }    
       

      執行單元(moduleGroup executor)

       
      為了保證頁面的數據填充效率,所以并不是一個module綁定一個服務接口,
      而是一個執行單元對應一個或多個module,負責多個module的數據渲染和數據寫入,moduleGroup executor是一個頁面計算單元。
      通過moduleCode動態路由對應的module group,執行相應的計算單元。
      每個module 執行單至少都包含讀、新增、編輯和刪除接口
      頁面上的每一個module就自動綁定了后端的業務接口,實現了前后端一體化搭建。  

       

      /**     * Created by hzliuxuan on 2022/5/27.     * @author hzliuxuan     * 模塊接口     */    public interface McubeModuleExecutor<T,V> {                  /**          * 填充數據,頁面渲染,一般是read接口          * @param value          * @return          */         T populate(V value);         /**          * 編輯模塊    
               * @param value          * @return          */         void edit(V value);    
              /**          * 寫接口          * @param value          * @return          */         void add(V value);         /**          * 刪除接口          * @param value          * @return          */         void delete(V value);    }    
       
      McubeModuleGroup  
      module執行組注解定義
       
      @Inherited    @Component    @Target({ElementType.TYPE})    @Retention(RetentionPolicy.RUNTIME)    public @interface McubeModuleGroup {        /**         * moduleGroup code (必填,唯一標識)         */        @NotNull        String code();        /**         * 對應module code值         */        @NotNull        String[] moduleCodes();        /**         * moduleGroup name         */        String name();    
         
         
         
         
             @NotNull        ModuleGroupType type();    }  

       

      field定義

       
      一個module對應多個field。
      如果要支持動態擴展,module需要對應一個實體模型。
      module只代表一個VO層的部分顯示片段,要想達到字段可以動態擴展需要定義一層實體模型的映射關系,這樣才能找到統一的feature對象去解析,完成DO、DTO、VO的相互自動轉換。
      當module需要動態擴展的時候,從實體模型中去選擇已經定義好的field。
      因為我們的VO也是動態生成的,這樣就不需要因為新增一個字段而進行模型變更或者代碼發布。即實現0代碼上線。
       

       

      field數據結構定義

      [

            {
              "key": "equityInvestment",
              "value": null,
              "label": null,
              "name": "權益投放記錄",
              "text": null,
              "width": null,
              "lock": null,
              "copyEnable": null,
              "copy": null,
              "sortable": null,
              "tooltip": null,
              "wordBreak": null,
              "fieldMapper": null,
              /**數據類型取值input, select, date, address(地址), switch(開關), staffSelector(花名選擇),textArea,upload(上傳)**/
              "dataType": "input",
              private String dataType;
              "format": null,
              "dataSource": null,
              "dataUrl": null,
              "required": null,
              "unit": null,
              "readOnly": false,
              "isHidden": false,
              "multiple": false,
              "features": null,
              "showTime": null,
              "maxLength": null
            }
          ]
         
      

      page 數據結構

         public class McubePageBeanDTO {        /**         * 頁面編碼         */        @CrmOperateLogBizCode        private String pageCode;        /**         * 業務線         */        private String bizCode;                /**         * 配置類型         */        private TemplateTypeEnum templateType;                /**         * 配置模塊         */        private List<McubeModuleBeanDTO> originalModules;        /**         * 配置字段         */        private Map<String, List<McubeField>> originalFields;                /**         * 實例的模塊         */        private List<McubeModuleBeanDTO> instanceModules;    
             private List<String> instanceModulesList;        /**         * 實例的字段         */        private Map<String, List<McubeField>> instanceFields;    
             private String subBizCode;        /**         * 元頁面version         */        private Byte originVersion;        /**         * 實例version         */        private Byte instanceVersion;        /**         * module version         */        private Byte moduleVersion;                /**         * 屬性集合         */        private List<Property> properties;    
         
         
             ///**        // * 顯示的模塊        // */        //private List<String> instanceModulesList;    
         
             private Boolean isCache;    
             @Data        public static class Property {            /**             * property             */            private Boolean checkable;            private Boolean isEdit;            private Boolean selectable;            private Boolean isLeaf;            private Boolean isAdd;            private Boolean isDelete;            private String showType;            private Integer level;            private String extendedField;    
             }    
         
         
         
         }    
       

       

      page渲染運行時序圖

       

      ?
       
       

      運行時類設計圖

       
      ?
       
      每一個模塊背后都會綁定一個 moduleGroup executor ,業務開發只需通過對這個executor實現,即可快速完成開發上線,整個過程無需前端參與。簡單的字段添加也無需發布上線,我們會通過動態擴展映射背后的DO擴展。
      ?  ?

       

       

      總結

       

      ?
      好了,整個魔方的產品設計到這里基本描述完。
      整個產品我理解更多的是貼近業務而產生的一種前后端一體化,低成本快速構建方案。如果要做大,做全,可以參考salesforce元數據驅動模型。
      模型關系圖:  ??
       
      ?
      可參考:  https://www.infoq.cn/article/rwstpgujoxxuw9tlm88t  ?
      salesforce官方架構文檔:  https://www.developerforce.com/media/ForcedotcomBookLibrary/Force.com_Multitenancy_WP_101508.pdf  ?
      salesforce 已經超脫了模型驅動,下探到元數據模型驅動架構 Metadata-driven Architectures。
      優點很明顯,就是真的強大,業務都不需要建任何表,想怎么擴展模型就怎么擴展,此架構一般適用于與SAAS產品。
      缺點也很明顯,完全失去業務語義,開發成本和維護成本高,需要一套強大的sql管理和解析、實時的ETL數據架構、檢索能力,后期成本也非常大。
      故不適用我這個業務團隊采納的方案,但這套設計方案也給我打開了些思路。
      魔方這套方案搭建工時約為50人日,兩個后端加一個前端,解決了十幾個業務線多租戶的個性化接入,一定程度實現了模型驅動,千人千面的能力,我認為是典型小投入大產出吧,希望對正遇到同樣問題的同學有一定的幫助和啟發,限于個人能力,最終要搞大的需要有專業的團隊支撐,也歡迎指正和探討。

      文章來源:阿里開發者
      作者:劉玄(玄哥)

      未經允許不得轉載:RPA中國 | RPA全球生態 | 數字化勞動力 | RPA新聞 | 推動中國RPA生態發展 | 流 > 一種關于低代碼平臺(LCDP)建設實踐與設計思路

      后臺-系統設置-擴展變量-手機廣告位-內容正文底部
      主站蜘蛛池模板: 富宁县| 芦溪县| 通渭县| 桃江县| 永仁县| 双牌县| 蕉岭县| 兴城市| 通许县| 南宁市| 乌海市| 荣昌县| 高尔夫| 洛浦县| 慈溪市| 宝应县| 乌海市| 于田县| 贞丰县| 称多县| 镇巴县| 洪湖市| 政和县| 界首市| 古交市| 左贡县| 蓬安县| 青州市| 东光县| 犍为县| 营口市| 盐源县| 河北区| 鱼台县| 大庆市| 甘德县| 酉阳| 龙川县| 临潭县| 邢台县| 陵川县|