最近在工作中重构一个老系统,烂的地方有很多,但是对于后台的页面模板(我指的是 JSP、FreeMarker、Velocity 这样的后台模板,JavaScript 前端模板不在此讨论范围内),却是我要说的部分,这似乎是一个被人遗忘的小角落。你可以很轻易地找到怎样重构 Java 类和方法的材料,你的 Java 代码可以写得很优雅;去搜搜 “重构”,到处是怎样重构你的 Java 代码、C++代码,我们也能找到许多前端设计师对于页面结构的重构,但是重构的范围远非至此。
后台业务逻辑写得再好,一个 jsp、ftl 模板页面还是可能写上几千行,业务逻辑耦合在呈现代码里面,看起来一团糟,对这部分的重构,既不属于传统的 Java 代码的重构,也不属于前端工程师的范畴,就这样姥姥不疼、舅舅不爱地被忽略了。但是对一个网站来说,模板数量大得惊人,这是一个不可避开的话题。微博上 @zhh-2009 说道:
阿里集团有成千上万的系统,一大堆的 Java 码农,有个更好笑的说法是: 阿里 Java 码农其实就是 velocity 模板码农。
把业务逻辑从模板中剥离出去
模板是用来做什么的?就是用来做页面生成和展现的,以分离业务逻辑代码和用户界面代码。理想情况下,模板代码中不应该包含任何业务逻辑的代码在里面。我见过通过向模板传递 service 对象的方式,再在模板里面通过 service 获取数据,这是糟糕的做法。模板要做的事情需要保持清晰,不要耦合那些模型层的业务逻辑。
剥离数据处理的重复劳动
Model 的数据,距离展现需要的数据,表现形式上会有诸多差异,所以往往在模板拿到以后,还需要经过加工处理才能展示。比如日期型数据,一个 java.util.Date 对象被送到了模板上,有时需要转换成 “2012-11-18” 这样的形式,有时需要转换成 “09:47:10” 这样的形式,于是我见到了大块的页面模板上数据处理的逻辑代码。
当然,这也是可以优化的:
1、使用标签。标签可以封装好一些通用的展示逻辑,这里指的标签就是纯粹为了展示的需要而封装的标签,并非封装了业务逻辑的功能标签。每一个标签都可以变成一个小的 MVC 组件,一样可以使用模板的方式来输出(而不是再标签实现类里面生写 HTML 代码)。
2、使用模板宏。比如 FreeMarker 的模板宏:
<#macro repeat count> <#local name = "default"> <#list 1..count as iter> ${name} ${count}/${iter}: <#nested> </#list> </#macro>
然后就可以使用了:
<@repeat count=3>abc</@repeat>
3、使用数据处理的工具方法。可以写辅助类,在模板中引入,也可以利用模板的 “静态引入” 能力,后文会提到。
4、在数据传递到模板前,增加一层数据预处理的逻辑。这个实现方式有很多,比较常见的是利用 Interceptor,将一种数据处理成多种展现形式,在模板中就可以直接拿来使用,后文也会提到。
管理好模板数据的上下文
说到模板数据的上下文,就要提到模板变成页面的方式,最基本的方式其实就是占位符(表达式)的替换,也就是将指定模板内容(字符串)中的特定标记(子字符串)替换一下便生成了最终需要的业务数据。在此基础上,才有编译型或者解释型的其它方式。
在占位符替换的过程中,需要根据表达式字符串,去特定的上下文中寻找相应的数据,以 JSTL 为例,所有通过 request.setAttribute(“key”, value) 方法放置的数据全部都能直接获取:
<div><c:out value="key" /></div>
或者也可以利用 JSP 对 EL 表达式原生的支持:
<div>${key}</div>
如果你使用的是 OGNL 表达式语言,为了更好用,它在 Struts 2 中做了进一步扩展,这时这个上下文就是总接触到的 “value stack”。
展示数据预处理层
这一层就是为了填补 Model 和模板展示需要的数据或者数据形式之沟壑而增加的,可以使用 Interceptor 实现,比如我在所有页面展示里面都需要用到当前用户 user 这个对象,那我就可以在 Interceptor 中,把 user 对象准备好,放置到模板数据的上下文中,这样在模板里面就可以拿来即用了。这是数据的沟壑,还有数据形式的沟壑,前面那个时间格式的例子已经提到过了。
静态引入也是一种常用的准备数据的方式,在 JSP 里就是:
<%@import page="userData.jsp" %>
用这种方式引入的页面在编译时就会引入进来,里面的数据、包、类拿来即用。
子页面划分
在页面模板重构上,这大概是我们最常用和最基础的办法。我们经常根据最终呈现页面的特点,把页面划分成展示功能独立的几个子页面,然后在需要的位置引入进来,比如 JSP 的动态引入:
<jsp:import page="detail.jsp" />
还有一种方式对页面模板开发的程序员更加透明,开发人员在自己关心的页面模板中可以看不到这些 import 的代码,转而把这个引入的规则配置放到页面模板之外去,有的根据 URL 规则来聚合子页面,有的根据自定义的页面特点来聚合那些子页面,比如 Tiles 2 的聚合规则配置:
<definition name="index" template="/siteLayout.jsp"> <put-attribute name="header" value="/header.jsp"></put-attribute> <put-attribute name="content" value="/content.jsp"/> <put-attribute name="footer" value="/footer.jsp"/> </definition>
更多的例子,在这个我曾经已经谈到过的这个页面聚合的话题。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
tiles? 为了配置一个母板页再去单写一份 xml 配置,不知道做这个东西的人是咋想的? 我是极度不适应。 python,.net 都有现成的母板页,java 社区的老大为啥不学学呢?
java 社区还真有你说的这种现成的母版页,不需要写复杂配置的,你可以看看我这篇文章,有提到:http://www.raychase.net/850
另外,tiles 现在也支持正则表达式了,配置可以比你说的简单。
你说的这个我没用过,用过一点 jsp 和 velocity。学习一下
使用 jsp 自定义标签可以缓解这个问题,我喜欢在系统中开发大量的自定义标签