這段時間陸續收到一些小伙伴的信息,對流程引擎和自定義表單比較感興趣,內心還是比較欣喜的。多數人還是對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。
開源地址:https://gitee.com/kuangqifu/sprite
體驗地址:http://47.108.141.193:8031(首次加載可能有點慢,用的阿里雲最差的服務器)
自定義表單文章地址:https://www.cnblogs.com/spritekuang/
流程引擎文章地址:https://www.cnblogs.com/spritekuang/category/834975.html(采用WWF開發,已過時,已改用Elsa實現,https://www.cnblogs.com/spritekuang/p/14970992.html )
Github地址:https://github.com/kuangqifu/CK.Sprite.Job