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

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

旧代码的问题


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

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

  • 可维护性差:开发者为了快速开发出相应的界面,随意地把整个界面的代码罗列在一起,形成了大量意大利面式的代码。这其中包括了各种不同类型的代码:界面结构声明、界面样式代码、动态界面代码、事件监听代码、事件逻辑控制代码、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 框架的设计

首先,与原系统一致,界面框架主要还是采用 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');
}
}
]
});
}
}
}

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();
}
}
}

产品前端重构(TypeScript、MVC框架设计)的更多相关文章

  1. 前端开发工程师 - 05.产品前端架构 - 协作流程 & 接口设计 & 版本管理 & 技术选型 &开发实践

    05.产品前端架构 第1章--协作流程 WEB系统 角色定义 协作流程 职责说明 第2章--接口设计 概述 接口规范 规范应用 本地开发 第3章--版本管理 见 Java开发工程师(Web方向) - ...

  2. openresty 前端开发轻量级MVC框架封装一(控制器篇)

    通过前面几章,我们已经掌握了一些基本的开发知识,但是代码结构比较简单,缺乏统一的标准,模块化,也缺乏统一的异常处理,这一章我们主要来学习如何封装一个轻量级的MVC框架,规范以及简化开发,并且提供类似p ...

  3. openresty 前端开发轻量级MVC框架封装二(渲染篇)

    这一章主要介绍怎么使用模板,进行后端渲染,主要用到了lua-resty-template这个库,直接下载下来,放到lualib里面就行了,推荐第三方库,已经框架都放到lualib目录里面,lua目录放 ...

  4. android——根据MVC框架设计的结构

  5. Web前端MVC框架的意义分析

    前言: Web前端开发是Web技术发展中的一个重要组成部分,在传统的前端开发中由于外界因素的影响导致其开发形式呈现出简单化的特点,即以页面为主体来展示界面中的信息.然而随着科学技术的不断进步,Web前 ...

  6. 写自己的ASP.NET MVC框架(上)

    http://www.cnblogs.com/fish-li/archive/2012/02/12/2348395.html 阅读目录 开始 ASP.NET程序的几种开发方式 介绍我的MVC框架 我的 ...

  7. ASP.NET MVC框架开发系列课程 (webcast视频下载)

    课程讲师: 赵劼 MSDN特邀讲师 赵劼(网名“老赵”.英文名“Jeffrey Zhao”,技术博客为http://jeffreyzhao.cnblogs.com),微软最有价值专家(ASP.NET ...

  8. PHP原生实现简易的MVC框架

    目录结构: —|controller —|Home.php —|model —|view —|welcome.php —|index.php 基本原理: 首页 index.php 通过获得地址栏中的路 ...

  9. 源码分析系列 | 从零开始写MVC框架

    1. 前言 2. 为什么要自己手写框架 3. 简单MVC框架设计思路 4. 课程目标 5. 编码实战 5.1 配置阶段 web.xml配置 config.properties 自定义注解 5.2 初始 ...

随机推荐

  1. HTML+CSS 项目总结

    在过去的大概一个月的学习,基本掌握了HTML+CSS的用法和特性. 这个星期老师给我们布置了一个PC端的实战项目,并且要求在3-4天内完成,我不惜废寝忘食,在紧迫的时间内大致地完成了,但是有些效果不能 ...

  2. C# salt+hash 加密

    一.先明确几个基本概念 1.伪随机数:pseudo-random number generators ,简称为:PRNGs,是计算机利用一定的算法来产生的.伪随机数并不是假随机 数,这里的" ...

  3. the Zen of Python---转载版

    摘自译文学习区 http://article.yeeyan.org/view/legendsland/154430 The Zen of Python Python 之禅 Beautiful is b ...

  4. IT持续集成之质量管理

    研发工具生态 质量相关工作 一次编译产出测试包与上线包 !从源头保证版本的⼀一致性!代码质量控制! 全⽅方位的⾃自动化测试体系保证! 提测冒烟效率! 全⾃自动上线流程杜绝⼈人⼯工犯错! 生产环境应⽤用 ...

  5. 高性能 TCP/UDP/HTTP 通信框架 HP-Socket v4.1.1

    HP-Socket 是一套通用的高性能 TCP/UDP/HTTP 通信框架,包含服务端组件.客户端组件和 Agent 组件,广泛适用于各种不同应用场景的 TCP/UDP/HTTP 通信系统,提供 C/ ...

  6. RabbitMQ + PHP (二)AMQP拓展安装

    上篇说到了 RabbitMQ 的安装. 这次要在讲案例之前,需要安装PHP的AMQP扩展.不然可能会报以下两个错误. 1.Fatal error: Class 'AMQPConnection' not ...

  7. 【干货分享】流程DEMO-请休假

    流程名: 请假申请  流程相关文件: 流程包.xml WebService业务服务.xml WebService.asmx WebService.cs  流程说明: 流程中集成了webservice服 ...

  8. i++、++i 、i--、--i

    总结: i++ 先用后加, ++i先加后用: i--先用后减, --i先减后用: //int i = 1; //Console.WriteLine(i);//1 //Console.WriteLine ...

  9. Hadoop 2.x 生态系统及技术架构图

    一.负责收集数据的工具:Sqoop(关系型数据导入Hadoop)Flume(日志数据导入Hadoop,支持数据源广泛)Kafka(支持数据源有限,但吞吐大) 二.负责存储数据的工具:HBaseMong ...

  10. 免费SSL证书 之Let’s Encrypt申请与部署(Windows Nginx)

    我着着皇帝的新衣,但是你看不见    有一颗愿意等待的心,说明你对未来充满希望.有一颗充满希望的心,那么等待又算什么.人就是在等待与希望中度过,我们永远要对未来充满信心! 读在最前面: 1.本文案例为 ...