Accueil Nos publications Blog Comment migrer un code legacy vers une architecture plus moderne et modulaire ?

Comment migrer un code legacy vers une architecture plus moderne et modulaire ?

Sommaire

  1. Introduction
  2. Utilisation de npm
  3. Modularisation et optimisation de l’application avec Webpack
  4. Implémentation de la syntaxe ES6+
  5. Mise en place de modules avec Webpack
  6. Convertir du SCSS en CSS
  7. Mettre en place des environnements de production et de développement
  8. Installation et configuration de TypeScript
  9. Amélioration de la performance avec le lazy loading des modules
  10. Se débarrasser de jQuery
  11. Conclusion

Introduction

Durant ma dernière mission chez l’un de nos clients grand compte, j’ai été confronté au défi de gérer et maintenir un code legacy * avec TypeScript.

Ce challenge peut être à mon avis un cauchemar pour certains développeurs.

 * Un système hérité, système patrimonial ou système obsolète est un matériel ou un logiciel continuant d’être utilisé dans une organisation, alors qu’il est supplanté par des systèmes plus modernes. (Définition de Wikipédia)

Cet article est destiné aux développeurs qui doivent réaliser des évolutions et répondre aux nouveaux besoins fonctionnels, tout en maintenant un code existant et en évitant de faire des régressions.

Un code legacy est généralement considéré comme problématique pour différentes raisons :

  • Il est difficile à maintenir par un manque de documentation, de compétences…
  • La prise en main peut être longue et donc coûteuse
  • Il y a des dépendances qui sont devenues obsolètes
  • Il y a des vulnérabilités liées aux difficultés d’apporter de correctif
  • Il existe des risques de lenteurs dus à un code mal optimisé

Dans cet article je vais détailler mon retour d’expérience sur la migration progressive d’une application développée avec la bibliothèque jQuery vers un code plus moderne et modulaire en TypeScript.

Nous avons opté pour l’utilisation de TypeScript puisque les frameworks modernes (Angular, React…) l’utilisent.
TypeScript permet aux développeurs d’écrire un code plus fiable et beaucoup plus facile à réécrire tout en évitant les erreurs.

Nous avons continué à maintenir le code existant en JS, tout en développant des nouvelles fonctionnalités en TypeScript.

L’objectif était d’arriver à faire des modules en React ou des Webcomponents, réutilisables et plus faciles à maintenir.

Utilisation de npm

Node Package Manager (npm) est un gestionnaire de paquets le plus populaire dans l’écosystème Node.js. Il permet aux développeurs de gérer et de déployer des paquets utiles rapidement et facilement.
La mise en place de npm nous a permis de mieux gérer les dépendances du projet en utlisant le CLI pour installer, mettre à jour et supprimer des paquets et ainsi accélérer le temps de développement en utilisant des modules tiers nécessaires au projet.

Voici les étapes et quelques exemples de commandes utilisés pour mettre en place et utiliser npm dans notre projet.

Création d’un fichier package.json


Tout d’abord on a besoin de Node.js installé sur la machine de développement. En installant Node.js, npm est aussi installé.
Pour vérifier que Node.js et npm sont bien installés, il suffit d’ouvrir un Terminal ou Invite de commande et taper `node -v` qui devrait nous renvoyer la version de Node.js installée et `npm -v` qui devrait nous renvoyer la version de npm installée.
Pour installer la dernière version de npm, on peut taper la commande :

npm install -g npm


Ensuite nous avons créé un fichier package.json qui va détailler les modules Node.js dépendants du projet pour faciliter la gestion et l’installation d’un paquet.
Pour cela nous avons utilisé la commande `npm init` à la racine du projet.

Installation de modules

Il existe deux méthodes d’installer des modules avec npm : soit localement, soit globalement.


Pour installer les paquets tiers nécessaires au projet on utilise la commande :

npm install <package_name>


Pour spécifier que le paquet sera sauvegardé comme une dépendance du projet, on doit ajouter le flag optionnel –save.

npm install <package_name> --save


Le paquet va être ajouté aux dépendances (Dependencies) dans le fichier package.json.

Les dépendances de développement ( devDependencies) sont les paquets qui ne sont nécessaires que pendant le développement d’un projet mais pas pour le construire ou le faire fonctionner en production.
Pour installer et enregistrer un paquet comme une dépendance qui n’est nécessaire que pour le développement on utilise le flag optionnel :

npm install <package_name> --save-dev


Lorsque on installe un paquet, un dossier node_modules est créé automatiquement pour stocker les dépendances nécessaires à notre projet ainsi qu’un fichier package-lock.json.

On peut aussi installer des paquets globalement pour qu’ils soient disponibles dans l’ensemble du système. Cela est utile pour les outils comme par exemple une CLI.
On doit donc utiliser la commande :

npm install <package-name> --global` ou `npm i <package-name> -g

Mettre à jour les modules

Npm permet aussi de vérifier et mettre à jour les modules afin que notre projet reste stable.
On peut vérifier quels paquets npm doivent être mis à jour en exécutant la commande :

npm outdated

Il suffit après de lancer la commande update ou up pour mettre à jour un paquet

npm update <package-name>


Désinstaller les modules

Enfin, pour supprimer un paquet, nous pouvons utiliser la commande :

npm uninstall <package-name>


Ce module ne sera plus présent dans le dossier node_modules, ni dans les fichiers package.json et package-lock.json.

Pour aller plus loin :
https://nodejs.org/fr
https://docs.npmjs.com/downloading-and-installing-packages-locally

Modularisation et optimisation de l’application avec Webpack

Webpack est un outil (bundler) qui permet de packager les fichiers (assets) de notre application et leurs dépendances en les regroupant en un seul ou plusieurs fichiers compilés (bundles) selon plusieurs standards d’import (CommonJS , AMD, etc.).
Il peut être étendu à l’aide de plugins et “loaders” tiers pour permettre d’importer d’autres types de fichier tels que les images, les templates et les feuilles de style ce qui permet d’avoir de vrais modules indépendants.
Cela est très utile surtout pour la performance de notre application.

Installation de Webpack
Pour installer Webpack, il faut taper simplement :

npm install --save-dev webpack@latest webpack-cli@latest webpack-dev-server@latest

Configuration de Webpack
On va créer le fichier webpack.config.js à la racine du projet soit à la main soit en utilisant la commande touch
`touch webpack.config.js`
Tout d’abord on va préciser dans ce fichier le point d’entrée (entry) et de sortie (output)
Voici un exemple de configuration :

const path = require('path'); 
module.exports = {
  entry: './src/index.js',  // ./path/to/my/file.js
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js', // my-app-bundle-name.js
  },
};

Babel : écrire un code JavaScript récent (ES6+) et le convertir en ES5

Babel est un outil de transpilation de code (Transpileur), il permet de compiler le code JavaScript récent en ES5 compatible avec les navigateurs modernes.
Babel utilise des “presets” et des “plugins” afin de gérer les différentes versions de JavaScript et d’appliquer les conversions nécessaires pour rendre le code compatible (avec des polyfills).
Cela nous a permis d’implémenter les nouvelles fonctionnalités et syntaxes de la nouvelle version de JavaScript tout en assurant la compatibilité de notre application avec des anciens navigateurs.

Ajout du fichier de configuration
Pour utiliser Babel, il faut tout d’abord l’installer et le configurer.
Installer Babel :

npm install --save-dev @babel/core @babel/cli @babel/preset-env 



Pour configurer Babel il y a plusieurs options (https://babeljs.io/docs/configuration)
Nous allons utiliser la méthode la plus simple en créant un fichier de configuration nommé .babelrc.json dans lequel nous pouvons préciser les consignes à Babel.

Par exemple:

{
  "presets":  [
    ["@babel/preset-env", 
      {
        "targets":  
          {
            "browsers": [
              "> 1%", 
              "last 2 versions", 
              "ie >= 11"
            ]
          }
      }
    ]
  ]
}`

Nous avons spécifié les versions et les navigateurs par défaut afin que Babel ne transpile pas tout le code ES2015+ sinon cela va générer un code plus lourd !
https://github.com/browserslist/browserslist

Mise en place de modules avec Webpack

Afin d’automatiser la conversion de tous nos fichiers JS d’ES6 en ES5, nous devons configurer un loader dans webpack.
Pour cela il faut tout d’abord installer cette dépendance :

npm install --save-dev babel-loader


On va ajouter ensuite notre loader dans l’objet “module” dans le fichier de config de webpack. Chaque loader aura cette forme :

module: {
  rules: [{
    test: , // Les fichiers à tester pour appliquer le loader
    exclude: , // Optionnel. Les fichiers à exclure du test par le loader
    use: // liste des loaders à appliquer
  }]
}

Pour tester les fichiers sur lesquels appliquer le loader, on va utiliser les expressions regulières pour filtrer sur les extensions du fichier. Par exemple pour le loader des fichiers js le pattern sera de ce format :  /\.js$/
Du coup pour le loader de babel on doit ajouter cette configuration :

module: {
  rules: [{
    test: /\.js$/,
    exclude: /node_modules/,
    use: ['babel-loader']
  }]
 }

Maintenant nous pouvons utiliser les import/export des modules ES6.

Convertir du SCSS en CSS

De la même manière que nous l’avions fait pour les fichiers JS, on va également mettre en place des loaders de CSS. Tout d’abord, nous allons installer un chargeur CSS “css-loader” pour générer le style CSS et un chargeur de style “style-loader” pour injecter ce dernier dans le DOM.

npm install --save-dev css-loader style-loader



Maintenant on ajoute ces loaders dans la configuration de webpack dans “rules” après le loader de Babel.

{
   test: /\.css$/,
   use: ['style-loader', 'css-loader'],
}`



Maintenant on doit mettre en place la génération et l’extraction du css compilé dans un fichier séparé en installant le plugin Webpack “MiniCssExtractPlugin” dans nos dépendances de développement.

npm install --save-dev mini-css-extract-plugin



Pour l’utiliser, on doit l’ajouter dans la configuration de Webpack à la place du loader de style et en plus comme un plugin :

...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  …
  module: {
    rules: [
      ...
      { 
        test: /\.css$/, 
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'], 
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin(),
    …
  ]
} `

Afin d’écrire du code CSS maintenable et modulable, il est recommandé d’utiliser des préprocesseurs CSS.
L’utilisation du préprocesseur CSS nous a permis de coder plus vite et de manière plus efficace.

Avec Webpack, il est maintenant possible de compiler un fichier Sass/SCSS en CSS.
Pour cela, nous avons eu besoin d’installer de nouvelles dépendances :

npm install --save-dev sass-loader node-sass

Et ajouter ces loaders dans le webpack.config.js après le babel-loader

{
   test: /\.(s(a|c)ss)$/,
   use: ['style-loader', 'css-loader', 'sass-loader']
}`

Notre CSS est maintenant propre, organisé et surtout bien structuré.

 

Mettre en place des environnements de production et de développement

La prochaine étape est d’optimiser notre code de production en réduisant la taille de nos fichiers afin d’accélérer leur chargement.
La minification des fichiers Javascript et CSS va nous permettre d’atteindre ce but.
Afin de faciliter le débogage de notre code, on va générer le code minifié qu’en environnement de production, pour cela il faut séparer les environnements de production et de développement.
Comme nous développons sous Windows, nous allons commencer par installer la dépendance “cross-env“

npm install --save-dev cross-env



Puis dans le fichier “package.json” nous avons modifié l’objet script de la manière suivante :

"scripts": {
  "build dev": "webpack",
  "build prod": "cross-env NODE_ENV=production webpack"
}

Minifier les fichiers Javascript

Nous avons pu mettre en place la minification des fichiers JS grâce au plugin UglifyJS

Tout d’abord nous allons installer les dépendances :

npm install --save-dev uglifyjs-webpack-plugin



Nous devons maintenant l’ajouter dans le fichier de configuration de Webpack mais que pour l’environnement de production.

...
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
...
if (process.env.NODE_ENV === 'production') {
  module.exports.plugins.push(
    new UglifyJsPlugin()
  );
}

Minifier les fichiers CSS

Pour minifier le fichier CSS nous allons installer cette nouvelle dépendance :

npm install --save-dev optimize-css-assets-webpack-plugin



De la même manière que le plugin “UglifyJS” nous avons modifié le fichier de configuration Webpack

...
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSPlugin = require("optimize-css-assets-webpack-plugin");
...
if (process.env.NODE_ENV === 'production') {
  module.exports.plugins.push(
    new UglifyJsPlugin(),
    new OptimizeCSSPlugin()
  );
}

Après on a mis en place un linter pour s’assurer que toute l’équipe travaille avec les mêmes standards et respecte les bonnes pratiques.
Donc on va installer le plugin ESLint.

npm install --save-dev eslint-webpack-plugin eslint

Après on doit l’ajouter dans le tableau d’objets “plugins” dans le fichier de config de webpack.

…
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  …
  plugins: [ 
    … 
    new ESLintPlugin()
  ]
}

Il faut aussi créer un fichier de configuration de ESLint :
De la même manière que Babel on va créer un nouveau fichier à la racine et qu’on va l’appeler “.eslintrc”.
On peut aussi générer ce fichier avec la commande

npm init @eslint/config



Pour ne pas nous embêter à définir toute cette configuration nous avons utilisé un standard d’AirBNB

npx install-peerdeps --dev eslint-config-airbnb-base



Pour en savoir plus sur l’utilisation et la configuration d’ESLint, vous pouvez aller sur le site de la documentation officielle d’Eslint.

Notre fichier de config va rassemble à ceci :

{
  "extends": "airbnb-base"
}

Installation et configuration de TypeScript

Typescript : ajout du fichier de configuration


Tout d’abord nous avons installé TypeScript via npm:

npm install -g typescript

Pour configurer et utiliser TypeScript nous avons créé un fichier de configuration à la racine de notre projet nommé “tsconfig.json”
On peut aussi générer ce fichier en exécutant cette commande

npx tsc --init


Cela va nous créer le fichier “tsconfig.json” avec les options par défaut activées, ainsi que d’autres options possibles commentées.

Intégration avec Webpack


L’intégration avec Webpack est assez simple.
On va utiliser ces deux loaders, le loader de TypeScript “ts-loader”.

npm install ts-loader



On a modifié aussi notre fichier de configuration de Webpack pour ajouter l’extension “.ts” dans la liste des extensions à resoudre dans “resolve.extensions”.
On a aussi ajouté les règles pour appliquer les nouveaus loaders :

{
  …
  resolve: {
    extensions: ["*", ".js", ".ts"],
  },
  module: {
    rules: [
      // JS
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      // TS
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: 'ts-loader'
      },
    ],
  },
  ...

A partir de cette étape, tous nous nouveaux fichiers ont été créé avec l’extensions “.ts”.
Pour que l’on puisse tous utiliser le même fichier de configuration pour TypeScript et JavaScript, nous avons activé l’option “allowJs” en la mettant à true dans “tsconfig.json” pour inclure les fichiers JavaScript. Par contre nous avons dû mettre l’option “checkJs” à false afin d’éviter d’avoir trop d’erreurs de type par le compilateur.
Voici donc à quoi ressemble une partie de notre configuration :

{
  "compilerOptions": {
    …
    "noImplicitAny": true, 
    "allowJs": true,
    "outDir": "dist",
    "checkJs ": false
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.js"
  ],
  "exclude": [
    "node_modules"
  ],
  …
}

On a aussi ajouté le plugin “typescript-eslint” d’ESLint pour analyser et corriger le code TypeScript. Pour le formatage de code nous avons utlisé Prettier.


On peut trouver plus de détail dans cette documentation:
https://typescript-eslint.io/getting-started
https://typescript-eslint.io/linting/troubleshooting/formatting

Amélioration de la performance avec le lazy loading

Afin d’améliorer la performance, on a mis en place le lazy loading pour certains fichiers pour ne charger que les modules nécessaires au démarrage de l’application et différer le reste et ne les charger que quand on a besoin.
Cela nous a permis de réduire la taille de notre bundle principal qui pèse lourd du au nombre de fichiers JavaScript utilisés pour faire fonctionner notre application.
Voici un exemple de chargement d’un autre module si on veut utiliser une fonction de ce dernier que lorsque l’on clique sur un bouton :

myButton.addEventListener('click', event => {
  import('./other-module').then(myModule => {
    myModule.doSomething();
  });
});

Pour analyser l’impact sur notre application (avant/après) nous avons utilisé Webpack Bundle Analyzer que nous avons installé via NPM

npm install --save-dev webpack-bundle-analyzer


Pour lancer l’analyse de nos dépendances :

webpack --profile --json > stats.json


Suite à notre analyse du bundle nous avons remarqué que jQuery est le fichier le plus lourd de toutes les dépendances.

Se débarrasser de jQuery

L’utilisation d’axios au lieu de $.ajax (jQuery) permet l’utilisation de Promises au lieu de callbacks.

Notre application utilise jQuery pour faire les requêtes HTTP pour récupérer ou sauvegarder des données depuis le backend. Nous avons utilisé le module Axios qui est compatible avec tous les navigateurs modernes et il est basé sur les Promises.
Tout d’abord on va installer axios via npm :

npm install axios


Après nous avons remplacer la fonction “$.ajax” de jQuery par les fonctions Axios pour effectuer n’importe quelle requête HTTP.

Par exemple pour faire une requête GET pour recupérer un utilisateur avec un ID donné :

axios.get('/users/1')
  .then(response => {
    console.log(response);
  })
  .catch(error => {
    console.log(error);
  });

Ou bien de faire une requête POST pour créer un nouveau utilisateur :

axios.post('/users', {
   firstName: 'Martin',
   lastName: 'Dupont'
 })
   .then(response => {
     console.log(response);
   })
   .catch(error => {
     console.log(error);
   });

Utilisation des API DOM natives

Nous utilisons jQuery aussi pour manipuler les éléments de DOM.
Afin de continuer notre stratégie de ne plus utiliser jQuery nous avons opté pour utiliser les API DOM natives qui sont supportées par les navigateurs récents.
Par exemple pour sélectionner des élément DIV on pourra faire:

// jQuery
var divs = $("div");

// JS
const divs = document.querySelectorAll("div");

Conclusion

Cette migration nous a permis de gagner en temps de compilation, d’avoir un code plus lisible et une architecture clean et modulaire.

Les risques pris et les coûts nécessaires pour maintenir une application dissuadent souvent la modernisation du code existant.

Aujourd’hui plusieurs outils permettent de migrer facilement et sans prise de risques afin d’avoir un code plus performant, facile à maintenir.

Notre but maintenant est d’écrire des composants réutilisables en utilisant des composants React ou des Web Components afin d’appliquer le principe de développement DRY (Don’t Repeat Yourself).

Ressources pour aller plus loin :
Packager son application React avec Webpack

Retour d’expérience sur TypeScript

Retour d’expérience sur TypeScript – Utilisation de librairies JavaScript – Partie 1 : Concepts clés

Préprocesseurs CSS : LESS vs Sass

Vous souhaitez en savoir plus ? Contactez-nous !