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

Efficient and user-friendly way to present slow-loading results

我已经阅读了 many similar questions 关于使用 jQuery 取消 POST 请求的内容,但是 none 似乎与我的接近。

我有你的日常表格,其中有一个 PHP 页作为一个动作:

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

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

$("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>

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

调用 ajax()

正如我所说,我也想停止服务器端的进程,根据我的阅读,我需要 exit() 为此。但是我怎样才能 exit 另一个 PHP 函数呢?例如,假设在 results.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");});
  });
}

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

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


我也读过 Websockets,它似乎可以工作,但我不喜欢设置 Websocket 服务器的麻烦,因为这需要我联系需要对新包进行广泛测试的 IT 人员.我宁愿将其保留为 PHP 和 JS,除了 jQuery.

之外没有第三方库

考虑到大多数评论和答案表明我想要的是不可能的,我也有兴趣听到替代方案。首先想到的是分页 Ajax 调用(类似于许多在无限滚动中提供搜索结果、图像和你有什么的网页)。向用户提供一个包含 X 个第一结果(例如 20)的页面,当他们单击按钮 "show next 20 results" 时,将附加显示的内容。该过程可以继续,直到显示所有结果。因为方便用户获取所有结果,所以我也会提供一个"download all results"选项。这也将花费很长时间,但至少用户应该能够浏览页面本身的第一个结果。 (因此下载按钮不应中断 Ajax 分页加载。)这只是一个想法,但我希望它能给你们一些灵感。

根据我的理解,重点是:

  • 如果已提交表单,则无法取消特定请求。原因在客户端,您没有任何东西可以识别表单请求的状态(是否已发布,是否正在处理等)。所以取消它的唯一方法是重置 $_POST 变量 and/or 刷新页面。所以连接会中断,之前的请求也不会完成。

  • 在您的替代解决方案中,当您使用 {terminate: true} 发送另一个 Ajax 调用时,result.php 可以使用简单的 die() 停止处理。但是因为它将是一个 async 调用——您不能将它映射到之前的 form submit。所以这实际上行不通。

  • 可能的解决方案:提交带有 Ajax 的表单。使用 jQuery ajax 你将有一个 xhr 对象,你可以 abort() 在 window 卸载时。

更新(根据评论):

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

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

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

HTML:

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

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

用户不必同步体验。

  1. 客户发布请求
  2. 服务器收到客户端请求并为其分配一个ID
  3. 服务器 "kicks off" 搜索并响应零数据页面和搜索 ID
  4. 客户端收到 "placeholder" 页面并开始根据 ID 检查结果是否准备就绪(使用轮询或 websockets 之类的东西)
  5. 搜索完成后,服务器会在下次轮询时返回结果(或在使用 websockets 时直接通知客户端)

当性能不是瓶颈并且处理的性质使得更长的等待时间可以接受时,这很好。想想定期 运行 30-90 秒的航班搜索聚合器,或者必须安排 运行 更长时间的报告生成器!

如果您不阻止用户交互,让他们了解搜索进度并在可能的情况下开始显示结果,您可以减少体验的沮丧。

在 BaseX 8.4 中,引入了新的 RESTXQ 注释 %rest:single,它允许您取消 运行 服务器端请求:http://docs.basex.org/wiki/RESTXQ#Query_Execution。它应该至少解决您描述的一些挑战。

目前只有 return 个结果块的方法是将索引传递给结果中的第一个和最后一个结果,并在 XQuery 中进行过滤:

$results[position() = $start to $end]

通过 return 比请求多一个结果,客户端将知道会有更多结果。这可能会有所帮助,因为计算总结果大小通常比 return 仅计算第一个结果要昂贵得多。

如何取消待处理的 Ajax 请求。 正如我之前在 another post.

中讨论的那样,有几个因素可能会干扰和延迟后续请求

TL;DR: 1. 尝试检测从长 运行ning 任务本身中取消的请求是非常不方便的,并且 2. 作为解决方法,您应该关闭会话 (session_write_close()) 尽早完成你的 long-运行ning 任务,以免阻塞后续请求。

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

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

问题可以改写为 "how to abort a specific process from the outside"。

, here is a possible implementation. For the sake of the demonstration I will rely on Symphony's Process component,但如果您愿意,可以设计更简单的自定义解决方案。

基本方法是:

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

  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"); });
      });
}

产生一个进程并跟踪它确实很棘手,这就是我建议使用现有模块的原因。只集成一个Symfony组件应该是相对easy via Composer: first install 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);

?>

您必须先从概念上解决这个问题,然后再编写任何代码。以下是一些随手想到的事情:

释放服务器上的资源是什么意思?

释放资源的正常中止是什么?

是否足以终止等待查询结果的 PHP 进程?如果是这样,RandomSeed 建议的路线可能会很有趣。请记住,它只能在单个服务器上运行。如果您有多个负载平衡服务器,您将无法终止另一台服务器上的进程(至少不那么容易)。

或者需要取消数据库本身的数据库请求?在那种情况下,Christian Grün 建议的答案更有趣。

还是说没有优雅关机,非得强行死掉?如果是这样,这似乎非常 hacky。

并非所有客户端都会显式中止

有些客户要关闭浏览器,但他们的最后一个请求不会通过;一些客户端会失去互联网连接并使服务挂起等。当客户端断开连接或离开时,您不能保证收到 "abort" 请求。

您必须决定是忍受可能不受欢迎的行为,还是实施额外的活动状态跟踪,例如客户端 ping 服务器以保持活动状态。

旁注

  • 30 秒或更长的查询时间可能很长,是否有更好的工具来完成这项工作;这样你就不必用这样的黑客来解决这个问题了?

  • 您正在寻找并发系统的特性,但您没有使用并发系统;如果你想并发使用更好的 tool/environment ,例如二郎.

希望我理解正确。

与其让浏览器 "natively" 提交表单,不如:编写执行此操作的 JS 代码。换句话说(我没有测试这个;所以解释为伪代码):

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

所以,现在,当 "submit" 按钮被点击时,什么也不会发生。

显然,您希望发布表单,因此编写 JS 以在该提交按钮上附加点击处理程序,从表单中的所有输入字段收集值(实际上,它并不像听起来那么可怕;查看link 下面),并将它发送到服务器,同时保存对请求的引用(检查下面的第 2 个 link),这样你就可以中止它(也可能向服务器发出退出信号) ) 当单击取消按钮时(或者,您可以简单地放弃它,而不关心结果)。

Submit a form using jQuery

Abort Ajax requests using jQuery

或者,要使 HTML 标记 "clearer" 与其功能相关,请考虑根本不使用 FORM 标记:否则,我的建议会使它的用法变得混乱(如果它是没用过;知道我的意思吗?)。但是,在你按照你想要的方式工作之前,不要被这个建议分心;它是可选的,改天再讨论(它甚至可能与您更改整个网站的架构有关)。

但是,需要考虑的一件事是:如果表单-post 已经到达服务器并且服务器已经开始处理它并且已经进行了一些 "world" 更改怎么办?也许您的获取结果例程不会更改数据,所以没关系。但是,这种方法可能不能用于更改数据 POST,因为如果单击取消按钮,"world" 不会更改。

希望对您有所帮助:)