如何让Web程序在点击按钮后出现如执行批处理程序般的效果

在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日--

原文地址:https://www.cnblogs.com/heyang78/p/12837015.html