產品前端重構(TypeScript、MVC框架設計)
來自: http://www.cnblogs.com/zgynhqf/p/5222221.html
最近兩周完成了對公司某一產品的前端重構,本文記錄重構的主要思路及相關的設計內容。
公司期望把某一管理類信息系統從項目代碼中抽取、重構為一個可復用的產品。該系統的前端是基于 ExtJs 5 進行構造的,后端是基于 Asp.net MVC 提供的 REST 數據接口。同時,希望通過這次重構,不但能將其本身重構至可用于快速二次開發的產品,同時還要求該前端代碼要保證相對的獨立,使得同時可以接入 .NET 和 JAVA 兩個不同的后端平臺所提供的數據接口。
舊代碼的問題
老系統的前端代碼如下圖所示:
在構造之初,并沒有考慮太多的產品化工作,而主要還是為了快速實現項目中的需求。也并沒有對前端代碼進行一個較好的架構設計。這導致了一些問題:
- 可維護性差:開發者為了快速開發出相應的界面,隨意地把整個界面的代碼羅列在一起,形成了大量意大利面式的代碼。這其中包括了各種不同類型的代碼:界面結構聲明、界面樣式代碼、動態界面代碼、事件監聽代碼、事件邏輯控制代碼、JS實體聲明代碼、數據源聲明代碼、數據獲取代碼……大量不同類型的邏輯與視圖的代碼混合在一起,導致了一個模塊的代碼文件越來越大,有的甚至達到了幾千行。
- 大量重復的代碼:由于在初期,并沒有搭建一個統一的框架,把一些通用的代碼提取出來,而且項目組的開發人員也很隨意地拷貝代碼,導致大量頁面都有些重復的邏輯。而當前開發的模塊本身的特性代碼,則混雜在其中。
- 無法統一處理許多問題:這也是大量重復代碼引發的另一個問題,項目組想要對統一的頁腳、頁面的自適應、Ajax 請求等進行統一處理,都必須逐一頁面進行修改。
- 可擴展性差:由于沒有前期設計,可擴展性較差。二次開發也只能是拷貝代碼并在該代碼基礎上進行修改。
- 易錯、難寫:這是 JavaScript 這種弱類型、解釋型腳本語言的通性,再加上 EXTJS 框架本身大量使用 JSON 對象來表達參數,開發環境無法提供智能提示,開發者只能靠不斷地查詢 Api 文檔才能編程,一不小心就會弄錯。
重構目標
- 獨立的前端:對數據接口層需要進行適當的封裝。使其同時可對接 .NET、JAVA 兩個版本的后端。
- 強類型化:使用強類型腳本語言 TypeScript 來編寫整個應用程序的代碼。
- 結構化:基于 MVC 模式來搭建,使視圖代碼、邏輯代碼分離。 產品化-模塊化:重構后的產品前端應該與后端遵循一致的業務模塊劃分,并在技術上提供插件化框架。
- 產品化-支持二次開發:不能以修改產品源碼的形式來進行二次開發,而是以擴展的形式完成。
- 產品化-提高可重用性:為二次開發提供方便易用的框架、基礎業務邏輯、基礎界面。
- 產品化-提高可擴展性:基于框架開發的界面,需要為二次開發提供易用、有粗有細的擴展點,方便二次開發團隊在產品的基礎上快速搭建新的界面。這些擴展點包含:模塊級別的擴展或替換、模塊中的指定界面擴展或替換、控制器中的業務邏輯的擴展或替換,甚至任意邏輯的擴展或替換。
- 類型系統沖突
由于EXTJS 中的 MVC 模式要求 Controller 從 Ext.app.Controller 類繼承,視圖則從 Ext.Component 類繼承。這種繼承需要使用的是 EXTJS 本身的面向對象類型系統框架帶來的繼承方案,即使用 Ext.define 來定義繼承的子類。但是我們又需要使用 TypeScript 來編寫整個應用程序,而 TypeScript 在語言層面提供了新的面向對象系統,使用后者將導致我們不能使用 EXTJS 5 本身自帶的 MVC 模式。由于我們更傾向于使用語言層面的面向對象系統,所以只有放棄 EXTJS 中的面向對象框架和 MVC 框架。 - TypeScript-MVC 框架的設計
- Controller 要能獲取到 View 中的指定 Id 的界面元素(如按鈕、表格、文本框等)。這樣,Controller 不但能監聽任意界面元素的事件;還可以把這些界面元素緩存下來,在 Controller 中的其它邏輯代碼處,來使用這些界面元素。(Controller 需要提供非常方便的 Api,來讓使用者快速建立上述關聯,這樣可以強化 Controller 和 ViewBuilder 之間的配對關系。)
- 添加 ViewModel,實現 View 的邏輯數據抽象,并由其完成自 Controller 到 View 的數據傳遞。
</ul>
設計難點
首先,與原系統一致,界面框架主要還是采用 EXTJS 5。不同的是,這里的 MVC 需要自行重新設計,Controller、View 都需要重新建立新的基類。由于視圖控件還是采用 EXTJS 中的控件,所以這個 MVC 框架中的 View 其實是圖中的 ViewBuilder,其職責為創建 EXTJS 中的控件。所有構造界面相關的代碼,都將編寫在 ViewBuilder 中。
其次,Controller 與 ViewBuilder 之間獨立開之后,還需要建立哪些關聯?
實現
目前已經實現了第一個版本。
過程中其實還解決了之前項目中老是出現的 Ext 控件 Id 重復的問題:通過定義新的 cId 來替換 Id,并提供相應的通過 cId 查詢對應控件的方法。這樣,就算有重復的 cId 的控件,也不會有什么問題了。
另外,完成后的框架,雖然帶來了諸多好處,但是開發者的第一感覺還是復雜了許多。之前全都堆在一個文件中的代碼,現在要分為控制器、視圖,而且還需要基于統一的底層框架來實現,框架中的 Api 還需要慢慢熟悉,學習門檻高了不少。
PS-----------------------------------------
附上基于該 MVC 框架的某模塊的最終部分 TS 代碼:
HolidayViewBuilder.ts:
module DBI.modules.holiday { /*** 假日頁面的視圖。 */ export class HolidayViewBuilder extends ViewBuilder { buildView(): View { return this.buildGrid({ cId: 'grid', region: 'center', store: this.buildStore(), tbar: this.buildToolbar({ items: [ DBI.Workflow.createStatusComboBox({ model: this.modelName }), { cId: 'btnSearch', text: "查詢", operationName: 'Search' }, { cId: 'btnAdd', text: '添加', operationName: 'Add' }, { cId: 'btnEdit', text: '修改', operationName: 'Edit' }, { cId: 'btnDelete', text: '刪除', operationName: 'Delete' }, { cId: 'btnSubmitWF', text: '提交審批', operationName: 'SubmitWF' } ] }), columns: [ { text: "ID", width: 60, dataIndex: 'Id', hidden: true, align: "center" }, { xtype: "rownumberer", text: "序號", width: 50, align: "center" }, { text: "開始時間", width: 150, dataIndex: 'StartDate', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d'); } }, { text: "結束時間", width: 150, dataIndex: 'EndDate', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d'); } }, { text: "節假日名稱", width: 150, dataIndex: 'HolidayName', sortable: true, align: 'center' }, { text: "狀態", width: 150, dataIndex: 'WF_ApprovalStatus', sortable: true, align: 'center' }, { text: "審核原因", width: 180, dataIndex: 'WF_ApprovalReason', sortable: true, align: 'center' }, //{ text: "生效時間", width: 135, dataIndex: 'WF_EffectiveTime', sortable: true, align: 'center' }, { text: "最后更新時間", width: 150, dataIndex: 'UpdatedTime', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d H:i:s'); } }, { text: "生效時間", width: 150, dataIndex: 'WF_EffectiveTime', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d'); } } ] }); } }
}</pre>
HolidayController.ts
module DBI.modules.holiday { /*** 假日模塊的控制器 */ export class HolidayController extends ViewController { viewBuilder = new HolidayViewBuilder(); modelName = "DBI.Holiday"; moduleTitle = "節假日管理"; store: Ext.data.IStore; grid: Ext.grid.IGridPanel; formWindow: Ext.IWindow; formPanel: Ext.IFormPanel; form: Ext.form.IBasic; init() { super.init(); this.grid = this.view; this.store = this.grid.store; this.control(this.view, { btnSearch: { click: this.onBtnSearchClick }, btnAdd: { click: this.onBtnAddClick }, btnEdit: { click: this.onBtnEditClick }, btnDelete: { click: this.onBtnDeleteClick }, btnSubmitWF: { click: this.onBtnSubmitWFClick } }); this.reloadData(); } onBtnAddClick() { this.showFormWindow(); this.formWindow.setTitle("添加節假日"); this.form.url = urls.Holiday.InsertHoliday; } /** * 打開提交申請的窗體 */ onBtnSubmitWFClick() { if (DBI.Workflow.canSubmitApply({ grid: this.grid })) { var applyController = new wf.CommonApplyWinController(); applyController.modelName = this.modelName; applyController.viewModel = { flowCode: "WF_HOLIDAY", windowTitle: "假日審批流程", columns: HolidayApporvalViewBuilder.buildApprovingGridColumns(), dataSource: new wf.ApplyWinDataSource(this.grid) }; applyController.init(); applyController.showWindow(); } } showFormWindow() { this.formWindow = this.viewBuilder.buildFormWindow(); this.formPanel = this.formWindow.getChild("form"); this.form = this.formPanel.getForm(); this.control(this.formWindow, { btnSubmit: { click: this.submitForm }, btnClose: { click: () => { this.formWindow.close(); } } }); this.formWindow.show(); } submitForm() { var form = this.form; if (!form.isValid()) return; var startDate = form.findField('StartDate').getValue(); var endDate = form.findField('EndDate').getValue(); if (startDate > endDate) { Ext.MessageBox.alert('提示', "開始時間不能大于結束時間"); return; } //提交數據到服務端。 form.submit({ success: () => { Ext.MessageBox.alert('提示', "提交成功!"); this.formWindow.close(); this.store.reload(); }, failure: () => { Ext.MessageBox.alert('提示', "提交失敗!"); this.formWindow.close(); this.store.reload(); } }); } reloadData() { var filter = DBI.Workflow.createStatusFilter(); this.store.proxy.url = DBI.OData.createUrl({ model: this.modelName, filter: filter }); this.store.load(); } }
}</pre> </div>