Быстрый старт для ExtJS back-end


    Проблема взаимопонимания


    Представьте себе ситуацию: вы — фрилансер и работаете с Ext. С вами удалённо работает один (или несколько) разработчиков back-end. Работа идёт быстро и гладко. Но вот вот случилось так, что разработчик серверной части сменился. Если у нового коллеги есть опыт взаимодействия с Ext — всё замечательно. Но вот если человек впервые будет писать back-end для Ext или впервые будет писать для веба (такое тоже случается), то вам будет необходимо найти общий язык.
    И вот тут могут начаться проблемы…
    Придётся тратить время на простое объяснение протокола, объяснения как должен реагировать сервер на те или иные запросы. Чтобы избежать этого, я подготовил документ, описывающий все (ну или почти все нюансы стандартного для Ext протокола). Этот документ и представлен под катом.

    Решение проблемы взаимопонимания



    ExtJs на полную использует возможности REST архитектуры взаимодействия. Что это и с чем едят уже писали на хабре:
    REST vs SOAP. Часть 1. Почувствуйте разницу
    Дао Вебсервиса. (Или да хватит же изобретать велосипеды!)
    Автору первого топика (Artiom1988) — огромное человеческое спасибо за таблицу, она сэкономила мне огромное количество времени при общении с разработчиками. И её (с небольшими изменениями) я украл для своего документа

    Стандартные HTTP операции

    URL GET POST PUT DELETE
    example.com/ticket Получаем список всех билетов Создаём новый билет Обновляем список билетов Удаляем все билеты
    example.com/ticket/15 (где 15 — id билета) Получаем информацию по конкретному билету -- Изменяем билет Удаляем конкретный билет

    Запросы должны работа в случаях, когда URL содержит финальный слеш (www.exam.com/ticket/) и когда слеш отсутствует (www.exam.com/ticket)

    У каждого объекта (билета, юзера и т.д.) есть поле id — уникальный идентификатор в виде целочисленого значения.

    Формат сообщений

    Все сообщения передаются в валидном JSON.
    Далее массивом будем называть конструкции вида:
    [1,2,3,4,"строка",true, false, "34"]

    а объектом конструкции вида
    {"param1" : "value1", "param2" : true, "param3": 45}

    обратите внимание, что имена параметров всегда строки (в двойных кавычках), значения параметров могут быть строками, числами и булевыми (true и false)
    Полями будем называть имена параметров, в объекте выше есть поля param1 и param2

    Объекты могут включать массивы, а массивы объекты, например
    {
      "success": true,
      "items" : [ {
        "name": "Vasya"
      }, {
      "name" : "Sveta"
      }]
    }


    Формат запросов клиента

    POST и PUT

    Содержит один параметр items, который представляет из себя объект
    {"name":"123","id":0,"region_id":2,"region_name":""}


    GET

    При запросе списков может сдержать стандартные параметры:
    start — номер записи с которой начинается ответ
    limit — максимальное количество возвращаемых записей
    filter — массив, содержащий объеты вида
    {"property":"name","value":"Вася"}

    т.е. GET запрос
    /users?start=40&limit=20&filter=[{"property":"name","value":"Вася"}]
    (запрос естественно uri кодирован) должен вернуть 20 пользователей с именем Вася, пропустив первые 40 из БД.
    Хорошей практикой использовать для полнотекстовой фильтрации фильтры с именами как у искомого поля + строка "_like". Т.е. фильтр
    {"property":"name_like","value":"Вас"}
    найдёт и Василиев и Василис. Пораметры, имеющие полнотекстовый поиск, естественно стоит оговаривать.

    Формат ответов сервера

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

    Ответы на GET запросы

    Если запрашивается список предметов, то в ответе должно быть поле total, представляющее из себя целое число, показывающее общее количество строк в БД для этого контроллера (т.е. если мы указали параметры filter и limit и получили несколько записей, то в total всё равно указывается обзее количество записей).
    Если запрашивается конкретный предмет ( exmple.org/users/5), то поле total не ставится.

    Сами данные находятся в поле items, которое представляет из себя массив, содержащий объекты-данные.
    {
      "items":[{
        "region_id":2,
        "name":"Moscow",
        "id":25
      },{
        "region_id":1,
        "name":"Piter",
        "id":18
      }],
      "success":true,
      "total":2
    }


    Если был запрошен один объект, то массив не используется
    {
      "items":{
        "region_id":2,
        "name":"Moscow",
        "id":25
      },{
        "region_id":1,
        "name":"Piter",
        "id":18
      },
      "success":true
    }


    Ответы на DELETE запросы

    {"success":true}
    — Этого достаточно

    Ответы на POST и PUT запросы

    Аналогичны ответу на GET запрос, при запросе конкретного объекта, только должны возвращать уже изменённые данные.

    Ошибки

    В случае ошибок поле success равно false, так же добавляется поле errors — массив строк с описанием ошибок
    {"errors":["Object not found"],"success":false}


    Именование данных

    В случае, если объекту нужно ссылаться на другой объект, используется параметр с приставкой _id, т.е. если сервер возвращает объект-город, а город находится в регионе, то ответ может быть такого вида:
    {"region_id":2,"name":"Moscow","id":25}

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

    Когда один объект содержит много и необходимо получить вложеную иерархию используется поле-массив с именем во множественном числе, т.е. если запрашивается регион, к которому прикреплены несколько городов, то ответ может быть таким:
    { 
      "id":2,
      "name":"Central",
      "cities" : [{
        "name": "Moscow", 
        "id":2
      },{
        "name":"New York", 
        "id":3
      }]
    }


    Методика тестирования бэкенда

    Для тестирования можно использовать замечательное расширение Google Chrome
    1. Отправка GET запроса (в БД на сервер должен быть хотя бы один айтем)
    2. Отправка POST запроса содержащего все или необходимые данные. В ответе должна придти информация по новому айтему.
    3. Отправка GET запроса и проверка наличия созданного в пункте 2 айтема
    4. Отправка PUT запроса для изменения айтема, созданного в пункте 2 и полученного среди прочих в списке в пункте 3. В ответе должна придти информация по изменённому айтему
    5. Отправка GET запроса и проверка изменения созданного в пункте 4 айтема
    6. Отправка DELETE запроса для удаления айтема, созданного в пункте 2. В ответ приходит подтверждение.
    7. Отправка GET запроса и проверка отсутствия созданного в пункте 2 айтема


    Заключение


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

    Ну. И что?
    Реклама
    Комментарии 16
      –1
      У меня данные на клиент в случае постраничного доступа, а не доступа по id передаются немного в в другом формате, что позволяет сэкономить на повторении названия ключей до двух раз и более.
        0
        Не могли бы дать пояснение как это работает?
          0
          Обычно json данные передаются как {k1:v1, k2:v2}. В случае постраничного доступа ключи всегда одни и те же => передаю список ключей один раз в отдельном поле. Поле deps, это для зависимых сущностей.
            0
            А цель такой модификации в чём? И много ли изменений на стороне клиента?
              –1
              цель — избавится от повторения названия ключей, на клиенте все собирается в привычный json элементарно, фу-ия на вход передается два массива одинаковой длинны, в одном список столбцов, во втором список значений, на выходе объект из {k1:v1, k2:v2,… kN:vN}
                0
                Я просто не пойму зачем это нужно, мы получаем лишнее кодирование, декодирование информации, а выигрываем пару сотен байт? Они же не стоят ничего при передаче.
                  0
                  на моих тестах при 5000 запросах (каждый запрос возвращает 20 записей, у каждой записи по 5 зависимых сущностей) без гзипа размер ответов+запросов+заголовков на моих тестах при обычном способе ~80mb, при моём ~40, гзипом жмет примерно в 4 раза. Так что вроде бы и копейки, но…
        +2
        мне кажется слегка нелогичный дизайн апи. например зачем нужны методы PUT и Delete для example.com/ticket

        /users?start=40&limit=20&filter=[{«property»:«name»,«value»:«Вася»}] — такой запрос лучше делать постом, поскольку при большом количестве опций и непонятной их длине — можно легко выйти за лимит гета. да и читаться постом он будет лучше.

        на случай если надо послать линк на результат такого запроса — то, мне кажется, логичнее его прихранить в кеше и сгенерить ликн типа /users?filter=kjwhelhelwhiw3WQR. примерно как яндекс со ссылками на карты делает.

        рекомендую книгу

        ну и просто поизучать REST API известных сервисов.

        >Обязательно должно быть поле success
        чисто семантически, лучше иметь поле result и оно имеет значение success или error.

          0
          Согласен, надо запрещать удалять список, чтобы нечаянно не сделать дурного. Не помню, чтобы удаление списка ВСЕХ объектов требовалось, максимум что-то вроде /goods/by_user/12 или что то в этом роде. И то это происходит параллельно с процессом удаления самого пользователя.
            0
            обновления/удаления лучше всегда делать постом, в том смысле, что параметры не в урле.

            на моей практике get всегда должен быть read only.

            Как частный прикол: Вася делает урл с удалением чегото и посылает его Пете, с которым они пользуются какимто сервисом. Петя негодует.
              0
              А, ну это отдельный разговор, разумеется, через POST. Я про вообще возможность (пусть через POST) удалить список пользователей.
            0
            Я понимаю, что это выглядит не самым лучшим образом. Это всё исключительно для ExtJS. Для ускорения и удобства разработки. Поле succes, id — стандартные в этом фреймворке.

            Вот по длине GET спорный вопрос. Конечно плохо иметь ограничение, но я пока ни разу не сталкивался.
              0
              Всё сделано для того, что бы при объявлении модели написать
              Ext.define('App.model.Region', {
              	extend: 'Ext.data.Model',
              	fields: [
              		{name: 'name'},
              		{name:'id',type:'int'}
              	],
              
              	proxy: {
              		url: '/regions',
              
              		type: 'rest',
              		simpleSortMode : true,
              		reader: {
              			type: 'json',
              			root: 'items'
              		},
              		writer: {
              			type: 'json',
              			writeAllFields :true,
              			root :"items",
              			encode :true
              		}
              	}
              });
              

              И оно будет работать.
                0
                оно точно также будет работать и в случае если REST интерфейс будет логичным. При всей моей нелюбви к ExtJS с данными он работает на 5

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

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