JSP Tag Files 技术总结

最新完整的源码在: 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>'}

参见资源:

©️2020 CSDN 皮肤主题: 岁月 设计师:pinMode 返回首页