Эта статья была обновлена 31 января 2019 года, чтобы отреагировать на отзывы читателей. Автор добавил возможности пользовательского запроса в API, основанный на компонентах, и описал, как он работает.
API — это канал связи для приложения для загрузки данных с сервера. В мире AA, REST была более установленной методологии, но в последнее время была омрачена Graph’L, который предлагает важные преимущества по сравнению с REST. В то время как REST требует нескольких запросов HTTP для получения набора данных для визуализации компонента, Graph’L может запрашивать и извлекать такие данные в одном запросе, и ответ будет именно тем, что требуется, без более или недостаточно йога данных, как это обычно происходит в REST.
В этой статье я опишу другой способ извлечения данных, которые я разработал и назвал «PoP» (и с открытым исходным кодом здесь), который расширяет идею извлечения данных для нескольких объектов в одном запросе, представленном Graph’L и принимает его на шаг дальше , т.е. в то время как REST получает данные для одного ресурса, а Граф-Л получает данные для всех ресурсов в одном компоненте, API на основе компонентов может получить данные для всех ресурсов из всех компонентов на одной странице.
Использование API на основе компонентов имеет наибольший смысл, когда веб-сайт построен с использованием компонентов, т.е. когда веб-страница итеративно состоит из компонентов, обертывания других компонентов, пока, в самом верху, мы не получим один компонент, представляющий страницу. Например, веб-страница, показанная на рисунке ниже, построена с компонентами, которые изложены с квадратами:
API, основанный на компонентах, может сделать один запрос на сервер, запрашивая данные для всех ресурсов в каждом компоненте (а также для всех компонентов на странице), что достигается путем сохранения взаимосвязи между компонентами API структуры себя.
Среди прочего, эта структура предлагает следующие несколько преимуществ:
- Страница с большим количеством компонентов вызовет только один запрос вместо многих;
- Данные, общие между компонентами, могут быть получены только один раз из DB и напечатаны только один раз в ответ;
- Это может значительно уменьшить — даже полностью удалить — необходимость в хранилище данных.
Мы рассмотрим их подробно на протяжении всей статьи, но сначала давайте рассмотрим, какие компоненты на самом деле и как мы можем построить сайт на основе таких компонентов, и, наконец, изучить, как работает API на основе компонентов.
Рекомендуемое чтение: Граф-Л Праймер: Почему нам нужен новый вид API
Строительство сайта через компоненты
Компонент — это просто набор фрагментов кода HTML, JavaScript и CSS, составленных для создания автономной сущности. Это может затем обернуть другие компоненты для создания более сложных структур, и быть сам обернут другими компонентами, тоже. Компонент имеет цель, которая может варьироваться от чего-то очень простого (например, ссылка или кнопка) к чему-то очень сложному (например, карусель или перетаскивание изображения загрузчик). Компоненты наиболее полезны, когда они являются общими и позволяют настройки через вводили свойства (или «проп»), так что они могут служить широкий спектр случаев использования. В крайнем случае, сам сайт становится компонентом.
Термин «компонент» часто используется для обозначения как функциональности, так и дизайна. Например, что касается функциональности, то платформы JavaScript, такие как React или Vue, позволяют создавать компоненты на стороне клиента, которые способны самостоятельно редиверцизировать (например, после того, как API получает необходимые данные) и используют реквизит для установки значения конфигурации на их завернутых компонентах, что позволяет повторное использование кода. Что касается дизайна, Bootstrap стандартизировал, как веб-сайты выглядят и чувствуют себя через его передний конец компонента библиотеки, и это стало здоровой тенденцией для команд для создания систем дизайна для поддержания своих веб-сайтов, что позволяет различным членам команды ( дизайнеры и разработчики, а также маркетологи и продавцы), чтобы говорить на едином языке и выразить последовательную идентичность.
Компонентизация сайта, то это очень разумный способ сделать веб-сайт стал более обслуживаемым. Сайты, использующие платформы JavaScript, такие как React и Vue, уже основаны на компонентах (по крайней мере, на стороне клиента). Использование библиотеки компонентов, как Bootstrap не обязательно сделать сайт компонентной основе (это может быть большой капли HTML), однако, он включает в себя концепцию многоразовых элементов для пользовательского интерфейса.
Если сайт представляет собой большую каплю HTML, для нас, чтобы компонентизировать его мы должны разорвать макет в ряд повторяющихся моделей, для которых мы должны определить и каталог разделов на странице на основе их сходства функциональности и стилей, и разорвать эти разделы вниз в слои, как гранулированные, насколько это возможно, пытаясь, чтобы каждый слой был сосредоточен на одной цели или действий, а также пытается сопоставить общие слои в различных разделах.
Примечание: Брэд Фрост в «Атомный дизайн» является большой методологии для выявления этих общих моделей и создания многоразовой системы проектирования.
Таким образом, создание сайта через компоненты сродни игре с LEGO. Каждый компонент является либо атомной функциональностью, составом других компонентов, либо комбинацией этих двух компонентов.
Как показано ниже, основной компонент (аватар) является итеративно составленным другими компонентами до получения веб-страницы в верхней части:
Спецификация API на основе компонентов
Для разработанного мною API на основе компонентов компонент называется «модуль», поэтому отныне термины «компонент» и «модуль» используются взаимозаменяемы.
Связь всех модулей, оборачивающих друг друга, от самого верхнего модуля до последнего уровня, называется «иерархией компонентов». Эта взаимосвязь может быть выражена через ассоциативный массив (массив ключа, который находится на стороне сервера, в котором каждый модуль указывает свое имя в качестве ключевого атрибута и его внутренние модули под свойством modules
. Затем API просто кодирует этот массив как объект JSON для потребления:
// Component hierarchy on server-side, e.g. through PHP:
[
"top-module" => [
"modules" => [
"module-level1" => [
"modules" => [
"module-level11" => [
"modules" => [...]
],
"module-level12" => [
"modules" => [
"module-level121" => [
"modules" => [...]
]
]
]
]
],
"module-level2" => [
"modules" => [
"module-level21" => [
"modules" => [...]
]
]
]
]
]
]
// Component hierarchy encoded as JSON:
{
"top-module": {
modules: {
"module-level1": {
modules: {
"module-level11": {
...
},
"module-level12": {
modules: {
"module-level121": {
...
}
}
}
}
},
"module-level2": {
modules: {
"module-level21": {
...
}
}
}
}
}
}
Взаимосвязь между модулями определяется строго сверху вниз: модуль обертывает другие модули и знает, кто они, но он не знает — и не заботится — какие модули его обертывания.
Например, в коде JSON выше, модуль module-level1
знает, что обертывания модулей module-level11
module-level12
и, транзитно, он также знает, что обертывания; module-level121
но модуль не module-level11
волнует, кто обертывания его, следовательно, не знает module-level1
.
Имея структуру на основе компонентов, теперь мы можем добавить фактическую информацию, требуемую каждым модулем, которая классифицируется в любой из настроек (например, значения конфигурации и другие свойства) и данные (например, идентифицировать запрашиваемые объекты базы данных и другие свойства), и размещены соответственно под входы modulesettings
и moduledata
:
{
modulesettings: {
"top-module": {
configuration: {...},
...,
modules: {
"module-level1": {
configuration: {...},
...,
modules: {
"module-level11": {
repeat...
},
"module-level12": {
configuration: {...},
...,
modules: {
"module-level121": {
repeat...
}
}
}
}
},
"module-level2": {
configuration: {...},
...,
modules: {
"module-level21": {
repeat...
}
}
}
}
}
},
moduledata: {
"top-module": {
dbobjectids: [...],
...,
modules: {
"module-level1": {
dbobjectids: [...],
...,
modules: {
"module-level11": {
repeat...
},
"module-level12": {
dbobjectids: [...],
...,
modules: {
"module-level121": {
repeat...
}
}
}
}
},
"module-level2": {
dbobjectids: [...],
...,
modules: {
"module-level21": {
repeat...
}
}
}
}
}
}
}
После этого API добавит данные объекта базы данных. Эта информация размещается не под каждым модулем, а под общим разделом databases
под названием, чтобы избежать дублирования информации, когда два или более различных модулей извлекают одни и те же объекты из базы данных.
Кроме того, API представляет данные объекта базы данных в реляционном порядке, чтобы избежать дублирования информации, когда два или более различных объектов базы данных связаны с общим объектом (например, две должности с одним и тем же автором). Другими словами, данные объектов базы данных базы данных нормализуется.
Рекомендуемое чтение: Создание безсерверной контактной формы для вашего статического сайта
Структура представляет собой словарь, организованный под каждым первым типом объекта и второй идентификатор объекта, из которого мы можем получить свойства объекта:
{
databases: {
primary: {
dbobject_type: {
dbobject_id: {
property: ...,
...
},
...
},
...
}
}
}
Этот объект JSON уже является ответом API, основанного на компонентах. Его формат является спецификацией сам по себе: до тех пор, как сервер возвращает ответ JSON в требуемом формате, клиент может потреблять API независимо от того, как она реализуется. Таким образом, API может быть реализован на любом языке (который является одной из красот Graph’L: будучи спецификацией, а не фактической реализации позволило ему стать доступным в множеству языков.)
Примечание: В предстоящей статье я опишу свою реализацию API на основе компонентов в PHP (который доступен в репо).
Пример ответа API
Например, приведенный ниже ответ API содержит иерархию компонентов с двумя модулями, page
где модуль получает сообщения в post-feed
post-feed
блоге. Пожалуйста, обратите внимание на следующее:
- Каждый модуль знает, какие из них запрашиваемые объекты из свойств
dbobjectids
(идентиповцев4
и9
для записей в блоге) - Каждый модуль знает тип объекта для своих запрашиваемых объектов из свойства
dbkeys
(данные каждого поста находятсяposts
под, и данные автора поста, соответствующие автору с идентификатором, приведенным под свойством столбаauthor
, находитсяusers
под) - Поскольку данные объекта базы данных базы данных являются реляционными, свойство
author
содержит идентификатор объекту автора вместо того, чтобы печатать данные автора напрямую.
{
moduledata: {
"page": {
modules: {
"post-feed": {
dbobjectids: [4, 9]
}
}
}
},
modulesettings: {
"page": {
modules: {
"post-feed": {
dbkeys: {
id: "posts",
author: "users"
}
}
}
}
},
databases: {
primary: {
posts: {
4: {
title: "Hello World!",
author: 7
},
9: {
title: "Everything fine?",
author: 7
}
},
users: {
7: {
name: "Leo"
}
}
}
}
}
Различия, извлеченные из данных на основе ресурсов, основанных на схемах и компонентных AIS
Давайте посмотрим, как aPI, основанный на компонентах, такой как PoP, сравнивает при получении данных с таким ресурсным API, как REST, и с API, основанным на схемах, таким как Graph’L.
Допустим, IMDB имеет страницу с двумя компонентами, которые необходимо получить данные: «Featured режиссер» (показывая описание Джорджа Лукаса и список его фильмов) и «Фильмы рекомендуется для вас» (показ фильмов, таких как «Звездные войны: Эпизод I — Призрачная угроза и Терминатор). Это может выглядеть следующим образом:
Давайте посмотрим, сколько запросов необходимо для получения данных через каждый метод API. Для этого примера, «Рекомендуемый режиссер» компонент приносит один результат («Джордж Лукас»), из которого он получает два фильма(Звездные войны: Эпизод I — Призрачная угроза и «Звездные войны: Эпизод II — Атака клонов«), и для каждого фильма два актера (» Эван Макгрегор» и «Натали Портман» для первого фильма, и «Натали Портман» и «Хайден Кристенсен» для второго фильма). Компонент «Фильмы рекомендуется для вас» приносит два результата(Звездные войны: Эпизод I — Призрачная угроза и Терминатор), а затем получает их директоров («Джордж Лукас» и «Джеймс Кэмерон» соответственно).
Используя REST для featured-director
визуализации компонента, нам могут понадобиться следующие 7 запросов (это число может варьироваться в зависимости от того, сколько данных предоставляется каждой конечной точкой, т.е. сколько переизбыток было реализовано):
GET - /featured-director
GET - /directors/george-lucas
GET - /films/the-phantom-menace
GET - /films/attack-of-the-clones
GET - /actors/ewan-mcgregor
GET - /actors/natalie-portman
GET - /actors/hayden-christensen
График позволяет, через сильно набранные схемы, получить все необходимые данные в одном запросе на компонент. Запрос на получение данных через Graph’L для компонента выглядит следующим образом featuredDirector
(после того, как мы реализовали соответствующую схему):
query {
featuredDirector {
name
country
avatar
films {
title
thumbnail
actors {
name
avatar
}
}
}
}
И он производит следующий ответ:
{
data: {
featuredDirector: {
name: "George Lucas",
country: "USA",
avatar: "...",
films: [
{
title: "Star Wars: Episode I - The Phantom Menace",
thumbnail: "...",
actors: [
{
name: "Ewan McGregor",
avatar: "...",
},
{
name: "Natalie Portman",
avatar: "...",
}
]
},
{
title: "Star Wars: Episode II - Attack of the Clones",
thumbnail: "...",
actors: [
{
name: "Natalie Portman",
avatar: "...",
},
{
name: "Hayden Christensen",
avatar: "...",
}
]
}
]
}
}
}
А запрос на компонент «Фильмы, рекомендованные для вас» дает следующий ответ:
{
data: {
films: [
{
title: "Star Wars: Episode I - The Phantom Menace",
thumbnail: "...",
director: {
name: "George Lucas",
avatar: "...",
}
},
{
title: "The Terminator",
thumbnail: "...",
director: {
name: "James Cameron",
avatar: "...",
}
}
]
}
}
PoP будет выдавать только один запрос, чтобы получить все данные для всех компонентов на странице, и нормализовать результаты. Конечная точка, которая будет называться просто такой же, как URL, для которого мы должны получить данные, просто добавив дополнительный параметр, output=json
чтобы указать, чтобы привести данные в формате JSON вместо печати его как HTML:
GET - /url-of-the-page/?output=json
Предполагая, что структура модуля имеет верхний модуль с page
именем, содержащий модули featured-director
и , и они также имеют films-recommended-for-you
подмодули, как это:
"page"
modules
"featured-director"
modules
"director-films"
modules
"film-actors"
"films-recommended-for-you"
modules
"film-director"
Единственный ответ JSON будет выглядеть следующим образом:
{
modulesettings: {
"page": {
modules: {
"featured-director": {
dbkeys: {
id: "people",
},
modules: {
"director-films": {
dbkeys: {
films: "films"
},
modules: {
"film-actors": {
dbkeys: {
actors: "people"
},
}
}
}
}
},
"films-recommended-for-you": {
dbkeys: {
id: "films",
},
modules: {
"film-director": {
dbkeys: {
director: "people"
},
}
}
}
}
}
},
moduledata: {
"page": {
modules: {
"featured-director": {
dbobjectids: [1]
},
"films-recommended-for-you": {
dbobjectids: [1, 3]
}
}
}
},
databases: {
primary: {
people {
1: {
name: "George Lucas",
country: "USA",
avatar: "..."
films: [1, 2]
},
2: {
name: "Ewan McGregor",
avatar: "..."
},
3: {
name: "Natalie Portman",
avatar: "..."
},
4: {
name: "Hayden Christensen",
avatar: "..."
},
5: {
name: "James Cameron",
avatar: "..."
},
},
films: {
1: {
title: "Star Wars: Episode I - The Phantom Menace",
actors: [2, 3],
director: 1,
thumbnail: "..."
},
2: {
title: "Star Wars: Episode II - Attack of the Clones",
actors: [3, 4],
thumbnail: "..."
},
3: {
title: "The Terminator",
director: 5,
thumbnail: "..."
},
}
}
}
}
Давайте проанализируем, как эти три метода соотносятся друг с другом, с точки зрения скорости и объема полученных данных.
Скорость
Через REST, имея, чтобы получить 7 запросов только для того, чтобы сделать один компонент может быть очень медленным, в основном на мобильных и шатких соединений данных. Таким образом, переход от REST к Graph’L представляет собой большую скорость, потому что мы можем сделать компонент только с одним запросом.
PoP, поскольку он может получить все данные для многих компонентов в одном запросе, будет быстрее для рендеринга многих компонентов одновременно; однако, скорее всего, в этом нет необходимости. Наличие компонентов в порядке (как они появляются на странице), уже является хорошей практикой, и для тех компонентов, которые появляются под складкой, конечно, нет спешки, чтобы сделать их. Таким образом, как achema-основанные, так и компонентные API уже довольно хороши и явно превосходят aPI, основанный на ресурсах.
Объем данных
По каждому запросу данные в ответе Накл могут быть дублированы: актриса «Натали Портман» получает дважды в ответ от первого компонента, и при рассмотрении совместного вывода для двух компонентов, мы также можем найти общие данные, такие как фильм Звездные войны: Эпизод I — Призрачная угроза.
PoP, с другой стороны, нормализует данные базы данных и печатает их только один раз, однако, он несет накладные расходы печати структуры модуля. Таким образом, в зависимости от конкретного запроса, продублирующего данные или нет, aPI на основе схемы или API, основанный на компонентах, будет иметь меньший размер.
В заключение, aPI, основанный на схеме, такой как Graph’L и aPI, основанный на компонентах, такой как PoP, также хороши в отношении производительности и превосходят такие ресурсные API, как REST.
Рекомендуемое чтение: Понимание и использование REST AIS
Определенные свойства API на основе компонентов
Если API, основанный на компонентах, не всегда лучше с точки зрения производительности, чем API, основанный на схеме, вы можете задаться вопросом, то чего я пытаюсь достичь с помощью этой статьи?
В этом разделе я попытаюсь убедить вас, что такой API имеет невероятный потенциал, предоставляя несколько функций, которые являются очень желательными, что делает его серьезным соперником в мире API. Я описываю и демонстрирую каждый из его уникальных замечательных особенностей ниже.
Данные, которые необходимо получить из базы данных, можно сделать вывод из иерархии компонентов
Когда модуль отображает свойство объекта DB, модуль может не знать или заботиться о том, какой объект он является; все, что его волнует, это определение того, какие свойства от загруженного объекта необходимы.
Например, рассмотрим изображение ниже. Модуль загружает объект из базы данных (в данном случае один пост), а затем его модули потомка будут отображать определенные свойства объекта, такие title
как: content
Таким образом, по иерархии компонентов, модули «загрузки данных» будут отвечать за загрузку запрашиваемых объектов (модуль загрузки одного поста, в данном случае), а его потомки модули будут определять, какие свойства от объекта DB требуются title
(и content
, в этом случае).
Получение всех необходимых свойств для объекта DB может быть сделано автоматически, пройдя иерархию компонентов: начиная с модуля загрузки данных, мы итерировать все его потомки модулей вплоть до достижения нового модуля загрузки данных, или до тех пор, пока конец дерева; на каждом уровне мы получаем все необходимые свойства, а затем объединяем все свойства вместе и запросим их из базы данных, все они только один раз.
В приведенной ниже структуре модуль single-post
получает результаты из DB (сообщение с ID 37) и подмодульов post-title
и определяет post-content
свойства, которые будут загружены для запрашиваемого объекта DB title
(и content
соответственно); подмодули post-layout
и не требуют fetch-next-post-button
каких-либо полей данных.
"single-post"
=> Load objects with object type "post" and ID 37
modules
"post-layout"
modules
"post-title"
=> Load property "title"
"post-content"
=> Load property "content"
"fetch-next-post-button"
Выполняемый запрос автоматически рассчитывается из иерархии компонентов и требуемых ими полей данных, содержащих все свойства, необходимые всем модулям и их подмодулям:
SELECT
title, content
FROM
posts
WHERE
= 37
Извлекая свойства для извлечения непосредственно из модулей, запрос будет автоматически обновляться при изменении иерархии компонентов. Если, например, мы добавляем post-thumbnail
подмодуль, который требует поля данных: thumbnail
"single-post"
=> Load objects with object type "post" and ID 37
modules
"post-layout"
modules
"post-title"
=> Load property "title"
"post-content"
=> Load property "content"
"post-thumbnail"
=> Load property "thumbnail"
"fetch-next-post-button"
Затем запрос автоматически обновляется для получения дополнительного свойства:
SELECT
title, content, thumbnail
FROM
posts
WHERE
= 37
Поскольку мы создали данные объектов базы данных, которые должны быть извлечены в реляционном порядке, мы также можем применить эту стратегию между отношениями между самими объектами базы данных.
Рассмотрим изображение ниже: Начиная с типа объекта post
и двигаясь вниз по иерархии компонентов, нам нужно будет перенести тип объекта DB user
comment
и, соответствующие автору публикации и каждому из комментариев поста соответственно, а затем, для каждого комментарий, он должен изменить тип объекта еще раз, чтобы user
соответствующие автору комментария.
Переход от объекта базы данных к реляционным объекту (возможно, изменяя тип объекта, как в post
qgt; author
переходя от post
к , или user
нет, как в author
йgt; последователи, идущие от user
к ) является user
то, что я называю «переключение доменов».
После перехода на новый домен, с этого уровня в иерархии компонентов вниз, все необходимые свойства будут подвергнуты новому домену:
-
name
извлекается изuser
объекта (представляющего автора поста), -
content
извлекается изcomment
объекта (представляющего каждый из комментариев поста), -
name
извлекается изuser
объекта (представляющий автора каждого комментария).
Пересекая иерархию компонентов, API знает, когда он переходит на новый домен и, соответственно, обновляет запрос, чтобы получить реляционный объект.
Например, если нам нужно показать данные от автора публикации, укладка подмодуль post-author
изменит домен на этом уровне с post
user
соответствующего, и с этого уровня вниз объект DB загружается в контекст, передаваемый модулю пользователя. Затем подмодули user-name
и user-avatar
под post-author
загружают свойства name
и под avatar
user
объект:
"single-post"
=> Load objects with object type "post" and ID 37
modules
"post-layout"
modules
"post-title"
=> Load property "title"
"post-content"
=> Load property "content"
"post-author"
=> Switch domain from "post" to "user", based on property "author"
modules
"user-layout"
modules
"user-name"
=> Load property "name"
"user-avatar"
=> Load property "avatar"
"fetch-next-post-button"
Результат в следующем запросе:
SELECT
p.title, p.content, p.author, u.name, u.avatar
FROM
posts p
INNER JOIN
users u
WHERE
p.id = 37 AND p.author = u.id
Таким образом, путем правильной настройки каждого модуля нет необходимости писать запрос для получения данных для API, основанного на компонентах. Запрос автоматически производится из структуры самой иерархии компонентов, получая, какие объекты должны быть загружены модулями загрузки данных, полядляны для извлечения для каждого загруженного объекта, определенного в каждом модуле потомка, и переключение домена определяется в каждом модуле потомка.
Добавление, удаление, замена или изменение любого модуля автоматически обновят запрос. После выполнения запроса, извлеченные данные будут именно то, что требуется — ничего более или менее.
Наблюдение за данными и расчет дополнительных свойств
Начиная с модуля загрузки данных по иерархии компонентов, любой модуль может наблюдать за возвращенными результатами и вычислять дополнительные элементы данных на основе них или feedback
значения, которые помещаются под moduledata
запись.
Например, модуль fetch-next-post-button
может добавить свойство, указывающем, есть ли больше результатов, чтобы получить или нет (на основе этого значения обратной связи, если нет дополнительных результатов, кнопка будет отключена или скрыта):
{
moduledata: {
"page": {
modules: {
"single-post": {
modules: {
"fetch-next-post-button": {
feedback: {
hasMoreResults: true
}
}
}
}
}
}
}
}
Неявные знания требуемых данных снижают сложность и делают концепцию «конечной точки» устаревшей
Как показано выше, API на основе компонентов может получить именно необходимые данные, поскольку он имеет модель всех компонентов на сервере и какие поля данных требуются каждому компоненту. Затем он может сделать знание требуемых полей данных неявным.
Преимущество заключается в том, что определение того, какие данные требуются компоненту, может обновляться только на стороне сервера, без необходимости передислокации файлов JavaScript, и клиент может быть немым, просто попросив сервер предоставить все данные, которые ему нужны, таким образом, снижение сложности клиентского приложения.
Кроме того, вызов API для извлечения данных для всех компонентов для определенного URL может быть выполнен просто путем запроса этого URL плюс добавления дополнительного параметра output=json
для указания возвращающихся данных API вместо печати страницы. Таким образом, URL становится своей собственной конечной точкой или, считается по-другому, понятие «конечная точка» устаревает.
Получение подмножества данных: данные могут быть извлечены для конкретных модулей, найденных на любом уровне иерархии компонентов
Что произойдет, если нам не нужно получать данные для всех модулей на странице, а просто данные для конкретного модуля, начиная с любого уровня иерархии компонентов? Например, если модуль реализует бесконечный прокрутки, при прокрутке вниз мы должны получать только новые данные для этого модуля, а не для других модулей на странице.
Это может быть достигнуто путем фильтрации ветвей иерархии компонентов, которые будут включены в ответ, чтобы включить свойства только начиная с указанного модуля и игнорировать все выше этого уровня. В моей реализации (которую я опишу в предстоящей статье), фильтрация включена путем добавления параметра modulefilter=modulepaths
к URL, а выбранный модуль (или модули) указывается через modulepaths[]
параметр, где «модульный путь» представляет собой список модулей, начиная с верхний самый модуль к конкретному модулю (например, module1
зgt; module2
йgt; module3
имеет путь модуля , module1
и module2
module3
передается в качестве параметра URL как module1.module2.module3
).
Например, в иерархии компонентов ниже каждый модуль имеет dbobjectids
запись:
"module1"
dbobjectids: [...]
modules
"module2"
dbobjectids: [...]
modules
"module3"
dbobjectids: [...]
"module4"
dbobjectids: [...]
"module5"
dbobjectids: [...]
modules
"module6"
dbobjectids: [...]
Затем запрос URL-адреса веб-страницы, добавляющего modulefilter=modulepaths
параметры, и modulepaths[]=module1.module2.module5
получит следующий ответ:
"module1"
modules
"module2"
modules
"module5"
dbobjectids: [...]
modules
"module6"
dbobjectids: [...]
По сути, API начинает загрузку данных, начиная с module1
qgt; module2
module5
Вот почему module6
, который подпадает под , а также приносит свои данные в module5
то время как и module3
module4
нет.
Кроме того, мы можем создавать пользовательские фильтры модулей, чтобы включить заранее подготовленный набор модулей. Например, вызов страницы с modulefilter=userstate
может печатать только те модули, которые требуют состояния пользователя для визуализации их в клиенте, таких как модули module3
module6
и:
"module1"
modules
"module2"
modules
"module3"
dbobjectids: [...]
"module5"
modules
"module6"
dbobjectids: [...]
Информация о том, какие стартовые модули подпадает под requestmeta
раздел, под запись filteredmodules
, как массив модулей пути:
requestmeta: {
filteredmodules: [
["module1", "module2", "module3"],
["module1", "module2", "module5", "module6"]
]
}
Эта функция позволяет реализовать несложное одностраничное приложение, в котором кадр сайта загружается по первоначальному запросу:
"page"
modules
"navigation-top"
dbobjectids: [...]
"navigation-side"
dbobjectids: [...]
"page-content"
dbobjectids: [...]
Но, из них, мы можем придатить параметр modulefilter=page
ко всем запрошенным URL-адресам, отфильтровывая кадр и принося только содержимое страницы:
"page"
modules
"navigation-top"
"navigation-side"
"page-content"
dbobjectids: [...]
Подобно модульным userstate
фильтрам, page
описанным выше, мы можем реализовать любой пользовательский модульный фильтр и создать богатый пользовательский опыт.
Модуль является собственным API
Как показано выше, мы можем фильтровать ответ API для получения данных, начиная с любого модуля. Как следствие, каждый модуль может взаимодействовать с самим собой от клиента к серверу, просто добавляя свой модульный путь к URL-адресу веб-страницы, в который он был включен.
Я надеюсь, что вы извините мой чрезмерное волнение, но я действительно не могу подчеркнуть достаточно, как замечательно эта функция. При создании компонента нам не нужно создавать API, чтобы идти вместе с ним для получения данных (REST, Graph’L, или вообще чего-либо), потому что компонент уже способен говорить с самим собой на сервере и загружать свои собственные данные – он полностью автономный и самоуверенный ving.
Каждый модуль загрузки данных экспортирует URL-адрес, чтобы взаимодействовать с ним dataloadsource
под записью из-под раздела: datasetmodulemeta
{
datasetmodulemeta: {
"module1": {
modules: {
"module2": {
modules: {
"module5": {
meta: {
dataloadsource: "https://page-url/?modulefilter=modulepaths&modulepaths[]=module1.module2.module5"
},
modules: {
"module6": {
meta: {
dataloadsource: "https://page-url/?modulefilter=modulepaths&modulepaths[]=module1.module2.module5.module6"
}
}
}
}
}
}
}
}
}
}
Получение данных разъединяется между модулями и DRY
Чтобы сделать мою точку зрения, что получение данных в компоненте на основе API очень разъединены и DRY(Don’t Repeat Yсебя), я сначала нужно, чтобы показать, как в схеме на основе API, таких как Graph’L это менее отделены и не DRY.
В Graph’L запрос для получения данных должен указывать поля данных для компонента, которые могут включать подкомпоненты, и они могут также включать подкомпоненты и так далее. Затем верхний компонент должен знать, какие данные требуются каждому из его подкомпонентов, чтобы получить эти данные.
Например, для визуализации <FeaturedDirector>
компонента могут потребоваться следующие подкомпоненты:
Render <FeaturedDirector>:
<div>
Country: {country}
{foreach films as film}
<Film film={film} />
{/foreach}
</div>
Render <Film>:
<div>
Title: {title}
Pic: {thumbnail}
{foreach actors as actor}
<Actor actor={actor} />
{/foreach}
</div>
Render <Actor>:
<div>
Name: {name}
Photo: {avatar}
</div>
В этом сценарии запрос Graph’L реализуется на <FeaturedDirector>
уровне. Затем, если подкомпонент <Film>
обновляется, запрашивая название через filmTitle
свойство, а не , запрос от title
<FeaturedDirector>
компонента также необходимо будет обновить, чтобы отразить эту новую информацию (Graph’L имеет механизм версии, который может иметь дело с этой проблемы, но рано или поздно мы все еще должны обновить информацию). Это создает сложность обслуживания, с которой может быть трудно справиться, когда внутренние компоненты часто меняются или производятся сторонними разработчиками. Таким образом, компоненты не полностью отделены друг от друга.
Аналогичным образом, мы можем захотеть визуализировать <Film>
компонент для какого-то конкретного фильма, для которого мы должны также реализовать запрос Graph’L на этом уровне, чтобы получить данные для фильма и его актеров, который добавляет избыточный код: части одного и того же запроса будут жить на диффе уровни арендной платы компонентной структуры. Таким образом, Граф-Л не DRY.
Поскольку API, основанный на компонентах, уже знает, как его компоненты окутывают друг друга в свою собственную структуру, тогда этих проблем полностью избегают. С одной из них клиент может просто запрашивать необходимые данные, в зависимости от того, какие данные; при изменении поля данных подкомпонента общая модель уже знает и адаптируется немедленно, без необходимости изменения запроса для родительского компонента клиента. Таким образом, модули сильно отделены друг от друга.
С другой стороны, мы можем получать данные, начиная с любого пути модуля, и он всегда будет возвращать точные необходимые данные, начиная с этого уровня; нет дублированных запросов вообще, или даже запросы, чтобы начать с. Таким образом, компонент на основе API полностью DRY. (Это еще одна особенность, которая действительно волнует меня и заставляет меня промокнуть.)
(Да, каламбур полностью предназначен. Извините за это.)
Извлечение значений конфигурации в дополнение к данным базы данных
Давайте вернемся к примеру featured-director
компонента для сайта IMDB, описанного выше, который был создан — вы догадались! — с Bootstrap. Вместо хардкодирования классов Bootstrap или других свойств, таких как HTML-тег заголовка или максимальная ширина аватара внутри файлов JavaScript (независимо от того, фиксируются ли они внутри компонента или установлены через реквизит родительскими компонентами), каждый модуль может установить их как значения конфигурации через API, так что затем они могут быть непосредственно обновлены на сервере и без необходимости передислокации файлов JavaScript. Аналогичным образом, мы можем передавать строки (например, Featured director
название), которые уже могут быть переведены / интернационализированы на стороне сервера, избегая необходимости развертывания файлов конфигурации локализации на переднем конце.
Подобно извлечению данных, пройдя иерархию компонентов, API способен предоставить требуемые значения конфигурации для каждого модуля и ничего более или менее.
Значения конфигурации для featured-director
компонента могут выглядеть следующим образом:
{
modulesettings: {
"page": {
modules: {
"featured-director": {
configuration: {
class: "alert alert-info",
title: "Featured director",
titletag: "h3"
},
modules: {
"director-films": {
configuration: {
classes: {
wrapper: "media",
avatar: "mr-3",
body: "media-body",
films: "row",
film: "col-sm-6"
},
avatarmaxsize: "100px"
},
modules: {
"film-actors": {
configuration: {
classes: {
wrapper: "card",
image: "card-img-top",
body: "card-body",
title: "card-title",
avatar: "img-thumbnail"
}
}
}
}
}
}
}
}
}
}
}
Пожалуйста, обратите внимание, как — потому что свойства конфигурации для различных модулей вложены под уровнем каждого модуля — они никогда не столкнутся друг с другом, если с одним и тем же именем (например, свойство classes
из одного модуля не переопределяет свойство classes
от другого модуля), избегая необходимости добавлять пространства имен для модулей.
Более высокая степень модульности, достигнутая в применении
Согласно Википедии,модульность означает:
Степень разделения и рекомбинированного разделения компонентов системы, зачастую с пользой гибкости и разнообразия в использовании. Концепция модульности используется в первую очередь для снижения сложности путем разбивания системы на различную степень взаимозависимости и независимости по пересчету и «скрыть сложность каждой части за абстракцией и интерфейсом».
Возможность обновления компонента только со стороны сервера, без необходимости передислокации файлов JavaScript, имеет последствие лучшего повторного использования и обслуживания компонентов. Я продемонстрирую это, переосмыслив, как этот пример, закодированный для React, будет работать в API, основанном на компонентах.
Допустим, что у нас есть <ShareOnSocialMedia>
компонент, в настоящее время с двумя пунктами: <FacebookShare>
и , как <TwitterShare>
это:
Render <ShareOnSocialMedia>:
<ul>
<li>Share on Facebook: <FacebookShare url={window.location.href} /></li>
<li>Share on Twitter: <TwitterShare url={window.location.href} /></li>
</ul>
Но потом Instagram получил вид прохладно, так что мы должны добавить элемент <InstagramShare>
для нашего <ShareOnSocialMedia>
компонента, тоже:
Render <ShareOnSocialMedia>:
<ul>
<li>Share on Facebook: <FacebookShare url={window.location.href} /></li>
<li>Share on Twitter: <TwitterShare url={window.location.href} /></li>
<li>Share on Instagram: <InstagramShare url={window.location.href} /></li>
</ul>
В реализации React, как это видно из связанного кода, добавление нового компонента <InstagramShare>
под <ShareOnSocialMedia>
компонентные силы для передислокации файла JavaScript для последнего, так что эти два модуля не так отделены, как они могли бы быть.
Однако в API, основанном на компонентах, мы можем легко использовать отношения между модулями, уже описанными в API, для объединения модулей. Хотя первоначально мы будем иметь этот ответ:
{
modulesettings: {
"share-on-social-media": {
modules: {
"facebook-share": {
configuration: {...}
},
"twitter-share": {
configuration: {...}
}
}
}
}
}
После добавления Instagram мы получим обновленный ответ:
{
modulesettings: {
"share-on-social-media": {
modules: {
"facebook-share": {
configuration: {...}
},
"twitter-share": {
configuration: {...}
},
"instagram-share": {
configuration: {...}
}
}
}
}
}
И только путем итерации всех значений modulesettings["share-on-social-media"].modules
под, компонент <ShareOnSocialMedia>
может быть обновлен, чтобы показать <InstagramShare>
компонент без необходимости передислокации любого файла JavaScript. Таким образом, API поддерживает добавление и удаление модулей без ущерба для кода из других модулей, достигая более высокой степени модульности.
Магазин кэша/данных родного клиента
Извлеченные данные базы данных нормализуется в структуре словаря, и стандартизированы так, что, начиная с значения на dbobjectids
, любой кусок данных под databases
могут быть достигнуты только следуя путь к нему, как указано через записи dbkeys
, в зависимости от того, как это было Структурированных. Таким образом, логика организации данных уже является родной для самого API.
Мы можем извлечь выгоду из этой ситуации несколькими способами. Например, возвращенные данные для каждого запроса могут быть добавлены в кэш на стороне клиента, содержащий все данные, запрошенные пользователем на протяжении всего сеанса. Таким образом, можно избежать добавления внешнего хранилища данных, такого как Redux, в приложение (я имею в виду обработку данных, не касающуюся других функций, таких как Undo/Redo, среда совместной работы или отладка времени).
Кроме того, структура на основе компонентов способствует кэша: иерархия компонентов зависит не от URL, а от того, какие компоненты необходимы в этом URL. Таким образом, два события под /events/1/
и будет иметь одну и ту же /events/2/
иерархию компонентов, и информация о том, какие модули необходимы, может быть использована в них. Как следствие, все свойства (кроме данных базы данных) могут быть кэшированы на клиенте после извлечения первого события и использованы с тех пор, так что только данные базы данных для каждого последующего события должны быть извлечены и ничего больше.
Расширяемость и повторное очищение
Раздел databases
API может быть расширен, что позволяет классифицировать свою информацию в индивидуальные подразделы. По умолчанию все данные объекта базы данных базы данных помещаются под primary
ввод, однако мы также можем создавать пользовательские записи, где размещать определенные свойства объекта DB.
Например, если компонент «Фильмы, рекомендованные для вас», описанный ранее, показывает список друзей зарегистрированного пользователя, которые смотрели этот фильм под свойством friendsWhoWatchedFilm
на film
объекте DB, потому что это значение будет меняться в зависимости от зарегистрированного пользователя, то мы сохранить это свойство под userstate
записью, поэтому, когда пользователь выключает, мы только удалим эту ветвь из кэшированной базы данных на клиенте, но все primary
данные остаются:
{
databases: {
userstate: {
films: {
5: {
friendsWhoWatchedFilm: [22, 45]
},
}
},
primary: {
films: {
5: {
title: "The Terminator"
},
}
"people": {
22: {
name: "Peter",
},
45: {
name: "John",
},
},
}
}
}
Кроме того, до определенного момента структура ответа API может быть перепрофилирована. В частности, результаты базы данных могут быть напечатаны в другой структуре данных, например в массиве вместо словаря по умолчанию.
Например, если тип объекта только один (например), films
он может быть отформатирован как массив, который будет подаваться непосредственно в компонент typeahead:
[
{
title: "Star Wars: Episode I - The Phantom Menace",
thumbnail: "..."
},
{
title: "Star Wars: Episode II - Attack of the Clones",
thumbnail: "..."
},
{
title: "The Terminator",
thumbnail: "..."
},
]
Поддержка ориентированного на аспект ы программирования
Помимо получения данных, API на основе компонентов также может размещать данные, например, для создания публикации или добавления комментария, а также выполнять любые операции, такие как регистрация пользователя в или выход, отправка электронных писем, журналирование, аналитика и так далее. Ограничений нет: любая функциональность, предоставляемая базовой CMS, может быть вызвана через модуль – на любом уровне.
Вдоль иерархии компонентов мы можем добавить любое количество модулей, и каждый модуль может выполнять свою собственную работу. Таким образом, не все операции обязательно должны быть связаны с ожидаемым действием запроса, как при выполнении операции POST, PUT или DELETE в REST или отправке мутации в Graph’L, но могут быть добавлены, чтобы обеспечить дополнительные функции, такие как отправка электронной почты админ, когда пользователь создает новую публикацию.
Таким образом, определяя иерархию компонентов с помощью файлов впрыски или конфигурации, Можно сказать, что API поддерживает ориентированное на аспект программирование,«парадигма программирования, которая направлена на увеличение модульности, позволяя разделение перекрестные проблемы».
Рекомендуемое чтение: Защита вашего сайта с помощью политики функций
Повышенная безопасность
Названия модулей не обязательно фиксируются при печати на выходе, но могут быть сокращены, искажены, изменены случайным образом или (вкратце) сделаны переменные любым способом. Хотя первоначально предполагалось, что сокращение вывода API (так, чтобы имена модулей carousel-featured-posts
или drag-and-drop-user-images
могли быть сокращены до базовой 64 обозначения, такие a1
a2
как, и так далее, для производственной среды), эта функция позволяет часто менять имена модулей в ответ от API по соображениям безопасности.
Например, имена ввода по умолчанию называются в качестве соответствующего модуля; затем, модули называется username
и , которые должны быть password
отображаются в клиенте, как <input type="text" name="{input_name}">
и <input type="password" name="{input_name}">
соответственно, могут быть установлены различные случайные значения для их имена ввода (например, zwH8DSeG
и QBG7m6EF
сегодня, и c3oMLBjo
c46oVgN6
завтра), что делает его более трудным для спамеров и ботов для целевой сайт.
Универсальность с помощью альтернативных моделей
Вложение модулей позволяет разветвиться в другой модуль, чтобы добавить совместимость для конкретной среды или технологии, или изменить некоторые стиль или функциональность, а затем вернуться к исходной ветви.
Например, предположим, что веб-страница имеет следующую структуру:
"module1"
modules
"module2"
modules
"module3"
"module4"
modules
"module5"
modules
"module6"
В этом случае, мы хотели бы сделать веб-сайт также работает для AMP, однако, модули module2
, и не AMP module4
module5
совместимы. Мы можем ветвять эти модули в аналогичные, AMP-совместимые модули module2AMP
, и , после чего мы продолжаем module4AMP
module5AMP
загрузку исходной иерархии компонентов, так что тогда только эти три модуля заменяются (и ничего больше):
"module1"
modules
"module2AMP"
modules
"module3"
"module4AMP"
modules
"module5AMP"
modules
"module6"
Это делает его довольно легко генерировать различные выходы из одной базы кода, добавив вилки только здесь и там по мере необходимости, и всегда scoped и ограничено отдельных модулей.
Время демонстрации
Код, реализующий API, как поясняется в этой статье, доступен в этом репозитории с открытым исходным кодом.
Я развернул PoP API https://nextapi.getpop.org
под для демонстрационных целей. Веб-сайт работает на WordPress, так что URL постоянные являются те, типичные для WordPress. Как отмечалось ранее, путем добавления параметра output=json
к ним эти URL-адреса становятся их собственными конечными точками API.
Сайт поддерживается той же базой данных с веб-сайта PoP Demo, поэтому визуализация иерархии компонентов и извлеченных данных может быть https://demo.getpop.org/u/leo/
выполнена с запросом того же URL на этом другом веб-сайте (например, посещение объясняет данные из https://nextapi.getpop.org/u/leo/?output=json
).
Ссылки ниже демонстрируют API для случаев, описанных ранее на:
- Главная страница, один пост, автор, список сообщений и список пользователей.
- Событие, фильтрующее сяпой из определенного модуля.
- Тег, модули фильтрации, которые требуют состояния пользователя и фильтрации, чтобы принести только страницу из одностраничного приложения.
- Массив мест, чтобы питаться в typeahead.
- Альтернативные модели для страницы «Кто мы такие»: Нормальный, Печатный, Встраиваемый.
- Изменение имен модулей: оригинал против искаженных.
- Информация о фильтрации: только настройки модуля, данные модуля плюс данные базы данных.
Заключение
Хороший API является ступенькой для создания надежных, легко подсобных и мощных приложений. В этой статье я описал концепции питания на основе компонентов API, который, я считаю, является довольно хорошим API, и я надеюсь, что я убедил вас тоже.
До сих пор проектирование и внедрение API включало в себя несколько итераций и заняло более пяти лет – и это еще не полностью готово. Тем не менее, он находится в довольно приличном состоянии, не готов к производству, но как стабильный альфа. В эти дни, я все еще работаю над этим; работа по определению открытой спецификации, внедрению дополнительных слоев (например, визуализация) и написанию документации.
В предстоящей статье я расскажу о том, как работает моя реализация API. До тех пор, если у вас есть какие-либо мысли об этом — независимо от того, положительные или отрицательные — я хотел бы прочитать ваши комментарии ниже.
Обновление (31-го января): Возможности пользовательского запроса
Ален Шлессер отметил, что API, который не может быть заказным от клиента ничего не стоит, принимая нас обратно в SOAP, как таковой он не может конкурировать ни с REST или Graph’L. После предоставления своего комментария несколько дней думал, что я должен был признать, что он прав. Однако вместо того, чтобы отклонить API на основе компонента как благие намерения, но не совсем-там-еще усилия, я сделал что-то гораздо лучше: я получил для реализации пользовательских запросов возможности для него. И это работает как шарм!
В следующих ссылках данные для ресурса или сбора ресурсов извлекаются, как это обычно делается через REST. Тем не менее, через параметр fields
мы можем также указать, какие конкретные данные для извлечения для каждого ресурса, избегая более или недоотдельных данных:
-
Один пост и набор сообщений, добавляя параметр
fields=title,content,datetime
-
Пользователь и коллекция пользователей, добавляющих параметр
fields=name,username,description
Приведенные выше ссылки демонстрируют получение данных только для запрашиваемых ресурсов. А как же их отношения? Например, предположим, что мы хотим получить список сообщений с полями "title"
и "content"
, комментарии каждого поста с полями "content"
и , и автор каждого комментария с "date"
полями и "name"
"url"
. Для достижения этой цели в Graph’L мы реализуем следующий запрос:
query {
post {
title
content
comments {
content
date
author {
name
url
}
}
}
}
Для реализации API на основе компонентов я перевел запрос в соответствующее выражение «точечный синтаксис», которое затем может быть поставлено по fields
параметру. Запрос на «почтовый» ресурс, это значение:
fields=title,content,comments.content,comments.date,comments.author.name,comments.author.url
Или его можно упростить, используя |
для группы все поля, применяемые к тому же ресурсу:
fields=title|content,comments.content|date,comments.author.name|url
При выполнения этого запроса на одном сообщении мы получаем именно необходимые данные для всех участвующих ресурсов:
{
"datasetmodulesettings": {
"dataload-dataquery-singlepost-fields": {
"dbkeys": {
"id": "posts",
"comments": "comments",
"comments.author": "users"
}
}
},
"datasetmoduledata": {
"dataload-dataquery-singlepost-fields": {
"dbobjectids": [
23691
]
}
},
"databases": {
"posts": {
"23691": {
"id": 23691,
"title": "A lovely tango",
"content": "<div class="responsiveembed-container"><iframe width="480" height="270" src="https://www.youtube.com/embed/sxm3Xyutc1s?feature=oembed" frameborder="0" allowfullscreen></iframe></div>n",
"comments": [
"25094",
"25164"
]
}
},
"comments": {
"25094": {
"id": "25094",
"content": "<p><a class="hashtagger-tag" href="https://newapi.getpop.org/tags/videos/">#videos</a>u00a0<a class="hashtagger-tag" href="https://newapi.getpop.org/tags/tango/">#tango</a></p>n",
"date": "4 Aug 2016",
"author": "851"
},
"25164": {
"id": "25164",
"content": "<p>fjlasdjf;dlsfjdfsj</p>n",
"date": "19 Jun 2017",
"author": "1924"
}
},
"users": {
"851": {
"id": 851,
"name": "Leonardo Losoviz",
"url": "https://newapi.getpop.org/u/leo/"
},
"1924": {
"id": 1924,
"name": "leo2",
"url": "https://newapi.getpop.org/u/leo2/"
}
}
}
}
Таким образом, мы можем запрашивать ресурсы в rest моды, и указать схемы на основе запросов в Граф-L моды, и мы получим именно то, что требуется, без более или недостаточной данных, и нормализации данных в базе данных, так что никаких данных дублируется. Благоприятно, запрос может включать в себя любое количество отношений, вложенных вглубь, и они решаются с линейной сложности времени: худший случай O (n’m), где n является число узлов, которые переключают домена (в данном случае 2: comments
и ) и м является число м comments.author
извлечены результаты (в данном случае 5: 1 пост no 2 комментариев и 2 пользователя), и средний случай O(n). (Это более эффективно, чем ГрафЗл, который имеет полиномиальное время сложности O(n’c) и страдает от увеличения времени выполнения по мере увеличения глубины уровня).
Наконец, этот API может также применять модификаторы при запросе данных, например для фильтрации полученных ресурсов, например, что может быть сделано с помощью Graph’L. Для достижения этой цели API просто сидит на верхней части приложения и может удобно использовать его функциональность, поэтому нет необходимости изобретать колесо. Например, добавленные параметры filter=posts&searchfor=internet
будут фильтровать все сообщения, "internet"
содержащиеся в коллекции сообщений.
Реализация этой новой функции будет описана в предстоящей статье.
Источник: smashingmagazine.com