JS-PHP MVC интерфейс — cобираем всё вместе

    Для начала я хочу поблагодарить m007, который подал идею, как можно упростить Programmer-friendly интерфейс взаимодействия Клиент(JS)-Сервер(PHP), что и поспособствовало написанию этой статьи.

    Данная статья является заключительной, в которой я объединю JS-Шаблоны, PHP Model-controller, и добавлю Динамическое создание UI методов на JS (некоторые идеи которого были взяты из статьи m007). Тем самым мы получим на выходе неплохой и удобный фундамент для создания своих AJAX приложений.

    Динамическое создание методов на JavaScript

    Для начала хочу обратить ваше внимание, что все наши методы лежат в PHP-библиотеке. Каждый метод можно вызвать, передав определенные параметры в стиле JSON. И чтобы вызвать PHP-метод из JS, я предлагал в прошлой статье использовать функцию server_exec. Но можно это сделать более удобно и
    информативно для программиста.
    m007 предложил вызывать функции прямо из JS, то есть вместо server_exec(«database_func(...);»,function(){...}) использовать в JS database.func(...).

    В PHP, для не объявленных функций, есть метод __call. В JS такого метода нет, но можно предварительно подгрузить методы из PHP-библиотеки и динамически добавить их в JS-класс.

    Для этого в PHP-класс UserInterface (см. прошлую статью) был добавлен метод, который при ‘пустом’ запросе выводил все зарегистрированные методы.
    На Javascript написан класс PHPUIclass, который является интерфейсом для UserInterface. В этом классе описан метод обработки `пустого` запроса (спасибо DmitryBaranovskiy за приведение кода в порядок):
    this.add=function(res){
          var methods=eval('{' + res.responseText + '}');
          for (var index in methods)
            that[methods[index]] = (function (name) {
              return function () {
                request.push('"' + name + '":' + that.methods(Array.prototype.slice.call(arguments)));
                if (!timer)
                  timer=window.setTimeout(function(){
                    ajax_load(url, «method={»+that.getrequest(request)+"}", that.ajax_run, true);
                  },0);
              };
            })(methods[index]);
        }
    * This source code was highlighted with <font size='1'
    color='gray'>Source Code Highlighter
    .
    Эта функция вызывается, когда PHP-библиотека возвращает названия зарегистрированных методов UI (ответ на ‘пустой’ запрос). Исходя из этих данных, этот метод создаёт внутри своего класса новые методы, которые используют для вызова public-методов PHP-библиотеки.

    При запуске метода, происходит кодирование входящих параметров (request.push('"'+methods[index]+'":'+that.methods(Array.prototype.slice.call(arguments)))) и установка нулевого таймера на функцию для отсылки запроса. Думаю, что некоторых читателей этот момент удивил. По этому я остановлюсь на нём поподробнее.

    Javascript не имеет потоков и весь код выполняет последовательно. Состояния у него всего два – выполнение кода и ожидание прерывания. Прерывания могут прийти, например, от таймера (по таймауту), от AJAX запроса (при смене статуса) и т д. При этом запускается состояния выполнение кода, который не прерывается, пока не дойдёт до конца.
    Именно для этого я использую нулевой таймер, который запустит выполнение функции отсылающей запрос на сервер, после окончания выполнения основного кода. Сделано это для того, чтобы отсылать всё одним инкрементированным запросом, состоящим из всех вызываеммых функций и обрабатывать результаты последовательно.

    Теперь можно перейти к обработке результатов запроса. Так как обработчик будет вызван не сразу – то это будет функция или несколько функций, которые необходимо вызвать, когда придет ответ.
    Этим занимаются методы run и ajax_run. Первый добавляет функции на запуск после получения ответа. Функции должны иметь вид function(data). А функция ajax_run занимается их запуском.
    this.run=function(eee){
      func.push(eee);
    }

    this.ajax_run=function(req){
      var ret=eval('('+req.responseText+')');
      for (var i in func)
       if (func[i])
         func[i](ret);
      request=[];
      func=[];
      timer=false;
    }
    * This source code was highlighted with <font size='1'
    color='gray'>Source Code Highlighter
    .
    В результате мы получаем модифицируйщийся класс, в зависимости от установленных методов в PHP. И взаимодействие с ним выглядит очень удобно:
    //Инициализация класса удаленной работы с файлами.
    /* Синхронный запрос на вызов FileManager.php.
    Получение методов dir,save,read.
    Динамическое создание функций. */
    var File=new PHPUIclass('FileManager.php');

    //Вызов удалённых функций
    /* Установка таймера и формирования запроса */
    File.save(«edited.txt»,«Отредактированная часть файла»);
    File.read(«save.log»);

    File.dir();

    //Установка функций для получения результатов запроса.
    /* Добавление функции для обработки запроса read */
    File.run(show_file);

    /* Добавление функции для обработки запроса dir */
    File.run(show_dir);

    //Конец выполнения скрипта, а значит выполнение POST-запроса по Timeout(0) и возвращение результатов в show_file и show_dir
    * This source code was highlighted with Source Code Highlighter.
    Ещё один пример, как воспользоваться результатом функции для запуска другой:
    function show_file(file){
      //…
    }

    function show_error(error){
      // ...
    }

    function check_file(result){
      if (result.ERROR && result.ERROR.checkfile)
        show_error(result.ERROR.checkfile);
      else{
        // Всё прошло нормально?
        // Тогда ещё один запрос, уже на чтение.
        File.read(result[«filename»]);
        File.run(show_file);
      }
    }

    //Вызов удалённых функций
    File.checkfile(«edited.txt»);

    File.run(check_file);
    * This source code was highlighted with Source Code Highlighter.
    Но в этом случае будет происходить два запроса. Этого избежать можно несколькими способами. Один из способов — это реализовать вызов необходимой функции с возвратом пряма в PHP.
    function checkfile($filename){
      global $UImethod;
      if (file_exists($filename))
       return $UImethod->read($filename);
      else
       return array(«ERROR» => array(«checkfile»=>$filename));
    }
    Теряется модульность, но в данной реализации Javascript библиотеки, пока это единственный выход. Возможно в будующем я усовершенствую её, при необходимости.

    Пример

    А вот — простой пример. File-viewer с возможностью редактировать файлы.

    PHP:
    UI.php основной класс UserInterface — подключается в библиотеку.
    mod.fileview.php библиотека для чтения-записи файлов — функции read и save.
    mod.dirview.php библиотека для чтения дириктории — функция dir.
    mod.backup.php библиотека для формирования save.log — функции read,save,dir.
    FileManager.php основной файл, который включает в себя все модули и запускает обработчик запроса.

    Javascript:
    PHPUI.js основной класс, который занимается подключением к интерфейсу UserInterface в php.
    skin.js — класс работы с шаблонами.
    index.html — загрузка шаблонов, вывод файлов.

    HTML:
    manager.skin — Шаблоны
    manager.css — CSS
    rus.xml — русский язык.

    Рабочий пример

    Послесловие

    Плюсы:
    — Удобство программирования и создания новых методов для JS-интерфейса
    — Ясность и прозрачность кода
    — Универсальность методов
    — Возможен вызов функций с неопределенным количеством параметров
    Минусы:
    — Нет проверки на параметры методов. Возможны простейшие ошибки при создании кода. Но для этого можно в PHP-функциях проверять пришедшие параметры (например для DEBUG-MODE). В случае не верных параметров, выводить alert(«ERROR»).

    p.s. кстати, модель этого проекта является MVC.
    Поделиться публикацией

    Комментарии 53

      +2
      Зачем же столько eval в коде? Да ещё и глобальные переменные. Надо бы вам подучить матчасть. :)
        +1
        По поводу обработки JSON, сама матчасть говорит о том, чтобы использовать eval(req.responseText).

        По поводу функции this.method[index]=eval('...') тут увы ничего не поделаешь. Так как принимаются параметры со стороны, то фокусы типа function(met){...}(method[index]) не пройдут.

        А к чему вопрос. Вы беспокоитесь за безопастность вашего клиента? Боитесь, что не те данные с сервера прийдут? :)
          0
          Это какая матчасть так говорит? Создатель JSON так не говорит к примеру. Поподробнее про фокусы и почему они не пройдут? Я, честно говоря, не понял.
          Тот же создатель JSON говорит, что использование eval — это плохой стиль программирования. Всегда есть возможность обойтись без него.
            0
            Как результат, в этой самой строке с eval у вас ошибка на сайте и я вижу только чёрный экран. «Вот тебе и первая выгода…»
              0
              Спасибо, поправил. Не пользуйтесь IE :).
              eval стал теперь больше - зато теперь нормально работает.
                0
                Я не пользовался IE. Не работало в Safari и Firefox.
                  0
                  Ну объясните же мне, зачем там eval?
                    0
                    Я пользуюсь там eval, чтобы сгенерить функцию.
                    Как я уже говорил, штуки типа that[q]=function(a){...}(b) не пройдут, потому что в этой функции идёт работа с параметрами. Других выходов, как значение b (в примере method[index]) сделать там константным и разнообразным для различных q (в примере method[index]), я не представляю. Если у вас есть идеи - выслушаю.
                      +1
                      Я уверен, что эту проблему можно решить. Я просто не могу её понять. Напишите упрощённый test case.
                      Что если написать так:
                      that[methods[index]] = (function (name) {
                      return function () {
                      request.push('"' + name + '":' + that.methods(arguments));
                      if (!timer) { /* ... */ }
                      };
                      })(methods[index]);


                      Ещё, в вашем примере вы забыли оператор new вот здесь: var File=PHPUIclass('FileManager.php');
                        0
                        А вот за это большое спасибо :)

                        Сначала пытался реализовать это именно так, но до конца не разобрался, как это можно сделать. То, что я пробовал - не работало. Буду знать теперь.

                        Добавил new :)
                +1
                в BASIC`е отучивали использовать GOTO
                в JavaScript отучиваем использовать eval()

                :o)
                  +1
                  Улыбнуло :)
                  Нет, они правы. Действительно важный момент был реализован через жопу! Сейчас я уже поправил код. :)
                    +1
                    Именно!

                    Абсолютно согласен с утверждением не использовать eval() в JS вообще, также как и GOTO в BASIC`е.

                    По крайней мере, при написании кода, на который приятно смотреть :)

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

                    Хотя.. Даже мыслить нужно так, чтобы изначально получался правильный код.
                    А не просто работающий.
                      0
                      Всё обсуждение проекта осталось на уровне - использовать eval или нет :)

                      Наверное мне стоит задуматься над тем, чтобы писать статьи по другому.
                +1
                Термин "замыкания" вам о чем нибудь говорит?

                function makeMethod(method){
                return function(){
                request.push('"' + method + '":' + that.methods(Array.prototype.slice.call(arguments)));
                if (!timer)
                ...
                }
                }
                this.add = function(res){
                var methods = eval('{' + res.responseText + '}');
                for (var index in methods)
                that[methods[index]] = makeMethod(methods[index]);
                }
                  0
                  Да, говорит. Спасибо, уже код привёл в порядок. Просто я сначала не понял, как это лучше всего реализовать. За что извиняюсь, конечно, как автор статьи.
                    0
                    Да, и вместо that.methods(Array.prototype.slice.call(arguments))) я бы использовал that.methods.call(null, arguments).
                      0
                      Так- не работает :)
                        0
                        Я вам даже скажу почему. Потому что не надо по массиву бегать при помощи for (i in arr) Используйте старый добрый for (var i = 0, ii = arr.length; i < ii; i++) Это убережёт вас от наступания на многие грабли.

                        Тогда можно просто написать that.methods(arguments);
                          0
                          Согласен.
                  0
                  Глобальная переменная? Вы возможно имеете ввиду global $UImethod?
                  Завтра с утра добавлю небольшой комментарий по этому поводу в текст, а пока, просто отвечу комментарием.

                  Функция __call в классе UserInterface из UI.php вызывает статические функции (подлинкованные с помощью add()). Да, красиво было-бы внутри их использовать не $UImethod, а $this - да и правильнее, но вот в PHP есть такая особенность функции call_user_func[_array], которая теряет $this даже при вызове статических методов из других объектов. Правильнее, если мы хотим, чтобы $this не потерялся, вызывать функцию eval("$class::$func(...)"); вместо call_user_func(array($class,$func)) но тут вам снова не понравится использования eval'a ;)

                  Так что приходится выбирать между eval и гибкостью. Для данного примера я выбрал call_user_func для большей ясности.
                    0
                    Я не про РНР, я про JavaScript.
                    Вот это:
                    this.ajax_run=function(req){
                    eval('var ret='+req.responseText);
                    for (var i in func){
                    if (func[i])
                    func[i](ret);
                    }
                    request=[];
                    func=[];
                    timer=false;
                    }

                    Зачем там eval? И разве request, func и timer не глобальные переменные?
                      0
                      А Вы, как я понял, предлагаете парсить строку и в процессе создавать объект? В таком случае возрастет нагрузка со стороны клиента. При больших объемах принимаемых данных это не эффективно.
                        0
                        а почему бы не написать хотя бы
                        var ret = eval(req.responseText);
                          0
                          С этим согласен, но eval все равно остался.
                            0
                            Совместимость с IE - так оно не работает!
                              0
                              Да ну нах... проверил - работает.
                                0
                                А у меня нет - IE 7.0.5730.13 сразу в ошибку вылетает на этой строчке :)
                                  0
                                  покажите req.responseText
                                    0
                                    http://zcn.ru/MVC/FileManager.php ['a','b','c','d'] итд.
                                    Думаю, что {['a','b',...]} он воспринял-бы куда более достойно, но я не вижу особого смысла - всеравно через eval провожу.
                                      0
                                      ну JSON это как раз второй вариант
                                        0
                                        Знаю. В данном примере это что-то поправит?
                                          0
                                          IMHO - код будет много приятнее.
                                            0
                                            Я ошибся с просони. %)
                                            Туда и так возвращается AJAX в виде {"dir":["a","b","c"...]} например. Какие предположения, почему выводит 'Expected ;' в IE при var ret=eval ?
                                              0
                                              ну как вариант, он должен возвращаться в формате {"dir":["a","b","c"...]};
                                                0
                                                Неа, не работает :)
                                                  0
                                                  тогда попробуйте ({"dir":["a","b","c"...]}) - вот такой вариант
                                  0
                                  ({...}) cработало. В чём могла быть проблемма?
                                    0
                                    в том что было не так как сейчас. А я тоже не сразу понял в чём проблема
                                      0
                                      Ну ладно, поверим наслово, разберемся позже :)
                                +1
                                eval выполняется учитывая контекст в котором его вызывают, видя локальные переменные и переменный в замыкании, лучше создававть через конструктор анонимную функцию. Она быстрее работает:
                                var ret = (new Function('return '+reg.responseText))();
                                  0
                                  Бля!!! вот это красота!!! решпект и уважуха!
                                    0
                                    Да! Отличная идея :)
                                      0
                                      Формально это тот же eval только написанный через другой синтаксис. Может оно и быстрее, но это те же яйца, только в профиль.
                                  0
                                  Это переменные класса PHPUIclass
                              0
                              А мне идея нравится. Может и не самая лучшая реализация, зато идея хорошая :)
                                0
                                Реализацию немного поправил в лучшую сторону. eval'ов осталось два :). Спасибо комментаторам.
                                +1
                                "за привЕдение кода в порядок"
                                либо, после привидение скобку закройте :)
                                  0
                                  Спасибо за привИдение)
                                  0
                                  А насколько тормозит данное дело? Помоему данный подход будет очень нагружать систему.
                                    0
                                    Для клиента или для сервера?
                                    Для сервера — это разгрузка — так как идёт снижение трафика и нет обработки типа Скинов.
                                    Для клиента — это небольшая нагрузка. Один раз на страницу можно и поработать :)
                                      0
                                      1. Для клиента
                                      2. Для сервера отдача файлов занимает мало время,
                                      и экономия трафика, когда нет этого:
                                      header(«Cache-Control: no-store, no-cache, must-revalidate»);
                                      header(«Cache-Control: post-check=0, pre-check=0», false);
                                      header(«Expires: Mon, 26 Jul 1997 05:00:00 GMT»);

                                      3. А я уверен что будет. Правда на ваших примерах этого не заметиш, но на вещах побольше уже будет заметнее.
                                      И как вы собираетесь например табличку нарисовать )

                                      Смысл в темплейтах на яваскрипте, если рисуем один раз, грузим и не сохраняем в кеше.

                                      Файл mod.fileview.php
                                      @iconv — Данная фича приводит к тормозам, в данном случае это правда неважно.

                                      $file=@iconv(«Windows-1251», «UTF-8», $file); А почему сразу не в UTF-8 делать файлы?

                                      Вместо preg_replace можно использовать strtr
                                        0
                                        Начну с конца :)

                                        Таблица рисуется JSом.

                                        for…
                                        skins.skin['GOODS_NAME']=…
                                        skins.skin['GOODS_COUNT']=…
                                        skins.skin['SKIN.TABLE']+=skins.use('%SKIN.TABLE.PIECE%');

                                        Смысл темплейтов:
                                        1) Ненужно подгружать их каждый раз. (если пользователь не жмёт F5). 2) Общаться с темплейтами в JS удобнее, чем на php, когда пишешь многофункциональный JS интерфейс.
                                        3) Нет необходимости для сложных вставок на страницу, пользоваться createElement. Кода получается в 4 раза меньше.

                                        Значения header'ов уже выставляются программистом в зависимости от поставленных задач. Само-собою, названия товаров в магазине, можно и нужно кешировать. А вот в игре, например, перемещение кораблей уже не удасться показывать с помощью кеша.
                                        Я не пытаюсь показать максимально оптимизированный код. iconf использован лишь для примера.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое