Problème de lien profond HATEOAS

Liens externes (liens profonds) - sur Internet, il s'agit du placement d'un lien hypertexte sur un site qui pointe vers une page sur un autre site Web, au lieu de pointer vers la page d'accueil (accueil, démarrage) de ce site. Ces liens sont appelés liens externes (liens profonds).
Wikipédia
Le terme «liens profonds» sera utilisé plus loin comme le plus proche des «liens profonds» en langue anglaise. Cet article se concentrera sur l'API REST, donc des liens profonds signifieront des liens vers des ressources HTTP. Par exemple, le lien profond habr.com/en/post/426691 pointe vers un article spécifique sur habr.com.

HATEOAS est un composant de l'architecture REST qui permet de fournir des informations aux clients API via hypermédia. Le client connaît la seule adresse fixe, le point d'entrée de l'API; il apprend toutes les actions possibles à partir des ressources reçues du serveur. Les vues de ressources contiennent des liens vers des actions ou d'autres ressources; le client interagit avec l'API, sélectionnant dynamiquement une action parmi les liens disponibles. Vous pouvez en savoir plus sur HATEOAS sur Wikipedia ou dans ce merveilleux article sur Habré.

HATEOAS est le prochain niveau de l'API REST. Grâce à l'utilisation d'hypermédia, il répond à de nombreuses questions qui se posent lors du développement de l'API: comment contrôler l'accès aux actions côté serveur, comment se débarrasser de la connectivité étroite entre le client et le serveur, comment changer les adresses des ressources si nécessaire. Mais il ne fournit pas de réponse à la question de savoir à quoi devraient ressembler les liens profonds avec les ressources.

Dans l'implémentation REST "classique", le client connaît la structure des adresses, il sait comment obtenir une ressource par identifiant dans l'API REST. Par exemple, un utilisateur suit un lien profond vers une page de livre dans une boutique en ligne. La barre d'URL https://domain.test/books/1 s'affiche dans la barre d'adresse du navigateur. Le client sait que «1» est l'identifiant de la ressource du livre, et pour l'obtenir, vous devez remplacer cet identifiant dans l'URL de l'API REST https://api.domain.test/api/books/{id} . Ainsi, le lien profond vers la ressource de ce livre dans l'API REST ressemble à ceci: https://api.domain.test/api/books/1 .

Dans HATEOAS, le client ne connaît pas les identificateurs de ressource ni la structure d'adresse. Il ne code pas en dur, mais "découvre" les liens. De plus, la structure des URL peut changer à l'insu du client, HATEOAS le permet. En raison de ces différences, les liens profonds ne peuvent pas être implémentés de la même manière que l'API REST classique. Étonnamment, une recherche sur Internet de recettes pour implémenter de tels liens dans HATEOAS n'a pas donné un grand nombre de résultats, seulement quelques questions déroutantes sur Stackoverflow. Par conséquent, nous considérerons plusieurs options possibles et essayerons de choisir la meilleure.

L'option zéro en dehors de la compétition n'est pas de mettre en place des liens profonds. Cela peut convenir à certains administrateurs ou applications mobiles qui ne nécessitent pas la possibilité de passer directement aux ressources internes. C'est tout à fait dans l'esprit de HATEOAS, l'utilisateur ne peut ouvrir les pages que séquentiellement, à partir du point d'entrée, car le client ne sait pas comment accéder directement à la ressource interne. Mais cette option ne convient pas aux applications Web - nous nous attendons à ce que le lien vers la page interne puisse être mis en signet, et la mise à jour de la page ne nous renverra pas à la page principale du site.

Donc, la première option: le code dur de l'URL de l'API HATEOAS. Le client connaît la structure des adresses de ressource pour lesquelles des liens profonds sont nécessaires et sait comment obtenir l'identifiant de ressource pour la recherche. Par exemple, le serveur renvoie l'adresse https://api.domain.test/api/books/1 comme référence à la ressource de livre. Le client sait que «1» est l'identifiant du livre et peut générer lui-même cette URL en cliquant sur le lien profond. C'est certainement une option de travail, mais viole les principes de HATEOAS. La structure d'adresse et l'identifiant de ressource ne peuvent plus être modifiés, sinon le client se cassera, il y a une connexion rigide. Ce n'est pas HATEOAS, ce qui signifie que l'option ne nous convient pas.

La deuxième option consiste à remplacer l'URL de l'API REST dans l'URL du client. Pour un exemple avec un livre, le lien profond ressemblera à ceci: https://domain.test/books?url=https://api.domain.test/api/books/1 . Ici, le client prend le lien de ressource reçu du serveur et le remplace entièrement dans l'adresse de la page. Cela ressemble plus à HATEOAS, le client ne connaît pas les identifiants et la structure des adresses, il reçoit un lien et l'utilise tel quel. En cliquant sur un tel lien approfondi, le client recevra la ressource souhaitée via le lien API REST du paramètre url. Il semblerait que la solution fonctionne, et tout à fait dans l'esprit de HATEOAS. Mais si vous ajoutez un tel lien à vos signets, à l'avenir, nous ne pourrons plus modifier l'adresse de la ressource dans l'API (ou nous devrons toujours rediriger vers une nouvelle adresse). Encore une fois, l'un des avantages de HATEOAS est perdu, cette option n'est pas idéale non plus.

Ainsi, nous voulons avoir des permaliens, qui peuvent cependant changer. Une telle solution existe et est largement utilisée sur Internet - de nombreux sites fournissent des liens courts vers des pages internes qui peuvent être partagées. En plus de la brièveté, leur avantage est que le site peut changer l'adresse réelle de la page, mais ces liens ne se rompent pas. Par exemple, Microsoft utilise des liens Windows pour aider les pages du formulaire http://go.microsoft.com/fwlink/?LinkId=XXX . Au fil des ans, les sites Microsoft ont été repensés plusieurs fois, mais les liens dans les anciennes versions de Windows continuent de fonctionner.

Il ne reste plus qu'à adapter cette solution à HATEOAS. Et c'est la troisième option - en utilisant des identifiants de liens profonds uniques dans l'API REST. L'adresse de la page du livre ressemble maintenant à ceci: https://domain.test/books?deepLinkId=3f0fd552-e564-42ed-86b6-a8e3055e2763 . En cliquant sur un tel lien approfondi, le client doit demander au serveur: quel lien de ressource correspond à un deepLinkId identifiant deepLinkId ? Le serveur renverra le lien https://api.domain.test/api/books/1 (enfin, ou immédiatement une ressource, pour ne pas y aller deux fois). Si l'adresse de ressource dans l'API REST change, le serveur renverra simplement un autre lien. Un enregistrement est sauvegardé dans la base de données que l'identifiant de référence 3f0fd552-e564-42ed-86b6-a8e3055e2763 correspond à l'identifiant d'entité du livre 1.

Pour cela, les ressources doivent contenir un champ deepLinkId avec les identifiants de leurs liens profonds, et le client doit les substituer dans l'adresse de la page. Cette adresse peut être mise en signet en toute sécurité et envoyée à des amis. Ce n'est pas très bien que le client travaille indépendamment avec certains identifiants, mais cela vous permet de préserver les avantages de HATEOAS pour l'API dans son ensemble.

Exemple


Cet article ne serait pas complet sans un exemple d'implémentation. Pour tester le concept, considérons un exemple d'un site de catalogue de boutique en ligne hypothétique avec un backend sur Spring Boot / Kotlin et un frontend SPA sur Vue / JavaScript. Le magasin vend des livres et des crayons, le site comprend deux sections dans lesquelles vous pouvez voir la liste des produits et ouvrir leurs pages.

Section "Livres":



Une page de livre:



Pour le stockage des marchandises, les entités Spring Data JPA sont définies:

 enum class EntityType { PEN, BOOK } @Entity class Pen(val color: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.PEN, id) } @Entity class Book(val name: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.BOOK, id) } @Entity class DeepLink( @Enumerated(EnumType.STRING) val entityType: EntityType, @Column(columnDefinition = "uuid") val entityId: UUID ) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() } 

Pour créer et stocker des DeepLink lien profond, l'entité DeepLink est DeepLink , dont une instance est créée avec chaque objet de domaine. L'identifiant lui-même est généré selon la norme UUID au moment de la création de l'entité. Sa table contient l'identifiant du lien profond, l'identifiant et le type de l'entité vers laquelle le lien mène.

L'API REST du serveur est organisée selon le concept HATEOAS, le point d'entrée de l'API contient des liens vers les collections de produits, ainsi qu'un lien #deepLink pour former des liens profonds en substituant un identifiant:

 GET http://localhost:8080/api { "_links": { "pens": { "href": "http://localhost:8080/api/pens" }, "books": { "href": "http://localhost:8080/api/books" }, "deepLink": { "href": "http://localhost:8080/api/links/{id}", "templated": true } } } 

Le client, lors de l'ouverture de la section "Livres", demande une collection de ressources sur le lien #books au point d'entrée:

 GET http://localhost:8080/api/books ... { "name": "Harry Potter", "deepLinkId": "4bda3c65-e5f7-4e9b-a8ec-42d16488276f", "_links": { "self": { "href": "http://localhost:8080/api/books/1272e287-07a5-4ebc-9170-2588b9cf4e20" } } }, { "name": "Cryptonomicon", "deepLinkId": "a23d92c2-0b7f-48d5-88bc-18f45df02345", "_links": { "self": { "href": "http://localhost:8080/api/books/5d04a6d0-5bbc-463e-a951-a9ff8405cc70" } } } ... 

SPA utilise Vue Router, pour lequel le chemin d'accès à la page du livre est défini { path: '/books/:deepLinkId', name: 'book', component: Book, props: true } , et les liens dans la liste des livres ressemblent à ceci: <router-link :to="{name: 'book', params: {link: book._links.self.href, deepLinkId: book.deepLinkId}}">{{ book.name }}</router-link> .

Autrement dit, lorsque vous ouvrez la page d'un livre spécifique, le composant Book est appelé, qui reçoit deux paramètres: link (lien vers la ressource book dans l'API REST, valeur du champ href du lien #self ) et deepLinkId de la ressource).

 const Book = { template: `<div>{{ 'Book: ' + book.name }}</div>`, props: { link: null, deepLinkId: null }, data() { return { book: { name: "" } } }, mounted() { let url = this.link == null ? '/api/links/' + this.deepLinkId : this.link; fetch(url).then((response) => { return response.json().then((json) => { this.book = json }) }) } } 

Vue Router définit la valeur de deepLinkId à l'adresse de la page /books/:deepLinkId , et le composant demande la ressource par lien direct à partir de la propriété link . Lors du forçage d'une actualisation de page, Vue Router définit la propriété de composant deepLinkId , en la récupérant à partir de l'adresse de page. La propriété de link reste null . Le composant vérifie: s'il existe un lien direct obtenu à partir de la collection, la ressource y est demandée. Si seul l'identifiant deepLinkId est deepLinkId , il est substitué au lien #deepLink partir du point d'entrée pour recevoir la ressource par le lien profond.

Sur le backend, la méthode du contrôleur pour les liens profonds ressemble à ceci:

 @GetMapping("/links/{id}") fun deepLink(@PathVariable id: UUID?, response: HttpServletResponse?): ResponseEntity<Any> { id!!; response!! val deepLink = deepLinkRepo.getOne(id) val path: String = when (deepLink.entityType) { EntityType.PEN -> linkTo(methodOn(MainController::class.java).getPen(deepLink.entityId)) EntityType.BOOK -> linkTo(methodOn(MainController::class.java).getBook(deepLink.entityId)) }.toUri().path response.sendRedirect(path) return ResponseEntity.notFound().build() } 

Par identifiant est l'essence du lien profond. Selon le type d'entité d'application, un lien est formé avec la méthode du contrôleur, qui renvoie sa ressource par entityId . La demande est redirigée vers cette adresse. Ainsi, si à l'avenir la liaison avec le contrôleur d'entité change, il sera possible de changer simplement la logique de formation de la liaison dans la méthode deepLink .

Le code source complet de l'exemple est disponible sur Github .

Source: https://habr.com/ru/post/fr445092/


All Articles