企業級自定義表單引擎解決方案(八)–表單模型管理
- 2021 年 12 月 22 日
- 筆記
- Core, 企業自定義表單引擎解決方案, 自定義表單, 表單引擎
這段時間陸續收到一些小夥伴的資訊,對流程引擎和自定義表單比較感興趣,內心還是比較欣喜的。多數人還是對elsa實現的流程引擎比較感興趣,要源碼,這部分內容原本是有打算把源碼開源出來的,但後來發現elsa的版本升級到了2.0之後,與之前的程式碼相差比較遠,要重構的話,前後端需要改很多東西,elsa1.x的流程流轉核心部分程式碼設計得還是比較巧妙,能滿足各種審批業務的變化需求,自己對核心部分的程式碼做了一些擴展,所以暫時沒有升級的打算。
自定義表單部分的文章還是繼續往下面寫吧,這部分文章都是偏設計方面的,真正想做低程式碼軟體架構方面的設計開發多少都有一些益處,整體工作流+自定義表單再整合前後端框架從前後設計技術研究及程式碼實現差不多前後花了一年左右的時間,當然都是工作之餘的時間寫的。
之前介紹的自定義表單中的視圖定義為單一功能的封裝,比如列表視圖(定義普通查詢區域,高級查詢區域,列表操作按鈕區域,行操作按鈕區域,分頁控制項區域,列顯示區域等)或者表單視圖(封裝表單行列定義,表單驗證等)等,都是具體某個特定功能的實現。而這裡介紹的表單模型,則把它定義為一個容器,容器裡面會進一步定義行列,容器裡面可以包含容器或者表單,每一個頁面會定義唯一一個最外層的表單容器,我們可以把它看作根容器,這樣就整體形成了一棵樹,根節點就是最外層的表單定義,樹的節點可以是子表單、視圖、表單行、表單列、視圖行、視圖列、視圖控制項等,整體就可以構造出一樹龐大的樹。
自定義表單最終會轉換為一棵樹,樹的話就會有樹的特性,樹上的每一個節點,都可以構造一個唯一的Code和PId,自定義表單中的樹節點還會擴展出它屬於哪個視圖或者哪個表單的屬性,那麼這裡就是引申出子表單子視圖,父表單父視圖的概念。有了樹模型的定義,那個後面絕大多數內容都是圍繞樹模型來實現的,前端在渲染介面的時候,根據樹節點一層一層的渲染介面,渲染介面的同時,將每個節點的Code和PId,節點屬於哪個表單或者視圖都會賦值到每個樹節點控制項中,有了樹模型的定義,那麼規則引擎就有了理論支撐,介面中的任何一個事件,都可以定義規則來實現自定義的邏輯(比如點擊列表視圖的行編輯按鈕,彈出編輯人員子表單,則大致的規則引擎執行邏輯為:找到列表視圖特定行編輯按鈕所在的列表視圖,在列表視圖中找到編輯人員子表單,把當前行的Id欄位取出來作為參數,用模態對話框彈出子表單,用Id欄位執行後端方法獲取單條人員數據,將人員數據綁定到人員表單中)。
表單模型沒有具體的功能,它的作用是一個容器,它充當視圖與視圖之間交互的橋樑的作用,當然是通過規則引擎來串聯起來的,另外表單也是頁面的入口與快取的存儲數據的入口。
表單的資料庫設計:
設計說明:
表單模型拆分為表單主表、表單項、表單行、表單列,關係都為1:n。常見的表單項只有一個,但像Tab布局或者有先後步驟的Step布局則會有多個。
表單主表關鍵欄位說明:
- Version(版本):每一次修改表單的任何資訊(包括關聯的數據),都會重新生成一個版本號,瀏覽器存儲表單資訊,每打開一個頁面,會將本地表單版本和視圖版本傳遞到服務端比較版本號,如果版本號發生變化,重新請求表單數據(一般系統交互後,視圖及表單定義資訊很少會發生變化)。
- FormType(表單類型):分為常規表單、Tab表單、Div表單等,前端根據此類型找到實現定義好的控制項渲染。
- PropertySettings(表單屬性):存儲前端的一些樣式,前端渲染時,讀取屬性並應用到控制項中,一般需要結合具體使用的前端框架設置。
- RelationInfos(關聯資訊):表單可能會關聯其他表單或視圖,比如彈窗,行存儲的視圖等,這個欄位資料庫不存儲,通過動態計算出來放入快取中。
- Rules(規則):定義表單的規則,將規則資訊冗餘序列化存儲到此欄位,規則有改動時,會同時更新此欄位(表單會冗餘存儲比較多的內容,這裡的規則為一類,主要是為了以最快的速度讀取表單相關數據,只需要表單Id訪問一張表即可獲取所有的數據)。
- WrapInfos(表單包裝器):前端在渲染視圖時,如果有包裝器,會用包裝器包裝視圖之後再渲染,常見為彈出框的功能封裝。
- FormItems(表單項內容):將表單項、表單行、表單列全部讀取出來序列化冗餘存儲到此欄位,同樣是為了讀取效率。
- IsTemplate(是否為模版):將一些典型的業務定義為模版,同樣存儲在表單中。
表單項、表單行:
- 對應物理結構的劃分,欄位比較好理解。
表單列:
表單列可以存儲單個控制項、子表單、子視圖等
- ColType(列類型):可以是控制項、視圖或者表單
- PropertySettings(列屬性):存儲前端的一些樣式,前端渲染時,讀取屬性並應用到控制項中,一般需要結合具體使用的前端框架設置。
- ComponentName和ControlSettings(控制項名稱和控制項設置):如果列類型是控制項,則為控制項名稱與控制項屬性,前端找到對應的控制項渲染。
- ObjId(對象Id):表單或者視圖Id,前端渲染時,根據此欄位找到具體的表單或者視圖。
- WrapConfigs(包裝定義):顯示到表單中的子表單或者子視圖的渲染封裝(表單和視圖可以用到任何需要的地方,相當於在用的地方再次進行樣式封裝,比如用Box樣式再次封裝子表單)。
快取設計簡單介紹:
自定義表單是典型的修改非常少,訪問非常平凡的,系統的每一個功能都需要讀取自定義表單的定義資訊。為了使自定義表單不影響性能,這裡採用了雙重快取設計,瀏覽器每訪問一個頁面,都會將表單和視圖的定義資訊存儲到瀏覽器本地資料庫中(IndexDb),應用程式後端將表單和視圖的定義資訊全部放到應用程式記憶體中,且將表單或視圖的相關資訊以欄位冗餘的方式存儲到特定欄位中,任何資訊的改變都會重新生成新的版本號並清空記憶體中的快取,前端請求頁面只,會帶上瀏覽器本地存儲的表單和關聯子表單子視圖版本號與伺服器版本號對比,版本號不同時,刷新瀏覽器快取數據,再渲染頁面。分散式部署中就存儲快取一致的問題,後面單獨寫文章來整體講解快取這塊的實現。
表單模版:
自定義表單本來就是要解放繁瑣的低效編碼問題,但是要把一個表單配置出來,還是會花費比較多的時間,且需要對這套表單引擎比較熟悉,配置同樣比較繁瑣且低效,那麼我們同樣可以採用自定義表單的思路,將常見的業務封裝為模版,(比如對單一表單進行的常規列表和表單操作,也就是最常見的CRUD操作。或者一對多表單,列表展示主表數據,點開一條件主表數據,對話框顯示主表數據及子表列表,對子表列表進行操作等),只需要動態渲染不同的地方,那麼就能夠實現只需要設置幾個簡單的參數,就能夠自動的生成自定義表單出來,這裡的不同地方無非就是Object對象(Object就定義了不同的欄位,在渲染欄位的地方全部替換為新的Object的欄位),標題內容等少數不同的地方。
模版的實現思路大致為:根據模版Id找到表單模型相關的所有表單和視圖,將關聯的所用數據表數據讀取到記憶體中,包括規則、控制項、視圖行、表單項、表單列等,再對Id進行Map映射(新建一個字典對象,讀取所有Guid欄位的地方,新建映射,Key存儲老的Guid,Value存儲新建的Guid),將所有數據Guid欄位替換為將建的Guid值,將Object對象相關的數據全部刪除,用將的Object欄位重新生成數據,不同的欄位類型設置默認的樣式,再將所有內容存儲到資料庫。
隨著表單引擎的使用,可以定義更多的表單模版,那麼表單引擎的功能將越來越豐富也越來越容易使用。
部分核心部分程式碼(可下載源碼查看):
private Dictionary<Guid, Guid> idMapes; private void CalculateId(Guid? oldId) { if (!oldId.HasValue) { return; } if (!idMapes.ContainsKey(oldId.Value)) { if (oldId.Value == Guid.Empty) { idMapes.Add(oldId.Value, Guid.Empty); } else { idMapes.Add(oldId.Value, Guid.NewGuid()); } } } public async Task CreateFormFromTemplate(Guid formId, string applicationCode, string objectNameMap, string descriptionMap, string strExcludeCreateFields, string category, int itemRowColCount = 2) { ...... // 查詢資料庫數據 spriteForms = await spriteCommonRepository.GetCommonList<SpriteForm>("SpriteForms", queryIdFormWhereModels); spriteViews = await spriteCommonRepository.GetCommonList<SpriteView>("SpriteViews", queryIdViewWhereModels); formControls = await spriteCommonRepository.GetCommonList<Control>("Controls", queryBusinessFormWhereModels); viewControls = await spriteCommonRepository.GetCommonList<Control>("Controls", queryBusinessViewWhereModels); formSpriteRules = await spriteCommonRepository.GetCommonList<SpriteRule>("SpriteRules", queryBusinessFormWhereModels); viewSpriteRules = await spriteCommonRepository.GetCommonList<SpriteRule>("SpriteRules", queryBusinessViewWhereModels); formRuleActions = await spriteCommonRepository.GetCommonList<RuleAction>("RuleActions", queryBusinessFormWhereModels); // 替換Id foreach (var spriteForm in spriteForms) { CalculateId(spriteForm.Id); CalculateId(spriteForm.Version); spriteForm.Id = idMapes[spriteForm.Id]; spriteForm.Version = idMapes[spriteForm.Version]; spriteForm.ApplicationCode = applicationCode; spriteForm.Name = ReplaceName(dictObjectNames, dictDescriptions, spriteForm.Name); spriteForm.Description = ReplaceName(dictObjectNames, dictDescriptions, spriteForm.Description); spriteForm.Category = category; spriteForm.IsTemplate = false; } foreach (var spriteView in spriteViews) { CalculateId(spriteView.Id); CalculateId(spriteView.Version); spriteView.Id = idMapes[spriteView.Id]; spriteView.Version = idMapes[spriteView.Version]; spriteView.ApplicationCode = applicationCode; spriteView.Name = ReplaceName(dictObjectNames, dictDescriptions, spriteView.Name); spriteView.Description = ReplaceName(dictObjectNames, dictDescriptions, spriteView.Description); spriteView.Category = category; } // 替換Object數據 ...... }
感覺還是沒有把這塊內容描述得特別清楚,很多設計思想用文字還是有點難表單出來!
自己做這些不知道有沒有意義,最近處於半離職狀態,很想把這塊內容應用到實際業務系統,再深入耕耘下去,但是又不善於推銷自己,也有很多無奈,最近為了生活,不得不從頭學習QT。
開源地址://gitee.com/kuangqifu/sprite
體驗地址://47.108.141.193:8031(首次載入可能有點慢,用的阿里雲最差的伺服器)
自定義表單文章地址://www.cnblogs.com/spritekuang/
流程引擎文章地址://www.cnblogs.com/spritekuang/category/834975.html(採用WWF開發,已過時,已改用Elsa實現,//www.cnblogs.com/spritekuang/p/14970992.html )
Github地址://github.com/kuangqifu/CK.Sprite.Job