Реализация пошаговой работы PHP-скрипта с помощью AJAX
Для чего это вообще нужно?
Бывает необходимо обработать скриптом какой-то очень уж большой файл, например, для импорта. Естественно, время работы скрипта увеличивается пропорционально размеру файла или количеству строк в нем.
Хотелось бы разбить обработку файла на несколько частей и запускать скрипт в работу уже по частям.
Принцип реализации давно известен — обмен данными между сервером и клиентом:
Клиент запускает скрипт, тот выполняет несколько итераций и возвращает клиенту номер строки, на которой он остановился. После этого клиент делает новый запрос, в котором передает скрипту этот номер и скрипт продолжает работу дальше.
Собственно сам код
Для работы нам понадобятся:
index.html
<html> <head> <title>ScriptOffset - инструмент для организации пошаговой работы скрипта</title> <script type="text/javascript" src="http://yandex.st/jquery/1.7.1/jquery.min.js"></script> <script type="text/javascript" src="/scriptoffset.js"></script> <link rel="stylesheet" type="text/css" href="/scriptoffset.css"> </head> <body> <div class="form"> <input id="url" name="url"> <input id="offset" name="offset" type="hidden"> <div class="progress" style="display: none;"> <div class="bar" style="width: 0%;"></div> </div> <a href="#" id="runScript" class="btn" data-action="run">Старт</a> <a href="#" id="refreshScript" class="btn" style="display: none;">Заново</a> </div> </body> </html>
scriptoffset.php
<?php // Отвечаем только на Ajax if ($_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') {return;} // Можно передавать в скрипт разный action и в соответствии с ним выполнять разные действия. $action = $_POST['action']; if (empty($action)) {return;} $count = 50; $step = 1; // Получаем от клиента номер итерации $url = $_POST['url']; if (empty($url)) return; $offset = $_POST['offset']; // Проверяем, все ли строки обработаны $offset = $offset + $step; if ($offset >= $count) { $sucsess = 1; } else { $sucsess = round($offset / $count, 2); } // И возвращаем клиенту данные (номер итерации и сообщение об окончании работы скрипта) $output = Array('offset' => $offset, 'sucsess' => $sucsess); echo json_encode($output);
scriptoffset.js
function setCookie (url, offset){ var ws=new Date(); if (!offset && !url) { ws.setMinutes(10-ws.getMinutes()); } else { ws.setMinutes(10+ws.getMinutes()); } document.cookie="scriptOffsetUrl="+url+";expires="+ws.toGMTString(); document.cookie="scriptOffsetOffset="+offset+";expires="+ws.toGMTString(); } function getCookie(name) { var cookie = " " + document.cookie; var search = " " + name + "="; var setStr = null; var offset = 0; var end = 0; if (cookie.length > 0) { offset = cookie.indexOf(search); if (offset != -1) { offset += search.length; end = cookie.indexOf(";", offset) if (end == -1) { end = cookie.length; } setStr = unescape(cookie.substring(offset, end)); } } return(setStr); } function showProcess (url, sucsess, offset, action) { $('#url, #refreshScript').hide(); $('.progress').show(); $('#runScript').text('Стоп!'); $('.bar').text(url); $('.bar').css('width', sucsess * 100 + '%'); setCookie(url, offset); $('#runScript').click(function(){ document.location.href=document.location.href }); scriptOffset(url, offset, action); } function scriptOffset (url, offset, action) { $.ajax({ url: "http://bfmn.ru/scriptoffset/scriptoffset.php", type: "POST", data: { "action":action , "url":url , "offset":offset }, success: function(data){ data = $.parseJSON(data); if(data.sucsess < 1) { showProcess(url, data.sucsess, data.offset, action); } else { setCookie(); $('.bar').css('width','100%'); $('.bar').text('OK'); $('#runScript').text('Еще'); } } }); } $(document).ready(function() { var url = getCookie("scriptOffsetUrl"); var offset = getCookie("scriptOffsetOffset"); if (url && url != 'undefined') { $('#refreshScript').show(); $('#runScript').text('Продолжить'); $('#url').val(url); $('#offset').val(offset); } $('#runScript').click(function() { var action = $('#runScript').data('action'); var offset = $('#offset').val(); var url = $('#url').val(); if ($('#url').val() != getCookie("scriptOffsetUrl")) { setCookie(); scriptOffset(url, 0, action); } else { scriptOffset(url, offset, action); } return false; }); $('#refreshScript').click(function() { var action = $('#runScript').data('action'); var url = $('#url').val(); setCookie(); scriptOffset(url, 0, action); return false; }); });
scriptoffset.css
input { font-size: 13px; margin: 0; padding: 0 3px; vertical-align: middle; border: 1px solid #CCCCCC; border-radius: 3px 3px 3px 3px; color: #808080; display: inline-block; font-size: 13px; height: 26px; line-height: 18px; width: 243px; -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) inset; } .btn { font-size: 13px; padding: 5px 8px; background-color: #0064CD; background-image: -moz-linear-gradient(center top , #049CDB, #0064CD); background-repeat: repeat-x; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); color: #FFFFFF; display: inline-block; vertical-align: middle; border-radius: 3px 3px 3px 3px; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); text-decoration: none; } .btn:hover { background-position: 0 -15px; } .btn:active { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset, 0 1px 2px rgba(0, 0, 0, 0.05); } .progress { font-size: 13px; margin: 0; vertical-align: middle; background-color: #F7F7F7; background-image: -moz-linear-gradient(center top , #F5F5F5, #F9F9F9); background-repeat: repeat-x; border-radius: 4px 4px 4px 4px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; height: 28px; width: 250px; overflow: hidden; display: inline-block; } .progress .bar { background-color: #0E90D2; background-image: -moz-linear-gradient(center top , #149BDF, #0480BE); background-size: 40px 40px; -moz-box-sizing: border-box; -moz-transition: width 0.6s ease 0s; background-repeat: repeat-x; box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15) inset; color: #FFFFFF; float: left; font-size: 12px; height: 100%; text-align: left; padding: 5px 8px; font-size: 13px; text-shadow: 1px 1px #333; white-space: nowrap; } div.form { margin: 150px auto 0; width: 500px; }
Для оформления css взял несколько правил из Bootstrap.
Что в итоге
В поле url мы указываем, например, ссылку на файл, который нужно обработать, и запускаем скрипт. Появляется прогресс-бар, а мы сидим и ждем, когда он доползет до 100 %, чтобы увидеть результат работы.
При работе с этим решением:
- Мы можем установить количество обрабатываемых строк за одну итерацию (в самом скрипте);
- Пользователю показывается настоящий прогресс-бар, а не бесконечная «крутилка» — если прогресс-бар стоит на середине, значит обработана половина файла;
- Пользователь может остановить выполнение скрипта. В этом случае offset записывается в cookies на 10 мин, чтобы он мог продолжить работу скрипта с того же места.
- Если пользователь обновит страницу, ему будет предложено продолжить работу скрипта с места остановки или начать заново (так же благодаря cookies).
Если у сообщества есть примеры реализации подобного функционала или вообще готовые решения для пошаговой работы со скриптами, буду благодарен ссылкам в комментариях.
Php+Ajax полоса загрузки ProgressBar
ProgressBar - это индикатор, который показывает скорость и процент выполнения любого процесса. ProgressBar также называют полосой загрузки, или индикатором загрузки. Обычно прогресс бар используют для отображения процесса скачивания или закачивания файлов, но существуют и другие способы применения. Почему то в интернете все ссылки связанные использованием ProgressBar на php, сводятся к загрузке файлов, неужели людям не нужно отслеживать процесс выполнения других задач? Например, таких как процент считывания XML файла, или процесс заполнения SQL таблицы данными. Немало важным индикатор отслеживания является и при работе с удаленным сервером.
Недавно мне пришлось разработать грабер для одной фирмы. Грабер должен был: получить с сайта конкурентов, каталог продукции, сохранив структуру вложенности разделов. Так вот при частом обращении с чужому серверу, с целью получения кода страницы, для дальнейшего парсинга, уходит много времени, что влечет за собой появление ошибки выполнения сценария:
Fatal error: Maximum execution time of 30 seconds exceeded in …
Для того чтобы избежать данной оказии, нужно разбить наш процесс на части, таким образом чтобы каждая из частей при выполнении укладывалась в 30 секундный интервал. В этот момент как раз таки и неплохо знать, сколько же процентов выполнилось, и сколько еще осталось ждать.
Пишем PHP скрипт для полосы загрузки с использованием AJAX
Представим, что у нас есть объемный алгоритм, который обрабатывает информацию по частям, и назовем его условно «сложной задачей». Чтобы было понятно, представьте что информацией является разбитый на части файл, который нужно скачать с сервера.
Создавать ProgressBar мы будем последовательно, выполнив для этого 3 шага.
AJAX обработчик (Шаг 1)
Сразу скажу, что для корректной работы AJAX технологии нам понадобится, уже готовый скрипт: ajax.js, в задачи которого входит отправка запросов на сервер и ожидание ответа. На самом деле он не большой, и можно было бы описать его более подробно, но боюсь, что это сделает статью слишком длинной. О работе с AJAX я напишу подробнее в другой статье, а пока просто выложу код упомянутого выше скрипта:
function XmlHttp() { var xmlhttp; try{xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");} catch(e) { try {xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");} catch (E) {xmlhttp = false;} } if (!xmlhttp && typeof XMLHttpRequest!='undefined') { xmlhttp = new XMLHttpRequest(); } return xmlhttp; } function ajax(param) { if (window.XMLHttpRequest) req = new XmlHttp(); send=""; for (var i in param.data) send+= i+"="+param.data[i]+"&"; req.open("POST", param.url, true); req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); req.send(send); req.onreadystatechange = function() { if (req.readyState == 4 && req.status == 200) //если ответ положительный { if(param.success)param.success(req.responseText); } } }
И так, скачайте данный скрипт, и положите его в корневую папку вашего сайта, либо в ручную создайте пустой файл ajax.js и вставьте в него приведенный выше код.
Пишем скрипт PHP выполняющий «сложную задачу» (Шаг 2)
Вторым действием создадим скрипт index.php , который будет отвечать за вывод полосы загрузки на страницу, а также будет выполнять расчет «сложной задачи». В корневой папке сайта создайте пустой файл index.phр. Откройте его и вставьте следующий код:
<? session_start();//открываем сессию для записи if($_POST["difficult_task"]){ $part=231; //общее количество задач $_SESSION['sucsess_part']++; //количество выполненных подзадач echo floor(($_SESSION['sucsess_part']*100)/$part);//процент выполненния общей задачи } ?> else{?> … <?}?>
Эта часть кода описывает процесс выполнения «сложной задачи», если интерпретировать ее на задачу скачивания файла, то переменная $part должна содержать количество частей разбитого файла. Счетчик уже скачанных частей должен храниться в сессии на сервере, поэтому сразу открываем ее для записи функцией session_start(), для того чтобы в дальнейшем увеличивать $_SESSION['sucsess_part'] на один, каждый раз при успешной закачке одной из частей файла. Функция floor() округляет выполненные проценты скачивания до целого числа в меньшую сторону.
Вот и весь скрипт расчета сложной задачи, конечно на практике данный скрипт разрастётся сотнями строк, но для общего примера хватит и данного кода. В принципе правильнее было бы выделить этот скрипт отдельно и обращаться к нему с html странички с помощью ajax.js, но т.к. он слишком мал, то будет понятнее включить обработчик «сложной задачи» непосредственно в код страницы.
Организация процесса обновления полосы загрузки и вывод её на страницу (3 шаг)
Код вывода HTML страницы и progressbar будет заключен между скобками else из предыдущего шага:
else{?> … <?}?>
Скопируйте следующий кусок кода и вставьте его вместо многоточия.
<html> <head> <title>Сложная задача</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <script type="text/javascript" src="/ajax.js"></script> <script> function ProgressBar(persent){ if(persent<100){ //если задача не достигла 100% готовности, отправляем запрос на ее выполнение ajax({ url:"http://<?=$_SERVER['HTTP_HOST']."/progressbar/index.php";?>", //путь к скрипту, который обрабатывает задачу data: //данные передаваемые в POST запросе { difficult_task:"difficult_task", }, success:function(data){ //функция обратного вызова, выполняется в случае успехной отработки скрипта document.getElementById("text").innerHTML="<br/>Завершено <b>"+data+"%</b>"; // выводим в информационный блок количество выполненных % document.getElementById("bar").style.width=data+"%"; //растягиваем полосу загрузки document.getElementById("persent").innerHTML=data+"%"; // изменяем счетчик процентов, на полосе загрузки ProgressBar(parseInt(data));// рекурсивно вызываем этуже функцию, она будет выполняться пока не выполнит 100% } }) } else{//если задача выполненна на 100%, то выводим информацию об этом. document.getElementById("status").innerHTML=""; document.getElementById("text").innerHTML="<br/> Задача успешно выполнена!<br/>Для повторного запуска нужно сбросить сессию. (Перезапустите браузер)"; document.getElementById("bar").style.width="0%"; document.getElementById("load").style.display="none"; document.getElementById("bar").style.display="none"; document.getElementById("btn").style.display="none"; } } </script> </head> <body> <form method="post" action=""> <div id="status"></div> <div id="load" style="text-align: center; color: white; display: block; height: 20px; width: 200px; background:blue; border: solid 1px black;"> <div id="bar" style="display: block; height: 20px; width: 0%; background:green;" ><div id="persent" style="position: relative; float:left; width:200px;">0%</div></div> </div> <br/> <div id="text"></div> <br/> <input id="btn" type='button' value='Выполнить сложную задачу' onclick='ProgressBar(0);'> <br/> </form> </body> </html>
Данный код выводит на страницу кнопку с названием ‘Выполнить сложную задачу’, при нажатии на которую запускается процесс обработки сложной задачи с использованием ajax запросов. В коде присутствуют комментарии, поэтому не стану их дублировать, просто советую внимательно посмотреть что там написанно.
Если вы все сделали правильно, то у вас в корне сайта должны лежать два файла: index.php и ajax.js, в противном случаем вы можете скачать их одним архивом.
Запустите index.php вы увидите кнопку ‘Выполнить сложную задачу’. Нажмите на нее, и синяя полоса загрузки начнет заполняться зеленым цветом.
При достижении 100% вы увидите сообщение о успешном выполнении «сложной задачи».
Помните о том, что счетчик выполнения хранится в сессии на стороне сервера, поэтому для того, чтобы вновь посмотреть работу скрипта вам нужно будет перезапустить браузер, либо дописать скрипт самостоятельно таким образом, чтобы сессия сбрасывалась при достижении 100% результата.
Сегодня в статье с ужасным названием php+ajax полоса загрузки progressbar
мы рассмотрели, очень полезный на мой взгляд, механизм отслеживания выполнения задачи в процентах с выводом графического индикатора. Полоса загрузки, или попросту progressbar, будет полезен при синхронизации данных, при скачивании или закачивании файлов, а также при удаленной работе с чужим сайтом.