目前主流的頁面靜態技術都是基於模板生成的,可是對於一些採用ajax+js渲染的頁面,這種方法是無能爲力的。要解決這個問題,首先要有一個能模擬瀏覽器的運行環境,其餘問題都比較容易解決。能模擬瀏覽器的技術有好多,seleninum , htmlunit等。其中htmlunit是java開發用無界面的瀏覽器,速度和性能很是好,對html建模而且提供API來訪問頁面,點擊連接等等,不須要任務驅動程序 ,提供javascript執行環境,如今不少支持ajax網絡爬蟲也是在它基礎上實現的。 javascript
如何基於htmlunit實現ajax頁面靜態化呢?下面我用一個例子闡述吧,沒什麼比用代碼更直接清楚。這個例子有個ajax渲染的頁面,頁面主要有兩塊內容,頂部是用戶信息,下面是讀取osc 首頁的綜合資訊,基本需求是綜合資訊內容要靜態化,用戶信息不須要。 css
index.jsp頁面代碼 html
<%@ 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>
動態頁面效果 java
注意上面圖的兩個ajax是加載動態內容觸發,而後用javascript渲染到頁面 web
點擊"生成靜態頁面「按鈕會觸發後臺調用靜態組件生成靜態頁面(index.html) ajax
/** * 觸發生成靜態頁面 */ 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); } } }
靜態頁面效果 瀏覽器
頁面效果和動態的index.jsp是同樣的,但此時只有一個ajax請求刷新用戶信息及訪問次數,綜合資訊的內容已經被靜態化的。基本算是實現了個人須要。須要說明有幾個地方。 網絡
一、如何通htmlunit取得ajax請求的url app
htmlunit提供了處理ajax請求接口,咱們只要簡單繼承NicelyResynchronizingAjaxController這個類,把ajax請求的url記錄下來就能夠了 jsp
二、靜態頁面也包含ajax加載綜合資訊代碼,這請求是處理攔截下來的
實際上靜態頁面會包含有跟原來頁面如出一轍的ajax加載動態內容代碼,這些代碼對於靜態頁面來講沒有用的,由於內容都被靜態化,不必再發請求加載。咱們通在生成靜態頁面會有頁面注入如下javascript,能夠把不必的請求攔截下來(只攔截一次)
三、不須要被攔截ajax要怎樣設置
在index.jsp 代碼的body標籤有個whitelist屬性可設置ajax白名單,注入攔截代碼時會讀取這個值過慮掉,默認會攔截掉頁面渲染觸發的全部ajax請求。
4、未解決的問題
htmlunit只能調page.asXml()取頁面html內容,可是這個方法不是很完美,它只是返回標準的xml代碼,會把html的DOCTYPE聲明刪除掉,這個會致使瀏覽解析css會出錯,臨時辦法把<!--?xml version="1.0" encoding="UTF-8"?-->替換回原代碼頁面的DOCTYPE。查遍了htmlunit文檔,都沒有找到能夠直接獲取完整html源代碼的方法,找到的同窗能夠告訴我。
完成的例子代碼下載http://pan.baidu.com/s/15qyPr