在cli程序中,输入命令得到连续的输出已经是一种进度而美观的页面交互形式,好比下图:
而web程序里也有类似的场景,比如执行一个耗时任务,除了显示出等待图标外,用户还希望把执行的状态及时显示出来.如下图:
这样的界面如何设计呢?我的思路如下:
1.点击按钮后,产生一个新ID,后台运行的线程拿到id后开始运行并及时往数据库中插入记录,同时id被送回到前台;
2.前台拿到id后,开始以此id轮询后台数据表,并将取得的数据显示出来,而取得的数据是后台运行的线程不断写入的;
3.后台线程写入状态1后,即认为任务完成,前台取得此数据后不再轮询并恢复成初始状态.
下面请看具体实现:
前台点击按钮触发Ajax:
$("#startPhonexCrawl").click(function(){ var taskId=$("#taskIdTxt").val(); if(taskId!="0"){ // 有任务启动则取状态 alert("有任务在执行,请稍等..."); }else{ // 没有任务则启动任务 $.get("/startCrawl/phonex",{},function(data,textStatus){ var taskId=data; $("#taskIdTxt").val(taskId); $("#crawlsDiv").html(""); $("#loadingImg").show(); showTask(); }); } });
后台接到请求后一边启动线程,一边将产生的taskid回传:
/** * Start crawl and return crawltask id * @param crawlName * @return */ @RequestMapping("/startCrawl/{crawlName}") String startCrawl(@PathVariable("crawlName") String crawlName) { logger.info("准备启动爬虫:"+crawlName); long taskId=crawlMapper.getNextTaskId(); BaseCTH cth=null; if("phonex".equalsIgnoreCase(crawlName)) { cth=new PhonexCTH(); logger.info("Phonex crawl thread is ready."); }else if("163".equalsIgnoreCase(crawlName) || "Netease".equalsIgnoreCase(crawlName)) { cth=new NeteaseCTH(); logger.info("Netease crawl thread is ready."); }else if("snowball".equalsIgnoreCase(crawlName)) { cth=new SnowballCTH(); logger.info("Snowball crawl thread is ready."); }else { logger.warn("Error crawlName:"+crawlName+",so no crawl thread started."); taskId=0; } if(cth!=null) { cth.setTaskId(taskId); cth.setStockMapper(stockMapper); cth.setCrawlMapper(crawlMapper); cth.start(); logger.info("Crawl thread started."); } return String.valueOf(taskId); }
从上面的程序也可看出,前台按钮和后台具体爬虫联系的纽带是crawlName,这样处理后,如果要增加新爬虫,只要前台做个链接,然后在分支中与具体爬虫联系上即可.
前台的ajax会在得到返回id后调用showTasks函数:
function showTask(){ var taskId=$("#taskIdTxt").val(); if(taskId!="0"){ $.get("/getCrawlTasks/"+taskId,{},function(data,textStatus){ var message=""; var state=""; var percent=""; for(var i=0,l=data.length;i<l;i++){ message+=data[i].ctime+" "+data[i].msg+"<br/>"; state=data[i].state; percent=data[i].percent; } $("#crawlsDiv").html(message); $("#percentSpan").html(percent+"%"); if(state=="1"){ //alert("爬虫任务"+taskId+"结束"); // 如果任务结束则可启动下一任务 clearTimeout(timerHandler); $("#taskIdTxt").val("0"); $("#loadingImg").hide(); $("#percentSpan").html(""); }else{ timerHandler=setTimeout("showTask()",3000); } }); } }
showTasks函数会在结束前查看数据状态,如果状态不是1则会以三秒为间隔不断调用自己,从而达到轮询的目的,而轮询取状态的后台函数是
@RequestMapping("/getCrawlTasks/{taskId}") List<CrawlTask> getCrawlTasks(@PathVariable("taskId") String taskId) { logger.info("取得taskId="+taskId+"的爬虫状态:"); return crawlMapper.getCrawlTasks(taskId); }
@Select("select id,taskid,state,msg,DATE_FORMAT(ctime,'%Y-%m-%d %T') as ctime,percent from crawltask where taskid=#{taskId} order by id ")
List<CrawlTask> getCrawlTasks(@Param("taskId") String taskId);
这样,每过三秒就会从crawltask表里取得信息显示在页面上.
整套设计里,taskid是前台从db取值和后台线程往数据库写值的联系纽带,有了它的出现,前后台可以在互不知情的情况下良好配合.
当从后台取得状态为1时,下面语句便会发挥作用:
clearTimeout(timerHandler);
timerHandler是启动时的句柄,而一旦clear掉,timeout便不会再起作用,从而结束轮询.
这就是全部设计过程.
--2020年5月6日--