Реализация пошаговой работы PHP-скрипта с помощью AJAX

в 11:52, , рубрики: ajax, javascript, php, метки: , ,

Искал более-менее простое и универсальное средство для организации пошаговой работы скрипта, но так ничего и не нашел. Даже вопрос в QA задал, везде только общие фразы. Поэтому решил сам сделать такой инструмент.

Для чего это вообще нужно?

Бывает необходимо обработать скриптом какой-то очень уж большой файл, например, для импорта. Естественно, время работы скрипта увеличивается пропорционально размеру файла или количеству строк в нем.

Хотелось бы разбить обработку файла на несколько частей и запускать скрипт в работу уже по частям.

image

Принцип реализации давно известен — обмен данными между сервером и клиентом:
Клиент запускает скрипт, тот выполняет несколько итераций и возвращает клиенту номер строки, на которой он остановился. После этого клиент делает новый запрос, в котором передает скрипту этот номер и скрипт продолжает работу дальше.

Собственно сам код

Для интересующихся, демо.

Для работы нам понадобятся:

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'];

// Проверяем, все ли строки обработаны
if ($offset >= $count) {$sucsess = 1;} else {$sucsess = round($offset / $count, 2); $offset = $offset + $step;}

// И возвращаем клиенту данные (номер итерации и сообщение об окончании работы скрипта)
$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).

Если у сообщества есть примеры реализации подобного функционала или вообще готовые решения для пошаговой работы со скриптами, буду благодарен ссылкам в комментариях.

Автор: ilyautkin

  1. Владимир:

    нерабочая ссылка

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js