Comments 57
Гляньте в сторону BackboneJS REST API клиента, результат получится более структурированным. Ваше решение походит на JQuery лапшу.
Мир JS потихоньку отказывается от конструкций вида:
object.method("param", "param").method2("param").method3("param")...

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

Такой подход далек от кошерного ООП и больше походит на процедурный стиль программирования, где все действия выполняются в виде одного длинного алгоритма. BackboneJS же позволяет разделить логику обращения к REST API на небольшие компоненты (объекты модели), которые проще тестировать и расширять.

Другими словами предлагаемое мной решение позволит вам использовать объектную модель для работы с REST API, вы же предлагаете использовать некую точку входа. Это как сравнивать Data Mapper и Table Gateway, понимаете разницу?
Кошерное ООП это в первую очередь простая объектная модель, разработанная с применением модульного тестирования и самодокументирующимся интерфейсом, достаточно гибкая для расширения, но не слишком, дабы не усложнять решение.

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

А теперь сравните с реализацией на BackboneJS:
var bookList = new BookList;
bookList.fetch();

var book = bookList.findWhere({name: 'Use Backbone'});
book.set('name', 'No use Backbone';
book.save();

var newBook = new Book({name: 'Use XHR'});
bookList.add(newBook);
//предполагается что name используется в качестве id
//иначе что в этом примере, что в вашем, надо разруливать
//возможные совпадения названий у двух и более книг
var bookRes = api.books('Use another-rest-client');

var book = bookRes.get();
book.name = 'Always use another-rest-client!';
bookRes.put(book);

var newBook = api.books.post({name: 'Don't use pure XHR'});

Как видно, по коду значимых отличий нет. Разумеется, есть отличия, вызванные другой парадигмой (у вас есть неявный локальный кеш), но объём кода в случае another-rest-client абсолютно такой же. Что до понятности… Ну, вообще, если бы в моём случае были бы шоткаты для методов на полученном объекте, код был бы ещё понятней, хотя он и сейчас вроде не выглядит чем-то странным.


var book = api.books('Use another-rest-client').get();
book.name = 'Always use another-rest-client!';
book.$.put();  //вот такой магии пока нет и не факт что будет, хотя я думал о ней и до этой публикации
Вот я вам и советую не изобретать свое, а посмотреть в сторону BackboneJS. Поверьте, это очень небольшая библиотека, вы с ней ознакомитесь в течении нескольких часов и не пожалеете, раз уж пришли к тем же решениям.

Из возможных отличий:
Как вы решаете проблему преобразования нестандартного ответа сервера в стандартные объекты?
Предполагается ли расширение коллекций и объектов логикой или это только простые хранилища данных (по аналогии с Data Gateway)?

Что подразумевается под нестандартным ответом? Если это какой-то кастомный Content-Type, можно зарегистрировать свой encoder/decoder, в readme это описано. Если ничего подобного нет, возвращается простая строка, с которой можно делать всё что душе угодно. Если сервер отвечает со статусом не 200, 201 или 204, Promise реджектится с инстансом xhr, с которым, опять же, можно делать что угодно.


Хранилищ данных там никаких нет — если только вы не подразумеваете под ними возвращаемые из промиз объекты. Последние — это просто преобразованные в удобоваримый вид ответы сервера, которые библиотекой вообще никак не запоминаются и не используются.

Что подразумевается под нестандартным ответом?

На пример ответ приходит не ввиде массива сущностей, а в таком виде:
{
  collection: [
    {сущность},
    ...
  ]
}


Последние — это просто преобразованные в удобоваримый вид ответы сервера, которые библиотекой вообще никак не запоминаются и не используются

Пример: что делать, если коллекция bookList должна включать нестандартный метод?

Ну, например:


var books = (await api.books.get()).collection;

Какой ещё нестандартный метод? Вроде бы в Rest API никаких нестандартных методов быть не может.

Тобишь парсинг ответа в ответсвенность библиотеки не входит.

Какой ещё нестандартный метод? Вроде бы в Rest API никаких нестандартных методов быть не может

В REST API то не может, но может быть на уровне JS, на пример такой:
var bookList = new BookList;

var book = bookList.findFromAuthor('Name');

На самом деле это хорошая идея — дополнительный обработчик для определённых ресурсов. Впрочем, над этим ещё нужно поразмыслить. Любые методы и дополнительные обработчики — это уже ответственность data layer, а не простого клиента. И да, я понимаю к чему вы клоните — используй BackboneJS, Amareis, велосипеды не нужны (несложно было догадаться, учитывая что вы сказали это прямым текстом несколько комментов назад :)! Но, как я уже говорил, это разные уровни архитектуры и разные цели. Я написал простую обёртку для XHR, предназначенную для упрощения кода взаимодействия с определённым типом API. Конкретно для этого тот же бэкбон будет явным оверкиллом — в конце концов, я мог и jQuery для того плагина подтащить, не так уж это и страшно.

Объектная модель — это, безусловно, хорошо. Проблема в том, что это уже скорее тот самый data layer, нежели простой клиент. Я могу (и я подумываю об этом) сделать another-data-layer, который вполне может использовать another-rest-client в качестве бэкенда, но это будет уже совсем другая история.
А ещё, цепочка вызовов здесь оправдана тем, что она, по сути, является отражением итогового URL.


api.games(15).players(2).pet(4).get()

Превратится в:


GET http://example.com/api/v1/games/15players/2/pet/4

Отображение практически один в один, но при этом абсолютно не замусорено лишними символами и позволяет легко спрятать используемую несколько раз часть цепочки под алиас:


let me = api.games(15).players(2);
me.pets(4).get();
me.friends(17).delete();

В моих задачах этого пока более чем достаточно. Если будет мало — действительно, можно и свой data layer над этим надстроить, но цель этой библиотеки совсем другая.

Если будет мало — действительно, можно и свой data layer над этим надстроить

Вот я вам и предлагаю BackboneJS ) Конечно вы можете отказаться, ведь дело ваше.
Подскажите пожалуйста. Для чего требуется превращать URL в цепочку вызовов? Я правда не понимаю.

Во-первых, это красиво… :)
Собственно, мне просто не нравится конструировать строки руками. Это выглядит хуже. Впрочем...


api.res('games/15/players/2/pet/4').get();

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

Понятно. Ну если вам нравится, то в общем и ладно. На вкус и цвет…

Мне вот ваш вариант со строчкой нравится больше, хотя бы потому, что я ее могу скопировать в REST client и выполнить не заморачиваясь перекодированием из часть().часть().часть() нотации в часть/часть/часть нотацию. Да и если ваш код придется читать кому-то, кто его первый раз видит — будет гораздо проще понять куда же вы ходите за данными, опять же поиск по коду можно делать.

Конструирование строк руками вы так и так делаете только в одном случае вы результат можете использовать как минимум двумя способами, а в другом только одним (переиспользование не только кода, но и артифактов кода). Ну и наверняка реализация будет проще без заворачивания строк в объекты.

Кстати если захочется выполнять какую-нибудь ODATA, то заварачивание выражений в объекты — это верный путь к костылям.

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


api.res('games/' + game.id + '/players/' + player.id +'/pet/' + pet.id);
//ну или так, что сути особо не меняет
api.res(`games/${game.id}/players/${player.id}/pet/${pet.id}`);

Все эти кавычки и прочие скобочки-плюсики — лишний визуальный мусор, который затрудняет чтение и понимание кода. Добавьте сюда усложнение алиасов — в них надо будет хранить строки и вручную прибавлять их в начало формируемой строки. Собственно, всё это ручное конструирование мне и не понравилось настолько, что я отказался от использования rest и взял restful.js.

Согласен. Реальный код скорее всего будет параметризован

* api.games(game.id).players(player.id).pet(pet.id).get()
* api.res(`games/${game.id}/players/${player.id}/pet/${pet.id}`)

но может ведь быть еще

* api.res(`games(${game.id})/players(${player.id})/pet(${pet.id})`)

что на сервере получается из предыдущего примера с помощью нехитрых настроек, так же как видите в случае строки — легким и непринужденным изменением строки, но вот когда у вас все завернуто в объекты, то вы уже вынуждены делать серьезный рефакторинг.

Но это уже будет не Rest API, так что использовать для него Rest client будет неразумно ;)

Почему это будет не REST API? ODATA ( http://www.odata.org/ ) использует именно такой способ обращения к элементам коллекции и очень даже себе REST. Разве где-то есть требования к REST API формировать адреса так как вы написали выше? Если да — можете ссылку кинуть я бы с удовольствием прочел.

Честно говоря, я немного растерян. Ладно что я слышу об ODATA впервые, но все, абсолютно все API которые я когда-либо видел, делал или щупал, использовали традиционный путь со слешами! Это настолько базовая вещь что я действительно удивлён что она не эксклюзивна… Хотя постойте, ODATA делали в майкрософт?.. Хорошо, теперь я удивлён чуть поменьше.
Но всё равно считаю что это тот случай, когда спасуют все универсальные решения — тот же Backbone, например (хотя вот rest… Rest с этим справится, да); так что я готов с чистой совестью признать что тут мой велосипед не проедет. Хотя вы, конечно, всё ещё можете запихнуть сырую строку в метод res, но я уже предупредил что это нецелевое использование, которое может вызывать undefined behavior, рак мозга и случайные чёрные дыры на орбите Земли.
Используйте на ваш страх и риск.

Но всё равно считаю что это тот случай, когда спасуют все универсальные решения — тот же Backbone, например

Backbone по умолчанию сам формирует url для запроса данных из API, но если вам его подход не нравится, переопределить алгоритм формирования url в Backbone (если я не путаю) не составляет особого труда.
Ну что ж. В любом случае спасибо за ваши ответы и за то, что сделали свою библиотеку, поделились с людьми и рассказали о ней. Успехов вам в нелегком этом труде :)
Если API реализовано соответствующим образом, такой подход реализует Query Object. Насколько актуально такое API, хз.
Ну собственно чем многие ORM-ы и занимаются.

(кстати перевод статьи промптом сумашедший — куда проще понять, что автор хотел сказать тут http://martinfowler.com/eaaCatalog/queryObject.html )
Не совсем, ORM-ы только преобразуют реляционную структуру в объектную и обратно. QO нужен для формирования запросов в объектно нотации. Решение автора в таком случае может послужить удобным механизмом для формирования запросов к API на основании данных формы фильтра (на пример).
Не понмаю в чем удобство. Может быть я что-то упускаю. Я вижу два варианта

  • get('/games/15/players/2/pet/4')…
  • api.games(15).players(2).pet(4).get()…


В одном случае реализация не зависит от того как автор API структурировал свои адреса (например /games(15)/ или /games/15/ или вообще /games?gamesId=15), в другом зависит и как решать неоднозначности вроде /games/(15) не понятно.

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

Выше я вам уже ответил — вспомните что id ресурсов вовсе даже не хардкодятся (по большей части).


get(`/games/${game.id}/players/${player.id}/pet/${pet.id}`)
api.games(game.id).players(player.id).pet(pet.id).get()

Писать получается меньше, читать — проще (лично мне, по-крайней мере). Насчёт эффективности вы правы — каждый такой вызов клонирует всё поддерево ресурсов, но я не думаю что эту задержку можно заметить в реальной жизни. Ну а насчёт размера приложения… Не знаю даже, вряд ли он будет у вас значимо меньше. У меня в коде вот это поведение занимает 70 строк, ещё около сотни — независимые функции, которые так или иначе придётся реализовывать.
Я не просто так сделал акцент на "client that makes your code lesser and more beautiful than without it" в описании репозитория.

Безусловно. Сейчас ваше приложение 70 строк, пока вы реализовали (как вы сами пишете в статье) часть того функционала, который может понадобиться. Я предполагаю, что своей реализацией вы покрыли 80% своих потребностей, и как всегда по эмпирическому принципу 80-20, оставшиеся 20% потребуют 80% усилий, а значит ваша библиотека если вы ее продолжите развивать — станет толще и тяжелее.

Если вам нужно собирать адрес для запроса динамически, то без Object Query это будет сделать сложно, ибо он инкапсулирует сборку строки в себе.

На пример вот так:
var url = api.from('site.ru/api/games')
if(filter.gameId !== undefined)
  url.games(filter.gameId);
if(filter.authorId !== undefined)
  url.author(filter.authorId);
Согласен, что возможны ситуации когда действительно нужно динамически строить запросы. Правда на моей памяти это всегда было плохой идеей (приложения клиенты чаще всего выполняют одни и те же запросы, что в свою очередь дает возможность серверу оптимизировать их выполнение), но все-таки еще раз скажу что пожалуй соглашусь с вами — если хотим делать динамические запросы — то некоторое удобство от библиотеки, которая динамически строит запросы будут.

Вопрос нужна ли эта библиотека в остальных случаях, когда запросы очень даже статичны?
Выглядит очень приятно в сравнении с другими rest клиентами, которыми приходилось пользоваться. Спасибо.
Как вот такой код будет выглядеть на XHR? (Кстати, внутри библиотеки именно он и используется).
var me = api.humans('me');
var i = await me.get();
console.log(i);    //just object, i.e. {id: 1, name: 'Amareis', profession: 'programmer'}
var post = await me.posts.post({site: 'habrahabr.ru', nick: i.name})
console.log(post);  //object
Я могу прорезюмировать лишь, что ее оправдано использовать если количество кода написаного в помощью XHR больше, в противном случае ради оптимизации лучше XHR; Чем меньше тем лучше, короче.
Оптимизации чего? Все подобные решения это всего лишь обертки над XHR, а они не сильно ресурсоемки.

Проблема в том, что каждый раз создавать и инициализировать XHR вручную — глупо. Наверняка вы напишете для этого некую обёртку. И тут, сюрприз-сюрприз, вы обнаружите что сами изобрели ещё один Rest API client :)
В общем-то another-rest-client именно так и появился на свет, я это и в статье описал.

Используя XHR Вы ведь всё равно обернёте вызов в код, который вернёт Вам промис, а это и будет практически эта самая библиотека, так зачем писать по-новой один и тот же велосипед в каждом проекте?
К примеру есть Лендинг на котором дергается всего 2-3 запроса, и ради этого использовать библиотеку? Серьезно?

Я, например, дольше буду вспоминать весь workflow XHR, чем клиенты суммарно времени потеряют, подтягивая несчастные 4 килобайта, которые весит минифицированный another-rest-client :)

Почему нет? Зачем мне снова писать точно такую же обёртку в каждом проекте?
ну подтягиваются к примеру отзывы пользователей о сервисе, реальные, и еще что то. Вполне нормально.
Ну если у вас в API один метод вида site.ru/api/comments то конечно подключать зависимость для вызова было бы странно. Не столько из за производительности, сколько из здравого смысла, ибо подключение зависимости у вас займет больше времени, чем написания велосипеда на XHR.

В таком случае можно использовать window.fetch и его полифил. Самый жирный плюс такого решения — это стандарт. И когда браузеры станут нормально его поддерживать, можно безболезненно убрать полифил.

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

Пока лично для меня ценность библиотеки как-то сомнительна, поскольку ни удобства использования, ни какой-то замечательной функциональности не вижу, но в любом случае библиотека, может, и неплоха

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


Описываем хранилище моделей:


define([
    "dojo/store/JsonRest"
], function(JsonRest){
    return new JsonRest({
        target: "/api/users/",
        _getTarget: function(id){
            if (typeof id !== "undefined") {
                return this.target + id + "/"; // В Django принято ставить / в конце адреса
            }
            return this.target;
        }
    });
});

Где-то в коде:


require([
    "dojo",
    "stores/users"
], function(
    dojo,
    users
) {
     // Получить список всех пользователей
    users.query({
        // Пустой объект - получить всё, а можно же и с параметрами
    });

    // Получить запись с id = 12
    users.get(12).then(
        // Тут всё стандартно - обработчики promise
        // Однако, dojo не терпит пустоты, поэтому на обработчик ошибки ставьте
        // dojo.noop, если писать свой лениво
    );

    // Обновление с перезаписью (можно и PATCH сделать, но это, мягко говоря, не для новичков)
    users.put(userModel).then(function(updatedUser){
        console.log(updatedUser);
    }, dojo.noop);

    // Удаление записи
    users.delete(5).then();
});

Официальная документация.


Если кому-то интересно, специально для Django есть несколько строк кода, позволяющих в каждый запрос вставлять CSRF-токены.

«Настоящий» REST не предполагает конструирование клиентом параметрических урлов вообще, кроме одного, указывающего на точку входа API. Все остальные урлы предоставляются сервером клиенту в самих ресурсах, это называется HATEOAS. (Понятно, что библиотека разрабатывалась для общения с «неправильными» сервисами, добавил комментарий лишь для академической полноты.)
просто оставлю это здесь

https://learn.javascript.ru/fetch

'use strict';

fetch('/article/fetch/user.json')
.then(function(response) {
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
alert(response.status); // 200

return response.json();
})
.then(function(user) {
alert(user.name); // iliakan
})
.catch( alert );

нативно, просто и быстро, без дурацких велосипедов

fetch это скорее замена велосипедов вокруг XMLHttpRequest, чем реализация REST.

>В самом начале своего существования restful.js была очень похожа на первые версии another-rest-client. Потом, видимо, скатилась в энтерпрайзщину.

А почему не был исследован этот вопрос? Почему ограничились предположением о том, что какой-то дядя «энтерпрайз» пришёл и сделал из хорошей и удобной библиотеки плохую и неудобную?

Может, библиотека развивалась от неудобного к удобному? Может, если пилить велосипед дальше, будет пройден ровно тот же самый путь?
Only those users with full accounts are able to leave comments. Log in, please.