产品前端重构(TypeScript、MVC框架设计)

简介:

最近两周完成了对公司某一产品的前端重构,本文记录重构的主要思路及相关的设计内容。

公司期望把某一管理类信息系统从项目代码中抽取、重构为一个可复用的产品。该系统的前端是基于 ExtJs 5 进行构造的,后端是基于 Asp.net MVC 提供的 REST 数据接口。同时,希望通过这次重构,不但能将其本身重构至可用于快速二次开发的产品,同时还要求该前端代码要保证相对的独立,使得同时可以接入 .NET 和 JAVA 两个不同的后端平台所提供的数据接口。

image

 

旧代码的问题


老系统的前端代码如下图所示:

image

image

在构造之初,并没有考虑太多的产品化工作,而主要还是为了快速实现项目中的需求。也并没有对前端代码进行一个较好的架构设计。这导致了一些问题:

  • 可维护性差:开发者为了快速开发出相应的界面,随意地把整个界面的代码罗列在一起,形成了大量意大利面式的代码。这其中包括了各种不同类型的代码:界面结构声明、界面样式代码、动态界面代码、事件监听代码、事件逻辑控制代码、JS实体声明代码、数据源声明代码、数据获取代码……大量不同类型的逻辑与视图的代码混合在一起,导致了一个模块的代码文件越来越大,有的甚至达到了几千行。
  • 大量重复的代码:由于在初期,并没有搭建一个统一的框架,把一些通用的代码提取出来,而且项目组的开发人员也很随意地拷贝代码,导致大量页面都有些重复的逻辑。而当前开发的模块本身的特性代码,则混杂在其中。
  • 无法统一处理许多问题:这也是大量重复代码引发的另一个问题,项目组想要对统一的页脚、页面的自适应、Ajax 请求等进行统一处理,都必须逐一页面进行修改。
  • 可扩展性差:由于没有前期设计,可扩展性较差。二次开发也只能是拷贝代码并在该代码基础上进行修改。
  • 易错、难写:这是 JavaScript 这种弱类型、解释型脚本语言的通性,再加上 EXTJS 框架本身大量使用 JSON 对象来表达参数,开发环境无法提供智能提示,开发者只能靠不断地查询 Api 文档才能编程,一不小心就会弄错。

 

重构目标


  • 独立的前端:对数据接口层需要进行适当的封装。使其同时可对接 .NET、JAVA 两个版本的后端。
  • 强类型化:使用强类型脚本语言 TypeScript 来编写整个应用程序的代码。
  • 结构化:基于 MVC 模式来搭建,使视图代码、逻辑代码分离。
  • 产品化-模块化:重构后的产品前端应该与后端遵循一致的业务模块划分,并在技术上提供插件化框架。
  • 产品化-支持二次开发:不能以修改产品源码的形式来进行二次开发,而是以扩展的形式完成。
  • 产品化-提高可重用性:为二次开发提供方便易用的框架、基础业务逻辑、基础界面。
  • 产品化-提高可扩展性:基于框架开发的界面,需要为二次开发提供易用、有粗有细的扩展点,方便二次开发团队在产品的基础上快速搭建新的界面。这些扩展点包含:模块级别的扩展或替换、模块中的指定界面扩展或替换、控制器中的业务逻辑的扩展或替换,甚至任意逻辑的扩展或替换。

 

设计难点


  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();
        }
    }
}
目录
相关文章
|
6月前
|
缓存 前端开发 jenkins
Serverless 应用引擎产品使用合集之前端的项目部署在镜像里时,页面总是自动刷新,是什么导致的
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
|
3月前
|
前端开发
【前端web入门第五天】03 清除默认样式与外边距问题【附综合案例产品卡片与新闻列表】
本文档详细介绍了CSS中清除默认样式的方法,包括清除内外边距、列表项目符号等;探讨了外边距的合并与塌陷问题及其解决策略;讲解了行内元素垂直边距的处理技巧;并介绍了圆角与盒子阴影效果的实现方法。最后通过产品卡片和新闻列表两个综合案例,展示了所学知识的实际应用。
75 11
|
5月前
|
开发框架 前端开发 JavaScript
循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理
循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理
|
5月前
|
人工智能 关系型数据库 分布式数据库
PolarDB产品使用问题之如何将自有BI系统接入NL2BI的Web前端
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
6月前
|
敏捷开发 缓存 前端开发
阿里云云效产品使用问题之流水线构建前端项目比较慢。该如何优化
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
6月前
|
前端开发 Java Serverless
阿里云云效产品使用问题之前端流水线部署时,在构建环节,编译和打包要分开还是放在一起
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
6月前
|
前端开发 程序员 API
视觉智能开放平台产品使用合集之web前端拉起人脸识别该如何操作
视觉智能开放平台是指提供一系列基于视觉识别技术的API和服务的平台,这些服务通常包括图像识别、人脸识别、物体检测、文字识别、场景理解等。企业或开发者可以通过调用这些API,快速将视觉智能功能集成到自己的应用或服务中,而无需从零开始研发相关算法和技术。以下是一些常见的视觉智能开放平台产品及其应用场景的概览。
|
7月前
|
缓存 监控 前端开发
【专栏】前端性能体验对于用户满意度和产品成功至关重要
【4月更文挑战第27天】在数字化时代,前端性能关乎用户满意度和产品成功。本文探讨提升前端性能的三大法则:1) 优化资源加载,如压缩合并文件、按需加载和利用缓存;2) 提升页面渲染速度,减少 DOM 操作,优化 CSS 样式和合理使用 JavaScript;3) 性能监控与持续优化,使用性能工具、分析用户行为数据并持续改进。遵循这些法则,打造快速响应的前端界面,提升用户体验,为产品成功打下基础。在实践中,应灵活应用并不断创新优化策略。
78 3
|
7月前
|
存储 前端开发 API
对象存储OSS产品常见问题之获取文件结构并在前端页面展示如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
300 0
|
7月前
|
存储 小程序 API
对象存储OSS产品常见问题之前端直传视频获取视频的长度获得多少秒如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
512 0