应用程序框架实战三十三:表现层及ASP.NET MVC介绍(二)
最近的更新速度越来越慢,主要是项目上比较忙,封装EasyUi也要花很多时间。不过大家请放心,本系列不会半途夭折,并且代码干货也会持续更新。本文继续介绍表现层和Asp.net Mvc,我将在本篇讨论一些重要的设计问题和封装技巧。
是否需要将控制器分离为独立项目
经常有人问我,是否有必要将控制器从Web项目中分离出来,下面谈一下我的认识,仅供你参考,不一定正确,请根据你自己的实际情况决定。
控制器的作用是调用业务逻辑,将获得的结果传给视图显示。从根本上说,控制器只是起协调作用,它不应该自身完成复杂的业务逻辑。
不过哪怕你将业务逻辑尽量放到了领域层,控制器上还是会有不少代码,比如查询条件的设置,事务控制等。在这种情况下,将控制器分离到一个独立的程序集,似乎说得过去。
但是分离控制器代码有很多方法,比如引入应用层服务,应用层服务可以看成是控制器的延伸,控制器上大部分代码被转移到应用层服务中,控制器变成很薄的一层,这种情况下,分离控制器到独立项目,没有任何价值。
将控制器分离到一个独立程序集后,你会发现查找控制器对应视图变得更加困难,哪怕Resharper可以帮你定位跳转。从目录结构上看,也不再遵循大家熟悉的约定,实际上降低了可维护性,增加了学习成本。
最后的结论就是,如果你采用了应用层服务,就不要分离控制器。
是否需要将MVC项目拆分为多个
对于表现层的模块化,MVC提供了Areas(区域)技术来支持较大项目的开发。每个区域包含独立的控制器和相关视图,为每个业务模块创建一个区域,对于一般项目的管理,应该都够用了。
我在学习一些流行的应用开发框架时发现,有些应用开发框架将同一个Asp.Net Mvc项目,拆分为多个Mvc项目,以实现网站级别的模块化。其中一个是主Web项目,包含大部分WEB资源,比如图片、CSS、JS等。其它一些附属Web项目,包含其它业务模块,如果包含js文件,该文件的生成操作需要设置成“嵌入的资源”,包含的cshtml文件还需要用RazorGenerator插件手工生成类。最后主Web项目引用其它附属Web项目,从而整合为一个网站。
这种架构的好处是允许复用业务UI模块,包括视图,同时方便团队协作,不同成员开发不同模块时干扰更小。
但是这种架构也有很多问题,将JS设置成“嵌入的资源”,cshtml文件使用RazorGenerator生成类,会导致JS和页面调试部署都很不方便,调试时经常会碰上缓存问题。
解决UI复用和协同工作的另一个办法是,多个网站独立开发部署,采用单点登录连接成一个系统。
对表现层的模块化建议如下:普通项目采用单Mvc项目架构,通过Areas进行模块化, 更大项目采用多Mvc项目架构,单独开发部署,以单点登录方式集成,尽量不要将js、cshtml等页面元素嵌入程序集,会导致更难维护,仅在具有特殊需求时采用,比如插件式开发。
Asp.Net Mvc抽象封装技巧
对于技术和业务逻辑,我们进行了复杂的分层和封装,那么对于页面本身,还要不要抽象?
Html由标签组成,大量的Html标签,让你无法清晰的看出页面结构。当需要修改页面上某个区域时,如果页面很长很复杂,你需要在杂乱的Html标签中来回扫描,以定位目标,这与几百行的长方法类似。有经验的开发人员,总是以单一职责原则SRP为指导,通过提取方法和组合方法重构,保持方法的简短,同时获得清晰的代码结构。
除了定位困难以外,未进行抽象的页面,将包含大量重复的Html,而冗余代码是可维护性的天敌。
对于Asp.Net WebForm,抽象封装主要依赖母版页、服务器控件、用户控件等元素。
母版页用于管理通用页面结构,帮助划分区域,它类似于抽象类,可以提供一部分具体实现,它将多个页面共享的部分抽取上来,并提供了内容占位符,占位符类似于虚方法或抽象方法,以方便具体页面填充内容。
服务器控件用于封装能够高度复用的UI元素,提供增强功能,比如自定义文本框,一般的文本框只能输入文本,自定义文本框可以进行验证,甚至能进行权限控制等。
用户控件是比服务器控件更轻量的封装方式,用户控件一般用于封装页面上的元素或区域。母版页的工作方式类似于继承,有经验的同学知道,继承的灵活性是比较低的,所以设计上流传一句话“多用委托,少用继承”,WebForm的“委托”就是用户控件,它同样可以切割页面,并且提供了更好的灵活性。
如果很多页面都需要分成上中下三个区域,每个区域有一些固定的元素,这时候采用母版页就是合适的。但如果只有一个或几个页面需要这个结构,采用母版页将是大炮打蚊子,用户控件则是更好的选择。
用户控件除了能够封装Html以外,还包含后置代码,可以处理逻辑。
了解了WebForm的基础封装元素后,再来看Asp.Net Mvc提供了哪些元素,与Web Form的元素是如何对应的。
首先看母版页,母版页的功能是提供布局结构,Mvc提供了布局页来支持类似功能。打开Mvc项目Views\Shared目录,会发现一个名为_Layout.cshtml的文件,这是系统定义的布局页,_Layout这个名称不是必须的,它能够起作用的原因是Views目录下的_ViewStart.cshtml设置指向它。不过没事不要乱改系统定义的文件名,这样会破坏约定,增加维护成本。
_ViewStart.cshtml包含了Layout的设置,它的作用是为视图提供默认布局设置,值得一提的是,根目录Views下的_ViewStart.cshtml设置不能继承,Areas各模块必须添加自己的_ViewStart.cshtml。
对于较复杂的系统,仅依靠一个_Layout.cshtml来抽象页面冗余是不够的,一般需要多层继承结构,_Layout.cshtml仅放置通用性很强的内容,比如js,css引用等,更具体的抽象可定义自己的布局页,继承_Layout.cshtml。
Asp.Net Mvc不再提供服务器控件这种可视化元素,但相关的封装思想却从未间断。Mvc提供了HtmlHelper、UrlHelper、AjaxHelper等几个帮助类来封装相关操作,其中HtmlHelper包含表单元素的封装,是与WebForm服务器控件相对应的东西,我们封装控件主要从它下手。
Mvc允许在视图中通过Html属性访问HtmlHelper,可以看到,所有控件都是通过扩展方法的方式添加上去的,这也给我们提供了一种思路。我在前文已经多次提到,使用扩展方法要很小心,特别是扩展系统的类,因为可能造成大面积污染。
对于HtmlHelper,我一般仅扩展少量方法,首先是通用UI技术的封装,我会把需要在视图上用到的通用技术封装到@Html.X()方法中,比如导入一个Js文件,可以这样调用@Html.X().ImportJs(“xx”),当然现在导入Js一般用Bundle,本篇后续再介绍。
其次是对特定UI技术的封装,比如Dwz,EasyUi,Ext等,也可能是其它组件,比如图表FusionCharts,ECharts等。对于每一个要用到的组件,都仅为其在HtmlHelper扩展一个方法,以EasyUi为例,你不能这样扩展,@Html.EasyUiTextBox(),@Html.EasyUiCombox(),这样会在HtmlHelper中扩展大量方法,导致查找其它方法变得困难,更好的方法是@Html.EasyUi().TextBox() ,@Html.EasyUi().Combox()。这里设计的核心是EasyUi方法返回一个接口,将EasyUi所有操作全部放到这个接口中。关于Mvc的控件封装,我会在后续EasyUi系列详细讲述。
最后,需要在HtmlHelper中扩展的是业务UI组件,比如字典、省市区三级联动、人员选择等,我会把所有业务UI组件扩展到@Html.Ui()方法中,以方便开发某些业务模块时复用,比如字典@Html.Ui().Dic()。
下面来看WebForm用户控件在Mvc中有哪些对应元素。
如果页面中的某个区域很复杂,根据逻辑结构,将区域分解为更小的部分,每个部分放到一个用户控件中,从而得到清晰的结构。
Mvc提供的@Html.Partial()方法允许将Html分离到部分视图中,并可以给这个部分视图传递一个实体,以进行数据绑定。
不过部分视图的能力是有限的,你的主视图必须能够提供部分视图相关数据,这就要求主视图的实体携带更多的数据,这在很多时候都不太方便。打个比方,你的页面上需要显示一个下拉人员列表,列表的内容是用另一个表的数据填充的,如果采用部分视图来封装这个列表,你的主视图对应的实体就必须提供人员集合。
Mvc提供了更为强大的@Html.Action()方法,Action也用于操作部分视图,但它能够独立的为视图提供数据。还是刚才的下拉人员列表,现在主视图的实体不再需要携带人员数据了,调用Action后,人员列表已加载完成。
虽然Action更强大,但它需要设置Url信息,以确定这个功能由哪个控制器的方法提供,当某个Action操作用得非常频繁时,考虑将该操作扩展到HtmlHelper中,这样可以封装掉Url和参数信息,以简化调用。
以上简单介绍了Mvc的一些封装元素,以供你编写出更清晰的UI代码。同时比较了Asp.net WebForm与Mvc的元素对应情况,你如果具有WebForm的基础,相信Mvc的封装也会很快上手。
补充一点,虽然我用方法与Html长度类比,但不能认为Html的封装粒度越细越好,我曾经尝试过很细粒度的UI拆分,最终效果并不理想,合适的拆分粒度更好维护,这方面根据自己的习惯进行摸索。
Bundle介绍
现在的系统Js和Css文件都很多,如果一个网站引用太多Js或css文件,对性能是有一定影响的,因为每个文件会发送一个请求。
我以前的办法是使用AjaxMinifier工具手工压缩Js,再手工合并,Css也采用类似办法,后面使用了一个第三方的工具,也比较麻烦。
Asp.Net为此提供了一个叫Bundle的打包压缩技术,它能够在运行时将js或css文件打包压缩,这正是我需要的。
不过在使用过程中,发现它并不是想像中那么易用,问了一些人,也用起来有问题。还有一些人没碰到啥问题,但观察他的代码,实际上没有起作用,因为他没有设置启用优化的选项。
使用Bundle有几点需要注意:
- 如果文件中的代码对路径很敏感,比如css中用了相对路径,你在配置Bundle时,虚拟路径就不能很随意,因为会破坏路径关系,导致失败。
- 如果没有在代码中设置BundleTable.EnableOptimizations = true,也没有在web.config进行相应配置,则打包压缩不会起作用,只是让你在引用文件时省点力。
- 已经压缩过的文件,比如jquery.min.js,不要用Bundle配置,使用常规方式引入,不然运行时可能出错。
Util最新代码示例更新
除了之前的大量代码已重构外,主要更新了EasyUi的行内编辑方式。
结束语
本文简单介绍了Mvc相关的一些问题和技巧,有不同意见,欢迎交流。
下载地址:下载时请顺手推荐,以支持本人写作.
https://files.cnblogs.com/files/xiadao521/Applications.2015.3.16.1.rar
https://files.cnblogs.com/files/xiadao521/Framework.2015.3.16.1.rar
https://files.cnblogs.com/files/xiadao521/Data.2015.3.5.1.rar
注意:本人每次发布新版本时,会删除老版本
.Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。
.Net Easyui开发交流QQ群(本群仅限Easyui开发者,非Easyui开发者勿进):157809322
谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/xiadao521/