最新完整的源码在: http://code.taobao.org/p/bigfoot_v2/src/tags/。
首先声明 Tag File 是门老技术,好用之余知道的人却不多!
简介
以前我们抽取一段JSP代码,整合到完整的页面中,一般使用 include 指令(例如<%@include file="public/nav.jsp"%>),这比较简单的说。而今天要介绍的是 include 的“高级版”——Tag Files。它着实十分强大,不仅可以完全替代 include,而且还可以创建高级的可复用标签库,使得快速开发和维护动态网页比以前更加容易,甚至网页作者无须学习 Java 程序语言本身,就能开发出全新的动态网页。学习过程中,发现以下两点比较有趣,而且都是往事:
- 原来 Tag Files 与 Cold Fusion 颇有渊源——CFML!好生不熟悉啊,当年只在《电脑报》合订本附录上久闻其大名,得知其为 Web 开发先驱中的一名,未竟其标签功能如此强大!
- 当年接触过的 Ext JS 标签库 Ext TLD,原来也是基于 Tag Files 的!
只恨俺当年有眼不识泰山、相见恨晚呀~呵呵~闲话休提,速速进入 Tag Files 之旅吧!
基本用法
首先说说怎么使用 Tag File。拿一个简单的例子。第一步创建被应用的 HTML 片段,假设当前是 Hello.tag,将它放置在 WEB-INF/tags/ 目录下。你可以在 Tag File 里直接使用 JSP 的语法来制作标签。标签文件的扩展名必须是“.tag”。
<% out.println("Hello from tag file."); %>
然后在页面中使用自定义标签时,需要先导入标签库,再使用标签。具体在 JSP 网页使用 Hello.tag 的方法如下:
<%@ taglib prefix="myTag" tagdir="/WEB-INF/tags" %> <myTag:Hello />
其中,prefix 用于确定标签前缀;而tagdir标签库路径下存放很多 Tag File,每 Tag File 对应一个标签。最后执行的结果如下:
Hello from tag file.
十分简单是吧?其实,再复杂的 Tag Files,也要比原生写 SimpleTag、写 Java 代码来得简单。所有 JSP 里面能做的事情,几乎在 Tag Files 里面都可以做的,包括模板语言 JSTL——不过我就没有推荐使用 JSTL,而是直接 Java if/for 来控制某些页面逻辑,也就是 <% ...Java code...%>。出于学习成本的原因,我不想再重复学习类似的东西。当然,EL 表达式我是推荐使用的,如果能够使用 EL 表达式的,尽量使用,能避免 Java Code <%%> 的尽量避免。
该小节小结如下:
导入格式为 <%@taglib prefix="test" tagdir="/WEB-INF/tags"%>
- tagdir:用于指定tag文件目录,当页面使用 <ui:xxxx> 会查找该目录下对应的 xxxx.tag 文件。
- prefix:指定使用时标签前缀
使用:<test:xxxx />
强大的包含接口 attribute
include 指令有个缺点,就是不能对被包含的页面片段进行参数的传递。你可能会想到使用 <jsp:include> 标签,如:
<jsp:forward page="add.jsp"> <jsp:param name="a" value="1" /> <jsp:param name="b" value="2" /> </jsp:forward>
虽然可以传参数,但 <jsp:include> 的方式与接着要介绍的 Tag Files 之 attribute 相比,还是弱很多。attribute 支持依赖性是否可选,类型检查等等更复杂的功能,设置可传递一大段 HTML 过去!例如下面一个例子。
<%@tag description="header 內容" pageEncoding="UTF-8"%> <%@attribute name="title" type="java.lang.String" description="标题" require="true"%> <head> <title>${title}</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head>
本体(本体的描述乃相对于被包含的 Tag File 而言)调用方式:
<%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="html" tagdir="/WEB-INF/tags" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <html:Header title="首页"/> <body> Foo... </body> </html>
像“title="首页"”这样就完成了 title “参数”的传递!当然准确说是 Attribute 属性。并且声明该项属性不能不填(require="true")。而且要求是 String 类型,别的不行哦。还带有 description 说明呢。另外请注意 type 这里不支持泛型,所以没有 Map<String, Object> 那样的写法——填 type="Map" 即可;数组也没问题,填 type="Map[]" 即可。如果前面有 import="java.util.Map" 的话,这里直接填 type="Map" 即可,无须写全称 type="java.util.Map"。
你可能会问,像字符串的类型就可以 title="首页" 这样传,但其他类型呢?你可以直接输入值,如 array="<%=homeService.getMsg()%>";也可以把其他类型保存在 request.setAttribute("foo", Object) 中,然后通过 title="${foo}" 传就可以了。另外对于 Tag File 里面的值获取,EL 表达式也是通用的。
该小节小结如下:
tag 指令如同 JSP 网页的 page 指令,用来设定标签文件 <%@tag display-name="" body-content="" dynamic-attributes="" small-icon="" large-icon="" description="" example=""language="" import="" pageEncoding="" isELIgnored="">
- body-content 表示可能的值有三种,分别是 empty、scriptless、tagdependent、empty。empty 为标签中没有主体内容;scriptlet 为标签中的主体内容 EL、JSP 动作元素,但不可以为 JSP 脚本元素;tagdependent 表示标签中的主体内容交由 tag 自己去处理,默认值为 scriptless;
- dynamic-attributes 表示设定标签文件动态属性的名称,当 dynamic- attributes 设定时,将会产生一个 Map 类型的集合对象,用来存放属性的名称和值;
- description 表示用来说明此标签文件的相关信息;
- example 表示用来增加更多的标签使用说明,包括标签应用时的范例;
- language、import、pageEncoding、 isELIgnored 这些属性与 page 指令相对应的属性相同。
传递 HTML Fragment
支持 HTML 片段传递是 Tag Files 的一大特点,简直令笔者心灵神往。心想,模板机制能做到这样,非常不错!什么标签的继承,都不在话下!
我们把上面的例子改改,变成 Tag:
<%@tag description="HTML 懶人標籤" pageEncoding="UTF-8"%> <%@attribute name="title"%> <html> <head> <title>${title}</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <jsp:doBody/> </body> </html>
注意这次 Tag 变成一个完整的 HTML 页面,而且里面有个 <jsp:doBody /> 特殊标记哦~。
这样的话,那么本体是这样的:
<%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="html" tagdir="/WEB-INF/tags" %> <html:Html title="新增書籤"> <form method="post" action="add.do"> 網址 http:// <input name="url" value="\${param.url}"><br> 網頁名稱:<input name="title" value="\${param.title}"><br> 分 類:<input type="text" name="category" value="\${param.category}"><br> <input value="送出" type="submit"><br> </form> </html:Html>
哈哈~不知你看到没有,<jsp:doBody /> 所指的就是 <html:Html title=...>(这里一大段的 HTML) </html:Html> 中间的内容,整段 Form 标签都传过去 Tag File 里面了。
更妙的是,<html:Html title=...>……</html:Html> 里面还可以是包含有 <%%> 的 incule 指令,注意是包含 <%%>!
<html:Html title=...> <%@include file="public/nav.jsp"%> </html:Html>
如此一来即可规避 body-content="scriptless" 不可出现 <% %>、<%= %> 或 <%! %> 之问题。
更妙的是,两套模板之间还可以相互嵌套的,请看:
<%@tag pageEncoding="UTF-8" description="呼叫客户端组件"%> <%@attribute fragment="true" name="button" required="false" description="按钮"%> <section class="openClient"> <jsp:doBody /> </section> <%@taglib prefix="dhtml" tagdir="/WEB-INF/tags/common/dhtml"%> <dhtml:msgBox title="温馨提示:"> <div align="center"> <jsp:invoke fragment="button"/> </div> </dhtml:msgBox><jsp:invoke fragment="button"/> 嵌入到 dhtml:msgBox 模板中去了。
一个 doBoady 是不够的,——来多几个怎么样?回答是绝对肯定的!只要我们把 Attibute 声明为 fragment="true" 即可。例如下面撰写一个 table.tag:
<%@attribute name="frag1" fragment="true"%> <%@attribute name="frag2" fragment="true"%> <table border="1"> <tr> <td><b>frag1</b></td> <td><jsp:invoke fragment="frag1" /></td> </tr> <tr> <td><b>frag2</b></td> <td><jsp:invoke fragment="frag2" /></td> </tr> </table>
在这个 Tag File 中,将 attribute 的属性设定为 Fragment,然后想取得指定的 Fragment 的话,就可以使用 <jsp:invoke> 动作元素,并指定 Fragment 的名称,使用下面这个 JSP 网页来测试:
<%@taglib prefix="caterpillar" tagdir="/WEB-INF/tags/"%> <html> <body> <caterpillar:table> <jsp:attribute name="frag1"> Fragment 1 here </jsp:attribute> <jsp:attribute name="frag2"> Fragment 2 here </jsp:attribute> </caterpillar:table> </body> </html>
JSP 网页中,同样的是使用 <jsp:attribute> 来指定 Fragment 的文字内容,那么执行这个 JSP 网页的话会生成以下的内容:
<html> <body> <table border="1"> <tr> <td><b>frag1</b></td> <td>Fragment 1 here</td> </tr> <tr> <td><b>frag2</b></td> <td>Fragment 2 here</td> </tr> </table> </body> </html>
该小节小结如下:
这个指令用来设定自定义标签的属性。其中 name 表示属性的名字:<%@attribute name="" required="" fragment="" rtexprvalue="" type="" description=""%>
- required 表示是否为必要,默认为 false;
- rtexprvalue 表示属性值是 否可以为 run-time 表达式。如为 true,表示属性可用动态的方式来指定,如:<mytag:read num="${param.num}"/>,如为 false,则一定要用静态的方式来指定属性值;
- type 表示这个属性的类型,默认值为 java.lang.String;description用来说明此属性的相关信息
返回变量给你:互通有无
Tag File 运算过的结果,都可以返回给本体,灰常强大是吧!?没错哦~的确可以。我们通过 <%@variable%> 指令完成。
注意下面的例子, doBody 多了 var="code":
<%@attribute name="preserve" fragment="true" %> <%@variable name-given="code" scope="NESTED" %> <jsp:doBody var="code" /> <table border="1"> <tr> <td> <pre><jsp:invoke fragment="preserve"/></pre> </td> </tr> </table>
本体文件:
<%@taglib prefix="caterpillar" tagdir="/WEB-INF/tags"%> <html> <body> <caterpillar:precode> <jsp:attribute name="preserve"> <b>${ code }</b> </jsp:attribute> <jsp:body> PROGRAM MAIN PRINT 'HELLO' END </jsp:body> </caterpillar:precode> </body> </html>
也许大家会问,这个所谓“强大”的功能有什么用呢?——很简单,你想想,把逻辑封装起来里,让变化的只是标签,也就是本体里面的——实际上也是如此,通常变化的都是 HTML 标签。那么要输出的值便是这些变量了,返回给本体,让本体去控制显示。标签这个特性在制作“标签迭代器”的时候十分适用,且看例子:
<%@tag pageEncoding="UTF-8" description="文章功能模块" import="java.util.Map, com.ajaxjs.core.Util"%> <%@attribute name="array" type="Map[]" required="true" description="要迭代的数据,是一个Map数组"%> <%@attribute name="itemTag" fragment="true" required="true" description="文章列表"%> <%@variable name-given="current" %> <ul> <% // 列出栏目 if(Util.isNotNull(array)){ for(Map item : array){ jspContext.setAttribute("current", item); %> <li> <jsp:invoke fragment="itemTag" /> </li> <% } } %> </ul>
本体:
<menu class="sub"> <%@taglib prefix="commonTag" tagdir="/WEB-INF/tags/common/html"%> <commonTag:iterator array="${results}"> <jsp:attribute name="itemTag"> <a href="${pageContext.request.contextPath}/blog/${current.uid}.sectionList">${current.name}</a> </jsp:attribute> </commonTag:iterator> </menu>
设置 name-from-attribute 还可以指定传递变量的名称!由主体来定义而不是 Tag File 来定义,也就是说,把上述例子的 current 改为你喜欢的!
该小节小结如下:
这个指令用来设定标签文件的变量,其中 name-given 表示直接指定变量的名称: <%@variable name-given="" name-from-attribute="" alias="" variable-class="" declare="" scope="" desription="">
- description 用来说明此变量的相关信息
- name-from-attribute 表示以自定义标签的某个属性值 为变量名称;
- alias 表示声明一个局部范围属性,用来接收变量的值;
- variable-class 表示变量的类名称,默认值为 java.lang.String;
- declare 表示此变量是否声明默认值为 true;
- scope 表示此变量的范围,范围是:AT_BEGIN、 AT_END 和 NESTED,默认值为 NESTED;作用范围为"NESTED",也就是在起始卷标与结束卷标之间
如何编译页面的?
Tag File 是自定义标签的简化。事实上,就如同 JSP 文件会编译成 Servlet 一样, TagFile 也会编译成 Tag 处理类,自定义标签的后台依然由标签处理类完成,而这个过程由容器完成。关于编译,参见:
前面提過Tag File會被容器轉譯,實際上是轉譯為javax.servlet.jsp.tagext.SimpleTagSupport的子類別。以Tomcat為例,Errors.tag轉譯後的類別原始碼名稱是Errors_tag.java。在Tag File中可以使用out、config、request、response、session、application、jspContext等隱含物件,其中jspContext在轉譯之後,實際上則是javax.servlet.jsp.JspContext物件。
所以,Tag File在JSP中,並不是靜態包含或動態包含,在Tag File中撰寫Scriplet的話,其中的區域變數也不可能與JSP中Scriptlet溝通。
JspContext是PageContext的父類別,JspContext上定義的API不像PageContext有使用到Servlet API,原本在設計上希望JSP的相關實現可以不依賴特定技術(例如Servlet),所以才會有JspContext這個父類別的存在。
附加一个例子:用 TagFile 完成一个迭代器:
<%@tag pageEncoding="UTF-8" description="文章功能模块" import="java.util.Map, com.ajaxjs.util.Util"%> <%@tag trimDirectiveWhitespaces="true"%> <%@attribute name="array" type="Map[]" required="true" description="要迭代的数据,是一个Map数组"%> <%@attribute name="itemTag" fragment="true" required="true" description="文章列表"%> <%@attribute name="isOutterInclude" type="Boolean" required="false" description="是否不包括外层标签,外层标签通常指的是 ul"%> <%@attribute name="isOutterItemInclude" type="Boolean" required="false" description="是否不包括外层 item 标签,外层标签通常指的是 li"%> <%@variable name-given="current"%> ${isOutterInclude? '' : '<ul>'} <% // 列出栏目 if(Util.isNotNull(array)) { for(Map<?, ?> item : array) { jspContext.setAttribute("current", item); %> ${isOutterItemInclude? '' : '<li>'} <jsp:invoke fragment="itemTag" /> ${isOutterItemInclude? '' : '</li>'} <% } }else{ System.out.println("iterator.tag 列表控件输出没有数据"); %> <span class="noRecord">没有记录</span> <% } %>${isOutterInclude? '' : '</ul>'}
参见资源:
- 我封装的标签库: http://code.taobao.org/p/bigfoot_v2/src/tags/
- 《JSP2.0 技术手册》
- Tag File 支持
- 簡介 Tag File
- jsp2.0 标记文件(tag)详解,告诉你怎么 include jsp 到 Tag Files
- JSP2.0 的福利(标签文件)