Эффективный и удобный способ представления результатов медленной загрузки

я прочитал много похожих вопросов Что касается отмены POST-запроса с помощью jQuery, но ни один из них не кажется мне близким.

У меня есть ваша повседневная форма, в которой 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() отправка формы и вместо этого сделать вызов ajax () из jQuery?


Как я уже сказал, я также хочу остановить процесс на стороне сервера, и из того, что я читаю, мне нужно 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");});
});
}

Я сделал еще несколько исследований, и я нашел этот ответ. Он упоминает connection_aborted (), а также session_write_close (), и я не совсем уверен, что для меня полезно. Я использую переменные SESSION, но мне не нужно записывать значения, когда процесс отменяется (хотя я хотел бы сохранить переменные SESSION активными).

Будет ли это путь? И если да, то как заставить одну функцию PHP завершать другую?


Я также прочитал Websockets, и кажется, что это может сработать, но мне не нравятся хлопоты по настройке сервера Websocket, так как для этого потребуется связаться с нашим ИТ-специалистом, который требует обширного тестирования новых пакетов. Я бы предпочел оставить его на PHP и JS, без сторонних библиотек, кроме jQuery.

Учитывая, что большинство комментариев и ответов показывают, что то, что я хочу, невозможно, мне также интересно услышать альтернативы. Первое, что приходит на ум, — это постраничные вызовы Ajax (аналогично многим веб-страницам, которые отображают результаты поиска, изображения и многое другое в бесконечной прокрутке). Пользователю предоставляется страница с X первыми результатами (например, 20), и когда они нажимают кнопку «показать следующие 20 результатов», те, которые отображаются, добавляются. Этот процесс может продолжаться, пока не будут показаны все результаты. Поскольку пользователям полезно получать все результаты, я также предоставлю опцию «загрузить все результаты». Это также займет очень много времени, но, по крайней мере, пользователи должны иметь возможность просмотреть первые результаты на самой странице. (Таким образом, кнопка загрузки не должна нарушать загрузку страниц Ajax.) Это всего лишь идея, но я надеюсь, что она вдохновит некоторых из вас.

18

Решение

Гимел Наг Рана продемонстрировал как отменить ожидающий Ajax-запрос.
Несколько факторов могут мешать и задерживать последующие запросы, как я обсуждал ранее в другой пост.

TL; DR: 1. очень неудобно пытаться обнаружить, что запрос был отменен из самой длительной задачи, и 2. как обходной путь, вы должны закрыть сеанс (session_write_close()) как можно раньше в вашей долгосрочной задаче, чтобы не блокировать последующие запросы.

connection_aborted() не может быть использован. Предполагается, что эта функция вызывается периодически во время длинной задачи (обычно внутри цикла). К сожалению, в вашем случае есть только одна важная атомарная операция: запрос к концу данных.

Если вы применили процедуры, рекомендованные Himel Nag Rana и мной, теперь вы сможете отменить запрос Ajax и немедленно разрешить выполнение новых запросов. Единственная проблема, которая остается, заключается в том, что предыдущий (отмененный) запрос может некоторое время работать в фоновом режиме (не блокируя пользователя, а просто тратя ресурсы на сервере).

Проблема может быть перефразирована так: «Как прервать определенный процесс извне».

Как Кристиан Бонато по праву советовал, вот возможная реализация. Ради демонстрации я буду полагаться на Симфонии Process составная часть, но вы можете разработать более простое решение, если хотите.

Основной подход:

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

Создать процесс и отследить его действительно сложно, поэтому я посоветовал использовать существующие модули. Интеграция только одного компонента Symfony должна быть относительно легко с помощью Composer: первый установить 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);

?>
3

Другие решения

Насколько я понимаю, ключевые моменты:

  • Вы не можете отменить конкретный запрос, если форма отправлена. Причины на стороне клиента, у вас нет ничего, чтобы вы могли определить состояния запроса формы (если он размещен, если он обрабатывается и т. д.). Так что единственный способ отменить это сбросить $_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>

Это пример кода — я просто использовал ваши примеры кода и кое-что изменил.

4

В BaseX 8.4 новая аннотация RESTXQ %rest:single был введен, что позволяет отменить запущенный запрос на стороне сервера: http://docs.basex.org/wiki/RESTXQ#Query_Execution. Это должно решить, по крайней мере, некоторые из проблем, которые вы описали.

Текущий способ вернуть только фрагменты результата — передать индекс по первому и последнему результату в вашем результате и выполнить фильтрацию в XQuery:

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

Возвращая еще один результат, чем запрошено, клиент будет знать, что будет больше результатов. Это может быть полезно, потому что вычисление общего размера результата часто намного дороже, чем возвращение только первых результатов.

2

Надеюсь, я правильно понял.

Вместо того, чтобы позволить браузеру «изначально» отправлять ФОРМУ, не пишите код JS, который делает это вместо этого. Другими словами (я не проверял это; так интерпретируйте как псевдокод):

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

Итак, теперь, когда нажата кнопка «отправить», ничего не произойдет.

Очевидно, что вы хотите, чтобы ваша форма POSTed, поэтому напишите JS, чтобы прикрепить обработчик кликов к этой кнопке отправки, собрать значения из всех полей ввода в форме (на самом деле, это НЕ почти так страшно, как кажется; проверьте ссылку ниже), и отправьте его на сервер, сохраняя при этом ссылку на запрос (проверьте 2-ю ссылку ниже), чтобы вы могли прервать ее (и, возможно, дать сигнал серверу также выйти) при нажатии кнопки отмены (альтернативно, вы можете просто отказаться от него, не заботясь о результатах).

Отправить форму, используя jQuery

Прервать Ajax-запросы, используя jQuery

В качестве альтернативы, чтобы сделать эту HTML-разметку «более понятной» относительно ее функциональности, рассмотрите возможность вообще не использовать тег FORM: в противном случае то, что я предложил, делает его использование запутанным (почему оно есть, если оно не используется; знаете, я имею в виду?). Но не отвлекайтесь на это предложение, пока не сделаете так, как хотите; это необязательно и тема для другого дня (это может даже относиться к вашей изменяющейся архитектуре всего сайта).

ОДНАКО, нужно подумать: что делать, если сообщение формы уже достигло сервера, а сервер уже начал его обрабатывать и некоторые «мировые» изменения уже были внесены? Может быть, ваша процедура получения результатов не меняет данные, так что все в порядке. Но этот подход, вероятно, не может быть использован с POST-данными изменений, ожидая, что «мир» не изменится, если нажать кнопку отмены.

Надеюсь, это поможет 🙂

1

Пользователь не должен испытывать это синхронно.

  1. Клиент отправляет запрос
  2. Сервер получает запрос клиента и присваивает ему идентификатор
  3. Сервер «запускает» поиск и отвечает страницей с нулевыми данными и идентификатором поиска
  4. Клиент получает страницу «заполнителя» и начинает проверять, готовы ли результаты на основе идентификатора (с помощью чего-то вроде опроса или веб-сокетов)
  5. После того как поиск завершен, сервер отвечает с результатами в следующий раз, когда он опрашивается (или уведомляет клиента непосредственно при использовании веб-сокетов)

Это хорошо, когда производительность не является узким местом, а характер обработки делает приемлемым более длительное время ожидания. Подумайте, агрегаторы поиска рейсов, которые обычно работают в течение 30-90 секунд, или отчеты генераторов, которые должны быть запланированы и работать еще дольше!

Вы можете сделать этот процесс менее расстраивающим, если не блокируете пользовательские взаимодействия, держите их в курсе хода поиска и начинайте показывать результаты по мере их поступления, если это возможно.

0

Вы должны решить это концептуально, прежде чем писать какой-либо код. Вот некоторые вещи, которые приходят на ум случайно:

Что значит освободить ресурсы на сервере?

Что представляет собой изящный аборт, который высвободит ресурсы?

Достаточно ли убить процесс PHP, ожидающий результатов запроса? Если это так, маршрут, предложенный RandomSeed, может быть интересным. Просто помните, что он будет работать только на одном сервере. Если у вас несколько серверов с балансировкой нагрузки, у вас не будет способа убить процесс на другом сервере (по крайней мере, не так легко).

Или вам нужно отменить запрос базы данных от самой базы данных? В этом случае ответ, предложенный Кристианом Грюном, представляет больший интерес.

Или это из-за того, что нет грациозного отключения, и вы должны заставить все умереть? Если так, то это кажется ужасно хакерским.

Не все клиенты собираются явно прервать

Некоторые клиенты собираются закрыть браузер, но их последний запрос не будет выполнен; некоторые клиенты теряют подключение к Интернету и оставляют сервис зависшим, и т. д. Вы не гарантированно получите запрос «прервать», когда клиент отключится или ушел.

Вы должны решить, следует ли жить с потенциально нежелательным поведением или внедрить дополнительное отслеживание активного состояния, например, пинг-сервер клиента для keepalive.

Примечания стороны

  • 30 секунд или больше время запроса потенциально долго, есть ли лучший инструмент для работы; так что вам не придется решать это с помощью такого хака?

  • вы ищете функции параллельной системы, но не используете параллельную систему; если вы хотите параллелизма, используйте для него лучший инструмент / среду, например, Erlang.

0