最近看到俱乐部有小伙伴自己开发的项目中有一些是使用的老版本的jfinal框架,比如经典的2.2版本,前端的页面也是使用的上古时代的jsp。自从jfianl 进入3.x时代,也引入了自家的模板引擎enjoy template之后,其强大程度已经远远把老版本甩在身后。因此我们很多人会有将项目的jfinal框架升级,并用enjoy模板替代jsp的想法,但又怕折腾过大,引发各种问题,于是陷入犹豫之中。
因为我之前成功的使自己公司的项目的jfinal框架从2.2升级到3.1,现在又升级到3.2,虽然中间确实遇到了一些问题,但在自己的钻研和波总耐心的指导下,终于如愿以偿,升级之后带来的好处和收获是远远大于付出的!
本文主要分享jsp改造的过程,但想要使用enyoy模板引擎,升级是必行的,先概括几个要点,给目前正在升级或者打算升级的小伙伴抛砖引玉一番。
1、enjoy模板是jfinal 3.x系列的产物,但并不具有捆绑关系。如果你的项目中的页面使用的其他模板引擎,甚至是jsp,你也可以继续沿用,丝毫不影响你把jfinal的版本更新到最新的3.2,而更新的步骤可以直接参考文档说明完成,没有任何难度,所以如何升级框架不是本文的重点。
2、3.2中的Action形参注入功能,是jfinal-java8分支特有,如果你的项目部署的环境依赖于低版本的jdk,为保证稳定,可以仅升级到兼容老版本jdk的jinal 3.2版本,放弃使用Action形参注入特性,或自己实现。其实使用jfinal以后,即使不提供这个特定,对我们的开发影响也微乎其微...
3、按照文档的方式升级,正常情况下不会有任何问题,但是我在升级过程中却遇到了一些奇葩问题,甚至连波总开始也觉得匪夷所思。其中最奇怪的一个问题就是将jfinal的版本依赖从2.2改成3.1之后,启动项目,居然报错说我引用的SDK版本是jre而不是jdk...所以在这里说一声,当你按照文档将框架版本升级,将所有明显的编译错误都纠正过来以后启动项目还是报一些莫名其妙的错误的时候,不妨将整个项目除源码,资源文件,pom.xml文件(maven项目),版本控制文件(使用了SVN或Git,默认是隐藏文件)以外的所有文件全部删除,比如IDE自己生成的项目配置文件或文件夹,比如.idea文件夹(使用IDEA),比如.classpath(使用Eclipse),基本所有以"."开头的文件或文件夹都是删除对象。删除这些东西后再重新导入项目。IDE会重新配置项目的工作环境,往往能解决很多疑难杂症,这是波总传授的经验,很有效哦!
4、关于jfinal的官方文档。工欲善其事必先利其器,想要用好jfinal,必须仔细研读该文档。其实相比如国外流行框架动辄几百页的user guide,甚至各种专业学习书籍,jfinal的文档可谓苗条,和波总的设计理念一致,文档也做到了极简!不说通读整个文档后会成为使用jfinal的高手,但是绝对能无师自通框架中最核心的用法,自己用来独立开发系统的各个模块是没有丝毫问题的。如果想要收获更多,那么假如俱乐部则是最好的选择!
好了,关于jfinal老版本的升级就唠叨这么多,下面进入正题。关于JSP页面改造,这个应该是很多老项目的开发者关心的问题。经过我的个人实践,配合enjoy模板,能够很快速的把规范的jsp页面改造成html页面(规范的页面指采用了合适的布局,并且存在合理分层,比如是使用include消除重复代码的页面),我们只需要将项目的页面的公共部分剥离出来做成若干layout(布局页面),然后保留每个页面特有的部分,就基本大功告成了,当然这只是最基础的做法,特点就是快,可以几分钟改造好一个页面。
如果项目中的页面实在太多,比如上百个了,那么可以考虑已经完成的页面不再改动,而新加的功能则使用html页面,并用enjoy渲染。因为jfinal的render方法多种多样,是完全可以支持同时使用多种模板引擎来渲染页面的。
最后,通过我自己项目的实例,图文假说jsp改造的几个核心步骤。我的项目页面还算简单,也没有经过太细致的优化,比如页面js完全分离这种就没有做。因此仅提供思路给大家,具体的改造,因人而异,欢迎指教!
1)项目的核心配置类,即继承JfinalConfig的类,如果在configConstant中该设置了viewType为JSP,移除该配置,如下图最后一行代码注释或直接删除:
public void configConstant(Constants me) {
// 加载少量必要配置,随后可用PropKit.get(...)获取值
PropKit.use("a_little_config.txt");
me.setDevMode(PropKit.getBoolean("devMode", false));
//me.setViewType(ViewType.JSP);
}
enjoy模板问世后,代替了FreeMarker成为默认的viewType,所以这里不需要再额外设置。
2)浏览项目目前的jsp页面,大致划分下页面的分类,如列表页,编辑页,详情页等,基本上这三类页面是最常见的。下面来看一下一个改造前的一个列表页。
上图是一种最常规的jsp布局模式,我特意省略了大量的业务代码,只保留了jsp页面的基本骨架,可以看出这个页面的构成如下:
a.头部的page声明和include页面引入,引入的页面taglib.jsp包含着一些jstl标签库或自定义标签、常用变量的声明,比如上下文ctx变量,taglib.jsp内容一般是这样的:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>
<%@ taglib uri="/jc-tag" prefix="jc"%>
b.head部分,一些meta标签,title,以及最重要的是公用的css部分,这一部分css,我们一般也会剥离到单独的页面中引入。如图中的basecss.jsp。
c.页面主体部分,其实最关键的就是上图中的核心html与核心js,这一部分内容每个页面都有所不同,有具体的业务逻辑,其余部分可归于页面骨架,比如某一个大的div块等。其中basejs.jsp页面看名字就知道,页面公用的js引用都在这个页面了。
经过如下分析,我们可以写一个布局页面,比如index_layout.html,内容如下:
#define indexLayout()
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta charset="utf-8" />
<title>xxx平台管理系统</title>
<meta name="description" content="overview & stats" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
### 引入基础css(必须)
#@includeBaseCss()
### 引入页面自身需要的其他css,需要各自页面自己实现该方法(可选,所以使用?,安全调用)
#@includePageCss?()
</head>
<body class="no-skin">
<!-- /section:basics/navbar.layout -->
<div class="main-container" id="main-container">
<script type="text/javascript">
try{ace.settings.check('main-container' , 'fixed')}catch(e){}
</script>
<div class="main-content" id="page-wrapper">
<div class="page-content" id="page-content">
<div class="row">
### 显示主体部分内容,各页面自身提供方法的实现
#@mainContent()
</div>
</div>
</div>
</div><!-- /.main-container -->
### 加载基础js(必须)
#@includeBaseJs()
### 加载当前页面自身js,需调用页面自身实现(可选,所以使用?,安全调用)
#@includePageJs?()
</body>
</html>
#end
以上代码就是一个典型的布局页面,将前面提到的jsp页面的公用部分剥离出来而成。下面简单讲解,会涉及到enjoy模板的语法,这里不会详细描述,初学者可以配合文档一起学习。
a.#define xxx(),这是enjoy模板的一个定义函数,在enjoy模板中所有的指令均以单个#开头,end结束。define指令相当于定义一个函数,这个函数的主体内容往往是一段html代码,当调用这个函数时,将会在调用处输出函数中编写的html代码段。如下所示:
#define indexLayout()
...//html代码
#end
b.### xxxxx,enjoy模板中的注释以三个#号开头,一般用于描述某一个区域的开始或者结束,或者添加简要说明,如下:
### 引入基础css(必须)
c.#@includeBaseCss()、#@includePageCss()?、#@includeBaseJs()、#@includePageJs()
#@xxxx(),以#@开头,xxxx是方法名,这个就是指令函数的调用方式,用来调用通过#define指令定义的方法,比如我们如果要调用上文提到的indexLayout()方法,就直接写#@indexLayout()即可,与定义函数时的区别,就是#后多写了一个@符号,接方法名,不需要#end结束。
那么在一个布局页面的define指令中调用其他的指令函数的意义何在?其实就是为了渲染每个页面中特有的一部分!我这里提到的四个指令函数,其实是渲染了整个页面中除页面骨架外的其他部分,看调用的函数名称便可知,分别是渲染通用css引用,页面特有css引用,通用js引用,页面特有js引用。当然其实我们可以直接把对通用css和通用js的引用代码直接写在这个布局页面里也是没有问题的。通用css页面和通用js页面代码大致就是这么一些东西,稍微搞过前端的程序员都知道的。以下是两个html页面,分别引用了公用的css和js的引资源,注意#define要用#end结束,截图中没有显示出来。
.
basecss.html
basejs.html
这里我们需要关注的不是页面的内容,而是又发现两个#define指令,注意指令的名称分别为includeBaseCss和includeBaseJs。而在我们之前提及的布局页面index_layout.html中有如下代码:
### 引入基础css(必须)
#@includeBaseCss()
### 引入页面自身需要的其他css,需要各自页面自己实现该方法(可选,所以使用?,安全调用)
#@includePageCss?()
其实,这里的#@includeBaseCss()就是对使用#define指令定义的includeBaseCss方法的一次调用。可以看到在布局页面中,就是通过对自定义的指令函数的调用来实现类似于jsp中的include的作用,填充具体页面中特有的部分。那么上图中的#@includeBaseCss()和#@includePageCss?()有何区别呢?
区别就在于函数名之后的?号,?号在enjoy模板中是用来做安全输出,不带?号,表示这个函数一定存在,也是页面必须渲染的一部分,而带有?号的方法,则表示部分页面需要渲染这个函数里面的代码,其他的则不需要,那么就可以对此类函数调用处加?号,表示非必须执行,除非有使用#define指令定义的同名函数,才会进行渲染。
通过#@includeBaseCss(),我们将公用的css资源都引入了,如果当前页面还有额外的css需要引入怎么办,这时布局页面index_layout.html中的#@includePageCss?()就有用武之地了。比如当前页面需要用到ztree的css,因为要做树形菜单,我们就可以在页面上通过define指令来渲染这个css的引入,如
#define includePageCss()
<link rel="stylesheet" href="#(ctx)/res/js/ztree/css/zTreeStyle/zTreeStyle.css" type="text/css">
#end
至于js引用部分的处理,思路和css引用处理一致,通过includeBaseJs函数引入通用js,再通过定义includePageJs函数来实现具体页面特有的js代码即可。注意这个布局页面中的#@mainContent,这是一个函数调用,其实就是对页面中核心业务html代码的渲染,这个函数的实现每个页面都不同,所以在布局页面中以指令函数调用的方式存在,由具体页面实现具体的渲染逻辑。
对于通用的指令函数,我们一般都在配置类中直接引入,比如这里我们可以直接在布局页面中调用includeBaseCss和includeBaseJs,是因为配置类中将定义这两个函数的html页面这作为全局共享函数添加到进来了。所以任何使用enjoy模板的页面可以通过#@函数名()的方式调用这些html中定义的函数,具体列子如下图所示:
@Override
public void configEngine(Engine me) {
me.addSharedObject("ctx", JFinal.me().getContextPath());
me.setDevMode(appConfig.getBoolean("devMode", false));
//使用JF模板渲染通用页面
me.addSharedFunction("/WEB-INF/view/common/basecss.html");
me.addSharedFunction("/WEB-INF/view/common/basejs.html");
me.addSharedFunction("/WEB-INF/view/common/top.html");
me.addSharedFunction("/WEB-INF/view/common/index_layout.html");
me.addSharedFunction("/WEB-INF/view/common/editpage_layout.html");
//扩展指令实现菜单动态化
me.addDirective("menu",new MenuDirective());
//扩展指令实现button按钮生成
me.addDirective("button",new ButtonDirective());
}
从上图图配置可以看到,通过addSharedFunction方法,我把通用的css引用页面,通用js引用页面,顶部通用页面,各模块主页布局页面,编辑页布局页面全部设置成了共享函数,意味着,前端所有的html页面,都可以通过#@xxx来调用到这些页面通过#define指令定义的各种函数了。至于addDirective,这是自定义指令扩展,用于代替jstl的自定义标签,后面再说。
3)了解清楚上述布局页面的使用和指令函数的调用方式后,我们终于可以编写特定模块一个具体的页面了,前期的工作都做好以后,这个页面其实非常简单了。比如一个列表页,整体页面如下(折叠业务代码)
.
从这个页面,我们就可很清楚的看出前期的铺垫起了什么作用,第一行调用了布局页面index_layout.html中定义的indexLayout方法,渲染出了整个页面的骨架部分。其实这个时候,页面中的公用部分就都包含了,剩下的部分其实就是靠调用三个指令函数mainContent()--主体内容,includePageCss()--特有css,includePageJs()--特有js,来实现页面的填充补全功能!第一个是必须的,所以必须实现,后面两个则是可选的,因为布局页面index_layout.html中调用的时候加了?号表示安全输出,页面没有定义则不执行这个函数。所以我们可以看到在这个列表页中我们在执行了indexLayout方法后,仅仅定义了mainContent和includePageJs的实现,因为不需要加载额外的css资源,所以并没有定义includePageCss指令函数,这样就完成了整个页面的渲染工作,是不是非常爽呢?相比jsp中大量重复的引用,就算是把一部分引用整合在单独的页面中去引用,还是显得不够简洁。而enjoy template这种方式,如果使用得当,几乎可以做到具体页面上只包含需要的html和js代码,其余的部分全部被封装为指令函数进行调用,几行代码就消除了大量的重复html代码块!
4)接下来,当然是要去除一些jsp页面独有的语法,大部分情况下当然是el表达式的运用,这个非常简单,直接用IDE的搜索功能,搜索"${"进行替换即可,使用#(xxx)替换即可,关于#()指令,是enjoy模板中唯一没有函数名的指令,它就是enjoy模板的输出指令,可以认为enjoy的#(xxx)等同于${xxx},但比jstl标签强大很多。语法也和java基本相同,学习成本很低。同理#for指令可以代替c标签的foreach,#if指令可以代替c标签的if等等,不再阐述详细用法,文档已经有非常详实的解说了。
5)除了el表达式用#()指令替代,我们有时候也会使用自定义标签配合tld文件,实现对包含复杂逻辑的html代码块进行简化输出,比如自定义标签实现动态菜单,按钮或者分页等等,这样可以把页面的生成逻辑放在后端完成,前端只需要一行代码调用即可。确实非常方便,enjoy模板怎么能少得了这个功能呢?
举个简单的例子,jsp页面通过定义自定义标签,实现了动态构造button按钮的功能。后台我们通常需要定义这么一个标签类。
.
然后需要在web-inf下建立这么一个tld文件
然后在我们的taglib.jsp中将自定义标签引入
<%@ taglib uri="/jc-tag" prefix="jc"%>
完成以上步骤之后,我们才可以在jsp页面使用自定义标签,像这样。
<jc:button className="btn btn-primary" id="btn-add" textName="添加"/>
<jc:button className="btn btn-info" id="btn-edit" textName="编辑"/>
使用enjoy模板,我们需要自定义扩展指令,比如我们也顶一个动态标签的自定义指令类,叫ButtonDirective。
package cn.ablefly.iov.core.directive;
import cn.ablefly.iov.model.SysUser;
import com.jfinal.kit.StrKit;
import com.jfinal.template.Directive;
import com.jfinal.template.Env;
import com.jfinal.template.stat.ParseException;
import com.jfinal.template.stat.Scope;
import java.io.Writer;
import java.util.Map;
/**
* Created by xyzx1 on 2017-06-25.
* 使用jf模板引擎扩展指令构造系统左侧菜单,代替jstl自定义标签的实现方式
*/
public class ButtonDirective extends Directive {
private String userId="";//用户id
private String btnId="";//按钮id属性值
private String className="";//按钮class名称
/**
* 需要的权限(按钮跳转地址)
*/
private String permission="";//按钮跳转路径
private String style="";//按钮自定义样式
private String btnName="";//按钮name属性值
private String btnValue="";//按钮value属性值
@Override
public void exec(Env env, Scope scope, Writer writer) {
boolean hasAuth=true;
//表达式必须通过exprList.eval(scope)求值,返回的数据形式与前端传递的数据格式一致
Map<String,Object> params= (Map<String,Object>) exprList.eval(scope);
if(params.get("permission")!=null && params.get("userId")==null){
throw new ParseException("按钮需要权限控制,请传入userId属性", location);
}
if(params.get("userId")!=null){
userId=String.valueOf(params.get("userId"));
}
if(params.get("btnId")!=null){
btnId=params.get("btnId").toString();
}
if(params.get("className")!=null){
className=params.get("className").toString();
}
if(params.get("permission")!=null){
permission=params.get("permission").toString();
}
if(params.get("style")!=null){
style=params.get("style").toString();
}
if(params.get("btnName")!=null){
btnName=params.get("btnName").toString();
}
if(params.get("btnValue")!=null){
btnValue=params.get("btnValue").toString();
}
StringBuffer button=new StringBuffer("");
if(StrKit.notBlank(permission)){
SysUser curUser=SysUser.me.findById(userId);
if(curUser==null||!curUser.getPermissionSets().contains(permission)){
hasAuth=false;
}
}
if(hasAuth){
button.append("<input type=\"button\"");
if(StrKit.notBlank(className)){
button.append(" class=\""+className+"\"");
}
if(StrKit.notBlank(btnId)){
button.append(" id=\""+btnId+"\"");
}
if(StrKit.notBlank(btnName)){
button.append(" name=\""+btnName+"\"");
}
if(StrKit.notBlank(style)){
button.append(" style=\""+style+"\"");
}
if(StrKit.notBlank(btnValue)){
button.append(" value=\""+btnValue+"\"");
}
button.append("/>\n");
}
write(writer,button.toString());
}
}
然后在主配置中的configEngine中添加这个扩展指令,如下添加了动态菜单指令和自定义按钮指令。
//扩展指令实现菜单动态化
me.addDirective("menu",new MenuDirective());
//扩展指令实现button按钮生成
me.addDirective("button",new ButtonDirective());
完成以上步骤以后,我们在页面上就可以使用啦,效果和使用自定义标签一模一样。
#button({btnId:"btn-add",className:"btn btn-primary",btnValue:"添加"})
#button({btnId:"btn-edit",className:"btn btn-info",btnValue:"编辑"})
#button({btnId:"btn-grant",className:"btn",btnValue:"资源授权",permission:"/sys/role",userId:session.sysUser.id??})
以上自定义指令,第三个级根据·后台自定义Directive的要求,传入了对应的permisson和userId,从而实现细密度的权限控制,而第一个和第二个,等同于只做了菜单过滤,只要能访问该页面,那么这个页面上不不需要额外权限控制的按钮就都可以使用。
jfinal club中关于权限控制的设计远比我这个例子优雅,简洁,强大,建议大家学习。这里主要目的是演示Directive的一个使用场景。
关于自定义指令的实现,请参考文档自行学习,这是个非常强大的功能。
完成了以上五个步骤后,一个页面从jsp改造成使用enjoy引擎的html页面就大体完成了,其实改造的主要工作量在于布局页面的编写,el表达式和jsp特有语法的替换,页面主体部分html与核心js这些基本上都可以直接从现有页面直接copy过来,定义成指定函数在对应的布局页面调用进行渲染就可以了。并且有一点,页面的渲染顺序其实只和指令函数的调用位置有关,而和函数的定义位置无关,这是一个很棒的特性,不知各位体会到了没有。我的一个项目大概几十个页面,通过以上方式,3天不到就全部改造完成!
最后,额外再补充几点,我们可能喜欢用c标签的set方法定义上下文ctx在页面上使用,jfinal中我们可以直接添加如下配置使用上下文,在configEngine中添加如下代码即可,然后在页面上直接使用#(ctx)就可以获取上下文路径了,是不是很爽?
me.addSharedObject("ctx", JFinal.me().getContextPath());
另外我们的web项目,可能需要使用session,然而enjoy模板并不是仅仅针对web项目设计,所以默认是不能直接使用#(xxx)获取session中的数据的,我们需要在主配置类的configInterceptor加入以下代码:
me.add(new SessionInViewInterceptor());
这样我们html页面就可以通过#(session.xxx) 获取session中的数据了! 在jfinal-club项目中波总已经放弃是用了传统的session,自己实现了session的功能,大家可以去看俱乐部代码,有很多优点哦,请自行挖掘!
还有关于jfinal的controller中的render和renderXXX系列方法。其中render()方法是以默认的viewType进行渲染,如果默认的viewType是Jsp,那么render()=renderJsp(),如果默认的viewType为enjoy template,那么render()=renderTemplate()。所以具体的renderXXX()方法就是在我们偶然需要渲染一个主视图类型以外的页面时,提供的渲染方式,比如我们完成了项目框架的升级,并且将原有默认的viewType为jsp的设置去除,采用enjoy template为主视图,但又想原有jsp页面不受影响,我们就可以把原来的render("xxx.jsp")方法统一改为renderJsp("xxx.jsp"),新的页面我们则可以直接用render("xxx.html"),并配合使用很爽的enjoy模板引擎了!
好了,本次分享到此结束,希望对jfinal的初学者和对jfinal框架升级还犹豫不前的小伙伴指一条明路。有任何问题,欢迎沟通!最后再次感谢波总在我升级路上遇到的一些奇葩问题的耐心引导!
感谢你的分享,不枉我花了这么多时间啊