最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

php - How to create Server-side Progress indicator in JavaScript? - Stack Overflow

programmeradmin0浏览0评论

I want to create a section in my site, where a user has a few simple update buttons.

Each of these update buttons will be going to the server, and will do a long crunching behind the scene.

While the server crunches data, I want the user to have a some kind of progress indicator, like progress bar or textual percentage.

I'm using jQuery as my JavaScript library, and CodeIgniter (PHP) as the server-side framework, if it's important...

What I was thinking about is using PHP's flush() function to report progress status to jQuery, but I'm not sure that jQuery's Ajax functions are reading the output before it's plete...

So any advice/explanation would be useful and helpful!

I want to create a section in my site, where a user has a few simple update buttons.

Each of these update buttons will be going to the server, and will do a long crunching behind the scene.

While the server crunches data, I want the user to have a some kind of progress indicator, like progress bar or textual percentage.

I'm using jQuery as my JavaScript library, and CodeIgniter (PHP) as the server-side framework, if it's important...

What I was thinking about is using PHP's flush() function to report progress status to jQuery, but I'm not sure that jQuery's Ajax functions are reading the output before it's plete...

So any advice/explanation would be useful and helpful!

Share Improve this question edited Apr 1, 2010 at 12:04 Peter Mortensen 31.6k22 gold badges110 silver badges133 bronze badges asked Mar 14, 2010 at 11:14 EliEli 2,6967 gold badges33 silver badges37 bronze badges 1
  • Just as an aside, I don't know how much more effective a Comet-based approach would be over straight-up AJAX. If the server sends down the current progress with an estimated rate of pletion (i.e.: percent per second), the client can give a rather accurate representation of progress by polling the server periodically. Sure, a "realtime" view would be cool, but make sure you're considering whether the inclusion of a push method outweighs the trouble you'll go through to build and debug something like this. – mattbasta Commented Mar 22, 2010 at 15:14
Add a ment  | 

3 Answers 3

Reset to default 4

I'm going to give you an example using WebSync On-Demand, but the same approach would work regardless of your choice of server.

Here's what you do. First, kick off the long-running operation somehow; your user clicks the button to start this process (I'm going to assume an Ajax call, but whatever works), and you return to them some sort of identifier, we'll call that 'myId', give it a value of '1'. Whether you do that by invoking a process of some sort, etc, is up to you.

Then, in your callback from that invocation, you would write something like so:

var myId = 1; // this would be set somewhere else
client.initialize('api key');
client.connect();
client.subscribe({
  channel: '/tasks/' + myId,
  onReceive: function(args){
    // update the progress bar
    myProgressBar.update(args.data.progress);
  }
});

What that'll do is subscribe your client to receive notification about updates to the task, so all that's left is to push out the updates, which you'd do in whatever process is actually running the task. That would look like (in PHP, using the SDK):

$publisher = new Publisher(
    "11111111-1111-1111-1111-111111111111", // your api key again
    "mydomain." // your domain
);

// publish data
$response = $publisher->publish(array(
    array(
        'channel' => '/tasks/' . $myId, //es from somewhere
        'data' => (object) array(
            'progress' => '45' //45% plete
        )
    )
));

// success if empty (no error)
$success = empty($response); 

That's it; as updates occur, they'll push out to your client in real-time.

It's pretty hard to get this right. What we've settled on for our system is a "faked" progress bar - it just animates over and over (which since it is an animated gif, you might expect!).

An alternative would be to submit to one script, and have that processing in the background (and outputting progress to a file) while making an Ajax request to another script whose only responsibility is to read that progress file and return how far through the process you are. This would work - it feels a little bit kludgy, but it would at least solve your immediate problem.

I know very little about Comet or the likes, so this is purely based on my current understanding.

3 years late, but here's a solution I came up with. Bonus: It works in IE7+

Uses:

  • jQuery 1.9.1
  • jQuery UI 1.10(quick dialog box and progress bar)
  • Remy's EventSource Polyfill
  • JSON2 polyfill

The event table:

create table updates(
    evt_id int unsigned not null auto_increment,
    user_id int unsigned not null,
    evt_type enum('start','update','finish') not null,
    evt_msg varchar(255) not null,
    primary key (evt_id)
)

The HTML:

<?php
include 'libconfig.php';
session_write_close();
if(count($_POST)){
    $db=db_get_connection();
    $stm=new PDOStatementWrapper(db_prepare($db,'INSERT INTO bupdates VALUES (:event_id,:user_id,:type,:message)'));
    if($stm->run(array(
        ':event_id'=>0,
        ':user_id'=>App::user()->getId(),
        ':type'=>$_POST['type'],
        ':message'=>$_POST['message']
    )))echo 'Inserted';
    return;
}
?>
<!doctype html>
<html>
<head>
<title>tester</title>
<link rel=stylesheet href="s/jquery-ui-1.10.3.custom.min.css">
<script src="http://ajax.googleapis./ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="js/jquery-ui-1.10.3.custom.min.js"></script>
<script src="js/eventsource.js"></script>
<script src="js/json2.js"></script>
<script>
var MixerStatusMonitor=(function(){
    var _src=null,
    _handler={
        onStart:function(e){
            MixerStatus.setMax(parseInt(e.data));
        },
        onUpdate:function(e){
            var data=JSON.parse(e.data);
            MixerStatus.setValue(parseInt(data.progress));
            MixerStatus.setStatus(data.message);
        },
        onFinish:function(e){
            //var data=JSON.parse(e.data);
            MixerStatus.hide();
            _src.close();
        }
    };
    return {
        init:function(){
            if(_src)_src.close();
            _src=new EventSource('/daemon/updates.php?type=b');
            _src.addEventListener('update',_handler.onUpdate,false);
            _src.addEventListener('start',_handler.onStart,false);
            _src.addEventListener('finish',_handler.onFinish,false);
            MixerStatus.show();
        }
    };
})();
var MixerStatus=(function(){
        var dialog=null,pbar=null,text=null;
        return {
            init:function(){
                dialog=$('#buildStatus').dialog({autoOpen:false});
                pbar=$('#buildStatus .progress').progressbar({value:false});
                text=$('#buildStatus .text').progressbar();
            },
            setStatus:function(txt){
                text.html(txt);
            },
            setMax:function(val){
                pbar.progressbar('option','max',val);
            },
            setValue:function(val){
                pbar.progressbar('option','value',val);
            },
            show:function(){
                dialog.dialog('open');
            },
            hide:function(){
                dialog.dialog('close');
            }
        };
})();
$(document).ready(function(){
    MixerStatus.init();//build the UI
    $('#updater').on('submit',function(){
        $.ajax({
            type:'post',
            url:'test-updates.php',
            data:$('#updater').serialize(),
            beforeSend:function(){
                if($('#updater select[name=type]').val()=='start'){
                    MixerStatusMonitor.init();
                }
            }
        });
        return false;
    });
});
</script>
</head>
<body>
<p>Start event sets the max
<p>update event: {"progress":"","message":""}
<p>finish event: {"progress":"","message":""}
<form id=updater>
message: <input type=text name=message value="15"><br>
event type: <select name=type>
<option value=start>start</option>
<option value=update>update</option>
<option value=finish>finish</option>
</select><br>
<button>send message</button>
</form>
<div id=buildStatus title="Building">
<div class=text></div>
<div class=progress></div>
</div>
<div id=messages></div>
</body>
</html>

The PHP:

<?php
header('Content-Type: text/event-stream');
define('TYPE_BROADCAST','b');
define('MAX_FAILURES',30);//30 seconds
define('MAX_WAIT',30);//30 seconds
define('MAX_START_WAIT',6);//30 seconds
/*
 * URL arguments:
 * type
 */
include '../libconfig.php';
session_write_close();
if(!App::loggedIn() || !App::user()){
    printEvent(0,'finish','Login session has expired.');
}
if($_GET['type']==TYPE_BROADCAST){//not needed;specific to the app I am creating
    $db=db_get_connection();
    $stm=new PDOStatementWrapper(db_prepare($db,'SELECT * FROM updates WHERE user_id=:user_id AND evt_id>:last_id'));
    $args=array(':user_id'=>App::user()->getId(),':last_id'=>0);
    $stm->bindParam(':user_id',$args[':user_id'],PDO::PARAM_INT);
    $stm->bindParam(':last_id',$args[':last_id'],PDO::PARAM_INT);
    $failures=0;
    $nomsg=0;
    if(!isset($_SERVER['HTTP_LAST_EVENT_ID'])){
        $start=new PDOStatementWrapper(db_prepare($db,'SELECT * FROM updates WHERE user_id=:user_id ORDER BY evt_id DESC'));
        $start->bindValue(':user_id',$args[':user_id'],PDO::PARAM_INT);
        $startwait=0;
        while(1){
            if($startwait>MAX_START_WAIT){
                printEvent(0,'finish','Timed out waiting for the process to start.');
                return;
            }
            sleep(5);
            $startwait++;
            if(!$start->run()){
                printEvent(0,'finish','DB error while getting the starting event.');
                return;
            }
            while($start->loadNext()){
                if($start->get('evt_type')=='finish')continue 2;
                if($start->get('evt_type')=='start')break;
            }
            if($start->get('evt_type')=='start'){
                $args[':last_id']=$start->get('evt_id');
                printEvent($start->get('evt_id'),'start',$start->get('evt_msg'));
                break;
            }
        }
    }else
        $args[':last_id']=$_SERVER['HTTP_LAST_EVENT_ID'];
    if($args[':last_id']===0){
        printEvent(0,'finish','ll');
        exit;
    }
    while(1){
        sleep(1);
        if(!$stm->run()){
            $failures++;
            if($failures>MAX_FAILURES){
                printEvent(0,'finish','Max failures reached.');
                break;
            }
        }
        if($stm->loadNext()){
            $failures=0;
            $nomsg=0;
            do{
                if($stm->get('evt_type')=='finish')break;
                $args[':last_id']=$stm->get('evt_id');
                printEvent($stm->get('evt_id'),$stm->get('evt_type'),$stm->get('evt_msg'));
            }while($stm->loadNext());
            if($stm->get('evt_type')=='finish'){
                printEvent($args[':last_id'],'finish',$stm->get('evt_msg'));
                break;
            }
        }else{
            $nomsg++;
            if($nomsg>MAX_WAIT){
                exit;//TODO: test
            }
        }
    }
}else{
    printEvent(0,'close','Unknown event type.');
}

function printEvent($id,$name,$data){
    echo "id: $id\nevent: $name\n";
    if(is_array($data)){
        foreach($data as $datum)
            echo "data: $datum\n";
        echo "\n";
    }else
        echo "data: $data\n\n";
    flush();
    if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
        $_SERVER['HTTP_X_REQUESTED_WITH']=='XMLHttpRequest')exit;//ajax request. Need to kill the connection.
}

In case you were wondering about PDOStatementWrapper the source for it is here. Sorry it doesn't include anything integrated with CodeIgniter.

发布评论

评论列表(0)

  1. 暂无评论