JSP模板繼承功能實現

逆風之羽發表於2015-06-24

背景

最近剛入職新公司,瀏覽一下新公司專案,發現專案中大多數JSP頁面都是獨立的、完整的頁面,因此許多頁面都會有如下重複的程式碼:

<%@ page language="java" contentType="text/html; charset=UTF-8" import="java.util.Calendar"    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> 
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="/common-tags" prefix="m"%>
<c:set var="ctx" value="${pageContext.request.contextPath}"></c:set>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<title>${webModule.module.name} ---xxxx</title>
<meta name="keywords" content="xxxx"/>
<meta name="description" content="xxxx"/>
<link rel="stylesheet" href="${ctx}/css/web-bbs.css"/>
<link rel="stylesheet" href="${ctx}/css/page.css"/>
<script type="text/javascript" src="${ctx}/js/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="${ctx}/js/bbs.js"></script>
<script type="text/javascript" src="${ctx}/js/webUtil.js"></script>
<script type="text/javascript" src="${ctx}/js/index.js"></script>
<script type="text/javascript" src="${ctx}/js/faces.js"></script>

小夥伴們每新新增一個頁面,就需要copy一份上面這坨程式碼,還需要在各自頁面重複引入公共的頭尾檔案(如header.jsp,footer.jsp等)。。。
對於這種開發方式,重複的工作量就不多描述了,更重要的問題是這種架構方式未來會導致更多的維護工作量、甚至是bug隱患。
舉兩個“栗子”:

  • 如果今後開發過程中我們需要全域性引入、刪除一些公共的指令碼(例如線上客服圖示、GA分析指令碼等),變更一下jQuery的版本,更改DocType型別為Html5型別等等。要完成類似的需求我們必須逐個修改JSP檔案,工作量就會與專案中JSP檔案數量成正比。
  • 更麻煩的問題是,對於上述這些全域性操作我們無法保證程式碼是否是在所有頁面上都生效了,手工檢查?呵呵...

解決方案

上面扯了那麼多,其實核心問題就是所有的jsp頁面都是各自為戰,沒有一個統一的公共的模板來維護一些全域性的資訊,所以這裡就介紹一下我們以前的實現方案:

  1. 實現JSP檔案的模板功能、讓所有的頁面都引入一個公共的模板。
  2. 公共部分資訊直接在模板中維護,可變部分在模板中定義佔位符,然後由頁面進行重寫來維護不同頁面的多樣性。

有了模板以後就可以這樣寫頁面了:

      

這樣的寫法好處顯而易見:

  • 首先,頁面結構一目瞭然,寫頁面時無須再關注內容以外的公共部分,減少了許多copy程式碼的工作量,同時也降低出錯率
  • 其次,公共樣式、指令碼等都在模板中引入,便於統一調整

模板內容大概是這個樣子的:

      

實現原理

實現原理其實很簡單,模板功能的實現主要是兩個自定義標籤(自定義標籤的開發步驟這裡就不講了)

BlockTag

該標籤主要用於在模板檔案中定義相應的模組(可以看做一個佔位符),在渲染JSP頁面時會將標籤定義的位置替換為頁面重寫的內容,替換時根據標籤的name屬性加上特定的字首作為key值從request的attribute中讀取內容。

/**
 * 自定義標籤,用於在Jsp模板中佔位
 * 
 * @author 逆風之羽
 *
 */
public class BlockTag extends BodyTagSupport {
    /**
     * 佔位模組名稱
     */
    private String name;

    private static final long serialVersionUID = 1425068108614007667L;
    
    @Override
    public int doStartTag() throws JspException{
        return super.doStartTag();                
    }
    
    @Override
    public int doEndTag() throws JspException {
        ServletRequest request = pageContext.getRequest();
        //block標籤中的預設值
        String defaultContent = (getBodyContent() == null)?"":getBodyContent().getString();        
        String bodyContent = (String) request.getAttribute(OverwriteTag.PREFIX+ name);
        //如果頁面沒有重寫該模組則顯示預設內容
        bodyContent = StringUtils.isEmpty(bodyContent)?defaultContent:bodyContent;
        try {
            pageContext.getOut().write(bodyContent);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }        
        // TODO Auto-generated method stub
        return super.doEndTag();
    } 
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
BlockTag程式碼

OverwriteTag

該標籤主要用於在最終的頁面上重寫模板中的相應模組,在頁面渲染時將標籤內部的內容寫入到當前request的attribute中,該標籤有一個必填引數name屬性作為該內容的key值,這個name屬性必須要和模板中對應要重寫的block的name值相同。

/**
 * 自定義標籤,用於在jsp模板中重寫指定的佔位內容
 * 
 * 基本原理:
 *         將overwrite標籤內容部分新增到ServletRequest的attribute屬性中
 *         在後續block標籤中再通過屬性名讀取出來,將其渲染到最終的頁面上即可
 * 
 * @author 逆風之羽
 *
 */
public class OverwriteTag extends BodyTagSupport {

    private static final long serialVersionUID = 5901780136314677968L;
    //模組名的字首
    public static final String PREFIX = "JspTemplateBlockName_";
    //模組名
    private String name;
    
    @Override
    public int doStartTag() throws JspException {
    
        // TODO Auto-generated method stub
        return super.doStartTag();
    }
    
    @Override
    public int doEndTag() throws JspException {
        ServletRequest request = pageContext.getRequest();
        //標籤內容
        BodyContent bodyContent = getBodyContent();
        request.setAttribute(PREFIX+name,  StringUtils.trim(bodyContent.getString()));        
        // TODO Auto-generated method stub
        return super.doEndTag();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }    
}
OverwriteTag程式碼

總結與擴充

  1. 所有頁面都使用了模板以後,就可以很方便的控制專案全域性的樣式、指令碼,由於遮蔽了許多頁面公共資訊,也使得日常頁面開發更加高效並減少錯誤率。
  2. JSP原生是不支援模板機制的,但是僅僅稍加一些手段使用兩個自定義標籤就可以實現模板功能,減少了許多重複的工作量。因此,工作過程中的痛點往往也是個人獲得成長的機會。
  3. 我在上面Demo中只簡單定義了一個base_template.jsp這一個模板,但是實際場景中一個網站可能有許多佈局風格不同型別的頁面,那麼一個模板顯然不能滿足多樣性的佈局要求,這時我們就可以給模板進行分級將模板定義為base,common,channel三個級別,抽象程度從高到低,實現channel->common->base的繼承關係,不同風格的頁面只需要引入對應的channel模板即可,具體如何抽象還需根據實際的場景區別對待。

相關文章