Статьи

React.js и Spring Data REST: часть 2

В предыдущем сеансе вы узнали, как настроить внутреннюю службу расчета заработной платы для хранения данных о сотрудниках с помощью Spring Data REST. Ключевой особенностью, которой не хватало, было использование элементов управления гипермедиа и навигации по ссылкам. Вместо этого он жестко закодировал путь для поиска данных.

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

В начале были данные … а затем был REST.


Я разочарован количеством людей, которые называют любой интерфейс на основе HTTP REST API.
Сегодняшний пример — API REST SocialSite. Это RPC. Это кричит RPC…. Что нужно сделать, чтобы сделать архитектурный стиль REST понятным для понятия, что гипертекст является ограничением? Другими словами, если механизм состояния приложения (и, следовательно, API) не управляется гипертекстом, то он не может быть RESTful и не может быть REST API. Период. Есть ли где-нибудь сломанное руководство, которое нужно починить?

Рой Т. Филдинг

Итак, что же такое элементы управления гипермедией, т.е. гипертекст, и как вы можете их использовать? Чтобы выяснить это, давайте сделаем шаг назад и рассмотрим основную миссию REST.

Концепция REST заключалась в том, чтобы заимствовать идеи, которые сделали Интернет настолько успешными, и применять их к API. Несмотря на огромные размеры, динамичность и низкую скорость обновления клиентов, т. Е. Браузеров, сеть имеет огромный успех. Рой Филдинг стремился использовать некоторые из его ограничений и возможностей и посмотреть, даст ли это аналогичное расширение производства и потребления API.

Одним из ограничений является ограничение количества глаголов. Для REST основными являются GET, POST, PUT, DELETE и PATCH. Есть и другие, но мы не будем вдаваться в них здесь.

  • GET — получить состояние ресурса без изменения системы
  • POST — создать новый ресурс, не говоря где
  • PUT — заменить существующий ресурс, перезаписав все, что уже есть (если есть)
  • DELETE — удалить существующий ресурс
  • PATCH — частично изменить существующий ресурс

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

Еще одним ограничением REST является использование типов медиа для определения формата данных. Вместо того, чтобы каждый писал свой собственный диалект для обмена информацией, было бы разумно разработать некоторые типы медиа. Одним из наиболее популярных из них является HAL, приложение медиа-типа / hal + json. Это тип данных Spring Data REST по умолчанию. Особое значение имеет то, что для REST не существует единого централизованного типа носителя. Вместо этого люди могут разрабатывать медиа-типы и подключать их. Попробуйте их. По мере появления различных потребностей отрасль может гибко двигаться.

Ключевой особенностью REST является включение ссылок на соответствующие ресурсы. Например, если вы просматриваете заказ, API RESTful будет содержать ссылку на связанного клиента, ссылки на каталог товаров и, возможно, ссылку на магазин, из которого был сделан заказ. На этом занятии вы познакомитесь с пейджингом и узнаете, как использовать навигационные пейджинговые ссылки.

Включение подкачки из бэкэнда

Чтобы начать использовать средства управления гипермедиа внешнего интерфейса, вам нужно включить некоторые дополнительные элементы управления. Spring Data REST обеспечивает поддержку подкачки. Чтобы использовать его, просто настройте определение репозитория:

SRC / главная / Java / COM / greglturnquist / начисления заработной платы / EmployeeRepository.java

public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {

}

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

Перезапустите приложение ( ./mvnw spring-boot:run) и посмотрите, как оно работает.

$ curl localhost:8080/api/employees?size=2
{
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "self" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "next" : {
      "href" : "http://localhost:8080/api/employees?page=1&size=2"
    },
    "last" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    }
  },
  "_embedded" : {
    "employees" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "description" : "ring bearer",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/1"
        }
      }
    }, {
      "firstName" : "Bilbo",
      "lastName" : "Baggins",
      "description" : "burglar",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/2"
        }
      }
    } ]
  },
  "page" : {
    "size" : 2,
    "totalElements" : 6,
    "totalPages" : 3,
    "number" : 0
  }
}

Размер страницы по умолчанию составляет 20, поэтому, чтобы увидеть его в действии, ?size=2применяется. Как и ожидалось, в списке только два сотрудника. Кроме того, есть также первая , следующая и последняя ссылка. Существует также ссылка на себя , свободная от контекста, включая параметры страницы .

Если вы перейдете к следующей ссылке, вы также увидите предыдущую ссылку:

$ curl "http://localhost:8080/api/employees?page=1&size=2"
{
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "prev" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "self" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "next" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    },
    "last" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    }
  },
...

Запись При использовании «&» в параметрах URL-запроса командная строка считает, что это разрыв строки. Оберните весь URL кавычками, чтобы обойти это.

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

Навигация по отношениям

Это оно! Больше никаких изменений в бэкэнде не требуется, чтобы начать использовать элементы управления гипермедиа, которые Spring Data REST предоставляет «из коробки». Вы можете переключиться на работу на веб-интерфейсе. (Это часть красоты Spring Data REST. Никаких грязных обновлений контроллера!)

Запись Важно отметить, что это приложение не «Spring Data REST-специфично». Вместо этого он использует HAL , шаблоны URI и другие стандарты. Вот как использовать rest.js совсем несложно: эта библиотека поставляется с поддержкой HAL.

В предыдущем сеансе вы жестко закодировали путь к /api/employees. Вместо этого ЕДИНСТВЕННЫЙ путь, который вы должны жестко закодировать, является корневым

...
var root = '/api';
...

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

componentDidMount: function () {
    this.loadFromServer(this.state.pageSize);
},

В предыдущем сеансе загрузка производилась прямо изнутри componentDidMount(). В этом сеансе мы даем возможность перезагрузить весь список сотрудников при обновлении размера страницы. Для этого мы перенесли вещи в loadFromServer().

loadFromServer: function (pageSize) {
    follow(client, root, [
        {rel: 'employees', params: {size: pageSize}}]
    ).then(employeeCollection => {
        return client({
            method: 'GET',
            path: employeeCollection.entity._links.profile.href,
            headers: {'Accept': 'application/schema+json'}
        }).then(schema => {
            this.schema = schema.entity;
            return employeeCollection;
        });
    }).done(employeeCollection => {
        this.setState({
            employees: employeeCollection.entity._embedded.employees,
            attributes: Object.keys(this.schema.properties),
            pageSize: pageSize,
            links: employeeCollection.entity._links});
    });
},

loadFromServerочень похож на предыдущий сеанс, но вместо этого использует follow():

  • Первым аргументом функции follow () является clientобъект, используемый для выполнения вызовов REST.
  • Второй аргумент — это корневой URI, с которого нужно начинать.
  • Третий аргумент — это массив отношений для навигации. Каждый из них может быть строкой или объектом.

Массив отношений может быть таким простым ["employees"], что означает, что при первом вызове ищите в _links отношения (или rel ) именованных сотрудников . Найдите его раздел и перейдите к нему. Если в массиве есть другое отношение, промойте и повторите.

Иногда само по себе не достаточно. В этом фрагменте кода он также включает параметр запроса ? Size = <pageSize> . Есть и другие варианты, которые можно поставить, как вы увидите дальше.

Получение метаданных схемы JSON

После перехода к сотрудникам с запросом на основе размера employeeCollection у вас под рукой. В предыдущем сеансе мы называли это днем ​​и отображали эти данные внутри <EmployeeList />. Сегодня вы выполняете еще один вызов для получения метаданных схемы JSON, найденных a /api/profile/employees.

Вы можете увидеть данные самостоятельно:

$ curl http://localhost:8080/api/profile/employees -H 'Accept:application/schema+json'
{
  "title" : "Employee",
  "properties" : {
    "firstName" : {
      "title" : "First name",
      "readOnly" : false,
      "type" : "string"
    },
    "lastName" : {
      "title" : "Last name",
      "readOnly" : false,
      "type" : "string"
    },
    "description" : {
      "title" : "Description",
      "readOnly" : false,
      "type" : "string"
    }
  },
  "definitions" : { },
  "type" : "object",
  "$schema" : "http://json-schema.org/draft-04/schema#"
}

Запись Форма метаданных по умолчанию в / profile / employee по умолчанию — ALPS. В этом случае, однако, вы используете контентную привязку для получения схемы JSON.

Захватив эту информацию в состоянии компонента `<App />`, вы сможете использовать ее позже при создании форм ввода.

Создание новых записей

Благодаря этим метаданным вы можете добавить некоторые дополнительные элементы управления в пользовательский интерфейс. Создайте новый компонент React <CreateDialog />.

var CreateDialog = React.createClass({

    handleSubmit: function (e) {
        e.preventDefault();
        var newEmployee = {};
        this.props.attributes.forEach(attribute => {
            newEmployee[attribute] = React.findDOMNode(this.refs[attribute]).value.trim();
        });
        this.props.onCreate(newEmployee);

        // clear out the dialog's inputs
        this.props.attributes.forEach(attribute => {
            React.findDOMNode(this.refs[attribute]).value = '';
        });

        // Navigate away from the dialog to hide it.
        window.location = "#";
    },

    render: function () {
        var inputs = this.props.attributes.map(attribute =>
            <p key={attribute}>
                <input type="text" placeholder={attribute} ref={attribute} className="field" />
            </p>
        );

        return (
            <div>
                <a href="#createEmployee">Create</a>

                <div id="createEmployee" className="modalDialog">
                    <div>
                        <a href="#" title="Close" className="close">X</a>

                        <h2>Create new employee</h2>

                        <form>
                            {inputs}
                            <button onClick={this.handleSubmit}>Create</button>
                        </form>
                    </div>
                </div>
            </div>
        )
    }

});

Этот новый компонент имеет как handleSubmit()функцию, так и ожидаемую render()функцию.

Давайте углубимся в эти функции в обратном порядке и сначала посмотрим на render()функцию.

оказание

Ваш код отображает данные схемы JSON, найденные в свойстве атрибутов, и преобразует их в массив <p><input></p>элементов.

  • ключ снова необходим Реагировать различать между несколькими дочерними узлами.
  • Это простое текстовое поле ввода.
  • Заполнитель — это то, где мы можем показать пользователя с полем.
  • Возможно, вы привыкли иметь атрибут name , но это не обязательно. В React ref — это механизм для захвата определенного узла DOM (как вы скоро увидите).

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

Внутри верхнего уровня этого компонента <div>есть тег привязки и другой <div>. Якорный тег — это кнопка для открытия диалога. И вложенным <div>является сам скрытый диалог. В этом примере вы используете чистый HTML5 и CSS3. Нет JavaScript вообще! Вы можете увидеть код CSS, используемый для отображения / скрытия диалога. Мы не будем погружаться в это здесь.

Внутри <div id="createEmployee">находится форма, в которую вводится динамический список полей ввода, за которым следует кнопка « Создать» . Эта кнопка имеет onClick={this.handleSubmit}обработчик событий. Это способ React регистрации обработчика событий.

Запись React не создает несколько обработчиков событий для каждого элемента DOM. Вместо этого у него есть намного более производительное и сложное решение. Дело в том, что вам не нужно управлять этой инфраструктурой, и вместо этого вы можете сосредоточиться на написании функционального кода.

Обработка пользовательского ввода

handleSubmit()Функция сначала останавливает событие из барботажных дальше вверх по иерархии. Затем он использует то же свойство атрибута JSON Schema, чтобы найти каждое <input>из них React.findDOMNode(this.refs[attribute]).

this.refsэто способ связаться с конкретным компонентом React по имени. В этом смысле вы получаете ТОЛЬКО виртуальный компонент DOM. Чтобы получить фактический элемент DOM, вам нужно использовать React.findDOMNode().

После перебора каждого ввода и создания newEmployeeобъекта мы вызываем обратный вызов onCreate()новому сотруднику. Эта функция находится внутри App.onCreateи была предоставлена ​​этому компоненту React как другое свойство. Посмотрите, как работает эта функция верхнего уровня:

onCreate: function (newEmployee) {
    follow(client, root, ['employees']).then(employeeCollection => {
        return client({
            method: 'POST',
            path: employeeCollection.entity._links.self.href,
            entity: newEmployee,
            headers: {'Content-Type': 'application/json'}
        })
    }).then(response => {
        return follow(client, root, [
            {rel: 'employees', params: {'size': this.state.pageSize}}]);
    }).done(response => {
        this.onNavigate(response.entity._links.last.href);
    });
},

Еще раз, используйте follow()функцию, чтобы перейти к ресурсу сотрудников, где выполняются операции POST. В этом случае не было необходимости применять какие-либо параметры, так что строковый массив rels в порядке. В этой ситуации вызов POST возвращается. Это позволяет следующему then()предложению обрабатывать результаты POST.

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

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

Это вводит понятие подкачки в нашем пользовательском интерфейсе. Давайте займемся этим дальше!

Первый раз, используя API на основе обещаний? Обещания — это способ запустить асинхронные операции, а затем зарегистрировать функцию, которая отвечает, когда задача выполнена. Обещания предназначены для объединения воедино, чтобы избежать «ада обратного вызова». Посмотрите на следующий поток:

when.promise(async_func_call())
.then(function(results) {
/* process the outcome of async_func_call */
})
.then(function(more_results) {
/* process the previous then() return value */
})
.done(function(yet_more) {
/* process the previous then() and wrap things up */
});

Для более подробной информации, ознакомьтесь с этим учебником по обещаниям .

Секрет, который следует помнить с обещаниями, заключается в том, что then()функциям необходимо что-то возвращать, будь то значение или другое обещание. done()функции ничего не возвращают, и вы ничего не цепляете после этого. В случае, если вы еще не заметили client(что является экземпляром restиз rest.js), а также followфункцию, возвращающую обещания.

Пейджинг через данные

Вы настроили пейджинг на бэкэнде и уже начали использовать его при создании новых сотрудников.

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

Сначала давайте проверим, какую onNavigate()функцию вы использовали.

onNavigate: function(navUri) {
    client({method: 'GET', path: navUri}).done(employeeCollection => {
        this.setState({
            employees: employeeCollection.entity._embedded.employees,
            attributes: this.state.attributes,
            pageSize: this.state.pageSize,
            links: employeeCollection.entity._links
        });
    });
},

Это определяется сверху, внутри App.onNavigate. Опять же, это позволяет управлять состоянием пользовательского интерфейса в верхнем компоненте. После перехода onNavigate()к <EmployeeList />компоненту React кодируются следующие обработчики для обработки нажатия на некоторые кнопки:

handleNavFirst: function(e){
    e.preventDefault();
    this.props.onNavigate(this.props.links.first.href);
},
handleNavPrev: function(e) {
    e.preventDefault();
    this.props.onNavigate(this.props.links.prev.href);
},
handleNavNext: function(e) {
    e.preventDefault();
    this.props.onNavigate(this.props.links.next.href);
},
handleNavLast: function(e) {
    e.preventDefault();
    this.props.onNavigate(this.props.links.last.href);
},

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

Теперь условно отобразите элементы управления, основанные на том, какие ссылки появляются в гиперссылках EmployeeList.render:

render: function () {
    var employees = this.props.employees.map(employee =>
        <Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
    );

    var navLinks = [];
    if ("first" in this.props.links) {
        navLinks.push(<button key="first" onClick={this.handleNavFirst}>&lt;&lt;</button>);
    }
    if ("prev" in this.props.links) {
        navLinks.push(<button key="prev" onClick={this.handleNavPrev}>&lt;</button>);
    }
    if ("next" in this.props.links) {
        navLinks.push(<button key="next" onClick={this.handleNavNext}>&gt;</button>);
    }
    if ("last" in this.props.links) {
        navLinks.push(<button key="last" onClick={this.handleNavLast}>&gt;&gt;</button>);
    }

    return (
        <div>
            <input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
            <table>
                <tr>
                    <th>First Name</th>
                    <th>Last Name</th>
                    <th>Description</th>
                    <th></th>
                </tr>
                {employees}
            </table>
            <div>
                {navLinks}
            </div>
        </div>
    )
}

Как и в предыдущем сеансе, он все еще превращается this.props.employeesв массив <Element />компонентов. Затем он создает массив navLinks, массив кнопок HTML.

Запись Поскольку React основан на XML, вы не можете поместить «<» внутрь <button>элемента. Вместо этого вы должны использовать закодированную версию.

Затем вы можете увидеть {navLinks}вставленный внизу возвращенный HTML.

Удаление существующих записей

Удаление записей намного проще. Возьмите его запись на основе HAL и примените DELETE к ее собственной ссылке.

var Employee = React.createClass({
    handleDelete: function () {
        this.props.onDelete(this.props.employee);
    },
    render: function () {
        return (
            <tr>
                <td>{this.props.employee.firstName}</td>
                <td>{this.props.employee.lastName}</td>
                <td>{this.props.employee.description}</td>
                <td>
                    <button onClick={this.handleDelete}>Delete</button>
                </td>
            </tr>
        )
    }
})

Эта обновленная версия компонента Employee показывает дополнительную запись в конце строки, кнопку удаления. Он зарегистрирован для вызова this.handleDeleteпри нажатии. Затем handleDelete()функция может вызвать обратный вызов, переданный при подаче контекстно важной this.props.employeeзаписи.

Важный Это еще раз показывает, что проще всего управлять состоянием в верхнем компоненте в одном месте. Это может не всегда иметь место, но часто управление состоянием в одном месте облегчает работу и делает ее проще и проще. Вызывая обратный вызов со специфичными для компонента деталями ( this.props.onDelete(this.props.employee)), очень легко организовать поведение между компонентами.

Проследив onDelete()функцию до вершины App.onDelete, вы можете увидеть, как она работает:

onDelete: function (employee) {
    client({method: 'DELETE', path: employee._links.self.href}).done(response => {
        this.loadFromServer(this.state.pageSize);
    });
},

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

Если вы удаляете последнюю запись на последней странице, она перейдет на первую страницу.

Регулировка размера страницы

Один из способов увидеть, как действительно светит гипермедиа, — обновить размер страницы. Spring Data REST плавно обновляет навигационные ссылки в зависимости от размера страницы.

В верхней части ElementList.render: есть элемент HTML <input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>.

  • ref="pageSize" позволяет легко получить этот элемент через this.refs.pageSize.
  • defaultValueинициализирует его с помощью pageSize состояния .
  • onInput регистрирует обработчик, как показано ниже.

handleInput: function (e) {
    e.preventDefault();
    var pageSize = React.findDOMNode(this.refs.pageSize).value;
    if (/^[0-9]+$/.test(pageSize)) {
        this.props.updatePageSize(pageSize);
    } else {
        React.findDOMNode(this.refs.pageSize).value =
            pageSize.substring(0, pageSize.length - 1);
    }
},

Он останавливает всплеск события. Затем он использует атрибут ref<input> для поиска узла DOM и извлечения его значения, все через findDOMNode()вспомогательную функцию React . Он проверяет, является ли ввод действительно числом, проверяя, является ли это строкой цифр. Если это так, он вызывает обратный вызов, отправляя новый размер страницы в Appкомпонент React. Если нет, только что введенный символ удаляется с ввода.

Что Appделать, когда он получает updatePageSize()? Проверьте это:

updatePageSize: function (pageSize) {
    if (pageSize !== this.state.pageSize) {
        this.loadFromServer(pageSize);
    }
},

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

Собираем все вместе

Со всеми этими приятными дополнениями у вас теперь действительно улучшенный пользовательский интерфейс.

гипермедиа 1

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

Внизу вы можете увидеть CreateDialogметаданные, вставленные в заполнители ввода HTML.

гипермедиа 2

Это действительно показывает мощь использования гипермедиа в сочетании с доменными метаданными (схема JSON). Веб-страница не должна знать, какое поле какое. Вместо этого пользователь может видеть его и знать, как его использовать. Если вы добавили другое поле к Employeeобъекту домена, это всплывающее окно автоматически отобразит его.

Рассмотрение

В этой сессии:

  • Вы включили функцию подкачки Spring Data REST.
  • Вы выбросили жестко закодированные пути URI и начали использовать корневой URI в сочетании с именами отношений или «rels».
  • Вы обновили пользовательский интерфейс, чтобы динамически использовать элементы управления гипермедиа на основе страниц.
  • Вы добавили возможность создавать и удалять сотрудников и обновлять пользовательский интерфейс по мере необходимости.
  • Вы сделали возможным изменение размера страницы и гибкий интерфейс.

Вопросы?

Вы сделали веб-страницу динамичной. Но откройте другую вкладку браузера и укажите ее в том же приложении. Изменения в одной вкладке ничего не обновят в другой.

Это то, что мы можем рассмотреть на следующей сессии. До тех пор, счастливого кодирования!

Учебник Грега Тернквиста.