高效且用户友好的方式呈现缓慢加载的结果

2022-08-30 22:26:38

我已经阅读了许多关于使用jQuery取消POST请求的类似问题,但似乎没有一个与我接近。

我有你的日常表单,它有一个PHP页面作为操作:

<form action="results.php">
  <input name="my-input" type="text">
  <input type="submit" value="submit">
</form>

根据表单中给出的帖子信息,服务器端的处理需要很长时间(30秒甚至更长时间,我们预计会增加,因为我们的搜索空间将在未来几周内增加)。我们正在访问包含所有数据的 Basex 服务器(版本 7.9,不可升级)。用户生成的 XPath 代码在表单中提交,然后操作 url 将 XPath 代码发送到返回结果的 Basex 服务器。从可用性的角度来看,我已经显示了一个“加载”屏幕,因此用户至少知道正在生成结果:results.php

$("form").submit(function() {
  $("#overlay").show();
});

<div id="overlay"><p>Results are being generated</p></div>

但是,我还希望为用户提供按按钮以取消请求的选项,并在用户关闭页面时取消请求。请注意,在前一种情况下(单击按钮时),这也意味着用户应保持在同一页面上,可以编辑其输入,并立即重新提交其请求。最重要的是,当他们取消请求时,他们也可以立即重新发送请求:服务器应该真正中止,并且在能够处理新查询之前不要完成查询。

我想像这样:

$("form").submit(function() {
  $("#overlay").show();
});
$("#overlay button").click(abortRequest);
$(window).unload(abortRequest);

function abortRequest() {
  // abort correct request
}

<div id="overlay">
  <p>Results are being generated</p>
  <button>Cancel</button>
</div>

但是正如你所看到的,我不完全确定如何填写以确保post请求被中止并终止,以便可以发送新的查询。请填写空白!或者我需要提交表单,而是从jQuery进行ajax()调用?abortRequest.preventDefault()


正如我所说,我也想停止服务器端的进程,从我所读到的内容来看,我需要这样做。但是我怎么能再做一个PHP函数呢?例如,假设我有一个处理脚本,我需要退出该脚本,我会做这样的事情吗?exit()exitresults.php

<?php
  if (isset($_POST['my-input'])) {
    $input = $_POST['my-input'];
    function processData() {
      // A lot of processing
    }
    processData()
  }

  if (isset($_POST['terminate'])) {
    function terminateProcess() {
      // exit processData()
    }
  }

然后在我需要终止进程时执行新的ajax请求?

$("#overlay button").click(abortRequest);
$(window).unload(abortRequest);

function abortRequest() {
  $.ajax({
    url: 'results.php',
    data: {terminate: true},
    type: 'post',
    success: function() {alert("terminated");});
  });
}

我做了更多的研究,我找到了这个答案。它提到了connection_aborted()和session_write_close(),我不完全确定哪个对我有用。我确实使用SESSION变量,但是当进程被取消时,我不需要写掉值(尽管我想保持SESSION变量处于活动状态)。

会是这样吗?如果是这样,我如何使一个PHP函数终止另一个?


我也读过Websockets,它似乎可以工作,但我不喜欢设置Websocket服务器的麻烦,因为这需要我联系我们的IT人员,他们需要对新软件包进行广泛的测试。我宁愿把它保留给PHP和JS,没有jQuery以外的第三方库。

考虑到大多数评论和答案表明我想要的东西是不可能的,我也有兴趣听到替代方案。首先想到的是分页的Ajax调用(类似于许多网页,这些网页在无限滚动中提供搜索结果,图像,您拥有的内容)。用户将获得一个包含X个第一个结果(例如20个)的页面,当他们单击“显示接下来的20个结果”按钮时,将显示这些结果。此过程可以继续,直到显示所有结果。由于获取所有结果对用户很有用,因此我还提供了“下载所有结果”选项。这也需要很长时间,但至少用户应该能够在页面本身上浏览第一个结果。(因此,下载按钮不应中断 Ajax 页面加载。这只是一个想法,但我希望它能给你们中的一些人一些灵感。


答案 1

根据我的理解,关键点是:

  • 如果提交了表单,则无法取消特定请求。原因在客户端,您没有任何内容,以便您可以识别表单请求的状态(如果它已发布,如果它正在处理等)。因此,取消它的唯一方法是重置变量和/或刷新页面。因此,连接将被断开,并且不会完成上一个请求。$_POST

  • 在您的替代解决方案上,当您发送另一个呼叫时,可以使用简单的.但是,由于这将是一个调用 - 您无法将其与上一个.因此,这实际上行不通。Ajax{terminate: true}result.phpdie()asyncform submit

  • 可能的解决方案:使用Ajax提交表单。使用ajax,您将拥有一个对象,您可以在窗口卸载时使用它。jQueryxhrabort()

更新(根据评论):

  • 同步请求是指您的页面将阻止(所有用户操作),直到结果准备就绪。按表单中的提交按钮 - 通过提交表单对服务器进行同步调用 - 根据定义 [https://www.w3.org/TR/html-markup/button.submit.html]。

  • 现在,当用户按下提交按钮时,从浏览器到服务器的连接是同步的 - 因此在结果出现之前不会受到阻碍。因此,当对服务器进行其他调用时 - 在提交过程中 - 其他人无法引用此操作 - 因为它尚未完成。这就是为什么使用Ajax发送终止调用不起作用的原因。

  • 第三:对于您的情况,您可以考虑以下代码示例:

网页

<form action="results.php">
  <input name="my-input" type="text">
  <input id="resultMaker" type="button" value="submit">
</form>

<div id="overlay">
  <p>Results are being generated</p>
  <button>Cancel</button>
</div>

JQUERY

<script type="text/javascript">
    var jqXhr = '';

    $('#resultMaker').on('click', function(){

      $("#overlay").show();

      jqXhr = $.ajax({
        url: 'results.php',
        data: $('form').serialize(),
        type: 'post',
        success: function() {
           $("#overlay").hide();
        });
      });
    });

    var abortRequest = function(){
      if (jqXhr != '') {
        jqXhr.abort();
      }
    };

    $("#overlay button").on('click', abortRequest);
    window.addEventListener('unload', abortRequest);
</script>

这是示例代码 - 我刚刚使用了您的代码示例并在这里和那里更改了一些东西。


答案 2

Himel Nag Rana演示了如何取消待处理的Ajax请求。有几个因素可能会干扰和延迟后续请求,正如我之前在另一篇文章中讨论的那样。

TL;DR: 1.尝试从长时间运行的任务本身和2中检测请求被取消是非常不方便的。作为一种解决方法,您应该在长时间运行的任务中尽早关闭会话(session_write_close()),以免阻止后续请求。

connection_aborted()不能使用。此函数应该在长时间任务期间定期调用(通常在循环内)。不幸的是,在你的例子中,只有一个重要的原子操作:对数据后端的查询。

如果您应用了 Himel Nag Rana 和我本人建议的程序,您现在应该能够取消 Ajax 请求并立即允许新的请求继续进行。剩下的唯一问题是,上一个(已取消的)请求可能会在后台运行一段时间(不会阻止用户,只是在服务器上浪费资源)。

这个问题可以改写为“如何从外部中止特定进程”。

正如克里斯蒂安·博纳托(Christian Bonato)理所当然地建议的那样,这是一个可能的实现。为了演示,我将依靠Symphony的流程组件,但如果您愿意,可以设计一个更简单的自定义解决方案。

基本方法是:

  1. 生成一个新进程来运行查询,将 PID 保存在会话中。等待它完成,然后将结果返回给客户端

  2. 如果客户端中止,它会向服务器发出信号,要求停止进程。


<?php // query.php

use Symfony\Component\Process\PhpProcess;

session_start();

if(isset($_SESSION['queryPID'])) {
    // A query is already running for this session
    // As this should never happen, you may want to raise an error instead
    // of just silently  killing the previous query.
    posix_kill($_SESSION['queryPID'], SIGKILL);
    unset($_SESSION['queryPID']);
}

$queryString = parseRequest($_POST);

$process = new PhpProcess(sprintf(
    '<?php $result = runQuery(%s); echo fetchResult($result);',
    $queryString
));
$process->start();

$_SESSION['queryPID'] = $process->getPid();
session_write_close();
$process->wait();

$result = $process->getOutput();
echo formatResponse($result);

?>


<?php // abort.php

session_start();

if(isset($_SESSION['queryPID'])) {

    $pid = $_SESSION['queryPID'];
    posix_kill($pid, SIGKILL);
    unset($pid);
    echo "Query $pid has been aborted";

} else {

    // there is nothing to abort, send a HTTP error code
    header($_SERVER['SERVER_PROTOCOL'] . ' 599 No pending query', true, 599);

}

?>


// javascript
function abortRequest(pendingXHRRequest) {
    pendingXHRRequest.abort();
    $.ajax({
        url: 'abort.php',
        success: function() { alert("terminated"); });
      });
}

生成一个进程并跟踪它确实很棘手,这就是为什么我建议使用现有模块的原因。通过Composer仅集成一个Symfony组件应该相对容易:首先安装Composer,然后安装Process组件()。composer require symfony/process

手动实现可能看起来像这样(请注意,这是未经测试的,不完整的,可能是不稳定的,但我相信你会明白这个想法):

<?php // query.php

    session_start();

    $queryString = parseRequest($_POST); // $queryString should be escaped via escapeshellarg()

    $processHandler = popen("/path/to/php-cli/php asyncQuery.php $queryString", 'r');

    // fetch the first line of output, PID expected
    $pid = fgets($processHandler);
    $_SESSION['queryPID'] = $pid;
    session_write_close();

    // fetch the rest of the output
    while($line = fgets($processHandler)) {
        echo $line; // or save this line for further processing, e.g. through json_encode()
    }
    fclose($processHandler);

?>


<?php // asyncQuery.php

    // echo the current PID
    echo getmypid() . PHP_EOL;

    // then execute the query and echo the result
    $result = runQuery($argv[1]);
    echo fetchResult($result);

?>

推荐