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

时间:2016-04-17 10:32:47

标签: php jquery basex

我已阅读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服务器。从可用性的角度来看,我已经展示了一个&#34; loading&#34;屏幕,以便用户至少知道结果正在生成:

$("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以外的第三方库。

考虑到大多数评论和答案表明我想要的是不可能的,我也有兴趣听取替代方案。首先想到的是分页Ajax调用(类似于许多提供搜索结果,图像,无限卷轴中的内容的网页)。向用户提供具有X个第一结果(例如20)的页面,并且当他们点击按钮时#34;显示接下来的20个结果&#34;显示的是附加的。此过程可以继续,直到显示所有结果。因为它对用户获得所有结果很有用,所以我还会提供一个&#34;下载所有结果&#34;选项。这将花费很长时间,但至少用户应该能够在页面本身上完成第一个结果。 (因此下载按钮不应该破坏Ajax页面加载。)这只是一个想法,但我希望它能给你们一些灵感。

6 个答案:

答案 0 :(得分:4)

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

  • 如果提交表单,则无法取消特定请求。原因是在客户端你没有任何东西,以便你可以识别表单请求的状态(如果它被发布,如果它正在处理,等等)。因此,取消它的唯一方法是重置$_POST变量和/或刷新页面。因此连接将被破坏,之前的请求将无法完成。

  • 当您使用Ajax发送另一个{terminate: true}来电时,在您的替代解决方案中,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>

这是示例代码 - 我刚刚使用了您的代码示例并在此处进行了更改。

答案 1 :(得分:3)

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,但如果您愿意,可以设计一个更简单的自定义解决方案。

基本方法是:

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

  2. 如果客户端中止,则表示服务器只是终止进程。


  3. <?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,然后是流程组件(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);
    
    ?>
    

答案 2 :(得分:2)

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

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

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

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

答案 3 :(得分:1)

我希望我能正确理解这一点。

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

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

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

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

Submit a form using jQuery

Abort Ajax requests using jQuery

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

然而,需要考虑一下:如果表格帖子已经到达服务器并且服务器已经开始处理它并且已经进行了一些“世界”更改,该怎么办?也许你的get-results例程不会改变数据,所以那很好。但是,这种方法可能无法与更改数据POST一起使用,并且如果单击取消按钮,期望“世界”不会改变。

我希望有帮助:)

答案 4 :(得分:0)

用户不必同步体验这一点。

  1. 客户发布请求
  2. 服务器接收客户端请求并为其分配ID
  3. 服务器&#34;开始&#34;搜索并使用零数据页面和搜索ID进行响应
  4. 客户收到&#34;占位符&#34;页面并开始检查结果是否已准备好基于ID(使用类似轮询或websockets)
  5. 搜索完成后,服务器会在下次轮询时响应结果(或在使用websockets时直接通知客户端)
  6. 当性能不是瓶颈而且处理性质使更长的等待时间可以接受时,这很好。想想常规运行30-90秒的航班搜索聚合器,或报告必须安排并运行更长时间的生成器!

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

答案 5 :(得分:0)

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

释放服务器上的资源意味着什么?

什么构成一个可以释放资源的优雅中止?

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

或者您是否需要从数据库本身取消数据库请求?在这种情况下,ChristianGrün提出的答案更有意义。

还是没有优雅的关闭,你必须强迫一切都死?如果是这样,这看起来非常糟糕。

并非所有客户都会明确中止

有些客户会关闭浏览器,但他们的最后一个请求将无法通过;一些客户将失去互联网连接并使服务暂停,等等。当客户断开连接或离开时,您无法保证获得“中止”请求。

您必须决定是否存在可能不受欢迎的行为,或实施其他有效状态跟踪,例如:客户端ping服务器为keepalive。

旁注

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

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