使用PHP和Ajax实现进度条

6
我正在开发一个进度条,使用ajax请求和会话变量来更新进度。当我的程序执行耗时操作(例如发送多个电子邮件等)时,它只需设置正确的会话变量(其中包含进度值)。此操作由下面代码中的post()函数启动。
同时,第二个函数ask()将在每500毫秒的循环中执行。它应该实时返回当前进度。这里有个问题:由ask()发送的每个请求都在等待post()函数发送的请求完成。有趣的是,如果我将某些URL(如google.com)设置为url/to/progress,它能正常工作,但这不是我想要的 : )。这意味着问题出在服务器端。
不确定是否重要,但我使用Yii框架。
下面的所有代码仅仅是草稿(但可用),其唯一目的是展示我的意思。
提前感谢您。
对于我的糟糕英语表示抱歉 : )
视图部分:
<script type="text/javascript">
function ask() {
  var d = new Date();
  var time = d.getTime();
  $.ajax({

    type: 'get',
    url: '/url/to/progress' + '?time=' + time,
    success: function(data) {
      $("#progress").html(data);
    }
  })
}

function post() {
  var d = new Date();
  var time = d.getTime();

  $.ajax({
      type: 'post',
      url: '/url/to/post' + '?time=' + time,
      data: {"some": "data"},
      success: function(data) {alert(data)}
    });
}

$("#test").click(
  function() {
    post();
    var progress = setInterval("ask();", 500);
  }
);
</script>

控制器部分:

public function actionPost($time) {
  sleep(5); // time consuming operation
  echo $time . ' : ' . microtime();
  exit;
}

public function actionProgress($time) {
  echo $time . ' : ' . microtime();
  exit;
}

你从服务器那里得到了什么响应?在开发工具中检查 > 网络 > XHR 请求。 - Gabo Esquivel
2个回答

8
我认为你的问题与会话相关。
当一个脚本有一个打开的会话时,它会锁定会话文件。这意味着任何后续使用相同会话ID的请求都将排队等待,直到第一个脚本释放了对会话文件的锁定。您可以使用session_write_close()来强制执行此操作,但这对您没有真正帮助,因为您试图与会话文件共享进度信息,所以post脚本需要保持会话数据处于打开和可写状态。
您需要想出另一种在postprogress脚本之间共享数据的方法-如果post在其执行期间一直保持会话数据处于打开状态,则progress将无法访问会话数据,直到post执行完成。也许您可以使用会话ID创建一个post具有写入访问权限的临时文件,在其中放置进度指示器数据。 progress可以检查该文件并返回该数据。有许多IPC(进程间通信)选项-这不是特别美观的选项,但它具有最大的可移植性。
顺便说一下,请不要将字符串传递给setInterval(),请传递函数。因此,您的行实际上应该是:
var progress = setInterval(ask, 500);

然而,最好在ask() ajax函数的success/error处理程序中使用setTimeout()。这是因为使用setInterval()时,无论上一个请求的状态如何,都将启动一个新的请求。在启动下一个请求之前,等待上一个请求完成会更有效率。因此,我会做出以下更改:

<script type="text/javascript">

  // We'll set this to true when the initail POST request is complete, so we
  // can easily know when to stop polling the server for progress updates
  var postComplete = false;

  var ask = function() {

    var time = new Date().getTime();

    $.ajax({

      type: 'get',
      url: '/url/to/progress' + '?time=' + time,

      success: function(data) {
        $("#progress").html(data);
        if (!postComplete)
          setTimeout(ask, 500);
        }
      },
      error: function() {
        // We need an error handler as well, to ensure another attempt gets scheduled
        if (!postComplete)
          setTimeout(ask, 500);
        }
      }

    });

  }

  $("#test").click(function() {

    // Since you only ever call post() once, you don't need a seperate function.
    // You can just put all the post() code here.

    var time = new Date().getTime();

    $.ajax({

      type: 'post',
      url: '/url/to/post' + '?time=' + time,
      data: {
        "some": "data"
      },

      success: function(data) {
        postComplete = true;
        alert(data);
      }
      error: function() {
        postComplete = true;
      }

    });

    if (!postComplete)
      setTimeout(ask, 500);
    }

  });

</script>

虽然这仍然无法解决会话问题。


你说得对。我找到了session_write_close()函数,看起来它是有效的!我所需要做的就是在开始操作时关闭会话,然后仅在更新会话变量时重新打开它。非常感谢你,并感谢你的建议。 - pawelo
注意不要在同一个脚本中多次打开和关闭会话数据 - 我曾经看到有人抱怨它的可靠性不高,所以请确保您彻底测试任何您想实现的功能。 - DaveRandom
嗯,或许更好的解决方案是将进度变量存储在数据库或文件中。我必须去检查一下,谢谢! - pawelo

5

@DaveRandom正确指出了您是会话存储锁的受害者。

解决方法非常简单。您需要使处理post()的脚本释放对会话数据的锁定,以便处理ask()的脚本可以访问此会话数据。您可以使用session_write_close来实现。

这里需要注意的是,在调用session_write_close之后,您将无法访问会话变量,因此您需要按照以下方式结构化post脚本:

  1. $_SESSION中读取所有需要的数据并保存副本。
  2. 调用session_write_close以释放会话锁。
  3. 继续您的漫长操作。如果需要会话数据,请直接从自己的副本中获取,而不是直接从$_SESSION获取。

或者,您可以在脚本生命周期内多次切换会话锁:

session_start();
$_SESSION['name'] = 'Jon';

// Quick operation that requires session data
echo 'Hello '.$_SESSION['name'];

//  Release session lock
session_write_close();

// Long operation that does not require session data.
sleep(10);

// Need access to session again
session_start();
echo 'Hello again '.$_SESSION['name'];

这种安排使得当脚本处于休眠状态时,其他脚本可以无问题地访问会话数据。


我差点说“在post脚本中只需使用session_write_close()”直到我重新阅读问题并意识到他想要通过会话数据共享漫长操作的进度,这显然是不可能的。您可以尝试在需要更新进度信息时执行session_write_close(),然后再执行session_start() - 我自己从未尝试过这样做,但我记得在这里看到有人发现这样做不起作用,因此如果它在某个地方起作用,那么它可能无法在任何地方起作用。因此,“您需要想出另一种共享数据的方法”。 - DaveRandom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接