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

公司期望把某一管理类信息系统从项目代码中抽取、重构为一个可复用的产品。该系统的前端是基于 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. CRL快速开发框架系列教程七(使用事务)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  2. spring源码分析之context

    重点类: 1.ApplicationContext是核心接口,它为一个应用提供了环境配置.当应用在运行时ApplicationContext是只读的,但你可以在该接口的实现中来支持reload功能. ...

  3. [数据结构]——链表(list)、队列(queue)和栈(stack)

    在前面几篇博文中曾经提到链表(list).队列(queue)和(stack),为了更加系统化,这里统一介绍着三种数据结构及相应实现. 1)链表 首先回想一下基本的数据类型,当需要存储多个相同类型的数据 ...

  4. Js 数组返回去重后的数据

    function removeRepeat(data) { var temp = ""; var mainData = []; for (var i = 0; i < dat ...

  5. js 基础篇(点击事件轮播图的实现)

    轮播图在以后的应用中还是比较常见的,不需要多少行代码就能实现.但是在只掌握了js基础知识的情况下,怎么来用较少的而且逻辑又简单的方法来实现呢?下面来分析下几种不同的做法: 1.利用位移的方法来实现 首 ...

  6. linux 如何对文件解压或打包压缩

    tar命令用与对文件打包压缩或解压,格式: tar [选项] [文件] 打包并压缩文件: tar -czvf  压缩包名 .tar.gz 解压并展开压缩包: tar -xzvf  压缩包名 .tar. ...

  7. mono for android 获取手机照片或拍照并裁剪保存

    axml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android ...

  8. Linux 系统中发博客必备的五大图片处理神器

    发博客时,总免不了要用图片说话.经过长时间的磨合,在 Linux 桌面系统下有几款图片处理软件我已经用得比较顺手了.这几款软件在 Linux 世界使用广泛,各个 Linux 发行版的软件仓库中都有自带 ...

  9. React-Native 渲染实现分析

    前言 React Native与传统的HybirdApp最大区别就是抛开WebView,使用JSC+原生组件的方式进行渲染,那么整个App启动/渲染流程又是怎样的呢? React Native启动流程 ...

  10. Visual Studio 宏的高级用法

    因为自 Visual Studio 2012 开始,微软已经取消了对宏的支持,所以本篇文章所述内容只适用于 Visual Studio 2010 或更早期版本的 VS. 在上一篇中,我已经介绍了如何编 ...