Выполнение очереди заданий через череду ajax запросов

от автора

Когда сталкивался с долгоиграющими скриптами (часто — это длинная очередь заданий), приходилось ждать, пока браузер думает над скриптом. Это казалось мне не очень красивым решением, хотелось видеть процесс в режиме реального времени, без перезагрузки страницы и еще где-то вести учет по каждому заданию.
Недавно попалось сразу 2 такие задачи. Одна из них — это добавление в ссылочную биржу через api кучу текстов ссылок. При этом, ожидание от сервера может быть довольно долгим, а то и вовсе закончится неудачно.
И вчера (точнее уже позавчера – примечание автора-редактора) решил написать заготовку такого скрипта, с комментариями, специально чтоб тут написать. Надеюсь, будет кому-то полезно.
Старался сделать как можно нагляднее и проще для понимания.

И так, вкратце, принцип работы…

Берется некая очередь заданий, кладется в базу SQLite (если кто не знает – это встроенная в php, упрощенная БД, которая создается тут же в текстовом файле)
Если запустить обработку, то задания из очереди (из базы) будут выполняться поочередно, по каждому будет выполнен ajax запрос и ответ в режиме реального времени выведется на экран. Если задание с ошибкой, это отметиться в БД (закачайте на сервер, посмотрите, скрипт рабочий).

В index.html есть несколько ссылок управления и область для отчетов #console.
В атрибутах data-action ссылок (кроме ScreenClear) хранится имя метода, который вызывается из класса stepWise (файл stepWise.class.php).
Всего 4 файла:
index.html — интерфейс управления
script.js — javascript/jquery скрипт
ajax.php — прокладка для ajax запросов
stepWise.class.php — серверный функционал
На github: github.com/adminoid/ajaxStepwiseBlank
И здесь:
index.html

index.html

<!DOCTYPE HTML> <html lang="en-US"> <head> 	<meta charset="UTF-8"> 	<title></title>     <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>     <script type="text/javascript" src="script.js"></script> </head> <body>  <!-- Принцип этой "системы", грубо говоря, следующий: script.js берет значение из атрибута data-action ссылки и вызывает одноименный метод из файла /class/stepWise.class.php, через прокладку /ajax/ajax.php -->  <!-- Работа с базой данных --> <ul class="dbActions">     <li><a class="db" data-action="DbRecreateWithQueue" href="#">Пересоздать базу c очередью задач</a></li>     <li><a class="db" data-action="DbShowData" href="#">Посмотреть какие данные лежат в БД</a></li> </ul>  <!-- Работа c экраном вывода --> <ul class="screenActions">     <li><a class="screen" data-action="ScreenClear" href="#">Очистить экран</a></li> </ul>  <!-- Работа с очередью задач --> <ul class="processActions">     <li><a class="process" data-action="ProcessQueue" href="#">Запустить исполнение очереди задач</a></li> </ul>  <!-- Область для вывода информации --> <div id="console"></div>  <!-- Чтобы ошибки были "красными", а сообщения - "зелеными" --> <style type="text/css">     .error{color: #730000;}     .success{color: #007300;} </style>  </body> </html> 

script.js

script.js

var globals = {     // Путь от корня сайта к ajax.php     ajaxPath: 'ajax.php',      // Метод для обработки очереди     process: function(action){         $.post(this.ajaxPath,{             action: action         })             .success(function(xhr) {                 var response = $.parseJSON(xhr);                  $('#console')                     .append("<span class='"+response.status+"'>"+response.message+"</span>");                  // Если статус не complete, то рекурсивно повторить                 if(response.status !== 'complete'){                     globals.process(action);                 }             });     } }  $(function(){     // При клике на любую ссылку с css классом "db"     $(".db").click(function(e){         e.preventDefault();         // Берем значение data-action, чтобы вызвать одноименный метод через ajax запрос         var action = $(e.target).data('action');         // отправляем ajax запрос на урл         $.post(globals.ajaxPath,{             action: action         })             .success(function(xhr) {                 var response = $.parseJSON(xhr);                 if(response.type == 'message'){                     $('#console')                         .empty()                         .html("<span class='"+response.status+"'>"+response.message+"</span>");                 }             });      });      // Ссылки с классом .screen используются не для ajax, а для каких-то действий на странице     $(".screen").click(function(e){         e.preventDefault();         // Берем значение ее data-action, чтобы понять что будем делать         var action = $(e.target).data('action');         switch(action){             case 'ScreenClear':                 $('#console').empty();                 break;         }     });      // Ссылки с классом .process используются для обработки очереди заданий     $(".process").click(function(e){         e.preventDefault();         // Берем значение data-action, чтобы вызвать одноименный метод через ajax запрос         var action = $(e.target).data('action');         $('#console').empty();         globals.process(action);     }); });

ajax.php

ajax.php

<?php /**  * User: Petja  * Date: 31.03.13  * Time: 14:15  */  header("Cache-Control: no-store, no-cache, must-revalidate");  // Пропустить только ajax запрос if(empty($_SERVER['HTTP_X_REQUESTED_WITH']) or ($_SERVER['HTTP_X_REQUESTED_WITH']) != 'XMLHttpRequest'){     die('Это не ajax запрос!'); }  // Пропустить только допустимые action: $availableActions = array('DbRecreateWithQueue', 'DbShowData', 'DbDeleteAll', 'ProcessQueue'); if(!in_array($action = $_POST['action'], $availableActions)){     die('Нет такого действия!'); }  // Подключить функционал include "stepWise.class.php"; $sw = new stepWise;  // Запустить действие echo json_encode($sw->$action());

stepWise.class.php

stepWise.class.php

<?php /**  * User: Petja  * Date: 31.03.13  * Time: 12:50  */  class stepWise {      public function DbRecreateWithQueue(){          // Берет очередь задач. Например, список картинок к обработке или список ссылок, чтобы добавить их в SAPE. У меня здесь просто массив.         $tasks = array(             'Задача 1',             'Задача 2',             'Задача 3',             'Задача 4',             'Задача 5',             'Задача 6',             'Задача 7',             'Задача 8',             'Задача 9',             'Задача 10'         );          try {             // Открыть БД SQLite или создать, если ее нет             $db = new PDO('sqlite:temp.db');             $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);              // Очищает таблицу             $db->exec(                 "DROP TABLE IF EXISTS tempTableForTasks;"             );              // Создает таблицу для очереди             $db->exec(                 "CREATE TABLE IF NOT EXISTS tempTableForTasks(                     id INTEGER PRIMARY KEY,                     tasks TEXT,                     status TEXT,                     optional TEXT                 );"             );              // Помещает очередь в БД             foreach($tasks as $task){                 static $n = 1;                 $db->exec("INSERT INTO tempTableForTasks(tasks, status, optional) VALUES ('".$task."', '', 'Запись № ".$n."');");                 $n++;             }              // Закрываем подключение к БД             $db = null;              // Возвращаем данные в случае успеха             return array(                 'status' => 'success',                 'type' => 'message',                 'method' => __METHOD__,                 'message' => 'БД успешно создана!'             );         }         catch(PDOException $e) {             // Возвращаем данные в случае ошибки             return array(                 'status' => 'error',                 'type' => 'message',                 'method' => __METHOD__,                 'message' => $e->getMessage()             );         }     }      public function DbShowData(){         try {             // Открыть БД SQLite или создать, если ее нет             $db = new PDO('sqlite:temp.db');             $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);              // Взять данные из таблицы             $result = $db->query('SELECT * FROM tempTableForTasks');             $result_all = $result->fetchall(PDO::FETCH_ASSOC);              // Формируются заголовки таблицы из ключей             $output = '<table><tr>';             foreach(array_keys($result_all[0]) as $key){                 $output .= "<th>".$key."</th>";             }             $output .= '</tr>';              // Формируется сама таблица             foreach($result_all as $tr){                 $output .= '<tr>';                 foreach($tr as $td){                     $output .= '<td>'.$td.'</td>';                 }                 $output .= '</tr>';             }             $output .= '</table>';              // Закрываем подключение к БД             $db = null;              // Возвращаем данные в случае успеха             return array(                 'status' => 'success',                 'type' => 'message',                 'method' => __METHOD__,                 'message' => $output             );         }         catch(PDOException $e) {             // Возвращаем данные в случае ошибки             return array(                 'status' => 'error',                 'type' => 'message',                 'method' => __METHOD__,                 'message' => $e->getMessage()             );         }     }      /*      * Взять из БД один не обработанный элемент      * Обработать его      * Вернуть успех или ошибку      * Пометить в БД этот элемент как обработанный      * */     public function ProcessQueue(){          try {             // Открыть БД SQLite или создать, если ее нет             $db = new PDO('sqlite:temp.db');             $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);              // Взять одну не обработанную строку из таблицы             $result = $db->query('SELECT * FROM tempTableForTasks WHERE status != "processed"');             $notProcessedColumn = $result->fetch(PDO::FETCH_ASSOC);             // Если необработанных строк не осталось, возвращаем status complete             if(!$notProcessedColumn){                 return array(                     'status' => 'complete',                     'type' => 'queue',                     'method' => __METHOD__,                     'message' => 'Конец'                 );             }              // Отправить задание на обработку             $status = $this->OneAction();             if($status == 'success'){                 $db->exec('UPDATE tempTableForTasks SET status="processed", optional="обработано" WHERE id = "'.$notProcessedColumn['id'].'"');             }elseif($status == 'error'){                 $db->exec('UPDATE tempTableForTasks SET status="processed", optional="ошибка" WHERE id = "'.$notProcessedColumn['id'].'"');             }              // Формируем строку ответа - получившаяся строка в БД после обработки             $result = $db->query('SELECT * FROM tempTableForTasks WHERE id = "'.$notProcessedColumn['id'].'"');             $processedColumn = $result->fetch(PDO::FETCH_ASSOC);              // Закрываем подключение к БД             $db = null;              // Возвращаем данные в случае успеха             return array(                 'status' => $status,                 'type' => 'queue',                 'method' => __METHOD__,                 'message' => implode(", ", $processedColumn)."<br>\n"             );         }         catch(PDOException $e) {             // Возвращаем данные в случае ошибки             return array(                 'status' => 'error',                 'type' => 'message',                 'method' => __METHOD__,                 'message' => $e->getMessage()             );         }     }      /*      * Это демонстрационная функция, выполняется с задержкой в 1 секунду      * и возвращает ошибку с вероятность 30%      * */     private function OneAction(){         // Задержка на секунду для видимости обработки         sleep(1);         // С вероятностью 30% возвращается ошибка         if (rand(1, 100) <= 30){             return 'error';         }         return 'success';     } } 

ссылка на оригинал статьи http://habrahabr.ru/post/175009/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *