產品前端重構(TypeScript、MVC框架設計)

最近兩週完成了對公司某一產品的前端重構,本文記錄重構的主要思路及相關的設計內容。html

公司指望把某一管理類信息系統從項目代碼中抽取、重構爲一個可複用的產品。該系統的前端是基於 ExtJs 5 進行構造的,後端是基於 Asp.net MVC 提供的 REST 數據接口。同時,但願經過此次重構,不但能將其自己重構至可用於快速二次開發的產品,同時還要求該前端代碼要保證相對的獨立,使得同時能夠接入 .NET 和 JAVA 兩個不一樣的後端平臺所提供的數據接口。前端

image

 

舊代碼的問題編程


老系統的前端代碼以下圖所示:後端

image

image

在構造之初,並無考慮太多的產品化工做,而主要仍是爲了快速實現項目中的需求。也並無對前端代碼進行一個較好的架構設計。這致使了一些問題:緩存

  • 可維護性差:開發者爲了快速開發出相應的界面,隨意地把整個界面的代碼羅列在一塊兒,造成了大量意大利麪式的代碼。這其中包括了各類不一樣類型的代碼:界面結構聲明、界面樣式代碼、動態界面代碼、事件監聽代碼、事件邏輯控制代碼、JS實體聲明代碼、數據源聲明代碼、數據獲取代碼……大量不一樣類型的邏輯與視圖的代碼混合在一塊兒,致使了一個模塊的代碼文件愈來愈大,有的甚至達到了幾千行。
  • 大量重複的代碼:因爲在初期,並無搭建一個統一的框架,把一些通用的代碼提取出來,並且項目組的開發人員也很隨意地拷貝代碼,致使大量頁面都有些重複的邏輯。而當前開發的模塊自己的特性代碼,則混雜在其中。
  • 沒法統一處理許多問題:這也是大量重複代碼引起的另外一個問題,項目組想要對統一的頁腳、頁面的自適應、Ajax 請求等進行統一處理,都必須逐一頁面進行修改。
  • 可擴展性差:因爲沒有前期設計,可擴展性較差。二次開發也只能是拷貝代碼並在該代碼基礎上進行修改。
  • 易錯、難寫:這是 JavaScript 這種弱類型、解釋型腳本語言的通性,再加上 EXTJS 框架自己大量使用 JSON 對象來表達參數,開發環境沒法提供智能提示,開發者只能靠不斷地查詢 Api 文檔才能編程,一不當心就會弄錯。

 

重構目標架構


  • 獨立的前端:對數據接口層須要進行適當的封裝。使其同時可對接 .NET、JAVA 兩個版本的後端。
  • 強類型化:使用強類型腳本語言 TypeScript 來編寫整個應用程序的代碼。
  • 結構化:基於 MVC 模式來搭建,使視圖代碼、邏輯代碼分離。
  • 產品化-模塊化:重構後的產品前端應該與後端遵循一致的業務模塊劃分,並在技術上提供插件化框架。
  • 產品化-支持二次開發:不能以修改產品源碼的形式來進行二次開發,而是以擴展的形式完成。
  • 產品化-提升可重用性:爲二次開發提供方便易用的框架、基礎業務邏輯、基礎界面。
  • 產品化-提升可擴展性:基於框架開發的界面,須要爲二次開發提供易用、有粗有細的擴展點,方便二次開發團隊在產品的基礎上快速搭建新的界面。這些擴展點包含:模塊級別的擴展或替換、模塊中的指定界面擴展或替換、控制器中的業務邏輯的擴展或替換,甚至任意邏輯的擴展或替換。

 

設計難點app


  1. 類型系統衝突
    因爲EXTJS 中的 MVC 模式要求 Controller 從 Ext.app.Controller 類繼承,視圖則從 Ext.Component 類繼承。這種繼承須要使用的是 EXTJS 自己的面向對象類型系統框架帶來的繼承方案,即便用 Ext.define 來定義繼承的子類。可是咱們又須要使用 TypeScript 來編寫整個應用程序,而 TypeScript 在語言層面提供了新的面向對象系統,使用後者將致使咱們不能使用 EXTJS 5 自己自帶的 MVC 模式。因爲咱們更傾向於使用語言層面的面向對象系統,因此只有放棄 EXTJS 中的面向對象框架和 MVC 框架。
  2. TypeScript-MVC 框架的設計

BrowserArchitecture

首先,與原系統一致,界面框架主要仍是採用 EXTJS 5。不一樣的是,這裏的 MVC 須要自行從新設計,Controller、View 都須要從新創建新的基類。因爲視圖控件仍是採用 EXTJS 中的控件,因此這個 MVC 框架中的 View 實際上是圖中的 ViewBuilder,其職責爲建立 EXTJS 中的控件。全部構造界面相關的代碼,都將編寫在 ViewBuilder 中。框架

其次,Controller 與 ViewBuilder 之間獨立開以後,還須要創建哪些關聯?模塊化

  • Controller 要能獲取到 View 中的指定 Id 的界面元素(如按鈕、表格、文本框等)。這樣,Controller 不但能監放任意界面元素的事件;還能夠把這些界面元素緩存下來,在 Controller 中的其它邏輯代碼處,來使用這些界面元素。(Controller 須要提供很是方便的 Api,來讓使用者快速創建上述關聯,這樣能夠強化 Controller 和 ViewBuilder 之間的配對關係。)
  • 添加 ViewModel,實現 View 的邏輯數據抽象,並由其完成自 Controller 到 View 的數據傳遞。

 

實現學習


目前已經實現了第一個版本。

image

過程當中其實還解決了以前項目中總是出現的 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');
                        }
                    }
                ]
            });
        }
    }
}

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();
        }
    }
}
相關文章
相關標籤/搜索