背景
现代页面上越来越多的内容是通过ajax更新, 因为页面可以显示的更加快速, 局部更新, 同时可以实现页面内容的动态性, 增加页面的内容的丰富性。
但是如果将页面内容保存下来, 以备离线浏览器查看, 则由于ajax存在的原因, 直接使用保存的方法不能实现, 因为会有ajax访问禁用的报错。
对于非ajax页面, 页面上内容仅仅使用 链接组织资源的情况, 页面的内容是完整的, 则浏览器保存下来的内容是完整的, 则可以在本地使用离线版本浏览。
方案思路
分析现有页面使用ajax更新的 方法, 有如下两种:
1、 使用ajax方式获取部分html代码, 其中可能有js代码混杂其中, 但是整体上 html代码, 然后使用 jquery.html 接口将 html代码 插入页面中。
插入过程, 页面html被解析显示, js代码被执行。
2、 使用ajax方式获取页面需要显示的数据, 一般为json格式和xml格式,ajax后去后, 到前台使用js给页面上的指定DOM元素赋值, 获取阻止为到html字符串中, 然后将构造的带有动态数据的html字符串, 插入到DOM节点中。
针对这两种情况, 希望此这两种情况的 数据如果能够存储在当前页面中, 并且重载ajax函数, ajax不去发起http请求, 替换的是从本地的页面中, 查找到缓存的数据, 并返回。那么, ajax调用后的其它部分的代码逻辑没有改变, 不用改动。
只要实现如下内容:
1、 ajax数据的抓取, 并存储到本页面DOM中。
2、 将此页面中使用ajax函数重载为 本地缓存 查找的函数。
3、 将此页面保存为离线文件形式。
然后,你就可以离线浏览了。 当然还有一些链接存在的 文件, 这个不能被保存, 需要另行下载或者准备。 但是被ajax容易多了。
方案实现
借助phantomjs实现抓取和插入DOM, 并修改ajax接口, 并保存为本地文件。
样例
集合 ajax请求html 和 json数据的场景, 顺序为 ajax先请求html文件, html文件中js脚本又发起请求, 请求json文件。
test.html
<!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" /> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.js"></script> </head> <body> <img src="./favicon.ico"> <form id="realform"> </form> <script> $(document).ready(function (){ $.get("./formContent.html", function (data) { $("#realform").html(data) }) }) </script> </body> </html>
formContent.html
<input type="text" name="Frm_username" id="Frm_username" value=""> <input type="password" name="Frm_password" id="Frm_password" value=""> <button type="submit">submit</button> <script> $(document).ready(function () { $.getJSON(‘./test.json‘, function(data) { $("#Frm_username").val(data.username); $("#Frm_password").val(data.password); }); }); </script>
test.json
{ "username":"fanqingsong", "password":"xxxx" }
PhantomJS 代码
"use strict"; var page = require(‘webpage‘).create(); var fs = require(‘fs‘); page.open(‘http://localhost/test.html‘, function () { page.evaluate(function(){ }); }); var ajaxURLCode = {}; page.onConsoleMessage = function(msg, lineNum, sourceId) { console.log(‘CONSOLE: ‘ + msg + ‘ (from line #‘ + lineNum + ‘ in "‘ + sourceId + ‘")‘); }; function hereDoc(f) { return f.toString().replace(/^[^\/]+\/\*!?\s?/, ‘‘).replace(/\*\/[^\/]+$/, ‘‘); } page.onLoadFinished = function() { console.log("page load finished"); // 待页面ajax内容加载完毕才 截图 和 保存页面内容 waitFor(function () { return page.evaluate(function () { if ( $("#Frm_username").val() != "" ) { return true; } return false; }); }, function () { // 将每个ajax请求的结果记录到当前page 的DOM 中 for (var urlPath in ajaxURLCode) { console.log("====== urlPath="+urlPath); var content = ajaxURLCode[urlPath]; page.evaluate(function(urlPath, content){ $("<div style=‘display:none‘ id=‘"+urlPath+"‘></div>") .text(content) .appendTo("body"); }, urlPath, content); }; // 注入 ajax 请求桩函数, 以实现从页面中缓存显示效果 page.evaluate(function(hereDoc){ $("body").prepend("<div id=‘overwriteAjaxFunc‘></div>"); var ajaxGetRewrite = hereDoc(function () {/* <script type=‘text/javascript‘> $(document).ready(function () { $.get = function (url, callback) { console.log(‘enter $.get refactor‘) url = url.match(‘./(.*)‘)[1]; var data = $(‘[id="‘+url+‘"]‘).text(); callback(data); } $.getJSON = function (url, callback) { console.log(‘enter $.getJSON refactor‘) url = url.match(‘./(.*)‘)[1]; var data = $(‘[id="‘+url+‘"]‘).text(); data = JSON.parse(data); callback(data); } }); </script> */}); $("#overwriteAjaxFunc").html(ajaxGetRewrite); }, hereDoc); // 保存页面图片和代码 page.render(‘test.png‘); fs.write(‘test.html‘, page.content, ‘w‘); }) }; page.onResourceRequested = function(requestData, networkRequest) { // 判断是否为ajax var isAjaxRequested = false; var headers = requestData.headers; for (var i = 0; i < headers.length; i++) { var oneHeader = headers[i]; var headerName = oneHeader.name; var headerValue = oneHeader.value; if ( headerName == "X-Requested-With" && headerValue == "XMLHttpRequest" ) { isAjaxRequested = true; } } // 只记录AJAX请求的结果 if ( !isAjaxRequested ) { return; } console.log(‘Request (#‘ + requestData.id + ‘): ‘ + JSON.stringify(requestData)); var url = requestData.url; console.log(" onResourceRequested url="+url) if ( url.match("json$") || url.match("html$") ) { var pageRaw = require("webpage").create() //pageRaw.settings.javascriptEnabled = false; // 借助有jquery的页面 下载 ajax内容 pageRaw.open(‘http://localhost/test.html‘, function () { console.log("url =----------------- ="+url) //console.log("url plainText ="+pageRaw.plainText) //console.log("url content ="+pageRaw.content) var content = pageRaw.evaluate(function (url) { var ajaxRet = ""; var request = $.ajax({ async: false, url: url, dataType: "text" }); request.done(function( msg ) { ajaxRet = msg; }); console.log("ajaxRet="+ajaxRet) return ajaxRet; }, url) console.log("!!!!!!!!!!!!!content="+content); // http://xxx/urlpath var matchRet = url.match("http://.*/(.*)") console.log("matchRet =----------------- ="+matchRet) var urlPath = matchRet[1] console.log("urlPath =----------------- ="+urlPath) ajaxURLCode[urlPath] = content; }) } }; // page.onResourceReceived = function(response) { // //console.log(‘Response (#‘ + response.id + ‘, stage "‘ + response.stage + ‘"): ‘ + JSON.stringify(response)); // }; function waitFor(testFx, onReady, timeOutMillis) { var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s start = new Date().getTime(), condition = false, interval = setInterval(function () { if ((new Date().getTime() - start < maxtimeOutMillis) && !condition) { // If not time-out yet and condition not yet fulfilled condition = (typeof (testFx) === "string" ? eval(testFx) : testFx()); //< defensive code } else { if (!condition) { // If condition still not fulfilled (timeout but condition is ‘false‘) console.log("‘waitFor()‘ timeout"); phantom.exit(1); } else { // Condition fulfilled (timeout and/or condition is ‘true‘) console.log("‘waitFor()‘ finished in " + (new Date().getTime() - start) + "ms."); typeof (onReady) === "string" ? eval(onReady) : onReady(); //< Do what it‘s supposed to do once the condition is fulfilled clearInterval(interval); //< Stop this interval } } }, 250); //< repeat check every 250ms };
截图
OFFLINE GUI Page Code
<!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"> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.js"></script> </head> <body><div id="overwriteAjaxFunc"> <script type="text/javascript"> $(document).ready(function () { $.get = function (url, callback) { console.log(‘enter $.get refactor‘) url = url.match(‘./(.*)‘)[1]; var data = $(‘[id="‘+url+‘"]‘).text(); callback(data); } $.getJSON = function (url, callback) { console.log(‘enter $.getJSON refactor‘) url = url.match(‘./(.*)‘)[1]; var data = $(‘[id="‘+url+‘"]‘).text(); data = JSON.parse(data); callback(data); } }); </script> </div> <img src="./favicon.ico"> <form id="realform"><input type="text" name="Frm_username" id="Frm_username" value=""> <input type="password" name="Frm_password" id="Frm_password" value=""> <button type="submit">submit</button> <script> $(document).ready(function () { $.getJSON(‘./test.json‘, function(data) { $("#Frm_username").val(data.username); $("#Frm_password").val(data.password); }); }); </script> </form> <script> $(document).ready(function (){ $.get("./formContent.html", function (data) { $("#realform").html(data) }) }) </script> <div style="display:none" id="formContent.html"><input type="text" name="Frm_username" id="Frm_username" value=""> <input type="password" name="Frm_password" id="Frm_password" value=""> <button type="submit">submit</button> <script> $(document).ready(function () { $.getJSON(‘./test.json‘, function(data) { $("#Frm_username").val(data.username); $("#Frm_password").val(data.password); }); }); </script> </div><div style="display:none" id="test.json">{ "username":"fanqingsongaaa", "password":"xxxx" }</div></body></html>