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

Posted

技术标签:

【中文标题】高效且用户友好的方式来呈现加载缓慢的结果【英文标题】:Efficient and user-friendly way to present slow-loading results 【发布时间】:2016-08-09 02:14:47 【问题描述】:

我已阅读 many similar questions 关于取消使用 jQuery 的 POST 请求,但似乎没有一个接近我的。

我有你的日常表单,其中包含一个 php 页面作为操作:

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

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

$("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 以确保发布请求被中止,并终止,以便可以发送新查询。请填空!还是我需要.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。

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

【问题讨论】:

据我所知,一旦服务器开始处理请求,就无法取消请求(不过我很乐意被证明是错误的!)。我可以想到一些开箱即用的解决方案,但对于 15 秒的过程来说,它们不值得。您可以尝试查看网络套接字而不是 ajax。 是的,网络套接字可能是一个很好的解决方案。 这会自动发生。一旦用户取消代码,PHP 就会终止代码。但是,如果没有,您可以阅读***.com/a/16592945/2729937 哪种写得不好的后端需要 30 秒才能处理?您正在尝试解决前端问题,但您遇到了后端问题。干净利落。也就是说,你需要一个中间件:你提交一个请求,给它一个 ID 和所有有用的信息(想到 IP 地址,一个即时生成的 cookie,任何有助于检索请求来源的东西),然后才查询已启动。 @BramVanroy — 抱歉,这听起来有点刺耳。尽管如此,在预处理方面是否可以做一些事情?有什么方法可以派发这 5000 万个代币?我的猜测是,同时查询 4 ​​个或 5 个 BaseX 会更快,而不是只查询一个。在 5000 万个条目中肯定存在某种可调度的层次结构...... 【参考方案1】:

据我了解,关键点是:

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

在您使用terminate: true 发送另一个Ajax 呼叫时,result.php 可以通过简单的die() 停止处理。但是因为这将是一个async 调用——你不能将它与之前的form submit 映射。所以这实际上行不通。

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

更新(根据评论):

同步请求是指您的页面将阻塞(所有用户操作)直到结果准备好。在表单中按下提交按钮 - 通过提交表单对服务器进行同步调用 - 根据定义 [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>

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

【讨论】:

:-) 好的,让我扩展一下我的答案。 不知道为什么要混合香草 JS 和 jQuery。此外,您只将数据发布到 url,但提交后不重定向。 @BramVanroy,首先是原生的 JS 部分——jQuery unload 选择器在 v1.8 之后被弃用。所以更安全的方法是在这种情况下使用传统方式。 其次,您可以使用window.location.href=&lt;your url&gt;success 函数中进行重定向——我认为重定向不是问题——正如我在我的观点中提到的——重定向意味着同步连接,这不可能中止。 我会在这个答案中补充一点,您还需要不断检查 connection_aborted() 返回的内容。您应该在代码的某些检查点中添加此类调用,并且当它返回 1 - 您需要取消处理。此外,如果您运行一些缓慢的外部代码,您可能需要引入锁定文件,以便在当前文件中止之前不开始新的处理。 感谢您的回复。我明白你的意思。我试过了,它可以工作,但显然我仍然无法停止服务器。因此,请查看我的编辑。【参考方案2】:

Himel Nag Rana demonstrated 如何取消挂起的 Ajax 请求。 正如我之前在another post 中讨论的那样,有几个因素可能会干扰和延迟后续请求。

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

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

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

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

作为Christian Bonato rightfully advised,这是一个可能的实现。为了演示,我将依赖 Symphony's Process component,但如果您愿意,可以设计一个更简单的自定义解决方案。

基本做法是:

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

    如果客户端中止,它会向服务器发出信号以终止进程。

<?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:首先是 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);

?>

【讨论】:

我对第一段代码有点困惑,特别是PhpProcess(如上所述,我不是 PHP 开发人员)。我是否正确地假设我不需要它(因为它来自 Symfony),只有 PID?另外,似乎魔法就在posix_kill,但是如果我不使用Symfony,我该如何使用进程ID?我读到了getmypid,但这似乎不是reliable。 我接受了您的回答,因为赏金即将到期,您的回答似乎很有希望。但是,我想对上面的 PhpProcess 和进程 ID 问题进行跟进。谢谢。 @BramVanroy 抱歉,我最近很忙。看我的编辑,希望对你有帮助。如果您担心 PID 不是唯一的,它通常是一个最大为 32767 的连续整数。如果您的进程持续时间足够长以使系统产生的进程数超过此数量,那么是时候采取严肃的解决方案了(搜索“消息队列”)。【参考方案3】:

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

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

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

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

【讨论】:

嗨克里斯蒂安,感谢您的快速回复!不幸的是,我们坚持使用 7.9。我已将这些信息添加到我的初始帖子中。 对不起,我明白了。这确实需要更多的思考。 – 我刚刚扩展了我最初的回复,以提供更多关于分块结果的反馈。 作为对您附录的回复:我有您对this question 的回复。所以如果我使用position() = 11 to 20,这只会显示下一个十个结果?所以理论上我可以通过我假设的 PHP 增加位置。但这会导致速度提升吗?如果我单击按钮(并询问下一个 X)结果,并且位置是从 20 到 30,XQuery 是否也会搜索/遇到 hts 1-19?还是 Basex 足够聪明,知道它在哪里中断了? 没错。它会快得多。如果您的查询允许迭代处理结果。这是例如(//*[text() = 'bla'])[position() = 1 to 10] 的情况,但如果你有“阻塞操作符”,例如 order by,这是不可能的。在后一种情况下,需要检查所有结果以确定需要输出哪个结果。 – 如果您请求命中 20-30,您的查询也将检索结果 1-19,但与检索总结果集相比,这通常难以察觉。 所以如果我理解正确,它会是这样的。页面加载,显示 20 个第一个结果。用户单击按钮(位置设置为 21-40)。 Basex 找到 0-40 个结果,但只返回 21-40。用户再次单击按钮(位置设置为 41-60)。 Basex 找到 0-60,但只返回 41-60。等等。所以每次,basex 都必须做一个新的查询,用户点击按钮的次数越多,需要的时间就越长(因为最终他们会寻找,比如说,1021-1040,这意味着寻找 0-1040) . 但是从好的方面来说,这对用户更友好,用户可能不会浏览所有结果【参考方案4】:

我希望我理解正确。

不要让浏览器“本机”提交表单,而是:编写执行此操作的 JS 代码。换句话说(我没有测试这个;所以解释为伪代码):

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

所以,现在,当点击那个“提交”按钮时,什么都不会发生。

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

Submit a form using jQuery

Abort Ajax requests using jQuery

或者,为了使 HTML 标记相对于其功能“更清晰”,请考虑完全不使用 FORM 标记:否则,我的建议会使它的使用变得混乱(如果不使用它为什么会存在;知道我的意思吗?) .但是,在你让它按照你想要的方式工作之前,不要被这个建议分心;它是可选的,是另一天的主题(它甚至可能与您不断变化的整个网站架构有关)。

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

希望对你有帮助:)

【讨论】:

【参考方案5】:

用户不必同步体验。

    客户发布请求 服务器接收客户端请求并为其分配一个ID 服务器“启动”搜索并以零数据页面和搜索 ID 进行响应 客户端收到“占位符”页面并开始根据 ID 检查结果是否准备就绪(使用轮询或 websockets 等) 一旦搜索完成,服务器会在下次轮询时响应结果(或在使用 websockets 时直接通知客户端)

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

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

【讨论】:

嗯,太好了。不幸的是,我对 Web 套接字一无所知,也不知道如何实现这个想法。有了 200 的赏金,我认为从请求中要求一些示例代码直到结果出来是公平的?【参考方案6】:

在编写任何代码之前,您必须先从概念上解决这个问题。以下是一些临时想到的事情:

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

什么是可以释放资源的优雅中止?

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

或者是否需要取消来自数据库本身的数据库请求?在这种情况下,Christian Grün 提出的答案更有趣。

或者是没有优雅的关机,你必须强迫一切死掉?如果是这样,这看起来非常h​​acky。

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

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

您必须决定是接受潜在有害行为,还是实施额外的活动状态跟踪,例如客户端 ping 服务器以获取保活。

旁注

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

您正在寻找并发系统的功能,但您没有使用并发系统;如果您想要并发使用更好的工具/环境,例如二郎。

【讨论】:

以上是关于高效且用户友好的方式来呈现加载缓慢的结果的主要内容,如果未能解决你的问题,请参考以下文章

单击链接时缓慢加载 Facebook 画布应用程序(如果目标顶部)

应用程序最初运行缓慢?

UITextField 初始化缓慢?

带有 MSSCCI 的 VFP 9 SP2:项目加载缓慢

vue 首次加载缓慢/刷新后加载缓慢 原因及解决方案

android意图选择器出现缓慢