Ajax, Templates И Много Мыслей

Давно мне не дает покоя одна фигня, связанная с динамическим редактированием данных.

Вот, смотрите.

Предположим, у меня есть набор данных для вывода во вьюхе (допустим, при помощи банальной таблицы). Допустим также, что я хочу редактировать их с помощью модального окна, которое, в свою очередь, использует ajax.

Есть три задачи:

  • создание записи: после сохранения новая запись должна появиться в таблице,

  • изменение записи: после сохранения данные в строчке таблицы должны быть обновлены,

  • удаление записи: после нажатия кнопки строчка из таблицы должна исчезнуть.

При обычных запросах (с обновлением страницы) это не проблема, но хочется динамики.

Какие решения вижу:

Для создания и изменения возвращать рендер какой-нибудь partial-вьюхи, типа _row.php, которая будет содержать в себе html-код одной строчки. В основной вьюхе (index.php) делать в цикле renderPartial(’_row’). Не нравится тем, что постоянно придется renderPartial дергать: накладные расходы растут.

Можно разбить на две вьюхи: в index обычный <? foreach (…) ?><tr>…</tr><? endforeach ?>, в _row отдельно код для одной строки. Не нравится тем, что не DRY.

Опять же, а каким образом осуществлять коммуникации? к примеру, если строку удалили - что передавать? А если обновили? (нужно будет передать не только новый html для строки, но и айдишник, например, чтобы на клиенте понять, в какое место пихать новый код)

Можно отказаться от серверного рендеринга и рендерить на клиенте (например, иногда использую knockoutJs), а во вьюху передавать json-данные: это чертовски приятно (работаем только с моделью как с observable-массивом данных, а клиент сам разбирается, что происходит), но по ощущениям явно недоиспользую (knockout и прочие angular хороши для создания полного SPA, а у меня только частичный)

Текущее решение-мутант - JsRender (от observables отказался, гоняю тупо шаблонизатор) + jQuery events + Json, во вьюхе серверный рендер списка через foreach, но там же неподалеку лежит template-script для использования аяксом.

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

Как вы вообще решаете такие задачи?

Кажется лучше использовать какой-то JS фреймворк на клиенте.

офигенный ответ ;D ;D ;D

Я имею в виду ваш второй кейс: отдача JSON сервером и самостоятельный рендеринг в браузере с помощью JS фреймворка (backbone).

Дело в том, что я также отдавал готовый HTML, и рендерил на JS, как вы описали, но второй вариант кажется более правильным. Впрочем я все же специализируюсь на back-end, так что в AJAX’е опыта не много.

Да не, отрендерить-то не проблема. И даже админку, допустим, сделать в виде SPA - не проблема, хоть и возни много.

Но потом внезапно возникает задача сделать нечто похожее, но для фронтэнда: скажем, страница списка товаров, которые поисковиками индексироваться должны (тут за клиентский рендеринг мне сеошники голову снимут), но с возможностью совершать какие-то действия (оставлять комментарий к товару, например). Тут-то оно и опаньки. А подключать backbone/knockout/angular для такой страницы - это из пушки по воробьям, поскольку половина функционала вообще задействована не будет.

Сейчас пока остановился на некотором решении-мутанте (клиентский рендеринг без observables, чисто на событиях): сервер в ответ на пост-запрос возвращает json, в котором одно из полей - название события (‘updated’, ‘created’). Клиентский скрипт, получив ответ, оповещает страницу (или кусок страницы) о произошедшем, а дальше пара хуков делают replaceWith, remove и прочее.

Кстати, я вот тут смотрю на соотношение между кол-вом просмотров моего топика и кол-вом ответов и думаю: это я непонятно излагаю свои мысли, или действительно нет хорошего решения у народа?

Эй, программеры, колитесь, как вы с ajax работаете?

С таким ньансом я вообще не сталкивался. Наверное моей квалификации недостаточно что бы вам что-то полезное посоветовать.

Думаю лучше смотреть соотношение "добавочные тормоза/легкость написания и поддержки кода" а вы смотрите на "сколько в фреймворке возможностей/сколько из них вы используете". Тот же backbone весит всего 16кб AFAIR.

Мы ведь и все возможности jQuery не используем, но не отказываемся от этой библиотеки из-за этого.

Да, но тут ведь как получается… вот листаю оглавление доки backbone:

  • Events - есть в jQuery

  • Model, sync - скорее не нужны, поскольку придется как-то дружить их с activeform

  • Router, history - это для SPA, не нужны.

В общем-то, только View и остается из всей библиотеки )

Кстати, кто-нибудь делал SPA на Yii?

Я конечно глуп, особенно с js, но как же gridview?

В 1 версии он обновлялся без проблем, $.fn.yiiGridView.update() прекрасно справлялся. Я собственно делал так редактирование в модальном окне с формой, все было хорошо.

Недавно так же реализовывал полный CRUD в модальных окнах на гриде, там не морочил голову и просто обновлял страницу.

Если делать без обновлений - тут действительно нужен js фреймворк. Я сам сейчас начинаю пробовать angular, лучше не мудрить, там это сделать в разы проще.

Насчет SPA - не делал никогда. Специализируюсь на бэкенде, а там он не нужен (и вреден на самом деле). Да и врятли с SPA будет нормальная индексация, как по мне то это имеет смысл если в наличии 5-6 страничек (либо статический контент), тогда можно сделать красивые переходы и прочее, а для индексации - выгружать весь контент сразу, а показывать кусками. Для интернет магазина это явно не подходит (IMHO).

Насчет количества ответов, действительно не хватает наверное квалификации для таких вопросов.

Я использовал два варианта:

  1. Использовать подход Yii виджетов, такой как в Grid View - если нужно обновить данные в таблице, то отправляется ajax-запрос на сервер, возвращающий HTML с новой версией таблицы. Т.е. так же, как работает постраничная разбивка, сортировка и поиск в CGridView.

Работать на уровне отдельных строк (создали запись - сервер вернул HTML c единственной строкой, на клиенте вставили новую строку в таблицу) я смысла не вижу по следующим причинам:

  • Обычно используется разбивка на страницы и в таблице 20-50 записей, заметной разницы в производительности при рендеринге всей таблицы / одной строки в большинстве случаев не будет.

  • Добавление и удаление одной строки в таблице ломают разбивку на страницы.

Например, мы показываем по 20 записей на страницу. Удалили 10-ю запись. Теперь если убрать одну строку из таблицы, то мы видим 19 записей, а не 20. Т.е. 20-ю запись сейчас не видно, а если перейдем на следующую страницу, то увидим записи с 21 по 40-ю - и опять 20-й невидно.

При рендеринге виджета целиком по ajax-запросу удается сохранить целостность состояния виджета на стороне клиента при небольших усилиях на стороне сервера.

В коде контроллера у меня что-то вроде этого:




    public function actionManage() {

        $dataProvider = $this->getDataProvider();

        if (!Yii::app()->request->getIsAjaxRequest()) {

            $this->render('manage', array('dataProvider' => $dataProvider));

        } else {

            echo $this->processOutput($this->widget('#grid', array('dataProvider' => $dataProvider), true));

            Yii::app()->end();

        }

    }



По обычному запросу выводится полный шаблон страницы, который включает в себя grid.

По ajax-запросу выводится только грид.

Достоинства метода:

  • "unobtrusive javascript" - возможность создания страницы, которая будет работать даже с отключенным javascript

  • использование готовых виджетов Yii

Недостатки:

  • между клиентом и сервером передаются лишние данные

  • дополнительная работа на стороне сервера для обработки обычных и ajax запросов

  • ощущение "неправильности" метода

  1. Yii на сервере + AngularJS на клиенте.

Сервер отдает на клиент HTML шаблон + последющие ajax запросы получают данные в виде json.

Достоинства метода:

  • удивительно, но объем кода и на клиенте и на сервере сокращается буквально в разы

  • "чистые" HTML шаблоны - нет смеси кода шаблонизатора и HTML

  • минимальный объем данных, передаваемых с сервера на клиент

  • отличное ощущение того, что наконец-то все сделано правильно

Недостатки:

  • SEO-оптимизация - для корректной работы поисковых машин нужна дополнительная работа

Варианты решения проблемы с SEO - генерация упрощенных страниц с уже вставленными данными специально для поисковых машин (подходит, если страниц мало) либо рендеринг статических версий страниц для поисковых машин с помощью PhantomJS (или других подобных технологий).

В некоторых случаях проблема вообще отсутствует - админка / сервис не предназначенный для индексации поисковыми машинами и т.д.

Также мне кажется, что в ближайшее время эта проблема будет решена на стороне поисковых систем.

Резюме - по возможности переходите на AngularJS, не пожалеете.

С angular работал, но мне почему-то knockout ближе и понятнее. Так-то в принципе один фиг. Ну и еще у меня не получилось подружить angular с парой плагинов, которые были в шоке от того, как DOM колбасит, а какого-либо внятного хука после окончания "рендера" на тот момент у ангулара не было.

А вот что касается "Добавление и удаление одной строки в таблице ломают разбивку на страницы" - тут не все так просто:

Предположим, у нас на странице есть сортировка по какому-нибудь из параметров, и по условиям сортировки (например, created_at ASC) новая запись появится в конце списка, на последней странице.

Тогда возникает немного непонятная ситуация: вроде страница обновилась, а записи на ней нет :) ну и нафига тогда обновлять было. Частично это решается введением дополнительного блока типа “последние обновленные” в стороне от основного грида, но все равно не очень красиво.

И еще вот какой пример: все то же самое, только вместо создания одной записи - массовое создание (например, массова загрузка пачки изображений). Изображение загрузилось - должно появиться. А если их кол-во превышает кол-во записей на страницу - через какое-то время наступит ситуация, когда прогресс-бар бежит, страница обновляется, а ничего не происходит :)

Удаление строк можно решить, к примеру, заменой контента строки на "удалено (восстановить?)", это нормально и понятно выглядит.

Кстати, да, Вы совершенно верно отметили, что тут еще и по пользовательскому интерфейсу много заморочек. И обновить, и чтобы понятно было, и вообще. АРРРРРРРРРР.

Да, я Вас прекрасно понимаю ))).

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

Гораздо проще и красивее получается, если интерфейс строится и обновляется на клиенте, а не на сервере.

Строил и на клиенте. Геморроя - полные штаны.

И, черт возьми, не отпускает ощущение, что я что-то не то делаю.

Вот прямо по шагам:

  1. Строим грид на клиенте! ура! это круто, модно и динамично!

Данные в него будем передавать, разумеется, через JSON.

Json будем формировать на сервере.

А как формировать? ну, например, вытащить все данные из БД в виде массива (asArray в Yii2 нам в этом здорово поможет) и отдавать их.

Через некоторое время понимаем, что сразу в виде массива вытаскивать неудобно: в модели могут быть прописаны геттеры (например, превращающие timestamp в человеческую дату). Окееей, будем вытаскивать в виде AR, а потом циклом формировать json, параллельно дергая геттеры. (в этом месте в голове начинает копошиться нехороший червячок, как бы намекающий, что за этот цикл можно было бы и html сформировать уже).

Ладно, сформировали массив, отдали на клиент, клиентский шаблонизатор это дело отрендерил. Теперь мы сможем добавлять, удалять и редактировать записи прямо в массиве, а оно будет само динамически рендериться. Круто же. Или… нет?

  1. Редактируем запись.

Для редактирования записи откроем-ка модальное окно прямо на основе данных из грида.

А хрен там: в гриде-то у нас только name и created, а для редактирования нужен еще id и description. Так, какие у нас варианты: либо хранить в массиве грида вообще все данные записи (но это нонсенс, мы как раз пытались сэкономить трафиик, передавая json, а тут придется передавать кучу лишнего), либо подгружать в окно данные через ajax.

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

  1. Проверяем запись.

Клиентская валидация - вещь хорошая. Особенно, если она хоть как-то соотносится с серверной (в модели). Тяжело вздыхаем, пишем валидацию модели (клиентской) еще раз, а червячок продолжает копошиться, намекая, что это вовсе не DRY, а в Yii уже есть клиентская валидация на основе серверной модели, только вот она к activeForm привязана, и стоит только отказаться от клиентского рендеринга…

Загоняем сомнения поглубже, и уверенным движением отправляем данные на сервер.

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

  1. Отображаем изменения в гриде.

Ок, данные были сохранены, надо бы грид поправить. Причем данные нашего окна редактирования для этого использовать нельзя, это можно сделать только при помощи данных с сервера: во-первых, геттеры, во-вторых, сеттеры, в третьих - некоторые поля могут быть заполнены самой БД (id, например), в четвертых - отношения. То есть, данные для грида (новая строка, измененная строка) должны формироваться отдельно.

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

Было бы логично (но небезопасно) делать всё на клиенте (модели, валидация, рендер итд).

Было бы логично (но устарело) делать все на сервере (клиенту голый html) отдавать.

А вот все промежуточные решения - это какие-то мутанты-инвалиды.

Так и есть, формируете JSON используя геттеры AR, все правильно. Сомнения отметайте, ну и что, можно было сразу HTML сгенерить, что с этого? Такова цена за меньшее число обновления страницы.

Вы что, затеяли эти пляски с AJAX только ради экономии трафика? Мне кажется что цель в более гибком UI.

Недавно смотрел встепление в Marionet (на осное backbone) там фреймворк брал на себя всю эту рутину с хранением полей, которые могут пригодится для редактирования. Думаю и для остальных "верстаков" должен быть такой функционал.

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

Очень просто, запросить с сервера одну запись, которая изменена (действие show), обновить её в памяти и потом уже выводить в грид.

Я наверное не так понимаю, но мне кажется вы пытаетесь чуть ли не с помощью спагетти кода на jQuery написать велосипед, лишь бы не использовать ангулар или марионетку, где все эти стандартные задачи должны уже быть решены. Конечно это будет выглядеть как мутатнт.

Но опять оговариваюсь, что я и даже этого не делал.

Не. Наверное, просто плохо пояснил.

Суть в том, что данные для формы редактирования и данные для грида могут очень(!) сильно отличаться, особенно в случае наличия отношений между записями.

Ну и траффик, конечно. Простой пример: на странице 20 записей, у каждой есть description (тип text), с учетом кодирования это чудовищный объем данных. Пять килобайт текста (предположим, это страница) в каждой записи даст полмегабайта json.

Спасибо, посмотрю. Прямой ссылкой не поделитесь?

Вот как раз-таки наоборот, с помощью спагетти на jQuery (события + ajax) у меня вполне красиво получилось в итоге.

А с помощью ангулара и прочего бекбона - получаются мутанты (дублирование функционала, избыточность данных и постоянное ощущение чего-то неправильного)

Кнокаут как раз и использую потому, что он существенно меньше этого ощущения дает :)

Тут вот кстати недавно доклад был на тему "почему все php-фреймворки сосут", один из тезисов - потому что заново реализуют уже готовый функционал.

В частности, большинство фреймворков так или иначе делают свой роутинг, а ведь задача-то уже давно решена mod_rewrite и аналогами.

И я все никак не могу избавиться от ощущения, что все эти телодвижения вокруг реализации MVC/MVVM на клиенте - это уход еще дальше.

Т.е. кто-то предлагает осуществлять роутинг средствами веб-сервера? Ой, скажите, что я вас не так понял. Как же тогда урлы генерить?

Можно пример?

Именно так.

Генерить, собственно, так же, только вместо config.php все урлы прописываются в .htaccess.

Соответственно, вместо единой точки входа (index.php) - каждый контроллер отрабатывает.

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

Легко.

Самый простой пример я уже привел в сообщении выше: если у нас есть список страниц, который мы хотим редактировать средствами навороченного грида без перезагрузки данных - нам придется передавать все текстовые данные (назовем это поле description) в грид, соответственно 500кБ на страницу. А это жесть :)

То есть, в гриде description нам не нужен, а вот при редактировании - нужен. И его надо откуда-то брать (подгружать отдельно?..)

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

В гриде выводим имя юзера и название города.

Поскольку при редактировании city_id может измениться, нам потребуется хранить где-то неподалеку от грида еще и полный список городов (чтобы, во-первых, выдавать его при редактировании, во-вторых - выводить название города в гриде при изменении).

И так по каждому классификатору.

А если у города еще и описание будет…

В общем, всё это сводится к одному из двух: либо настолько сложная клиентская логика, что взрывается мозг (и не дай бог где-то js-ошибка возникнет - упадет всё), либо кучу данных туда-сюда гонять. А мы, вроде как, стремимся к удобству для клиента.

Ну я про генерацию внутри РНР, в Yii ведь есть хелперы для генерации URL, и они работают, потому что роутинг является частью фреймворка. И это удобно и гибко. Странно, что кому-то хочется перенести это на сторону вебсервера.

Тогда давайте уберем требование "без перезагрузки данных", оставим только "без перезагрузки страницы"

В стандартный CRUD входит действие “show” - возвращающую данные для отображения записи. И действие ‘create’/‘update’ - отвечающее за отображение формы и за её обработку. Кстати в Рельсах, они состоят каждое из двух отдельных действия ‘new’/‘edit’ и ‘create’/‘update’, это немного упрощает понимание.

Тогда получается модель:

  • Запрашиваем действие ‘index’ и получаем JSON с полями для отображения в гриде, так, как они должны отображаться. Ну и ID записи обязательно, даже если показывать в гриде её не надо.

  • Если юзер начал редактировать строку в гриде, мы знаем id записи и запрашиваем действие ‘update’ - получаем поля для отображения в полях формы и рендерим форму в диалоге, или вместо грида.

  • Когда юзер нажал “сохранить”, отправляем данные на сервер, и если есть ошибки, показываем их в форме, если нет, то запрашиваем ‘show’ что бы получить данные именно для этой записи и обновляем грид (данные для грида других записей у нас все еще в памяти)

Таким образом валидация и форматирование данных отдельно для показа и отдельно для форм остаются на стороне сервера.

Самое забавное, что в реальной жизни от генерации урлов толку особо нет.

Казалось бы, как удобно: поменялись требования к адресам - изменить пару правил и жить долго и счастливо.

А вот нифига :) Внезапно оказывается, что в контенте (статьи, новости) старые урлы прописаны (ну, если не было предпринято каких-то специальных телодвижений), а на запись, которая теперь отдается по новому адресу, стоит куча внешних ссылок, и все равно приходится дергать htaccess для переправки запросов.

Изменение урлов - это в любом случае дофига ручной работы.

Тут примерно так же, как со сменой БД. Давайте напишем кучу абстракций, чтобы можно было хот-свап делать, а потом выясняется, что постгрес не приемлет поганые данные mysql :)))

Ну да, только это как раз и возвращает нас к одному из предыдущих сообщений, про бессмысленность бытия json-данных в гриде. То есть, вопрос такой: а какой профит мы имеем, гоняя туда-сюда json при указанной выше модели?

Все таки посмотрите angular, там все описанные Вами ниже проблемы на клиентской стороне очень просто решаются.

Ну примерно так я и делал, только не делал getters, а перегружал getAttributes() / setAttributes() и в базовом контроллере методы для конвертации модели или массива моделей в json.

В результате код в контроллерах очень короткий и однотипный - выбрали из базы нужную модель / модели, преобразовали в json, отправили на клиент.

Создание / обновление модели примерно так:




    public function actionCreate() {

        $model = new Game;

        $this->editGame($model);

    }


   public function actionUpdate($id) {

        $model = Game::model()->findByPk($id);

        $this->editGame($model);

    }


    private function editGame(Game $model) {

        $item = $this->getRequestJsonData(true);

        $model->setAttributes($item);

        $model->save();

        $this->sendModelJson($model, 'save');

    }



С angular у нас единственный источник данных - массив с моделями.

Он же отображается в гриде, его же используем для отображения в модальном окне.

Данные между модальным окном и гридом синхронизируются автоматически ангуляром.

Тут да - валидация на клиенте для удобства пользователя + валидация на сервере собственно для проверки даннных.

Но особых сложностей не наблюдатется - на сервере обычные Yii-шные правила валидации в моделях, на клиенте - декларативные правила ангуляра (описываются прямо в шаблоне формы).

Отображение сервенрых ошибок на клиенте тоже привязать не сложно.

Это ангуляр сам сделает, мы только модели обновляем.

Итого на сервере имеем по сути API интерфейс для доступа к базе (т.е. данные + серверная бизнес логика, валидация данных). На клиенте - интерфейс и связанная с ним логика, причем ангуляр выполняет всю работу по синхронизации моделей и интерфейса.