项目背景
基于Grails + groovy 框架开发了一个web系统,因为groovy是基于Java的脚本语言,所以这个方案在Java中也是可行的。
现在有这样的需求,需要每天定时生成HTML报告,并发送到固定邮件组。这份HTML报告不是静态的,数据会通过ajax获取,图表通过highchart.js来渲染。
需求点分析
这项需求的难点是,后端如何获得js代码运行之后的HTML页面内容。
- 刚开始想走Java引擎解析HTML这类的方案,就是写一个template,然后将数据填充进去,但是js代码如何编译呢?朝这个方向去搜资料,并没有整理出一个可执行的方案。
- 后来考虑到,首先将报告写成一个页面形式,然后在groovy中访问这个URL不就可以了吗? 但是没有考虑到在浏览器打开一个URL,和使用curl(或者说使用java中httpClient包请求一个URL)的差别。那么这两者的区别在哪里?前者会运行js代码,而后者不会。问题来了,怎么在groovy(或者Java)中打开一个URL能有浏览器的效果呢?偶然间查到了
phantomjs
,一个据说就是一个没有界面的浏览器程序。
方案
phantomjs
的使用方法这里不详细描述。感兴趣的可以参考这个链接phantomjs教程
那么首先编写执行脚本executeJs.js
啦。希望这个脚本能完成如下功能---当页面加载完成之后,能获得报告的HTML源码。
为什么在这里要强调页面加载完成之后。因为这个报告页面,是有highchat.js绘制图表的,还有ajax发送请求。当我打开这个页面的时候,当phantomjs
给我返回status为success
的状态时,并不代表这个页面完全的渲染完成(在这里指的是绘图完成)。如果在页面没有渲染完成的时候,获得的页面内容就是这样的:
但是我需要的是这样的:
那么,在executeJs.js
脚本中,如何得知页面已经渲染完成呢?当然有非常偷懒的做法。打开URL之后,等待10秒钟,一般情况下,页面肯定已经渲染完成了。
但是,我想处理得更精细一点,不想傻等。在被请求的URL这个页面要画四幅图,四幅图都完成了,这个页面也就渲染完成了。那么我怎么知道,highchart 画图完成了呢?进一步的,我怎么知道最后一副完成的图是哪一个呢?
第一个问题---highchart 的series属性有这样一个方法
...
series: [{
data: vals,
events: {
afterAnimate: function() {
chartHasDone = chartHasDone + 1;
}
}
}],
...
当afterAnimate
被调用时,说明图片已经渲染完成了。
第二个问题---我确实不知道最后一幅图是哪一个?不妨换一个思路,定义一个全局变量,每一幅图画完之后,给这个全局变量+1 ,当全局变量等于4时,代表四幅图全部渲染完成。
Linux下phantomjs的安装
在Linux环境下安装phantomjs之前需要安装如下三个依赖
- libstdc++.so.6
- glibc
- fontconfig
前面两个使用yum来安装。后面一个下载fontconfig的压缩包使用make安装。
安装libstdc++.so.6
> yum provides libstdc++.so.6 //查看哪个安装包包含该库.结果显示
libstdc++-4.4.7-16.el6.i686 : GNU Standard C++ Library
> yum install libstdc++-4.4.7-4.el6.i686 //安装这个包,即可
安装glibc
> yum install glibc
如果yum源没有问题的话,应该就可以安装成功,但是我执行这个命令的时候报如下错误
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的官方文档,执行安装步骤如下所示
> sudo ./configure --prefix=/usr \
--sysconfdir=/etc \
--localstatedir=/var \
--disable-docs \
--docdir=/usr/share/doc/fontconfig-2.12.4 &&
make
依赖项安装成功。然后在phantomjs
的官网上,根据系统的类型和版本,选择对应的包下载,解压。进入bin目录下直接执行即可
> phantomjs test.js
实现细节
那么再加上一些异常处理的代码,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") {
//计算一下是否能读到值
var maxTimes = 0;
var timer = setInterval(function() {
maxTimes++
var chartHasDone = page.evaluate(function() {
return chartHasDone //这个是被打开页面中记录渲染完成的图表数的全局变量。
});
//重试1分钟,若1分钟还没有结束,自动结束进程。返回false
if (maxTimes >= 5) {
clearInterval(timer);
//保存结果
console.log(false)
phantom.exit();
}
//chartHasDone变为5,说明图表渲染完成
if (chartHasDone == 5) {
clearInterval(timer);
var content = page.evaluate(function() {
return document.getElementById('reportDetail').innerHTML;
});
console.log(content)
phantom.exit();
}
}, 2000)
}
});
同时,groovy(java)中代码如下所示:
def sendEmail() {
def mailTo = '[email protected]'
def mailtitle = "日报-${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(); //等待命令执行完成
if (exitStatus != 0) {
log.error("EXIT-STATUS - " + process.toString());
return [success: false, message: "执行phantomjs文件出错: ${process.toString()}"]
}
def content = process.text
if (content?.trim() == 'false') {
log.error "请求URL超时"
return [success: false, message: '请求URL超时']
} else {
def result = [success: true]
try {
mailService.sendMail {
to mailTo
from "[email protected]"
subject mailtitle
html content?.trim()
}
} catch (ex) {
log.error "send mail Failed: ${ex.cause} (${ex.message})"
result.success = false
result.message = "邮件发送失败: " + ex.message
}
return result
}
}
然后在Grails的job中,定时调用sendEmail
函数,即可。
遇到的坑
phantomjs 与浏览器的差别
phantomjs
声称是一个没有界面的浏览器。虽然它可以执行js代码,但是和在chrome中访问页面还有差别的。我跳进去的这个坑就是--phantomjs
无法解析 多行字符串的反引号
在chrome上如下一段代码是可以正常执行
var tmpl = `
hello
world
`
但是,在 phantomjs
中上面一段代码会出现错误。关键是还不提示错误信息。最开始的时候都没法排查!!!后来将那个页面的js代码一段段注释,才找到出错的原因。
执行脚本出现问题
在Grails中执行executeJs.js
的命令如下所示:
${System.properties['user.home']}/phantomjs/phantomjs ${System.properties['user.home']}/phantomjs/executeJs.js test
但是该命令在测试环境下并没有执行成功。本地调试时,该命令是成功的。后来发现区别是,在测试环境下是以root用户执行该命令的。以root用户执行命令的话,${System.properties['user.home']}
的值和以普通用户执行命令时的值是不一样的。前者是/root/
,后者是/home/www
。所以,phantomjs
程序的目录需要发生变更。
优化代码
我将executeJs.js
和phantomjs
放在同一个本地目录下。如果万一executeJs.js
发生变更的话,那我还得去机器上更新代码。为了修改方便,决定将executeJs.js
放在了Grails 工程中。相应的,上一段代码也要发生如下变更,现在的问题是,如何在Grails代码中找Grails工程中的资源文件。executeJs.js
放在grails-app/src/main/resource
中。代码修改如下所示
....
def yesterDay = yesterday()
def mailtitle = "ZEUS-持续集成日报-${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的绝对路径
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 "执行content的命令是 ${getHtmlContentCmd}"
...
使用this.class.classLoader.getResource('executeJs.js').path
来获取executeJs.js
的绝对路径。