Développer (… et maintenir) une application Web à moindre coût – 2/2

Dans la première partie de l’article, nous avons vu quelle était l’architecture à mettre en place pour développer une application web à moindre coût. Nous avons également déterminé les critères de sélection des technologies. Il nous reste, pour la partie front-end, à définir comment packager et déployer la solution. (Version PDF complète de l’article)

L’enfer du packaging

Le développement d’applications web à base de Web component en TypeScript va nécessiter de faire du transpilage pour générer du JavaScript compréhensible par les navigateurs, et de regrouper le code des différents Web components. En effet, si pour un web component on a deux fichiers (template + controller/vueModel), alors l’affichage d’une page va nécessiter le téléchargement de plusieurs dizaines de fichiers (il faut également compter les images et les CSS). Cela va générer des temps d’affichage pouvant dépasser les 10 secondes dans certains cas.

Le tout en un

Il nous faut donc trouver un outil de build permettant de réaliser ces deux tâches. Après quelques recherches, Webpack 2 semble l’outil tout désigné pour réaliser cette tâche. Il dispose de ‘loader’ permettant de traiter les différents fichiers.

Cliquez pour agrandir

 

La configuration présentée est censée permettre de packager en un seul fichier JavaScript ES5 notre code. Mais il s’avère que la classe HTMLElement est un peu particulière, et nécessite la mise en place de polyfill pour que le transpilage soit fonctionnel.

L’utilisation d’un loader Babel permet de facilement régler le problème. Le premier écueil dû au transpilage TypeScript à JavaScript a ainsi été évité.

La configuration ci-après est pleinement fonctionnelle.

Cliquez pour agrandir

Utilisation d’un CDN

L’intérêt de l’approche modulaire est la ré-utilisabilité de ses composants, ils ont pour but d’être utilisés non seulement dans plusieurs pages mais aussi dans plusieurs applications. Les packager dans chacune des applications n’est pas une solution satisfaisante en terme de maintenance car une correction sur un composant nécessite le redéploiement de toutes les applications qui l’utilise.

Principe de mutualisation des composants

Coté back-end, ce problème est déjà résolu grâce à l’OSGI, car un composant, par exemple de la couche Business, peut être utilisé par autant d’application que nécessaire et sa mise à jour peut se faire de manière unitaire sans avoir à re-déployer ni arrêter les applications qui l’utilisent. Il nous faut trouver une approche similaire pour le front-end, c’est là qu’intervient l’utilisation d’un CDN (Content delivery network).

Il a pour fonction de fournir les composants aux différentes applications qui en ont besoin.

On remarque sur le schéma que tous les composants ne sont pas systématiquement réutilisables, certains peuvent êtres spécifiques à une application et donc fournis directement par celle-ci.

Prise en compte du CDN dans le packaging

Dans le paragraphe ‘Le tout en un’, nous avons utilisé Webpack pour le build de notre projet. Il nous faut maintenant lui indiquer que certain des composants ne font pas partie du projet et doivent être directement récupérés depuis le CDN (en paramétrant si possible la localisation du CDN) au moment du runtime.

Malheureusement Webpack n’en est pas capable, car il n’a tout simplement pas été développé dans ce but.

En effet, Webpack identifie tous les modules (au sens JavaScript) de notre développement en créant un tableau et assure l’injection de dépendances entre modules en se basant sur leur index dans ce tableau. Un module externe n’étant pas recensé, il ne peut être injecté.

La notion d’injection de dépendance va être le nœud du problème. Non seulement les composants peuvent se trouver sur un CDN mais en plus un fichier sur le CDN peut contenir plusieurs composants pour éviter la problématique des téléchargements multiples énoncée au début de ce chapitre. Pour que cela puissent fonctionner, il faut que l’on puisse désigner les composants non seulement par leur localisation mais aussi par un identifiant.

Il existe 3 méthodes, issues de spécifications, permettant de gérer les dépendances entre modules : AMD, CommonJS et la directive import (ES2015). Et il va nous falloir des modules loader spécifiques pour définir la manière de charger nos dépendances. Cette notion de modules loader n’existe pas en ES2015 et est à l’état de proposition pour CommonJS. La seule option restante est donc AMD.

Il nous suffit d’implémenter les modules loader cdn et pack pour répondre au besoin. Ce qui est relativement simple en utilisant l’implémentation RequireJS. Ce choix n’est pas totalement innocent car RequireJS possède un optimiseur qui va nous être utile lors du packaging.

Les syntaxes que l’on obtient sont :

  module jquery@1.2.3.js depuis le cdn
  module jquery depuis le package util.js
  module jquery depuis le package util.js sur le cdn

Mais ce n’est que le JavaScript que l’on veut obtenir, rappelons que nous avons décidé de développer en TypeScript. Les dépendances se font uniquement via la directive import.

Il n’est pas souhaitable à ce moment-là de spécifier où se trouve la dépendance car c’est du ressort du build et nous devons pouvoir changer la localisation en fonction des environnements (développement, production, …). On rentre alors de plein pied dans les inconvénients du transpilage, et les réticences que j’avais émises se trouvent complétement justifiées.

Le tableau n’est quand même pas aussi noir que ce qu’il paraît. On a prouvé que le transpilage du TypeScript en ECMAScript ES2107 marche bien et celui de ECMAScript ES2017 en JavaScript via babel fonctionne également très bien. Il nous suffirait, entre ces deux étapes, de modifier les chemins d’import (Ex : import * as $ from ‘jquery’ ) pour faire le job.

La prochaine étape est donc de trouver le bon outil de build.

Choix d’un outil de build

Je vous rassure tout de suite, il n’existe pas d’outil aussi avancé que Webpack pour réaliser notre build. On cherche donc un outil fortement paramétrable et évolutif, dans le monde du JavaScript il en existe deux principaux Grunt et Gulp. Personnellement je ne trouve pas que l’un soit meilleur que l’autre, j’ai donc arbitrairement choisi d’utilisé Gulp.

On le configure pour générer :

  • de l’ECMAScript 2017,
  • des modules UMD (compatibles avec CommonJS et AMD).

 

 

 

Et on définit la localisation de nos bundles.

 

Il n’y a plus qu’à définir la tâche de transpilage :

On remarque que l’on a comme dépendance une tache de compilation (« compile ») car il faut s’assurer que le code compile avant de le modifier. On remplace ensuite les imports (CSS, HTML, Localisation) en fonction des modules loaders que l’on veut activer puis on lance le transpilage, en ECMA2017 puis en ES5 via babel.

Pour le packaging permettant de regrouper les fichiers, j’utilise une version personnalisée du plugin gulp-requirejs-bundler qui permet de spécifier finement les composants que je désire regrouper.

Le résultat est celui attendu : 3 fichiers main.js, page1.js et page2.js. Le fichier main incluant toutes les dépendances communes au site et appelant en just in time, les packages des pages 1 et 2 qui contiennent leur code et celui des composants qu’elles incluent.

Et les couches Business et Consumer du front-end dans tout ça ?

Pour l’instant nous nous sommes intéressés à la couche Provider du front-end, pour essayer de trouver une approche permettant de nous décorréler des frameworks pour l’implémentation des Web components.

Si nous reprenons le schéma standard d’une application modulaire, il en va de même pour les autres couches. Il va donc falloir définir des contrats d’interface pour chacun de nos composants. Et les communications ne pourront se faire qu’au travers ces interfaces.

Cette approche est en fait utilisée systématiquement dans les développements back-end (qui n’a pas défini une interface pour accéder à un DAO ?). Il est étonnant qu’elle ne soit pas systématiquement utilisée dans les développements front-end (si ce n’est la volonté de vouloir produire le plus rapidement possible, l’introduction a démontré que c’est une approche la plus couteuse au final que l’approche composant).

L’injection directe de dépendance n’est alors plus possible. Car c’est une approche déterministe, avec l’AMD, CommonJS ou les imports on spécifie l’implémentation (~le fichier JS) que l’on veut utiliser. Un changement de l’implémentation d’une interface oblige à modifier tous les composants qui en dépendent. L’approche correcte est de sélectionner/filtrer automatiquement le composant exposant l’interface souhaité. C’est exactement le fonctionnement de Spring (@Autowired), de CDI (@Inject) ou de l’OSGI (ServiceTracker).

Gestionnaire de service

Il nous faut donc mettre à disposition un registre de service. La notion d’interface en Javascript n’existant pas, il nous faut également un registre d’API. L’objectif est qu’un composant s’inscrive auprès du registre de service comme implémentant une ou plusieurs API.

Ce code donne un exemple de déclaration de l’API de publish/subscribe. Vous noterez que cette description donne la liste des méthodes (subscribe, publish) ainsi que leur signature, paramètres et valeur de retour (via JSON Schema).

Cette description est nécessaire car l’enregistrement des services se fait au runtime. Et il faut valider qu’ils implémentent bien l’API déclarée.

On pourrait penser que l’utilisation des interfaces TypeScript pourrait pallier à cette vérification, mais dans ce cas, la validation se fait au build time.

L’enregistrement d’un service se fait simplement en précisant l’implémentation choisie (Ici un exemple utilisant la librairie PubSub).

Ce principe peut être utilisé pour exposer n’importe quel service de nos composants.

Le référencement du service se fait uniquement en précisant l’API désirée Le registre de service fournit alors l’implémentation correspondante.

Versionning et filtrage

Il faut également définir la version de l’API et les propriétés (facultatives) du service. Les utilisateurs d’OSGI sont familiers avec cette pratique, car elle est indispensable pour la maintenance des applications. Vous l’avez peut-être remarqué, mais dans l’exemple de déclaration de service du paragraphe précédent, nous n’avons pas seulement précisé l’API :
Le filtrage sert lorsque plusieurs composants exposent la même API. Envisageons une API CRUD, les propriétés permettent de spécifier sur quelle entité s’applique le service CRUD exposée.

Quand on parle de versionning, il s’agit obligatoirement de versionning sémantique, et on doit pouvoir préciser lors du référencement service un intervalle de versions.

Il est important, pour les versions mineures, de s’assurer que l’implémentation du service contient bien toutes les fonctionnalités attendues et pour les versions majeures qu’il y a bien compatibilité avec le composant que vous être en train de développer.

Il n’est pas commun, d’avoir au sein d’une même application plusieurs versions d’un même service, et donc plusieurs versions de la même interface, pourtant ce cas arrive fréquemment.

Mettons-nous dans le contexte d’une application gérant la gestion d’identité dans votre SI. Elle propose pour toutes les autres applications des services REST pour faire la recherche et la mise à jour des utilisateurs.

Et elle propose également les composants client (couche Consumer) pour que les applications puissent facilement interroger ces services. Ces composants sont mis à disposition via le CDN. Et ces composants sont utilisés dans différentes pages de différentes applications.

Lorsque l’application de gestion des identités évolue, elle peut faire évoluer son API en V2 (non compatible V1). Les composants clients en place ne sont plus compatibles. On pourrait penser qu’il suffit de déployer sur le CDN la nouvelle version des composants client pour que tout fonctionne automatiquement. Mais ces derniers exposent, au sein des applications 1 & 2, une API en correspondance avec l’API REST. La nouvelle version des composants clients n’est alors pas compatible avec les composants de la couche business.

La solution de modifier l’ensemble des composants de la couche business est inenvisageable pour des questions de coût. Il nous faut donc trouver une solution permettant aux anciens développements de fonctionner (compatible V1), et au nouveau développement de pouvoir prendre en compte la nouvelle API (V2). Nous aurons ainsi inévitablement au sein de notre application deux versions de la même API.

Une alternative serait de faire évoluer les composants client V1, en une version V1.1, qui fait l’adaptation des appels de l’API cliente V1 vers l’API REST V2.

Ces composants étant sur le CDN, ils sont automatiquement pris en compte par l’ensemble des applications et le coût de maintenance est réduit à son minimum.

Les nouveaux développements peuvent utiliser la dernière version de l’API REST. La migration complète vers cette version peut se faire au rythme des cycles de vie des différentes applications.

Une démarche similaire peut-être envisagée au sein d’une même application quand le nombre d’écrans ou de composants impactés engendre des coûts de maintenance évolutive importants.

Conclusion

Cet article a défini une solution permettant de développer des applications entièrement modulaires. Si la partie back-end reste dans les standards actuels de développement, la partie front-end propose une nouvelle manière de développer nos frontaux web.

La tendance actuelle est d’utiliser les derniers frameworks éprouvés. Cela revient, pour le choix des technologies, à répondre à la question avec quoi (cf. §Choix des technologies) sans réellement envisager le comment.

Il est évident que la solution proposée implique un coût de développement initial supérieur, ne serait-ce que pour la définition des API. Mais elle assure un coût global largement inférieur.

Cette solution n’est évidemment pas une silver bullet, si votre besoin est le développement d’un frontal web :

  • De quelques pages,
  • N’ayant pas besoin de réutiliser des composants existants,
  • Ne fournissant pas de fonctionnalités réutilisables par d’autres applications,
  • N’étant pas destiné à évoluer fonctionnellement.

Alors cette approche n’est pas forcement la plus rentable. Dans les autres cas, elle assure une diminution du coût du cycle de vie de vos applications et de votre SI.

Leave a Reply

Your email address will not be published. Required fields are marked *