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

cby000cby 8年前發布 | 25K 次閱讀 MVC模式 前端技術 TypeScript

來自: 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 模式來搭建,使視圖代碼、邏輯代碼分離。
    • 產品化-模塊化:重構后的產品前端應該與后端遵循一致的業務模塊劃分,并在技術上提供插件化框架。

    </ul>

    • 產品化-支持二次開發:不能以修改產品源碼的形式來進行二次開發,而是以擴展的形式完成。
    • 產品化-提高可重用性:為二次開發提供方便易用的框架、基礎業務邏輯、基礎界面。
    • 產品化-提高可擴展性:基于框架開發的界面,需要為二次開發提供易用、有粗有細的擴展點,方便二次開發團隊在產品的基礎上快速搭建新的界面。這些擴展點包含:模塊級別的擴展或替換、模塊中的指定界面擴展或替換、控制器中的業務邏輯的擴展或替換,甚至任意邏輯的擴展或替換。

    設計難點

    1. 類型系統沖突
      由于EXTJS 中的 MVC 模式要求 Controller 從 Ext.app.Controller 類繼承,視圖則從 Ext.Component 類繼承。這種繼承需要使用的是 EXTJS 本身的面向對象類型系統框架帶來的繼承方案,即使用 Ext.define 來定義繼承的子類。但是我們又需要使用 TypeScript 來編寫整個應用程序,而 TypeScript 在語言層面提供了新的面向對象系統,使用后者將導致我們不能使用 EXTJS 5 本身自帶的 MVC 模式。由于我們更傾向于使用語言層面的面向對象系統,所以只有放棄 EXTJS 中的面向對象框架和 MVC 框架。
    2. TypeScript-MVC 框架的設計

    首先,與原系統一致,界面框架主要還是采用 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 的數據傳遞。

    實現

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

    過程中其實還解決了之前項目中老是出現的 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>

 本文由用戶 cby000cby 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!