Fermer

janvier 21, 2019

Présentation de l'API basée sur les composants


À propos de l'auteur

Leonardo Losoviz est le créateur de PoP un cadre permettant de créer des sites Web modulaires basés sur PHP et les guidons, et optimisé par WordPress. Il habite à Kuala…
Plus à propos de Leonardo

Dans le monde des API, GraphQL a récemment éclipsé REST en raison de sa capacité à interroger et à récupérer toutes les données requises dans une seule requête. Dans cet article, je vais décrire un type différent d'API, basé sur des composants, qui va encore plus loin en termes de quantité de données pouvant être extraites à partir d'une seule requête.

Une API est le canal de communication permettant à une application de charger des données à partir du serveur. Dans le monde des API, REST a été la méthodologie la plus établie, mais a été récemment éclipsée par par GraphQL qui offre des avantages importants par rapport à REST . Alors que REST nécessite plusieurs requêtes HTTP pour extraire un ensemble de données afin de restituer un composant, GraphQL peut interroger et extraire de telles données dans une seule requête. La réponse correspond exactement à ce qui est requis, sans sous-extraction ou sous-extraction, comme cela se produit généralement dans REST.

Dans cet article, je vais décrire une autre manière de récupérer des données que j'ai conçues et appelées «PoP» (et open source ici ), qui développe l'idée de récupérer des données pour plusieurs entités. dans une requête unique introduite par GraphQL et va encore plus loin, c’est-à-dire que REST récupère les données d’une ressource et que GraphQL récupère les données de toutes les ressources d’un composant, l’API basée sur les composants peut extraire les données de toutes les ressources de toutes les ressources. composants sur une seule page.

L’utilisation d’une API basée sur les composants est logique lorsque le site Web est lui-même construit à l’aide de composants, c’est-à-dire lorsque la page Web est composée de manière itérative de composants englobant d’autres composants jusqu’à obtenir composant unique qui représente la page. Par exemple, la page Web affichée dans l'image ci-dessous est construite avec des composants délimités par des carrés:


 Capture d'écran d'une page Web basée sur les composants
La page est un composant qui enveloppe des composants, qui envelopperont des composants, comme indiqué par les carrés. ( Grand aperçu )

Une API basée sur les composants peut faire une requête unique au serveur en demandant les données pour toutes les ressources de chaque composant (ainsi que pour tous les composants de la page), ce qui est accompli en conservant les relations. parmi les composants de la structure API elle-même.

Cette structure offre, entre autres, les avantages suivants:

  • Une page comportant de nombreux composants ne déclenchera qu'une requête au lieu de plusieurs;
  • Les données partagées entre les composants ne peuvent être extraites. une fois à partir de la base de données et imprimée une seule fois dans la réponse;
  • Cela peut grandement réduire, voire supprimer complètement, la nécessité d'un magasin de données.

Nous allons les explorer en détail tout au long de l'article, mais commençons par explorer les composants sont réellement et comment nous pouvons construire un site basé sur de tels composants, et enfin, explorer le fonctionnement d'une API basée sur les composants.

Lecture recommandée : Introduction à GraphQL: Pourquoi avons-nous besoin de A? Nouveau type d’API [19659017] Smashing Cat, juste en train de se préparer à faire quelque chose de magique. "Width =" 310 "height =" 400 « />

Création d'un site à l'aide de composants

Un composant est simplement un ensemble d'éléments de code HTML, JavaScript et CSS. tous ensemble pour créer une entité autonome. Cela peut ensuite envelopper d'autres composants pour créer des structures plus complexes, et être lui-même enveloppé par d'autres composants. Un composant a un objectif, qui peut aller de quelque chose de très basique (comme un lien ou un bouton) à quelque chose de très élaboré (comme un carrousel ou un outil de téléchargement d’image par glisser-déposer). Les composants sont plus utiles lorsqu'ils sont génériques et permettent la personnalisation à l'aide de propriétés injectées (ou «props»), de sorte qu'ils puissent servir à un large éventail de cas d'utilisation. Dans le meilleur des cas, le site lui-même devient un composant.

Le terme «composant» est souvent utilisé pour désigner à la fois la fonctionnalité et la conception. Par exemple, en ce qui concerne les fonctionnalités, les frameworks JavaScript tels que React ou Vue permettent de créer des composants côté client, capables de s'auto-rendre (par exemple, après que l'API ait récupéré les informations requises). données) et utilisent des accessoires pour définir les valeurs de configuration des composants encapsulés, permettant ainsi la réutilisation du code. Concernant le design, Bootstrap a normalisé l’affichage des sites Web dans sa bibliothèque de composants front-end, et il est devenu une tendance saine pour les équipes de créer des systèmes de conception pour gérer leurs sites Web, ce qui permet aux différents membres de l’équipe ( concepteurs et développeurs, mais aussi marketing et vendeurs) pour parler un langage unifié et exprimer une identité cohérente.

La décomposition d’un site est alors un moyen très judicieux de le rendre plus facile à gérer. Les sites utilisant des frameworks JavaScript tels que React et Vue sont déjà basés sur des composants (au moins du côté client). L'utilisation d'une bibliothèque de composants telle que Bootstrap n'implique pas nécessairement que le site soit basé sur des composants (il pourrait s'agir d'un gros blob HTML), mais intègre le concept des éléments réutilisables pour l'interface utilisateur.

Si le site ] est un gros bloc de HTML, pour le composant, nous devons diviser la mise en page en une série de motifs récurrents, pour lesquels nous devons identifier et cataloguer les sections de la page en fonction de la similitude de leurs fonctionnalités et de leurs styles. ces sections en couches, aussi granulaires que possible, essayant de concentrer chaque couche sur un seul objectif ou une seule action, et essayant également de faire correspondre les couches communes à différentes sections.

Note : « Atomic Design » de Brad Frost est une excellente méthodologie pour identifier ces modèles communs et créer un système de conception réutilisable.


 Identifier les éléments constitutifs d'une page Web
Brad Frost identifie cinq niveaux distincts dans atomi c design pour la création de systèmes de design. ( Grand aperçu )

Par conséquent, créer un site à l'aide de composants revient à jouer à LEGO. Chaque composant est soit une fonctionnalité atomique, soit une composition d'autres composants, soit une combinaison des deux.

Comme indiqué ci-dessous, un composant de base (un avatar) est composé de manière itérative par d'autres composants jusqu'à l'obtention de la page Web en haut: [19659028] Séquence de composants créant une page Web « />

Séquence de composants produits, d'un avatar à la page Web. ( Grand aperçu )

Spécification de l’API basée sur les composants

Pour l’API que j’ai conçue, un composant est appelé «module», c’est désormais le terme «composant». et «module» sont utilisés de manière interchangeable.

La relation de tous les modules qui s’enroulent les uns dans les autres, depuis le module situé le plus haut jusqu’au dernier niveau, est appelée «hiérarchie des composants». Cette relation peut être exprimée via un tableau associatif (un tableau de clé => propriété) côté serveur, dans lequel chaque module indique son nom en tant qu'attribut de clé et ses modules internes sous la propriété modules . L’API code ensuite simplement ce tableau en tant qu’objet JSON à des fins de consommation:

 // Hiérarchie des composants côté serveur, par exemple. via PHP:
[
  "top-module" => [
    "modules" => [
      "module-level1" => [
        "modules" => [
          "module-level11" => [
            "modules" => [...]
          ],
          "module-level12" => [
            "modules" => [
              "module-level121" => [
                "modules" => [...]
              ]
            ]
          ]
        ]
      ],
      "module-level2" => [
        "modules" => [
          "module-level21" => [
            "modules" => [...]
          ]
        ]
      ]
    ]
  ]
]

// Hiérarchie des composants codée au format JSON:
{
  "top-module": {
    modules: {
      "module-level1": {
        modules: {
          "module-level11": {
            ...
          },
          "module-level12": {
            modules: {
              "module-level121": {
                ...
              }
            }
          }
        }
      },
      "module-level2": {
        modules: {
          "module-level21": {
            ...
          }
        }
      }
    }
  }
}

La relation entre les modules est définie de manière strictement descendante: un module englobe les autres modules et sait qui ils sont, mais il ne sait pas – et se moque – quels modules l'enveloppent.

Par exemple, dans le code JSON ci-dessus, le module module-level1 sait qu'il englobe les modules module-level11 et module-level12 et transitoirement, il sait également elle englobe module-level121 ; mais le module module-level11 ne se soucie pas de savoir qui l'enveloppe, par conséquent, il ignore le module-level1 .

Ayant la structure à base de composants, nous pouvons maintenant ajouter le réel informations requises par chaque module, classées dans les paramètres (tels que les valeurs de configuration et autres propriétés) et les données (telles que les ID des objets de base de données interrogés et d'autres propriétés), et placées en conséquence dans les entrées modulesettings et moduledata :

 {
  modulesettings: {
    "top-module": {
      configuration: {...},
      ...
      modules: {
        "module-level1": {
          configuration: {...},
          ...
          modules: {
            "module-level11": {
              répéter...
            },
            "module-level12": {
              configuration: {...},
              ...
              modules: {
                "module-level121": {
                  répéter...
                }
              }
            }
          }
        },
        "module-level2": {
          configuration: {...},
          ...
          modules: {
            "module-level21": {
              répéter...
            }
          }
        }
      }
    }
  },
  moduledata: {
    "top-module": {
      dbobjectides: [...],
      ...
      modules: {
        "module-level1": {
          dbobjectides: [...],
          ...
          modules: {
            "module-level11": {
              répéter...
            },
            "module-level12": {
              dbobjectides: [...],
              ...
              modules: {
                "module-level121": {
                  répéter...
                }
              }
            }
          }
        },
        "module-level2": {
          dbobjectides: [...],
          ...
          modules: {
            "module-level21": {
              répéter...
            }
          }
        }
      }
    }
  }
}

Ensuite, l'API ajoutera les données d'objet de base de données. Ces informations ne sont pas placées sous chaque module, mais sous une section partagée appelée bases de données afin d'éviter la duplication d'informations lorsque deux ou plusieurs modules différents récupèrent les mêmes objets dans la base de données.

En outre, l'API représente les données d'objet de base de données de manière relationnelle, afin d'éviter la duplication d'informations lorsque deux objets de base de données différents ou plus sont liés à un objet commun (tel que deux publications ayant le même auteur). En d'autres termes, les données des objets de base de données sont normalisées.

Lectures recommandées : Création d'un formulaire de contact sans serveur pour votre site statique

La structure est un dictionnaire, organisé sous chaque type d'objet et ID d'objet en second, à partir duquel nous pouvons obtenir les propriétés de l'objet:

 {
  bases de données: {
    primaire: {
      dbobject_type: {
        dbobject_id: {
          propriété: ...,
          ...
        },
        ...
      },
      ...
    }
  }
}

Cet objet JSON est déjà la réponse de l'API basée sur les composants. Son format est une spécification à part entière: tant que le serveur renvoie la réponse JSON dans le format requis, le client peut utiliser l'API indépendamment de son implémentation. Par conséquent, l’API peut être implémentée sur n’importe quel langage (ce qui est l’une des beautés de GraphQL: être une spécification et non une implémentation réelle lui a permis de devenir disponible dans une multitude de langages.)

Note : Dans un prochain article, je vais décrire ma mise en oeuvre de l'API à base de composants en PHP (qui est celle disponible dans le rapport ).

Exemple de réponse d'API

For Par exemple, la réponse API ci-dessous contient une hiérarchie de composants avec deux modules, page => post-feed où le module post-feed récupère les billets de blog. Veuillez noter les points suivants:

  • Chaque module sait quels sont ses objets interrogés dans la propriété dbobjectids (IDs 4 et 9 pour les articles de blog)
  • . Chaque module connaît le type d'objet pour ses objets interrogés dans la propriété dbkeys (les données de chaque article se trouvent sous posts et les données de l'auteur de l'article, correspondant à l'auteur avec l'ID indiqué sous le post author se trouve sous users )
  • Étant donné que les données de l'objet de base de données sont relationnelles, property author contient l'identifiant de l'objet auteur au lieu d'être imprimé. les données de l'auteur directement.
 {
  moduledata: {
    "page": {
      modules: {
        "post-feed": {
          dbobjectides: [4, 9]
        }
      }
    }
  },
  modulesettings: {
    "page": {
      modules: {
        "post-feed": {
          dbkeys: {
            id: "posts",
            auteur: "utilisateurs"
          }
        }
      }
    }
  },
  bases de données: {
    primaire: {
      des postes: {
        4: {
          titre: "Bonjour le monde!",
          auteur: 7
        },
        9: {
          titre: "Tout va bien?",
          auteur: 7
        }
      },
      utilisateurs: {
        7: {
          nom: "Leo"
        }
      }
    }
  }
}

Différences lors de l'extraction de données à partir d'API basées sur les ressources, les schémas et les composants

Voyons comment une API basée sur les composants, telle que PoP, compare, lors de l'extraction de données, à une API basée sur des ressources telle que REST, et à une API basée sur un schéma tel que GraphQL.

Supposons qu'IMDB comporte une page avec deux composants devant extraire des données: «Directeur vedette» (affiche une description de George Lucas et une liste de ses films) et «Films recommandés for you ”(présentant des films tels que Star Wars: Épisode I – La menace fantôme et Le Terminator ). Cela pourrait ressembler à ceci:


 IMDB nouvelle génération
Composants "Réalisateur vedette" et "Films recommandés pour vous" pour le site IMDB de prochaine génération. ( Grand aperçu )

Voyons combien de demandes sont nécessaires pour récupérer les données via chaque méthode API. Pour cet exemple, la composante "Directeur vedette" donne un résultat ("George Lucas"), à partir de laquelle il récupère deux films ( Star Wars: Épisode I – La menace fantôme et Star Wars: Épisode II – L'attaque des clones ), et pour chaque film deux acteurs (“Ewan McGregor” et “Natalie Portman” pour le premier film et “Natalie Portman” et “Hayden Christensen” pour le second film). La composante "Films recommandés pour vous" donne deux résultats ( Star Wars: Épisode I – La menace fantôme et The Terminator ), puis récupère leurs réalisateurs ("George Lucas" et " James Cameron ”respectivement.]

En utilisant REST pour rendre le composant en vedette-réalisateur nous aurons peut-être besoin des 7 demandes suivantes (ce nombre peut varier en fonction de la quantité de données fournie par chaque point d'extrémité, c.-à-d. la surexploitation a été mise en œuvre):

 GET - / vedette-réalisateur
GET - / directeurs / george-lucas
GET - / films / la menace fantôme
GET - / films / attaque des clones
GET - / acteurs / ewan-mcgregor
GET - / acteurs / natalie-portman
GET - / acteurs / hayden-christensen

GraphQL permet, par le biais de schémas fortement typés, d'extraire toutes les données requises en une seule requête par composant. La requête permettant d'extraire des données via GraphQL pour le composant en vedetteDirector se présente comme suit (après que ait mis en œuvre le schéma correspondant ):

 requête {
  sélectionnéeDirecteur {
    prénom
    pays
    avatar
    films {
      Titre
      la vignette
      acteurs {
        prénom
        avatar
      }
    }
  }
}

Et il produit la réponse suivante:

 {
  Les données: {
    sélectionnéeDirecteur: {
      nom: "George Lucas",
      pays: "USA",
      avatar: "...",
      films: [
        { 
          title: "Star Wars: Episode I - The Phantom Menace",
          thumbnail: "...",
          actors: [
            {
              name: "Ewan McGregor",
              avatar: "...",
            },
            {
              name: "Natalie Portman",
              avatar: "...",
            }
          ]
        },
        {
          titre: "Star Wars: Episode II - L'Attaque des Clones",
          la vignette: "...",
          acteurs: [
            {
              name: "Natalie Portman",
              avatar: "...",
            },
            {
              name: "Hayden Christensen",
              avatar: "...",
            }
          ]
        }
      ]
    }
  }
}

Et interroger le composant “Films recommandés pour vous” donne la réponse suivante:

 {
  Les données: {
    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 émettra une seule demande pour extraire toutes les données de tous les composants de la page et normaliser les résultats. Le point final à appeler est simplement identique à l'URL pour laquelle nous avons besoin d'obtenir les données, en ajoutant simplement un paramètre supplémentaire output = json pour indiquer que les données doivent être importées au format JSON au lieu de les imprimer au format HTML. :

 GET - / url-of-the-page /? Output = json

En supposant que la structure du module comporte un module supérieur nommé page contenant les modules présenté par le réalisateur et films recommandés par vous et ceux-ci ont également sous-modules, comme ceci:

 "page"
  modules
    "metteur en scène"
      modules
        "réalisateur-films"
          modules
            "acteurs de cinéma"
  "films recommandés pour vous"
    modules
      "réalisateur"

La réponse JSON renvoyée unique ressemble à ceci:

 {
  modulesettings: {
    "page": {
      modules: {
        "vedette-réalisateur": {
          dbkeys: {
            id: "personnes",
          },
          modules: {
            "réalisateur-films": {
              dbkeys: {
                films: "films"
              },
              modules: {
                "acteurs de cinéma": {
                  dbkeys: {
                    acteurs: "personnes"
                  },
                }
              }
            }
          }
        },
        "films recommandés pour vous": {
          dbkeys: {
            id: "films",
          },
          modules: {
            "réalisateur": {
              dbkeys: {
                directeur: "personnes"
              },
            }
          }
        }
      }
    }
  },
  moduledata: {
    "page": {
      modules: {
        "vedette-réalisateur": {
          dbobjectides: [1]
        },
        "films recommandés pour vous": {
          dbobjectides: [1, 3]
        }
      }
    }
  },
  bases de données: {
    primaire: {
      personnes {
        1: {
          nom: "George Lucas",
          pays: "USA",
          avatar: "..."
          films: [1, 2]
        },
        2: {
          nom: "Ewan McGregor",
          avatar: "..."
        },
        3: {
          nom: "Natalie Portman",
          avatar: "..."
        },
        4: {
          nom: "Hayden Christensen",
          avatar: "..."
        },
        5: {
          nom: "James Cameron",
          avatar: "..."
        },
      },
      films: {
        1: {
          titre: "Star Wars: Episode I - La Menace Fantôme",
          acteurs: [2, 3],
          directeur: 1,
          la vignette: "..."
        },
        2: {
          titre: "Star Wars: Episode II - L'Attaque des Clones",
          acteurs: [3, 4],
          la vignette: "..."
        },
        3: {
          titre: "The Terminator",
          directeur: 5,
          la vignette: "..."
        },
      }
    }
  }
}

Analysons la façon dont ces trois méthodes se comparent, en termes de rapidité et de quantité de données extraites.

Speed ​​

Grâce à REST, le fait d'extraire 7 requêtes pour rendre un composant peut être très lent, principalement sur des connexions de données mobiles et instables. Par conséquent, le saut de REST à GraphQL représente un avantage considérable en termes de rapidité, car nous sommes en mesure de restituer un composant avec une seule requête.

PoP, car il peut extraire toutes les données de nombreux composants dans une requête, sera plus rapide pour rendre plusieurs composants à la fois; Cependant, très probablement, cela n’est pas nécessaire. Faire en sorte que les composants soient restitués dans l’ordre (tels qu’ils apparaissent dans la page) est déjà une bonne pratique, et il n’est certainement pas urgent de les rendre pour les composants qui apparaissent sous le pli. Par conséquent, les API basées sur les schémas et les composants sont déjà très bonnes et nettement supérieures aux API basées sur les ressources.

Quantité de données

À chaque demande, les données de la réponse GraphQL peuvent être dupliquées: actress “ Natalie Portman ”est extraite deux fois dans la réponse du premier composant, et lorsque l'on examine la sortie conjointe des deux composants, on peut également trouver des données partagées, telles que le film Star Wars: Épisode I – La menace fantôme .

De son côté, PoP normalise les données de la base de données et ne les imprime qu’une fois. Toutefois, il entraîne l’impression superficielle de la structure du module. Par conséquent, en fonction de la demande particulière comportant des données dupliquées ou non, l'API basée sur un schéma ou l'API basée sur un composant aura une taille plus petite.

En conclusion, une API basée sur un schéma telle que GraphQL et une interface basée sur un composant Les API telles que PoP présentent les mêmes performances et sont supérieures aux API basées sur les ressources telles que REST.

Lecture recommandée : Comprendre et utiliser les API REST

Propriétés particulières d'un composant Basée sur des API

Si une API basée sur des composants n'est pas nécessairement meilleure en termes de performances qu'une API basée sur un schéma, vous vous demandez peut-être, alors qu'est-ce que j'essaie de faire avec cet article?

Dans cette section, Je vais essayer de vous convaincre qu'une telle API a un potentiel incroyable, offrant plusieurs fonctionnalités très souhaitables, ce qui en fait un concurrent sérieux dans le monde des API. Je décris et démontre chacune de ses grandes caractéristiques uniques ci-dessous.

Les données à extraire de la base de données peuvent être déduites de la hiérarchie des composants

Lorsqu'un module affiche une propriété d'un objet de base de données, il peut ne pas savoir, ou soin, quel objet c'est; tout ce qui lui importe est de définir quelles propriétés de l'objet chargé sont requises.

Par exemple, considérons l'image ci-dessous. Un module charge un objet de la base de données (dans ce cas, un seul post), puis ses modules descendants affichent certaines propriétés de l'objet, telles que title et content : [19659088] Les données indiquées sont définies à différents intervalles « />

Alors que certains modules chargent l'objet de base de données, d'autres chargent des propriétés. ( Grand aperçu )

Ainsi, tout au long de la hiérarchie des composants, les modules de «chargement de données» seront chargés de charger les objets interrogés (le module chargeant la publication individuelle, dans ce cas), et ses modules descendants définissant les propriétés de l'objet de base de données. required ( title et content dans ce cas.)

L'extraction de toutes les propriétés requises pour l'objet de base de données peut s'effectuer automatiquement en parcourant la hiérarchie des composants: à partir du module de chargement de données. , nous parcourons tous ses modules descendants jusqu’à atteindre un nouveau module de chargement de données ou jusqu’à la fin de l’arborescence; à chaque niveau, nous obtenons toutes les propriétés requises, puis nous fusionnons toutes les propriétés et nous les interrogeons une fois dans la base de données.

Dans la structure ci-dessous, le module single-post extrait les résultats de la DB (le poste portant l'ID 37) et les sous-modules post-titre et post-contenu définissent les propriétés à charger pour l'objet DB interrogé ( titre et content respectivement); les sous-modules post-layout et fetch-next-post-button ne nécessitent aucun champ de données.

 "single-post"
  => Charger des objets avec le type d'objet "post" et l'ID 37
  modules
    "post-layout"
      modules
        "titre de l'article"
          => Charger la propriété "titre"
        "Publier un contenu"
          => Charger la propriété "contenu"
    "chercher-next-post-button"

La requête à exécuter est calculée automatiquement à partir de la hiérarchie des composants et de leurs champs de données requis, contenant toutes les propriétés requises par tous les modules et leurs sous-modules:

 SELECT
  titre, contenu
DE
  des postes
OÙ
  id = 37

En recherchant les propriétés à récupérer directement à partir des modules, la requête sera automatiquement mise à jour chaque fois que la hiérarchie des composants sera modifiée. Si, par exemple, nous ajoutons ensuite le sous-module post-thumbnail qui nécessite un champ de données thumbnail :

 "single-post"
  => Charger des objets avec le type d'objet "post" et l'ID 37
  modules
    "post-layout"
      modules
        "titre de l'article"
          => Charger la propriété "titre"
        "Publier un contenu"
          => Charger la propriété "contenu"
        "post-vignette"
          => Propriété de chargement "miniature"
    "chercher-next-post-button"

La requête est ensuite automatiquement mise à jour pour extraire la propriété supplémentaire:

 SELECT
  titre, contenu, miniature
DE
  des postes
OÙ
  id = 37

Comme nous avons défini les données d'objet de base de données à récupérer de manière relationnelle, nous pouvons également appliquer cette stratégie aux relations entre les objets de base de données eux-mêmes.

Considérez l'image ci-dessous: À partir du type d'objet post et en descendant dans la hiérarchie des composants, nous devrons déplacer le type d'objet de base de données sur utilisateur et comment correspondant à l'auteur du message et à chacun des commentaires du message, et puis, pour chaque commentaire, il doit changer à nouveau le type d'objet en utilisateur correspondant à l'auteur du commentaire.

Passage d'un objet de base de données à un objet relationnel (éventuellement en modifiant le type d'objet, comme dans post => auteur allant de post à utilisateur ou non, comme dans auteur => disciples à partir de utilisateur à utilisateur ) est w Ce que j'appelle «les domaines de commutation».


 Affichage des données pour les objets relationnels
Modification de l'objet de base de données d'un domaine à un autre. ( Grand aperçu )

Après avoir basculé vers un nouveau domaine, à partir de ce niveau de la hiérarchie des composants, toutes les propriétés requises seront soumises au nouveau domaine:

  • le nom est extrait de l'utilisateur . ] objet (représentant l'auteur du message),
  • contenu est extrait de l'objet comment (représentant chacun des commentaires du message),
  • nom est récupéré depuis l'objet user (représentant l'auteur de chaque commentaire).

En parcourant la hiérarchie des composants, l'API sait quand elle bascule vers un nouveau domaine et met à jour la requête pour extraire le objet relationnel.

Par exemple, si nous devons afficher les données de l'auteur du message, le sous-module d'empilement post-auteur modifiera le domaine à ce niveau de post au [ utilisateur et à partir de ce niveau, l'objet DB chargé dans le contexte transmis au module est l'utilisateur. Ensuite, les sous-modules nom d'utilisateur et utilisateur-avatar sous post-auteur vont charger les propriétés nom et avatar . sous l'objet utilisateur :

 "single-post"
  => Charger des objets avec le type d'objet "post" et l'ID 37
  modules
    "post-layout"
      modules
        "titre de l'article"
          => Charger la propriété "titre"
        "Publier un contenu"
          => Charger la propriété "contenu"
        "post-auteur"
          => Changer le domaine de "post" à "utilisateur", basé sur la propriété "auteur"
          modules
            "mise en page utilisateur"
              modules
                "Nom d'utilisateur"
                  => Charger la propriété "nom"
                "utilisateur-avatar"
                  => Charger la propriété "avatar"
    "chercher-next-post-button"

Résultat dans la requête suivante:

 SELECT
  p.title, p.content, p.author, u.name, u.avatar
DE
  messages p
JOINTURE INTERNE
  les utilisateurs vous
OÙ
  p.id = 37 ET p.author = u.id

En résumé, en configurant chaque module de manière appropriée, il n'est pas nécessaire d'écrire la requête pour extraire les données d'une API basée sur des composants. La requête est automatiquement produite à partir de la structure même de la hiérarchie des composants, en obtenant quels objets doivent être chargés par les modules de chargement de données, les champs à récupérer pour chaque objet chargé défini sur chaque module descendant et la commutation de domaine définie sur chaque module descendant. 19659005] L'ajout, la suppression, le remplacement ou la modification de tout module mettra automatiquement à jour la requête. Après avoir exécuté la requête, les données récupérées correspondront exactement à ce qui est requis – rien de plus.

Observer des données et calculer des propriétés supplémentaires

À partir du module de chargement de données dans la hiérarchie des composants, tout module peut observer les résultats renvoyés et calculer des éléments de données supplémentaires en fonction de celles-ci, ou les valeurs de retour qui sont placées sous l'entrée moduledata .

Par exemple, le module fetch-next-post-button ] peut ajouter une propriété indiquant s'il y a plus de résultats à extraire ou non (en fonction de cette valeur de retour, s'il n'y a pas plus de résultats, le bouton sera désactivé ou masqué):

 {
  moduledata: {
    "page": {
      modules: {
        "poste unique": {
          modules: {
            "fetch-next-post-button": {
              retour d'information: {
                hasMoreResults: true
              }
            }
          }
        }
      }
    }
  }
}

La connaissance implicite des données requises diminue la complexité et rend obsolète le concept de «point de terminaison»

Comme indiqué ci-dessus, l'API à base de composants peut extraire exactement les données requises, car elle contient le modèle de tous les composants du système. serveur et quels champs de données sont requis par chaque composant. La connaissance des champs de données requis peut alors être implicite.

L’avantage est que la définition des données requises par le composant peut être mise à jour uniquement côté serveur, sans avoir à redéployer des fichiers JavaScript, et le client peut être bête, demander simplement au serveur de fournir toutes les données dont il a besoin, réduisant ainsi la complexité de l'application côté client.

De plus, l'appel de l'API pour récupérer les données de tous les composants d'une URL spécifique peut être effectué simplement en interrogeant cette URL et en ajoutant le paramètre supplémentaire output = json pour indiquer le retour des données de l'API au lieu de l'impression de la page. Par conséquent, l'URL devient son propre noeud final ou, considéré différemment, le concept de «noeud final» devient obsolète.


 Demandes d'extraction de ressources avec différentes API
Requêtes d'extraction de ressources avec différentes API. ( Grand aperçu )

Récupération de sous-ensembles de données: possibilité d'extraire des données pour des modules spécifiques, à n'importe quel niveau de la hiérarchie des composants

Que se passe-t-il si nous n'avons pas besoin d'extraire les données de tous les modules d'une page, mais simplement les données pour un module spécifique commençant à n’importe quel niveau de la hiérarchie des composants? Par exemple, si un module implémente un défilement infini, lorsque vous faites défiler l'écran vers le bas, vous devez extraire uniquement les nouvelles données pour ce module et non pour les autres modules de la page.

Pour cela, vous pouvez filtrer les branches de la hiérarchie des composants. qui sera inclus dans la réponse, afin d'inclure les propriétés à partir du module spécifié et d'ignorer tout ce qui se situe au-dessus de ce niveau. Dans mon implémentation (que je décrirai dans un prochain article), le filtrage est activé en ajoutant le paramètre modulefilter = modulepaths à l'URL, et le ou les modules sélectionnés sont indiqués par un modulepaths. Paramètre [] où "chemin de module" est la liste des modules allant du module supérieur au module spécifique (par exemple module1 => module2 => module3 a chemin de module [ module1 module2 module3 ] et est passé comme paramètre d'URL sous la forme module1.module2.module3 ]).

Par exemple, dans la hiérarchie des composants située au-dessous de chaque module, une entrée dbobjectids :

 "module1"
  dbobjectides: [...]
  modules
    "module2"
      dbobjectides: [...]
      modules
        "module3"
          dbobjectides: [...]
        "module4"
          dbobjectides: [...]
        "module5"
          dbobjectides: [...]
          modules
            "module6"
              dbobjectides: [...]

Demander ensuite à l'URL de la page Web d'ajouter des paramètres modulefilter = modulepaths et modulepaths [] = module1.module2.module5 produira la réponse suivante:

 "module1"
  modules
    "module2"
      modules
        "module5"
          dbobjectides: [...]
          modules
            "module6"
              dbobjectides: [...]

En substance, l'API commence à charger des données à partir de module1 => module2 => module5 . C'est pourquoi le module module6 qui relève du module module5 apporte également ses données alors que module3 et module4 n'en ont pas.

En outre,

nous pouvons créer des filtres de module personnalisés pour inclure un ensemble de modules pré-arrangé. Par exemple, l'appel d'une page avec modulefilter = userstate peut uniquement imprimer les modules nécessitant un état utilisateur pour les rendre dans le client, tels que les modules module3 et module6 :

 "module1"
  modules
    "module2"
      modules
        "module3"
          dbobjectides: [...]
        "module5"
          modules
            "module6"
              dbobjectides: [...]

The information of which are the starting modules comes under section requestmetaunder entry filteredmodulesas an array of module paths:

requestmeta: {
  filteredmodules: [
    ["module1", "module2", "module3"],
    ["module1", "module2", "module5", "module6"]
  ]
}

This feature allows to implement an uncomplicated Single-Page Application, in which the frame of the site is loaded on the initial request:

"page"
  modules
    "navigation-top"
      dbobjectids: [...]
    "navigation-side"
      dbobjectids: [...]
    "page-content"
      dbobjectids: [...]

But, from them on, we can append parameter modulefilter=page to all requested URLs, filtering out the frame and bringing only the page content:

"page"
  modules
    "navigation-top"
    "navigation-side"
    "page-content"
      dbobjectids: [...]

Similar to module filters userstate and page described above, we can implement any custom module filter and create rich user experiences.

The Module Is Its Own API

As shown above, we can filter the API response to retrieve data starting from any module. As a consequence, every module can interact with itself from client to server just by adding its module path to the webpage URL in which it has been included.

I hope you will excuse my over-excitement, but I truly can’t emphasize enough how wonderful this feature is. When creating a component, we don’t need to create an API to go alongside with it to retrieve data (REST, GraphQL, or anything at all), because the component is already able to talk to itself in the server and load its own data — it is completely autonomous and self-serving.

Each dataloading module exports the URL to interact with it under entry dataloadsource from under section 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"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Fetching Data Is Decoupled Across Modules And DRY

To make my point that fetching data in a component-based API is highly decoupled and DRY (Don’t Repeat Yourself), I will first need to show how in a schema-based API such as GraphQL it is less decoupled and not DRY.

In GraphQL, the query to fetch data must indicate the data fields for the component, which may include subcomponents, and these may also include subcomponents, and so on. Then, the topmost component needs to know what data is required by every one of its subcomponents too, as to fetch that data.

For instance, rendering the component might require the following subcomponents:

Render :
  
    Country: {country}     {foreach films as film}            {/foreach}   
Render :   
    Title: {title}     Pic: {thumbnail}     {foreach actors as actor}            {/foreach}   
Render :   
    Name: {name}     Photo: {avatar}   

In this scenario, the GraphQL query is implemented at the level. Then, if subcomponent is updated, requesting the title through property filmTitle instead of titlethe query from the component will need to be updated, too, to mirror this new information (GraphQL has a versioning mechanism which can deal with this problem, but sooner or later we should still update the information). This produces maintenance complexity, which could be difficult to handle when the inner components often change or are produced by third-party developers. Hence, components are not thoroughly decoupled from each other.

Similarly, we may want to render directly the component for some specific film, for which then we must also implement a GraphQL query at this level, to fetch the data for the film and its actors, which adds redundant code: portions of the same query will live at different levels of the component structure. So GraphQL is not DRY.

Because a component-based API already knows how its components wrap each other in its own structure, then these problems are completely avoided. For one, the client is able to simply request the required data it needs, whichever this data is; if a subcomponent data field changes, the overall model already knows and adapts immediately, without having to modify the query for the parent component in the client. Therefore, the modules are highly decoupled from each other.

For another, we can fetch data starting from any module path, and it will always return the exact required data starting from that level; there are no duplicated queries whatsoever, or even queries to start with. Hence, a component-based API is fully DRY. (This is another feature that really excites me and makes me get wet.)

(Yes, pun fully intended. Sorry about that.)

Retrieving Configuration Values In Addition To Database Data

Let’s revisit the example of the featured-director component for the IMDB site described above, which was created — you guessed it! — with Bootstrap. Instead of hardcoding the Bootstrap classnames or other properties such as the title’s HTML tag or the avatar max width inside of JavaScript files (whether they are fixed inside the component, or set through props by parent components), each module can set these as configuration values through the API, so that then these can be directly updated on the server and without the need to redeploy JavaScript files. Similarly, we can pass strings (such as the title Featured director) which can be already translated/internationalized on the server-side, avoiding the need to deploy locale configuration files to the front-end.

Similar to fetching data, by traversing the component hierarchy, the API is able to deliver the required configuration values for each module and nothing more or less.

The configuration values for the featured-director component might look like this:

{
  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"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Please notice how — because the configuration properties for different modules are nested under each module’s level — these will never collide with each other if having the same name (e.g. property classes from one module will not override property classes from another module), avoiding having to add namespaces for modules.

Higher Degree Of Modularity Achieved In The Application

According to Wikipediamodularity means:

The degree to which a system’s components may be separated and recombined, often with the benefit of flexibility and variety in use. The concept of modularity is used primarily to reduce complexity by breaking a system into varying degrees of interdependence and independence across and ‘hide the complexity of each part behind an abstraction and interface’.

Being able to update a component just from the server-side, without the need to redeploy JavaScript files, has the consequence of better reusability and maintenance of components. I will demonstrate this by re-imagining how this example coded for React would fare in a component-based API.

Let’s say that we have a component, currently with two items: and like this:

Render :
  
  • Share on Facebook:
  • Share on Twitter:
  •   

But then Instagram got kind of cool, so we need to add an item to our component, too:

Render :
  
  • Share on Facebook:
  • Share on Twitter:
  • Share on Pinterest:
  •   

In the React implementation, as it can be seen in the linked code, adding a new component under component forces to redeploy the JavaScript file for the latter one, so then these two modules are not as decoupled as they could be.

In the component-based API, though, we can readily use the relationships among modules already described in the API to couple the modules together. While originally we will have this response:

{
  modulesettings: {
    "share-on-social-media": {
      modules: {
        "facebook-share": {
          configuration: {...}
        },
        "twitter-share": {
          configuration: {...}
        }
      }
    }
  }
}

After adding Instagram we will have the upgraded response:

{
  modulesettings: {
    "share-on-social-media": {
      modules: {
        "facebook-share": {
          configuration: {...}
        },
        "twitter-share": {
          configuration: {...}
        },
        "instagram-share": {
          configuration: {...}
        }
      }
    }
  }
}

And just by iterating all the values under modulesettings["share-on-social-media"].modulescomponent can be upgraded to show the component without the need to redeploy any JavaScript file. Hence, the API supports the addition and removal of modules without compromising code from other modules, attaining a higher degree of modularity.

Native Client-Side Cache/Data Store

The retrieved database data is normalized in a dictionary structure, and standardized so that, starting from the value on dbobjectidsany piece of data under databases can be reached just by following the path to it as indicated through entries dbkeyswhichever way it was structured. Hence, the logic for organizing data is already native to the API itself.

We can benefit from this situation in several ways. For instance, the returned data for each request can be added into a client-side cache containing all data requested by the user throughout the session. Hence, it is possible to avoid adding an external data store such as Redux to the application (I mean concerning the handling of data, not concerning other features such as the Undo/Redo, the collaborative environment or the time-travel debugging).

Also, the component-based structure promotes caching: the component hierarchy depends not on the URL, but on what components are needed in that URL. This way, two events under /events/1/ and /events/2/ will share the same component hierarchy, and the information of what modules are required can be reutilized across them. As a consequence, all properties (other than database data) can be cached on the client after fetching the first event and reutilized from then on, so that only database data for each subsequent event must be fetched and nothing else.

Extensibility And Re-purposing

The databases section of the API can be extended, enabling to categorize its information into customized subsections. By default, all database object data is placed under entry primaryhowever, we can also create custom entries where to place specific DB object properties.

For instance, if the component “Films recommended for you” described earlier on shows a list of the logged-in user’s friends who have watched this film under property friendsWhoWatchedFilm on the film DB object, because this value will change depending on the logged-in user then we save this property under a userstate entry instead, so when the user logs out, we only delete this branch from the cached database on the client, but all the primary data still remains:

{
  databases: {
    userstate: {
      films: {
        5: { 
          friendsWhoWatchedFilm: [22, 45]
        },
      }
    },
    primary: {
      films: {
        5: { 
          title: "The Terminator"
        },
      }
      "people": {
        22: {
          name: "Peter",
        },
        45: {
          name: "John",
        },
      },
    }
  }
}

In addition, up to a certain point, the structure of the API response can be re-purposed. In particular, the database results can be printed in a different data structure, such as an array instead of the default dictionary.

For instance, if the object type is only one (e.g. films), it can be formatted as an array to be fed directly into a typeahead component:

[
  { 
    title: "Star Wars: Episode I - The Phantom Menace",
    thumbnail: "..."
  },
  { 
    title: "Star Wars: Episode II - Attack of the Clones",
    thumbnail: "..."
  },
  { 
    title: "The Terminator",
    thumbnail: "..."
  },
]

Support For Aspect-Oriented Programming

In addition to fetching data, the component-based API can also post data, such as for creating a post or adding a comment, and execute any kind of operation, such as logging the user in or out, sending emails, logging, analytics, and so on. There are no restrictions: any functionality provided by the underlying CMS can be invoked through a module — at any level.

Along the component hierarchy, we can add any number of modules, and each module can execute its own operation. Hence, not all operations must necessarily be related to the expected action of the request, as when doing a POST, PUT or DELETE operation in REST or sending a mutation in GraphQLbut can be added to provide extra functionalities, such as sending an email to the admin when a user creates a new post.

So, by defining the component hierarchy through dependency-injection or configuration files, the API can be said to support Aspect-oriented programming“a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.”

Recommended reading: Protecting Your Site With Feature Policy

Enhanced Security

The names of the modules are not necessarily fixed when printed in the output, but can be shortened, mangled, changed randomly or (in short) made variable any way intended. While originally thought for shortening the API output (so that module names carousel-featured-posts or drag-and-drop-user-images could be shortened to a base 64 notation, such as a1a2 and so on, for the production environment), this feature allows to frequently change the module names in the response from the API for security reasons.

For instance, input names are by default named as their corresponding module; then, modules called username and passwordwhich are to be rendered in the client as and respectively, can be set varying random values for their input names (such as zwH8DSeG and QBG7m6EF today, and c3oMLBjo and c46oVgN6 tomorrow) making it more difficult for spammers and bots to target the site.

Versatility Through Alternative Models

The nesting of modules allows to branch out to another module to add compatibility for a specific medium or technology, or change some styling or functionality, and then return to the original branch.

For instance, let’s say the webpage has the following structure:

"module1"
  modules
    "module2"
      modules
        "module3"
        "module4"
          modules
            "module5"
              modules
                "module6"

In this case, we’d like to make the website also work for AMP, however, modules module2module4 and module5 are not AMP compatible. We can branch these modules out into similar, AMP-compatible modules module2AMPmodule4AMP and module5AMPafter which we keep loading the original component hierarchy, so then only these three modules are substituted (and nothing else):

"module1"
  modules
    "module2AMP"
      modules
        "module3"
        "module4AMP"
          modules
            "module5AMP"
              modules
                "module6"

This makes it fairly easy to generate different outputs from a single codebase, adding forks only here and there as needed, and always scoped and restrained to individual modules.

Demonstration Time

The code implementing the API as explained in this article is available in this open-source repository.

I have deployed the PoP API under https://nextapi.getpop.org for demonstration purposes. The website runs on WordPress, so the URL permalinks are those typical to WordPress. As noted earlier, through adding parameter output=json to them, these URLs become their own API endpoints.

The site is backed by the same database from the PoP Demo website, so a visualization of the component hierarchy and retrieved data can be done querying the same URL in this other website (e.g. visiting the https://demo.getpop.org/u/leo/ explains the data from https://nextapi.getpop.org/u/leo/?output=json).

The links below demonstrate the API for cases described earlier on:


Example of JSON code returned by the API
Example of JSON code returned by the API. (Large preview)

Conclusion

A good API is a stepping stone for creating reliable, easily maintainable and powerful applications. In this article, I have described the concepts powering a component-based API which, I believe, is a pretty good API, and I hope I have convinced you too.

So far, the design and implementation of the API have involved several iterations and taken more than five years — and it’s not completely ready yet. However, it is in a pretty decent state, not ready for production but as a stable alpha. These days, I am still working on it; working on defining the open specification, implementing the additional layers (such as rendering) and writing documentation.

In an upcoming article, I will describe how my implementation of the API works. Until then, if you have any thoughts about it — regardless whether positive or negative — I would love to read your comments below.

Smashing Editorial(rb, ra, yk, il)




Source link