Table des matières
Qu'est-ce que Google Lighthouse?
Installation
Ouverture de chrome avec node.js
Exécution du phare programmatique
Sauver les rapports de phare
Créer le répertoire
Sauver le rapport
Comparaison des rapports de phare
Comparez le nouveau rapport avec le rapport précédent
Comparez deux rapports
Logique de comparaison
Complete source code
Étapes suivantes
Maison interface Web tutoriel CSS Créez un outil Node.js pour enregistrer et comparer Google Lighthouse Rapports

Créez un outil Node.js pour enregistrer et comparer Google Lighthouse Rapports

Apr 09, 2025 am 09:18 AM

Créez un outil Node.js pour enregistrer et comparer Google Lighthouse Rapports

Dans ce didacticiel, je vous montrerai étape par étape comment créer un outil simple dans Node.js pour exécuter Google Lighthouse Audits via la ligne de commande, enregistrer les rapports qu'ils génèrent au format JSON, puis les comparer afin que les performances Web puissent être surveillées à mesure que le site Web se développe et développe.

J'espère que cela pourra servir de bonne introduction à tout développeur intéressé à apprendre comment travailler avec Google Lighthouse par programme.

Mais d'abord, pour les non-initiés…

Qu'est-ce que Google Lighthouse?

Google Lighthouse est l'un des outils les mieux automatisés disponibles sur la courroie d'utilité d'un développeur Web. Il vous permet de auditer rapidement un site Web dans un certain nombre de domaines clés qui peuvent ensemble former une mesure de sa qualité globale. Ce sont:

  • Performance
  • Accessibilité
  • Meilleures pratiques
  • Référencement
  • Application Web progressive

Une fois l'audit terminé, un rapport est ensuite généré sur ce que votre site Web fait bien… et pas si bien, ce dernier ayant l'intention de servir d'indicateur pour vos prochaines étapes pour améliorer la page.

Voici à quoi ressemble un rapport complet.

En plus d'autres diagnostics généraux et des mesures de performances Web, une caractéristique vraiment utile du rapport est que chacun des domaines clés est agrégé en scores codés en couleur entre 0 et 100.

Non seulement cela permet aux développeurs d'évaluer rapidement la qualité d'un site Web sans analyse, mais cela permet également aux gens non techniques tels que les parties prenantes ou les clients de comprendre également.

Par exemple, cela signifie qu'il est beaucoup plus facile de partager la victoire avec Heather du marketing après avoir passé du temps à améliorer l'accessibilité du site Web car elle est plus en mesure d'apprécier l'effort après avoir vu le score d'accessibilité au phare augmenter à 50 points dans le vert.

Mais également, Simon, le chef de projet, peut ne pas comprendre ce que signifie l'index de vitesse ou la première peinture contente, mais quand il voit le rapport du phare montrant le score de performance du site Web au plus profond du rouge, il sait que vous avez toujours du travail à faire.

Si vous êtes dans Chrome ou dans la dernière version d'Edge, vous pouvez exécuter un audit de phare pour vous-même en utilisant Devtools. Voici comment:

Vous pouvez également exécuter un audit de phare en ligne via Pagespeed Insights ou via des outils de performance populaires, tels que WebPageTest.

Cependant, aujourd'hui, nous ne sommes intéressés que par le phare comme module de nœud, car cela nous permet d'utiliser l'outil par programme pour auditer, enregistrer et comparer les mesures de performances Web.

Découvrons comment.

Installation

Tout d'abord, si vous ne l'avez pas déjà, vous aurez besoin de node.js. Il existe un million de façons différentes de l'installer. J'utilise le Homebrew Package Manager, mais vous pouvez également télécharger un installateur directement sur le site Web de Node.js si vous préférez.Ce tutoriel a été écrit avec Node.js V10.17.0 à l'esprit, mais cela fonctionnera très probablement très bien sur la plus grande version publiée au cours des dernières années.

Vous aurez également besoin de Chrome installé, car c'est ainsi que nous exécuterons les audits du phare.

Ensuite, créez un nouveau répertoire pour le projet, puis CD dans la console. Ensuite, exécutez NPM init pour commencer à créer un fichier package.json. À ce stade, je recommanderais simplement de dénigrer la touche Entrée encore et encore pour en sauter autant que possible jusqu'à ce que le fichier soit créé.

Maintenant, créons un nouveau fichier dans le répertoire du projet. J'ai appelé le mien lh.js, mais n'hésitez pas à l'appeler comme vous voulez. Cela contiendra tout JavaScript pour l'outil. Ouvrez-le dans votre éditeur de texte de choix et pour l'instant, écrivez une instruction Console.log.

 Console.log («Hello World»);
Copier après la connexion

Ensuite, dans la console, assurez-vous que votre CWD (répertoire de travail actuel) est votre répertoire de projet et exécutez le nœud lh.js, en remplaçant mon nom de fichier pour tout ce que vous avez utilisé.

Vous devriez voir:

 $ node lh.js
Bonjour le monde
Copier après la connexion

Sinon, vérifiez que l'installation de votre nœud fonctionne et vous êtes certainement dans le bon répertoire de projet.

Maintenant, c'est à l'écart, nous pouvons passer au développement de l'outil lui-même.

Ouverture de chrome avec node.js

Installons la première dépendance de notre projet: Lighthouse lui-même.

 NPM Installer Lighthouse --Save-Dev
Copier après la connexion

Cela crée un répertoire Node_Modules qui contient tous les fichiers du package. Si vous utilisez GIT, la seule chose que vous voudrez faire avec cela est de l'ajouter à votre fichier .gitignore.

Dans lh.js, vous voudrez ensuite supprimer la console de test.log () et importer le module de phare afin que vous puissiez l'utiliser dans votre code. Comme ainsi:

 const lighthouse = require ('phare');
Copier après la connexion

En dessous, vous devrez également importer un module appelé Chrome-Launcher, qui est l'une des dépendances de Lighthouse et permet à Node de lancer Chrome par lui-même afin que l'audit puisse être exécuté.

 const lighthouse = require ('phare');
const chromelaUncher = require ('chrome-launcher');
Copier après la connexion

Maintenant que nous avons accès à ces deux modules, créons un script simple qui ouvre simplement Chrome, exécute un audit de phare, puis imprime le rapport à la console.

Créez une nouvelle fonction qui accepte une URL en tant que paramètre. Parce que nous l'exécuterons en utilisant Node.js, nous pouvons utiliser en toute sécurité la syntaxe ES6 car nous n'avons pas à nous soucier de ces utilisateurs embêtants de l'explorateur Internet.

 const lankchrome = (url) => {

}
Copier après la connexion

Dans la fonction, la première chose que nous devons faire est d'ouvrir Chrome en utilisant le module Chrome-Launcher que nous avons importé et l'envoyant à tout argument passé par le paramètre URL.

Nous pouvons le faire en utilisant sa méthode Launch () et son option de démarrage.

 const lankchrome = url => {
  chromelauncher.launch ({
    Starterl: URL
  });
};
Copier après la connexion

Appeler la fonction ci-dessous et passer une URL de votre choix entraîne l'ouverture de Chrome à l'URL lorsque le script de nœud est exécuté.

 LaunchChrome ('https://www.lukeharrison.dev');
Copier après la connexion

La fonction de lancement renvoie en fait une promesse, qui nous permet d'accéder à un objet contenant quelques méthodes et propriétés utiles.

Par exemple, en utilisant le code ci-dessous, nous pouvons ouvrir Chrome, imprimer l'objet à la console, puis fermer le chrome trois secondes plus tard en utilisant sa méthode kill ().

 const lankchrome = url => {
  chromelauncher
    .lancement({
      Starterl: URL
    })
    .Then (chrome => {
      console.log (chrome);
      setTimeout (() => chrome.kill (), 3000);
    });
};

LaunchChrome ("https://www.lukeharrison.dev");
Copier après la connexion

Maintenant que nous avons compris Chrome, passons au phare.

Exécution du phare programmatique

Tout d'abord, renommeons notre fonction LaunchChrome () à quelque chose de plus reflétant sa fonctionnalité finale: LaunchComRomeandrunLightHouse (). Avec la partie difficile à l'écart, nous pouvons désormais utiliser le module de phare que nous avons importé plus tôt dans le tutoriel.

Dans la fonction du Chrome Launcher, qui ne s'exécute qu'une fois que le navigateur est ouvert, nous passerons le phare de l'argument URL de la fonction et déclencherons un audit de ce site Web.

 const lankchromeandrunlighthouse = url => {
  chromelauncher
    .lancement({
      Starterl: URL
    })
    .Then (chrome => {
      const opts = {
        Port: Chrome.port
      };
      Lighthouse (URL, opts);
    });
};

LaunchChromeandrunLighthouse ("https://www.lukeharrison.dev");
Copier après la connexion

Pour lier l'instance de phare à notre fenêtre Chrome Browser, nous devons passer son port avec l'URL.

Si vous deviez exécuter ce script maintenant, vous appuyez sur une erreur dans la console:

 (Node: 47714) UNHANDLEDPROMISEREINGWARNING: ERREUR: Vous avez probablement plusieurs onglets ouverts à la même origine.
Copier après la connexion

Pour résoudre ce problème, nous avons juste besoin de supprimer l'option Starterl du lanceur de Chrome et de laisser le phare gérer la navigation d'URL à partir d'ici.

 const lankchromeandrunlighthouse = url => {
  chromelauncher.launch (). puis (chrome => {
    const opts = {
      Port: Chrome.port
    };
    Lighthouse (URL, opts);
  });
};
Copier après la connexion

Si vous deviez exécuter ce code, vous remarquerez que quelque chose semble définitivement se produire. Nous n'obtenons tout simplement aucun commentaire dans la console pour confirmer que l'audit du phare a définitivement fonctionné, pas plus que l'instance chromée ne se ferme pas par elle-même.

Heureusement, la fonction Lighthouse () renvoie une promesse qui nous permet d'accéder aux résultats d'audit.

Turons Chrome, puis imprimons ces résultats au terminal au format JSON via la propriété du rapport de l'objet de résultats.

 const lankchromeandrunlighthouse = url => {
  chromelauncher.launch (). puis (chrome => {
    const opts = {
      Port: Chrome.port
    };
    Lighthouse (URL, opts) .Then (résultats => {
      chrome.kill ();
      Console.log (résultats.Report);
    });
  });
};
Copier après la connexion

Bien que la console ne soit pas le meilleur moyen d'afficher ces résultats, si vous deviez les copier dans votre presse-papiers et visiter la visionneuse du rapport du phare, le collage ici affichera le rapport dans toute sa gloire.

À ce stade, il est important de ranger un peu le code pour que la fonction LaunchCromeAndrunLighthouse () renvoie le rapport une fois qu'il a terminé l'exécution. Cela nous permet de traiter le rapport plus tard sans entraîner une pyramide désordonnée de JavaScript.

 const Lighthouse = require ("Lighthouse");
const chromelaUncher = requis ("chrome-launcher");

const lankchromeandrunlighthouse = url => {
  return chromelauncher.launch (). alors (chrome => {
    const opts = {
      Port: Chrome.port
    };
    return Lighthouse (URL, opts) .Then (résultats => {
      return chrome.kill (). alors (() => result.Report);
    });
  });
};

LaunchChromeandrunLighthouse ("https://www.lukeharrison.dev") .Then (résultats => {{
  console.log (résultats);
});
Copier après la connexion

Une chose que vous avez peut-être remarquée, c'est que notre outil ne peut auditer qu'un seul site Web pour le moment. Changeons cela afin que vous puissiez passer l'URL comme argument via la ligne de commande.

Pour éliminer la douleur de travailler avec des arguments en ligne de commande, nous les gérerons avec un package appelé Yargs.

 Installation de NPM - Save-Dev Yargs
Copier après la connexion

Importez-le ensuite en haut de votre script avec Chrome Launcher et Lighthouse. Nous n'avons besoin que de sa fonction Argv ici.

 const lighthouse = require ('phare');
const chromelaUncher = require ('chrome-launcher');
const argv = require ('yargs'). argv;
Copier après la connexion

Cela signifie que si vous deviez passer un argument de ligne de commande dans le terminal comme tel:

 Node lh.js --url https://www.google.co.uk
Copier après la connexion

… Vous pouvez accéder à l'argument dans le script comme tel:

 const url = argv.url // https://www.google.co.uk
Copier après la connexion

Montons notre script pour passer l'argument URL de ligne de commande au paramètre URL de la fonction. Il est important d'ajouter un petit filet de sécurité via l'instruction IF et le message d'erreur au cas où aucun argument n'est passé.

 if (argv.url) {
  LaunchChromeandrunLighthhouse (argv.url) .Then (résultats => {
    console.log (résultats);
  });
} autre {
  Jetez "vous n'avez pas passé d'URL au phare";
}
Copier après la connexion

Tada! Nous avons un outil qui lance Chrome et exécute un audit de phare par programme avant d'imprimer le rapport au terminal au format JSON.

Sauver les rapports de phare

Le fait que le rapport est imprimé à la console n'est pas très utile car vous ne pouvez pas facilement lire son contenu, et ils ne sont pas enregistrés pour une utilisation future. Dans cette section du tutoriel, nous modifierons ce comportement afin que chaque rapport soit enregistré dans son propre fichier JSON.

Pour arrêter les rapports de différents sites Web se mélangeant, nous les organiserons comme:

  • lukeharrison.dev
    • 2020-01-31T18: 18: 12.648z.json
    • 2020-01-31T19: 10: 24.110Z.json
  • cnn.com
    • 2020-01-14T22: 15: 10.396Z.json
  • lh.js

Nous nommerons les rapports avec un horodatage indiquant la date / heure du rapport. Cela ne signifiera pas que deux noms de fichiers de rapport ne seront jamais les mêmes, et cela nous aidera facilement à distinguer les rapports.

Il y a un problème avec Windows qui nécessite notre attention: le côlon (:) est un caractère illégal pour les noms de fichiers. Pour atténuer ce problème, nous remplacerons tous les Colons par des soulignements (_), donc un nom de fichier typique de rapport ressemblera:

  • 2020-01-31T18_18_12.648Z.json

Créer le répertoire

Tout d'abord, nous devons manipuler l'argument de l'URL de ligne de commande afin que nous puissions l'utiliser pour le nom du répertoire.

Cela implique plus que le simple de supprimer le www, car il doit tenir compte des audits exécutés sur des pages Web qui ne se trouvent pas à la racine (par exemple: www.foo.com/bar), car les barres obliques sont des caractères non valides pour les noms de répertoire.

Pour ces URL, nous remplacerons à nouveau les personnages invalides par des soulignements. De cette façon, si vous exécutez un audit sur https://www.foo.com/bar, le nom du répertoire résultant contenant le rapport serait foo.com_bar.

Pour faciliter la gestion des URL, nous utiliserons un module Node.js natif appelé URL. Cela peut être importé comme n'importe quel autre package et sans avoir à l'ajouter à thepackage.json et à le tirer via NPM.

 const lighthouse = require ('phare');
const chromelaUncher = require ('chrome-launcher');
const argv = require ('yargs'). argv;
const url = require ('url');
Copier après la connexion

Ensuite, utilisons-le pour instancier un nouvel objet URL.

 if (argv.url) {
  const urlobj = new URL (argv.url);

  LaunchChromeandrunLighthhouse (argv.url) .Then (résultats => {
    console.log (résultats);
  });
}
Copier après la connexion

Si vous deviez imprimer URLOBJ à la console, vous verriez beaucoup de données URL utiles que nous pouvons utiliser.

 $ node lh.js --url https://www.foo.com/bar
URL {
  href: 'https://www.foo.com/bar',
  Origine: «https://www.foo.com»,
  protocole: 'https:',
  nom d'utilisateur: '',
  mot de passe: '',
  Hôte: «www.foo.com»,
  nom d'hôte: «www.foo.com»,
  port: '',
  PathName: '/ Bar',
  recherche: '',
  SearchParams: UrlSearchParams {},
  Hash: ''
}
Copier après la connexion

Créez une nouvelle variable appelée dirname et utilisez la méthode String Remplace () sur la propriété hôte de notre URL pour se débarrasser du www en plus du protocole HTTPS:

 const urlobj = new URL (argv.url);
Soit dirname = urlobj.host.replace ('www.', '');
Copier après la connexion

Nous avons utilisé le LET ICI, qui, contrairement à const, peut être réaffecté, car nous devons mettre à jour la référence si l'URL a un chemin d'accès, pour remplacer les barres obliques par des traits de soulignement. Cela peut être fait avec un modèle d'expression régulière et ressemble à ceci:

 const urlobj = new URL (argv.url);
Soit dirname = urlobj.host.replace ("www.", "");
if (urlobj.pathname! == "/") {
  dirname = dirname urlobj.pathname.replace (/ \ // g, "_");
}
Copier après la connexion

Maintenant, nous pouvons créer le répertoire lui-même. Cela peut être fait grâce à l'utilisation d'un autre module native Node.js appelé FS (abréviation de «Système de fichiers»).

 const lighthouse = require ('phare');
const chromelaUncher = require ('chrome-launcher');
const argv = require ('yargs'). argv;
const url = require ('url');
const fs = require ('fs');
Copier après la connexion

Nous pouvons utiliser sa méthode mkdir () pour créer un répertoire, mais nous devons d'abord utiliser sa méthode existant () pour vérifier si le répertoire existe déjà, car Node.js allait autrement lancer une erreur:

 const urlobj = new URL (argv.url);
Soit dirname = urlobj.host.replace ("www.", "");
if (urlobj.pathname! == "/") {
  dirname = dirname urlobj.pathname.replace (/ \ // g, "_");
}
if (! fs.existSync (dirname)) {
  fs.mkDiRSync (dirname);
}
Copier après la connexion

Le test du script au point devrait entraîner la création d'un nouveau répertoire. Passer https://www.bbc.co.uk/news en tant qu'argument URL aboutirait à un répertoire nommé bbc.co.uk_news.

Sauver le rapport

Dans la fonction d'alors pour LaunchChromeAndrunLightHouse (), nous voulons remplacer la console existante.log par la logique pour rédiger le rapport sur le disque. Cela peut être fait en utilisant la méthode WriteFile () du module FS.

 LaunchChromeandrunLighthhouse (argv.url) .Then (résultats => {
  fs.writeFile ("report.json", résultats, err => {
    si (err) jetez ERR;
  });
});
Copier après la connexion

Le premier paramètre représente le nom du fichier, le second est le contenu du fichier et le troisième est un rappel contenant un objet d'erreur si quelque chose ne va pas pendant le processus d'écriture. Cela créerait un nouveau fichier appelé report.json contenant l'objet JSON RAPPORT du phare de retour.

Nous devons encore l'envoyer dans le bon répertoire, avec un horodatage comme nom de fichier.

 LaunchChromeandrunLighthhouse (argv.url) .Then (résultats => {
  fs.writeFile (`$ {dirname} / report.json`, résultats, err => {
    si (err) jetez ERR;
  });
});
Copier après la connexion

Ce dernier nous oblige cependant à récupérer en quelque sorte un horodatage du moment où le rapport a été généré. Heureusement, le rapport lui-même capture cela comme point de données et est stocké comme la propriété FetchTime.

Nous avons juste besoin de nous rappeler d'échanger n'importe quel Colons (:) pour les traits de soulignement (_) afin qu'il joue bien avec le système de fichiers Windows.

 LaunchChromeandrunLighthhouse (argv.url) .Then (résultats => {
  fs.writefile (
    `$ {dirname} / $ {résultats [" fetchtime "]. remplace (/: / g," _ ")}. JSON`,
    résultats,
    err => {
      si (err) jetez ERR;
    }
  ));
});
Copier après la connexion

Si vous deviez exécuter ceci maintenant, plutôt qu'un nom de fichier horodomoteur.json, vous verriez probablement une erreur similaire à:

 Non-standledpromisejectionWarning: typeError: Impossible de lire la propriété «remplacer» non défini
Copier après la connexion

Cela se produit parce que Lighthouse renvoie actuellement le rapport au format JSON, plutôt qu'un objet consommable par JavaScript.

Heureusement, au lieu d'analyser le JSON nous-mêmes, nous pouvons simplement demander à Lighthouse de retourner le rapport comme un objet JavaScript ordinaire à la place.

Cela nécessite de modifier la ligne ci-dessous à partir de:

 return chrome.kill (). alors (() => result.Report);
Copier après la connexion

…à:

 return chrome.kill (). alors (() => results.lhr);
Copier après la connexion

Maintenant, si vous réinterrisez le script, le fichier sera nommé correctement. Cependant, lorsqu'il est ouvert, c'est le seul contenu qui sera malheureusement…

 [objet objet]
Copier après la connexion

C'est parce que nous avons maintenant le problème opposé comme auparavant. Nous essayons de rendre un objet JavaScript sans le lancer d'abord dans un objet JSON.

La solution est simple. Pour éviter d'avoir à gaspiller des ressources en analyse ou en tronçant cet énorme objet, nous pouvons retourner les deux types du phare:

 return Lighthouse (URL, opts) .Then (résultats => {
  return chrome.kill (). puis (() => {
    retour {
      js: results.lhr,
      JSON: Résultats.Report
    };
  });
});
Copier après la connexion

Ensuite, nous pouvons modifier l'instance WriteFile à ceci:

 fs.writefile (
  `$ {dirname} / $ {results.js [" fetchtime "]. Remplace (/: / g," _ ")}. JSON`,
  résultats.json,
  err => {
    si (err) jetez ERR;
  }
));
Copier après la connexion

Traté! À la fin de l'audit de Lighthouse, notre outil devrait désormais enregistrer le rapport dans un fichier avec un nom de fichier timestampé unique dans un répertoire nommé d'après l'URL du site Web.

Cela signifie que les rapports sont désormais beaucoup plus efficaces et ne se remplaceront pas les uns les autres, peu importe le nombre de rapports enregistrés.

Comparaison des rapports de phare

Pendant le développement quotidien, lorsque je me concentre sur l'amélioration des performances, la capacité de comparer très rapidement les rapports directement dans la console et de voir si je me dirige dans la bonne direction pourrait être extrêmement utile. Dans cet esprit, les exigences de cette fonctionnalité de comparaison devraient être:

  1. Si un rapport précédent existe déjà pour le même site Web lorsqu'un audit de phare est terminé, effectuez automatiquement une comparaison avec elle et affichez les modifications des mesures de performance clés.
  2. Je devrais également être en mesure de comparer les mesures de performances clés à partir de deux rapports, à partir de deux sites Web, sans avoir à générer un nouveau rapport de phare dont je ne pourrais pas avoir besoin.

Quelles parties d'un rapport doivent être comparées? Ce sont les mesures de performance clés numériques collectées dans le cadre de tout rapport de phare. Ils donnent un aperçu de l'objectif et des performances perçues d'un site Web.

De plus, Lighthouse collecte également d'autres mesures qui ne sont pas répertoriées dans cette partie du rapport mais qui sont toujours dans un format approprié pour être inclus dans la comparaison. Ce sont:

  • Le temps de premier octet - le temps de premier octet identifie l'heure à laquelle votre serveur envoie une réponse.
  • Temps de blocage total - somme de toutes les périodes entre le FCP et le temps à interactif, lorsque la longueur de la tâche dépassait 50 ms, exprimé en millisecondes.
  • Latence d'entrée estimée - la latence d'entrée estimée est une estimation de la durée de votre application pour répondre à la saisie des utilisateurs, en millisecondes, pendant la fenêtre 5S la plus fréquentée de la charge de page. Si votre latence est supérieure à 50 ms, les utilisateurs peuvent percevoir votre application comme laggy.

Comment la comparaison métrique doit-elle être sortie de la console? Nous allons créer une comparaison simple basée sur le pourcentage en utilisant les anciennes et nouvelles métriques pour voir comment ils sont passés d'un rapport à l'autre.

Pour permettre un balayage rapide, nous allons également coder les métriques individuelles en fonction de leur plus rapide, plus lent ou inchangé.

Nous viserons cette sortie:

Comparez le nouveau rapport avec le rapport précédent

Commençons par la création d'une nouvelle fonction appelée comparaison () juste en dessous de notre fonction LaunchComEndrunLightHouse (), qui contiendra toute la logique de comparaison. Nous allons lui donner deux paramètres - d'après et à accepter les deux rapports utilisés pour la comparaison.

Pour l'instant, en tant qu'espace réservé, nous allons simplement imprimer des données de chaque rapport à la console pour valider qu'il les reçoit correctement.

 const ComparreEports = (de, à, à) => {
  console.log (de ["finurl"] "" de ["fetchtime"]);
  console.log (à ["finurl"] "" à ["fetchtime"]);
};
Copier après la connexion

Comme cette comparaison commencerait après la création d'un nouveau rapport, la logique pour exécuter cette fonction devrait se situe dans la fonction alors pour LaunchCromeAndrunLightHouse ().

Si, par exemple, vous avez 30 rapports assis dans un répertoire, nous devons déterminer lequel est le plus récent et le définir comme le rapport précédent contre lequel le nouveau sera comparé. Heureusement, nous avons déjà décidé d'utiliser un horodatage comme nom de fichier pour un rapport, donc cela nous donne quelque chose avec lequel travailler.

Tout d'abord, nous devons collecter tous les rapports existants. Pour faciliter ce processus, nous allons installer une nouvelle dépendance appelée glob, ce qui permet la correspondance de motifs lors de la recherche de fichiers. Ceci est essentiel car nous ne pouvons pas prédire combien de rapports existeront ou comment ils seront appelés.

L'installez comme toute autre dépendance:

 NPM Install Glob --Save-Dev
Copier après la connexion

Puis l'importez-le en haut du fichier de la même manière que d'habitude:

 const lighthouse = require ('phare');
const chromelaUncher = require ('chrome-launcher');
const argv = require ('yargs'). argv;
const url = require ('url');
const fs = require ('fs');
const glob = require ('glob');
Copier après la connexion

Nous utiliserons Glob pour collecter tous les rapports dans le répertoire, dont nous connaissons déjà le nom de la variable Dirname. Il est important de définir son option de synchronisation sur true car nous ne voulons pas que l'exécution de JavaScript se poursuive jusqu'à ce que nous sachions combien d'autres rapports existent.

 LaunchChromeandrunLighthhouse (argv.url) .Then (résultats => {
  const prevReports = glob (`$ {dirname} / *. json`, {
    Sync: vrai
  });

  // et al

});
Copier après la connexion

Ce processus renvoie un tableau de chemins. Donc, si le répertoire du rapport ressemblait à ceci:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

… Alors le tableau qui en résulterait ressemblerait à ceci:

 [
 'lukeharrison.dev/2020-01-31t10_18_12.648z.json',
 'lukeharrison.dev/2020-01-31t10_18_24.110z.json'
]]
Copier après la connexion

Parce que nous ne pouvons effectuer une comparaison que si un rapport précédent existe, utilisons ce tableau comme conditionnel pour la logique de comparaison:

 const prevReports = glob (`$ {dirname} / *. json`, {
  Sync: vrai
});

if (prevReports.length) {
}
Copier après la connexion

Nous avons une liste de chemins de fichier de rapport et nous devons comparer leurs noms de fichiers horodgiens pour déterminer lequel est le plus récent.

Cela signifie que nous devons d'abord collecter une liste de tous les noms de fichiers, réduire toutes les données non pertinentes telles que les noms de répertoires et prendre soin de remplacer les traits de soulignement (_) avec des Colons (:) pour les transformer en dates valides. La façon la plus simple de le faire est d'utiliser Path, un autre module natif Node.js.

 const path = require ('path');
Copier après la connexion

Passer le chemin comme un argument à sa méthode d'analyse, comme ainsi:

 path.parse ('lukeharrison.dev/2020-01-31t10_18_24.110z.json');
Copier après la connexion

Renvoie cet objet utile:

 {
  racine: '',
  dir: 'lukeharrison.dev',
  base: '2020-01-31T10_18_24.110z.json',
  ext: '.json',
  Nom: '2020-01-31T10_18_24.110Z'
}
Copier après la connexion

Par conséquent, pour obtenir une liste de tous les noms de fichiers d'horodatage, nous pouvons le faire:

 if (prevReports.length) {
  dates = [];
  pour (rapport dans les prevraports) {
    dates.push (
      Nouvelle date (path.parse (prevReports [rapport]). name.replace (/ _ / g, ":"))
    ));
  }
}
Copier après la connexion

Ce qui encore une fois si notre répertoire ressemblait:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

Entraînerait:

 [
 «2020-01-31T10: 18: 12.648Z»,
 '2020-01-31T10: 18: 24.110Z'
]]
Copier après la connexion

Une chose utile à propos des dates est qu'ils sont intrinsèquement comparables par défaut:

 const alpha = new Date ('2020-01-31');
const bravo = new Date ('2020-02-15');

console.log (alpha> bravo); // FAUX
console.log (bravo> alpha); // vrai
Copier après la connexion

Ainsi, en utilisant une fonction de réduction, nous pouvons réduire notre éventail de dates jusqu'à ce que les restes les plus récents:

 dates = [];
pour (rapport dans les prevraports) {
  dates.push (new Date (path.parse (prevReports [rapport]). name.replace (/ _ / g, ":")));
}
const max = dates.reduce (fonction (a, b) {
  return math.max (a, b);
});
Copier après la connexion

Si vous deviez imprimer le contenu de Max à la console, cela jetterait un horodat Unix, alors maintenant, nous devons simplement ajouter une autre ligne pour convertir notre date la plus récente au format ISO correct:

 const max = dates.reduce (fonction (a, b) {
 return math.max (a, b);
});
const reterReport = new Date (max) .toiSoString ();
Copier après la connexion

En supposant que ce sont la liste des rapports:

  • 2020-01-31T23_24_41.786Z.json
  • 2020-01-31T23_25_36.827Z.json
  • 2020-01-31T23_37_56.856Z.json
  • 2020-01-31T23_39_20.459Z.json
  • 2020-01-31T23_56_50.959Z.json

La valeur de la réduction serait 2020-01-31T23: 56: 50.959Z.

Maintenant que nous connaissons le rapport le plus récent, nous devons ensuite extraire son contenu. Créez une nouvelle variable appelée récentReportContents sous la variable Récente et attribuez-le une fonction vide.

Comme nous le savons, cette fonction devra toujours s'exécuter, plutôt que de l'appeler manuellement, il est logique de le transformer en une IFFE (expression de fonction immédiatement invoquée), qui s'exécutera par elle-même lorsque l'analyseur JavaScript l'atteint. Ceci est signifié par la parenthèse supplémentaire:

 const reterReportContents = (() => {

}) ();
Copier après la connexion

Dans cette fonction, nous pouvons retourner le contenu du rapport le plus récent en utilisant la méthode readFilesYC () du module FS natif. Parce que ce sera au format JSON, il est important de l'analyser dans un objet JavaScript ordinaire.

 const reterReportContents = (() => {
  const Output = fs.readfilesync (
    dirname "/" recentReport.replace (/: / g, "_") ".json",
    "UTF8",
    (err, résultats) => {
      Résultats de retour;
    }
  ));
  return JSON.Parse (sortie);
}) ();
Copier après la connexion

Et puis, il s'agit d'appeler la fonction CompareReports () et de passer à la fois le rapport actuel et le rapport le plus récent comme arguments.

 ComparreReports (RécentReportContents, results.js);
Copier après la connexion

À l'heure actuelle, cela imprime quelques détails sur la console afin que nous puissions tester les données du rapport sur OK:

 https://www.lukeharrison.dev/ 2020-02-01T00: 25: 06.918Z
https://www.lukeharrison.dev/ 2020-02-01T00: 25: 42.169z
Copier après la connexion

Si vous obtenez des erreurs à ce stade, essayez de supprimer des fichiers ou des rapports Report.json sans contenu valide de plus tôt dans le tutoriel.

Comparez deux rapports

L'exigence clé restante était la possibilité de comparer deux rapports de deux sites Web. La façon la plus simple de mettre en œuvre cela serait de permettre à l'utilisateur de passer les chemins de fichier de rapport complet en tant que arguments de ligne de commande que nous enverrons ensuite à la fonction comparableports ().

Dans la ligne de commande, cela ressemblerait à:

 Node LH.JS --From Lukeharrison.dev/2020-02-01T00:25:06.918Z --pas CNN.com/2019-16T15:12:07.169Z
Copier après la connexion

La réalisation de ceci nécessite la modification de l'instruction conditionnelle IF qui vérifie la présence d'un argument de ligne de commande d'URL. Nous ajouterons un chèque supplémentaire pour voir si l'utilisateur vient de passer un chemin de et sur le chemin, sinon vérifiez l'URL comme auparavant. De cette façon, nous empêcherons un nouvel audit de phare.

 if (argv.from && argv.to) {

} else if (argv.url) {
 // et al
}
Copier après la connexion

Extraitons le contenu de ces fichiers JSON, analysons-les dans des objets JavaScript, puis les transmettons à la fonction comparableports ().

Nous avons déjà analysé JSON auparavant lors de la récupération du rapport le plus récent. Nous pouvons simplement extrapoler cette fonctionnalité dans sa propre fonction d'assistance et l'utiliser dans les deux endroits.

En utilisant la fonction récentReportContents () comme une base, créez une nouvelle fonction appelée getContents () qui accepte un chemin de fichier comme argument. Assurez-vous que ce n'est qu'une fonction régulière, plutôt qu'une IFFE, car nous ne voulons pas qu'elle s'exécute dès que l'analyseur JavaScript le trouve.

 const getContents = pathstr => {
  const Output = fs.readfilesync (pathstr, "utf8", (err, résultats) => {
    Résultats de retour;
  });
  return JSON.Parse (sortie);
};

const ComparreEports = (de, à, à) => {
  console.log (de ["finurl"] "" de ["fetchtime"]);
  console.log (à ["finurl"] "" à ["fetchtime"]);
};
Copier après la connexion

Mettez ensuite à jour la fonction récentReportContents () pour utiliser cette fonction d'assistance extrapolée à la place:

 const reterReportContents = getContents (dirname '/' recentReport.replace (/: / g, '_') '.json');
Copier après la connexion

De retour dans notre nouveau conditionnel, nous devons transmettre le contenu des rapports de comparaison à la fonction comparableports ().

 if (argv.from && argv.to) {
  comparaison (
    getContents (argv.from ".json"),
    getContents (argv.to ".json")
  ));
}
Copier après la connexion

Comme avant, cela devrait imprimer quelques informations de base sur les rapports de la console pour nous faire savoir que tout fonctionne bien.

 Node LH.JS --From lukeharrison.dev/2020-01-31t23_24_41.786z --to lukeharrison.dev/2020-02-01t11_16_25.221z
Copier après la connexion

Conduirait à:

 https://www.lukeharrison.dev/ 2020-01-31T23_24_41.786Z
https://www.lukeharrison.dev/ 2020-02-01T11_16_25.221Z
Copier après la connexion

Logique de comparaison

Cette partie du développement implique la logique de comparaison de la construction pour comparer les deux rapports reçus par la fonction CompareReports ().

Dans l'objet que le phare renvoie, il y a une propriété appelée audits qui contient un autre objet répertoriant les mesures de performances, les opportunités et les informations. Il y a beaucoup d'informations ici, dont une grande partie ne nous intéresse pas aux fins de cet outil.

Voici l'entrée pour la première peinture contente, l'une des neuf mesures de performance que nous souhaitons comparer:

 "Presque de premier contenu": {
  "id": "Presque premièrement",
  "Titre": "First Contentful Paint",
  "Description": "La première peinture contente marque le temps auquel le premier texte ou l'image est peint. [En savoir plus] (https://web.dev/first-contentful-paint).",
  "Score": 1,
  "scoredisplaymode": "numérique",
  "NumericValue": 1081.661,
  "DisplayValue": "1.1 S"
}
Copier après la connexion

Créez un tableau répertoriant les clés de ces neuf mesures de performance. Nous pouvons l'utiliser pour filtrer l'objet d'audit:

 const ComparreEports = (de, à, à) => {
  const MetricFilter = [
    "Presque de premier contenu",
    "Presque de premier plan",
    "index de vitesse",
    "Estimation-Input-latence",
    "temps de blocage total",
    "Max-potentiel-fid",
    "temps pour premier octet",
    "First-Cpu-Idle",
    "interactif"
  ]]
};
Copier après la connexion

Ensuite, nous allons parcourir l'un des objets Audits du rapport, puis référer son nom par rapport à notre liste de filtres. (Peu importe quel objet d'audit, car ils ont tous les deux la même structure de contenu.)

Si c'est là, alors brillant, nous voulons l'utiliser.

 const MetricFilter = [
  "Presque de premier contenu",
  "Presque de premier plan",
  "index de vitesse",
  "Estimation-Input-latence",
  "temps de blocage total",
  "Max-potentiel-fid",
  "temps pour premier octet",
  "First-Cpu-Idle",
  "interactif"
]]

for (let auditobj dans ["Audits"]) {
  if (MetricFilter. y compris (AuditObj)) {
    Console.log (AuditOBJ);
  }
}
Copier après la connexion

Cette console.log () imprimerait les touches ci-dessous à la console:

 peinture de premier ordre
peinture de premier plan
index de vitesse
à la latence à l'entrée estimée
temps de blocage total
max-potentiel-fid
temps de premier octet
premier processeur
interactif
Copier après la connexion

Ce qui signifie que nous utiliserions à partir de [«audits»] [auditobj] .numericValue et à [«audits»] [AuditObj] .numericValue respectivement dans cette boucle pour accéder aux mesures elles-mêmes.

Si nous devions les imprimer sur la console avec la clé, cela entraînerait une sortie comme ceci:

 Pressage de premier contenu 1081.661 890.774
Pressage de premier ordre 1081.661 954.774
Speed-Index 15576.70313351777 1098.622294504341
Estimé-INFUT-LATENCE 12.8 12.8
Temps de blocage total 59 31,5
max-potentiel-fid 153 102
Temps-temps 16.859999999999985 16.096000000000004
Premier-CPU-IDLE 1704.8490000000002 1918.774
Interactif 2266.2835 2374.3615
Copier après la connexion

Nous avons toutes les données dont nous avons besoin maintenant. Nous devons simplement calculer la différence en pourcentage entre ces deux valeurs, puis la connecter à la console à l'aide du format codé couleur décrit précédemment.

Savez-vous comment calculer la variation en pourcentage entre deux valeurs? Moi non plus. Heureusement, le moteur de recherche monolithe préféré de tout le monde est venu à la rescousse.

La formule est:

 ((De - à) / de) x 100
Copier après la connexion

Disons donc que nous avons un indice de vitesse de 5,7 s pour le premier rapport (de), puis une valeur de 2,1 s pour le second (à). Le calcul serait:

 5,7 - 2,1 = 3,6
3,6 / 5,7 = 0,63157895
0,63157895 * 100 = 63.157895
Copier après la connexion

Arronner à deux décimales entraînerait une diminution de l'indice de vitesse de 63,16%.

Mettons cela dans une fonction d'assistance à l'intérieur de la fonction CompareReports (), en dessous du tableau MetricFilter.

 const calcpercentagediff = (de, à) => {
  const per = ((à - de) / de) * 100;
  retour math.round (par * 100) / 100;
};
Copier après la connexion

Back in our auditObj conditional, we can begin to put together the final report comparison output.

First off, use the helper function to generate the percentage difference for each metric.

 for (let auditObj in from["audits"]) {
  if (metricFilter.includes(auditObj)) {
    const percentageDiff = calcPercentageDiff(
      from["audits"][auditObj].numericValue,
      to["audits"][auditObj].numericValue
    ));
  }
}
Copier après la connexion

Next, we need to output values in this format to the console:

This requires adding color to the console output. In Node.js, this can be done by passing a color code as an argument to the console.log() function like so:

 console.log('\x1b[36m', 'hello') // Would print 'hello' in cyan
Copier après la connexion

You can get a full reference of color codes in this Stackoverflow question. We need green and red, so that's \x1b[32m and \x1b[31m respectively. For metrics where the value remains unchanged, we'll just use white. This would be \x1b[37m.

Depending on if the percentage increase is a positive or negative number, the following things need to happen:

  • Log color needs to change (Green for negative, red for positive, white for unchanged)
  • Log text contents change.
    • '[Name] is X% slower for positive numbers
    • '[Name] is X% faster' for negative numbers
    • '[Name] is unchanged' for numbers with no percentage difference.
  • If the number is negative, we want to remove the minus/negative symbol, as otherwise, you'd have a sentence like 'Speed Index is -92.95% faster' which doesn't make sense.

There are many ways this could be done. Here, we'll use theMath.sign() function, which returns 1 if its argument is positive, 0 if well… 0, and -1 if the number is negative. Ça va faire.

 for (let auditObj in from["audits"]) {
  if (metricFilter.includes(auditObj)) {
    const percentageDiff = calcPercentageDiff(
      from["audits"][auditObj].numericValue,
      to["audits"][auditObj].numericValue
    ));

    let logColor = "\x1b[37m";
    const log = (() => {
      if (Math.sign(percentageDiff) === 1) {
        logColor = "\x1b[31m";
        return `${percentageDiff "%"} slower`;
      } else if (Math.sign(percentageDiff) === 0) {
        return "unchanged";
      } autre {
        logColor = "\x1b[32m";
        return `${percentageDiff "%"} faster`;
      }
    }) ();
    console.log(logColor, `${from["audits"][auditObj].title} is ${log}`);
  }
}
Copier après la connexion

So, there we have it.

You can create new Lighthouse reports, and if a previous one exists, a comparison is made.

And you can also compare any two reports from any two sites.

Complete source code

Here's the completed source code for the tool, which you can also view in a Gist via the link below.

 const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
const argv = require("yargs").argv;
const url = require("url");
const fs = require("fs");
const glob = require("glob");
const path = require("path");

const launchChromeAndRunLighthouse = url => {
  return chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    return lighthouse(url, opts).then(results => {
      return chrome.kill().then(() => {
        retour {
          js: results.lhr,
          json: results.report
        };
      });
    });
  });
};

const getContents = pathStr => {
  const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
    return results;
  });
  return JSON.parse(output);
};

const compareReports = (from, to) => {
  const metricFilter = [
    "first-contentful-paint",
    "first-meaningful-paint",
    "speed-index",
    "estimated-input-latency",
    "total-blocking-time",
    "max-potential-fid",
    "time-to-first-byte",
    "first-cpu-idle",
    "interactif"
  ]]

  const calcPercentageDiff = (from, to) => {
    const per = ((to - from) / from) * 100;
    return Math.round(per * 100) / 100;
  };

  for (let auditObj in from["audits"]) {
    if (metricFilter.includes(auditObj)) {
      const percentageDiff = calcPercentageDiff(
        from["audits"][auditObj].numericValue,
        to["audits"][auditObj].numericValue
      ));

      let logColor = "\x1b[37m";
      const log = (() => {
        if (Math.sign(percentageDiff) === 1) {
          logColor = "\x1b[31m";
          return `${percentageDiff.toString().replace("-", "") "%"} slower`;
        } else if (Math.sign(percentageDiff) === 0) {
          return "unchanged";
        } autre {
          logColor = "\x1b[32m";
          return `${percentageDiff.toString().replace("-", "") "%"} faster`;
        }
      }) ();
      console.log(logColor, `${from["audits"][auditObj].title} is ${log}`);
    }
  }
};

if (argv.from && argv.to) {
  compareReports(
    getContents(argv.from ".json"),
    getContents(argv.to ".json")
  ));
} else if (argv.url) {
  const urlObj = new URL(argv.url);
  let dirName = urlObj.host.replace("www.", "");
  if (urlObj.pathname !== "/") {
    dirName = dirName urlObj.pathname.replace(/\//g, "_");
  }

  if (!fs.existsSync(dirName)) {
    fs.mkdirSync(dirName);
  }

  launchChromeAndRunLighthouse(argv.url).then(results => {
    const prevReports = glob(`${dirName}/*.json`, {
      sync: true
    });

    if (prevReports.length) {
      dates = [];
      for (report in prevReports) {
        dates.push(
          new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))
        ));
      }
      const max = dates.reduce(function(a, b) {
        return Math.max(a, b);
      });
      const recentReport = new Date(max).toISOString();

      const recentReportContents = getContents(
        dirName "/" recentReport.replace(/:/g, "_") ".json"
      ));

      compareReports(recentReportContents, results.js);
    }

    fs.writeFile(
      `${dirName}/${results.js["fetchTime"].replace(/:/g, "_")}.json`,
      results.json,
      err => {
        si (err) jetez ERR;
      }
    ));
  });
} autre {
  throw "You haven't passed a URL to Lighthouse";
}
Copier après la connexion

View Gist

Étapes suivantes

With the completion of this basic Google Lighthouse tool, there's plenty of ways to develop it further. Par exemple:

  • Some kind of simple online dashboard that allows non-technical users to run Lighthouse audits and view metrics develop over time. Getting stakeholders behind web performance can be challenging, so something tangible they can interest with themselves could pique their interest.
  • Build support for performance budgets, so if a report is generated and performance metrics are slower than they should be, then the tool outputs useful advice on how to improve them (or calls you names).

Bonne chance!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn

Outils d'IA chauds

Undresser.AI Undress

Undresser.AI Undress

Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover

AI Clothes Remover

Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool

Undress AI Tool

Images de déshabillage gratuites

Clothoff.io

Clothoff.io

Dissolvant de vêtements AI

Video Face Swap

Video Face Swap

Échangez les visages dans n'importe quelle vidéo sans effort grâce à notre outil d'échange de visage AI entièrement gratuit !

Outils chauds

Bloc-notes++7.3.1

Bloc-notes++7.3.1

Éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise

SublimeText3 version chinoise

Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1

Envoyer Studio 13.0.1

Puissant environnement de développement intégré PHP

Dreamweaver CS6

Dreamweaver CS6

Outils de développement Web visuel

SublimeText3 version Mac

SublimeText3 version Mac

Logiciel d'édition de code au niveau de Dieu (SublimeText3)

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

Il est sorti! Félicitations à l'équipe Vue pour l'avoir fait, je sais que ce fut un effort massif et une longue période à venir. Tous les nouveaux documents aussi.

Pouvez-vous obtenir des valeurs de propriété CSS valides du navigateur? Pouvez-vous obtenir des valeurs de propriété CSS valides du navigateur? Apr 02, 2025 pm 06:17 PM

J'ai eu quelqu'un qui écrivait avec cette question très légitime. Lea vient de bloguer sur la façon dont vous pouvez obtenir les propriétés CSS valides elles-mêmes du navigateur. C'est comme ça.

Un peu sur CI / CD Un peu sur CI / CD Apr 02, 2025 pm 06:21 PM

Je dirais que "Site Web" correspond mieux que "Application mobile" mais j'aime ce cadrage de Max Lynch:

Cartes empilées avec un positionnement collant et une pincée de sass Cartes empilées avec un positionnement collant et une pincée de sass Apr 03, 2025 am 10:30 AM

L'autre jour, j'ai repéré ce morceau particulièrement charmant sur le site Web de Corey Ginnivan où une collection de cartes se cassent les uns sur les autres pendant que vous faites défiler.

Utilisation de Markdown et de la localisation dans l'éditeur de blocs WordPress Utilisation de Markdown et de la localisation dans l'éditeur de blocs WordPress Apr 02, 2025 am 04:27 AM

Si nous devons afficher la documentation à l'utilisateur directement dans l'éditeur WordPress, quelle est la meilleure façon de le faire?

Comparaison des navigateurs pour une conception réactive Comparaison des navigateurs pour une conception réactive Apr 02, 2025 pm 06:25 PM

Il existe un certain nombre de ces applications de bureau où l'objectif montre votre site à différentes dimensions en même temps. Vous pouvez donc, par exemple, écrire

Comment utiliser la grille CSS pour les en-têtes et pieds de page collants Comment utiliser la grille CSS pour les en-têtes et pieds de page collants Apr 02, 2025 pm 06:29 PM

CSS Grid est une collection de propriétés conçues pour faciliter la mise en page qu'elle ne l'a jamais été. Comme tout, il y a un peu une courbe d'apprentissage, mais Grid est

Pourquoi les zones réduites pourpre dans la disposition Flex sont-elles considérées à tort «espace de débordement»? Pourquoi les zones réduites pourpre dans la disposition Flex sont-elles considérées à tort «espace de débordement»? Apr 05, 2025 pm 05:51 PM

Questions sur les zones de slash violet dans les dispositions flexibles Lorsque vous utilisez des dispositions flexibles, vous pouvez rencontrer des phénomènes déroutants, comme dans les outils du développeur (D ...

See all articles