天天看點

JSP模闆繼承功能實作

背景

最近剛入職新公司,浏覽一下新公司項目,發現項目中大多數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. 公共部分資訊直接在模闆中維護,可變部分在模闆中定義占位符,然後由頁面進行重寫來維護不同頁面的多樣性。

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

JSP模闆繼承功能實作

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

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

模闆内容大概是這個樣子的:

JSP模闆繼承功能實作

實作原理

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

BlockTag

該标簽主要用于在模闆檔案中定義相應的子產品(可以看做一個占位符),在渲染JSP頁面時會将标簽定義的位置替換為頁面重寫的内容,替換時根據标簽的name屬性加上特定的字首作為key值從request的attribute中讀取内容。

JSP模闆繼承功能實作
JSP模闆繼承功能實作
/**
 * 自定義标簽,用于在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模闆繼承功能實作
JSP模闆繼承功能實作
/**
 * 自定義标簽,用于在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模闆即可,具體如何抽象還需根據實際的場景差別對待。

繼續閱讀