項(xiàng)目背景
基于Grails + groovy 框架開發(fā)了一個(gè)web系統(tǒng)家肯,因?yàn)間roovy是基于Java的腳本語言,所以這個(gè)方案在Java中也是可行的盟猖。
現(xiàn)在有這樣的需求讨衣,需要每天定時(shí)生成HTML報(bào)告换棚,并發(fā)送到固定郵件組。這份HTML報(bào)告不是靜態(tài)的反镇,數(shù)據(jù)會(huì)通過ajax獲取固蚤,圖表通過highchart.js來渲染。
需求點(diǎn)分析
這項(xiàng)需求的難點(diǎn)是歹茶,后端如何獲得js代碼運(yùn)行之后的HTML頁面內(nèi)容夕玩。
- 剛開始想走Java引擎解析HTML這類的方案,就是寫一個(gè)template惊豺,然后將數(shù)據(jù)填充進(jìn)去燎孟,但是js代碼如何編譯呢?朝這個(gè)方向去搜資料尸昧,并沒有整理出一個(gè)可執(zhí)行的方案揩页。
- 后來考慮到,首先將報(bào)告寫成一個(gè)頁面形式烹俗,然后在groovy中訪問這個(gè)URL不就可以了嗎爆侣? 但是沒有考慮到在瀏覽器打開一個(gè)URL,和使用curl(或者說使用java中httpClient包請(qǐng)求一個(gè)URL)的差別幢妄。那么這兩者的區(qū)別在哪里累提?前者會(huì)運(yùn)行js代碼,而后者不會(huì)磁浇。問題來了斋陪,怎么在groovy(或者Java)中打開一個(gè)URL能有瀏覽器的效果呢?偶然間查到了
phantomjs
,一個(gè)據(jù)說就是一個(gè)沒有界面的瀏覽器程序置吓。
方案
phantomjs
的使用方法這里不詳細(xì)描述无虚。感興趣的可以參考這個(gè)鏈接phantomjs教程
那么首先編寫執(zhí)行腳本executeJs.js
啦。希望這個(gè)腳本能完成如下功能---當(dāng)頁面加載完成之后衍锚,能獲得報(bào)告的HTML源碼友题。
為什么在這里要強(qiáng)調(diào)頁面加載完成之后。因?yàn)檫@個(gè)報(bào)告頁面戴质,是有highchat.js繪制圖表的度宦,還有ajax發(fā)送請(qǐng)求。當(dāng)我打開這個(gè)頁面的時(shí)候告匠,當(dāng)phantomjs
給我返回status為success
的狀態(tài)時(shí)戈抄,并不代表這個(gè)頁面完全的渲染完成(在這里指的是繪圖完成)。如果在頁面沒有渲染完成的時(shí)候后专,獲得的頁面內(nèi)容就是這樣的:
但是我需要的是這樣的:
那么划鸽,在executeJs.js
腳本中,如何得知頁面已經(jīng)渲染完成呢?當(dāng)然有非常偷懶的做法裸诽。打開URL之后嫂用,等待10秒鐘,一般情況下丈冬,頁面肯定已經(jīng)渲染完成了嘱函。
但是,我想處理得更精細(xì)一點(diǎn)埂蕊,不想傻等往弓。在被請(qǐng)求的URL這個(gè)頁面要畫四幅圖,四幅圖都完成了粒梦,這個(gè)頁面也就渲染完成了。那么我怎么知道荸实,highchart 畫圖完成了呢匀们?進(jìn)一步的,我怎么知道最后一副完成的圖是哪一個(gè)呢准给?
第一個(gè)問題---highchart 的series屬性有這樣一個(gè)方法
...
series: [{
data: vals,
events: {
afterAnimate: function() {
chartHasDone = chartHasDone + 1;
}
}
}],
...
當(dāng)afterAnimate
被調(diào)用時(shí)泄朴,說明圖片已經(jīng)渲染完成了。
第二個(gè)問題---我確實(shí)不知道最后一幅圖是哪一個(gè)露氮?不妨換一個(gè)思路祖灰,定義一個(gè)全局變量,每一幅圖畫完之后畔规,給這個(gè)全局變量+1 局扶,當(dāng)全局變量等于4時(shí),代表四幅圖全部渲染完成叁扫。
Linux下phantomjs的安裝
在Linux環(huán)境下安裝phantomjs之前需要安裝如下三個(gè)依賴
- libstdc++.so.6
- glibc
- fontconfig
前面兩個(gè)使用yum來安裝三妈。后面一個(gè)下載fontconfig的壓縮包使用make安裝。
安裝libstdc++.so.6
> yum provides libstdc++.so.6 //查看哪個(gè)安裝包包含該庫.結(jié)果顯示
libstdc++-4.4.7-16.el6.i686 : GNU Standard C++ Library
> yum install libstdc++-4.4.7-4.el6.i686 //安裝這個(gè)包莫绣,即可
安裝glibc
> yum install glibc
如果yum源沒有問題的話畴蒲,應(yīng)該就可以安裝成功,但是我執(zhí)行這個(gè)命令的時(shí)候報(bào)如下錯(cuò)誤
rpmdb: Thread/process 6539/140448388269824 failed: Thread died in Berkeley DB library
error: db3 error(-30974) from dbenv->failchk: DB_RUNRECOVERY: Fatal error, run database recovery
error: cannot open Packages index using db3 - (-30974)
error: cannot open Packages database in /var/lib/rpm
然后搜到解決辦法如下所示
cd /var/lib/rpm/
for i in `ls | grep 'db.'`;do mv $i $i.bak;done
rpm --rebuilddb
yum clean all
安裝fontconfig
按照fontconfig的官方文檔对室,執(zhí)行安裝步驟如下所示
> sudo ./configure --prefix=/usr \
--sysconfdir=/etc \
--localstatedir=/var \
--disable-docs \
--docdir=/usr/share/doc/fontconfig-2.12.4 &&
make
依賴項(xiàng)安裝成功模燥。然后在phantomjs
的官網(wǎng)上,根據(jù)系統(tǒng)的類型和版本掩宜,選擇對(duì)應(yīng)的包下載蔫骂,解壓。進(jìn)入bin目錄下直接執(zhí)行即可
> phantomjs test.js
實(shí)現(xiàn)細(xì)節(jié)
那么再加上一些異常處理的代碼牺汤,execute.js就很好寫了
var page = require('webpage').create();
page.viewportSize = { width: 1920, height: 960 }
page.open('http://localhost.zeus.vdian.net:9000/ci/dailyReport?showReportHref=true', function(status) {
if(status === "success") {
//計(jì)算一下是否能讀到值
var maxTimes = 0;
var timer = setInterval(function() {
maxTimes++
var chartHasDone = page.evaluate(function() {
return chartHasDone //這個(gè)是被打開頁面中記錄渲染完成的圖表數(shù)的全局變量纠吴。
});
//重試1分鐘,若1分鐘還沒有結(jié)束慧瘤,自動(dòng)結(jié)束進(jìn)程戴已。返回false
if (maxTimes >= 5) {
clearInterval(timer);
//保存結(jié)果
console.log(false)
phantom.exit();
}
//chartHasDone變?yōu)?,說明圖表渲染完成
if (chartHasDone == 5) {
clearInterval(timer);
var content = page.evaluate(function() {
return document.getElementById('reportDetail').innerHTML;
});
console.log(content)
phantom.exit();
}
}, 2000)
}
});
同時(shí)固该,groovy(java)中代碼如下所示:
def sendEmail() {
def mailTo = 'zangsan@xx.com'
def mailtitle = "日?qǐng)?bào)-${yesterday()}"
def phantomjsDir = "${System.properties['user.home']}/phantomjs"
def phantomjsFile = new File("${phantomjsDir}/phantomjs")
def executeJsFile = new File("${phantomjsDir}/executeJs.js")
if (!phantomjsFile.exists() || !executeJsFile.exists()) {
log.error("phantomjs文件不存在");
return [success: false, message: 'phantomjs文件不存在']
}
def phantomjsPath = phantomjsFile.getAbsolutePath()
def executeJsPath = executeJsFile.getAbsolutePath()
def getHtmlContentCmd = "${phantomjsPath} ${executeJsPath}"
Process process = getHtmlContentCmd.execute();
int exitStatus = process.waitFor(); //等待命令執(zhí)行完成
if (exitStatus != 0) {
log.error("EXIT-STATUS - " + process.toString());
return [success: false, message: "執(zhí)行phantomjs文件出錯(cuò): ${process.toString()}"]
}
def content = process.text
if (content?.trim() == 'false') {
log.error "請(qǐng)求URL超時(shí)"
return [success: false, message: '請(qǐng)求URL超時(shí)']
} else {
def result = [success: true]
try {
mailService.sendMail {
to mailTo
from "lisi@xx.com"
subject mailtitle
html content?.trim()
}
} catch (ex) {
log.error "send mail Failed: ${ex.cause} (${ex.message})"
result.success = false
result.message = "郵件發(fā)送失敗: " + ex.message
}
return result
}
}
然后在Grails的job中,定時(shí)調(diào)用sendEmail
函數(shù)糖儡,即可伐坏。
遇到的坑
phantomjs 與瀏覽器的差別
phantomjs
聲稱是一個(gè)沒有界面的瀏覽器。雖然它可以執(zhí)行js代碼握联,但是和在chrome中訪問頁面還有差別的桦沉。我跳進(jìn)去的這個(gè)坑就是--phantomjs
無法解析 多行字符串的反引號(hào)
在chrome上如下一段代碼是可以正常執(zhí)行
var tmpl = `
hello
world
`
但是,在 phantomjs
中上面一段代碼會(huì)出現(xiàn)錯(cuò)誤金闽。關(guān)鍵是還不提示錯(cuò)誤信息纯露。最開始的時(shí)候都沒法排查!4摺埠褪!后來將那個(gè)頁面的js代碼一段段注釋,才找到出錯(cuò)的原因挤庇。
執(zhí)行腳本出現(xiàn)問題
在Grails中執(zhí)行executeJs.js
的命令如下所示:
${System.properties['user.home']}/phantomjs/phantomjs ${System.properties['user.home']}/phantomjs/executeJs.js test
但是該命令在測(cè)試環(huán)境下并沒有執(zhí)行成功钞速。本地調(diào)試時(shí),該命令是成功的嫡秕。后來發(fā)現(xiàn)區(qū)別是渴语,在測(cè)試環(huán)境下是以root用戶執(zhí)行該命令的。以root用戶執(zhí)行命令的話昆咽,${System.properties['user.home']}
的值和以普通用戶執(zhí)行命令時(shí)的值是不一樣的驾凶。前者是/root/
,后者是/home/www
掷酗。所以,phantomjs
程序的目錄需要發(fā)生變更狭郑。
優(yōu)化代碼
我將executeJs.js
和phantomjs
放在同一個(gè)本地目錄下。如果萬一executeJs.js
發(fā)生變更的話汇在,那我還得去機(jī)器上更新代碼翰萨。為了修改方便,決定將executeJs.js
放在了Grails 工程中糕殉。相應(yīng)的亩鬼,上一段代碼也要發(fā)生如下變更,現(xiàn)在的問題是阿蝶,如何在Grails代碼中找Grails工程中的資源文件雳锋。executeJs.js
放在grails-app/src/main/resource
中。代碼修改如下所示
....
def yesterDay = yesterday()
def mailtitle = "ZEUS-持續(xù)集成日?qǐng)?bào)-${yesterDay}"
def phantomjsFile = new File("${System.properties['user.home']}/phantomjs/phantomjs")
log.error("phantomjsFile的位置${phantomjsFile.absolutePath}")
if (!phantomjsFile.exists()) {
log.error("phantomjs程序不存在");
return [success: false, message: 'phantomjs程序不存在']
}
def executeJsResource = this.class.classLoader.getResource('executeJs.js') //獲取resource中executeJs.js的絕對(duì)路徑
def executeJsPath = executeJsResource.file
def executeJsFile = new File("${executeJsPath}")
if (!executeJsFile.exists()) {
log.error("executeJs文件不存在");
return [success: false, message: 'executeJs文件不存在']
}
def phantomjsPath = phantomjsFile.getAbsolutePath()
def getHtmlContentCmd = "${phantomjsPath} ${executeJsPath} ${env}"
log.error "執(zhí)行content的命令是 ${getHtmlContentCmd}"
...
使用this.class.classLoader.getResource('executeJs.js').path
來獲取executeJs.js
的絕對(duì)路徑羡洁。