支持ajax的静态页面生成

目前主流的页面静态技术都是基于模板生成的,但是对于一些采用ajax+js渲染的页面,这种方法是无能为力的。要解决这个问题,首先要有一个能模拟浏览器的运行环境,其他问题都比较容易解决。能模拟浏览器的技术有好多,seleninum , htmlunit等。其中htmlunit是java开发用无界面的浏览器,速度和性能非常好,对html建模并且提供API来访问页面,点击链接等等,不需要任务驱动程序 ,提供javascript执行环境,现在很多支持ajax网络爬虫也是在它基础上实现的。

如何基于htmlunit实现ajax页面静态化呢?下面我用一个例子阐述吧,没什么比用代码更直接清楚。这个例子有个ajax渲染的页面,页面主要有两块内容,顶部是用户信息,下面是读取osc 首页的综合资讯,基本需求是综合资讯内容要静态化,用户信息不需要。

index.jsp页面代码

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body  whitelist="/userServlet" >
<div id="top"></div>
<h1>osc综合资讯</h1>
<div id="content"></div>
<div>
<button  onclick="generateStaticHtml(this);">生成静态页面</button>
<script type="text/javascript">
//渲染页面
(function renderPage(){
	
	var xmlhttp = new XMLHttpRequest() ;
	xmlhttp.onreadystatechange=function()
	  {
	  if (xmlhttp.readyState==4 && xmlhttp.status==200)
	    {
	    document.getElementById("top").innerText=xmlhttp.responseText;
	    }
	  }
	xmlhttp.open("GET","${pageContext.request.contextPath }/userServlet",true);
	xmlhttp.send();
	
	var xmlhttp2 = new XMLHttpRequest() ;
	xmlhttp2.onreadystatechange=function()
	  {
	  if (xmlhttp2.readyState==4 && xmlhttp2.status==200)
	    {
	    document.getElementById("content").innerHTML=xmlhttp2.responseText;
	    }
	  }
	xmlhttp2.open("GET","${pageContext.request.contextPath }/contentServlet",true);
	xmlhttp2.send();
	
})() ;

function generateStaticHtml(btn){
	btn.innerText = "在处理中,请稍后"
	var xmlhttp = new XMLHttpRequest() ;
	xmlhttp.onreadystatechange=function()
	  {
	  if (xmlhttp.readyState==4 && xmlhttp.status==200)
	    {
		  btn.innerText ="重新生成" ;
		  window.open("${pageContext.request.contextPath }/index.html") ;
	    }
	  }
	xmlhttp.open("GET","${pageContext.request.contextPath }/generateStaticServlet",true);
	xmlhttp.send();
}

</script>
</div>
</body>
</html>




动态页面效果

支持ajax的静态页面生成_第1张图片

注意上面图的两个ajax是加载动态内容触发,然后用javascript渲染到页面

点击"生成静态页面“按钮会触发后台调用静态组件生成静态页面(index.html)

/**
	 * 触发生成静态页面
	 */
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		new StaticHtml().process("http://127.0.0.1:8080/ajax/index.jsp", request.getServletContext().getRealPath("/index.html"));
	}



/**
 * 生成静态页面组件
 * @author Wen
 *
 */
public class StaticHtml {

	//javascript 拦截   ajax 请求
	private final static String ajaxInterceptJs = "(function(XHR) { "
			+ "var open = XHR.prototype.open;"
			+ "var send = XHR.prototype.send;"
			+ "%s"
			+ "XHR.prototype.open = function(method, url, async, user, pass) {"
			+ "    this._url = url;"
			+ "    open.call(this, method, url, async, user, pass);" 
			+ "};"
			+ "XHR.prototype.send = function(data) {" 
			+ "	if(XHR[this._url]){"
			+ "		this.abort() ;" 
			+ "		delete XHR[this._url] ;" 
			+ "		return  ;"
			+ "	}" + "	send.call(this, data);" + "}" 
			+ "})(XMLHttpRequest);";

	public void process(String dynamicUrl, String staticPath)
			throws FailingHttpStatusCodeException, MalformedURLException,
			IOException {

		final WebClient webClient = new WebClient();
		
		LogAjaxController logAjaxController = new LogAjaxController();

		webClient.setAjaxController(logAjaxController);
		final HtmlPage page = webClient.getPage(dynamicUrl);

		//取出加载页面过程中触发的ajax url
		List<String> ajaxRequests = logAjaxController.getAjaxRequests();
		
		//页面完整html(包含ajax动态获取)
		String htmlContent = page.asXml();
		
		
		//页面还包含ajax加载代码,需要把这些jax请求拦截下来,但是有些情况是不须要拦截的就要添加到白名单
		Document document = Jsoup.parse(htmlContent);
		String whitelistStr = document.body().attr("whitelist");
		if (ajaxRequests.size() > 0  && whitelistStr != null) {
			String[] whitelist = whitelistStr.split(",");
			List<String> list = new ArrayList<String>();
			for (String url : ajaxRequests) {
				boolean find = false;
				for (String wlUrl : whitelist) {
					if (url.indexOf(wlUrl) != -1) {
						find = true;
						break;
					}
				}

				if (!find) {
					list.add(url);
				}
			}

			ajaxRequests = list;

		}
		
		if( ajaxRequests.size() > 0 ){
			
			Element script = new Element(Tag.valueOf("script"), "");
			script.attr("type", "text/javascript");
			
			StringBuilder sb  = new StringBuilder() ;
			
			for(String url : ajaxRequests ){
				sb.append("XHR['").append(url).append("']=true;") ;
			}
			
			script.text( String.format(ajaxInterceptJs, sb.toString()) ) ;
			document.head().prependChild(script);//注入拦截ajax js  保证拦截ajax的代码最先执行
			
		}


		
		//写入文件
		FileUtils.writeStringToFile(new File(staticPath), document.html() ,"utf-8");

		webClient.closeAllWindows();

	}

	/**
	 * 记录所有ajax请求url
	 * 
	 * @author Wen
	 *
	 */
	static class LogAjaxController extends NicelyResynchronizingAjaxController {
		private List<String> ajaxRequests = new ArrayList<String>();

		@Override
		public boolean processSynchron(HtmlPage page, WebRequest settings,
				boolean async) {
			ajaxRequests.add(settings.getUrl().getPath());
			return super.processSynchron(page, settings, async);
		}

		public List<String> getAjaxRequests() {
			return Collections.unmodifiableList(ajaxRequests);
		}
	}

}





静态页面效果

支持ajax的静态页面生成_第2张图片

页面效果和动态的index.jsp是一样的,但此时只有一个ajax请求刷新用户信息及访问次数,综合资讯的内容已经被静态化的。基本算是实现了我的需要。需要说明有几个地方。

如何通htmlunit取得ajax请求的url

htmlunit提供了处理ajax请求接口,我们只要简单继承NicelyResynchronizingAjaxController这个类,把ajax请求的url记录下来就可以了

静态页面也包含ajax加载综合资讯代码,这请求是处理拦截下来的

实际上静态页面会包含有跟原来页面一模一样的ajax加载动态内容代码,这些代码对于静态页面来说没有用的,因为内容都被静态化,没必要再发请求加载。我们通在生成静态页面会有页面注入以下javascript,可以把没必要的请求拦截下来(只拦截一次)

不需要被拦截ajax要怎样设置

在index.jsp 代码的body标签有个whitelist属性可设置ajax白名单,注入拦截代码时会读取这个值过虑掉,默认会拦截掉页面渲染触发的所有ajax请求。


四、未解决的问题

htmlunit只能调page.asXml()取页面html内容,但是这个方法不是很完美,它只是返回标准的xml代码,会把html的DOCTYPE声明删除掉,这个会导致浏览解析css会出错,临时办法把<!--?xml version="1.0" encoding="UTF-8"?-->替换回原代码页面的DOCTYPE。查遍了htmlunit文档,都没有找到可以直接获取完整html源代码的方法,找到的同学可以告诉我。

完成的例子代码下载http://pan.baidu.com/s/15qyPr

你可能感兴趣的:(htmlunit,java静态页面生成)