VinceOPS.mehttps://vinceops.me2020-12-12T00:00:00+00:00Télétravail : Mon bureau "home made"https://vinceops.me/remote-bureau-diy/2020-12-12T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 650px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/28a90607224f9e246f0289f911342e14/8ea4d/diy-desk.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 43.07692307692307%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEBf/EABUBAQEAAAAAAAAAAAAAAAAAAAID/9oADAMBAAIQAxAAAAGCjMfYsEFT/8QAGRAAAwEBAQAAAAAAAAAAAAAAAAECAzFC/9oACAEBAAEFAlaZluoNE6v3PUf/xAAWEQEBAQAAAAAAAAAAAAAAAAAAESH/2gAIAQMBAT8BjX//xAAXEQADAQAAAAAAAAAAAAAAAAAAAQIR/9oACAECAQE/AdyRQj//xAAaEAACAgMAAAAAAAAAAAAAAAAAAQJRECEy/9oACAEBAAY/AqOIjko6dDHj/8QAGhABAAMBAQEAAAAAAAAAAAAAAQARMSFBof/aAAgBAQABPyExHrIE513W5R4SwxCHxT1P/9oADAMBAAIAAwAAABCA/wD/xAAWEQEBAQAAAAAAAAAAAAAAAAABACH/2gAIAQMBAT8Q0yjL/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQAhQf/aAAgBAgEBPxB2J2SDf//EAB0QAQACAgIDAAAAAAAAAAAAAAEAESExQVFxobH/2gAIAQEAAT8QXKhUOYS3gDdHlvAwHVQBQ6xPunvJp5T/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/28a90607224f9e246f0289f911342e14/798b8/diy-desk.webp 195w, /static/28a90607224f9e246f0289f911342e14/0e356/diy-desk.webp 390w, /static/28a90607224f9e246f0289f911342e14/309d5/diy-desk.webp 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/webp" /> <source srcset="/static/28a90607224f9e246f0289f911342e14/0e421/diy-desk.jpg 195w, /static/28a90607224f9e246f0289f911342e14/ce2e0/diy-desk.jpg 390w, /static/28a90607224f9e246f0289f911342e14/8ea4d/diy-desk.jpg 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/28a90607224f9e246f0289f911342e14/8ea4d/diy-desk.jpg" alt="Bureau v1" title="Bureau v1" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Ces dernières semaines, j’ai vu passer plusieurs vidéos de Youtubeurs (dévs & <em>gamers</em>) présentant leur <em>setup</em> et certains m’ont bluffé ! Puisque je reprends le télétravail à temps complet avec mon nouveau job, j’ai décidé de me fabriquer un bureau digne de ce nom, en piquant quelques bonnes idées. L’objectif étant de gagner avant tout en confort, puisque j’utilisais depuis quelques semaines une console de 45cm de profondeur 😅.</p> <p>Dans cet article un peu inhabituel car <strong>non technique</strong>, je résume les étapes de création de mon modeste “nouveau” <em>setup</em>.</p> <hr /> <h2>Le matériel</h2> <p><em>Aucun sponsoring, ni placement de produit. La plupart des fournitures peuvent être achetées dans des enseignes physiques… Mais c’est plus compliqué en plein confinement.</em></p> <p>Avant de me lancer, j’ai évidemment fait le point sur les contraintes esthétiques et budgétaires ainsi que sur l’espace disponible. Mais aussi sur le temps que je voulais et pouvais y accorder. Le bricolage, comme le développement, a ce goût facheux de “on peut toujours faire mieux” et “oups, ça fait déjà X heures que j’y suis”.</p> <h3>Plan, ou plateau, ou tablette</h3> <p>Je le voulais épais et en bois brut, profond d’au moins 60 cm et long de 140 cm. J’ai trouvé chez Leroy Merlin des plans de travail en chêne <a href="https://fr.wikipedia.org/wiki/Bois_lamell%C3%A9-coll%C3%A9" target="_blank" rel="noopener noreferrer">lamellé collé</a> de 250 x 65 x 3.6 cm, <a href="https://www.leroymerlin.fr/v3/p/produits/plan-de-travail-bois-chene-lamelle-brut-l-250-x-p-65-cm-ep-36-mm-e1501080820" target="_blank" rel="noopener noreferrer">pour 149.5€</a>. J’ai payé 5€ (en magasin) pour une coupe à 140 cm. J’ai déjà prévu de faire un banc avec la chute de 110 cm, mais ça, c’est totalement hors-sujet 😂.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/ede58e03a1b07b5f7c5cc46ec316d913/e224a/bois-avant-apres.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 63.5897435897436%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEDBP/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAG6wbZWTKP/xAAaEAACAwEBAAAAAAAAAAAAAAAAAQIDEQQS/9oACAEBAAEFAsTMwaRzrL7Z+IqTP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABkQAAMAAwAAAAAAAAAAAAAAAAEQEQBBYf/aAAgBAQAGPwLTHRllX//EAB0QAAIBBAMAAAAAAAAAAAAAAAABESExUWFBcdH/2gAIAQEAAT8he+6JW1WBhyQRciZuBkieH//aAAwDAQACAAMAAAAQ9M//xAAWEQEBAQAAAAAAAAAAAAAAAAABABH/2gAIAQMBAT8Q0S2//8QAFhEBAQEAAAAAAAAAAAAAAAAAAREA/9oACAECAQE/EIjNN//EABsQAQADAAMBAAAAAAAAAAAAAAEAESExQVHB/9oACAEBAAE/EEdETsLGtanKExR3wgMU5p5vyW76AYi7qa8Cgn//2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/ede58e03a1b07b5f7c5cc46ec316d913/798b8/bois-avant-apres.webp 195w, /static/ede58e03a1b07b5f7c5cc46ec316d913/0e356/bois-avant-apres.webp 390w, /static/ede58e03a1b07b5f7c5cc46ec316d913/411c4/bois-avant-apres.webp 600w" sizes="(max-width: 600px) 100vw, 600px" type="image/webp" /> <source srcset="/static/ede58e03a1b07b5f7c5cc46ec316d913/0e421/bois-avant-apres.jpg 195w, /static/ede58e03a1b07b5f7c5cc46ec316d913/ce2e0/bois-avant-apres.jpg 390w, /static/ede58e03a1b07b5f7c5cc46ec316d913/e224a/bois-avant-apres.jpg 600w" sizes="(max-width: 600px) 100vw, 600px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/ede58e03a1b07b5f7c5cc46ec316d913/e224a/bois-avant-apres.jpg" alt="bois-avant-apres" title="bois-avant-apres" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> </p> <p style="text-align: center"><em>La coupe de 140 cm vernie (derrière) et sa chute</em></p> <p>J’ai aussi acheté du vernis pour embellir la teinte du bois et le protéger des liquides (<em>coffee & tea snob speaking 🤓</em>). Un vernis mat couleur chêne clair à <a href="https://www.leroymerlin.fr/v3/p/produits/vernis-meuble-et-objet-v33-chene-clair-mat-0-25l-e1400037775" target="_blank" rel="noopener noreferrer">11.90€</a>. J’avais déjà le pinceau (premier prix).</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 650px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/77f500c49c42023a57d92e4e78103e87/8ea4d/vernis.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 68.20512820512819%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIEA//EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAGhsXGoUl//xAAaEAACAwEBAAAAAAAAAAAAAAABAgADEhAR/9oACAEBAAEFArHIWptCONSpPOf/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAZEAABBQAAAAAAAAAAAAAAAAAQAAECIXH/2gAIAQEABj8CWGVuP//EABsQAAMAAgMAAAAAAAAAAAAAAAABESExQWFx/9oACAEBAAE/IbZOeCY1vsU028CqLE0pwU//2gAMAwEAAgADAAAAEGDP/8QAFhEAAwAAAAAAAAAAAAAAAAAAEBEh/9oACAEDAQE/EKx//8QAFhEBAQEAAAAAAAAAAAAAAAAAEQEQ/9oACAECAQE/EGGf/8QAGxABAAMBAAMAAAAAAAAAAAAAAQARIUExYcH/2gAIAQEAAT8QvJC0vkRmmKrVqc3sRBF49Jf7WEFhM+wNabP/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/77f500c49c42023a57d92e4e78103e87/798b8/vernis.webp 195w, /static/77f500c49c42023a57d92e4e78103e87/0e356/vernis.webp 390w, /static/77f500c49c42023a57d92e4e78103e87/309d5/vernis.webp 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/webp" /> <source srcset="/static/77f500c49c42023a57d92e4e78103e87/0e421/vernis.jpg 195w, /static/77f500c49c42023a57d92e4e78103e87/ce2e0/vernis.jpg 390w, /static/77f500c49c42023a57d92e4e78103e87/8ea4d/vernis.jpg 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/77f500c49c42023a57d92e4e78103e87/8ea4d/vernis.jpg" alt="vernis" title="vernis" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><strong>Coût</strong> : ~ 167€ (avec une chute de 110 cm).</p> <blockquote> <p><span class="emoji">💡</span> <strong>Minute menuiserie</strong> : j’ai lu plusieurs recommandations d’utiliser des produits Rubio plutôt que du vernis : n’ayant pas pu essayé, je n’ai pas d’avis. Mais ils sont bien plus chers et je n’avais pas envie d’attendre la livraison !</p> </blockquote> <p>Enfin, il faut poncer ce plateau ! J’avais déjà la ponceuse, les feuilles abrasives (grain 120 et 240) et les tréteaux. Les tréteaux sont optionnels mais il faut absolument une cale à poncer (à défaut d’une ponceuse) et des feuilles abrasives.</p> <h3>Les pieds</h3> <p>Je savais précisément ce que je voulais : des cadres en acier d’au moins 70 cm de haut et de 50 cm de profondeur. C’est sur Amazon que j’ai trouvé le meilleur prix. <a href="https://www.amazon.fr/gp/product/B07NQLG9M6" target="_blank" rel="noopener noreferrer">Ce modèle</a> de 72 x 50 x 6 cm, pour 94€ (84€ de pieds, 10€ de livraison).</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/30c414e8bef4ae6efd3ac43a27c78345/953fe/pied.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 96.9230769230769%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAATABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAMEBQL/xAAVAQEBAAAAAAAAAAAAAAAAAAABAv/aAAwDAQACEAMQAAAB9QhqisiZOjUHnij/xAAbEAEAAgIDAAAAAAAAAAAAAAABAgMAERIiQv/aAAgBAQABBQLfHJW9bZsFWycIjlzuwBPEAc//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AR//xAAgEAACAAQHAAAAAAAAAAAAAAAAAQIRMUEDEBIhMlGh/9oACAEBAAY/AhrDtcUM6I00FLgvRm6yin2f/8QAGRABAQEBAQEAAAAAAAAAAAAAAREAIVFx/9oACAEBAAE/IShnKSCOA/ox+7oc+6AMW/WrjcWOeKea6Fb3/9oADAMBAAIAAwAAABBXF0L/xAAXEQEAAwAAAAAAAAAAAAAAAAABABAh/9oACAEDAQE/EF22f//EABcRAQEBAQAAAAAAAAAAAAAAAAEAETH/2gAIAQIBAT8QDCAiOX//xAAeEAEAAgIDAAMAAAAAAAAAAAABABExQSFhcVGhsf/aAAgBAQABPxC8iDeWGyODtWgPVjReQS9l/Zx92pydTEUNdW/D7nfamoMC0Mk+NUgrXEIBoLfCf//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/30c414e8bef4ae6efd3ac43a27c78345/798b8/pied.webp 195w, /static/30c414e8bef4ae6efd3ac43a27c78345/0e356/pied.webp 390w, /static/30c414e8bef4ae6efd3ac43a27c78345/601b1/pied.webp 500w" sizes="(max-width: 500px) 100vw, 500px" type="image/webp" /> <source srcset="/static/30c414e8bef4ae6efd3ac43a27c78345/0e421/pied.jpg 195w, /static/30c414e8bef4ae6efd3ac43a27c78345/ce2e0/pied.jpg 390w, /static/30c414e8bef4ae6efd3ac43a27c78345/953fe/pied.jpg 500w" sizes="(max-width: 500px) 100vw, 500px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/30c414e8bef4ae6efd3ac43a27c78345/953fe/pied.jpg" alt="Pied cadre en acier" title="Pied cadre en acier" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Pour l’assemblage, j’ai acheté des vis <a href="https://www.leroymerlin.fr/v3/p/quincaillerie-en-vrac-en-libre-service-l1401590326" target="_blank" rel="noopener noreferrer">au poids</a> chez Leroy Merlin, de 6 mm de diamètre (le plus épais à la vente, avant de passer aux tire-fonds) et de 30 mm de long, la planche faisant 36 mm d’épaisseur. Coût négligeable : moins de 2€.</p> <p><strong>Coût</strong> : ~ 96€.</p> <h2>Au travail</h2> <h3>Assemblage</h3> <p>J’ai pu préparer le plateau en attendant les pieds. Les étapes sont relativement simples :</p> <ul> <li>Ponçage (grain 120), un coup d’aspirateur et on essuie au chiffon humide.</li> <li>Première couche de vernis (au pinceau) que je laisse sécher 2 heures.</li> <li>Ponçage très léger (grain 240) pour enlever les aspérités (“égrener”), un coup d’aspirateur et on essuie au chiffon humide.</li> <li>Deuxième et dernière couche de vernis (toujours au pinceau), que je laisse sécher 24 heures.</li> <li>Ponçage superficiel (grain 240) pour obtenir une surface lisse… Et on nettoie encore.</li> </ul> <p>J’ai regardé quelle face avait le meilleur aspect, pour en faire le dessus du bureau. C’est naturellement dans l’autre face que j’ai vissé les pieds.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/92523c953fb2d0ad12539ded143b4914/953fe/assemblage-pieds.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 133.33333333333331%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAbABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIDBAH/xAAXAQADAQAAAAAAAAAAAAAAAAAAAQID/9oADAMBAAIQAxAAAAFtEeZ3MiNa890luIB//8QAGxABAQADAAMAAAAAAAAAAAAAAQIAAxESIkH/2gAIAQEAAQUC2htjWkxR18Tt0GN7VrrM5PqGfc//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAVEQEBAAAAAAAAAAAAAAAAAAARIP/aAAgBAgEBPwFj/8QAHRAAAQQCAwAAAAAAAAAAAAAAAQAQETEhUQIgIv/aAAgBAQAGPwKKA6ZvS88UYtYEbJUBi3//xAAeEAADAAEEAwAAAAAAAAAAAAAAARExIUFRcRCBsf/aAAgBAQABPyGTbi8CZsmqjX3S1tXvBGyBrpFpohETTaVhz9v0SnAuRj6eP//aAAwDAQACAAMAAAAQmxrD/8QAFxEAAwEAAAAAAAAAAAAAAAAAABARQf/aAAgBAwEBPxCaV//EABcRAAMBAAAAAAAAAAAAAAAAAAARIRD/2gAIAQIBAT8QqCHn/8QAHhABAQACAwEAAwAAAAAAAAAAAREAITFBUWFxgeH/2gAIAQEAAT8QCxZKhQJJ51kEQbw4yo4eg8zb5O4rXs4uFgBOgVubRnQDHyv5zRntuP1hA8J7Vd/1iW+T6XtfuMwVabuG6daYM0Z//9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/92523c953fb2d0ad12539ded143b4914/798b8/assemblage-pieds.webp 195w, /static/92523c953fb2d0ad12539ded143b4914/0e356/assemblage-pieds.webp 390w, /static/92523c953fb2d0ad12539ded143b4914/601b1/assemblage-pieds.webp 500w" sizes="(max-width: 500px) 100vw, 500px" type="image/webp" /> <source srcset="/static/92523c953fb2d0ad12539ded143b4914/0e421/assemblage-pieds.jpg 195w, /static/92523c953fb2d0ad12539ded143b4914/ce2e0/assemblage-pieds.jpg 390w, /static/92523c953fb2d0ad12539ded143b4914/953fe/assemblage-pieds.jpg 500w" sizes="(max-width: 500px) 100vw, 500px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/92523c953fb2d0ad12539ded143b4914/953fe/assemblage-pieds.jpg" alt="Assemblage" title="Assemblage" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p style="text-align: center"><em>Centrés sur la profondeur et écartés du bord de ~1/8ème de la largeur du plan</em></p> <p>Et voici ce qu’on obtient pour 263€ (hors pinceau et ponceuse/papier de verre) :</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/5fbd6adc98fb6ef43d2940269f5f12d1/953fe/bureau-en-place.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 105.64102564102565%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAVABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAMEBQEC/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQID/9oADAMBAAIQAxAAAAH3LnczvTVyZdoScD//xAAaEAACAwEBAAAAAAAAAAAAAAABAgADEhEh/9oACAEBAAEFAtExX5GqSwofHdhEToBmNBXyP//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABYRAQEBAAAAAAAAAAAAAAAAABABEf/aAAgBAgEBPwHCn//EABsQAAICAwEAAAAAAAAAAAAAAAABERICEDEh/9oACAEBAAY/AunpYtPFtieTkg//xAAbEAEAAwEAAwAAAAAAAAAAAAABABEhMUFRgf/aAAgBAQABPyGp1Hzsl/L8Zw0tCQ70JiTZYwg8k2ljhhGrlhP/2gAMAwEAAgADAAAAELQXfP/EABkRAAIDAQAAAAAAAAAAAAAAAAABEBEhMf/aAAgBAwEBPxC1WmC5H//EABgRAAMBAQAAAAAAAAAAAAAAAAARIRAx/9oACAECAQE/EGcKdZ//xAAdEAEAAwEAAgMAAAAAAAAAAAABABExIUFhUYGR/9oACAEBAAE/EPXQKHNJRWfP5JarUDE/Ip1VPGiOFTiHH3AvVV1dmCTXfZWwQJyPrzAAMCvZ/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/5fbd6adc98fb6ef43d2940269f5f12d1/798b8/bureau-en-place.webp 195w, /static/5fbd6adc98fb6ef43d2940269f5f12d1/0e356/bureau-en-place.webp 390w, /static/5fbd6adc98fb6ef43d2940269f5f12d1/601b1/bureau-en-place.webp 500w" sizes="(max-width: 500px) 100vw, 500px" type="image/webp" /> <source srcset="/static/5fbd6adc98fb6ef43d2940269f5f12d1/0e421/bureau-en-place.jpg 195w, /static/5fbd6adc98fb6ef43d2940269f5f12d1/ce2e0/bureau-en-place.jpg 390w, /static/5fbd6adc98fb6ef43d2940269f5f12d1/953fe/bureau-en-place.jpg 500w" sizes="(max-width: 500px) 100vw, 500px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/5fbd6adc98fb6ef43d2940269f5f12d1/953fe/bureau-en-place.jpg" alt="Basic desk" title="Basic desk" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>J’ai profité des étapes de ponçage pour arrondir les angles (<strong>border-radius</strong> quand tu nous tiens !) et rendre la pose des avant-bras plus confortables, en plus d’adoucir le rendu visuel du plan. À ce stade, j’avais déjà investi environ 4 heures dans le projet, en comptant l’aller-retour en magasin.</p> <h3>Pimp My Desk</h3> <p>C’est super, mais on est <a href="https://www.youtube.com/watch?v=aRgqQe-8zYk" target="_blank" rel="noopener noreferrer">encore loin</a> des bureaux de <a href="https://www.youtube.com/watch?v=QVEp781Welg" target="_blank" rel="noopener noreferrer">grands seigneurs</a> de Youtube. Certes, mais on peut toujours s’en approcher. Enfin, voilà où j’en suis actuellement :</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 650px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/855c9dae8bf19a7dbb75aa2633cd6a46/8ea4d/final-desk-red.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 65.64102564102564%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAwAEBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAHKvIYpZoP/xAAZEAACAwEAAAAAAAAAAAAAAAABAgADMRH/2gAIAQEAAQUCFkW7osR2fSpgz//EABYRAQEBAAAAAAAAAAAAAAAAAAABIf/aAAgBAwEBPwGRj//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/Aar/xAAZEAEAAwEBAAAAAAAAAAAAAAABABARAmH/2gAIAQEABj8C9ImckXKyv//EABoQAAMBAQEBAAAAAAAAAAAAAAABEUEhMWH/2gAIAQEAAT8hqmmoyQb1V88EgtZGV0o21miqeYf/2gAMAwEAAgADAAAAEH//AP/EABcRAQEBAQAAAAAAAAAAAAAAAAEAESH/2gAIAQMBAT8QBOlk/8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERMf/aAAgBAgEBPxClhR//xAAZEAEAAwEBAAAAAAAAAAAAAAABABEhMbH/2gAIAQEAAT8QpvUWkd8Ybt0S+06LpH6viBKi40K7T2KYChpdgLbKFWf/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/855c9dae8bf19a7dbb75aa2633cd6a46/798b8/final-desk-red.webp 195w, /static/855c9dae8bf19a7dbb75aa2633cd6a46/0e356/final-desk-red.webp 390w, /static/855c9dae8bf19a7dbb75aa2633cd6a46/309d5/final-desk-red.webp 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/webp" /> <source srcset="/static/855c9dae8bf19a7dbb75aa2633cd6a46/0e421/final-desk-red.jpg 195w, /static/855c9dae8bf19a7dbb75aa2633cd6a46/ce2e0/final-desk-red.jpg 390w, /static/855c9dae8bf19a7dbb75aa2633cd6a46/8ea4d/final-desk-red.jpg 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/855c9dae8bf19a7dbb75aa2633cd6a46/8ea4d/final-desk-red.jpg" alt="Pimped desk" title="Pimped desk" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <h4>Un piédestal pour leurs majestés</h4> <p>J’aime travailler en dual screen, en utilisant mon laptop comme écran secondaire. Pour des raisons de confort (et faciliter son refroidissement), j’ai acheté un support dédié à <a href="https://www.amazon.fr/gp/product/B07T4K1SXX" target="_blank" rel="noopener noreferrer">30€</a>, simple mais efficace, avec un design tout à fait valide pour ce prix.</p> <p>Quant à moi, j’ai découvert récemment les joies du coussin repose-pieds et en ai acheté un pour <a href="https://www.amazon.fr/gp/product/B0855J8KR6" target="_blank" rel="noopener noreferrer">15€</a>.</p> <h4>Couvrez ce câble que je ne saurais voir</h4> <p>Le <em>cable management</em>, étape si chère à tant de perfectionnistes, joue un role clé dans le rendu final du <em>setup</em>. J’avais déjà des serre-câbles auto-bloquants, j’ai donc seulement acheté des clips adhésifs sur Amazon pour <a href="https://www.amazon.fr/gp/product/B07Z1JTZNP/" target="_blank" rel="noopener noreferrer">12€</a> et du ruban adhésif double-face pour <a href="https://www.amazon.fr/gp/product/B00EDLPG7Y" target="_blank" rel="noopener noreferrer">10€</a>.</p> <p>La prise de courant la plus proche étant face au pied “arrière gauche”, j’ai profité de celui-ci pour y cacher ma multiprise (collée au double-face). Dans la mesure où je n’ai pas opté pour des <a href="https://www.fully.com/standing-desks/jarvis.html" target="_blank" rel="noopener noreferrer">pieds réglables</a> (je n’aime pas bosser debout), le reste a été extrêmement simplifié par l’achat d’un bras-support pour mon écran (un vieux iiyama 24” qui prendra bientôt sa retraite) pour <a href="https://www.amazon.fr/gp/product/B01MZ70QJB" target="_blank" rel="noopener noreferrer">35€</a>, déjà équipé de de bagues passe-câbles.</p> <p>J’ai suivi deux règles : </p> <ul> <li>Cacher tout ce qui était visible en étant face au bureau, ou de biais (assis et debout).</li> <li>Ne pas faire de “plats de spaghetti”, pour faciliter l’ajout de futurs équipements.</li> </ul> <p>Les clips permettent de plaquer les câbles sous le bureau et de les faire remonter jusqu’au bras-support de l’écran, qui permet de tout cacher (hub USB + HDMI + alimentation, branché au laptop avec un seul port USB-C).</p> <h4>Gimme the light</h4> <p>Enfin, cette petite touche “kikoo” qui fait toute la différence (et un peu de rétro-éclairage la nuit) : la bande de LEDs !</p> <p>J’ai acheté une longueur de 2 mètres (4 x 50 cm) pour <a href="https://www.amazon.fr/gp/product/B07WNQXGC1" target="_blank" rel="noopener noreferrer">12€</a>, que j’ai faite passer sur les deux chants face au mur, avec un retour de 50 cm sous le bureau, qui éclaire gentiment le sol. La bande est alimentée en USB et peut être pilotée en bluetooth ou avec sa télécommande.</p> <blockquote> <p><span class="emoji">🦀</span> Grâce à <a href="https://blog.wokwi.com/reverse-engineering-a-bluetooth-lightbulb/" target="_blank" rel="noopener noreferrer">cet article</a>, j’ai pu récupérer les messages envoyés à la bande, puis identifier ceux qui permettaient de contrôler la couleur et l’intensité lumineuse… Et grâce à <a href="https://github.com/szeged/blurz" target="_blank" rel="noopener noreferrer">cette lib</a> Rust, alterner le Rouge & Vert <strong>depuis mon laptop</strong>. J’ai donc bien envie de bricoler un plugin Jest qui rythmerait mes sessions de red/green/refacto en TDD 🤩.</p> </blockquote> <h2>Bilan</h2> <p>Du côté des coûts :</p> <ul> <li>Environ 263€ pour le meuble (+ une chute de 110 cm de plan)</li> <li>Environ 114€ d’accessoires (j’ai utilisé un dixième des clips et du ruban double-face)</li> </ul> <p>Du côté du temps :</p> <ul> <li>Environ 4 heures pour le meuble</li> <li>Environ 2 heures pour l’assemblage du bras, le <em>cable management</em>, le passage des LEDs</li> <li>4 bonnes heures à : récupérer les échanges Bluetooth entre mon smartphone et la bande de LEDs, configurer le bluetooth sur ma machine toute neuve (Arch 5.9+) et m’éclater comme un gosse avec Rust et les couleurs 🤦😂…</li> </ul> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 650px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/4c656c2150aac4146e264f7e778664dd/8ea4d/final-desk-blue.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 70.76923076923076%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQCAwX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAgP/2gAMAwEAAhADEAAAAUGMljeaiQf/xAAbEAABBAMAAAAAAAAAAAAAAAABAAMREgIhIv/aAAgBAQABBQK4IDjdMgZnc8r/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAXEQEBAQEAAAAAAAAAAAAAAAABABIh/9oACAECAQE/ATpYL//EABsQAAIBBQAAAAAAAAAAAAAAAAARARMgITFB/9oACAEBAAY/AhUofMmrP//EABsQAQADAAMBAAAAAAAAAAAAAAEAESExQZFR/9oACAEBAAE/IRfp+Ma9q7abg9LyCsksUxuW1y+z/9oADAMBAAIAAwAAABCgP//EABcRAQEBAQAAAAAAAAAAAAAAAAEAITH/2gAIAQMBAT8QTewt/8QAGBEBAQEBAQAAAAAAAAAAAAAAAREAMUH/2gAIAQIBAT8QTMLO+ZZt3//EABoQAQADAQEBAAAAAAAAAAAAAAEAESFBUTH/2gAIAQEAAT8QHFAx+kzNASgjNayV22BAUaVnGh1TTuQreXqf/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/4c656c2150aac4146e264f7e778664dd/798b8/final-desk-blue.webp 195w, /static/4c656c2150aac4146e264f7e778664dd/0e356/final-desk-blue.webp 390w, /static/4c656c2150aac4146e264f7e778664dd/309d5/final-desk-blue.webp 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/webp" /> <source srcset="/static/4c656c2150aac4146e264f7e778664dd/0e421/final-desk-blue.jpg 195w, /static/4c656c2150aac4146e264f7e778664dd/ce2e0/final-desk-blue.jpg 390w, /static/4c656c2150aac4146e264f7e778664dd/8ea4d/final-desk-blue.jpg 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/4c656c2150aac4146e264f7e778664dd/8ea4d/final-desk-blue.jpg" alt="Pimped desk" title="Pimped desk" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>J’étais parti pour fabriquer mon propre tiroir, mais cela nécessite beaucoup plus de matériel pour être bien fait. Du coup, j’hésite encore entre plusieurs modèles de caissons noirs en acier.</p> <blockquote> <p><span class="emoji">💡</span> Mon siège vient d’Ikea, modèle “VOLMAR” (acheté 300€ en 2016).<br /> Le clavier est un Logitech K800 (49€ lors d’une promotion Amazon, il y a un mois).<br /> La souris est le modèle 2020 “ergonomique” de VicTising (15€).<br /> Le hub USB est une sous-marque aussi, à 35€ (HDMI + ports USB + USB-C power).<br /> Je projette d’acheter l’écran 27” iiyama ProLite XUB2792QSU pour remplacer l’actuel 24”.</p> </blockquote> <p>À très vite pour un article plus technique 🎅 !</p></div>GitLab CI : Intégré comme jamaishttps://vinceops.me/gitlab-ci-cd/2020-11-12T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/c27591b83841c00b5c49a1c29690fbe4/485b7/gitlab-ci.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 35.8974358974359%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAMEBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHcqhIK/8QAGRABAAIDAAAAAAAAAAAAAAAAAgEDBBIh/9oACAEBAAEFAu7ZFkCsXBH/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAWEQEBAQAAAAAAAAAAAAAAAAAAARH/2gAIAQIBAT8BjH//xAAXEAEBAQEAAAAAAAAAAAAAAAABAAIh/9oACAEBAAY/AmTT1Lmr/8QAGBABAQEBAQAAAAAAAAAAAAAAAREAIYH/2gAIAQEAAT8hlFZOGQSIkMNaeb//2gAMAwEAAgADAAAAEPvv/8QAFhEBAQEAAAAAAAAAAAAAAAAAARAR/9oACAEDAQE/EF2f/8QAFhEAAwAAAAAAAAAAAAAAAAAAAAEh/9oACAECAQE/EFo6P//EABkQAQADAQEAAAAAAAAAAAAAAAEAESExcf/aAAgBAQABPxALSqIlDBwqHReY8I8wUCrG68n/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/c27591b83841c00b5c49a1c29690fbe4/798b8/gitlab-ci.webp 195w, /static/c27591b83841c00b5c49a1c29690fbe4/0e356/gitlab-ci.webp 390w, /static/c27591b83841c00b5c49a1c29690fbe4/23bbb/gitlab-ci.webp 780w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/c27591b83841c00b5c49a1c29690fbe4/0e421/gitlab-ci.jpg 195w, /static/c27591b83841c00b5c49a1c29690fbe4/ce2e0/gitlab-ci.jpg 390w, /static/c27591b83841c00b5c49a1c29690fbe4/485b7/gitlab-ci.jpg 780w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/c27591b83841c00b5c49a1c29690fbe4/485b7/gitlab-ci.jpg" alt="Gitlab CI" title="Gitlab CI" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Dernièrement, j’ai constaté que la CI (<em>Continuous Integration</em>) est un <em>skill</em> rare chez les développeurs, même pour des profils expérimentés. Sans en être un expert absolu, je voulais en livrer <strong>ma</strong> vision et quelques éléments pour démarrer. Je présente dans cet article une intégration continue “type” que j’ai utilisée dans l’un de mes projets, avec GitLab CI et mon propre serveur <em>runner</em>.</p> <hr /> <h2>Rappel sur la CI</h2> <h3>L’objectif</h3> <p>La plupart du temps, les développeurs écrivent du code. L’intégration continue encourage la fusion (<em>merge</em>) fréquente de ce code dans le projet, plutôt que d’attendre l’approche d’une livraison pour y intégrer tous les derniers développements simultanément. Cela permet de drastiquement diminuer la quantité de conflits à résoudre entre <em>feature branches</em>. Les équipes de <a href="https://en.wikipedia.org/wiki/Quality_assurance" target="_blank" rel="noopener noreferrer">QA</a> apprécient aussi la réduction de la durée/intensité de leurs sessions de test.</p> <h3>En pratique</h3> <p>Pour augmenter la fréquence des fusions, il est impératif que chaque développeur sache le plus rapidement possible si ses modifications entraînent des régressions. La CI a pour but d’<strong>automatiser les tâches garantissant la santé du code</strong> : <em>lint</em>, compilation, mesure de qualité, exécution des tests, etc. Ces tâches sont ainsi exécutées à chaque modification du code d’une branche, pour déterminer si le nouveau code est intégrable sans risque. Les erreurs sont ainsi détectées très tôt dans le processus de développement (<em>fail fast!</em>). </p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 454px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/8b78b29b9a694aa39bdbe6350f3864ca/5460d/ci-pipeline-process.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 57.94871794871794%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAd4sBX//xAAUEAEAAAAAAAAAAAAAAAAAAAAg/9oACAEBAAEFAl//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAg/9oACAEBAAY/Al//xAAYEAADAQEAAAAAAAAAAAAAAAAAAREQQf/aAAgBAQABPyGjKPhM/9oADAMBAAIAAwAAABCsz//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABoQAQEBAQADAAAAAAAAAAAAAAEAETFRYXH/2gAIAQEAAT8QU5nmQ56YDd/Uh7Bl/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/8b78b29b9a694aa39bdbe6350f3864ca/798b8/ci-pipeline-process.webp 195w, /static/8b78b29b9a694aa39bdbe6350f3864ca/0e356/ci-pipeline-process.webp 390w, /static/8b78b29b9a694aa39bdbe6350f3864ca/19cdf/ci-pipeline-process.webp 454w" sizes="(max-width: 454px) 100vw, 454px" type="image/webp" /> <source srcset="/static/8b78b29b9a694aa39bdbe6350f3864ca/0e421/ci-pipeline-process.jpg 195w, /static/8b78b29b9a694aa39bdbe6350f3864ca/ce2e0/ci-pipeline-process.jpg 390w, /static/8b78b29b9a694aa39bdbe6350f3864ca/5460d/ci-pipeline-process.jpg 454w" sizes="(max-width: 454px) 100vw, 454px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/8b78b29b9a694aa39bdbe6350f3864ca/5460d/ci-pipeline-process.jpg" alt="CI process" title="CI process" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>En plus des outils de mesure de la qualité et des tests automatisés déjà présents dans le projet, il faut investir dans l’écriture d’un <strong>pipeline</strong> d’intégration continue. Cet investissement est vite rentabilisé par un gain significatif de productivité (confort, confiance).</p> <blockquote> <p><span class="emoji">💡</span> Une bonne pratique consiste à configurer l’interdiction de <em>Merge</em> une branche dont le pipeline d’intégration a échoué, puisque le code est jugé non-conforme. La branche principale gagne naturellement en “capital confiance”, quand on sait que le code qu’elle contient est forcément validé (en plus d’avoir été revu par des collègues).</p> </blockquote> <h2>Ce que j’utilise : GitLab CI</h2> <p>GitLab CI est la solution <em>open source</em> de CI/CD intégrée à la plateforme GitLab, elle-même open source. Chaque <em>push</em> sur chaque branche du projet va déclencher l’exécution du pipeline d’intégration. Elle est bien <a href="https://docs.gitlab.com/ee/ci/quick_start/" target="_blank" rel="noopener noreferrer">documentée</a> et gratuite dans la limite de 400 minutes par mois sur les <em>shared runners</em> : au-delà, il faut payer, ou utiliser son propre <em>runner</em>.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/e30e6d9c87efb82869f117f49e3eccb7/58df5/gitlab-ci-pipeline.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 15.8974358974359%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAADABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB34AH/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAGBAAAgMAAAAAAAAAAAAAAAAAAAEQETH/2gAIAQEAAT8hoeR//9oADAMBAAIAAwAAABBzz//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABgQAAMBAQAAAAAAAAAAAAAAAAABETFR/9oACAEBAAE/EI4IlSMw/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/e30e6d9c87efb82869f117f49e3eccb7/798b8/gitlab-ci-pipeline.webp 195w, /static/e30e6d9c87efb82869f117f49e3eccb7/0e356/gitlab-ci-pipeline.webp 390w, /static/e30e6d9c87efb82869f117f49e3eccb7/23bbb/gitlab-ci-pipeline.webp 780w, /static/e30e6d9c87efb82869f117f49e3eccb7/7c9ed/gitlab-ci-pipeline.webp 942w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/e30e6d9c87efb82869f117f49e3eccb7/0e421/gitlab-ci-pipeline.jpg 195w, /static/e30e6d9c87efb82869f117f49e3eccb7/ce2e0/gitlab-ci-pipeline.jpg 390w, /static/e30e6d9c87efb82869f117f49e3eccb7/485b7/gitlab-ci-pipeline.jpg 780w, /static/e30e6d9c87efb82869f117f49e3eccb7/58df5/gitlab-ci-pipeline.jpg 942w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/e30e6d9c87efb82869f117f49e3eccb7/485b7/gitlab-ci-pipeline.jpg" alt="Pipeline" title="Pipeline" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> </p> <p style="text-align: center"><em>Pipeline visible sur la page d'une Merge Request</em></p> <p>Il existe d’autres solutions, comme les <a href="https://github.com/features/actions" target="_blank" rel="noopener noreferrer">GitHub Actions</a>, <a href="https://travis-ci.org/" target="_blank" rel="noopener noreferrer">Travis</a> ou <a href="https://circleci.com/" target="_blank" rel="noopener noreferrer">CircleCI</a>. Contrairement à ses concurrents, GitLab CI n’offre <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/224124" target="_blank" rel="noopener noreferrer">pas encore</a> de <em>runner</em> “clé en main” sous Mac OS (donc pas de <em>build</em> Xcode).</p> <blockquote> <p><span class="emoji">💡</span> Dans mon équipe, la compilation de nos applications mobiles (<strong>iOS</strong> inclus) est confiée à <a href="https://www.bitrise.io/" target="_blank" rel="noopener noreferrer">Bitrise</a>.</p> </blockquote> <p>Enfin, il est possible de transformer n’importe quelle machine (PC, Macbook, carte ARM, serveur dédié…) en <em>runner</em>, en y installant <a href="https://docs.gitlab.com/runner/" target="_blank" rel="noopener noreferrer">GitLab Runner</a>. J’en dis plus dans la suite de l’article !</p> <h3>🔄 Dessine-moi un pipeline</h3> <p>Chaque pipeline est constitué d’un ou plusieurs <strong>jobs</strong> (travaux) regroupés en <strong>stages</strong> (étapes). Les <em>jobs</em> d’une même <em>stage</em> sont exécutés en parallèle. Si un <em>job</em> échoue, la <em>stage</em> échoue et interrompt l’exécution du pipeline : les <em>stages</em> suivantes ne sont pas exécutées. <em>À moins que le job n’ait été <a href="https://docs.gitlab.com/ee/ci/yaml/#allow_failure" target="_blank" rel="noopener noreferrer">autorisé à échouer</a></em>. La terminologie change d’une plateforme à une autre, mais on retrouve souvent la même construction. <a href="https://circleci.com/docs/2.0/workflows/" target="_blank" rel="noopener noreferrer">CircleCI</a> parle de Workflows et de Jobs, <a href="https://docs.travis-ci.com/user/build-stages/" target="_blank" rel="noopener noreferrer">Travis CI</a> de Build Stages et de Jobs.</p> <p>La documentation des <a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">Pipelines GitLab CI</a> est très bien faite et il existe aussi de nombreuses <strong><a href="https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates" target="_blank" rel="noopener noreferrer"><em>templates</em></a> officielles prêtes à l’usage</strong>.</p> <blockquote> <p><span class="emoji">👮</span> Gardons à l’esprit, pour la suite de l’article, qu’un pipeline se doit d’être <strong>déterministe</strong> et aussi <strong>rapide</strong> que possible.</p> </blockquote> <p>La définition du pipeline se fait dans un fichier <code class="language-text">.gitlab-ci.yml</code> à la racine du projet. GitLab détecte automatiquement ce fichier. Dans mon projet (<em>mono-repo, fullstack</em>), j’ai défini les <em>stages</em> suivant :</p> <div class="gatsby-highlight" data-language="yml"><pre style="counter-reset: linenumber NaN" class="language-yml line-numbers"><code class="language-yml"><span class="token comment"># .gitlab-ci.yml</span> <span class="token key atrule">stages</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>test <span class="token punctuation">-</span> deployment</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>Et je vais présenter, ici, deux <em>jobs</em> de la <em>stage</em> <code class="language-text">build-and-test</code> : la construction de l’image docker de l’application <strong>frontend</strong> et celle de l’application <strong>backend</strong>. Ces <em>jobs</em> sont exécutés, <strong>à la racine du dépôt</strong>, à chaque fois qu’une modification du code est poussée dans ce dernier.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 328px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/1ad048621145fbc1302ab0be830ef1d0/e2074/pipeline-stages-and-jobs.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 62.05128205128205%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAAAAEDBAX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHs0bBIP//EABgQAAMBAQAAAAAAAAAAAAAAAAIDEgEg/9oACAEBAAEFAmDeAiS4/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGxAAAgEFAAAAAAAAAAAAAAAAAREAAhIgcZH/2gAIAQEABj8CTI0Y76+4/wD/xAAYEAEBAAMAAAAAAAAAAAAAAAABEQAQIf/aAAgBAQABPyHp+u2TghfGg6yaM//aAAwDAQACAAMAAAAQUw//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAbEAEBAQACAwAAAAAAAAAAAAABEQAhMUFhkf/aAAgBAQABPxASMR8As6ykbcKJ4EnOESmDEXj3rum//9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/1ad048621145fbc1302ab0be830ef1d0/798b8/pipeline-stages-and-jobs.webp 195w, /static/1ad048621145fbc1302ab0be830ef1d0/bcc52/pipeline-stages-and-jobs.webp 328w" sizes="(max-width: 328px) 100vw, 328px" type="image/webp" /> <source srcset="/static/1ad048621145fbc1302ab0be830ef1d0/0e421/pipeline-stages-and-jobs.jpg 195w, /static/1ad048621145fbc1302ab0be830ef1d0/e2074/pipeline-stages-and-jobs.jpg 328w" sizes="(max-width: 328px) 100vw, 328px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/1ad048621145fbc1302ab0be830ef1d0/e2074/pipeline-stages-and-jobs.jpg" alt="Pipeline" title="Pipeline" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> </p> <p style="text-align: center"><em>Quand toutes les stages sont exécutées avec succès, le commit est "passé".</em></p> <h4>Frontend</h4> <div class="gatsby-highlight" data-language="yml"><pre style="counter-reset: linenumber 5" class="language-yml line-numbers"><code class="language-yml"><span class="token comment"># .gitlab-ci.yml</span> <span class="token comment"># [...]</span> 🏠 Frontend <span class="token punctuation">-</span> Build and tests<span class="token punctuation">:</span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>test <span class="token key atrule">image</span><span class="token punctuation">:</span> docker<span class="token punctuation">:</span>stable <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> docker build <span class="token punctuation">-</span>f app/Dockerfile <span class="token punctuation">-</span>t frontend<span class="token punctuation">-</span>build .</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le <em>job</em> nommé <code class="language-text">🏠 Frontend - Build and tests</code>, associé à la <code class="language-text">stage</code> ”<code class="language-text">build-and-test</code>”, utilise l’ <code class="language-text">image</code> docker <code class="language-text">docker:stable</code> pour exécuter son <code class="language-text">script</code> qui consiste en un <code class="language-text">docker build</code>. Le Dockerfile <code class="language-text">app/Dockerfile</code> va :</p> <ul> <li>Installer les modules NPM (<code class="language-text">yarn install</code>)</li> <li>Valider la bonne compilation de l’ensemble de la <em>codebase</em> TypeScript (<code class="language-text">yarn tsc</code>)</li> <li>S’assurer du bon <em>linting</em> du code (<code class="language-text">yarn lint</code>)</li> <li>Vérifier que le projet puisse être construit (<code class="language-text">yarn build</code>)</li> <li>Exécuter les tests unitaires et end-to-end (Cypress) (<code class="language-text">yarn test</code> et <code class="language-text">yarn e2e</code>)</li> </ul> <div class="gatsby-highlight has-highlighted-lines" data-language="docker"><pre style="counter-reset: linenumber NaN" class="language-docker line-numbers"><code class="language-docker"><span class="token comment"># ./app/Dockerfile</span> <span class="token comment"># Image cypress : node, npm, yarn + cypress et ses dépendances</span> <span class="token keyword">FROM</span> cypress/base<span class="token punctuation">:</span>14.15.0 <span class="token keyword">WORKDIR</span> /app <span class="token keyword">ENV</span> NODE_ENV=<span class="token string">'development'</span> <span class="token keyword">ENV</span> TZ=<span class="token string">'UTC'</span> <span class="token keyword">ENV</span> CI=true <span class="token comment"># dans ce projet fullstack, le répertoire ./app contient le frontend</span> <span class="token keyword">ARG</span> FRONTEND_DIR=<span class="token string">'/app'</span> <span class="token comment"># le frontend et le backend sont des yarn workspaces: chacun son package.json</span> <span class="token keyword">COPY</span> $<span class="token punctuation">{</span>FRONTEND_DIR<span class="token punctuation">}</span>/package.json /app/package.json <span class="token keyword">COPY</span> yarn.lock /app/yarn.lock <span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>frozen<span class="token punctuation">-</span>lockfile <span class="token punctuation">-</span><span class="token punctuation">-</span>prefer<span class="token punctuation">-</span>offline <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>progress <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>emoji</span> <span class="token keyword">ADD</span> $<span class="token punctuation">{</span>FRONTEND_DIR<span class="token punctuation">}</span>/. /app <span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn tsc<span class="token punctuation">:</span>all</span><span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn lint</span><span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn build</span><span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn test</span><span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn cypress install</span><span class="gatsby-highlight-code-line"><span class="token keyword">RUN</span> yarn e2e<span class="token punctuation">:</span>ci <span class="token comment"># start-server-and-test start http://localhost:8000 cypress run</span></span> <span class="token keyword">CMD</span> yarn start</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Si l’une de ces tâches échoue, la commande <code class="language-text">docker build</code> échoue, entraînant l’échec du <em>job</em>.</p> <p><strong>Il est évidemment possible d’exécuter ces commandes <code class="language-text">yarn</code> directement dans le <code class="language-text">script</code> du <em>job</em></strong> :</p> <div class="gatsby-highlight has-highlighted-lines" data-language="yml"><pre style="counter-reset: linenumber 5" class="language-yml line-numbers"><code class="language-yml"><span class="token comment"># .gitlab-ci.yml</span> <span class="token comment"># [...]</span> 🏠 Frontend <span class="token punctuation">-</span> Build and tests<span class="token punctuation">:</span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>test <span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>14.15.0<span class="token punctuation">-</span>stretch<span class="token punctuation">-</span>slim <span class="token comment"># on utilise une image node directement !</span> <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="gatsby-highlight-code-line"> <span class="token punctuation">-</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>frozen<span class="token punctuation">-</span>lockfile <span class="token punctuation">-</span><span class="token punctuation">-</span>prefer<span class="token punctuation">-</span>offline <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>progress <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>emoji</span><span class="gatsby-highlight-code-line"> <span class="token punctuation">-</span> yarn tsc<span class="token punctuation">:</span>all</span><span class="gatsby-highlight-code-line"> <span class="token punctuation">-</span> yarn lint</span> <span class="token comment"># ...</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>L’avantage d’exécuter ces tâches en construisant une image Docker est de profiter de son <a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache" target="_blank" rel="noopener noreferrer">build cache</a> incrémental <span class="emoji">🐋</span>, quand le pipeline est exécuté systématiquement sur la même machine. Ainsi, à la prochaine exécution de <code class="language-text">docker build</code>, <code class="language-text">yarn install</code> ne sera pas ré-exécuté si <code class="language-text">package.json</code> n’a pas été modifié. Même principe pour <code class="language-text">lint</code>, <code class="language-text">build</code>, <code class="language-text">test</code> : pas exécutés si le code à copier dans <code class="language-text">./app</code> n’a pas été modifié. </p> <blockquote> <p><span class="emoji">💡</span> Pour davantage d’explications, consultez <a href="https://www.docker.com/blog/speed-up-your-development-flow-with-these-dockerfile-best-practices/" target="_blank" rel="noopener noreferrer">cet article</a> 🐳.<br /> Et, Oui, j’aurais pu utiliser un build <a href="https://docs.docker.com/develop/develop-images/multistage-build/" target="_blank" rel="noopener noreferrer">multi-stages</a> mais ce n’est pas le sujet de l’article !</p> </blockquote> <img src="https://media.giphy.com/media/l0HlBTpop4XgVOjZK/giphy.gif" alt="pipeline" style="width: 350px; margin: 20px auto; display: block;" /> <h4>Backend</h4> <div class="gatsby-highlight" data-language="yml"><pre style="counter-reset: linenumber 14" class="language-yml line-numbers"><code class="language-yml"><span class="token comment"># .gitlab-ci.yml</span> <span class="token comment"># [...]</span> 🏗 Backend <span class="token punctuation">-</span> Build and tests<span class="token punctuation">:</span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>test <span class="token key atrule">image</span><span class="token punctuation">:</span> docker/compose<span class="token punctuation">:</span>latest <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> docker build <span class="token punctuation">-</span>f backend/Dockerfile <span class="token punctuation">-</span>t backend<span class="token punctuation">-</span>build . <span class="token punctuation">-</span> docker<span class="token punctuation">-</span>compose <span class="token punctuation">-</span>f backend/ci/docker<span class="token punctuation">-</span>compose.yml up <span class="token punctuation">-</span><span class="token punctuation">-</span>force<span class="token punctuation">-</span>recreate <span class="token punctuation">-</span><span class="token punctuation">-</span>exit<span class="token punctuation">-</span>code<span class="token punctuation">-</span>from backend</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le <em>job</em> construit une image Docker à partir du Dockerfile <code class="language-text">backend/Dockerfile</code>, à la manière du projet frontend (<code class="language-text">install</code>, <code class="language-text">lint</code>, <code class="language-text">build</code>, <code class="language-text">test</code>…). </p> <p>Si l’image est construite, alors <strong><code class="language-text">docker-compose</code></strong> est utilisé pour lancer deux containers : un pour lancer les tests d’intégration et <em>end-to-end</em> de l’application <strong>backend</strong>, un autre pour l’émulateur de la base de données (<a href="https://firebase.google.com/docs/firestore" target="_blank" rel="noopener noreferrer">Firestore</a>). </p> <div class="gatsby-highlight has-highlighted-lines" data-language="yaml"><pre style="counter-reset: linenumber NaN" class="language-yaml line-numbers"><code class="language-yaml"><span class="token comment"># backend/ci/docker-compose.yml</span> <span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'3.2'</span> <span class="token key atrule">services</span><span class="token punctuation">:</span> <span class="gatsby-highlight-code-line"> <span class="token key atrule">backend</span><span class="token punctuation">:</span></span><span class="gatsby-highlight-code-line"> <span class="token key atrule">image</span><span class="token punctuation">:</span> backend<span class="token punctuation">-</span>build</span> <span class="token key atrule">environment</span><span class="token punctuation">:</span> <span class="token key atrule">FIRESTORE_EMULATOR_HOST</span><span class="token punctuation">:</span> <span class="token string">'firestore-emulator:8081'</span> <span class="token key atrule">FIREBASE_PROJECT_ID</span><span class="token punctuation">:</span> <span class="token string">'firestore-testing'</span> <span class="token key atrule">CI</span><span class="token punctuation">:</span> <span class="token string">'true'</span> <span class="token key atrule">depends_on</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> firestore<span class="token punctuation">-</span>emulator <span class="token key atrule">command</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> bash <span class="token punctuation">-</span> <span class="token punctuation">-</span>c <span class="token punctuation">-</span> <span class="token punctuation">|</span><span class="token scalar string"> for ((i=0;i<30;++i)); do curl -s firestore-emulator:8081 && break || (echo "Waiting for firestore emulator..." && sleep 1); done; <span class="gatsby-highlight-code-line"> yarn integration && yarn e2e;</span></span> <span class="token key atrule">firestore-emulator</span><span class="token punctuation">:</span> <span class="token comment"># using "alpine" as it is ~600MB smaller (but JDK 8+ is still required)</span> <span class="token key atrule">image</span><span class="token punctuation">:</span> google/cloud<span class="token punctuation">-</span>sdk<span class="token punctuation">:</span>316.0.0<span class="token punctuation">-</span>alpine <span class="token key atrule">expose</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token number">8081</span> <span class="token key atrule">command</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> bash <span class="token punctuation">-</span> <span class="token punctuation">-</span>c <span class="token punctuation">-</span> <span class="token punctuation">|</span><span class="token scalar string"> apk --update add openjdk8-jre gcloud --quiet beta emulators firestore start --project=firestore-testing --host-port 0.0.0.0:8081</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>En passant <code class="language-text">--exit-code-from backend</code> à <code class="language-text">docker-compose</code>, le <em>job</em> réussit seulement si la commande du service <code class="language-text">backend</code> (<code class="language-text">yarn integration && yarn e2e</code>) réussit.</p> <p>Kudos à mon ex CTO <a href="https://twitter.com/digitalumberjak" target="_blank" rel="noopener noreferrer">@DigitalLumberjack</a> pour cette technique <span class="emoji">💗</span>.</p> <blockquote> <p><span class="emoji">💡</span> Plutôt que d’utiliser Docker Compose afin de lancer un deuxième container pour la base de données, il est aussi possible d’utiliser les <a href="https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service" target="_blank" rel="noopener noreferrer"><strong>Services</strong></a> de GitLab CI. Cela demande moins de travail, mais ce n’est pas exécutable sur votre machine, contrairement à Docker Compose.<br /> Dans le cas de <code class="language-text">Firestore</code>, il n’existe aucun service dédié, donc pas le choix : on télécharge l’image de Google Cloud et on lance l’émulateur à la main.</p> </blockquote> <img src="https://media.giphy.com/media/NygxobfPsIHNS/giphy.gif" alt="docker-compose" style="width: 350px; margin: 20px auto; display: block;" /> <h4>Et c’est tout ?</h4> <p>C’est un bon début ! Bien sûr, on peut aller beaucoup plus loin, en intégration comme en déploiement. Pour en apprendre davantage sur les possibilités de GitLab CI, je recommande (<strong>entre autres</strong>) :</p> <ul> <li>Les <em>jobs</em> conditionnels, avec <a href="https://docs.gitlab.com/ee/ci/yaml/#rules" target="_blank" rel="noopener noreferrer"><code class="language-text">rules</code></a>, qui remplace <a href="https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-basic" target="_blank" rel="noopener noreferrer"><code class="language-text">only/expect</code></a> : pratique, par exemple, pour déployer automatiquement l’application sur un environnement <a href="https://en.wikipedia.org/wiki/Deployment_environment#Staging" target="_blank" rel="noopener noreferrer">Staging</a> quand le pipeline est exécuté sur la branche <code class="language-text">master</code>.</li> <li> <p><a href="https://docs.gitlab.com/ee/ci/yaml/#when" target="_blank" rel="noopener noreferrer"><code class="language-text">when</code></a>, utilisable avec ou sans <code class="language-text">rules</code>, permet avec la valeur <code class="language-text">manual</code> d’ajouter des <em>jobs</em> à lancer manuellement (e.g. “Déployer en production”), mais aussi des <em>jobs</em> automatiquement lancés suite à un échec du pipeline (<code class="language-text">on_failure</code>), etc. <span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 257px;"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 58.97435897435898%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3SlB/8QAFhAAAwAAAAAAAAAAAAAAAAAAAAEg/9oACAEBAAEFAhT/AP/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABsQAAEEAwAAAAAAAAAAAAAAACEAARARUWGB/9oACAEBAAE/ITtAD2KbCpo//9oADAMBAAIAAwAAABDgD//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABoQAQADAAMAAAAAAAAAAAAAAAEAESExQWH/2gAIAQEAAT8QGqI9rKluze3A+TnxrcAbAEgZP//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/f57eb19e56940bec25e81b062674f84f/798b8/manual-job.webp 195w, /static/f57eb19e56940bec25e81b062674f84f/5dc06/manual-job.webp 257w" sizes="(max-width: 257px) 100vw, 257px" type="image/webp" /> <source srcset="/static/f57eb19e56940bec25e81b062674f84f/0e421/manual-job.jpg 195w, /static/f57eb19e56940bec25e81b062674f84f/89bfd/manual-job.jpg 257w" sizes="(max-width: 257px) 100vw, 257px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/f57eb19e56940bec25e81b062674f84f/89bfd/manual-job.jpg" alt="Job manuel" title="Job manuel" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </span> </p> <p style="text-align: center"><em>Job manuel : déployer en production ("master" seulement).</em></p> </li> </ul> <div class="gatsby-highlight" data-language="yml"><pre style="counter-reset: linenumber 20" class="language-yml line-numbers"><code class="language-yml">🚀 Frontend <span class="token punctuation">-</span> Deploy production<span class="token punctuation">:</span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> production <span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>14.15.0<span class="token punctuation">-</span>stretch<span class="token punctuation">-</span>slim <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>frozen<span class="token punctuation">-</span>lockfile <span class="token punctuation">-</span> echo "$<span class="token punctuation">{</span>FIREBASE_PKEY_FILE_JSON<span class="token punctuation">}</span>" <span class="token punctuation">|</span> base64 <span class="token punctuation">-</span>d <span class="token punctuation">></span> ./firebase<span class="token punctuation">-</span>credentials.json <span class="token punctuation">-</span> yarn firebase deploy <span class="token punctuation">-</span><span class="token punctuation">-</span>token "$<span class="token punctuation">{</span>FIREBASE_TOKEN<span class="token punctuation">}</span>" <span class="token key atrule">rules</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> <span class="token string">'$CI_COMMIT_BRANCH == "master"'</span> <span class="token comment"># seulement possible avec "master"</span> <span class="token key atrule">when</span><span class="token punctuation">:</span> manual</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <ul> <li> <p>Les <a href="https://docs.gitlab.com/ee/ci/yaml/#artifacts" target="_blank" rel="noopener noreferrer"><code class="language-text">artifacts</code></a>, fichiers conservés après l’exécution d’un <em>job</em> et téléchargeable par l’interface de GitLab. Utiles pour extraire les logs d’un test de performances, des captures d’écran et vidéos de tests Cypress, des rapports/extraits, etc. <span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 281px;"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 68.20512820512819%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAQAF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB2lRqP//EABYQAAMAAAAAAAAAAAAAAAAAAAAgIf/aAAgBAQABBQIq/wD/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAg/9oACAEBAAY/Al//xAAaEAACAwEBAAAAAAAAAAAAAAABEQAQMSFB/9oACAEBAAE/IUXkdGXgg95X/9oADAMBAAIAAwAAABBgz//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABwQAQEAAgMBAQAAAAAAAAAAAAERADEhQWFxof/aAAgBAQABPxCNi37grJOOzCwrXOR8t7/MIggPO8ACGf/Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/79cc8e5af0a2922e9193ca43cdc7c7c4/798b8/job-artifacts.webp 195w, /static/79cc8e5af0a2922e9193ca43cdc7c7c4/ad69b/job-artifacts.webp 281w" sizes="(max-width: 281px) 100vw, 281px" type="image/webp" /> <source srcset="/static/79cc8e5af0a2922e9193ca43cdc7c7c4/0e421/job-artifacts.jpg 195w, /static/79cc8e5af0a2922e9193ca43cdc7c7c4/d6ee7/job-artifacts.jpg 281w" sizes="(max-width: 281px) 100vw, 281px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/79cc8e5af0a2922e9193ca43cdc7c7c4/d6ee7/job-artifacts.jpg" alt="Artifacts" title="Artifacts" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </span> </p> <p style="text-align: center"><em>Téléchargeables sous forme d'archive, ou à parcourir dans l'explorateur de GitLab Web</em></p> </li> <li> <p><a href="https://docs.gitlab.com/ee/ci/yaml/#parallel" target="_blank" rel="noopener noreferrer"><code class="language-text">parallel</code></a> permettant plusieurs exécutions simultanées d’un même <em>job</em>. <span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 247px;"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 120.51282051282051%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAYABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAECAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB91FjNkOiQoD/xAAZEAEBAAMBAAAAAAAAAAAAAAABAAIQETH/2gAIAQEAAQUCgJz5os/dJf/EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8BH//EABYQAAMAAAAAAAAAAAAAAAAAABAg4f/aAAgBAQAGPwIxv//EAB0QAAIBBAMAAAAAAAAAAAAAAAABESExcbGBkfD/2gAIAQEAAT8hzsQsTYgYsNY+exeqIbqj/9oADAMBAAIAAwAAABBwwAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAeEAEAAgMAAgMAAAAAAAAAAAABABEhMUFRcYHB8P/aAAgBAQABPxBVwDClyqvbbm4w5MXufDb5T8VU0fSusDw25LSxK92g5da9z//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/5820db269e923436c13066df5a3526f9/798b8/stage-with-parallel-jobs.webp 195w, /static/5820db269e923436c13066df5a3526f9/b9f6d/stage-with-parallel-jobs.webp 247w" sizes="(max-width: 247px) 100vw, 247px" type="image/webp" /> <source srcset="/static/5820db269e923436c13066df5a3526f9/0e421/stage-with-parallel-jobs.jpg 195w, /static/5820db269e923436c13066df5a3526f9/9d1a4/stage-with-parallel-jobs.jpg 247w" sizes="(max-width: 247px) 100vw, 247px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/5820db269e923436c13066df5a3526f9/9d1a4/stage-with-parallel-jobs.jpg" alt="Jobs simultanés" title="Jobs simultanés" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </span> </p> <p style="text-align: center"><em>Les tests E2E d'un autre projet, répartis dans 4 instances d'un même job.</em></p> </li> <li>L’injection de <a href="https://docs.gitlab.com/ee/ci/variables/" target="_blank" rel="noopener noreferrer">variables d’environnement</a> dans GitLab CI, rendues accessibles dans <code class="language-text">.gitlab-ci.yml</code>.</li> <li>La directive <a href="https://docs.gitlab.com/ee/ci/caching/#caching-nodejs-dependencies" target="_blank" rel="noopener noreferrer"><code class="language-text">cache</code></a> qui permet de transférer des fichiers d’un <em>job</em> à un autre. Utile si vous ne pouvez/voulez pas profiter du cache de build incrémental en lançant vos <em>jobs</em> dans des <code class="language-text">docker build</code>.</li> <li>En quête de performances pour les machines virtuelles servant de <em>runners</em>, GitLab CI permet de configurer un <a href="https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching" target="_blank" rel="noopener noreferrer">cache distribué</a> (<a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section" target="_blank" rel="noopener noreferrer">AWS S3</a>, <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscachegcs-section" target="_blank" rel="noopener noreferrer">Cloud Storage</a>, Azure…).</li> </ul> <h3>🏃 Dessine-moi un runner</h3> <p>Utiliser une machine dédiée peut mener à la <strong>réduction du temps d’exécution</strong> des pipelines : meilleur <em>hardware</em> <span class="emoji">🚀</span>, réutilisation du cache Docker <span class="emoji">🐳</span> (<code class="language-text">docker build</code> plus rapide) et du cache <code class="language-text">npm</code>/<code class="language-text">yarn</code> <span class="emoji">📦</span> (les <em>packages</em> déjà en cache ne sont pas téléchargés à nouveau).<br /> Accessoirement, en n’utilisant pas les <em>shared runners</em> de GitLab, on peut aussi :</p> <ul> <li>Éviter l’envoi de fichiers/codes sensibles sur des machines qu’on ne possède pas</li> <li>Utiliser la CLI Xcode si disponible sur la machine hôte (macOS)</li> <li>S’affranchir des limitations (commerciales) sur les <em>shared runners</em></li> </ul> <p><em>Si vous souhaitez déployer votre propre flotte de runners (kubernetes, cloud autoscaling, etc), il existe des <a href="https://docs.gitlab.com/runner/configuration/autoscale.html" target="_blank" rel="noopener noreferrer">exemples</a> officiels. Pour ce projet personnel, j’ai utilisé un seul serveur dédié.</em></p> <p>Il suffit d’<a href="https://docs.gitlab.com/runner/install/" target="_blank" rel="noopener noreferrer">installer</a> l’utilitaire <a href="https://docs.gitlab.com/runner/" target="_blank" rel="noopener noreferrer">GitLab Runner</a> sur la machine en question (avec <code class="language-text">docker</code> pour <em>executor</em>) et d’<a href="https://docs.gitlab.com/runner/register/" target="_blank" rel="noopener noreferrer">enregistrer</a> le <em>runner</em> dans votre instance GitLab. Les <em>stages</em> du pipeline seront directement exécutés sur la machine enregistrée. </p> <blockquote> <p><span class="emoji">💰</span> Pour de meilleures performances si votre budget est serré, dans le cas d’un serveur dédié, favorisez (en priorité) un <strong>SSD</strong> plutôt qu’un disque dur mécanique. Le reste (CPU, RAM) dépendra de ce que vous ferez/lancerez dans vos pipelines. Il vous appartient d’étudier toutes les possibilités en terme de budget (votre machine de travail, un serveur dédié, une flotte de machines virtuelles <a href="https://aws.amazon.com/ec2/spot" target="_blank" rel="noopener noreferrer">spot</a>/<a href="https://cloud.google.com/compute/docs/instances/preemptible" target="_blank" rel="noopener noreferrer">préemptives</a>…).</p> </blockquote> <h4>Une note sur Cypress</h4> <p>Si vous rencontrez des problèmes de mémoire (<code class="language-text">Out of memory</code>) lorsque vous exécutez vos tests e2e utilisant Cypress, c’est peut-être dû à une <em>shared memory</em> (shm) insuffisante, Docker lui allouant 64MB par défaut. Si vous utilisez votre propre <em>runner</em>, vous pouvez augmenter celle-ci en conséquence, en passant par exemple <strong><code class="language-text">--shm-size=1G</code></strong> à Docker (dans mon cas, directement à <code class="language-text">docker build</code>) et en ajoutant la même configuration à <code class="language-text">/etc/gitlab-runner/config.toml</code> avec le paramètre <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section" target="_blank" rel="noopener noreferrer">shm_size</a> :</p> <div class="gatsby-highlight" data-language="toml"><pre style="counter-reset: linenumber NaN" class="language-toml line-numbers"><code class="language-toml"><span class="token comment"># [...]</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">runners</span><span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token comment"># [...]</span> <span class="token punctuation">[</span><span class="token table class-name">runners.docker</span><span class="token punctuation">]</span> <span class="token comment"># [...]</span> <span class="token key property">shm_size</span> <span class="token punctuation">=</span> <span class="token number">1073741824</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <h2>Et avec un projet déjà existant ?</h2> <p><strong>Démarrez petit</strong> ! On peut utiliser une image <code class="language-text">node</code> officielle et directement exécuter <code class="language-text">yarn build</code>, <code class="language-text">yarn test</code>, etc, dans le <code class="language-text">script</code> d’un unique <em>job</em> :</p> <div class="gatsby-highlight" data-language="yml"><pre style="counter-reset: linenumber NaN" class="language-yml line-numbers"><code class="language-yml"><span class="token comment"># .gitlab-ci.yml</span> <span class="token key atrule">stages</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>test build backend<span class="token punctuation">:</span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> build<span class="token punctuation">-</span>and<span class="token punctuation">-</span>test <span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>14.15.0<span class="token punctuation">-</span>stretch<span class="token punctuation">-</span>slim <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>frozen<span class="token punctuation">-</span>lockfile <span class="token punctuation">-</span><span class="token punctuation">-</span>prefer<span class="token punctuation">-</span>offline <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>progress <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>emoji <span class="token punctuation">-</span> yarn lint <span class="token punctuation">-</span> yarn build <span class="token punctuation">-</span> yarn test</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Ces quelques lignes suffisent à lancer un pipeline déjà très utile. Dans mon projet “starter” TypeScript, Gatsby, Material-UI (<a href="https://github.com/VinceOPS/gatsby-gojob-starter" target="_blank" rel="noopener noreferrer">gatsby-gojob-starter</a>), j’ai fourni deux <strong>bases</strong> de pipeline, une <a href="https://github.com/VinceOPS/gatsby-gojob-starter/blob/master/.gitlab-ci.yml" target="_blank" rel="noopener noreferrer">pour CircleCI</a>, une autre <a href="https://github.com/VinceOPS/gatsby-gojob-starter/blob/master/.gitlab-ci.yml" target="_blank" rel="noopener noreferrer">pour GitLab CI</a>. Loin d’être des modèles de perfection, elles permettent cependant de se lancer <span class="emoji">😬👌</span>.</p> <h2>CD, “c’est plus qu’un Job”</h2> <img src="https://media.giphy.com/media/1xnQ0TsDFcrFLdlJUC/giphy.gif" alt="décollage" style="width: 350px; margin: 20px auto; display: block;" /> <p>Concernant le <a href="https://docs.gitlab.com/ee/ci/introduction/#continuous-delivery" target="_blank" rel="noopener noreferrer">Continuous Delivery</a> et le <a href="https://docs.gitlab.com/ee/ci/introduction/#continuous-deployment" target="_blank" rel="noopener noreferrer">Continuous Deployment</a>, un nouvel article serait de rigueur. Dans le cadre de mon projet personnel, j’ai un environnement de production en <strong>continuous delivery</strong> : la nouvelle version de <code class="language-text">master</code> est déployée par une action manuelle. Le <em>job</em> ressemble à celui-ci :</p> <div class="gatsby-highlight has-highlighted-lines" data-language="yml"><pre style="counter-reset: linenumber 29" class="language-yml line-numbers"><code class="language-yml"><span class="gatsby-highlight-code-line">🚀 Frontend <span class="token punctuation">-</span> Deploy production<span class="token punctuation">:</span></span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> deploy <span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>14.15.0<span class="token punctuation">-</span>stretch<span class="token punctuation">-</span>slim <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>frozen<span class="token punctuation">-</span>lockfile <span class="token punctuation">-</span> echo "$<span class="token punctuation">{</span>FIREBASE_PKEY_FILE_JSON<span class="token punctuation">}</span>" <span class="token punctuation">|</span> base64 <span class="token punctuation">-</span>d <span class="token punctuation">></span> ./firebase<span class="token punctuation">-</span>credentials.json <span class="gatsby-highlight-code-line"> <span class="token punctuation">-</span> yarn firebase deploy <span class="token punctuation">-</span><span class="token punctuation">-</span>token "$<span class="token punctuation">{</span>FIREBASE_TOKEN<span class="token punctuation">}</span>"</span> <span class="token key atrule">rules</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> <span class="token string">'$CI_COMMIT_BRANCH == "master"'</span> <span class="token key atrule">when</span><span class="token punctuation">:</span> manual</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Pour l’environnement de <em>staging</em>, j’utilise une version légèrement modifié de ce <em>job</em> : il est déclenché automatiquement quand une branche est fusionnée à <code class="language-text">master</code> et vise <code class="language-text">staging</code> comme <em>hosting target</em> pour déployer l’application vers un domaine Firebase spécifique. On parle ici de <strong>continuous deployment</strong>.</p> <div class="gatsby-highlight has-highlighted-lines" data-language="yml"><pre style="counter-reset: linenumber 40" class="language-yml line-numbers"><code class="language-yml"><span class="gatsby-highlight-code-line">🚀 Frontend <span class="token punctuation">-</span> Deploy staging<span class="token punctuation">:</span></span> <span class="token key atrule">stage</span><span class="token punctuation">:</span> deploy <span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>14.15.0<span class="token punctuation">-</span>stretch<span class="token punctuation">-</span>slim <span class="token key atrule">script</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>frozen<span class="token punctuation">-</span>lockfile <span class="token punctuation">-</span> echo "$<span class="token punctuation">{</span>FIREBASE_PKEY_FILE_JSON<span class="token punctuation">}</span>" <span class="token punctuation">|</span> base64 <span class="token punctuation">-</span>d <span class="token punctuation">></span> ./firebase<span class="token punctuation">-</span>credentials.json <span class="gatsby-highlight-code-line"> <span class="token punctuation">-</span> yarn firebase deploy <span class="token punctuation">-</span><span class="token punctuation">-</span>token "$<span class="token punctuation">{</span>FIREBASE_TOKEN<span class="token punctuation">}</span>" <span class="token punctuation">-</span><span class="token punctuation">-</span>only hosting<span class="token punctuation">:</span>staging</span> <span class="token key atrule">rules</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> <span class="token string">'$CI_COMMIT_BRANCH == "master"'</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><em>Il est possible de “refactorer” du YAML pour éviter les copy-paste massifs… À vous de jouer 😬.</em></p> <p>La technique employée ici est spécifique à Firebase (<code class="language-text">firebase deploy</code>). Il faut adapter le job aux contraintes <strong>techniques</strong> (Heroku ? Helm/Kubernetes ? Gestion des environnements…) et <strong>professionnelles</strong> (Sécurité ? Droits d’accès ? Politique de mise à jour…) de chaque entreprise.</p> <p>Happy CI/CDing!</p></div>"Refactoring" de Martin Fowlerhttps://vinceops.me/refactoring-martin-fowler/2020-05-02T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/cec3ebcaebf2f63ebdb606393d4c5bf8/7293b/refactoring-cover.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 33.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUE/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAHXFCkJv//EABgQAAMBAQAAAAAAAAAAAAAAAAECBBIh/9oACAEBAAEFAknfXdisqP/EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/AVf/xAAVEQEBAAAAAAAAAAAAAAAAAAAQMf/aAAgBAgEBPwGn/8QAGxAAAgIDAQAAAAAAAAAAAAAAAREAAhIyQWH/2gAIAQEABj8CsScWORWJbi29M//EABgQAQEBAQEAAAAAAAAAAAAAAAEhABFB/9oACAEBAAE/IQAAhVzxCOF7jZZ8Lv/aAAwDAQACAAMAAAAQ98//xAAYEQACAwAAAAAAAAAAAAAAAAAAASExcf/aAAgBAwEBPxBRRlH/xAAWEQEBAQAAAAAAAAAAAAAAAAABECH/2gAIAQIBAT8QV0z/xAAaEAEBAAIDAAAAAAAAAAAAAAABEQAhMXGh/9oACAEBAAE/EB4TV4XjqY0jO3ETGAwmn4c//9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/cec3ebcaebf2f63ebdb606393d4c5bf8/798b8/refactoring-cover.webp 195w, /static/cec3ebcaebf2f63ebdb606393d4c5bf8/0e356/refactoring-cover.webp 390w, /static/cec3ebcaebf2f63ebdb606393d4c5bf8/23bbb/refactoring-cover.webp 780w, /static/cec3ebcaebf2f63ebdb606393d4c5bf8/d0742/refactoring-cover.webp 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/cec3ebcaebf2f63ebdb606393d4c5bf8/0e421/refactoring-cover.jpg 195w, /static/cec3ebcaebf2f63ebdb606393d4c5bf8/ce2e0/refactoring-cover.jpg 390w, /static/cec3ebcaebf2f63ebdb606393d4c5bf8/485b7/refactoring-cover.jpg 780w, /static/cec3ebcaebf2f63ebdb606393d4c5bf8/7293b/refactoring-cover.jpg 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/cec3ebcaebf2f63ebdb606393d4c5bf8/485b7/refactoring-cover.jpg" alt="Refactoring: Improving the Design of Existing Code" title="Refactoring: Improving the Design of Existing Code" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Le blog entre en 2020 avec la présentation de <strong>Refactoring</strong>, de <a href="https://martinfowler.com/" target="_blank" rel="noopener noreferrer">Martin Fowler</a> 🚀 : la trame principale et les grandes idées. L’article est long, mais je pense qu’il apportera l’essentiel du livre à ceux qui n’ont pas le temps de le lire… Sans priver les autres de l’envie de le faire. Et je conseille à tous de le faire <span class="emoji">😃</span>.</p> <hr /> <p>Cette seconde édition du livre est une réécriture (partielle) pour JavaScript. Son but est de guider les développeurs dans l’amélioration de leur code, par petites itérations, sans en changer le comportement. C’est l’exacte définition du <em>refactoring</em> : <strong>changer la structure du code, sans changer son comportement</strong>…</p> <p>Et <strong>non</strong>, Refactoring ne signifie pas “réécrire un code moche”.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 300px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/06ad900d91082a0f6443571fc386a6ae/0cdb7/meme-watching-you.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 83.58974358974359%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAARABQDASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAAAAMEBQL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHfoz8EwJMwLoP/xAAbEAACAgMBAAAAAAAAAAAAAAABAgARAxAyEv/aAAgBAQABBQKIxl1KIIKA1603eLj/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AR//xAAaEAACAgMAAAAAAAAAAAAAAAAAEAERAmFx/9oACAEBAAY/Ai6Umy1l1f/EAB4QAQACAgEFAAAAAAAAAAAAAAEAESExYRBBcbHw/9oACAEBAAE/IXUaqsrW42UQNLZ2blEapiAFHHHT4OZr8vuf/9oADAMBAAIAAwAAABCwDzz/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAeEAEAAgIBBQAAAAAAAAAAAAABABEhURAxYXGR8P/aAAgBAQABPxBUnRFx36xgjIuPdRodKo17iRch85u9yqK3S1x4F8jaf//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/06ad900d91082a0f6443571fc386a6ae/798b8/meme-watching-you.webp 195w, /static/06ad900d91082a0f6443571fc386a6ae/77434/meme-watching-you.webp 300w" sizes="(max-width: 300px) 100vw, 300px" type="image/webp" /> <source srcset="/static/06ad900d91082a0f6443571fc386a6ae/0e421/meme-watching-you.jpg 195w, /static/06ad900d91082a0f6443571fc386a6ae/0cdb7/meme-watching-you.jpg 300w" sizes="(max-width: 300px) 100vw, 300px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/06ad900d91082a0f6443571fc386a6ae/0cdb7/meme-watching-you.jpg" alt="I'm watching you" title="I'm watching you" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><em>Pour ceux qui ne connaissaient pas <a href="https://en.wikipedia.org/wiki/Martin_Fowler_(software_engineer)" target="_blank" rel="noopener noreferrer">l’auteur</a>, je vous invite à lire son <a href="https://www.martinfowler.com/" target="_blank" rel="noopener noreferrer">excellent site</a> et à le suivre sur <a href="https://twitter.com/martinfowler" target="_blank" rel="noopener noreferrer">Twitter</a>.</em></p> <p>L’ouvrage présente un long catalogue des techniques de refactorings, consultable <a href="https://refactoring.com/catalog/" target="_blank" rel="noopener noreferrer">sur le site officiel</a>.<br /> J’en profite aussi pour partager <strong><a href="https://refactoring.guru/" target="_blank" rel="noopener noreferrer">Refactoring.Guru</a></strong> qui présente du contenu de qualité sur le refactoring et les design patterns, inspiré des livres comme : <a href="https://www.oreilly.com/library/view/refactoring-improving-the/9780134757681/" target="_blank" rel="noopener noreferrer">Refactoring</a>, <a href="https://www.oreilly.com/library/view/effective-java-3rd/9780134686097/" target="_blank" rel="noopener noreferrer">Effective Java</a>, <a href="https://www.oreilly.com/library/view/design-patterns-elements/0201633612/" target="_blank" rel="noopener noreferrer">Design Patterns (GoF)</a>, <a href="https://www.oreilly.com/library/view/head-first-design/0596007124/" target="_blank" rel="noopener noreferrer">Head First Design Patterns</a>…</p> <h2>Chapitre 1 - Un premier exemple de Refactoring</h2> <p>Le livre ouvre directement la marche avec un exemple. Le code de départ présente plusieurs imperfections (et <em><a href="https://martinfowler.com/bliki/CodeSmell.html" target="_blank" rel="noopener noreferrer">code smells</a></em>). Et ô surprise, une évolution est demandée par l’utilisateur <span class="emoji">😱</span>. Avant de se lancer, l’auteur effectue un rapide “état des lieux” (analyse critique).</p> <blockquote> <p><span class="emoji">💡</span> Le premier chapitre est consultable gratuitement et légalement <a href="https://files.thoughtworks.com/pdfs/Refactoring2-free-chapter.pdf" target="_blank" rel="noopener noreferrer">en ligne</a>. Vous pourrez y voir ledit code et toutes les étapes du refactoring, avec les explications de Martin Fowler.</p> </blockquote> <p>Si l’ajout d’une fonctionnalité est compliqué à cause de l’état du code, l’auteur recommande le refactoring <strong>avant</strong> tout nouveau développement. Et avant de procéder à la moindre modification, il faut s’assurer que le code soit couvert par des <strong>tests</strong>.<br /> S’ensuivent plusieurs techniques de refactorings du <a href="https://refactoring.com/catalog/" target="_blank" rel="noopener noreferrer">catalogue</a>, en exécutant continuellement les tests pour vérifier que le comportement du code n’ait pas été altéré. Les refactorings appliqués sont assez communs : <a href="https://refactoring.guru/extract-method" target="_blank" rel="noopener noreferrer">Extract Function (Method)</a>, <a href="https://refactoring.guru/inline-temp" target="_blank" rel="noopener noreferrer">Inline Variable</a>, <a href="https://refactoring.guru/move-method" target="_blank" rel="noopener noreferrer">Move Method</a>, etc. Petit à petit, le code servant d’exemple devient plus lisible et maintenable.</p> <h3>En résumé</h3> <ul> <li><strong>Pas de refactoring sans test</strong></li> <li> <p>Le refactoring :</p> <ul> <li>est effectué en <strong>petites étapes</strong>, précédées par une avancée dans la compréhension du code</li> <li>ne modifie pas le <strong>comportement</strong> du code : les modifications ne nécessitent pas de nouveaux tests</li> <li>n’a pas besoin d’être violent : même une petite <strong>amélioration</strong> est bonne à prendre, <em>Boy scout rule</em>, “Laissez le camp plus propre que vous ne l’avez trouvé”</li> </ul> </li> <li>Les tests existants sont exécutés <strong>continuellement</strong>, c’est notre filet de sécurité</li> <li>Un code a été “amélioré” lorsqu’il est plus <strong>simple à modifier</strong> : la <strong>productivité</strong> du développeur est augmentée car il peut ajouter de la <strong>valeur</strong> au logiciel plus rapidement… <em>“Code should be obvious”</em>.</li> </ul> <p>Au risque de me répéter… Des <strong>petites étapes</strong>, appuyées par les <strong>tests</strong>. À l’image des <em>baby steps</em> du TDD, elles permettent d’éviter de longues sessions de debugging. Refactorman n’est pas un héros <span class="emoji">😬</span>.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/51b6118e7fa35390aa132a6165e35c64/03ffe/refactor-man.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 102.05128205128204%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAQFAgP/xAAYAQACAwAAAAAAAAAAAAAAAAAAAgEDBP/aAAwDAQACEAMQAAAB3I55EnbZSlreTQnoA//EAB4QAAICAQUBAAAAAAAAAAAAAAEDAAIEERITFCFB/9oACAEBAAEFAvr28ZFtQcv27Ay67Db1Fw4i5XGoJ//EABkRAAEFAAAAAAAAAAAAAAAAAAACEBEhQf/aAAgBAwEBPwFMaU//xAAZEQACAwEAAAAAAAAAAAAAAAABAgAQEVH/2gAIAQIBAT8BYbAj9v8A/8QAHxAAAQMDBQAAAAAAAAAAAAAAAAECkRAhMRESMoGS/9oACAEBAAY/AhL6dU4G5yLJZF9GXSZdJl0n/8QAGhABAAMBAQEAAAAAAAAAAAAAAQARITFRgf/aAAgBAQABPyFwGbE1dc1cYEcZ8h9nzBcwqBF8tFm4hRAdL9j/2gAMAwEAAgADAAAAEE/Avv/EABgRAAIDAAAAAAAAAAAAAAAAAAERABAh/9oACAEDAQE/ECaCXy//xAAcEQABAwUAAAAAAAAAAAAAAAABABARIUGB8PH/2gAIAQIBAT8QpOWKAES3L//EAB0QAQACAgMBAQAAAAAAAAAAAAEAESExQVFh0XH/2gAIAQEAAT8QQhhpuEZeTSgHC8RAqFjuyBYStG5dMW5OqLBj8goto6DyOlcq6fIOVm8fIjq8tbOA68n/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/51b6118e7fa35390aa132a6165e35c64/798b8/refactor-man.webp 195w, /static/51b6118e7fa35390aa132a6165e35c64/0e356/refactor-man.webp 390w, /static/51b6118e7fa35390aa132a6165e35c64/23bbb/refactor-man.webp 780w, /static/51b6118e7fa35390aa132a6165e35c64/b47fd/refactor-man.webp 1170w, /static/51b6118e7fa35390aa132a6165e35c64/cbd37/refactor-man.webp 1200w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/51b6118e7fa35390aa132a6165e35c64/0e421/refactor-man.jpg 195w, /static/51b6118e7fa35390aa132a6165e35c64/ce2e0/refactor-man.jpg 390w, /static/51b6118e7fa35390aa132a6165e35c64/485b7/refactor-man.jpg 780w, /static/51b6118e7fa35390aa132a6165e35c64/faa76/refactor-man.jpg 1170w, /static/51b6118e7fa35390aa132a6165e35c64/03ffe/refactor-man.jpg 1200w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/51b6118e7fa35390aa132a6165e35c64/485b7/refactor-man.jpg" alt="Refactor-man" title="Refactor-man" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <h2>Chapitre 2 - Les fondements du Refactoring</h2> <p>Martin Fowler donne une <a href="https://martinfowler.com/bliki/DefinitionOfRefactoring.html" target="_blank" rel="noopener noreferrer">définition</a> précise de Refactoring, à la fois en tant que nom et en tant que verbe :</p> <blockquote> <p><strong>Refactoring</strong> (nom) : Changement de la structure d’un code pour faciliter sa compréhension et sa modification, sans changer son comportement observable.</p> </blockquote> <p>Le comportement <strong>observable</strong> est celui connu par l’utilisateur. Il n’est pas question du comportement <strong>technique</strong> (allocations mémoires, utilisation des cycles CPU, etc.).</p> <blockquote> <p><strong>Refactoring</strong> (verbe) : Restructurer du code en appliquant une série de refactorings.</p> </blockquote> <p>Donc, quand on refactor, on applique des refactorings. La prochaine fois qu’un collègue vous dira “ce code bug, je dois faire une refacto”, n’hésitez pas à prendre un air hautain et toxique pour lui expliquer qu’il dit n’importe quoi <span class="emoji">😏</span>.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 555px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/1fce44020d515bb661d27d19b1f3859e/f20da/meme-should-be-refactored.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 81.02564102564102%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAQABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAQFAQP/xAAWAQEBAQAAAAAAAAAAAAAAAAADAQL/2gAMAwEAAhADEAAAAZ9RBm67mCn/AP/EABkQAQEAAwEAAAAAAAAAAAAAAAECAAMREv/aAAgBAQABBQIqgl9Ty81g55ZNddj/xAAWEQADAAAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8BMX//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPwEn/8QAHBAAAgAHAAAAAAAAAAAAAAAAAAEQITEyYXGh/9oACAEBAAY/ApU0ZL+CUf/EABsQAQADAQADAAAAAAAAAAAAAAEAESFBMVFh/9oACAEBAAE/IfCmvSNrWLWp80ionWnsVUyjjKK5/9oADAMBAAIAAwAAABD0/wD/xAAYEQADAQEAAAAAAAAAAAAAAAAAAREhMf/aAAgBAwEBPxB4k0fT/8QAFhEBAQEAAAAAAAAAAAAAAAAAAREA/9oACAECAQE/EDGuIm//xAAcEAEAAwACAwAAAAAAAAAAAAABABEhcdExUfD/2gAIAQEAAT8QrdGgqbykJAwANL9so6ncdlQFPL1zDGqjYlQ04TGf/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/1fce44020d515bb661d27d19b1f3859e/798b8/meme-should-be-refactored.webp 195w, /static/1fce44020d515bb661d27d19b1f3859e/0e356/meme-should-be-refactored.webp 390w, /static/1fce44020d515bb661d27d19b1f3859e/fac4c/meme-should-be-refactored.webp 555w" sizes="(max-width: 555px) 100vw, 555px" type="image/webp" /> <source srcset="/static/1fce44020d515bb661d27d19b1f3859e/0e421/meme-should-be-refactored.jpg 195w, /static/1fce44020d515bb661d27d19b1f3859e/ce2e0/meme-should-be-refactored.jpg 390w, /static/1fce44020d515bb661d27d19b1f3859e/f20da/meme-should-be-refactored.jpg 555w" sizes="(max-width: 555px) 100vw, 555px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/1fce44020d515bb661d27d19b1f3859e/f20da/meme-should-be-refactored.jpg" alt="Mauvaise utilisation du terme Refactoring" title="Mauvaise utilisation du terme Refactoring" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>En effet, le terme est souvent mal employé (pour désigner une session de debug, voire la réécriture complète d’un code…). L’auteur a écrit un article à ce sujet : <a href="https://martinfowler.com/bliki/RefactoringMalapropism.html" target="_blank" rel="noopener noreferrer">Refactoring Malapropism</a>.</p> <h3>Le Refactoring : Pourquoi ?</h3> <ul> <li>Faciliter la compréhension du code (et par conséquent, la <em>recherche</em> de bugs)</li> <li>Améliorer la conception (design) du logiciel</li> <li><strong>Développer plus rapidement</strong></li> </ul> <p>Dans son article <a href="https://martinfowler.com/bliki/DesignStaminaHypothesis.html" target="_blank" rel="noopener noreferrer">Design Stamina Hypothesis</a>, Martin Fowler présente l’hypothèse du <strong>seuil de rentabilité</strong> du <em>design</em> (qualité, conception), alias <em>design payoff line</em>.</p> <p><img src="https://vinceops.me/7951ea8fb6d669260055ab07c624fb16/designStaminaGraph.gif" alt="design stamina" /></p> <p>Sous ce seuil, il est <em>possible</em> de gagner en productivité (livrer davantage de fonctionnalités) en négligeant la qualité (le design) du code. C’est ce qu’on appelle Acheter de la <strong>dette technique</strong>. Passé ce seuil, c’est l’inverse qui se produit : le développement est ralenti, les livraisons prennent du retard, la moindre modification devient coûteuse.</p> <p>Le refactoring permet, par l’amélioration continue du code, de maintenir un certain niveau de qualité <strong>et</strong> de productivité <span class="emoji">🤓</span>.</p> <h3>Le Refactoring : Quand ?</h3> <p>L’auteur affirme faire du refactoring <strong>en permanence</strong>, comme de nombreux développeurs qui y ont pris goût.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 666px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/25fe1e650706ee70c8c231c3297b0361/1f41c/meme-always-refactoring.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 56.41025641025641%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABAADBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAEPTGiA2dX/xAAcEAACAgIDAAAAAAAAAAAAAAABAwACEhMUISP/2gAIAQEAAQUCocCxtePsiuwAI0+n/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8BR//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/AVf/xAAbEAACAQUAAAAAAAAAAAAAAAAAAREQITEyQf/aAAgBAQAGPwJQStu0ujCGf//EABwQAQACAQUAAAAAAAAAAAAAAAEAIRFBUWGBsf/aAAgBAQABPyFULqWAKrDSZ7QhoLySn5RACif/2gAMAwEAAgADAAAAEIw//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8QQ//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/EFP/xAAaEAEAAwEBAQAAAAAAAAAAAAABABEhUTFB/9oACAEBAAE/EHyDe9EUTwWNT8qOWbrdfZrO2gsrLS+eMTWhwCp//9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/25fe1e650706ee70c8c231c3297b0361/798b8/meme-always-refactoring.webp 195w, /static/25fe1e650706ee70c8c231c3297b0361/0e356/meme-always-refactoring.webp 390w, /static/25fe1e650706ee70c8c231c3297b0361/4c012/meme-always-refactoring.webp 666w" sizes="(max-width: 666px) 100vw, 666px" type="image/webp" /> <source srcset="/static/25fe1e650706ee70c8c231c3297b0361/0e421/meme-always-refactoring.jpg 195w, /static/25fe1e650706ee70c8c231c3297b0361/ce2e0/meme-always-refactoring.jpg 390w, /static/25fe1e650706ee70c8c231c3297b0361/1f41c/meme-always-refactoring.jpg 666w" sizes="(max-width: 666px) 100vw, 666px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/25fe1e650706ee70c8c231c3297b0361/1f41c/meme-always-refactoring.jpg" alt="That's my secret, I'm always refactoring" title="That's my secret, I'm always refactoring" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Le meilleur moment est celui qui précède l’ajout d’une fonctionnalité, ou la correction d’un bug. C’est ce que Martin appelle le <strong>Refactoring préparatoire</strong>. Il s’agit d’améliorer l’état du code avant d’avoir à le modifier.</p> <p>L’auteur définit d’autres “catégories” de refactorings, comme :</p> <ul> <li>Compréhension : si le code a nécessité un effort intellectuel pour être compris, alors il est certainement possible de l’améliorer. Encore une fois, <em>“Code should be obvious”</em>. Des actions aussi simples que <a href="https://refactoring.com/catalog/renameVariable.html" target="_blank" rel="noopener noreferrer">Rename Variable</a> peuvent avoir un sérieux impact.</li> <li>Ramassage des déchets : quand on remarque que le code est mal conçu, ou inutilement complexe. Il n’a pas besoin d’être entièrement réécrit, mais on peut l’améliorer progressivement, sur la durée. “Laissez le camp plus propre que vous ne l’avez trouvé”.</li> </ul> <blockquote> <p><span class="emoji">💡</span> Si vous faites des <a href="https://vinceops.me/git-astuces-productivite-1/">conventional commits</a>, pensez aussi à isoler vos refactorings dans un commit <code class="language-text">refactor</code>. Vos collègues chargés de la revue de code apprécieront certainement.</p> </blockquote> <h3>Etc.</h3> <p>Le chapitre est long et dense. L’article n’a pas pour vocation de couvrir tous les éléments qui donnent sa valeur au livre. </p> <p>Néanmoins, puisque c’est un vice très répandu dans l’industrie, je partage l’une des notions abordées : la <a href="https://refactoring.guru/smells/speculative-generality" target="_blank" rel="noopener noreferrer">Speculative Generality</a>. On connait tous un dév qui écrit parfois 3 fois plus de code que nécessaire “juste au cas où”. Et il est même souvent “prêt à parier que ça va servir”.<br /> L’auteur avance l’idée qu’avec un refactoring continu, il est très facile de rendre le code plus flexible <strong>au besoin</strong>, plutôt que <strong>d’anticiper</strong> cette flexibilité (flexi-complexité…). C’est l’opposition entre le <em>Use-case driven development</em> et ce que j’appelle le <em>“Just in case” driven development</em>.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/7178309156d61f218932ce4a04aee7c3/d7c02/over-engineering.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 41.02564102564102%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB2aCgf//EABcQAQADAAAAAAAAAAAAAAAAAAAhIjH/2gAIAQEAAQUCsxL/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAWEAADAAAAAAAAAAAAAAAAAAABEDH/2gAIAQEABj8Cof8A/8QAGxAAAgIDAQAAAAAAAAAAAAAAAAERMSFhcYH/2gAIAQEAAT8hvsoltGF6J9H/2gAMAwEAAgADAAAAEHAP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGRABAQEBAQEAAAAAAAAAAAAAAREhADFB/9oACAEBAAE/EF3RcBfeUMDW4vSYR8Wd/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/7178309156d61f218932ce4a04aee7c3/798b8/over-engineering.webp 195w, /static/7178309156d61f218932ce4a04aee7c3/0e356/over-engineering.webp 390w, /static/7178309156d61f218932ce4a04aee7c3/23bbb/over-engineering.webp 780w, /static/7178309156d61f218932ce4a04aee7c3/1bf08/over-engineering.webp 823w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/7178309156d61f218932ce4a04aee7c3/0e421/over-engineering.jpg 195w, /static/7178309156d61f218932ce4a04aee7c3/ce2e0/over-engineering.jpg 390w, /static/7178309156d61f218932ce4a04aee7c3/485b7/over-engineering.jpg 780w, /static/7178309156d61f218932ce4a04aee7c3/d7c02/over-engineering.jpg 823w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/7178309156d61f218932ce4a04aee7c3/485b7/over-engineering.jpg" alt="Over engineering" title="Over engineering" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p style="text-align: center"><em>Over-engineering...</em></p> <p><em>Speculative Generality</em> est aussi abordé dans le chapitre 3 sur les <strong>code smells</strong>. Dans ce chapitre 2, Martin Fowler parle plus précisément de “Speculative flexibility”.</p> <h2> Chapitre 3 - Les Smells dans le code</h2> <p><strong>Une longue série de Code smells</strong>, voilà ce qu’est ce (court) chapitre coécrit avec Kent Beck, l’auteur de <a href="https://www.oreilly.com/library/view/test-driven-development/0321146530/" target="_blank" rel="noopener noreferrer">Test Driven Development: by Example</a> qui est aussi le créateur du “TDD”. </p> <p><a href="https://refactoring.guru/smells/large-class" target="_blank" rel="noopener noreferrer">Large class</a>, <a href="https://refactoring.guru/smells/divergent-change" target="_blank" rel="noopener noreferrer">Divergent Change</a>, <a href="https://refactoring.guru/smells/shotgun-surgery" target="_blank" rel="noopener noreferrer">Shotgun Surgery</a>, <a href="https://refactoring.guru/smells/duplicate-code" target="_blank" rel="noopener noreferrer">Duplicated code</a>… Retrouvez-les sur <a href="https://refactoring.guru/refactoring/smells" target="_blank" rel="noopener noreferrer">Refactoring.guru</a>.</p> <h3><em>Looks familiar…</em></h3> <p>Au sujet de <strong>Large Class</strong>, <strong>Divergent Change</strong> et <strong>Shotgun Surgery</strong>, je trouve inévitable le rapprochement avec le <a href="https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html" target="_blank" rel="noopener noreferrer">Single Responsibility Principle</a> (SRP) d’Uncle Bob, trop souvent mal interpété. En effet, celui-ci ne stipule pas qu’un module ne devrait <strong>faire qu’une seule chose</strong> mais qu’il ne devrait <strong>avoir qu’une seule raison de changer</strong>. </p> <blockquote> <p>“Regroupez les choses qui changent pour les mêmes raisons. Séparez celles qui changent pour des raisons différentes”.</p> </blockquote> <p>En d’autres termes, le SRP (<strong>S</strong> de <a href="https://en.wikipedia.org/wiki/SOLID" target="_blank" rel="noopener noreferrer">SOLID</a>) a pour objectif de <strong>limiter l’impact des changements effectués</strong> dans le code. On peut considérer que c’est ce que préconise aussi le livre, avec ces <em>smells</em>.</p> <h2> Chapitre 4 - Construire les tests</h2> <p>Martin Fowler explique la valeur des tests automatisés, leur nécessité dans le refactoring, et le temps qu’ils font économiser en “debugging” pour mieux le réinvestir dans la production de valeur ajoutée.</p> <p>Si vous êtes, comme moi, habitué et rompu au TDD, ce chapitre ne sera pas le plus passionnant de l’ouvrage. Il a cependant le mérite de présenter les fondamentaux des tests unitaires et de recommander le TDD <span class="emoji">🔥</span>.</p> <blockquote> <p>When you get a bug report, start by writing a unit test that exposes the bug.</p> </blockquote> <h2>Résumé</h2> <p>Puisque tous les chapitres restants présentent le catalogue <a href="https://refactoring.com/catalog/" target="_blank" rel="noopener noreferrer">disponible en ligne</a>, je passe directement au résumé <span class="emoji">🎉</span>.</p> <p>Au-delà des techniques de refactorings présentées, j’ai apprécié la lecture de cet ouvrage aussi <strong>sage</strong> que <strong>pragmatique</strong>. Martin Fowler offrait, déjà en 2000, une solution à certains “travers” de l’industrie que l’on subit encore en 2020. J’aurais d’ailleurs souhaité le lire il y a 10 ans, quand j’ai écrit mes premières lignes de code en tant que Développeur “professionnel”.</p> <p>Quand on démarre un projet, il est impératif d’écrire le minimum de code pour répondre au besoin exprimé, qui est souvent encore mal compris et mal connu. Plus un code est simple, moins il demande d’effort en refactoring… Refactoring qui facilite son évolution tout en contenant sa complexité (inexorablement amenée à croître). Ce qui implique une vélocité préservée pour les développeurs. Comme le dit l’auteur dans le chapitre 3 :</p> <blockquote> <p>After all, software is meant to be soft.</p> </blockquote> <p>Puisque le refactoring ne doit pas être effectué sans test, et puisqu’il doit être effectué par petites itérations, c’est un parfait associé du <strong>TDD</strong>. Mieux que ça, il fait partie intégrante de son mantra “Red, Green, Refactor”.</p> <p>Je conclurai en vous recommandant chaudement la lecture et en citant cette punchline (que j’adore) présente sur la couverture :</p> <blockquote> <p><strong>Any fool can write code that a computer can understand. Good programmers write code that humans can understand</strong>.</p> </blockquote></div>Nest Serverless : Firebase Functionshttps://vinceops.me/nest-serverless-firebase-functions/2019-12-05T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 650px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/08654a56ae6c80bce5365bf37d5a5af6/8ea4d/nest-in-firebase.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAIDBf/EABYBAQEBAAAAAAAAAAAAAAAAAAMBAv/aAAwDAQACEAMQAAAB0ZmpUEH/xAAYEAACAwAAAAAAAAAAAAAAAAAAAQIDEv/aAAgBAQABBQKtE2LR/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAEh/9oACAEDAQE/AVuv/8QAFREBAQAAAAAAAAAAAAAAAAAAEEH/2gAIAQIBAT8Bp//EABgQAAMBAQAAAAAAAAAAAAAAAAABETFB/9oACAEBAAY/AqmVYcP/xAAZEAACAwEAAAAAAAAAAAAAAAAAAREhQTH/2gAIAQEAAT8hXOSh7cEodLZYf//aAAwDAQACAAMAAAAQgD//xAAXEQEBAQEAAAAAAAAAAAAAAAABEQAh/9oACAEDAQE/EE5NJEN//8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERIf/aAAgBAgEBPxCUJYf/xAAZEAEBAAMBAAAAAAAAAAAAAAABEQAhMUH/2gAIAQEAAT8QPbao3vAQAd5X3KUqNUTWf//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/08654a56ae6c80bce5365bf37d5a5af6/798b8/nest-in-firebase.webp 195w, /static/08654a56ae6c80bce5365bf37d5a5af6/0e356/nest-in-firebase.webp 390w, /static/08654a56ae6c80bce5365bf37d5a5af6/309d5/nest-in-firebase.webp 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/webp" /> <source srcset="/static/08654a56ae6c80bce5365bf37d5a5af6/0e421/nest-in-firebase.jpg 195w, /static/08654a56ae6c80bce5365bf37d5a5af6/ce2e0/nest-in-firebase.jpg 390w, /static/08654a56ae6c80bce5365bf37d5a5af6/8ea4d/nest-in-firebase.jpg 650w" sizes="(max-width: 650px) 100vw, 650px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/08654a56ae6c80bce5365bf37d5a5af6/8ea4d/nest-in-firebase.jpg" alt="Your Cat in the Cloud" title="Your Cat in the Cloud" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><a href="https://nestjs.com/" target="_blank" rel="noopener noreferrer">Nest</a> est mon framework Node.js préféré et aussi mon framework de travail. Je l’ai présenté sur ce blog l’année dernière et <a href="https://vinceops.me/nest-framework-nodejs-qu-il-nous-fallait/"><strong>l’article</strong> est toujours d’actualité si tu veux y jeter un œil</a>. Je l’utilise très souvent dans mes projets Node.js : Rest API, serveur GraphQL, micro-services, CLI, scripts, etc.</p> <p>Récemment, c’est avec Nest que j’ai décidé de développer quelques <em>Functions</em>. Dans cet article, je vais rappeler ce que sont les Functions puis présenter une façon simple d’y intégrer Nest. <em>En l’occurrence, avec <a href="https://firebase.google.com/docs/functions" target="_blank" rel="noopener noreferrer">Firebase Functions</a> (<a href="https://firebase.google.com/" target="_blank" rel="noopener noreferrer">Firebase</a> avec des <a href="https://cloud.google.com/functions/" target="_blank" rel="noopener noreferrer">Cloud Functions</a>).</em></p> <hr /> <h2>FaaS : Functions As A Service</h2> <p>Les Functions permettent de déployer et d’exécuter du code sur un serveur qui ne nous appartient pas. Zéro minute consacrée à la configuration, la sécurisation et l’entretien des machines. Tout ce qui est demandé au développeur, c’est d’écrire son code en respectant quelques règles (langage, structure, temps d’exécution…), puis de le déployer via une CLI, ou à la main.</p> <blockquote> <p>💡 Chez Amazon, le service s’appelle <a href="https://aws.amazon.com/lambda/features/" target="_blank" rel="noopener noreferrer">AWS Lambda</a>. Chez Google, c’est <a href="https://cloud.google.com/functions/" target="_blank" rel="noopener noreferrer">Cloud Functions</a>, Microsoft a les <a href="https://azure.microsoft.com/fr-fr/services/functions/" target="_blank" rel="noopener noreferrer">Azur Functions</a>. Les <a href="https://www.cloudflare.com/products/cloudflare-workers/" target="_blank" rel="noopener noreferrer">CloudFlare Workers</a> utilisent directement V8 (pas Node.js), mais l’import de modules NPM est possible et WebAssembly est supporté 🎉.</p> </blockquote> <p>L’exécution dudit code peut ensuite être déclenchée par un appel HTTP et/ou par un évènement : un <a href="https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html" target="_blank" rel="noopener noreferrer">fichier déposé sur AWS S3</a>, un <a href="https://firebase.google.com/docs/functions/auth-events#trigger_a_function_on_user_creation" target="_blank" rel="noopener noreferrer">utilisateur inscrit dans Firebase Authentication</a>, etc.</p> <p>Le FaaS offre de nombreux avantages :</p> <ul> <li>Un déploiement et une disponibilité quasi instantannés du code</li> <li>Pas de serveur à louer, configurer, maintenir</li> <li>Auto-scaling : la montée en charge (comme la baisse…) est assurée par le service</li> <li>Une tarification à la carte dépendante du nombre d’exécutions, de leur durée, de la mémoire vive allouée</li> </ul> <p>Et présente quelques limitations par nature et/ou fournisseur :</p> <ul> <li>Plafond de ressources, d’exécutions simultanées, de durée d’exécution</li> <li>Passé un délai d’inactivité, un délai de démarrage (<em>cold start</em>) est à prévoir à la prochaine exécution</li> </ul> <hr /> <h2>Dessine-moi une Function</h2> <p>On installe <code class="language-text">firebase-tools</code> pour utiliser la CLI de Firebase, puis on crée le projet :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash"><span class="token function">npm</span> i -g firebase-tools <span class="token function">mkdir</span> my-cat-in-the-cloud <span class="token operator">&&</span> <span class="token builtin class-name">cd</span> my-cat-in-the-cloud firebase login firebase projects:list <span class="token comment"># vérifie la bonne authentification</span> firebase init functions <span class="token comment"># crée un projet ou utilise un existant</span> * Please <span class="token keyword">select</span> an option: Create a new project * What language would you like to use to <span class="token function">write</span> Cloud Functions? TypeScript <span class="token comment"># 💖</span> * Do you want to use TSLint to catch probable bugs and enforce style? No <span class="token comment"># ESLint ftw</span> * Do you want to <span class="token function">install</span> dependencies with <span class="token function">npm</span> now? No <span class="token comment"># on va s'en occuper nous-même</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>La CLI génère <code class="language-text">firebase.json</code> et, le plus intéressant, <code class="language-text">./functions</code>. Si on étudie rapidement ce dernier avant de l’effacer (<strong>#yolo</strong>), on voit dans <code class="language-text">package.json</code> que sont inclus : <code class="language-text">firebase-admin</code>, <code class="language-text">firebase-functions</code> (<em>dependencies</em>) et <code class="language-text">typescript</code>, <code class="language-text">firebase-functions-test</code> (<em>dev dependencies</em>). Quant à <code class="language-text">src/index.ts</code> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> functions <span class="token keyword">from</span> <span class="token string">'firebase-functions'</span><span class="token punctuation">;</span> <span class="token comment">// Start writing Firebase Functions</span> <span class="token comment">// https://firebase.google.com/docs/functions/typescript</span> <span class="token keyword">export</span> <span class="token keyword">const</span> helloWorld <span class="token operator">=</span> functions<span class="token punctuation">.</span>https<span class="token punctuation">.</span><span class="token function">onRequest</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> response<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span><span class="token string">'Hello from Firebase!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Il expose seulement une <em>function</em> <code class="language-text">helloWorld</code> qui peut être exécutée localement de deux manières :</p> <ul> <li><code class="language-text">yarn serve</code> pour la servir avec un serveur HTTP: <a href="http://localhost:5000/my-cat-in-the-cloud/us-central1/helloWorld" target="_blank" rel="noopener noreferrer">http://localhost:5000/my-cat-in-the-cloud/us-central1/helloWorld</a> <em>(“my-cat-in-the-cloud” étant le nom de mon répertoire <strong>et</strong> de mon projet Firebase)</em></li> <li><code class="language-text">yarn start</code> (ou <code class="language-text">firebase functions:shell</code>) pour l’utiliser directement en mode shell :</li> </ul> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">$ <span class="token function">yarn</span> start ✔ functions: Emulator started at http://localhost:5000 i functions: Loaded functions: helloWorld firebase <span class="token operator">></span> helloWorld<span class="token punctuation">(</span><span class="token punctuation">)</span> Sent request to function. firebase <span class="token operator">></span> RESPONSE RECEIVED FROM FUNCTION: <span class="token number">200</span>, Hello from Firebase<span class="token operator">!</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Dans ce code, pas de framework Web particulier : seulement une fonction <code class="language-text">onRequest</code> qui prend en paramètre un callback de type Requête-Réponse. </p> <blockquote> <p>💡 Le fichier <code class="language-text">firebase.json</code> situé au même niveau que le répertoire <code class="language-text">./functions</code> permet la configuration (et le déploiement) des différents services de Firebase (Hosting, Functions, Storage, Firestore…).</p> </blockquote> <hr /> <h2>Release the <del>Kraken</del> Cat</h2> <p>Comme annoncé plus haut, on va supprimer <code class="language-text">./functions</code> ! … Et créer un projet Nest à la place.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 461px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/1e603d5d2d086779b4240b74cfafed71/7967d/all-the-things.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 65.12820512820512%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAUBAwT/xAAVAQEBAAAAAAAAAAAAAAAAAAADBP/aAAwDAQACEAMQAAABeZrlkKuSCov/xAAZEAADAAMAAAAAAAAAAAAAAAAAAQIDBDH/2gAIAQEAAQUCbJzK7H2m1sn/xAAWEQEBAQAAAAAAAAAAAAAAAAACEBL/2gAIAQMBAT8BA1P/xAAXEQEBAQEAAAAAAAAAAAAAAAABAgAS/9oACAECAQE/AZvuqMLv/8QAGhAAAgIDAAAAAAAAAAAAAAAAAQIAEDFBYf/aAAgBAQAGPwKFdrmwO1//xAAaEAACAwEBAAAAAAAAAAAAAAAAAREhMRBh/9oACAEBAAE/IYhS9FZc9hQHUBKtP//aAAwDAQACAAMAAAAQvP8A/8QAGBEAAwEBAAAAAAAAAAAAAAAAAAEhEUH/2gAIAQMBAT8QbK4tIf/EABsRAQACAgMAAAAAAAAAAAAAAAEAIRFRcbHR/9oACAECAQE/EEyzQpeuJgldez//xAAbEAEBAAMBAQEAAAAAAAAAAAABEQAhMbFRkf/aAAgBAQABPxBnrvuM2gFBtrq95gkhsMtQpDIkLZPpPFxAF/hn/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/1e603d5d2d086779b4240b74cfafed71/798b8/all-the-things.webp 195w, /static/1e603d5d2d086779b4240b74cfafed71/0e356/all-the-things.webp 390w, /static/1e603d5d2d086779b4240b74cfafed71/7786c/all-the-things.webp 461w" sizes="(max-width: 461px) 100vw, 461px" type="image/webp" /> <source srcset="/static/1e603d5d2d086779b4240b74cfafed71/0e421/all-the-things.jpg 195w, /static/1e603d5d2d086779b4240b74cfafed71/ce2e0/all-the-things.jpg 390w, /static/1e603d5d2d086779b4240b74cfafed71/7967d/all-the-things.jpg 461w" sizes="(max-width: 461px) 100vw, 461px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/1e603d5d2d086779b4240b74cfafed71/7967d/all-the-things.jpg" alt="all-the-things" title="all-the-things" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell"><span class="token function">rm</span> -rf functions <span class="token function">npm</span> i -g @nestjs/cli <span class="token comment"># si vous ne l'avez pas déjà installé</span> nest new api <span class="token operator">&&</span> <span class="token builtin class-name">cd</span> api <span class="token function">yarn</span> <span class="token operator">&&</span> <span class="token function">yarn</span> <span class="token function">add</span> firebase-functions firebase-admin</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>Puisque le projet Nest a été nommé <strong><code class="language-text">api</code></strong>, on change la propriété <strong><code class="language-text">"source"</code></strong> des <code class="language-text">"functions"</code> dans <code class="language-text">firebase.json</code> :</p> <div class="gatsby-highlight" data-language="json"><pre style="counter-reset: linenumber NaN" class="language-json line-numbers"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">"functions"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"predeploy"</span><span class="token operator">:</span> <span class="token string">"npm --prefix \"$RESOURCE_DIR\" run build"</span><span class="token punctuation">,</span> <span class="token property">"source"</span><span class="token operator">:</span> <span class="token string">"api"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le fichier <code class="language-text">api/src/main.ts</code> doit <strong>exposer une Function</strong>, que l’on nomme très originalement <code class="language-text">api</code> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> NestFactory <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/core'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> ExpressAdapter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/platform-express'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> express <span class="token keyword">from</span> <span class="token string">'express'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> functions <span class="token keyword">from</span> <span class="token string">'firebase-functions'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> AppModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.module'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token function-variable function">startNestApplication</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">expressApp<span class="token operator">:</span> Express<span class="token punctuation">.</span>Application</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token keyword">await</span> NestFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>AppModule<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">ExpressAdapter</span><span class="token punctuation">(</span>expressApp<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> app<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token keyword">const</span> server <span class="token operator">=</span> <span class="token function">express</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">startNestApplication</span><span class="token punctuation">(</span>server<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token keyword">catch</span><span class="token punctuation">(</span><span class="token parameter">err</span> <span class="token operator">=></span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Nest did not start'</span><span class="token punctuation">,</span> err<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">const</span> api <span class="token operator">=</span> functions<span class="token punctuation">.</span><span class="token function">region</span><span class="token punctuation">(</span><span class="token string">'europe-west1'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>https<span class="token punctuation">.</span><span class="token function">onRequest</span><span class="token punctuation">(</span>server<span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 Notez la configuration de la <strong>région</strong> avec <code class="language-text">europe-west1</code>, afin de diminuer la latence entre mes utilisateurs et ma Function, une fois celle-ci déployée.</p> </blockquote> <p>Ensuite, la CLI de Firebase doit connaître le <strong>point d’entrée</strong> de notre Function. Il faut ajouter une propriété <code class="language-text">"main"</code> ayant pour valeur <code class="language-text">"dist/main.js"</code> dans le <code class="language-text">package.json</code> généré par <code class="language-text">@nestjs/cli</code>.</p> <p>Reste à compiler puis à exécuter !</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">$ <span class="token function">yarn</span> build <span class="token operator">&&</span> firebase serve --only functions ✔ functions: Emulator started at http://localhost:5000 i functions: Watching <span class="token string">"/home/vince/blog/my-cat-in-the-cloud/api"</span> <span class="token keyword">for</span> Cloud Functions<span class="token punctuation">..</span>. <span class="token operator">></span> <span class="token punctuation">[</span>Nest<span class="token punctuation">]</span> <span class="token number">20843</span> - <span class="token punctuation">[</span>NestFactory<span class="token punctuation">]</span> Starting Nest application<span class="token punctuation">..</span>. ✔ functions<span class="token punctuation">[</span>api<span class="token punctuation">]</span>: http <span class="token keyword">function</span> initialized <span class="token punctuation">(</span>http://localhost:5000/my-cat-in-the-cloud/europe-west1/api<span class="token punctuation">)</span>. <span class="token comment"># [...]</span> <span class="token operator">></span> <span class="token punctuation">[</span>Nest<span class="token punctuation">]</span> <span class="token number">20903</span> - <span class="token punctuation">[</span>NestApplication<span class="token punctuation">]</span> Nest application successfully started +2ms</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>On vérifie que l’API répond à nos requêtes :</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">$ <span class="token function">curl</span> http://localhost:5000/my-cat-in-the-cloud/europe-west1/api Hello World<span class="token operator">!</span> <span class="token comment"># Ça fonctionne 🎉</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>Et c’est gagné !</p> <img src="https://media.giphy.com/media/l0IsIC9ZNOELYmuqc/giphy.gif" alt="c'est gagné" style="display: block; margin: 0 auto 25px auto; width: 280px; height: 200px;" /> <p>Pour <strong>déployer la Function</strong>, il faut indiquer à Firebase quelle version du runtime (Node.js) utiliser via <code class="language-text">package.json</code>, et utiliser la commande <code class="language-text">deploy</code> :</p> <div class="gatsby-highlight" data-language="json"><pre style="counter-reset: linenumber NaN" class="language-json line-numbers"><code class="language-json"> <span class="token property">"engines"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"node"</span><span class="token operator">:</span> <span class="token string">"10"</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span></span></pre></div> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">$ firebase deploy --only functions <span class="token operator">==</span><span class="token operator">=</span> Deploying to <span class="token string">'my-cat-in-the-cloud'</span><span class="token punctuation">..</span>. i deploying functions Running command: <span class="token function">npm</span> --prefix <span class="token string">"<span class="token variable">$RESOURCE_DIR</span>"</span> run build <span class="token operator">></span> functions@0.0.1 prebuild /home/vince/blog/my-cat-in-the-cloud/api <span class="token operator">></span> rimraf dist <span class="token operator">></span> functions@0.0.1 build /home/vince/blog/my-cat-in-the-cloud/api <span class="token operator">></span> nest build ✔ functions: Finished running predeploy script. i functions: ensuring necessary APIs are enabled<span class="token punctuation">..</span>. ✔ functions: all necessary APIs are enabled i functions: preparing api directory <span class="token keyword">for</span> uploading<span class="token punctuation">..</span>. i functions: packaged api <span class="token punctuation">(</span><span class="token number">197.62</span> KB<span class="token punctuation">)</span> <span class="token keyword">for</span> uploading ✔ functions: api folder uploaded successfully i functions: creating Node.js <span class="token number">10</span> <span class="token punctuation">(</span>Beta<span class="token punctuation">)</span> <span class="token keyword">function</span> api<span class="token punctuation">(</span>europe-west1<span class="token punctuation">)</span><span class="token punctuation">..</span>. ✔ functions<span class="token punctuation">[</span>api<span class="token punctuation">(</span>europe-west1<span class="token punctuation">)</span><span class="token punctuation">]</span>: Successful create operation. Function URL <span class="token punctuation">(</span>api<span class="token punctuation">)</span>: https://europe-west1-my-cat-in-the-cloud.cloudfunctions.net/api ✔ Deploy complete<span class="token operator">!</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 La <strong>Console</strong> de Firebase offre des metrics et du logging pour chaque Function déployée.</p> </blockquote> <p>Partant de là, on peut imaginer :</p> <ul> <li>Créer de nouveaux controllers/endpoints (e.g. <code class="language-text">@Post('api/users')</code>, <code class="language-text">@Get('healthcheck')</code>…) </li> <li>Présenter une <a href="https://docs.nestjs.com/recipes/swagger" target="_blank" rel="noopener noreferrer">documentation Swagger</a> (e.g. <code class="language-text">api/<route_choise_pour_le_document_Swagger></code>) </li> <li>Ajouter un endpoint pour <a href="https://docs.nestjs.com/graphql/quick-start" target="_blank" rel="noopener noreferrer">GraphQL</a> (e.g. <code class="language-text">api/graphql</code>) </li> </ul> <p>👮 Néanmoins, l’utilisation d’Express et de Nest entraîne nécessairement un surcoût en terme temps de lancement, de temps d’exécution et de mémoire consommée. <strong>Je ne recommande pas</strong> de le faire en production sous prétexte que “c’est possible”. L’article <a href="https://medium.com/lumigo/web-frameworks-implication-on-serverless-cold-start-18ee5eb6c62a" target="_blank" rel="noopener noreferrer">Web Frameworks Implication on Serverless Cold Start Performance in NodeJS</a> traîte bien le sujet, bien qu’il soit concentré sur le <em>Cold start</em>. Pour un fonctionnement optimal, il est recommandé de créer plusieurs Functions très spécialisées (et avec un minimum d’<em>overhead</em>), plutôt qu’une seule offrant une API complète.</p> <p>À vos <em>use cases</em> 😬 ! </p> <p><em>Credits à <a href="https://twitter.com/jeffdelaney23" target="_blank" rel="noopener noreferrer">@Jeffdelaney23</a> pour l’exposition de Nest en tant que Function</em><br /> <em>Credits à <a href="s4m-ur4i.itch.io">Sam</a> pour le pixel-art de Nuage ☁</em></p></div>Rust et WebAssembly : Wasm everywherehttps://vinceops.me/rust-webassembly-wasm-everywhere/2019-11-21T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/9a43467e363f440544dd8b4e14c4d80e/a22ce/rust-wasm.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECA//EABcBAAMBAAAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAd6iIFa//8QAGRABAAIDAAAAAAAAAAAAAAAAAQACERIT/9oACAEBAAEFAuhluTcn/8QAFREBAQAAAAAAAAAAAAAAAAAAAhD/2gAIAQMBAT8BE//EABURAQEAAAAAAAAAAAAAAAAAAAAB/9oACAECAQE/AUf/xAAWEAEBAQAAAAAAAAAAAAAAAAAAIWH/2gAIAQEABj8CxFf/xAAYEAADAQEAAAAAAAAAAAAAAAAAAREhQf/aAAgBAQABPyGTCx9ilrT/2gAMAwEAAgADAAAAEIA//8QAFxEBAQEBAAAAAAAAAAAAAAAAAQARQf/aAAgBAwEBPxDIiHLb/8QAGBEAAwEBAAAAAAAAAAAAAAAAAAERITH/2gAIAQIBAT8QndGcP//EABoQAQEAAgMAAAAAAAAAAAAAAAERADEhYZH/2gAIAQEAAT8Qomogcb7xdFWqMuEHkHP/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/9a43467e363f440544dd8b4e14c4d80e/798b8/rust-wasm.webp 195w, /static/9a43467e363f440544dd8b4e14c4d80e/0e356/rust-wasm.webp 390w, /static/9a43467e363f440544dd8b4e14c4d80e/4c79d/rust-wasm.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/9a43467e363f440544dd8b4e14c4d80e/0e421/rust-wasm.jpg 195w, /static/9a43467e363f440544dd8b4e14c4d80e/ce2e0/rust-wasm.jpg 390w, /static/9a43467e363f440544dd8b4e14c4d80e/a22ce/rust-wasm.jpg 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/9a43467e363f440544dd8b4e14c4d80e/a22ce/rust-wasm.jpg" alt="wasm-pack" title="wasm-pack" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>J’en parlais dans le <a href="https://vinceops.me/rust-premiers-retours/">premier article</a> sur Rust : l’attrait de <a href="https://webassembly.org/" target="_blank" rel="noopener noreferrer">WebAssembly</a> est l’une des raisons qui m’ont poussé vers ce langage. Dans cet article, j’aborde la compilation de code Rust en un fichier binaire Wasm et la génération du <em>package</em> NPM permettant son exploitation. Avec Node.js <strong>et</strong> dans le navigateur… <em>Et après je parle <em>beaucoup</em> de WebAssembly 😁</em>.</p> <img src="https://media.giphy.com/media/PORxKijkF3xw4/giphy.gif" alt="here we go" style="display: block; margin: 0 auto 25px auto; width: 400px; height: 200px;" /> <blockquote> <p>🦕 <strong>Deno</strong> ne sera pas abordé dans l’article, mais <a href="https://github.com/denoland/deno/releases/tag/v0.24.0" target="_blank" rel="noopener noreferrer">la version 0.24</a> supporte désormais l’import des modules WebAssembly.</p> </blockquote> <hr /> <h2>D’abord, un peu de Rust</h2> <p>Pour changer des exemples <code class="language-text">sayHello(name)</code> et <code class="language-text">add(a, b)</code>, on va écrire en Rust une petite <em>lib</em> simpliste qui compte le nombre de jours calendaires séparant deux dates. Par exemple :</p> <ul> <li><code class="language-text">365</code> jours entre Noël 2018 et Noël 2019,</li> <li><code class="language-text">0</code> jour entre <code class="language-text">2019-11-14</code> et <code class="language-text">2019-11-14T16:58:56.822Z</code></li> <li><code class="language-text">29</code> jours entre <code class="language-text">2012-02-01</code> et <code class="language-text">2012-03-01</code> (année bisextile)</li> </ul> <p>C’est proche de ce que fait <a href="https://momentjs.com/" target="_blank" rel="noopener noreferrer">moment.js</a> avec sa fonction <code class="language-text">diff</code> (<code class="language-text">momentA.diff(momentB, 'days')</code>). Pour simplifier le cas, la fonction développée prendra en paramètres des timestamps en millisecondes.</p> <blockquote> <p>👮 Chère “Rust Police”, ma pratique du langage est toujours très sporadique. Si tu constates une aberration qui t’offusque, n’hésite pas à me DM <a href="https://twitter.com/VinceOPS" target="_blank" rel="noopener noreferrer">sur Twitter</a> 🚨.</p> </blockquote> <p>Allez viens, on se crée une <strong>lib</strong> 🚀 ! <em>Je fais l’hypothèse que tu as déjà utilisé Rust et Cargo.</em></p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">cargo new --lib days-count</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Dans <code class="language-text">src/lib.rs</code>, on se donne la définition suivante :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">pub</span> <span class="token keyword">fn</span> <span class="token function">count_days_between</span><span class="token punctuation">(</span>timestamp_ms_a<span class="token punctuation">:</span> u64<span class="token punctuation">,</span> timestamp_ms_b<span class="token punctuation">:</span> u64<span class="token punctuation">)</span> <span class="token punctuation">-></span> u64 <span class="token punctuation">{</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>On rapporte chaque timestamp au nombre de jours écoulés depuis le 01/01/1970. Par exemple, pour le 10/01/1970 (timestamp : <code class="language-text">777600000</code>), on obtient <code class="language-text">9</code> jours écoulés. Il suffit ensuite de retourner la différence des deux.</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token comment">/// Count the number of calendar days between two dates given as</span> <span class="token comment">/// timestamps in milliseconds. Make the assumption that One day is</span> <span class="token comment">/// 86_400_000 milliseconds (leap seconds are ignored).</span> <span class="token keyword">pub</span> <span class="token keyword">fn</span> <span class="token function">count_days_between</span><span class="token punctuation">(</span>timestamp_ms_a<span class="token punctuation">:</span> u64<span class="token punctuation">,</span> timestamp_ms_b<span class="token punctuation">:</span> u64<span class="token punctuation">)</span> <span class="token punctuation">-></span> u64 <span class="token punctuation">{</span> <span class="token keyword">let</span> days_count_a <span class="token operator">=</span> timestamp_ms_a <span class="token operator">/</span> <span class="token number">1000</span> <span class="token operator">/</span> <span class="token number">3600</span> <span class="token operator">/</span> <span class="token number">24</span><span class="token punctuation">;</span> <span class="token keyword">let</span> days_count_b <span class="token operator">=</span> timestamp_ms_b <span class="token operator">/</span> <span class="token number">1000</span> <span class="token operator">/</span> <span class="token number">3600</span> <span class="token operator">/</span> <span class="token number">24</span><span class="token punctuation">;</span> <span class="token keyword">let</span> days_count_between <span class="token operator">=</span> <span class="token keyword">match</span> days_count_a<span class="token punctuation">.</span><span class="token function">checked_sub</span><span class="token punctuation">(</span>days_count_b<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">Some</span><span class="token punctuation">(</span>difference<span class="token punctuation">)</span> <span class="token operator">=></span> difference<span class="token punctuation">,</span> None <span class="token operator">=></span> days_count_b <span class="token operator">-</span> days_count_a<span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token keyword">return</span> days_count_between<span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 Pour le code complet (avec ses tests), c’est <a href="https://gist.github.com/VinceOPS/d8d5cbb34e2cbcd62ac3b8f55bb13c5b" target="_blank" rel="noopener noreferrer">par ici</a>.</p> </blockquote> <img src="https://media.giphy.com/media/IspgDOjRHwtbi/giphy.gif" alt="dangerust" style="display: block; margin: 0 auto 25px auto; width: 340px; height: 200px;" /> <h2>Et maintenant, du WebAssembly</h2> <p>Chouette 🦉, on a un code Rust qui fonctionne ! Maintenant, il faut le compiler en WebAssembly. C’est <a href="https://rustwasm.github.io/wasm-pack" target="_blank" rel="noopener noreferrer">wasm-pack</a> qui s’en charge, en plus de générer aussi le package NPM contenant le binaire <code class="language-text">.wasm</code>, notre <code class="language-text">package.json</code> et les fichiers <code class="language-text">.js</code> permettant le <em>binding</em> Javascript ↔ Rust 🎆. <em>Merci qui ? Merci <a href="https://rustwasm.github.io/docs/wasm-bindgen/" target="_blank" rel="noopener noreferrer">wasm-bindgen</a></em>.</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell"><span class="token comment"># on ajoute la cible permettant de compiler du Rust en WebAssembly</span> rustup target <span class="token function">add</span> wasm32-unknown-unknown <span class="token comment"># et on installe wasm-pack</span> <span class="token function">curl</span> https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf <span class="token operator">|</span> <span class="token function">sh</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>Il faut aussi ajouter une dépendance et une <em>lib</em> à notre fichier <code class="language-text">Cargo.toml</code> :</p> <div class="gatsby-highlight" data-language="toml"><pre style="counter-reset: linenumber NaN" class="language-toml line-numbers"><code class="language-toml"><span class="token comment"># [...]</span> <span class="token punctuation">[</span><span class="token table class-name">dependencies</span><span class="token punctuation">]</span> <span class="token key property">wasm-bindgen</span> <span class="token punctuation">=</span> <span class="token string">"0.2"</span> <span class="token punctuation">[</span><span class="token table class-name">lib</span><span class="token punctuation">]</span> <span class="token key property">crate-type</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"cdylib"</span><span class="token punctuation">]</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Et enfin, ajouter la macro <a href="https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/index.html" target="_blank" rel="noopener noreferrer"><code class="language-text">wasm_bindgen</code></a> à notre fonction :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">use</span> wasm_bindgen<span class="token punctuation">::</span>prelude<span class="token punctuation">::</span><span class="token operator">*</span><span class="token punctuation">;</span> <span class="token attribute attr-name">#[wasm_bindgen]</span> <span class="token keyword">pub</span> <span class="token keyword">fn</span> <span class="token function">count_days_between</span><span class="token punctuation">(</span>timestamp_ms_a<span class="token punctuation">:</span> u64<span class="token punctuation">,</span> timestamp_ms_b<span class="token punctuation">:</span> u64<span class="token punctuation">)</span> <span class="token punctuation">-></span> u64 <span class="token punctuation">{</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <h3>Dans Node.js</h3> <p>On compile ensuite notre <em>lib</em> en exécutant :</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">wasm-pack build --release --target nodejs</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <blockquote> <p>⚙ <code class="language-text">--release</code> pour la compilation optimisée, <code class="language-text">--target nodejs</code> pour des bindings adaptés.</p> </blockquote> <p>La commande génère le module NPM dans un dossier <code class="language-text">./pkg</code> que l’on va directement utiliser avec Node.js 10+ 🔥 :</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell"><span class="token function">mkdir</span> nodejs <span class="token operator">&&</span> <span class="token builtin class-name">cd</span> nodejs <span class="token function">yarn</span> init -y <span class="token comment"># ou : npm init -y</span> <span class="token function">yarn</span> <span class="token function">add</span> --dev <span class="token punctuation">..</span>/pkg <span class="token comment"># ou : npm i -D ../pkg</span> <span class="token function">touch</span> index.js</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>Dans le <code class="language-text">index.js</code> fraîchement créé, on importe le module <code class="language-text">days-count</code> généré par <code class="language-text">wasm-pack</code> et on exécute notre fonction <code class="language-text">count_days_between</code> :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">const</span> daysCount <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'days-count'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> daysCount<span class="token punctuation">.</span><span class="token function">count_days_between</span><span class="token punctuation">(</span> <span class="token comment">// 01/02/2012</span> <span class="token function">BigInt</span><span class="token punctuation">(</span><span class="token number">1330560000000</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 01/03/2012</span> <span class="token function">BigInt</span><span class="token punctuation">(</span><span class="token number">1328054400000</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 29 👍</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 C’est <code class="language-text">BingInt</code> que l’on utilise comme équivalent au type <code class="language-text">u64</code> de Rust, puisque <code class="language-text">number</code> ne peut pas contenir une valeur supérieure à 2<sup>53</sup> - 1 (<code class="language-text">Number.MAX_SAFE_INTEGER</code>).</p> </blockquote> <p>Accessoirement, <code class="language-text">wasm-pack</code> a <strong>aussi</strong> généré les typedefs (<code class="language-text">*.d.ts</code>) des <em>bindings</em> JavaScript… On profite donc du typage, de l’autocomplétion et même d’une JSDoc tirée des commentaires Rust (<code class="language-text">///</code>) 🎉.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/e6edfbc41e03783c536106d8afa75b80/14c60/wasm-bindgen-typedefs.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 26.666666666666668%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAFABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABzQQH/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAFxAAAwEAAAAAAAAAAAAAAAAAABARIf/aAAgBAQABPyG4qf/aAAwDAQACAAMAAAAQgB//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAYEAEAAwEAAAAAAAAAAAAAAAABABFhQf/aAAgBAQABPxA7FrLwjif/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/e6edfbc41e03783c536106d8afa75b80/798b8/wasm-bindgen-typedefs.webp 195w, /static/e6edfbc41e03783c536106d8afa75b80/0e356/wasm-bindgen-typedefs.webp 390w, /static/e6edfbc41e03783c536106d8afa75b80/23bbb/wasm-bindgen-typedefs.webp 780w, /static/e6edfbc41e03783c536106d8afa75b80/1dd27/wasm-bindgen-typedefs.webp 948w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/e6edfbc41e03783c536106d8afa75b80/0e421/wasm-bindgen-typedefs.jpg 195w, /static/e6edfbc41e03783c536106d8afa75b80/ce2e0/wasm-bindgen-typedefs.jpg 390w, /static/e6edfbc41e03783c536106d8afa75b80/485b7/wasm-bindgen-typedefs.jpg 780w, /static/e6edfbc41e03783c536106d8afa75b80/14c60/wasm-bindgen-typedefs.jpg 948w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/e6edfbc41e03783c536106d8afa75b80/485b7/wasm-bindgen-typedefs.jpg" alt="Type definitions from Rust" title="Type definitions from Rust" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <img src="https://media.giphy.com/media/5VKbvrjxpVJCM/giphy.gif" alt="omg" style="display: block; margin: 0 auto 25px auto; width: 340px; height: 270px;" /> <h3>Dans le Navigateur</h3> <p><em>La lecture du paragraphe précédent (Node.js) est fortement recommandée.</em></p> <p>On recompile notre <em>lib</em> en changeant la <code class="language-text">--target</code> :</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell">wasm-pack build --release --target bundler</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <blockquote> <p>💡 <code class="language-text">bundler</code> est la cible par défaut à ce jour, mais tu peux aussi utiliser <code class="language-text">web</code> <a href="https://rustwasm.github.io/wasm-pack/book/commands/build.html#target" target="_blank" rel="noopener noreferrer">sous certaines conditions</a>.</p> </blockquote> <p>On crée un projet basé sur la template NPM <code class="language-text">wasm-app</code> :</p> <div class="gatsby-highlight" data-language="shell"><pre style="counter-reset: linenumber NaN" class="language-shell line-numbers"><code class="language-shell"><span class="token function">mkdir</span> browser <span class="token operator">&&</span> <span class="token builtin class-name">cd</span> browser <span class="token function">npm</span> init wasm-app <span class="token function">yarn</span> <span class="token function">add</span> --dev <span class="token punctuation">..</span>/pkg <span class="token comment"># ou : npm i -D ../pkg</span> <span class="token function">yarn</span> <span class="token operator">&&</span> <span class="token function">yarn</span> build <span class="token comment"># ou npm install && npm run build</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>Dans <code class="language-text">index.js</code>, on procède de la même manière qu’avec Node.js :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> daysCount <span class="token keyword">from</span> <span class="token string">'days-count'</span><span class="token punctuation">;</span> <span class="token comment">// pour changer du console.log 🤷</span> document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span> daysCount<span class="token punctuation">.</span><span class="token function">count_days_between</span><span class="token punctuation">(</span> <span class="token comment">// 01/02/2012</span> <span class="token function">BigInt</span><span class="token punctuation">(</span><span class="token number">1330560000000</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 01/03/2012</span> <span class="token function">BigInt</span><span class="token punctuation">(</span><span class="token number">1328054400000</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Un coup de <code class="language-text">yarn start</code> pour lancer <code class="language-text">webpack-dev-server</code> et on constate sur <code class="language-text">http://localhost:8080</code> que “29” est bien ajouté au corps de la page.</p> <p>Pour les utilisateurs de <a href="https://webpack.js.org/" target="_blank" rel="noopener noreferrer">Webpack</a>, le plugin <a href="https://github.com/wasm-tool/wasm-pack-plugin" target="_blank" rel="noopener noreferrer"><code class="language-text">wasm-pack-plugin</code></a> est intéressant : il permet l’automatisation du processus que l’on vient de suivre, en <code class="language-text">import</code>ant directement un projet Rust.</p> <p>Pour les utilisateurs de <a href="https://parceljs.org/" target="_blank" rel="noopener noreferrer">Parcel</a>, il existe aussi une template NPM dédiée : <a href="https://github.com/rustwasm/rust-parcel-template" target="_blank" rel="noopener noreferrer"><code class="language-text">rust-parcel</code></a>. Le <a href="https://github.com/rustwasm/rust-parcel-template/blob/master/crate/Cargo.toml" target="_blank" rel="noopener noreferrer"><code class="language-text">Cargo.toml</code></a> emploie par ailleurs deux <em>crates</em> très utiles : <code class="language-text">console_error_panic_hook</code>, <code class="language-text">wee_alloc</code>.</p> <h2>Ce qu’il faut retenir</h2> <img src="https://media.giphy.com/media/JpdjC3bCBKlA0zt8PP/giphy.gif" alt="so what?" style="display: block; margin: 0 auto 25px auto; width: 300px; height: 180px;" /> <p>Ok, on a compilé une <em>lib</em> Rust simplissime en WebAssembly, on en a fait un module, on l’a utilisé avec Node.js <strong>et</strong> Webpack. C’est cool, mais ça ne règle <strong>aucun</strong> problème… Ou peut-être que si ?</p> <p>Les principaux bénéfices à en tirer sont :</p> <ul> <li>Des gains <em>très</em> significatifs de <strong>performances</strong> dans l’exécution d’opérations longues et/ou intensives (calcul, compression, multimédia, jeux vidéo…).</li> <li>La capacité d’utiliser un autre langage que JavaScript dans Node.js et/ou le navigateur. Notamment Rust, C et C++, pour des binaires Wasm optimisés. Si le poids du binaire ou la performance ne sont pas des critères pour votre projet, on peut aussi imaginer que les <em>features</em> du langage utilisé soient un argument suffisant (<a href="https://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">TypeScript</a> avec <a href="https://github.com/AssemblyScript/assemblyscript" target="_blank" rel="noopener noreferrer">AssemblyScript</a>, <a href="https://github.com/golang/go/wiki/WebAssembly" target="_blank" rel="noopener noreferrer">Go</a>, etc).</li> </ul> <blockquote> <p>💡 Ce point sera abordé dans un prochain article, mais il est possible d’<strong>utiliser les API Web</strong> depuis le code Rust. Tout est <strong>bien</strong> détaillé dans la <a href="https://rustwasm.github.io/docs/wasm-bindgen/" target="_blank" rel="noopener noreferrer">documentation de wasm-bindgen</a>, dans la partie concernant <a href="https://crates.io/crates/web-sys" target="_blank" rel="noopener noreferrer">web-sys</a> : DOM, Events, fetching, WebGL, WebSockets 🎉…</p> <p>C’est aussi vrai avec C et C++ grâce à <a href="https://emscripten.org" target="_blank" rel="noopener noreferrer">https://emscripten.org</a>.</p> </blockquote> <p>Les opportunités apportées par cette technologie sont très prometteuses (et excitantes, <em>ain’t they?</em>). Comme beaucoup l’ont dit avant moi, <strong>WebAssembly ne remplacera pas JavaScript</strong>. Et si l’on s’en tient au seul aspect de la performance, même en JavaScript, V8 offre des performances souvent suffisantes pour beaucoup d’applications… Sans parler des outils plus simples à utiliser (et/ou mieux intégrés) en JavaScript <strong>pour le moment</strong> : debugging, code splitting, etc.</p> <p>Mozilla, Intel, Fastly (créateurs de <a href="https://www.fastly.com/blog/announcing-lucet-fastly-native-webassembly-compiler-runtime" target="_blank" rel="noopener noreferrer">Lucet</a>) et Red Hat ont créé la <a href="https://bytecodealliance.org" target="_blank" rel="noopener noreferrer">Bytecode Alliance</a>, ayant pour but d’étendre l’existence de WebAssembly au-delà des navigateurs. Plus d’infos sur le <a href="https://blog.mozilla.org/press-fr/2019/11/12/la-nouvelle-alliance-bytecode-apporte-plus-de-securite-dubiquite-et-dinteroperabilite-au-web/" target="_blank" rel="noopener noreferrer">blog de Mozilla</a>… Et concernant <strong>Wasmtime</strong> et les <strong>WebAssembly Interface Types</strong> (WASI), <a href="https://www.youtube.com/watch?v=Qn_4F3foB3Q" target="_blank" rel="noopener noreferrer">cette vidéo de Lin Clark</a> est incontournable 🔥 (et une version plus longue/développée <a href="https://www.youtube.com/watch?v=fh9WXPu0hw8" target="_blank" rel="noopener noreferrer">ici</a>).</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/e3f1c349058085e9295720835751c238/7293b/wasm-everywhere.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 68.20512820512819%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAwAC/8QAFQEBAQAAAAAAAAAAAAAAAAAAAQP/2gAMAwEAAhADEAAAAT0STodiT//EABgQAQEAAwAAAAAAAAAAAAAAAAEAAhAR/9oACAEBAAEFApFbtma//8QAFhEBAQEAAAAAAAAAAAAAAAAAAAES/9oACAEDAQE/AbGH/8QAFhEBAQEAAAAAAAAAAAAAAAAAABES/9oACAECAQE/Aa0//8QAGBAAAgMAAAAAAAAAAAAAAAAAARAAAhL/2gAIAQEABj8CYmjZf//EABsQAAMAAgMAAAAAAAAAAAAAAAABESExQVFx/9oACAEBAAE/IVJa7wiAST8HsjCu70MtTrmENdH/2gAMAwEAAgADAAAAEI8v/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEh/9oACAEDAQE/EKYp/8QAFhEAAwAAAAAAAAAAAAAAAAAAAAER/9oACAECAQE/EE1IP//EABsQAAICAwEAAAAAAAAAAAAAAAERACFBUWFx/9oACAEBAAE/EDFlgDAdwA1hU++XCDIsRDNgAx2NaMQEKh3sAyzGGLn/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/e3f1c349058085e9295720835751c238/798b8/wasm-everywhere.webp 195w, /static/e3f1c349058085e9295720835751c238/0e356/wasm-everywhere.webp 390w, /static/e3f1c349058085e9295720835751c238/23bbb/wasm-everywhere.webp 780w, /static/e3f1c349058085e9295720835751c238/d0742/wasm-everywhere.webp 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/e3f1c349058085e9295720835751c238/0e421/wasm-everywhere.jpg 195w, /static/e3f1c349058085e9295720835751c238/ce2e0/wasm-everywhere.jpg 390w, /static/e3f1c349058085e9295720835751c238/485b7/wasm-everywhere.jpg 780w, /static/e3f1c349058085e9295720835751c238/7293b/wasm-everywhere.jpg 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/e3f1c349058085e9295720835751c238/485b7/wasm-everywhere.jpg" alt="wasm-everywhere" title="wasm-everywhere" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Ah, au fait. J’ai créé un <a href="https://github.com/VinceOPS/rust-wasm-introduction" target="_blank" rel="noopener noreferrer">projet <strong>TypeScript</strong> (💝)</a> exploitant un binaire Wasm avec Node.js <strong>et</strong> Webpack, de la même manière que je l’ai exposé en JavaScript dans cet article.</p> <h3>Bonus - Des ressources qui valent le détour</h3> <ul> <li>Diablo 1 en JavaScript (React) et WebAssembly : <a href="https://github.com/d07RiV/diabloweb" target="_blank" rel="noopener noreferrer">diabloweb</a></li> <li><a href="https://tehnokv.com/posts/pico-to-wasm/" target="_blank" rel="noopener noreferrer">Compiling a face detector written in C to WebAssembly</a></li> <li>ImageMagick compilé en WebAssembly : <a href="https://github.com/cancerberoSgx/magica/tree/master/magick-wasm" target="_blank" rel="noopener noreferrer">magica</a></li> <li>Skia, de Google, compilé en WebAssembly : <a href="https://github.com/Zubnix/skia-wasm-port" target="_blank" rel="noopener noreferrer">Zubnix/skia-wasm-port</a></li> <li>Un émulateur de Game Boy, développé en TypeScript et compilé par <a href="https://github.com/AssemblyScript/assemblyscript" target="_blank" rel="noopener noreferrer">AssemblyScript</a> : <a href="https://github.com/torch2424/wasmboy" target="_blank" rel="noopener noreferrer">wasmboy</a></li> <li>Qt Framework compatible avec WebAssembly : <a href="https://doc.qt.io/qt-5/wasm.html" target="_blank" rel="noopener noreferrer">Qt 5.13+</a></li> <li><a href="https://www.youtube.com/watch?v=njt-Qzw0mVY" target="_blank" rel="noopener noreferrer">WebAssembly for Web Developers (Google I/O ’19)</a> et notamment <a href="https://squoosh.app/" target="_blank" rel="noopener noreferrer">Squoosh</a> (<strong>!!</strong>)</li> </ul></div>AWS Acceleration Roadshow 2019https://vinceops.me/aws-acceleration-roadshow/2019-10-23T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/44c9d5b187d57c0d05e5ee9e53a416ae/7293b/aws-ar-2019.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 33.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAID/8QAFgEBAQEAAAAAAAAAAAAAAAAAAwEC/9oADAMBAAIQAxAAAAHCRNwKn//EABYQAAMAAAAAAAAAAAAAAAAAAAAQIf/aAAgBAQABBQJU/8QAGBEAAgMAAAAAAAAAAAAAAAAAAAIBERP/2gAIAQMBAT8BWLM1P//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/AVf/xAAWEAADAAAAAAAAAAAAAAAAAAAAEDH/2gAIAQEABj8CVP/EABoQAAICAwAAAAAAAAAAAAAAAAARASExYZH/2gAIAQEAAT8hpZlkvfRF/9oADAMBAAIAAwAAABBz3//EABYRAAMAAAAAAAAAAAAAAAAAAAEQMf/aAAgBAwEBPxANK//EABURAQEAAAAAAAAAAAAAAAAAABEA/9oACAECAQE/EEnf/8QAHBAAAgICAwAAAAAAAAAAAAAAAREAITFBkdHw/9oACAEBAAE/EFiv6KCgpXI+p5kz/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/44c9d5b187d57c0d05e5ee9e53a416ae/798b8/aws-ar-2019.webp 195w, /static/44c9d5b187d57c0d05e5ee9e53a416ae/0e356/aws-ar-2019.webp 390w, /static/44c9d5b187d57c0d05e5ee9e53a416ae/23bbb/aws-ar-2019.webp 780w, /static/44c9d5b187d57c0d05e5ee9e53a416ae/d0742/aws-ar-2019.webp 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/44c9d5b187d57c0d05e5ee9e53a416ae/0e421/aws-ar-2019.jpg 195w, /static/44c9d5b187d57c0d05e5ee9e53a416ae/ce2e0/aws-ar-2019.jpg 390w, /static/44c9d5b187d57c0d05e5ee9e53a416ae/485b7/aws-ar-2019.jpg 780w, /static/44c9d5b187d57c0d05e5ee9e53a416ae/7293b/aws-ar-2019.jpg 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/44c9d5b187d57c0d05e5ee9e53a416ae/485b7/aws-ar-2019.jpg" alt="aws-ar" title="aws-ar" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Voilà déjà 1 semaine que j’ai assisté à l’<a href="https://aws.amazon.com/fr/events/acceleration-roadshow-2019/" target="_blank" rel="noopener noreferrer">AWS Acceleration Roadshow</a> d’Aix en Provence, avec David, aussi <em>lead dev</em> chez <a href="https://twitter.com/GojobT" target="_blank" rel="noopener noreferrer">Gojob</a>. On a décidé de s’y rendre pour les sujets d’<strong>IA/ML</strong>, par <strong>curiosité technique</strong> et par <strong>curiosité vis à vis de GCP</strong>. En effet, on travaille quotidiennement avec <em>Google Cloud Provider</em> et je n’ai pas utilisé AWS depuis 2017.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 74.87179487179488%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAIBAwT/xAAUAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAERIGwyg//EABkQAQADAQEAAAAAAAAAAAAAAAABAiEREv/aAAgBAQABBQLEa4rZOPVn/8QAFREBAQAAAAAAAAAAAAAAAAAAARD/2gAIAQMBAT8BJ//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAECAQE/AWf/xAAaEAACAgMAAAAAAAAAAAAAAAAAIRAxETJR/9oACAEBAAY/AtsFlnYSP//EABoQAQACAwEAAAAAAAAAAAAAAAEAITFBUYH/2gAIAQEAAT8hQbV5UQSQdwUxBBY3oi7Q9T//2gAMAwEAAgADAAAAEIPP/8QAFxEBAAMAAAAAAAAAAAAAAAAAAAERIf/aAAgBAwEBPxCFsf/EABYRAQEBAAAAAAAAAAAAAAAAABEAAf/aAAgBAgEBPxDYE3//xAAbEAEBAAIDAQAAAAAAAAAAAAABEQAxIVGBkf/aAAgBAQABPxCcxS6o93ggKqkQGXb1iRaCsRNWYNbVTlgkCZTPqLzECReTt8z/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/e243f8e9fbfdf654e14b55101be454d4/798b8/aws-ar-1.webp 195w, /static/e243f8e9fbfdf654e14b55101be454d4/0e356/aws-ar-1.webp 390w, /static/e243f8e9fbfdf654e14b55101be454d4/23bbb/aws-ar-1.webp 780w, /static/e243f8e9fbfdf654e14b55101be454d4/d0742/aws-ar-1.webp 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/e243f8e9fbfdf654e14b55101be454d4/0e421/aws-ar-1.jpg 195w, /static/e243f8e9fbfdf654e14b55101be454d4/ce2e0/aws-ar-1.jpg 390w, /static/e243f8e9fbfdf654e14b55101be454d4/485b7/aws-ar-1.jpg 780w, /static/e243f8e9fbfdf654e14b55101be454d4/7293b/aws-ar-1.jpg 900w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/e243f8e9fbfdf654e14b55101be454d4/485b7/aws-ar-1.jpg" alt="aws-renaissance" title="aws-renaissance" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </span> <i style="text-align: center; display: block; margin: 10px 0">Hôtel Renaissance : un joli cadre pour cette conf gratuite. Merci aux sponsors, Intel et <a href="https://twitter.com/premaccess" target="_blank" rel="noopener noreferrer">@premaccess</a> !</i></p> <blockquote> <p>🤫 D’ailleurs, on recrute des développeurs de tout horizon, n’hésitez pas à <a href="https://twitter.com/VinceOPS" target="_blank" rel="noopener noreferrer">me contacter</a> si vous cherchez un environnement (technique, pro…) qui vaut le détour</p> </blockquote> <hr /> <p>Un programme chargé que 4 speakers talentueux ont partagé entre : migration dans le cloud, CI/CD, Intelligence artificielle et <em>Machine Learning</em>, bases de données, IoT, sécurité… Slides disponibles <a href="https://aws.amazon.com/fr/events/acceleration-roadshow-2019/contenu/" target="_blank" rel="noopener noreferrer">ici</a>.</p> <h2>Pourquoi choisir de créer son infra dans le Cloud ?</h2> <p>C’est <a href="https://twitter.com/sebsto" target="_blank" rel="noopener noreferrer">Sébastien Stormacq</a>, <em>Developer Advocate</em>, qui ouvre le bal avec “Réussir sa transformation digitale”.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/cfb59b181922e5b9996bb3a407acfbe5/d03b0/aws-ar-4-pillar.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 40%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIE/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAHDJaoB/8QAGhAAAQUBAAAAAAAAAAAAAAAAAAECERITIv/aAAgBAQABBQJKHI/Of//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/Aar/xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFH/8QAFxABAQEBAAAAAAAAAAAAAAAAAAERIv/aAAgBAQAGPwKK51//xAAZEAACAwEAAAAAAAAAAAAAAAABEQAhMUH/2gAIAQEAAT8hBrlHYeBKdQNiKf/aAAwDAQACAAMAAAAQc9//xAAYEQEBAAMAAAAAAAAAAAAAAAABABExYf/aAAgBAwEBPxB5JydX/8QAFxEBAAMAAAAAAAAAAAAAAAAAAAFBYf/aAAgBAgEBPxC0af/EABwQAAIDAAMBAAAAAAAAAAAAAAERACExQWHR8P/aAAgBAQABPxAAJWHfXssqi24ggsaMnflP/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/cfb59b181922e5b9996bb3a407acfbe5/798b8/aws-ar-4-pillar.webp 195w, /static/cfb59b181922e5b9996bb3a407acfbe5/0e356/aws-ar-4-pillar.webp 390w, /static/cfb59b181922e5b9996bb3a407acfbe5/23bbb/aws-ar-4-pillar.webp 780w, /static/cfb59b181922e5b9996bb3a407acfbe5/b47fd/aws-ar-4-pillar.webp 1170w, /static/cfb59b181922e5b9996bb3a407acfbe5/a478c/aws-ar-4-pillar.webp 1285w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/cfb59b181922e5b9996bb3a407acfbe5/0e421/aws-ar-4-pillar.jpg 195w, /static/cfb59b181922e5b9996bb3a407acfbe5/ce2e0/aws-ar-4-pillar.jpg 390w, /static/cfb59b181922e5b9996bb3a407acfbe5/485b7/aws-ar-4-pillar.jpg 780w, /static/cfb59b181922e5b9996bb3a407acfbe5/faa76/aws-ar-4-pillar.jpg 1170w, /static/cfb59b181922e5b9996bb3a407acfbe5/d03b0/aws-ar-4-pillar.jpg 1285w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/cfb59b181922e5b9996bb3a407acfbe5/485b7/aws-ar-4-pillar.jpg" alt="4 piliers de la transformation" title="4 piliers de la transformation" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Slides disponibles <a href="https://awsmarketingbucket.s3-eu-west-1.amazonaws.com/2019/Acceleration%20Roadshow/Decks/Pr%C3%A9sentations%20sessions/Session%201%20-%20R%C3%A9ussir%20sa%20transformation%20digitale.pdf" target="_blank" rel="noopener noreferrer"><strong>ici</strong></a></p> <p>On retiendra, dans les grandes lignes :</p> <ul> <li>L’agilité et l’élasticité offertes par le Cloud : disponibilité immédiate des machines, dont le cycle de vie est maîtrisé</li> <li>Les nouveaux domaines technologiques mis au service des métiers : AI/ML/Data lake, IoT, …</li> <li>Le choix de l’emplacement des données entreposées, parmi 22 régions, en fonction de la latence et des aspects légaux</li> <li>Les certifications de l’infrastructure : PCI DSS, HDS, …</li> <li>Le coût total de l’infrastructure (TCO, <em>Total Cost of Ownership</em>) significativement réduit (locaux, équipements, machines, administrateurs, dépannages, planification…)</li> <li>L’autonomie offertes aux équipes (grâce à l’agilité/elasticité du Cloud)</li> <li>Le site <a href="https://www.aws.training/" target="_blank" rel="noopener noreferrer">AWS Training</a>, offrant des formations et certifications pour les services AWS.</li> </ul> <p>Pour l’anecdote, Amadou Merico présente dans “Migrer ses data-centers dans le cloud”, l’<a href="https://aws.amazon.com/fr/snowmobile/" target="_blank" rel="noopener noreferrer">AWS Snowmobile</a>, un container de 14 mètres tiré par un semi-remorque pouvant contenir jusqu’à 100 PB (100000 TB, <em>damn!</em>), spécialement dédié au transfert de données.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/2e6aec76ff57ec8a255fff3c20b9771e/e224a/snowmobile.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 40.51282051282051%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAIDBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHWiJaF/8QAGBAAAwEBAAAAAAAAAAAAAAAAAAESAhH/2gAIAQEAAQUCvpbpabP/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAaEAACAgMAAAAAAAAAAAAAAAAAEQECITKR/9oACAEBAAY/AsCVuGsn/8QAGRAAAwEBAQAAAAAAAAAAAAAAAREhAEFx/9oACAEBAAE/IS5PCjN1p9EYVRe7/9oADAMBAAIAAwAAABB8D//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABsQAQEAAgMBAAAAAAAAAAAAAAERADFBYXGh/9oACAEBAAE/EK4Rvdt46yROiGT6uUqYyAF+5//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/2e6aec76ff57ec8a255fff3c20b9771e/798b8/snowmobile.webp 195w, /static/2e6aec76ff57ec8a255fff3c20b9771e/0e356/snowmobile.webp 390w, /static/2e6aec76ff57ec8a255fff3c20b9771e/411c4/snowmobile.webp 600w" sizes="(max-width: 600px) 100vw, 600px" type="image/webp" /> <source srcset="/static/2e6aec76ff57ec8a255fff3c20b9771e/0e421/snowmobile.jpg 195w, /static/2e6aec76ff57ec8a255fff3c20b9771e/ce2e0/snowmobile.jpg 390w, /static/2e6aec76ff57ec8a255fff3c20b9771e/e224a/snowmobile.jpg 600w" sizes="(max-width: 600px) 100vw, 600px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/2e6aec76ff57ec8a255fff3c20b9771e/e224a/snowmobile.jpg" alt="AWS Snowmobile" title="AWS Snowmobile" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Il a aussi assuré une belle présentation sur les différentes base de données AWS, <a href="https://awsmarketingbucket.s3-eu-west-1.amazonaws.com/2019/Acceleration%20Roadshow/Decks/Pr%C3%A9sentations%20sessions/Session%205%20-%20Comment%20choisir%20sa%20base%20de%20donn%C3%A9es.pdf" target="_blank" rel="noopener noreferrer">Comment choisir sa base de données</a>.</p> <h2>Les services exploitant IA et ML</h2> <p>Les intervenants n’ont évidemment pas présenté les 165 services offerts par AWS, mais parmi ceux utilisant IA et ML, on retiendra :</p> <ul> <li><a href="https://aws.amazon.com/fr/rekognition/" target="_blank" rel="noopener noreferrer">AWS Rekognition</a> : Analyse intelligente de photos et de vidéos permettant l’identification de personnes, textes, objets… Ainsi qu’un redoutable outil de reconnaissance faciale.</li> <li><a href="https://aws.amazon.com/fr/polly/" target="_blank" rel="noopener noreferrer">AWS Polly</a> : Synthèse vocale intelligente, capable (grâce au <em>Machine learning</em>) de lire un texte en utilisant différentes intonations, en s’adaptant aux prononciations d’une langue spécifique.</li> <li><a href="https://aws.amazon.com/fr/textract/" target="_blank" rel="noopener noreferrer">AWS Textract</a> : Extraction de texte depuis des documents scannés, avec la capacité d’identifier des zones spécifiques (champs de formulaire, etc).</li> <li><a href="https://aws.amazon.com/fr/comprehend/" target="_blank" rel="noopener noreferrer">AWS Comprehend</a> : Traitement de langue naturelle permettant de déduire d’un texte les organismes cités, le sentiment général (positif, négatif, neutre…), la langue utilisée, etc.</li> <li><a href="https://aws.amazon.com/fr/translate/" target="_blank" rel="noopener noreferrer">AWS Translate</a> : Traduction sans langue intermédiaire (e.g. Français => Japonais directement, plutôt que Français => Anglais, puis Anglais => Japonais) basée sur des modèles de <em>deep learning</em>.</li> <li><a href="https://aws.amazon.com/fr/lex/" target="_blank" rel="noopener noreferrer">Amazon Lex</a> : Création d’interfaces de type <em>chatbot</em> (basé sur les mêmes modèles d’apprentissage qu’Alexa).</li> </ul> <h2>Et face à Google Cloud ?</h2> <p>Si la majorité des plateformes Cloud offrent les mêmes services fondamentaux, tels qu’<a href="https://aws.amazon.com/fr/ec2/" target="_blank" rel="noopener noreferrer">EC2</a>, <a href="https://aws.amazon.com/fr/rds/" target="_blank" rel="noopener noreferrer">RDS</a> et <a href="https://aws.amazon.com/fr/s3/" target="_blank" rel="noopener noreferrer">S3</a> (respectivement, dans <a href="https://cloud.google.com/products" target="_blank" rel="noopener noreferrer">Google Cloud</a> : <a href="https://cloud.google.com/compute" target="_blank" rel="noopener noreferrer">Compute Engine</a>, <a href="https://cloud.google.com/sql/" target="_blank" rel="noopener noreferrer">Cloud SQL</a> et <a href="https://cloud.google.com/storage" target="_blank" rel="noopener noreferrer">Cloud Storage</a>), certains services n’ont pas encore d’égal chez leurs rivaux.</p> <p>Un comparatif (certes proposé par Google 😁) <a href="https://cloud.google.com/docs/compare/aws" target="_blank" rel="noopener noreferrer">est disponible ici</a>. Celui-ci n’est pas exhaustif : en effet, nul équivalent à <a href="https://aws.amazon.com/fr/neptune/" target="_blank" rel="noopener noreferrer">AWS Neptune</a>, BDD orientée Graphe. Pas plus qu’à <a href="https://aws.amazon.com/fr/timestream/" target="_blank" rel="noopener noreferrer">AWS Timestream</a>, BDD chronologique (oui, on parle de besoins de niches 🤷). <strong>En revanche</strong>, Google Cloud offre une alternative à tous les services suscités (IA/ML). Et aussi, <a href="https://www.elastic.co/fr/blog/elasticsearch-service-now-available-on-google-cloud-marketplace" target="_blank" rel="noopener noreferrer"><strong>depuis hier</strong></a>, des instances <em>managées</em> d’Elasticsearch (comme <a href="https://aws.amazon.com/fr/elasticsearch-service/" target="_blank" rel="noopener noreferrer">AWS Elasticsearch Service</a>).</p> <img src="https://media.giphy.com/media/Na33dsU2umStO/giphy.gif" alt="omg-yes" style="display: block; margin: 0 auto 25px auto; width: 230px; height: 200px;" /> <p>Mais l’article n’a pas pour but de comparer les plateformes : cela mériterait un livre entier réédité tous les 6 mois, tant les facteurs à prendre en compte sont nombreux et sujets au changement. Richesse fonctionnelle des services, facilité d’utilisation, qualité des documentations, tarifications, <a href="https://fr.wikipedia.org/wiki/Service-level_agreement" target="_blank" rel="noopener noreferrer">SLA</a>, etc.</p> <h2>En conclusion</h2> <p>Les présentations étaient d’une grande qualité et méritent le coup d’œil (slides disponibles <a href="https://aws.amazon.com/fr/events/acceleration-roadshow-2019/contenu/" target="_blank" rel="noopener noreferrer"><strong>ici</strong></a>) si vous n’avez pas encore franchi le pas du Cloud, pour le déploiement d’applications comme pour l’utilisation de services : les premières utilisations (en heures, en stockage…) des services AWS sont souvent offertes.</p> <p><em>Premaccess organisera aussi un récap’ du AWS re:Invent 2019 à Marseille, le 19 décembre.</em></p></div>Git : Astuces et productivité #2https://vinceops.me/git-astuces-productivite-2/2019-09-18T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 279px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/c64f76aff2baa71f3261bcb2a79738df/3412f/git.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 43.07692307692307%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMCBAX/xAAUAQEAAAAAAAAAAAAAAAAAAAAC/9oADAMBAAIQAxAAAAHThfClDRH/xAAZEAACAwEAAAAAAAAAAAAAAAABAgMQEhP/2gAIAQEAAQUCYv0Z3MkesX//xAAWEQEBAQAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8BXZ//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAgEBPwGH/8QAGxAAAgEFAAAAAAAAAAAAAAAAAAECEiAxQbH/2gAIAQEABj8C4SitCrzZ/8QAHBAAAQQDAQAAAAAAAAAAAAAAAQARQXEgITFR/9oACAEBAAE/IdOBSCFw6IMwpTT2+H//2gAMAwEAAgADAAAAEPvP/8QAFxEBAAMAAAAAAAAAAAAAAAAAEQEQIf/aAAgBAwEBPxCMYFf/xAAWEQADAAAAAAAAAAAAAAAAAAABEGH/2gAIAQIBAT8QFL//xAAfEAEBAAECBwAAAAAAAAAAAAABEXEAISAxQWGBkaH/2gAIAQEAAT8QG+sJZF+Y0KbQHNBpt7ufetroSON3xwf/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/c64f76aff2baa71f3261bcb2a79738df/798b8/git.webp 195w, /static/c64f76aff2baa71f3261bcb2a79738df/99323/git.webp 279w" sizes="(max-width: 279px) 100vw, 279px" type="image/webp" /> <source srcset="/static/c64f76aff2baa71f3261bcb2a79738df/0e421/git.jpg 195w, /static/c64f76aff2baa71f3261bcb2a79738df/3412f/git.jpg 279w" sizes="(max-width: 279px) 100vw, 279px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/c64f76aff2baa71f3261bcb2a79738df/3412f/git.jpg" alt="git" title="git" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Dans ce second article de la série, je parle d’alias, de <code class="language-text">git add -p</code> (patch) et de l’intégration de <a href="https://git-scm.com/" target="_blank" rel="noopener noreferrer">git</a> dans <a href="https://code.visualstudio.com/" target="_blank" rel="noopener noreferrer">VS Code</a>.</p> <hr /> <h2>Alias de commandes</h2> <p>Ce n’est pas grand chose, car on parle de quelques secondes grattées par-ci par-là. Mais grattées jusqu’à plusieurs dizaines de fois par jour, ça compte comme de la productivité 😏 !</p> <img src="https://media.giphy.com/media/5isTFajX8zbcA/giphy.gif" alt="cat-scratching-oklm" style="display: block; margin: 0 auto 25px auto; width: 230px; height: 200px;" /> <p>Au quotidien, j’utilise le Z shell aka <strong>zsh</strong>, avec <a href="https://ohmyz.sh/" target="_blank" rel="noopener noreferrer">oh-my-zsh</a>. L’idée n’est pas de vous présenter mon thème ou mon <em>setup</em> (Debian 9 avec <a href="https://i3wm.org/" target="_blank" rel="noopener noreferrer">i3wm</a>), mais de partager quelques alias <strong>très</strong> pratiques importés par le <strong>plugin git</strong> de oh-my-zsh. Il est activé par défaut. Vous pouvez aussi les récupérer directement <a href="https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/git/git.plugin.zsh" target="_blank" rel="noopener noreferrer">ici</a> et les intégrer à votre fichier <em>run commands</em> (<code class="language-text">.bashrc</code>, <code class="language-text">.zshrc</code>…).</p> <p>Parmi ceux que j’utilise souvent :</p> <ul> <li><code class="language-text">g</code> : <code class="language-text">git</code></li> <li><code class="language-text">ga</code> : <code class="language-text">git add</code> (<code class="language-text">gaa</code> pour <code class="language-text">git add --all</code>)</li> <li><code class="language-text">gcb</code> : <code class="language-text">git checkout -b</code></li> <li><code class="language-text">gcm</code> : <code class="language-text">git checkout master</code></li> <li><code class="language-text">gc</code> : <code class="language-text">git commit</code></li> <li><code class="language-text">gcn!</code> : <code class="language-text">git commit --no-edit --amend</code></li> <li><code class="language-text">gd</code> : <code class="language-text">git diff</code></li> <li><code class="language-text">ggpur</code> : <code class="language-text">git pull --rebase origin <current_branch></code></li> <li><code class="language-text">gfa</code> : <code class="language-text">git fetch --all --prune</code></li> <li><code class="language-text">gpf</code> : <code class="language-text">git push --force--with-lease</code> (<code class="language-text">gpf!</code> pour <code class="language-text">--force</code>)</li> <li><code class="language-text">grb</code> : <code class="language-text">git rebase</code> (<code class="language-text">grba</code> pour <code class="language-text">--abort</code>, <code class="language-text">grbc</code> pour <code class="language-text">--continue</code>, <code class="language-text">grbi</code> pour <code class="language-text">-i</code>) 💖</li> <li><code class="language-text">groh</code> : <code class="language-text">git reset --hard origin/<current_branch></code></li> <li><code class="language-text">gst</code> : <code class="language-text">git status</code></li> </ul> <p>Évidemment, on les mémorise à force de réutilisation.</p> <p>Au passage, oh-my-zsh a aussi un <strong>plugin yarn</strong> avec <a href="https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/yarn/yarn.plugin.zsh" target="_blank" rel="noopener noreferrer">quelques alias</a> tous aussi efficaces :</p> <ul> <li><code class="language-text">y</code> : <code class="language-text">yarn</code></li> <li><code class="language-text">ya</code> : <code class="language-text">yarn add</code> (<code class="language-text">yad</code> pour <code class="language-text">--dev</code>)</li> <li><code class="language-text">yb</code> : <code class="language-text">yarn build</code></li> <li><code class="language-text">yrm</code> : <code class="language-text">yarn remove</code></li> <li><code class="language-text">yst</code> : <code class="language-text">yarn start</code></li> <li><code class="language-text">yt</code> : <code class="language-text">yarn test</code> 🔥</li> </ul> <hr /> <p>Et maintenant, parlons de trucs utiles !</p> <h2>Indexation partielle avec <code class="language-text">git add -p</code></h2> <p>C’est une option présentée dans le mode interactif (<code class="language-text">-i</code>) de <code class="language-text">git add</code> (cf. le ”<a href="https://git-scm.com/book/en/v2" target="_blank" rel="noopener noreferrer">Book</a>” 📚 !) qui permet l’indexation (<em>staging</em>) <strong>partielle</strong> d’un fichier.</p> <h3>Exemple <em>“True story”</em></h3> <p>Vous travaillez sur une évolution de votre produit en suivant votre cycle <em>Red/Green/Refacto</em> habituel. Et là, c’est le drame. Vous trouvez un bout de code <strong>“legacy”</strong> (<em>“oh my god, he said the L word! 😱”</em>) que vous savez refacto par cœur.</p> <blockquote> <p>- 👼 : “Fais d’abord passer ton test. Cette refacto peut attendre.”<br /> - 👿 : “C’est quoi ce code ?! Allez nettoie, y en a pour 20 secondes !”</p> </blockquote> <p>Quoi qu’il arrive, le fichier va subir deux modifications, une pour l’évolution, une pour le <em>refactoring</em>.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/45e0a85c3efb2d41a91adbc024ec2402/5fbd7/depressed-developer-refactoring.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 48.71794871794871%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsSAAALEgHS3X78AAABvUlEQVQozyWS2cuxYRDGnz8V+R8cOHKgJJSirAekpJR9C8mSE7zKvu9lSei1v8L3+5iDeea5Z65rrrnnFiqVSrfbbTabjUaj0+m0221+e73eYDCoVquJRILDer3earXwtVqNMgI8sZDL5RaLBbnz+fx+v5/P536/3+12+Pl87vF4KCgUCtlstlwu5/N5uH5+fiKRyGg0EtLpNBx0puHxeARMbrlcHg4H6KPRKCeXy+V0Ol2v1/V6vdlsbrcb8e/vrwCSn/v9TufH4wHGaDSmUinSq9UK8FfO6/UiQKNSqYzH418uAcF/HyOH93q9Op3OZrNBCjgWi33PUT6ZTEKhkEqlslqtqEOOwAe19KRoNpuZzWafz+f3+91u93g8DofDaKEOsPVjgUCArMvlQv//mdG23W4BM4bJZNLr9QaDwel0TqdTishyBWShgFetVpN1OBx0FSwWC3ylUgkkTVAlkUikUimzJZNJvN1uZ52kmFOr1YpEIrFYDEUwGBTgSH6M9dAfsEwmY2washiFQpH4GPdMfzByuVyj0RSLRbYtsL1MJvNdJkffnfFCuFh862PUsdXhcNjv98kS8AS4v39dOt1bhuWg3AAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/45e0a85c3efb2d41a91adbc024ec2402/798b8/depressed-developer-refactoring.webp 195w, /static/45e0a85c3efb2d41a91adbc024ec2402/0e356/depressed-developer-refactoring.webp 390w, /static/45e0a85c3efb2d41a91adbc024ec2402/23bbb/depressed-developer-refactoring.webp 780w, /static/45e0a85c3efb2d41a91adbc024ec2402/b47fd/depressed-developer-refactoring.webp 1170w, /static/45e0a85c3efb2d41a91adbc024ec2402/7974c/depressed-developer-refactoring.webp 1276w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/45e0a85c3efb2d41a91adbc024ec2402/25fa3/depressed-developer-refactoring.png 195w, /static/45e0a85c3efb2d41a91adbc024ec2402/506f3/depressed-developer-refactoring.png 390w, /static/45e0a85c3efb2d41a91adbc024ec2402/8a72f/depressed-developer-refactoring.png 780w, /static/45e0a85c3efb2d41a91adbc024ec2402/77666/depressed-developer-refactoring.png 1170w, /static/45e0a85c3efb2d41a91adbc024ec2402/5fbd7/depressed-developer-refactoring.png 1276w" sizes="(max-width: 780px) 100vw, 780px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/45e0a85c3efb2d41a91adbc024ec2402/8a72f/depressed-developer-refactoring.png" alt="REFACTOR ME" title="REFACTOR ME" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Plusieurs options s’offrent à vous, dont :</p> <ul> <li>Pousser tous les changements dans un seul et même commit (<strong>DON’T</strong>, les <a href="https://vinceops.me/git-astuces-productivite-1">commits atomiques</a>, tout ça…)</li> <li>Remiser (<code class="language-text">git stash</code>) les changements, faire et enregistrer le <em>refactoring</em> dans un <em>commit</em> dédié, reprendre les changements remisés (<code class="language-text">git stash pop</code>)</li> <li>Avoir recours à <strong><code class="language-text">git add -p</code></strong> pour indéxer et <em>commit</em> <strong>seulement</strong> le <em>refactoring</em> 🤷</li> </ul> <p><em>Pour traiter un cas simple, les exemples qui suivent montrent l’ajout d’un test unitaire 🙄.</em></p> <h3>Via la CLI</h3> <p>Vous devez faire évoluer le code d’un collègue fan de <em>retrospective testing</em> (ou “j’écris mes tests une fois que je crois avoir fini”) et vous constatez qu’un cas d’usage n’a pas été couvert. Vous écrivez le test qui va bien et décidez de l’indexer immédiatement :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">$ <span class="token function">git</span> <span class="token function">add</span> -p <span class="token function">diff</span> --git a/src/input-parsing/input-reporting.service.spec.ts b/src/input-parsing/input-reporting.service.spec.ts index 10e5cc8<span class="token punctuation">..</span>b9cf134 <span class="token number">100644</span> --- a/src/input-parsing/input-reporting.service.spec.ts +++ b/src/input-parsing/input-reporting.service.spec.ts</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Pour chaque fichier modifié, ici <code class="language-text">src/input-parsing/input-reporting.service.spec.ts</code>, le terminal affiche les changements indexables :</p> <div class="gatsby-highlight" data-language="diff"><pre style="counter-reset: linenumber NaN" class="language-diff line-numbers"><code class="language-diff">@@ -10,6 +10,11 @@ describe('InputReportingService', () => { <span class="token unchanged"> const separatorRow = `🤸;🏋;🚴;🚣;🏊;`; const emptyRow = `;;;;;`; </span> <span class="token inserted-sign inserted">+ it('writes any new file starting with a "separator"', () => { + const result = service.generateSynthesis([]); + expect(result.startsWith(separatorRow)).toEqual(true); + }); + </span><span class="token unchanged"> it('adds an empty row after a new entry', () => { const result = service.generateSynthesis([]); expect(result.startsWith(separatorRow)).toEqual(true);</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 Voici du <em>bullshit</em> code : totalement factice, à usage purement démonstratif. <em>Et oui Jamy !</em></p> </blockquote> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">Stage this hunk <span class="token punctuation">[</span>y,n,q,a,d,e,?<span class="token punctuation">]</span>? ?</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Différentes options s’offrent à vous pour le traitement de ce ”<em>hunk</em>” (morceau) :</p> <ul> <li>y - Indexer le morceau présenté</li> <li>n - Ignorer ce morceau et passer au suivant</li> <li>q - Abandonner / quitter ce mode interactif</li> <li>a - Indexer ce morceau et tous ceux du fichier courant</li> <li>d - Ignorer ce morceau et passer au fichier suivant</li> <li>e - Modifier manuellement le morceau courant (pour les ajouts !)</li> </ul> <blockquote> <p>💡 Sur des plus gros <em>hunks</em>, quelques autres options sont disponibles :</p> <ul> <li>g - Sélectionner un morceau dans le fichier courant</li> <li>/ - Chercher un morceau en utilisant une Regex</li> <li>s - Proposer à nouveau ces options avec un morceau plus petit (découpé en plusieurs morceaux)</li> <li>j - Marquer ce morceau comme “indécis” et sélectionner le prochain morceau indécis</li> <li>J - Marquer ce morceau comme “indécis” et sélectionner le morceau suivant</li> </ul> </blockquote> <p>Dans le cas d’un fichier ayant été modifié à deux endroits, on peut donc <strong>indexer un premier morceau</strong>, enregistrer un <strong>commit</strong>, puis à nouveau <strong>indexer le deuxième morceau</strong> et enregistrer un autre <strong>commit</strong>.<br /> Comme argumenté dans le <a href="http://localhost:8000/git-astuces-productivite-1" target="_blank" rel="noopener noreferrer">premier article de cette série</a> : un historique plus clair, un <em>versionning</em> plus granulaire et des <em>reviewers</em> plus heureux !</p> <h3>Dans VS Code</h3> <p>L’éditeur offre une interface très pratique pour l’indexation de <em>hunks</em> :</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/039ac746c2a4fd6836a1ae67e93d5b57/c19a6/git_add_patch_vscode.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsTAAALEwEAmpwYAAABj0lEQVQY0y3OS0sbUQAF4Pkxkfpo7Nzcx8y9dx6XRCY6j5iMySBJW7OJxkmiMT5CQEpREf1HgqB7QReCQaGrFulCFMVSW9HoED18i8NZHYkyLcI0XaEMIoIIQbgPYfgGvy+Ua7ZtLzcbrcVGp726VVuQkEIIVSOAkISqyATLhAAFyxjJBAGMEgh+AjKAiQ+DA8FM5aB7vXfyc//092H3RmKaKpI6Z5QxqgvBOSWMEpNjTiFTEVOxQgAEEMOh4cFyZf7s193R+eXRxeXxjysJIaJowkylzZTFqMpI9ACpOgcAfBwZifczGo/LshyLxear1d7z/9ubq/vb6+enR2lsgqUzws2NuZNJ2zVsz3BczcsYjqe7nm473LKZ5ehpR+ditLO+1Ou93N3/+fvwLypSObRmlzKzLb/SmpprZWtrfq2dD9tBuFaor2bnml654ZZr6ZnQ+lJNhSv+xm7z23b9+05jc7sj+UURfE0GJRF8FoWSGckXoyLyRdOPlERu2shOa32GF7DxLJzIIXsKpSzlFUu1klyWTKOHAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/039ac746c2a4fd6836a1ae67e93d5b57/798b8/git_add_patch_vscode.webp 195w, /static/039ac746c2a4fd6836a1ae67e93d5b57/0e356/git_add_patch_vscode.webp 390w, /static/039ac746c2a4fd6836a1ae67e93d5b57/23bbb/git_add_patch_vscode.webp 780w, /static/039ac746c2a4fd6836a1ae67e93d5b57/179a7/git_add_patch_vscode.webp 959w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/039ac746c2a4fd6836a1ae67e93d5b57/25fa3/git_add_patch_vscode.png 195w, /static/039ac746c2a4fd6836a1ae67e93d5b57/506f3/git_add_patch_vscode.png 390w, /static/039ac746c2a4fd6836a1ae67e93d5b57/8a72f/git_add_patch_vscode.png 780w, /static/039ac746c2a4fd6836a1ae67e93d5b57/c19a6/git_add_patch_vscode.png 959w" sizes="(max-width: 780px) 100vw, 780px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/039ac746c2a4fd6836a1ae67e93d5b57/8a72f/git_add_patch_vscode.png" alt="Stage a Hunk with VS Code" title="Stage a Hunk with VS Code" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>En ouvrant un fichier depuis l’onglet <strong>Source Control</strong>, on peut sélectionner des morceaux (changés, ajoutés, ou supprimés) et les indexer, ou les désindexer. Je préfère cette approche visuelle, que je trouve plus efficace pour les besoins les plus communs. C’est d’ailleurs une habitude bien ancrée : j’alterne en permanence entre les terminaux et l’éditeur, en fonction de ce qui me proccure la meilleure vélocité 🚀.</p> <h2>VS Code + Git = 💕</h2> <p>En ce qui concerne VS Code, il y a quelques “trucs” qu’il faut absolument connaître :</p> <h3>Éditeur</h3> <p>Vous pouvez définir VS Code comme éditeur pour git.<br /> En effet, après avoir vérifié que <code class="language-text">code</code> était disponible dans votre <code class="language-text">PATH</code>, lancez la commande <code class="language-text">git config --global core.editor "code --wait"</code>.<br /> La modification de la configuration Git, les rebasages intéractifs, etc, peuvent ainsi être effectués via VS Code :</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 625px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/60dd27ff6cd09b3321a46740b2da3063/0f1e4/vscode_git_editor.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 23.076923076923077%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAFABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABzwQH/8QAFhAAAwAAAAAAAAAAAAAAAAAAABAR/9oACAEBAAEFAlD/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAY/An//xAAYEAACAwAAAAAAAAAAAAAAAAAAARAxcf/aAAgBAQABPyFVGj//2gAMAwEAAgADAAAAEPPP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGRABAAMBAQAAAAAAAAAAAAAAAQARMSFx/9oACAEBAAE/EOiq37KLyKXU/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/60dd27ff6cd09b3321a46740b2da3063/798b8/vscode_git_editor.webp 195w, /static/60dd27ff6cd09b3321a46740b2da3063/0e356/vscode_git_editor.webp 390w, /static/60dd27ff6cd09b3321a46740b2da3063/529ab/vscode_git_editor.webp 625w" sizes="(max-width: 625px) 100vw, 625px" type="image/webp" /> <source srcset="/static/60dd27ff6cd09b3321a46740b2da3063/0e421/vscode_git_editor.jpg 195w, /static/60dd27ff6cd09b3321a46740b2da3063/ce2e0/vscode_git_editor.jpg 390w, /static/60dd27ff6cd09b3321a46740b2da3063/0f1e4/vscode_git_editor.jpg 625w" sizes="(max-width: 625px) 100vw, 625px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/60dd27ff6cd09b3321a46740b2da3063/0f1e4/vscode_git_editor.jpg" alt="VSCode comme Éditeur git" title="VSCode comme Éditeur git" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Ici, il est question d’être efficace : je préfère me retrouver sur VSCode que sur un éditeur <em><code class="language-text">vi</code>-like</em> pour ce genre d’opérations. C’est personnel.</p> <img src="https://media.giphy.com/media/RDmVxCrAp7kti/giphy.gif" style="display: block; margin: 0 auto 25px auto; width: 250px; height: 250px;" /> <h3>GitLens</h3> <p>Extension absolument incontournable de VS Code, <a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens" target="_blank" rel="noopener noreferrer">GitLens</a> offre entre autres :</p> <ul> <li>L’affichage du <code class="language-text">git blame</code> (auteur/date de la dernière modification) de la ligne courante</li> </ul> <div class="gatsby-highlight has-highlighted-lines" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="gatsby-highlight-code-line"><span class="token keyword">const</span> weekMoment <span class="token operator">=</span> <span class="token function">moment</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// VinceOPS, a month ago • feat: a week as a period</span></span> <span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span><span class="token string">"week"</span><span class="token punctuation">,</span> weekNumber<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span><span class="token string">"year"</span><span class="token punctuation">,</span> year<span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span></span></pre></div> <ul> <li>La comparaison, pour le fichier courant, de ses révisions N et N-1 (puis N-1 et N-2, et ainsi de suite)…</li> </ul> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/9d38c477337abf5bb481e79c7e12953d/8b5d7/vscode_gitlens_diff.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 21.538461538461537%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAEABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAHLA0Wf/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAFRABAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQEAAT8hr//aAAwDAQACAAMAAAAQe/8A/8QAFhEAAwAAAAAAAAAAAAAAAAAAARAx/9oACAEDAQE/EDF//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGRAAAwEBAQAAAAAAAAAAAAAAAAERcUGB/9oACAEBAAE/EKr0Tws4n4f/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/9d38c477337abf5bb481e79c7e12953d/798b8/vscode_gitlens_diff.webp 195w, /static/9d38c477337abf5bb481e79c7e12953d/0e356/vscode_gitlens_diff.webp 390w, /static/9d38c477337abf5bb481e79c7e12953d/23bbb/vscode_gitlens_diff.webp 780w, /static/9d38c477337abf5bb481e79c7e12953d/e7a10/vscode_gitlens_diff.webp 1044w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/9d38c477337abf5bb481e79c7e12953d/0e421/vscode_gitlens_diff.jpg 195w, /static/9d38c477337abf5bb481e79c7e12953d/ce2e0/vscode_gitlens_diff.jpg 390w, /static/9d38c477337abf5bb481e79c7e12953d/485b7/vscode_gitlens_diff.jpg 780w, /static/9d38c477337abf5bb481e79c7e12953d/8b5d7/vscode_gitlens_diff.jpg 1044w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/9d38c477337abf5bb481e79c7e12953d/485b7/vscode_gitlens_diff.jpg" alt="VSCode & GitLens - Diff" title="VSCode & GitLens - Diff" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Au quotidien, ce sont ces deux premières fonctionnalités qui me sont les plus utiles.</p> <ul> <li> <p>Dans un onglet dédié de la <em>side bar</em> :</p> <ul> <li>L’historique des commits de la branche courante, avec affichage (<code class="language-text">diff</code>) de chaque modification</li> <li>La navigation dans les différentes branches du dépôt</li> <li>La liste des différentes remises (<em>stash</em>) du dépôt</li> <li>La recherche de commits par auteur, message, SHA1, fichiers modifiés…</li> <li>Plein de trucs que l’on peut faire plus vite depuis un terminal (créer/supprimer/<em>merge</em> une branche, etc)</li> </ul> </li> </ul> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 554px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/722bad6c9cb7ecfe5473a222233c7067/b8007/vscode_gitlens_sidebar.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 177.43589743589743%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAjABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIBBP/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHhnYNSKnMTUioFA//EABQQAQAAAAAAAAAAAAAAAAAAADD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BX//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8BX//EABcQAAMBAAAAAAAAAAAAAAAAAAAgMTD/2gAIAQEABj8CxhF//8QAGxAAAgIDAQAAAAAAAAAAAAAAACEBERBBUWH/2gAIAQEAAT8hW4EXHCbZfmLRM4foYfDc5//aAAwDAQACAAMAAAAQE8LPCC//xAAWEQEBAQAAAAAAAAAAAAAAAAARACD/2gAIAQMBAT8QIz//xAAWEQEBAQAAAAAAAAAAAAAAAAARACD/2gAIAQIBAT8QZz//xAAhEAEAAgEEAQUAAAAAAAAAAAABABFRITFBYRBxgcHw8f/aAAgBAQABPxCkNDT8RpwekYkBXZeOor8SnDDU1N+YFyzMbWlGaj0HtPtUSjOo7vj/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/722bad6c9cb7ecfe5473a222233c7067/798b8/vscode_gitlens_sidebar.webp 195w, /static/722bad6c9cb7ecfe5473a222233c7067/0e356/vscode_gitlens_sidebar.webp 390w, /static/722bad6c9cb7ecfe5473a222233c7067/78631/vscode_gitlens_sidebar.webp 554w" sizes="(max-width: 554px) 100vw, 554px" type="image/webp" /> <source srcset="/static/722bad6c9cb7ecfe5473a222233c7067/0e421/vscode_gitlens_sidebar.jpg 195w, /static/722bad6c9cb7ecfe5473a222233c7067/ce2e0/vscode_gitlens_sidebar.jpg 390w, /static/722bad6c9cb7ecfe5473a222233c7067/b8007/vscode_gitlens_sidebar.jpg 554w" sizes="(max-width: 554px) 100vw, 554px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/722bad6c9cb7ecfe5473a222233c7067/b8007/vscode_gitlens_sidebar.jpg" alt="VSCode & GitLens - Diff" title="VSCode & GitLens - Diff" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>L’historique des commits d’une branche est très utile pour de la <strong>code review</strong>, ou pour se remémorer ses derniers travaux 😁. Consultez la <a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens" target="_blank" rel="noopener noreferrer">liste exhaustive</a> des fonctionnalités, vous dénicherez peut-être <strong>votre</strong> pépite.</p> <p>C’est tout pour ce second article. <code class="language-text">#teaser</code> Dans le prochain, on parlera de <code class="language-text">amend</code>, de <code class="language-text">rebase</code> (avec <code class="language-text">squash</code> et <code class="language-text">fixup</code>), ainsi que du <code class="language-text">reflog</code>.</p></div>Rust : Premiers retours sur le langagehttps://vinceops.me/rust-premiers-retours/2019-08-26T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/b58d88e470b99d8602b0c704933ca334/a22ce/rustart.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAIDBP/EABYBAQEBAAAAAAAAAAAAAAAAAAMBBP/aAAwDAQACEAMQAAABukU9Qxv/AP/EABgQAAMBAQAAAAAAAAAAAAAAAAECEQAD/9oACAEBAAEFAo7Et1Bm/8QAGBEAAwEBAAAAAAAAAAAAAAAAAAERBCL/2gAIAQMBAT8BzdOsiP/EABoRAAICAwAAAAAAAAAAAAAAAAABAgMREiH/2gAIAQIBAT8BvnjiNmf/xAAZEAACAwEAAAAAAAAAAAAAAAAAARIxQVH/2gAIAQEABj8CTlRjOH//xAAaEAACAgMAAAAAAAAAAAAAAAAAAREhMUGR/9oACAEBAAE/Ib3TiUHihTUuz0f/2gAMAwEAAgADAAAAEAP/AP/EABkRAAIDAQAAAAAAAAAAAAAAAAABESFBYf/aAAgBAwEBPxBZrYzDkj//xAAXEQADAQAAAAAAAAAAAAAAAAAAARFh/9oACAECAQE/EGSiU3Z//8QAGhABAQACAwAAAAAAAAAAAAAAAREAITFBgf/aAAgBAQABPxAmwqzALzrNyA9oeYpaBUAxz//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/b58d88e470b99d8602b0c704933ca334/798b8/rustart.webp 195w, /static/b58d88e470b99d8602b0c704933ca334/0e356/rustart.webp 390w, /static/b58d88e470b99d8602b0c704933ca334/4c79d/rustart.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/b58d88e470b99d8602b0c704933ca334/0e421/rustart.jpg 195w, /static/b58d88e470b99d8602b0c704933ca334/ce2e0/rustart.jpg 390w, /static/b58d88e470b99d8602b0c704933ca334/a22ce/rustart.jpg 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/b58d88e470b99d8602b0c704933ca334/a22ce/rustart.jpg" alt="rust-starting" title="rust-starting" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Quand je n’écris pas en <a href="https://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">TypeScript</a>, je m’intéresse à <a href="https://www.rust-lang.org/" target="_blank" rel="noopener noreferrer"><strong>Rust</strong></a>, langage de programmation système au typage fort et statique créé par <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Rust" target="_blank" rel="noopener noreferrer">Mozilla</a>. Entre autres, par nostalgie pour la prog. système <strong>et</strong> surtout pour son interopérabilité <a href="https://www.rust-lang.org/what/wasm" target="_blank" rel="noopener noreferrer">optimisée</a> avec <a href="https://rustwasm.github.io/docs/book/" target="_blank" rel="noopener noreferrer"><strong>WebAssembly</strong></a>. À ce sujet, j’ai d’abord étudié <a href="https://github.com/AssemblyScript/assemblyscript" target="_blank" rel="noopener noreferrer">AssemblyScript</a> (compilation TypeScript vers WebAssembly) mais celui-ci n’est pas une option viable <a href="https://docs.assemblyscript.org/faq#how-does-assemblyscript-compare-relate-to-c-rust" target="_blank" rel="noopener noreferrer">pour le moment</a>.</p> <p><strong>Dans cet article, je partage <em>quelques</em> éléments notables et mes premières impressions sur Rust et son écosystème</strong>, en tant que développeur travaillant avec Node.js et TypeScript depuis plus de 4 ans (bien qu’ayant aussi travaillé avec PHP, C, Java, C# et d’autres).</p> <p><strong>TL;DR</strong>: Si vous connaissez déjà bien Rust, vous n’apprendrez probablement rien dans cet article. Je ne suis moi-même pas encore un expert du sujet.</p> <blockquote> <p>👮 <strong>Disclaimer</strong> : Un minimum de connaissances en POO et en allocation mémoire est requis pour certains passages de l’article.</p> </blockquote> <h2>Installation et “Hello World”</h2> <p>L’installation est très simple :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash"><span class="token function">curl</span> https://sh.rustup.rs -sSf <span class="token operator">|</span> <span class="token function">sh</span> <span class="token comment"># Pour une mise à jour : `rustup update`</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>Vous disposez ensuite de <a href="https://doc.rust-lang.org/cargo/" target="_blank" rel="noopener noreferrer">Cargo</a>, le <strong>gestionnaire de <a href="https://crates.io" target="_blank" rel="noopener noreferrer">paquets</a> officiel</strong> du langage (<code class="language-text">cargo <update | search | ...></code>).<br /> Il permet aussi de :</p> <ul> <li>Créer un projet : <code class="language-text">cargo new</code></li> <li>Compiler le projet : <code class="language-text">cargo build</code></li> <li>Générer la documentation du projet : <code class="language-text">cargo doc</code></li> <li>Lancer l’exécutable du projet : <code class="language-text">cargo run</code></li> <li>Lancer les tests : <code class="language-text">cargo test</code></li> <li><em>Linter</em> le code : <code class="language-text">cargo clippy</code> (<a href="https://github.com/rust-lang/rust-clippy" target="_blank" rel="noopener noreferrer">clippy</a>)</li> <li>Formatter le code <code class="language-text">cargo fmt</code> (<a href="https://github.com/rust-lang/rustfmt" target="_blank" rel="noopener noreferrer">rustfmt</a>)</li> <li><a href="https://crates.io/search?q=cargo" target="_blank" rel="noopener noreferrer">etc</a>, etc</li> </ul> <blockquote> <p>💡 La plupart de ces tâches sont exécutées par d’autres programmes. Cargo est leur interface commune.</p> </blockquote> <p>On crée un nouveau projet :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">cargo new my_project</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Le projet ainsi généré contient : un dépôt <strong>git</strong> et son <code class="language-text">.gitignore</code>, <code class="language-text">Cargo.toml</code> (équivalent au <code class="language-text">package.json</code>), <code class="language-text">Cargo.lock</code> (équivalent au <code class="language-text">package-lock.json</code> / <code class="language-text">yarn.lock</code>), ainsi qu’un premier <em>Hello World</em> dans <code class="language-text">src/main.rs</code>.</p> <p>Et c’est <em>déjà</em> prêt ! L’installation est simplissime et fournit un outil standard effectuant toutes les tâches usuelles du développement : 🎉. Quant au <strong>Hello world</strong>, on le compile et l’exécute avec <code class="language-text">cargo run</code> :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token comment">// src/main.rs</span> <span class="token keyword">fn</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"Hello world"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <div class="gatsby-highlight" data-language="language-none"><pre style="counter-reset: linenumber NaN" class="language-language-none line-numbers"><code class="language-language-none">$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.06s Running `target/debug/my_project` Hello world</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <hr /> <h2>Remarques et impressions</h2> <h3>Documentations</h3> <p>La <a href="https://doc.rust-lang.org/stable/book" target="_blank" rel="noopener noreferrer">documentation du langage</a> (“The Book”) est très bien construite, avec un niveau de détails parfaitement jaugé. On peut y accéder, même hors-ligne, en exécutant la commande <code class="language-text">rustup doc --book</code>.</p> <p>J’en profite pour citer l’excellent <a href="https://doc.rust-lang.org/rust-by-example" target="_blank" rel="noopener noreferrer">Rust by Example</a> et les <a href="https://rust-lang-nursery.github.io/api-guidelines/about.html" target="_blank" rel="noopener noreferrer">Rust API Guidelines</a>, sans oublier <a href="https://doc.rust-lang.org/reference" target="_blank" rel="noopener noreferrer">The Rust Reference</a>.</p> <h3>Intégration avec VS Code</h3> <p>L’équipe en charge du <a href="https://github.com/rust-lang/rls" target="_blank" rel="noopener noreferrer">Rust Language Server</a> (comparable au <a href="https://github.com/Microsoft/TypeScript/wiki/Standalone-Server-(tsserver)" target="_blank" rel="noopener noreferrer">TSserver</a>) a développé un client VS Code : le plugin officiel <a href="https://marketplace.visualstudio.com/items?itemName=rust-lang.rust" target="_blank" rel="noopener noreferrer">Rust</a>. Il apporte une intégration complète, quoiqu’encore un peu lente : coloration, autocompletion, documentation, lint, etc.</p> <p>Ces plugins, bien qu’optionnels, peuvent s’avérer utiles :</p> <ul> <li><a href="https://marketplace.visualstudio.com/items?itemName=bungcip.better-toml" target="_blank" rel="noopener noreferrer">Better TOML</a> pour un meilleur support des fichiers TOML (<code class="language-text">Cargo.toml</code>).</li> <li><a href="https://marketplace.visualstudio.com/items?itemName=serayuzgur.crates" target="_blank" rel="noopener noreferrer">crates</a>, qui fournit une interface pour la gestion des versions des crates dans <code class="language-text">Cargo.toml</code>.</li> </ul> <h3>Cargo et les Crates</h3> <p>Les packages, en Rust, sont composés d’un à plusieurs <strong>crates</strong> (bibliothèque ou exécutable). Le registre officiel, <a href="https://crates.io/" target="_blank" rel="noopener noreferrer">crates.io</a> en contient à ce jour plus de 29 000, dont certains fournis par l’équipe officielle, comme <a href="https://crates.io/crates/rand" target="_blank" rel="noopener noreferrer">rand</a>, <a href="https://crates.io/crates/time" target="_blank" rel="noopener noreferrer">time</a> et <a href="https://crates.io/users/alexcrichton" target="_blank" rel="noopener noreferrer">beaucoup</a> d’<a href="https://crates.io/teams/github:rust-lang-nursery:libs" target="_blank" rel="noopener noreferrer">autres</a>.</p> <p>Pour ajouter une dépendance au projet, on insère (manuellement) son nom et sa version aux <code class="language-text">[dependencies]</code> de <code class="language-text">Cargo.toml</code> :</p> <div class="gatsby-highlight" data-language="toml"><pre style="counter-reset: linenumber NaN" class="language-toml line-numbers"><code class="language-toml"><span class="token punctuation">[</span><span class="token table class-name">dependencies</span><span class="token punctuation">]</span> <span class="token key property">rand</span> <span class="token punctuation">=</span> <span class="token string">"0.7.0"</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>Chaque paquet est ensuite installé en exécutant <code class="language-text">cargo build</code>, qui met aussi à jour <code class="language-text">Cargo.lock</code>.</p> <blockquote> <p>💡 Pour installer une dépendance en passant par la CLI, il est nécessaire d’utiliser le <a href="https://github.com/killercup/cargo-edit" target="_blank" rel="noopener noreferrer">crate <code class="language-text">cargo-edit</code></a>. Son intégration officielle est supposément <a href="https://github.com/rust-lang/cargo/issues/5586" target="_blank" rel="noopener noreferrer">en cours</a>.</p> </blockquote> <h3>Quelques éléments notables du langage</h3> <p>Comme annoncé en introduction, Rust est fortement <strong>et</strong> statiquement typé. Il offre une inférence (déduction) de typage <a href="https://doc.rust-lang.org/stable/rust-by-example/types/inference.html" target="_blank" rel="noopener noreferrer">très efficace</a>. Les trois axes mis en avant par ses créateurs sont la <strong>Performance</strong> (rapide, gestion de la mémoire efficace, absence de <em>garbage collector</em>…), la <strong>Fiabilité</strong> (système de typage, utilisation de la mémoire…) et la <strong>Productivité</strong> (excellente documentation, compilateur très expressif, outils optimaux…).</p> <h4>Immutabilité</h4> <p>Les variables sont <strong>immutables par défaut</strong>. On utilise le mot clé <code class="language-text">mut</code> si l’on souhaite déclarer une variable mutable. La convention de nommage est le <code class="language-text">snake_case</code> 🐍.</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token comment">// type "i32" déduit pour les deux variables</span> <span class="token keyword">let</span> immutable_x <span class="token operator">=</span> <span class="token number">3</span> <span class="token operator">+</span> <span class="token number">5</span><span class="token punctuation">;</span> <span class="token keyword">let</span> <span class="token keyword">mut</span> mutable_x <span class="token operator">=</span> <span class="token number">3</span> <span class="token operator">+</span> <span class="token number">5</span><span class="token punctuation">;</span> immutable_x <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">;</span> <span class="token comment">// err. "Cannot assign twice to immutable variable</span> mutable_x <span class="token operator">=</span> <span class="token number">7</span><span class="token punctuation">;</span> <span class="token comment">// OK</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <h4>Le compilateur</h4> <p>Deux mots sur le compilateur, <a href="https://rust-lang.github.io/rustc-guide" target="_blank" rel="noopener noreferrer">rustc</a> :</p> <ul> <li>Il repose sur <a href="https://llvm.org/" target="_blank" rel="noopener noreferrer">LLVM</a> pour la génération du code machine</li> <li>L’expressivité des messages d’erreur est excellente.</li> </ul> <p><em>Exemple ici (messages copiés en commentaire), lorsqu’on tente de ré-affecter une variable non mutable</em></p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> x <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">;</span> <span class="token comment">// ❌ error[E0384]: cannot assign twice to immutable variable `x`</span> <span class="token comment">// -</span> <span class="token comment">// |</span> <span class="token comment">// first assignment to `x`</span> <span class="token comment">// help: make this binding mutable: `mut x`</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"The value of x is: {}"</span><span class="token punctuation">,</span> x<span class="token punctuation">)</span><span class="token punctuation">;</span> x <span class="token operator">=</span> <span class="token number">6</span><span class="token punctuation">;</span> <span class="token comment">// ^^^^^ cannot assign twice to immutable variable</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <img src="https://media.giphy.com/media/BzkNVs4orw81xHOKHA/giphy.gif" alt="thanks" style="width: 300px; margin: 20px auto; display: block;" /> <h4>Types et Instances, Méthodes, Visibilité…</h4> <p>Dans les types du langage, on retrouve les :</p> <ul> <li>Booléens (<code class="language-text">bool</code>, 1 octet)</li> <li>Entiers signés <code class="language-text">iX</code> et non signés <code class="language-text">uX</code>, <code class="language-text">X</code> étant la taille en bits (8, 16, 32, 64, 128)</li> <li>Flottants avec <code class="language-text">f32</code> et <code class="language-text">f64</code></li> <li>Caractères (<code class="language-text">char</code> 4 octets, support complet d’Unicode (<code class="language-text">'a'</code>, <code class="language-text">'火'</code>, <code class="language-text">'🔥'</code>))</li> <li>Tuples (<code class="language-text">let tup = ('a', 'b', 'c')</code>, <code class="language-text">let tup = (5, 4.5)</code>)</li> <li>Tableaux (à taille fixe; pour des tailles dynamiques, on utilise des <em>vectors</em>, comme en C++)</li> </ul> <p>Pas de <code class="language-text">null</code> en Rust 🎊, mais une énumération standard <code class="language-text">Option</code> pouvant contenir une valeur (<code class="language-text">Some</code>) ou rien (<code class="language-text">None</code>).</p> <p>On définit des types personnalisés à l’aide des <code class="language-text">enum</code> et <code class="language-text">struct</code> :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">enum</span> IpAddressKind <span class="token punctuation">{</span> V4<span class="token punctuation">,</span> V6<span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">struct</span> IpAddress <span class="token punctuation">{</span> kind<span class="token punctuation">:</span> IpAddressKind<span class="token punctuation">,</span> address<span class="token punctuation">:</span> String<span class="token punctuation">,</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>En effet, pas de <strong>classes</strong> ni de <strong>Programmation orientée Objet</strong>, mais <a href="https://doc.rust-lang.org/book/ch17-00-oop.html" target="_blank" rel="noopener noreferrer">d’autres mécanismes</a> remplacent très bien l’héritage, le polymorphisme, etc. Il n’y a d’ailleurs pas de constructeur en Rust : par convention, on définit une <em>fonction associée</em> <code class="language-text">new</code> responsable de fournir une instance du type défini. C’est par exemple le cas de <code class="language-text">String::new()</code>, du <a href="https://doc.rust-lang.org/std/string/struct.String.html#method.new" target="_blank" rel="noopener noreferrer">crate du même nom</a>.<br /> Les fonctions associées ou <em>“Associated functions”</em> sont l’équivalent des méthodes statiques en POO : des fonctions associées à un type, plutôt qu’à une instance. On les exécute avec l’opérateur <em>double colon</em> (<code class="language-text">::</code>) : <code class="language-text"><Type>::<method_name></code>.</p> <p>L’encapsulation est possible à l’aide du mot clé <code class="language-text">pub</code>, qui permet de rendre publiques les modules, types, méthodes et fonctions. Par défaut, tout est privé. On peut ajouter des méthodes à un type en définissant une implémentation (<code class="language-text">impl</code>) :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">pub</span> <span class="token keyword">struct</span> Account <span class="token punctuation">{</span> balance<span class="token punctuation">:</span> i64<span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">impl</span> Account <span class="token punctuation">{</span> <span class="token keyword">pub</span> <span class="token keyword">fn</span> <span class="token function">new</span><span class="token punctuation">(</span>amount<span class="token punctuation">:</span> i64<span class="token punctuation">)</span> <span class="token punctuation">-></span> Account <span class="token punctuation">{</span> Account <span class="token punctuation">{</span> balance<span class="token punctuation">:</span> amount <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">pub</span> <span class="token keyword">fn</span> <span class="token function">get_balance</span><span class="token punctuation">(</span><span class="token operator">&</span><span class="token keyword">self</span><span class="token punctuation">)</span> <span class="token punctuation">-></span> i64 <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">self</span><span class="token punctuation">.</span>balance<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le champs <code class="language-text">balance</code> est privé : il n’est pas accessible par les autres modules constituant le projet.</p> <p>Pas d’interfaces ni d’héritage, mais Rust supporte également les <a href="https://doc.rust-lang.org/rust-by-example/generics.html" target="_blank" rel="noopener noreferrer">types génériques</a> ainsi que les <a href="https://doc.rust-lang.org/book/ch10-02-traits.html" target="_blank" rel="noopener noreferrer">Traits</a>.</p> <h4><em>Ownership</em></h4> <p>Cette fonctionnalité de Rust (concept unique !) qui joue un rôle majeur dans sa gestion sûre de la mémoire est basée sur 3 règles simples :</p> <ul> <li>Chaque valeur stockée en mémoire a une variable désignée comme son “propriétaire”</li> <li>Il n’y a qu’un seul propriétaire à la fois</li> <li>Quand le <em>scope</em> du propriétaire prend fin, la valeur est supprimée</li> </ul> <p>Lorsqu’une valeur ayant entrainé une allocation mémoire est réassignée ou passée en paramètre, on parle de ”<em>move</em>” (transfert de propriété), rendant impossible l’utilisation de la variable initiale :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> name <span class="token operator">=</span> String<span class="token punctuation">::</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"Jean"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">say_hello</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ❌ error[E0382]: borrow of moved value: `name`</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"{}"</span><span class="token punctuation">,</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ^ value borrowed here after move</span> <span class="token punctuation">}</span> <span class="token keyword">fn</span> <span class="token function">say_hello</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> String<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"Hello {}"</span><span class="token punctuation">,</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Pour des raisons de performances, Rust ne prend pas l’initiative de faire une copie de cette valeur en mémoire et fait un <em>move</em>. Certaines valeurs sont systématiquement copiées (<em>pass by value</em>), comme les références (et non la valeur qu’elle réfère) et les types scalaires (entiers, caractères…).</p> <p>On peut passer <code class="language-text">name</code> par référence (typée <code class="language-text">&</code>) pour procéder à un <strong>emprunt</strong> (<em>borrow</em>) :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> name <span class="token operator">=</span> String<span class="token punctuation">::</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"Jean"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">say_hello</span><span class="token punctuation">(</span><span class="token operator">&</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"{}"</span><span class="token punctuation">,</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">fn</span> <span class="token function">say_hello</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token operator">&</span>String<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"Hello {}"</span><span class="token punctuation">,</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>L’emprunteur <code class="language-text">say_hello</code> utilise une référence de <code class="language-text">name</code>. Lorsque la fin de son scope est atteinte, rien ne se passe : le propriétaire de la valeur n’a jamais changé.<br /> 🤔 Que se passe-t-il si l’on essaie de compiler du code modifiant la valeur pour la préfixer avec “Hello” ?</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> s <span class="token operator">=</span> String<span class="token punctuation">::</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"Jean"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">prefix_with_hello</span><span class="token punctuation">(</span><span class="token operator">&</span>s<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"{}"</span><span class="token punctuation">,</span> s<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">fn</span> <span class="token function">prefix_with_hello</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token operator">&</span>String<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// ❌ error[E0596]: cannot borrow `*name` as mutable, as it is behind a `&` reference</span> <span class="token comment">// ------- help: consider changing this to be a mutable</span> name<span class="token punctuation">.</span><span class="token function">insert_str</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token string">"Hello "</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ^^^^ `name` is a `&` reference, so the data it refers to cannot be borrowed as mutable</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le compilateur <strong>explique</strong> (littéralement !) que <code class="language-text">name</code> ne peut pas être emprunté en tant que valeur mutable, car passé par “référence <code class="language-text">&</code>”. Il recommande l’emploi d’une référence mutable, de type <code class="language-text">&mut String</code>. On applique donc 3 changements : 1- <code class="language-text">name</code> doit être mutable, 2- passé en tant que “référence <code class="language-text">&mut</code>”, 3- et <code class="language-text">prefix_with_hello</code> doit accepter le bon type en paramètre :</p> <div class="gatsby-highlight" data-language="rust"><pre style="counter-reset: linenumber NaN" class="language-rust line-numbers"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> <span class="token keyword">mut</span> s <span class="token operator">=</span> String<span class="token punctuation">::</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"Jean"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">prefix_with_hello</span><span class="token punctuation">(</span><span class="token operator">&</span><span class="token keyword">mut</span> s<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">println!</span><span class="token punctuation">(</span><span class="token string">"{}"</span><span class="token punctuation">,</span> s<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// "Hello Jean"</span> <span class="token punctuation">}</span> <span class="token keyword">fn</span> <span class="token function">prefix_with_hello</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token operator">&</span><span class="token keyword">mut</span> String<span class="token punctuation">)</span> <span class="token punctuation">{</span> name<span class="token punctuation">.</span><span class="token function">insert_str</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token string">"Hello "</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>En résumé, l’<em>ownership</em> effectue un suivi de chaque partie de code utilisant des valeurs stockées dans le tas (<em>heap</em>), y minimise les duplications, et libère la mémoire occupée par des valeurs inutilisées.</p> <blockquote> <p>💡 Je recommande au passage la lecture de <a href="https://words.steveklabnik.com/borrow-checking-escape-analysis-and-the-generational-hypothesis" target="_blank" rel="noopener noreferrer">cet excellent article</a> (teaser : on y parle aussi <strong>un peu</strong> du <em>gradual typing</em> de TypeScript).</p> </blockquote> <h3>Conclusion</h3> <p>Encore un article trop long à lire et à écrire, bien qu’il ne contienne même pas 30% de ce que j’aurais voulu y rapporter tellement j’apprécie la découverte de ce langage ! Mais le but est seulement de partager mon enthousiasme avec quelques éléments techniques. Pour les plus curieux, je recommande à nouveau la consultation du <a href="https://doc.rust-lang.org/stable/book" target="_blank" rel="noopener noreferrer">Book</a> pour y découvrir, entre autres :</p> <ul> <li>Les <a href="https://doc.rust-lang.org/stable/book/ch04-03-slices.html#string-slices" target="_blank" rel="noopener noreferrer">slices</a></li> <li>Le <a href="https://doc.rust-lang.org/book/ch06-02-match.html" target="_blank" rel="noopener noreferrer">pattern matching</a>, déjà suggéré <a href="https://github.com/tc39/proposal-pattern-matching" target="_blank" rel="noopener noreferrer">à la TC39</a> pour EcmaScript</li> <li>Le <a href="https://doc.rust-lang.org/edition-guide/rust-2018/data-types/field-init-shorthand.html" target="_blank" rel="noopener noreferrer">field init shorthand</a></li> <li>Le <a href="https://doc.rust-lang.org/book/ch11-01-writing-tests.html" target="_blank" rel="noopener noreferrer">testing</a></li> <li>Le <a href="https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html" target="_blank" rel="noopener noreferrer">threading</a></li> <li>Etc. Lisez tout, en fait 😏.</li> </ul> <p>D’autres articles sur Rust (et donc WebAssembly !) viendront. Soyez prêts !</p> <img src="https://media.giphy.com/media/jxzEhHBMmH7tm/giphy.gif" alt="Be prepared" style="width: 300px; margin: 20px auto; display: block;" /> <hr /> <p><em><a href="https://tizmah.artstation.com" target="_blank" rel="noopener noreferrer">Credits</a> pour la superbe plage en pixel art</em> 🏖</p></div>Git : Astuces et productivité #1https://vinceops.me/git-astuces-productivite-1/2019-05-01T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 279px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/c64f76aff2baa71f3261bcb2a79738df/3412f/git.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 43.07692307692307%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMCBAX/xAAUAQEAAAAAAAAAAAAAAAAAAAAC/9oADAMBAAIQAxAAAAHThfClDRH/xAAZEAACAwEAAAAAAAAAAAAAAAABAgMQEhP/2gAIAQEAAQUCYv0Z3MkesX//xAAWEQEBAQAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8BXZ//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAgEBPwGH/8QAGxAAAgEFAAAAAAAAAAAAAAAAAAECEiAxQbH/2gAIAQEABj8C4SitCrzZ/8QAHBAAAQQDAQAAAAAAAAAAAAAAAQARQXEgITFR/9oACAEBAAE/IdOBSCFw6IMwpTT2+H//2gAMAwEAAgADAAAAEPvP/8QAFxEBAAMAAAAAAAAAAAAAAAAAEQEQIf/aAAgBAwEBPxCMYFf/xAAWEQADAAAAAAAAAAAAAAAAAAABEGH/2gAIAQIBAT8QFL//xAAfEAEBAAECBwAAAAAAAAAAAAABEXEAISAxQWGBkaH/2gAIAQEAAT8QG+sJZF+Y0KbQHNBpt7ufetroSON3xwf/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/c64f76aff2baa71f3261bcb2a79738df/798b8/git.webp 195w, /static/c64f76aff2baa71f3261bcb2a79738df/99323/git.webp 279w" sizes="(max-width: 279px) 100vw, 279px" type="image/webp" /> <source srcset="/static/c64f76aff2baa71f3261bcb2a79738df/0e421/git.jpg 195w, /static/c64f76aff2baa71f3261bcb2a79738df/3412f/git.jpg 279w" sizes="(max-width: 279px) 100vw, 279px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/c64f76aff2baa71f3261bcb2a79738df/3412f/git.jpg" alt="git" title="git" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Dans ce premier article d’une série dédiée à <a href="https://git-scm.com/" target="_blank" rel="noopener noreferrer"><strong>git</strong></a>, j’aborde les <strong>commit atomiques</strong>.<br /> Au programme de ladite série, <em>Tips & tricks!</em> : raccourcis, astuces et méthodologie (<em>opinionated content!</em>).</p> <hr /> <h2>Commits atomiques</h2> <p>L’emploi des commits “atomiques” est encore peu courant, alors qu’il constitue la base de tout bon contrôle de version, en tant que concept comme en tant qu’outil, du <em>commit history</em> (avec tout ce qu’il a à offrir) à la <em>code review</em>.</p> <p>📚 Un commit est dit <strong>atomique</strong> lorsque :</p> <ul> <li>Le changement qu’il apporte ne casse pas la cohérence du dépôt (compilation garantie et tests réussis).</li> <li>Il concerne <strong>une</strong> tâche et ne peut être découpé davantage sans enfreindre la règle précédente.</li> <li>Son message est précis et explique clairement le périmètre de son opération.</li> </ul> <p>✌ Et les bénéfices sont aussi nombreux qu’évidents.</p> <p>L’atomicité tend à réduire la taille des commits et à augmenter la fréquence des <code class="language-text">push</code> : le code est <strong>sauvegardé plus régulièrement</strong>.</p> <p>Le périmètre des commits atomiques, en plus d’être clairement défini, est généralement peu fragmenté à travers la <em>codebase</em> : les <strong><em>merge conflicts</em></strong> se raréfient et <strong>l’intention</strong> des autres développeurs se devine facilement.</p> <p><strong>L’<a href="https://git-scm.com/book/en/v2/Git-Basics-Viewing-the-Commit-History" target="_blank" rel="noopener noreferrer">historique</a> des commits</strong> devient très pertinent : chaque entrée décrit précisément une modification; couplé à une <a href="https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit" target="_blank" rel="noopener noreferrer">convention de <em>commit message</em></a>, il joue pratiquement le rôle de <a href="https://keepachangelog.com/fr/1.0.0/" target="_blank" rel="noopener noreferrer">changelog</a>.</p> <p>Les <strong>revues de code sont plus efficaces</strong>, réalisables “commit par commit” : le <em>reviewer</em> peut se concentrer sur chaque modification <strong>isolée</strong>. Chacune doit aussi être <strong>complète</strong> : quand c’est approprié, documentation et tests inclus.</p> <p>Chaque modification apportée à un fichier devient légitime : aucun hors-sujet dans les <a href="https://git-scm.com/docs/git-blame" target="_blank" rel="noopener noreferrer">git blame</a>. Terminés les commits <code class="language-text">"fix bug"</code> touchant à 9 fichiers dont 150 lignes de code et 7 fonctions ! Naturellement, cela s’accompagne aussi d’une <strong>meilleure chasse des régressions</strong> (manuelle, ou avec <a href="https://git-scm.com/docs/git-bisect" target="_blank" rel="noopener noreferrer">git bisect</a> pour les <em>lords</em> 😁).</p> <p>… etc!</p> <img src="https://media.giphy.com/media/rlfUniMvoWZOw/giphy.gif" alt="amazed-face" style="width: 200px; margin: 20px auto; display: block;" /> <p>💡 En pratique, cela se traduit principalement par <strong>l’augmentation de la fréquence des commits</strong>.</p> <p>Une nouvelle fonctionnalité ? <code class="language-text">"feat(dashboard): add the step-by-step tutorial"</code><br /> Des tests manquants sur une méthode ? <code class="language-text">"test(fcm): add tests for android payload factory"</code><br /> Toutes les indentations passées de 2 à 4 espaces (à éviter 😁) ? <code class="language-text">"style: change indent. from 2 to 4 spaces"</code> </p> <p>Ces exemples de <em>commits messages</em> sont construits selon la convention suivante : <a href="https://www.conventionalcommits.org/" target="_blank" rel="noopener noreferrer">conventional commits</a>.<br /> Ils sont composés :</p> <ul> <li>d’un type (<code class="language-text">feat</code>, <code class="language-text">fix</code>, <code class="language-text">test</code>…)</li> <li>d’un <em>scope</em> optionnel (<code class="language-text">dashboard</code>, <code class="language-text">fcm</code>) faisait référence à une partie du code</li> <li>d’un message décrivant le changement apporté</li> </ul> <p>Il n’y a aucun mal à faire de très petits commits, pourvu qu’ils respectent les règles citées en introduction. Il est de toute façon plus aisé de regrouper plusieurs commits en un seul, que d’en séparer un en plusieurs.<br /> Dernier point positif : avec un historique clair, le ”<em>squash pre-merge</em>” (bouh! 🤮) n’a plus de raison d’être (mais en a-t-il déjà été autrement ? 😁).</p></div>React : Suivi des erreurs avec Bugsnaghttps://vinceops.me/react-suivi-erreurs-bugsnag/2019-03-13T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/2007409910c7a13f76b0d23fe8c5adf9/ca1dc/bugsnag_react.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABs0lEQVQoz2Ng4HWby8DrHgfEUQw8bqpA2hwoFgpkZwPZ9kA6Eci3AbJdgLiMgcfdDiiWBqQDgXxrIDsfLAcE/DJhjAwMfB7lzCK+ZkB6JlAyGCiZBsRdQA2WQLocKBYOpEuBOBvI1gbSG4EWAOXcQGJTgTTQAe6VgrKRvAwQYG3IwGCWyMCgKgw0NBioyQlomDxQUToQtwCxHRCHArEhEIsBsRkQgyzJBGI9II5n4HHpZoCBLf//czb//69a//8/P1yQx00RqDCDkc/dhwEPYOZ3l2Zgdko0s0/Onb9gwtKeOTPCGOKufvOLv/p9Qvz1H1EgRUIWuUxcMsGsDOzO0sialc3aGGFsThEnZhCdkjORDUiJLN+wl7usqOTr4tKeIwwJN3+ZAXFu4q3f9iBFqVOWgzXu3jqLd/LkyR1Nzc2VPT09Te3t7YlAfnpzc7MfkB8B5NdWV1cHTJnYX13d2tC4sKLn6BWVmi8MiTd/sQMNEwNiLpBBCRuPgQ0MDIpgT05Oto2Pj9eOjY01CA0NVU5PTzcE8s1jYmLkgHKmHh4eMjkJ6eZWLfGay9TSOu8Jlv8FALPeimYd/3RoAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/2007409910c7a13f76b0d23fe8c5adf9/798b8/bugsnag_react.webp 195w, /static/2007409910c7a13f76b0d23fe8c5adf9/0e356/bugsnag_react.webp 390w, /static/2007409910c7a13f76b0d23fe8c5adf9/4c79d/bugsnag_react.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/2007409910c7a13f76b0d23fe8c5adf9/25fa3/bugsnag_react.png 195w, /static/2007409910c7a13f76b0d23fe8c5adf9/506f3/bugsnag_react.png 390w, /static/2007409910c7a13f76b0d23fe8c5adf9/ca1dc/bugsnag_react.png 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/2007409910c7a13f76b0d23fe8c5adf9/ca1dc/bugsnag_react.png" alt="bugsnag-react" title="bugsnag-react" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Vendredi 18h30, après une bonne semaine de livraison continue et de revue de code, quoi de tel qu’un <em>product manager</em> qui entre paniqué dans le bureau en vous demandant ce que signifie ce <code class="language-text">"Uncaught ReferenceError"</code> ?</p> <p>Bien qu’il ne permette pas d’éviter cette erreur (mais <a href="https://vinceops.me/2018/04/22/typescript-partie-2-3-pourquoi-ladopter/" target="_blank" rel="noopener noreferrer">TypeScript</a> peut aider 😁), l’outil <a href="https://www.bugsnag.com" target="_blank" rel="noopener noreferrer">Bugsnag</a> sait (entre autres) remonter automatiquement les erreurs non-gérées de votre application.</p> <hr /> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 766px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/d8b7cb910a8ea2a518e87547045970c3/3cc96/2019-03-13_22-50-25.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 58.97435897435898%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAe8SWzH/xAAXEAEAAwAAAAAAAAAAAAAAAAAAAREg/9oACAEBAAEFAsSt/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAGhAAAgIDAAAAAAAAAAAAAAAAAAEQMREhcf/aAAgBAQABPyFIzuoXS0P/2gAMAwEAAgADAAAAEFDP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGxABAQEAAgMAAAAAAAAAAAAAAREAITFRYaH/2gAIAQEAAT8Qs2jPdyuBWr4xa8vmU6Go6Df/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/d8b7cb910a8ea2a518e87547045970c3/798b8/2019-03-13_22-50-25.webp 195w, /static/d8b7cb910a8ea2a518e87547045970c3/0e356/2019-03-13_22-50-25.webp 390w, /static/d8b7cb910a8ea2a518e87547045970c3/1d625/2019-03-13_22-50-25.webp 766w" sizes="(max-width: 766px) 100vw, 766px" type="image/webp" /> <source srcset="/static/d8b7cb910a8ea2a518e87547045970c3/0e421/2019-03-13_22-50-25.jpg 195w, /static/d8b7cb910a8ea2a518e87547045970c3/ce2e0/2019-03-13_22-50-25.jpg 390w, /static/d8b7cb910a8ea2a518e87547045970c3/3cc96/2019-03-13_22-50-25.jpg 766w" sizes="(max-width: 766px) 100vw, 766px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/d8b7cb910a8ea2a518e87547045970c3/3cc96/2019-03-13_22-50-25.jpg" alt="bugsnag-breadcrumb" title="bugsnag-breadcrumb" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> <em>Breadcrumbs: L’outil trace la succession d’actions ayant provoqué l’erreur</em></p> <p>Les avantages sont multiples : </p> <ul> <li>On n’attend pas après les retours utilisateurs pour découvrir qu’une fonctionnalité provoque des <em>runtime errors</em> (#<strong>réact</strong>ivité #blagueDrôle) </li> <li>Chaque erreur rapportée peut entraîner le déclenchement d’un Webhook (e.g. créer un issue sur Github, envoyer un message sur Slack…)</li> <li>On peut tracer manuellement des cas critiques avec un maximum de <em>metadata</em>, facilitant la reproduction d’un bug ou usage (<em>state</em>, historique des <em>actions</em> Redux, saisies, ID, etc)</li> </ul> <img src="https://media.giphy.com/media/Oebj5fZNZOdwI/giphy.gif" alt="lazer-cat" style="width: 250px; margin: 20px auto; display: block;" /> <blockquote> <p>Disclaimer : </p> <ul> <li><a href="https://sentry.io" target="_blank" rel="noopener noreferrer">Sentry</a> est une excellente alternative à Bugsnag </li> <li>Les exemples donnés ci-après nécessitent <strong>React 16+</strong> et ses <a href="https://reactjs.org/docs/error-boundaries.html" target="_blank" rel="noopener noreferrer">Error Boundaries</a> pour fonctionner.</li> </ul> </blockquote> <p>Après avoir créé un nouveau projet sur Bugsnag (Browser > React), on obtient une clé API permettant d’émettre de nouveaux “événements” (erreur gérée, erreur non-gérée, rapport personnalisé, etc).</p> <span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 723px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/80c7605d23aee6f55c12421cb7d6fd17/487ae/2019-03-07_00-19-59.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 54.358974358974365%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAIBAwX/xAAVAQEBAAAAAAAAAAAAAAAAAAABAv/aAAwDAQACEAMQAAAB2i1WWICv/8QAGRABAAIDAAAAAAAAAAAAAAAAAQACEBEi/9oACAEBAAEFAtM6yhYAqf/EABURAQEAAAAAAAAAAAAAAAAAABAR/9oACAEDAQE/AYf/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAaEAACAgMAAAAAAAAAAAAAAAAAARARMaHh/9oACAEBAAY/AuGdTTKR/8QAGBABAQEBAQAAAAAAAAAAAAAAAREAEFH/2gAIAQEAAT8hascAPjqYaaYYb//aAAwDAQACAAMAAAAQFw//xAAXEQEAAwAAAAAAAAAAAAAAAAABEBHw/9oACAEDAQE/EK5Y/8QAFREBAQAAAAAAAAAAAAAAAAAAAQD/2gAIAQIBAT8QGb//xAAbEAEBAAMAAwAAAAAAAAAAAAABEQAhMRDB0f/aAAgBAQABPxB7CC8294JkVs+r4hbN4sC/RwCE+Bn/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/80c7605d23aee6f55c12421cb7d6fd17/798b8/2019-03-07_00-19-59.webp 195w, /static/80c7605d23aee6f55c12421cb7d6fd17/0e356/2019-03-07_00-19-59.webp 390w, /static/80c7605d23aee6f55c12421cb7d6fd17/d196b/2019-03-07_00-19-59.webp 723w" sizes="(max-width: 723px) 100vw, 723px" type="image/webp" /> <source srcset="/static/80c7605d23aee6f55c12421cb7d6fd17/0e421/2019-03-07_00-19-59.jpg 195w, /static/80c7605d23aee6f55c12421cb7d6fd17/ce2e0/2019-03-07_00-19-59.jpg 390w, /static/80c7605d23aee6f55c12421cb7d6fd17/487ae/2019-03-07_00-19-59.jpg 723w" sizes="(max-width: 723px) 100vw, 723px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/80c7605d23aee6f55c12421cb7d6fd17/487ae/2019-03-07_00-19-59.jpg" alt="nouveau-projet-bugsnag" title="nouveau-projet-bugsnag" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> <p>Il ne reste qu’à intégrer le client au projet !</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash"><span class="token function">yarn</span> <span class="token function">add</span> @bugsnag/js @bugsnag/plugin-react <span class="token comment"># ou npm install @bugsnag/js @bugsnag/plugin-react</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <blockquote> <p>💡 Les paquets sont fournis avec leurs définitions 😌 #typeSafety</p> </blockquote> <p>On crée un client bugsnag :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// src/<whatever/bugsnag-client.ts</span> <span class="token keyword">import</span> bugsnag <span class="token keyword">from</span> <span class="token string">'@bugsnag/js'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> bugsnagClient <span class="token operator">=</span> <span class="token function">bugsnag</span><span class="token punctuation">(</span><span class="token punctuation">{</span> apiKey<span class="token operator">:</span> <span class="token constant">BUGSNAG_API_KEY</span><span class="token punctuation">,</span> <span class="token function-variable function">beforeSend</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">report</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// les rapports sont ignorés si l'app n'est pas en production</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span> <span class="token operator">!==</span> <span class="token string">'production'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> report<span class="token punctuation">.</span><span class="token function">ignore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// le state est ajouté aux données meta du rapport (debugging facilité)</span> report<span class="token punctuation">.</span>metadata<span class="token punctuation">.</span>state <span class="token operator">=</span> store<span class="token punctuation">.</span><span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">default</span> bugsnagClient<span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Deux options sont utilisées : </p> <ul> <li><code class="language-text">apiKey</code>, la clé API fournie par Bugsnag</li> <li><code class="language-text">beforeSend</code>, un <em>callback</em> appelé avant chaque émission </li> </ul> <p>On peut ajouter toute sorte de <a href="https://docs.bugsnag.com/platforms/javascript/react/customizing-error-reports/" target="_blank" rel="noopener noreferrer">données</a> au rapport remonté, facilitant le travail de debug/analyse.</p> <h2>Les erreurs non-gérées : Error Boundary</h2> <p>Pour rapporter automatiquement les erreurs non gérées, on crée un <em>component</em> Error Boundary <a href="https://docs.bugsnag.com/platforms/javascript/react/" target="_blank" rel="noopener noreferrer">avec le client</a> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// src/<whatever>/BugsnagBoundary.ts</span> <span class="token keyword">import</span> React<span class="token punctuation">,</span> <span class="token punctuation">{</span> ReactNode <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> bugsnagReact <span class="token keyword">from</span> <span class="token string">'@bugsnag/plugin-react'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> bugsnagClient <span class="token keyword">from</span> <span class="token string">'./bugsnag-client.ts'</span><span class="token punctuation">;</span> bugsnagClient<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>bugsnagReact<span class="token punctuation">,</span> React<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> BugsnagBoundary<span class="token operator">:</span> FunctionComponent<span class="token operator"><</span><span class="token punctuation">{</span> FallbackComponent<span class="token operator">?</span><span class="token operator">:</span> ReactNode <span class="token punctuation">}</span><span class="token operator">></span> <span class="token operator">=</span> bugsnagClient<span class="token punctuation">.</span><span class="token function">getPlugin</span><span class="token punctuation">(</span><span class="token string">'react'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">default</span> BugsnagBoundary<span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><code class="language-text">BugsnagBoundary</code> est directement utilisable pour envelopper notre <em>component</em> de plus haut niveau :</p> <div class="gatsby-highlight" data-language="jsx"><pre style="counter-reset: linenumber NaN" class="language-jsx line-numbers"><code class="language-jsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">BugsnagBoundary</span></span><span class="token punctuation">></span></span><span class="token plain-text"> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">App</span></span><span class="token punctuation">/></span></span><span class="token plain-text"> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">BugsnagBoundary</span></span><span class="token punctuation">></span></span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span></span></pre></div> <img src="https://media.giphy.com/media/nYSlA9xdfKuNa/giphy.gif" alt="safety-net" style="width: 400px; margin: 20px auto; display: block;" /> <p>Cependant, cette solution présente deux défauts : </p> <ul> <li>Aucun contenu n’est affiché à l’utilisateur si c’est l’étape de rendu (<em>rendering</em>) qui provoque une erreur. Bien que ladite erreur soit remontée à Bugsnag. Un <em>component</em> de remplacement peut être affiché, cas échéant, en le passant à la prop <code class="language-text">FallbackComponent</code>. </li> <li>Les erreurs provoquées lors des phases de conception ne sont plus remontées visuellement par les outils de développement (<code class="language-text">react-error-overlay</code>, <code class="language-text">react-hot-loader</code>, etc).</li> </ul> <p>On peut wrapper <code class="language-text">BugsnagBoundary</code> dans un nouveau composant pour y remédier :</p> <div class="gatsby-highlight" data-language="jsx"><pre style="counter-reset: linenumber NaN" class="language-jsx line-numbers"><code class="language-jsx"><span class="token comment">// src/<whatever>/ErrorBoundary.ts</span> <span class="token keyword">import</span> React<span class="token punctuation">,</span> <span class="token punctuation">{</span> FunctionComponent <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> BugsnagBoundary<span class="token operator">:</span> FunctionComponent<span class="token operator"><</span><span class="token punctuation">{</span> FallbackComponent<span class="token operator">?</span><span class="token operator">:</span> ReactNode <span class="token punctuation">}</span><span class="token operator">></span> <span class="token operator">=</span> bugsnagClient<span class="token punctuation">.</span><span class="token function">getPlugin</span><span class="token punctuation">(</span><span class="token string">'react'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> ErrorBoundary<span class="token operator">:</span> <span class="token function-variable function">FunctionComponent</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> children <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span> <span class="token operator">===</span> <span class="token string">'production'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">BugsnagBoundary</span></span> <span class="token attr-name">FallbackComponent</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>FallbackComponent<span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">BugsnagBoundary</span></span><span class="token punctuation">></span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> children<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">default</span> ErrorBoundary<span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><code class="language-text">BugsnagBoundary</code> n’est rendu qu’en production et affiche <code class="language-text">FallbackComponent</code> si <code class="language-text">children</code> ne peut pas l’être. Autrement, seul <code class="language-text">children</code> est rendu et l’arbre des <em>components</em> reste inchangé. </p> <blockquote> <p>💡 <code class="language-text">FallbackComponent</code> reçoit les props suivantes :</p> </blockquote> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">type</span> FallbackComponentProps <span class="token operator">=</span> <span class="token punctuation">{</span> error<span class="token operator">:</span> Error<span class="token punctuation">;</span> info<span class="token operator">:</span> <span class="token punctuation">{</span> componentStack<span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>On wrap ensuite notre <code class="language-text"><App/></code> avec <code class="language-text">ErrorBoundary</code>.</p> <h2>Les erreurs gérées : <code class="language-text">notify</code></h2> <p>Les erreurs gérées (et les rapports personnalisés) peuvent être émis avec <a href="https://docs.bugsnag.com/platforms/javascript/react/reporting-handled-errors/" target="_blank" rel="noopener noreferrer"><strong><code class="language-text">notify</code></strong></a> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// dans un processus critique (en cas d'erreur), ou dans un flux sensible :</span> bugsnagClient<span class="token punctuation">.</span><span class="token function">notify</span><span class="token punctuation">(</span><span class="token string">'Unexpected error during Sign Up'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> metaData<span class="token operator">:</span> <span class="token punctuation">{</span> validation<span class="token operator">:</span> <span class="token punctuation">{</span> errors<span class="token punctuation">,</span> signUpForm <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> severity<span class="token operator">:</span> <span class="token string">'info'</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Ici aussi, on ajoute un maximum d’informations facilitant la compréhension du contexte (en prenant garde de ne jamais aller à l’encontre de la RGPD 😬).</p> <h2> Suivi 🧐</h2> <p>Depuis l’inbox (dans le dashboard), on peut suivre en temps quasi-réel les rapports, leur fréquence, la date de dernière occurrence, les assigner à un développeur, les marquer comme <em>Fixed</em>, etc.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/e8f8097c788eee87d368a826140c18ea/ee44d/bugsnag_report.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 18.46153846153846%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAEABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAdewQD//xAAXEAADAQAAAAAAAAAAAAAAAAAAAxMC/9oACAEBAAEFAp4JLJLP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFxAAAwEAAAAAAAAAAAAAAAAAAAIykf/aAAgBAQAGPwKFwhcIXD//xAAWEAEBAQAAAAAAAAAAAAAAAAABABD/2gAIAQEAAT8hMwRif//aAAwDAQACAAMAAAAQc9//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAbEAACAQUAAAAAAAAAAAAAAAAAAREhUWGRwf/aAAgBAQABPxCPmsJFXTEHxn//2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/e8f8097c788eee87d368a826140c18ea/798b8/bugsnag_report.webp 195w, /static/e8f8097c788eee87d368a826140c18ea/0e356/bugsnag_report.webp 390w, /static/e8f8097c788eee87d368a826140c18ea/23bbb/bugsnag_report.webp 780w, /static/e8f8097c788eee87d368a826140c18ea/b47fd/bugsnag_report.webp 1170w, /static/e8f8097c788eee87d368a826140c18ea/1f97d/bugsnag_report.webp 1525w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/e8f8097c788eee87d368a826140c18ea/0e421/bugsnag_report.jpg 195w, /static/e8f8097c788eee87d368a826140c18ea/ce2e0/bugsnag_report.jpg 390w, /static/e8f8097c788eee87d368a826140c18ea/485b7/bugsnag_report.jpg 780w, /static/e8f8097c788eee87d368a826140c18ea/faa76/bugsnag_report.jpg 1170w, /static/e8f8097c788eee87d368a826140c18ea/ee44d/bugsnag_report.jpg 1525w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/e8f8097c788eee87d368a826140c18ea/485b7/bugsnag_report.jpg" alt="bugsnag-react" title="bugsnag-react" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> Dans le détail de chaque rapport, on trouve, en plus des <em>Breadcrumbs</em> : la <em>stacktrace</em>, un extrait du code ayant provoqué l’erreur (ou émis le rapport), les données meta rapportées, des données sur l’appareil de l’utilisateur, etc.</p> <blockquote> <p>📜 Les <em>source maps</em> doivent être hébergées sur <a href="https://docs.bugsnag.com/platforms/javascript/source-maps/" target="_blank" rel="noopener noreferrer">Bugsnag</a>, pour permettre le bon usage de cette fonctionnalité (sinon, c’est la portion de code <em>minifiée</em>/transpilée qui est affichée). </p> </blockquote> <img src="https://media.giphy.com/media/k39w535jFPYrK/giphy.gif" alt="enjoy-coding" style="width: 400px; margin: 20px auto; display: block;" /> <p><em>Enjoy coding!</em></p> <p>Merci à mon très inspirant collègue (et super lead frontend dev 💓) <a href="https://twitter.com/timurrustamov8" target="_blank" rel="noopener noreferrer">@Timur</a>, pour son indéfectible soutien 🔥.</p></div>TypeScript : Typage et Généricitéhttps://vinceops.me/typescript-typage-genericite/2019-02-10T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/38e586115b0a2e23e660d106053215d8/a22ce/ts-generics.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAMBAgQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABRKKG45of/8QAGxAAAgEFAAAAAAAAAAAAAAAAAAECERIxMkL/2gAIAQEAAQUC5jrahyMp1P/EABYRAAMAAAAAAAAAAAAAAAAAAAEQMf/aAAgBAwEBPwERf//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAECAQE/AWf/xAAYEAADAQEAAAAAAAAAAAAAAAAAAREhEP/aAAgBAQAGPwLBchDG0f/EABwQAQACAQUAAAAAAAAAAAAAAAEAESEQQVFh4f/aAAgBAQABPyFHsDEQ5PdBF2EsVGKuohVRwM//2gAMAwEAAgADAAAAEOcf/8QAFxEAAwEAAAAAAAAAAAAAAAAAARAhEf/aAAgBAwEBPxCMGr//xAAXEQEBAQEAAAAAAAAAAAAAAAABEQAh/9oACAECAQE/EFWnNXf/xAAaEAEBAAIDAAAAAAAAAAAAAAABEQAhMUFR/9oACAEBAAE/EKiQ6DvFxJTYpt7hsZa3Hh2QZzxiCMw8YrXi0wc//9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/38e586115b0a2e23e660d106053215d8/798b8/ts-generics.webp 195w, /static/38e586115b0a2e23e660d106053215d8/0e356/ts-generics.webp 390w, /static/38e586115b0a2e23e660d106053215d8/4c79d/ts-generics.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/38e586115b0a2e23e660d106053215d8/0e421/ts-generics.jpg 195w, /static/38e586115b0a2e23e660d106053215d8/ce2e0/ts-generics.jpg 390w, /static/38e586115b0a2e23e660d106053215d8/a22ce/ts-generics.jpg 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/38e586115b0a2e23e660d106053215d8/a22ce/ts-generics.jpg" alt="TS Generics" title="TS Generics" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>La <a href="https://en.wikipedia.org/wiki/Generic_programming" target="_blank" rel="noopener noreferrer">généricité</a> permet d’écrire des définitions (de classes, interfaces, fonctions, types…) paramétriques. On appelle ces définitions des <a href="https://www.typescriptlang.org/docs/handbook/generics.html" target="_blank" rel="noopener noreferrer">Génériques</a>. Présents sous le même nom dans Java (1.5), C# (2), ils existent dans TypeScript depuis sa création (merci qui ? Merci Anders !).</p> <hr /> <p>Un exemple très commun en TypeScript est celui des tableaux, avec <code class="language-text">Array<T></code>. <code class="language-text">T</code> est appelé “paramètres de type” (<em>type parameter</em>, à ne pas confondre avec un type de paramètre 🙃).</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">const</span> items<span class="token operator">:</span> <span class="token builtin">Array</span><span class="token operator"><</span><span class="token builtin">number</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">]</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ok</span> items<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// erreur: argument of type 'a' is not assignable to parameter of type 'number'</span> <span class="token comment">// "n" est déduit comme étant de type 'number'</span> <span class="token keyword">const</span> results <span class="token operator">=</span> items<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">n</span> <span class="token operator">=></span> n<span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ["1.0", "2.0", "3.0", "4.0"]</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Les mêmes méthodes (<code class="language-text">push</code>, <code class="language-text">map</code>, etc) et la même analyse statique existent avec <code class="language-text">Array<string></code>, <code class="language-text">Array<Date></code>, etc. <code class="language-text">Array</code> est dit “type générique”. </p> <blockquote> <p>💡 C’est aussi vrai pour <code class="language-text">Map<K, V></code>, <code class="language-text">Set<T></code>, <code class="language-text">Promise<T></code>, <a href="https://github.com/ReactiveX/rxjs/blob/master/src/internal/Observable.ts" target="_blank" rel="noopener noreferrer"><code class="language-text">Observable<T></code></a> (RxJS), etc.</p> </blockquote> <h3>Show time 🎉</h3> <p><em>Certains exemples sont volontairement simplistes, l’objectif étant de lever toute ambiguïté en se concentrant sur les sujets de la généricité et du typage 🤗</em></p> <p>Grâce aux generics, il est possible de :</p> <p>🌟 <strong>Déclarer des contraintes génériques</strong></p> <p>On définit une <strong>fonction générique</strong> <code class="language-text">pick</code> <em>type-safe</em> permettant de récupérer un extrait d’objet, ici, à l’aide de <code class="language-text">K extends keyof T</code>, dit ”<code class="language-text">K</code> <strong>contraint</strong> par <code class="language-text">T</code>“.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">function</span> pick<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token constant">K</span> <span class="token keyword">extends</span> <span class="token class-name">keyof</span> <span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">(</span>source<span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">,</span> <span class="token operator">...</span>keys<span class="token operator">:</span> <span class="token constant">K</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token operator">:</span> Partial<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> result<span class="token operator">:</span> Partial<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span> keys<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">key</span> <span class="token operator">=></span> result<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> source<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> result<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token punctuation">{</span> weight<span class="token operator">:</span> <span class="token number">55</span><span class="token punctuation">,</span> name<span class="token operator">:</span> <span class="token string">'Winry'</span><span class="token punctuation">,</span> birthDate<span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token string">'1985-06-13'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token function">pick</span><span class="token punctuation">(</span>user<span class="token punctuation">,</span> <span class="token string">'name'</span><span class="token punctuation">,</span> <span class="token string">'weight'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// { name: 'Winry', weight: 55 }</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Bien sûr, tout est analysé/déduit par le compilateur <code class="language-text">tsc</code> et l’IDE sait aussi faire l’auto-complétion des propriétés saisies (<code class="language-text">name</code> et <code class="language-text">weight</code>) 🔥. </p> <p>🌟 <strong>Obtenir les propriétés d’un certain type d’une classe ou interface</strong></p> <p>En utilisant les types conditionnels (<em>conditional types</em>), qui prennent la forme d’une expression ternaire utilisant <code class="language-text">extends</code> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">type</span> StringProperty<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">P</span> <span class="token keyword">in</span> <span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span> <span class="token keyword">extends</span> <span class="token class-name">string</span> <span class="token operator">?</span> <span class="token constant">P</span> <span class="token operator">:</span> <span class="token builtin">never</span> <span class="token punctuation">}</span><span class="token punctuation">[</span><span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token punctuation">{</span> birthDate<span class="token operator">:</span> Date<span class="token punctuation">;</span> isAdmin<span class="token operator">:</span> <span class="token builtin">boolean</span> firstName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> lastName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">type</span> <span class="token constant">S1</span> <span class="token operator">=</span> StringProperty<span class="token operator"><</span>User<span class="token operator">></span> <span class="token comment">// 'firstName' | 'lastName'</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>On peut rendre ce type générique … encore plus générique !</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// toutes les propriétés de type "A" du type "T"</span> <span class="token keyword">type</span> PropertyOfType<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token constant">A</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">P</span> <span class="token keyword">in</span> <span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span> <span class="token keyword">extends</span> <span class="token class-name">A</span> <span class="token operator">?</span> <span class="token constant">P</span> <span class="token operator">:</span> <span class="token builtin">never</span> <span class="token punctuation">}</span><span class="token punctuation">[</span><span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// décliné en :</span> <span class="token keyword">type</span> StringProperty<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> PropertyOfType<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token builtin">string</span><span class="token operator">></span><span class="token punctuation">;</span> <span class="token keyword">type</span> NumberProperty<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> PropertyOfType<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token builtin">number</span><span class="token operator">></span><span class="token punctuation">;</span> <span class="token keyword">type</span> MethodProperty<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> PropertyOfType<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">any</span><span class="token operator">></span><span class="token punctuation">;</span> <span class="token comment">// ...</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p> <br /> 🌟 <strong>Définir des types élaborés (<em>mapped types</em>)</strong></p> <p>Rendus possibles grâce à la généricité, vous connaissez la plupart d’entre eux si vous utilisez TypeScript régulièrement : </p> <ul> <li><code class="language-text">Partial<T></code> : Type ayant toutes les propriétés de <code class="language-text">T</code>, optionnelles (modificateur <code class="language-text">?</code>).</li> <li><code class="language-text">Required<T></code> : Type ayant toutes les propriétés de <code class="language-text">T</code>, requises (modificateur <code class="language-text">-?</code>).</li> <li><code class="language-text">Readonly<T></code> : Type ayant toutes les propriétés de <code class="language-text">T</code>, non ré-assignables (modificateur <code class="language-text">readonly</code>).</li> <li><code class="language-text">Pick<T, K extends keyof T></code> : Type ayant toutes les propriétés <code class="language-text">K</code> de <code class="language-text">T</code>.</li> </ul> <p>Quelques autres qui, bien qu’absents des <code class="language-text">lib.*.d.ts</code>, sont très communément utilisés dans les projets TypeScript: </p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// Type ayant toutes les propriétés de T, pouvant aussi être `null` </span> <span class="token keyword">type</span> Nullable<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">P</span> <span class="token keyword">in</span> <span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span> <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token comment">// Type ayant toutes les propriétés de T, sans modificateur `readonly`</span> <span class="token keyword">type</span> Writable<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">-</span><span class="token keyword">readonly</span> <span class="token punctuation">[</span><span class="token constant">P</span> <span class="token keyword">in</span> <span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token comment">// Type ayant toutes les propriétés de T sauf celles données pour K</span> <span class="token comment">// e.g.: Omit<User, 'firstName' | 'lastName'> </span> <span class="token keyword">type</span> Omit<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token constant">K</span> <span class="token keyword">extends</span> <span class="token class-name">keyof</span> <span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> Pick<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> Exclude<span class="token operator"><</span><span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">,</span> <span class="token constant">K</span><span class="token operator">>></span><span class="token punctuation">;</span> <span class="token comment">// `Partial<T>` supportant plusieurs niveaux de profondeur (sous-objets, sous-sous-objets, etc)</span> <span class="token keyword">type</span> DeepPartial<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">P</span> <span class="token keyword">in</span> <span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">?</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span> <span class="token keyword">extends</span> <span class="token class-name">Array</span><span class="token operator"><</span>infer <span class="token constant">U</span><span class="token operator">></span> <span class="token operator">?</span> <span class="token builtin">Array</span><span class="token operator"><</span>DeepPartial<span class="token operator"><</span><span class="token constant">U</span><span class="token operator">>></span> <span class="token operator">:</span> DeepPartial<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span><span class="token operator">></span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 L’implémentation de <code class="language-text">DeepPartial</code> ci-dessus est incomplète (pour des raisons de lisibilité) : elle doit aussi intégrer l’assignation des types <code class="language-text">Date</code> (à conserver “tel quel”) et <code class="language-text">ReadonlyArray<T></code>.</p> </blockquote> <img src="https://media.giphy.com/media/3oKIPnAiaMCws8nOsE/giphy.gif" alt="cute-cat" style="width: 300px; margin: 20px auto; display: block;" /> <p>🌟 <strong>Introduire des variables de Type à déduire (<code class="language-text">infer</code>)</strong></p> <p>L’introduction des types conditionnels (TypeScript 2.8), avec le mot-clé <code class="language-text">extends</code>, a aussi introduit le mot-clé <code class="language-text">infer</code>, qui permet de déclarer une variable de type déduit.<br /> <code class="language-text">DeepPartial<T></code> en montre un premier exemple ci-dessus, en transformant les <code class="language-text">Array<U></code> en <code class="language-text">Array<DeepPartial<U>></code> (souhaité) et non en <code class="language-text">DeepPartial<Array<U>></code> (insensé). </p> <p>Autres exemples :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// Type de retour d'une fonction T, introduit comme "R"</span> <span class="token keyword">type</span> ReturnType<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> infer <span class="token constant">R</span> <span class="token operator">?</span> <span class="token constant">R</span> <span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">;</span> <span class="token comment">// Type déduit "U" d'une Promise, d'un Array, d'une fonction, ou T</span> <span class="token keyword">type</span> Unpacked<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token punctuation">(</span>infer <span class="token constant">U</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">?</span> <span class="token constant">U</span> <span class="token operator">:</span> <span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> infer <span class="token constant">U</span> <span class="token operator">?</span> <span class="token constant">U</span> <span class="token operator">:</span> <span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token class-name">Promise</span><span class="token operator"><</span>infer <span class="token constant">U</span><span class="token operator">></span> <span class="token operator">?</span> <span class="token constant">U</span> <span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">;</span> <span class="token keyword">type</span> <span class="token constant">U1</span> <span class="token operator">=</span> Unpacked<span class="token operator"><</span><span class="token builtin">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span><span class="token punctuation">;</span> <span class="token comment">// number</span> <span class="token keyword">type</span> <span class="token constant">U2</span> <span class="token operator">=</span> Unpacked<span class="token operator"><</span><span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">>></span><span class="token punctuation">;</span> <span class="token comment">// string</span> <span class="token keyword">type</span> <span class="token constant">U3</span> <span class="token operator">=</span> Unpacked<span class="token operator"><</span>Unpacked<span class="token operator"><</span><span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">>>></span><span class="token punctuation">;</span> <span class="token comment">// string</span> <span class="token keyword">type</span> <span class="token constant">U4</span> <span class="token operator">=</span> Unpacked<span class="token operator"><</span>Date<span class="token operator">></span><span class="token punctuation">;</span> <span class="token comment">// Date</span> <span class="token comment">// Type (tuple) contenant les types des paramètres d'une fonction</span> <span class="token keyword">type</span> Parameters<span class="token operator"><</span><span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">any</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> infer <span class="token constant">P</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">any</span> <span class="token operator">?</span> <span class="token constant">P</span> <span class="token operator">:</span> <span class="token builtin">never</span><span class="token punctuation">;</span> <span class="token comment">// type T1 = Parameters<typeof Math.min> // [number[]]</span> <span class="token comment">// Type (tuple) contenant les types des paramètres d'un constructeur</span> <span class="token keyword">type</span> ConstructorParameters<span class="token operator"><</span><span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token class-name">new</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">any</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token constant">T</span> <span class="token keyword">extends</span> <span class="token class-name">new</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args<span class="token operator">:</span> infer <span class="token constant">P</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">any</span> <span class="token operator">?</span> <span class="token constant">P</span> <span class="token operator">:</span> <span class="token builtin">never</span><span class="token punctuation">;</span> <span class="token comment">// type T2 = ConstructorParameters<typeof Account></span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p> <br /> 🌟 <strong>Utiliser un “paramètre du reste” générique</strong></p> <p>Dans cet exemple, avec <code class="language-text">...T[]</code>.<br /> On définit un type <code class="language-text">ColorFilter</code> attendant soit : une et une seule valeur de type <code class="language-text">Color</code>, <strong>ou</strong> un opérateur <code class="language-text">'AND'</code> ou <code class="language-text">'OR'</code> suivi d’<strong>au moins</strong> deux valeurs de type <code class="language-text">Color</code>.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">type</span> FilterAndOr<span class="token operator"><</span><span class="token constant">T</span><span class="token punctuation">,</span> <span class="token constant">K</span> <span class="token operator">=</span> <span class="token string">'AND'</span> <span class="token operator">|</span> <span class="token string">'OR'</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token constant">T</span><span class="token punctuation">]</span> <span class="token operator">|</span> <span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">,</span> <span class="token constant">T</span><span class="token punctuation">,</span> <span class="token constant">T</span><span class="token punctuation">,</span> <span class="token operator">...</span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">type</span> Color <span class="token operator">=</span> <span class="token string">'red'</span> <span class="token operator">|</span> <span class="token string">'green'</span> <span class="token operator">|</span> <span class="token string">'blue'</span> <span class="token operator">|</span> <span class="token string">'yellow'</span> <span class="token operator">|</span> <span class="token string">'white'</span><span class="token punctuation">;</span> <span class="token comment">// Permet l'utilisation des valeurs:</span> <span class="token comment">// ["blue"]</span> <span class="token comment">// ["AND", "blue", "red"]</span> <span class="token comment">// ["OR", "yellow", "green", "red"]</span> <span class="token comment">// ["AND", "blue", "white", "red", "green"]</span> <span class="token keyword">type</span> ColorFilter <span class="token operator">=</span> FilterAndOr<span class="token operator"><</span>Color<span class="token operator">></span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p> <br /> 🌟 <strong>Etc, etc, etc</strong></p> <p>Il existe une infinité de cas d’usage des Generics et il m’est impossible d’en faire une liste exhaustive, cependant, comme tout bon outil, attention à ne pas tomber dans le piège habituel : quand on a un marteau, tout ressemble à un clou 😁.</p> <img src="https://media.giphy.com/media/l2JhucuqxI0UswQa4/giphy.gif" alt="weak-tools" style="width: 300px; margin: 20px auto; display: block;" /> <p>Pour toujours plus de TypeScript 💕, je vous invite à regarder <a href="https://www.youtube.com/watch?v=ET4kT88JRXs" target="_blank" rel="noopener noreferrer">cette vidéo</a> d’Anders Hejlsberg à la DotJS 2018 à Paris. <em>Enjoy!</em></p></div>Nest : Tests E2E et Effets de bordhttps://vinceops.me/nest-e2e-tests-effets-de-bord/2018-12-12T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/e041f28ed0e2f17e2ec5575b11d1e6cd/a22ce/wait-for-assertion.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABwqFB/8QAFxAAAwEAAAAAAAAAAAAAAAAAAAEQEf/aAAgBAQABBQKaj//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABYQAAMAAAAAAAAAAAAAAAAAAAAQMv/aAAgBAQAGPwJSf//EABoQAAIDAQEAAAAAAAAAAAAAAAABESExQWH/2gAIAQEAAT8h4KF6OtQrb0//2gAMAwEAAgADAAAAEAPP/8QAFhEBAQEAAAAAAAAAAAAAAAAAARAR/9oACAEDAQE/EBxn/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGhABAQACAwAAAAAAAAAAAAAAAREAITFBof/aAAgBAQABPxCiRQyesvppN+4MC4iVtec//9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/e041f28ed0e2f17e2ec5575b11d1e6cd/798b8/wait-for-assertion.webp 195w, /static/e041f28ed0e2f17e2ec5575b11d1e6cd/0e356/wait-for-assertion.webp 390w, /static/e041f28ed0e2f17e2ec5575b11d1e6cd/4c79d/wait-for-assertion.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/e041f28ed0e2f17e2ec5575b11d1e6cd/0e421/wait-for-assertion.jpg 195w, /static/e041f28ed0e2f17e2ec5575b11d1e6cd/ce2e0/wait-for-assertion.jpg 390w, /static/e041f28ed0e2f17e2ec5575b11d1e6cd/a22ce/wait-for-assertion.jpg 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/e041f28ed0e2f17e2ec5575b11d1e6cd/a22ce/wait-for-assertion.jpg" alt="wait-for-assertion" title="wait-for-assertion" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Dans le monde merveilleux des tests d’intégration et E2E (<em>end-to-end</em>, de “bout en bout” 🥐), il est fréquent de vérifier le bon fonctionnement d’un service tiers. Cependant, dans un scénario complet, les interactions avec ledit service sont parfois faites de manière asynchrone car non-critiques ou non-bloquantes. Alors comment les tester ?</p> <img src="https://media.giphy.com/media/Nm8ZPAGOwZUQM/giphy.gif" alt="question-cat" style="width: 300px; margin: 20px auto; display: block;" /> <hr /> <p>Notre application (API REST) est développée avec <a href="https://nestjs.com/" target="_blank" rel="noopener noreferrer">Nest</a>; <a href="https://jestjs.io/" target="_blank" rel="noopener noreferrer">Jest</a> sert de framework et lanceur de tests (unitaires, d’intégration, E2E), <a href="https://www.npmjs.com/package/supertest" target="_blank" rel="noopener noreferrer">supertest</a> est utilisé pour exécuter des assertions HTTP.</p> <p>Suite à un appel HTTP, on souhaite vérifier la mise à jour d’un document dans une base de données Elasticsearch, sachant que l’exécution de celle-ci est asynchrone : c’est un effet de bord. Le client reçoit une réponse <strong>avant</strong> que la mise à jour ne soit effective.</p> <blockquote> <p>🕺 Les exemples de code ci-après sont volontairement restreints à une forme simple et concise, facilitant leur lecture et leur compréhension</p> </blockquote> <h3> Test first</h3> <p>Une première approche naïve (et invalide 🤷) consiste à faire une assertion immédiate, considérant qu’Elasticsearch a déjà été mis à jour :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">'should update the value in Elasticsearch'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// appel HTTP</span> <span class="token keyword">await</span> <span class="token function">request</span><span class="token punctuation">(</span>server<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>endpointURL<span class="token punctuation">)</span> <span class="token comment">// PUT @ "/users/:userId"</span> <span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span> <span class="token comment">// { firstName: 'Mike' }</span> <span class="token punctuation">.</span><span class="token function">expect</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">NO_CONTENT</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// récupération de la donnée & assertion</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> document <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> elasticsearchService<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>UserIndex<span class="token punctuation">,</span> userId<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token function">expect</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>firstName<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBe</span><span class="token punctuation">(</span>updatedUser<span class="token punctuation">.</span>firstName<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>La majorité des exécutions de ce test se soldera par un échec puisque la réponse HTTP arrivera <strong>avant</strong> qu’Elastic n’ait reçu l’ordre de (ou n’ait pu) se mettre à jour. Cependant, l’essentiel y est. Il ne reste qu’à attendre le succès de notre assertion en la ré-exécutant jusqu’à ce qu’elle passe, ou que le test expire (<em>timeout</em>).</p> <p>Si le scénario n’est toujours pas clair, voici une ébauche du contrôleur gérant cette route de mise à jour :</p> <h3>Contrôleur</h3> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Put</span><span class="token punctuation">(</span><span class="token string">'/users/:userId'</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token function">updateUser</span><span class="token punctuation">(</span> @<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'userId'</span><span class="token punctuation">)</span> userId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> @<span class="token function">Body</span><span class="token punctuation">(</span>ValidationPipe<span class="token punctuation">)</span> user<span class="token operator">:</span> UserInputDto<span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersService<span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span>userId<span class="token punctuation">,</span> user<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>elasticsearchService<span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span>UserIndex<span class="token punctuation">,</span> <span class="token punctuation">{</span> userId<span class="token punctuation">,</span> <span class="token operator">...</span>user <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Contrairement à l’exécution de <code class="language-text">usersService.update</code>, le contrôleur n’attend pas celle de <code class="language-text">elasticsearchService.update</code> : il envoie immédiatement une réponse.</p> <blockquote> <p>⚠ Dans une application réelle, le déclenchement de la synchronisation d’Elasticsearch devrait être effectuée dans/par <code class="language-text">usersService</code>, avec (par exemple) un gestionnaire d’événements. Pas dans le code du contrôleur 😏.</p> </blockquote> <hr style="margin: 50px 150px;" /> <p>On souhaite donc écrire un flux consistant à :</p> <ol> <li>À intervalle fixe,</li> <li>(Ré-)Exécuter l’assertion,</li> <li>Ignorer l’erreur lancée s’il y en a une (échec de l’assertion), sauf runtime errors relevant d’un problème de code (<code class="language-text">TypeError</code>, <code class="language-text">ReferenceError</code>)</li> <li>“Compléter” si une valeur est émise 🎉, <strong>ou</strong> <em>timeout</em> si le temps imparti est écoulé 😿</li> </ol> <blockquote> <p>⏲ Dans Jest, le délai d’expiration d’un test est de 5 secondes par défaut. Il est possible de le modifier en utilisant <a href="https://jestjs.io/docs/en/jest-object.html#jestsettimeouttimeout" target="_blank" rel="noopener noreferrer">jest.setTimeout</a>.</p> </blockquote> <h3>Implémentation</h3> <p>Le test est modifié pour confier l’exécution de l’assertion à une fonction <code class="language-text">waitForAssertion</code>, responsable dudit flux :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">'should asynchronously update the value in Elasticsearch'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">await</span> <span class="token function">request</span><span class="token punctuation">(</span>server<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>endpointURL<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">expect</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">NO_CONTENT</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> <span class="token function">waitForAssertion</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> document <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> elasticsearchService<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>UserIndex<span class="token punctuation">,</span> userId<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token function">expect</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>firstName<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBe</span><span class="token punctuation">(</span>updatedUser<span class="token punctuation">.</span>firstName<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Et le code de <code class="language-text">waitForAssertion</code>, écrit avec <a href="https://rxjs-dev.firebaseapp.com" target="_blank" rel="noopener noreferrer">RxJS</a> (qui fait partie des dépendances de Nest) : <em>Il existe bien sûr d’autres moyens d’atteindre le même objectif, avec ou sans RxJs.</em></p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token keyword">from</span><span class="token punctuation">,</span> interval<span class="token punctuation">,</span> throwError <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'rxjs'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> catchError<span class="token punctuation">,</span> first<span class="token punctuation">,</span> switchMap<span class="token punctuation">,</span> timeout <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'rxjs/operators'</span><span class="token punctuation">;</span> <span class="token comment">/** * (Doc. et tests disponibles dans le Gist en fin d'article 📚) */</span> <span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">waitForAssertion</span><span class="token punctuation">(</span> <span class="token function-variable function">assertion</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">any</span><span class="token punctuation">,</span> timeoutDelay<span class="token operator">:</span> <span class="token builtin">number</span> <span class="token operator">=</span> <span class="token number">1000</span><span class="token punctuation">,</span> intervalDelay<span class="token operator">:</span> <span class="token builtin">number</span> <span class="token operator">=</span> <span class="token number">100</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// 1. À intervalle fixe,</span> <span class="token keyword">return</span> <span class="token function">interval</span><span class="token punctuation">(</span>intervalDelay<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span> <span class="token comment">// 2. (Ré-)Exécuter l'assertion,</span> <span class="token function">switchMap</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">from</span><span class="token punctuation">(</span><span class="token builtin">Promise</span><span class="token punctuation">.</span><span class="token function">resolve</span><span class="token punctuation">(</span><span class="token function">assertion</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 3. Ignorer l'erreur lancée s'il y en a une (échec de l'assertion), sauf runtime errors relevant d'un problème de code</span> <span class="token function">catchError</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> o</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span>err <span class="token keyword">instanceof</span> <span class="token class-name">ReferenceError</span> <span class="token operator">||</span> err <span class="token keyword">instanceof</span> <span class="token class-name">TypeError</span> <span class="token operator">?</span> <span class="token function">throwError</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token operator">:</span> o<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 4.1. "Compléter" si une valeur est émise 🎉,</span> <span class="token function">first</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 4.2. ou timeout si le temps imparti est écoulé 😿</span> <span class="token function">timeout</span><span class="token punctuation">(</span>timeoutDelay<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">toPromise</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <img src="https://media.giphy.com/media/EmMWgjxt6HqXC/giphy.gif" alt="cat-stream" style="width: 400px; margin: 20px auto; display: block;" /> <p>Code <strong>documenté et testé</strong> de <code class="language-text">waitForAssertion</code> : <a href="https://gist.github.com/VinceOPS/56e25ed3f22822202ec92abeb80eaa80" target="_blank" rel="noopener noreferrer">Gist</a>.</p></div>TypeScript : résolution des modules JSONhttps://vinceops.me/typescript-json-modules-resolution/2018-12-03T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/940dfbb4eb7d3f0a5084fbf30f09da47/a22ce/ts-resolveJsonModule.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAwAB/9oADAMBAAIQAxAAAAHMCjIs/8QAGBAAAgMAAAAAAAAAAAAAAAAAAAECEDH/2gAIAQEAAQUC0ca//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAGhAAAwADAQAAAAAAAAAAAAAAAAERITFBYf/aAAgBAQABPyFVOY8IadGj/9oADAMBAAIAAwAAABDz3//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABURAQEAAAAAAAAAAAAAAAAAAAAB/9oACAECAQE/EFf/xAAZEAEBAQADAAAAAAAAAAAAAAABMQARQVH/2gAIAQEAAT8QLJB4wODp4uUYb//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/940dfbb4eb7d3f0a5084fbf30f09da47/798b8/ts-resolveJsonModule.webp 195w, /static/940dfbb4eb7d3f0a5084fbf30f09da47/0e356/ts-resolveJsonModule.webp 390w, /static/940dfbb4eb7d3f0a5084fbf30f09da47/4c79d/ts-resolveJsonModule.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/940dfbb4eb7d3f0a5084fbf30f09da47/0e421/ts-resolveJsonModule.jpg 195w, /static/940dfbb4eb7d3f0a5084fbf30f09da47/ce2e0/ts-resolveJsonModule.jpg 390w, /static/940dfbb4eb7d3f0a5084fbf30f09da47/a22ce/ts-resolveJsonModule.jpg 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/940dfbb4eb7d3f0a5084fbf30f09da47/a22ce/ts-resolveJsonModule.jpg" alt="resolveJsonModule" title="resolveJsonModule" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Cette fonctionnalité de <a href="https://vinceops.me/2018/04/22/typescript-partie-1-3-presentation/" target="_blank" rel="noopener noreferrer">TypeScript</a> disponible depuis la <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html" target="_blank" rel="noopener noreferrer">version 2.9</a> du langage permet d’améliorer la sûreté du typage (<em>type safety</em>) dans quelques cas d’utilisations qui, bien qu’assez spécifiques, peuvent s’avérer critiques pour une application.</p> <p>L’article n’a pas pour vocation de faire l’inventaire desdits cas mais de présenter le concept avec des exemples pratiques, transposables à tout type d’applications.</p> <hr /> <h2>Configuration</h2> <p>La <a href="https://www.typescriptlang.org/docs/handbook/module-resolution.html" target="_blank" rel="noopener noreferrer">résolution des modules</a> <strong>JSON</strong> est activée par l’option de compilation <code class="language-text">resolveJsonModule</code> et permet aux développeurs d’écrire du code exploitant la structure (déduite) du JSON importé.</p> <p>Les exemples ci-après utilisent un fichier <code class="language-text">tsconfig.json</code> contenant notamment les valeurs suivantes :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token string">"compilerOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"module"</span><span class="token operator">:</span> <span class="token string">"commonjs"</span><span class="token punctuation">,</span> <span class="token string">"esModuleInterop"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token string">"resolveJsonModule"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token comment">// 💟</span> <span class="token string">"strict"</span><span class="token operator">:</span> <span class="token boolean">true</span>"<span class="token punctuation">,</span> <span class="token comment">// ...</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <h2>Cas pratiques</h2> <h3>Fichier de configuration</h3> <p>Il est commun d’utiliser un fichier de configuration dans les scripts et applications JS/TS. Dans l’exemple suivant (totalement fictif et scabreux, mais tout aussi démonstratif), un script est responsable de vérifier le bon fonctionnement (<em>health check</em>) de services d’envoi de SMS. </p> <blockquote> <p>sms-settings.json</p> </blockquote> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token punctuation">{</span> <span class="token string">"providers"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"twilio"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"apiKey"</span><span class="token operator">:</span> <span class="token string">"1b3b5543-e4c5"</span><span class="token punctuation">,</span> <span class="token string">"expectedMessage"</span><span class="token operator">:</span> <span class="token string">"Hello from Twilio"</span><span class="token punctuation">,</span> <span class="token string">"expectedStatus"</span><span class="token operator">:</span> <span class="token number">200</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token string">"messagebird"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"apiKey"</span><span class="token operator">:</span> <span class="token string">"b78ab91a-4fe5"</span><span class="token punctuation">,</span> <span class="token string">"expectedMessage"</span><span class="token operator">:</span> <span class="token string">"Hi from Messagebird"</span><span class="token punctuation">,</span> <span class="token string">"expectedStatus"</span><span class="token operator">:</span> <span class="token string">"200"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>Notez la valeur <code class="language-text">"200"</code> typée comme une chaîne de caractères dans <code class="language-text">messagebird.expectedStatus</code> (⚠ Spoiler alert : on veut provoquer une erreur de compilation 😁).</p> </blockquote> <p>Ces configurations sont importées et utilisées par le script suivant :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">/* script.ts */</span> <span class="token keyword">import</span> settings <span class="token keyword">from</span> <span class="token string">'./sms-settings.json'</span><span class="token punctuation">;</span> <span class="token comment">// [...]</span> <span class="token keyword">const</span> providers <span class="token operator">=</span> settings<span class="token punctuation">.</span>providers<span class="token punctuation">;</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> provider <span class="token keyword">of</span> Object<span class="token punctuation">.</span><span class="token function">entries</span><span class="token punctuation">(</span>providers<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">checkSmsProviderStatus</span><span class="token punctuation">(</span>provider<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> provider<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/* sms-check.ts */</span> <span class="token keyword">interface</span> <span class="token class-name">ISmsProviderConfiguration</span> <span class="token punctuation">{</span> apiKey<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> expectedMessage<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> expectedStatus<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">checkSmsProviderStatus</span><span class="token punctuation">(</span> <span class="token parameter">providerName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> config<span class="token operator">:</span> ISmsProviderConfiguration</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// [...]</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le type de <code class="language-text">providers</code> (Ligne 5) est ainsi déduit et conforme au contenu de <code class="language-text">sms-settings.json</code> 😍 :</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 416px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/71793d6806f3663daf8326f089a62bbc/76a14/2018-12-02_14-16-15.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 69.23076923076923%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHlXNIE/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABBQJf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAFxABAQEBAAAAAAAAAAAAAAAAEQABEP/aAAgBAQABPyFLY3mkX//aAAwDAQACAAMAAAAQMN//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAaEAEAAwEBAQAAAAAAAAAAAAABABEhQTGh/9oACAEBAAE/EBp9bdgeIBGzX7Fqq97BrIg6z//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/71793d6806f3663daf8326f089a62bbc/798b8/2018-12-02_14-16-15.webp 195w, /static/71793d6806f3663daf8326f089a62bbc/0e356/2018-12-02_14-16-15.webp 390w, /static/71793d6806f3663daf8326f089a62bbc/67d05/2018-12-02_14-16-15.webp 416w" sizes="(max-width: 416px) 100vw, 416px" type="image/webp" /> <source srcset="/static/71793d6806f3663daf8326f089a62bbc/0e421/2018-12-02_14-16-15.jpg 195w, /static/71793d6806f3663daf8326f089a62bbc/ce2e0/2018-12-02_14-16-15.jpg 390w, /static/71793d6806f3663daf8326f089a62bbc/76a14/2018-12-02_14-16-15.jpg 416w" sizes="(max-width: 416px) 100vw, 416px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/71793d6806f3663daf8326f089a62bbc/76a14/2018-12-02_14-16-15.jpg" alt="resolveJsonModule-typeof-providers" title="resolveJsonModule-typeof-providers" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Le type de <code class="language-text">provider[1]</code> (Ligne 8) peut être rapproché à :</p> <pre class="language-typescript"><code class="no-numbers-language-typescript"><span class="token punctuation">{</span> apiKey<span class="token punctuation">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> expectedMessage<span class="token punctuation">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> <span class="token comment">// 200 et "200" entraînent une déduction de : "string" ou "number"</span> expectedStatus<span class="token punctuation">:</span> <span class="token builtin">string</span> <span class="token operator">|</span> <span class="token builtin">number</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> </code></pre> <p>et n’est donc <strong>pas</strong> compatible avec le type <code class="language-text">ISmsProviderConfiguration</code> spécifié par <code class="language-text">checkSmsProviderStatus</code> (à cause du type de <code class="language-text">expectedStatus</code>) :</p> <pre class=" language-bash"><code class="no-numbers-language-bash">$ tsc script.ts:8:39 - error TS2345: Argument of <span class="token function">type</span> [...] is not assignable to parameter of <span class="token function">type</span> <span class="token string">'ISmsProviderConfiguration'</span>. [...] Types of property <span class="token string">'expectedStatus'</span> are incompatible. Type <span class="token string">'string'</span> is not assignable to <span class="token function">type</span> <span class="token string">'number'</span><span class="token keyword">.</span> 8 checkSmsProviderStatus<span class="token punctuation">(</span>provider<span class="token punctuation">[</span>0<span class="token punctuation">]</span>, provider<span class="token punctuation">[</span>1<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> ~~~~~~~~~~~ </code></pre> <p>En remplaçant <code class="language-text">"200"</code> par <code class="language-text">200</code> (<code class="language-text">expectedStatus</code>), le code compile sans erreur ✅<br /> C’est donc le compilateur qui garantie le bon typage des données de configuration : <strong>how cool is it?</strong></p> <img src="https://media.giphy.com/media/xTiQyBOIQe5cgiyUPS/giphy.gif" alt="not-bad" style="width: 400px; margin: 20px auto; display: block;" /> <p>Ce n’est pas tout ! Du point de vue de la <em>developer experience</em>, il existe un cas d’utilisation encore plus commun où cette fonctionnalité est très puissante : l’internationalisation d’une application.</p> <h3>Traductions</h3> <p>L’utilisation de tableaux associatifs, comme des fichiers de traductions (clé-valeur), accompagné de son exemple capilo-tracté au typage excessif totalement assumé ➡</p> <pre class=" language-javascript"><code class="no-numbers-language-javascript"><span class="token comment">// fr.json</span> <span class="token punctuation">{</span> <span class="token string">"Il n'a pas dit bonjour"</span><span class="token punctuation">:</span> <span class="token string">"Il n'a pas dit bonjour"</span><span class="token punctuation">,</span> <span class="token string">"Du coup, son code n'était pas clair"</span><span class="token punctuation">:</span> <span class="token string">"Du coup, son code n'était pas clair"</span> <span class="token punctuation">}</span> <span class="token comment">// en.json</span> <span class="token punctuation">{</span> <span class="token string">"Il n'a pas dit bonjour"</span><span class="token punctuation">:</span> <span class="token string">"He did not say hello"</span><span class="token punctuation">,</span> <span class="token string">"Du coup, son code n'était pas clair"</span><span class="token punctuation">:</span> <span class="token string">"So, his code was not clear"</span><span class="token punctuation">,</span> <span class="token string">"Oh, vraiment ?"</span><span class="token punctuation">:</span> <span class="token string">"O RLY?"</span> </code></pre> <p>Avec le code exploitant ces fichiers de traductions, découpée en plusieurs étapes :</p> <p><strong>1</strong> - Les imports et le typage des clés de traductions.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> frLocales <span class="token keyword">from</span> <span class="token string">'./fr.json'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> enLocales <span class="token keyword">from</span> <span class="token string">'./en.json'</span><span class="token punctuation">;</span> <span class="token comment">// union de toutes les propriétés communes à fr.json et en.json</span> <span class="token keyword">type</span> TranslationKeys <span class="token operator">=</span> <span class="token keyword">keyof</span> <span class="token punctuation">(</span><span class="token keyword">typeof</span> frLocales <span class="token operator">|</span> <span class="token keyword">typeof</span> enLocales<span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 595px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/eb6355d2ab73877d10399681bdbab6f4/90326/2018-12-02_22-42-15.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 16.923076923076923%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAADABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAHMqMwB/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFREBAQAAAAAAAAAAAAAAAAAAARD/2gAIAQMBAT8BJ//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAECAQE/AWf/xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAY/An//xAAVEAEBAAAAAAAAAAAAAAAAAAAQQf/aAAgBAQABPyGP/9oADAMBAAIAAwAAABAMD//EABYRAAMAAAAAAAAAAAAAAAAAAAEQMf/aAAgBAwEBPxAqv//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QH//EABgQAAMBAQAAAAAAAAAAAAAAAAABETEh/9oACAEBAAE/EFt13pWNXT//2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/eb6355d2ab73877d10399681bdbab6f4/798b8/2018-12-02_22-42-15.webp 195w, /static/eb6355d2ab73877d10399681bdbab6f4/0e356/2018-12-02_22-42-15.webp 390w, /static/eb6355d2ab73877d10399681bdbab6f4/05e3e/2018-12-02_22-42-15.webp 595w" sizes="(max-width: 595px) 100vw, 595px" type="image/webp" /> <source srcset="/static/eb6355d2ab73877d10399681bdbab6f4/0e421/2018-12-02_22-42-15.jpg 195w, /static/eb6355d2ab73877d10399681bdbab6f4/ce2e0/2018-12-02_22-42-15.jpg 390w, /static/eb6355d2ab73877d10399681bdbab6f4/90326/2018-12-02_22-42-15.jpg 595w" sizes="(max-width: 595px) 100vw, 595px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/eb6355d2ab73877d10399681bdbab6f4/90326/2018-12-02_22-42-15.jpg" alt="resolveJsonModule-i18n-keys" title="resolveJsonModule-i18n-keys" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><strong>2</strong> - La déclaration des langues et l’association des traductions correspondantes.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">type</span> Language <span class="token operator">=</span> <span class="token string">'en'</span> <span class="token operator">|</span> <span class="token string">'fr'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token constant">DEFAULT_LANG</span><span class="token operator">:</span> Language <span class="token operator">=</span> <span class="token string">'fr'</span><span class="token punctuation">;</span> <span class="token comment">// composition des langues possibles et de leurs traductions</span> <span class="token keyword">type</span> TranslationsMap <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span>l <span class="token keyword">in</span> Language<span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// module JSON: ensemble de couples clé-traduction</span> <span class="token punctuation">[</span>k <span class="token keyword">in</span> TranslationKeys<span class="token punctuation">]</span><span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token keyword">const</span> translations<span class="token operator">:</span> TranslationsMap <span class="token operator">=</span> <span class="token punctuation">{</span> fr<span class="token operator">:</span> frLocales<span class="token punctuation">,</span> en<span class="token operator">:</span> enLocales<span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><strong>3</strong> - Et l’exploitation de ces initialisations.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">function</span> <span class="token function">translate</span><span class="token punctuation">(</span><span class="token parameter">key<span class="token operator">:</span> TranslationKeys<span class="token punctuation">,</span> locale<span class="token operator">:</span> Language <span class="token operator">=</span> <span class="token constant">DEFAULT_LANG</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> translations<span class="token punctuation">[</span>locale<span class="token punctuation">]</span><span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token function">translate</span><span class="token punctuation">(</span><span class="token string">"Il n'a pas dit bonjour"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token function">translate</span><span class="token punctuation">(</span><span class="token string">"Du coup, son code n'était pas clair"</span><span class="token punctuation">,</span> <span class="token string">'en'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ===></span> <span class="token comment">// Il n'a pas dit bonjour</span> <span class="token comment">// So, his code was not clear</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 618px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/cd4e9364ea729051ce364f6bdbc0153a/1a8dd/2018-12-02_22-34-37.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 11.794871794871796%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAACABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAIDBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEA/9oADAMBAAIQAxAAAAHHuEiI/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAGRAAAQUAAAAAAAAAAAAAAAAAEQABEDFB/9oACAEBAAE/IS5tbH//2gAMAwEAAgADAAAAEIAv/8QAFREBAQAAAAAAAAAAAAAAAAAAARD/2gAIAQMBAT8QZ//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAECAQE/ECf/xAAaEAACAgMAAAAAAAAAAAAAAAAAAREhMUFR/9oACAEBAAE/EHuM9GrEn//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/cd4e9364ea729051ce364f6bdbc0153a/798b8/2018-12-02_22-34-37.webp 195w, /static/cd4e9364ea729051ce364f6bdbc0153a/0e356/2018-12-02_22-34-37.webp 390w, /static/cd4e9364ea729051ce364f6bdbc0153a/e571f/2018-12-02_22-34-37.webp 618w" sizes="(max-width: 618px) 100vw, 618px" type="image/webp" /> <source srcset="/static/cd4e9364ea729051ce364f6bdbc0153a/0e421/2018-12-02_22-34-37.jpg 195w, /static/cd4e9364ea729051ce364f6bdbc0153a/ce2e0/2018-12-02_22-34-37.jpg 390w, /static/cd4e9364ea729051ce364f6bdbc0153a/1a8dd/2018-12-02_22-34-37.jpg 618w" sizes="(max-width: 618px) 100vw, 618px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/cd4e9364ea729051ce364f6bdbc0153a/1a8dd/2018-12-02_22-34-37.jpg" alt="resolveJsonModule-autocomplete-i18n" title="resolveJsonModule-autocomplete-i18n" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p> </p> <p>Ce code présente quelques inconvénients :</p> <ul> <li>Les nouvelles langues ont besoin d’être importées et intégrées manuellement</li> <li>Il va (quelque peu 🙄) corser le <em>code splitting</em> et l’optimisation du <em>lazy loading</em> d’une application frontend</li> </ul> <p>Cependant, il a aussi quelques avantages :</p> <ul> <li>Autocomplétion des clés de traduction et des langues disponibles</li> <li><em>Type safety</em> des clés (et langues) utilisées (une clé supprimée ou modifiée par inadvertance empêchera la compilation du code)</li> <li>Garantie que chaque clé utilisée existe dans chaque fichier de traduction (à moins d’utiliser une intersection (<code class="language-text">typeof frLocales & typeof enLocales</code>) plutôt qu’une union pour autoriser toutes les clés, comme <code class="language-text">"Oh, vraiment ?"</code> présente seulement dans <code class="language-text">en.json</code>)</li> </ul> <img src="https://media.giphy.com/media/DfbpTbQ9TvSX6/giphy.gif" alt="mic-drop" style="width: 300px; margin: 20px auto; display: block;" /></div>Nest : Le framework Node.js qu'il nous fallaithttps://vinceops.me/nest-framework-nodejs-qu-il-nous-fallait/2018-08-03T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/46996226c601de04e807d46438e1dc60/a22ce/nest-header.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABwrCAf//EABcQAAMBAAAAAAAAAAAAAAAAAAARIhL/2gAIAQEAAQUCnMI//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGBAAAgMAAAAAAAAAAAAAAAAAASEAEDH/2gAIAQEABj8CxxA1/8QAGRABAAIDAAAAAAAAAAAAAAAAAQBhECFB/9oACAEBAAE/IR0KtF6W3H//2gAMAwEAAgADAAAAEIQP/8QAFhEBAQEAAAAAAAAAAAAAAAAAAREQ/9oACAEDAQE/EFsz/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGBABAQEBAQAAAAAAAAAAAAAAAREAIUH/2gAIAQEAAT8Qs9SQPnusQWRJMl6dN//Z'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/46996226c601de04e807d46438e1dc60/798b8/nest-header.webp 195w, /static/46996226c601de04e807d46438e1dc60/0e356/nest-header.webp 390w, /static/46996226c601de04e807d46438e1dc60/4c79d/nest-header.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/46996226c601de04e807d46438e1dc60/0e421/nest-header.jpg 195w, /static/46996226c601de04e807d46438e1dc60/ce2e0/nest-header.jpg 390w, /static/46996226c601de04e807d46438e1dc60/a22ce/nest-header.jpg 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/46996226c601de04e807d46438e1dc60/a22ce/nest-header.jpg" alt="nestjs" title="nestjs" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><a href="https://nestjs.com/" target="_blank" rel="noopener noreferrer">Nest</a> ne vous a probablement pas échappé si vous faites de la veille technique… Ou peut-être que si. Après tout, <a href="https://dayssincelastjavascriptframework.com/" target="_blank" rel="noopener noreferrer">0 jour</a> s’est écoulé depuis la sortie d’un nouveau framework JavaScript 😁. Mais alors pourquoi prendrait-on la peine d’en découvrir <strong>un de plus</strong> ?</p> <ul> <li><a href="#introduction">Introduction</a></li> <li> <p><a href="#existing">L’existant</a></p> <ul> <li><a href="#controllers">Contrôleurs</a></li> <li><a href="#middlewares">Middlewares</a></li> </ul> </li> <li><a href="#nest">NestJS</a></li> <li><a href="#conclusion">Conclusion</a></li> </ul> <hr /> <p><a name="introduction"></a></p> <h2>Introduction <a class="anchor-link" href="#introduction" style="font-size: 0.5em">📎</a></h2> <p>Avec l’essor fulgurant qu’a connu le développement web ces 10 dernières années, l’environnement <em>frontend</em> a considérablement évolué : l’apparition de frameworks et <em>libs</em> comme <a href="https://angular.io/" target="_blank" rel="noopener noreferrer">Angular</a>, <a href="https://reactjs.org/" target="_blank" rel="noopener noreferrer">React</a>, ou <a href="https://vuejs.org/" target="_blank" rel="noopener noreferrer">Vue.js</a>, ont permis aux développeurs d’être plus productifs, tout en construisant des applications plus volumineuses et élégantes, sans que leurs performances ne soient lésées. </p> <p>Dans l’environnement <em>backend</em>, cependant, malgré un écosystème <a href="http://www.modulecounts.com/" target="_blank" rel="noopener noreferrer">gigantesque</a>, de puissants outils et une communauté pro-active, un problème persiste : <strong>comment architecturer nos applications ?</strong></p> <img src="https://media.giphy.com/media/dXICCcws9oxxK/giphy.gif" alt="stark" style="width: 450px; margin: 20px auto; display: block;" /> <hr /> <p><a name="existing"></a></p> <h2>L’existant <a class="anchor-link" href="#existing" style="font-size: 0.5em">📎</a></h2> <p><a href="http://expressjs.com/" target="_blank" rel="noopener noreferrer">Express</a> est le plus poulaire des frameworks web Node.js. Il est aussi le plus <em>unopinionated</em> (sic), soit celui qui laisse le plus de choix aux développeurs en terme d’architecture applicative. Malheureusement, <em><strong>trop</strong> de choix</em> entraîne souvent la prise d’<em><strong>aucune</strong> (bonne) décision</em>. </p> <p><em>La plupart des faits suivants concernent aussi Hapi, Koa ou Fastify.</em></p> <p><a name="controllers"></a></p> <h3>Contrôleurs <a class="anchor-link" href="#controllers" style="font-size: 0.5em">📎</a></h3> <p>Dans un projet Express “traditionnel”, il est fréquent de voir des contrôleurs ressemblant à cela :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">ctrl app <span class="token operator">=</span> express<span class="token punctuation">.</span><span class="token function">Router</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> app<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/companies/:id'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> Companies<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>params<span class="token punctuation">.</span>id<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> company</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>err<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span>company<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>On en trouve des similaires, parfois par centaines, agrégés dans des arborescences de fichiers construites totalement arbitrairement.</p> <p>En l’état, ce contrôleur est pénible à tester. On peut extraire son callback dans une <code class="language-text">class</code> dédiée, puis améliorer sa lisibilité en utilisant <code class="language-text">async/await</code> et une version <em>promisified</em> de <code class="language-text">findById</code> :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">ctrl app <span class="token operator">=</span> express<span class="token punctuation">.</span><span class="token function">Router</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> app<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/companies/:id'</span><span class="token punctuation">,</span> companiesController<span class="token punctuation">.</span>getById<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// companies-controller.js</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">CompaniesController</span> <span class="token punctuation">{</span> <span class="token keyword">async</span> <span class="token function">getById</span><span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> company <span class="token operator">=</span> <span class="token keyword">await</span> Companies<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>params<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span>company<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><em>La gestion des erreurs est ignorée pour la simplicité des exemples.</em></p> <p>C’est mieux ! Le contrôleur est plus facilement testable. On évite aussi le <em>callback hell</em>.<br /> Pour autant, est-ce <strong>suffisant</strong> ? Du point de vue de la testabilité et du découplage, notre code souffre toujours de la présence de <code class="language-text">req</code> et <code class="language-text">res</code> (dans une moindre mesure, <code class="language-text">app</code>).</p> <p><strong>Avec Nest</strong>, voici à quoi ressemble un contrôleur équivalent :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Get</span><span class="token punctuation">(</span><span class="token string">'/companies/:id'</span><span class="token punctuation">)</span> <span class="token function">getById</span><span class="token punctuation">(</span>@<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> companyId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> Companies<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>companyId<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>🔥 <em>Exit</em> le couplage fort à Express (<code class="language-text">req</code>, <code class="language-text">res</code>, <code class="language-text">app</code>) et le <em>mocking</em> qui l’accompagnait dans les tests unitaires. Une simple fonction, facilement testable, indépendante de toute couche de transport (HTTP, WebSocket, …), retournant une valeur synchrone ou asynchrone (<code class="language-text">Promise</code>, <code class="language-text">Observable</code>) 🎉.</p> <p><a name="middlewares"></a></p> <h3>Middlewares <a class="anchor-link" href="#middlewares" style="font-size: 0.5em">📎</a></h3> <p>Si l’on souhaite exécuter avant notre contrôleur des actions assez simples telles que :</p> <ul> <li>L’authentification de l’émetteur de la requête,</li> <li>Du <em>logging</em>,</li> <li>La validation des données d’entrée,</li> <li>La transformation des données d’entrée,</li> </ul> <p>La solution <em>de facto</em> dans une application Express est le <strong>middleware</strong> : une fonction exécutée entre l’étape de <em>routing</em> et le contrôleur.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 661px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/9af2ab4d6960ee7c342e87564e1f5d24/4128c/express-middlewares.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 34.35897435897436%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsSAAALEgHS3X78AAACG0lEQVQoz2P43szA+SWHgfNzMgPnp3AGzjehDJz/d7pw1vz/z6xSs8hQtWaRqVjJHBOBglkmMuXzTXWalhn8//+f0bhnoxp/wWwD1pwZ2gzRPToMKZN1BIvnajN8nK7x4/M8s/+fFlj9/7TY8d/PZQ7/P09Uul8cHBwtU7X4v3jVkv/67Wv+m3at/6/QsPK/UPGcX8l+4dE65bNPC9eu/i9bvfS/Re+m/+rNq//zlSz4z/BymkHO86kGhS+mAfEM84JnM0xL3nRJJ6bHZZjKlC28qFm37IZD9/qr9l3rr1q0r7kpUzLvrKdvnJlm5dwFgkVzHihWzLvu07vqoVHLiqu8eTOvMuAFlW5iDGW+UgyhmpIMfvKSDCnWUgzNEWJgOY8+HoaceeKG7vFaAca2IQzmNaISuZMlGT7PMXzxZYHV+y+L7d99WWT79vMi269fp+leKM5y8lVYmvNNbGHKH52VRb/M11X+kl+a9U90XuLHuDRHH63yBQcFK5b8V65b+tO+d/1f9cYVv/iK5v5ieDXdsO3lVL0eIO5+OdO068Us0/63XcrlOemOLjILM14qLs5+b7a68q3p6oq3hqvKPkjPS30aEmfhplU+f5tAybxf6vVL3zj0bXin27ziPV/hnHc4fQuMSWaXrmwR585scdXacDGlmlAxs5Yk8dAJFaJAOSad2lk8TI5x/AzqfrwMzCZ8DIpevHzeabwAyN7oUwnn6MwAAAAASUVORK5CYII='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/9af2ab4d6960ee7c342e87564e1f5d24/798b8/express-middlewares.webp 195w, /static/9af2ab4d6960ee7c342e87564e1f5d24/0e356/express-middlewares.webp 390w, /static/9af2ab4d6960ee7c342e87564e1f5d24/07833/express-middlewares.webp 661w" sizes="(max-width: 661px) 100vw, 661px" type="image/webp" /> <source srcset="/static/9af2ab4d6960ee7c342e87564e1f5d24/25fa3/express-middlewares.png 195w, /static/9af2ab4d6960ee7c342e87564e1f5d24/506f3/express-middlewares.png 390w, /static/9af2ab4d6960ee7c342e87564e1f5d24/4128c/express-middlewares.png 661w" sizes="(max-width: 661px) 100vw, 661px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/9af2ab4d6960ee7c342e87564e1f5d24/4128c/express-middlewares.png" alt="express-middlewares" title="express-middlewares" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> <em>Dans les faits, un middleware peut interrompre un cycle Requête-Réponse en retournant une réponse.</em> </p> <p>Ce qui peut se traduire comme ceci :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">ctrl app <span class="token operator">=</span> express<span class="token punctuation">.</span><span class="token function">Router</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> app<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span> <span class="token string">'/companies/:id'</span><span class="token punctuation">,</span> <span class="token comment">// router-level middlewares</span> <span class="token punctuation">[</span>fnAuthenticate<span class="token punctuation">,</span> fnLog<span class="token punctuation">,</span> fnValidate<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">// controller : fait partie de la série</span> companiesController<span class="token punctuation">.</span>createCompany <span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>On peut bien sûr en exécuter <strong>après</strong> un contrôleur. Et il existe aussi des middlewares spéciaux permettant la gestion des erreurs HTTP (<code class="language-text">404</code>, <code class="language-text">500</code>, …). </p> <p>Par conséquent, dans bien des projets, on confie aux middlewares : l’authentification, le <em>logging</em>, la transformation et la validation des données d’entrée/sortie, la gestion des erreurs, … 🤔<br /> <strong>Tout va bien</strong> ! Express <a href="http://expressjs.com/en/guide/using-middleware.html" target="_blank" rel="noopener noreferrer">est décrit</a> comme un framework de routing/middlewares dont les applications sont essentiellement des séries de Middlewares. Il n’y a donc rien d’anormal à ce que toute l’application soit construite comme telle… Pour autant, est-ce <strong>suffisant</strong> ?</p> <p>❔ Quid de l’évolutivité, de la testabilité et de la maintenabilité d’une telle application ? Quelles meilleures pratiques suivre ? Faut-il les définir en interne et s’assurer que chaque nouveau collaborateur les applique ?</p> <hr /> <p><a name="nest"></a></p> <h2>NestJS <a class="anchor-link" href="#nest" style="font-size: 0.5em">📎</a></h2> <p>Et la question fatidique… Qu’apporte réellement Nest en terme de <strong>productivité</strong>, de <strong>maintenabilité</strong>, de <strong>testabilité</strong> et d’<strong>architecture</strong> ? </p> <hr style="margin: 50px 150px;" /> <h4>💓 Typescript</h4> <p>Conçu <em>avec</em> TypeScript <em>pour</em> des applications TypeScript, Nest exploite et apporte toute la puissance du langage (que j’ai longuement présenté <a href="https://vinceops.me/2018/04/22/typescript-partie-2-3-pourquoi-ladopter/" target="_blank" rel="noopener noreferrer">ici</a>), y compris toutes les fonctionnalités d’<code class="language-text">ES2015</code>, <code class="language-text">ES2016</code> et <code class="language-text">ES2017</code>. De fait, Node.js v8.9+ est <a href="https://node.green/" target="_blank" rel="noopener noreferrer">recommandé</a> pour un support complet. Sinon, transpilez vers une <code class="language-text">target</code> plus ancienne. </p> <blockquote> <p>Bien qu’il soit possible de développer une application Nest avec JavaScript et Babel, il est recommandé d’utiliser TypeScript pour une expérience optimale.<br /> 🧙 ”<strong><em>Types are priceless</em></strong>” - Kamil Myśliwiec</p> </blockquote> <hr style="margin: 50px 150px;" /> <h4>🍱 Modularité</h4> <p>La philosophie du framework est fidèle aux principes de conception <a href="https://en.wikipedia.org/wiki/SOLID" target="_blank" rel="noopener noreferrer">SOLID</a> ainsi qu’à la <a href="https://en.wikipedia.org/wiki/Separation_of_concerns" target="_blank" rel="noopener noreferrer">séparation des intérêts</a>. À ce titre, le code d’une application Nest se découpe généralement en <strong>Modules</strong> constitués de <strong>Composants</strong> ou <em>providers</em> et éventuellement de <strong>Contrôleurs</strong>.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span> imports<span class="token operator">:</span> <span class="token punctuation">[</span>OAuthModule<span class="token punctuation">]</span><span class="token punctuation">,</span> controllers<span class="token operator">:</span> <span class="token punctuation">[</span>AuthController<span class="token punctuation">]</span><span class="token punctuation">,</span> providers<span class="token operator">:</span> <span class="token punctuation">[</span>AuthService<span class="token punctuation">]</span><span class="token punctuation">,</span> exports<span class="token operator">:</span> <span class="token punctuation">[</span>AuthService<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AuthModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>Les développeurs Angular noteront la proximité syntaxique avec <code class="language-text">@NgModule</code>.</p> </blockquote> <p>Cette séparation du code par tâche logique (ou métier) en <a href="https://docs.nestjs.com/modules" target="_blank" rel="noopener noreferrer">modules</a> encourage l’isolation et la réutilisation des composants de l’application.</p> <hr style="margin: 50px 150px;" /> <h4>📦 Injection de dépendances</h4> <p>Les <em>controllers</em> et <em>providers</em> peuvent injecter (via leur constructeur) d’autres <em>providers</em> grâce au conteneur d’<strong>injection de dépendances</strong> de Nest.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Controller</span><span class="token punctuation">(</span><span class="token string">'auth'</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AuthController</span> <span class="token punctuation">{</span> <span class="token comment">// this.authService est assigné dans le constructeur</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">private</span> <span class="token keyword">readonly</span> authService<span class="token operator">:</span> AuthService</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Il est aussi possible de définir des <a href="https://docs.nestjs.com/fundamentals/custom-providers" target="_blank" rel="noopener noreferrer"><em>providers</em> personnalisés</a> (et <a href="https://docs.nestjs.com/fundamentals/async-providers" target="_blank" rel="noopener noreferrer">asynchrones</a>), afin d’injecter des bibliothèques et modules npm, des valeurs calculées dynamiquement… Par exemple :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// user-service.provider.ts</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token constant">USER_SERVICE</span> <span class="token operator">=</span> <span class="token string">'UserService'</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">const</span> UserServiceProvider <span class="token operator">=</span> <span class="token punctuation">{</span> provide<span class="token operator">:</span> <span class="token constant">USER_SERVICE</span><span class="token punctuation">,</span> useClass<span class="token operator">:</span> FirebaseService<span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Nest injecte les dépendances grâce à un <em>token</em> : on utilise <code class="language-text">USER_SERVICE</code> pour identifier notre service de gestion des utilisateurs.<br /> <code class="language-text">@Inject()</code> indique à Nest <em>quoi</em> injecter, quand l’annotation de type ne suffit pas à l’identifier :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// auth.service.ts : Injection + Typage par Interface</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter">@<span class="token function">Inject</span><span class="token punctuation">(</span><span class="token constant">USER_SERVICE</span><span class="token punctuation">)</span> <span class="token keyword">private</span> <span class="token keyword">readonly</span> userService<span class="token operator">:</span> IUserService</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>Ici, on admet évidemment que <code class="language-text">FirebaseService</code> implémente l’interface <code class="language-text">IUserService</code> et qu’<code class="language-text">UserServiceProvider</code> a été ajouté aux <code class="language-text">providers</code> d’<code class="language-text">AuthModule</code>.<br /> 👍 <code class="language-text">AuthService</code> est désormais découplé : il dépend d’une implémentation d’<code class="language-text">IUserService</code> mais ignore laquelle.</p> <blockquote> <p>Ce n’est pas la façon la plus élégante de régler ce problème, mais elle a le mérite de ne pas trop alourdir l’article. Si le sujet vous intéresse, n’hésitez pas à m’en faire part.</p> </blockquote> <hr style="margin: 50px 150px;" /> <h4>✅ Testabilité</h4> <p>Le test des composants est très largement facilité par :</p> <ul> <li>L’injection de dépendance : toute dépendance peut être facilement simulée (<em>mock</em>) avant d’être passée en paramètre d’un constructeur. </li> <li>L’aspect <em>framework agnostic</em> de Nest : il n’y a pas (ou rarement) besoin de <em>mock</em> des objets propres à Express (<code class="language-text">req</code>, <code class="language-text">res</code>…) et toute dépendance externe (package npm, etc) peut aussi être injectée à l’aide d’un <em>custom provider</em>.</li> </ul> <p>Le test de l’application (End-to-End) est quelque peu facilité par la modularité de celle-ci. En effet, Nest fournit un <a href="https://docs.nestjs.com/fundamentals/e2e-testing" target="_blank" rel="noopener noreferrer"><code class="language-text">TestingModule</code></a> dans lequel on importe un à plusieurs modules pour lancer notre application. Grâce (encore) aux <em>custom providers</em>, on peut, au besoin, remplacer certains composants : par exemple, pour tester différentes implémentations d’une interface <code class="language-text">ILambdaService</code> (AWS Lambda, Google Cloud Functions, …) <strong>ou</strong> deux états d’une même implémentation (service disponible et service hors-ligne/en maintenance).</p> <hr style="margin: 50px 150px;" /> <h4>💥 Gestion des erreurs</h4> <p>Autre fonctionnalité, les <strong>filtres d’exception</strong> permettent la gestion des exceptions lancées par l’application. Dans un contrôleur Nest, aucun <code class="language-text">return res.status(404).end()</code>, ni même d’import du brave <a href="https://www.npmjs.com/package/boom" target="_blank" rel="noopener noreferrer">boom</a>. Le framework inclut des classes <code class="language-text">NotFoundException</code>, <code class="language-text">ForbiddenException</code>, etc, toutes filles de <code class="language-text">HttpException</code>.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token function">findOne</span><span class="token punctuation">(</span>@<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> userId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userService<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span><span class="token punctuation">{</span> userId <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>user<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">NotFoundException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> user<span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Les <em>exception filters</em> sont exécutés en fin de chaîne, pour les types d’exceptions qu’on leur attribue (si aucun type n’est attribué, alors le filtre est exécuté pour chaque exception). C’est dans la méthode <code class="language-text">catch</code> du filtre que l’on va retourner une réponse au client.</p> <blockquote> <p>🐨 Les Exception Filters peuvent injecter des dépendances</p> </blockquote> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// filtre pour : NotFoundException et ForbiddenException</span> @<span class="token function">Catch</span><span class="token punctuation">(</span>NotFoundException<span class="token punctuation">,</span> ForbiddenException<span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">HttpExceptionFilter</span> <span class="token keyword">implements</span> <span class="token class-name">ExceptionFilter</span> <span class="token punctuation">{</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">private</span> <span class="token keyword">readonly</span> loggerService<span class="token operator">:</span> LoggerService</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token keyword">catch</span><span class="token punctuation">(</span>exception<span class="token operator">:</span> HttpException<span class="token punctuation">,</span> host<span class="token operator">:</span> ArgumentsHost<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>loggerService<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">'Uncaught exception'</span><span class="token punctuation">,</span> exception<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// dans le cas d'un cycle Requête-Réponse via HTTP</span> <span class="token keyword">const</span> res <span class="token operator">=</span> host<span class="token punctuation">.</span><span class="token function">switchToHttp</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getResponse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// par exemple :</span> <span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>exception<span class="token punctuation">.</span><span class="token function">getStatus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <hr style="margin: 50px 150px;" /> <h4>🚇 Pipes</h4> <p>Les Pipes permettent la transformation et/ou la validation d’une donnée d’entrée. Prenons l’exemple d’un contrôleur permettant l’écriture d’un commentaire dans un article :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Post</span><span class="token punctuation">(</span><span class="token string">'article/:articleId/comment'</span><span class="token punctuation">)</span> <span class="token function">createComment</span><span class="token punctuation">(</span> @<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'articleId'</span><span class="token punctuation">)</span> articleId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> @<span class="token function">Body</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">ValidationPipe</span><span class="token punctuation">(</span><span class="token punctuation">{</span> whitelist<span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span> comment<span class="token operator">:</span> CreateCommentDto<span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>articleService<span class="token punctuation">.</span><span class="token function">addComment</span><span class="token punctuation">(</span>comment<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Il est facile de créer ses propres Pipes, mais on utilise ici un <code class="language-text">Pipe</code> intégré : <code class="language-text">ValidationPipe</code>. Il effectue la validation d’une donnée passée en entrée, en fonction d’un modèle défini en amont (ici, <code class="language-text">CreateCommentDto</code>), à l’aide du paquet <a href="https://github.com/typestack/class-validator" target="_blank" rel="noopener noreferrer"><code class="language-text">class-validator</code></a> et de ses précieux décorateurs :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">CreateCommentDto</span> <span class="token punctuation">{</span> @<span class="token function">IsString</span><span class="token punctuation">(</span><span class="token punctuation">)</span> @<span class="token function">IsNotEmpty</span><span class="token punctuation">(</span><span class="token punctuation">)</span> @<span class="token function">MinLength</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">)</span> content<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> @<span class="token function">IsString</span><span class="token punctuation">(</span><span class="token punctuation">)</span> @<span class="token function">IsNotEmpty</span><span class="token punctuation">(</span><span class="token punctuation">)</span> @<span class="token function">IsEmail</span><span class="token punctuation">(</span><span class="token punctuation">)</span> author<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Par défaut, si le corps de la requête (d’où le décorateur <code class="language-text">@Body()</code>) ne correspond pas au modèle attendu, <code class="language-text">ValidationPipe</code> interrompt le cycle Requête-Réponse pour lancer une <code class="language-text">BadRequestException</code> (<em>aka Error 400</em>). </p> <blockquote> <p>🐨 Les Pipes peuvent injecter des dépendances</p> </blockquote> <hr style="margin: 50px 150px;" /> <h4>🎣 Interceptors</h4> <p>Exécutés avant <strong>et</strong> après un contrôleur, les Interceptors peuvent, entre autres :</p> <ul> <li>Transformer la valeur (ou l’exception) retournée par un contrôleur (avant de l’envoyer)</li> <li>Empêcher l’exécution d’un contrôleur (en répondant à sa place) sous certaines conditions</li> </ul> <p>Cette manipulation de flux est possible grâce au puissant <a href="http://reactivex.io/rxjs/" target="_blank" rel="noopener noreferrer">RxJS</a>. </p> <p>Voici un exemple d’intercepteur (tiré de la documentation de Nest) qui interrompt le cycle Requête-Réponse en émettant une erreur si aucune valeur n’est émise sur le flux (nommé ici <code class="language-text">call$</code>) sous 5 secondes (cf. <a href="http://reactivex.io/documentation/operators/timeout.html" target="_blank" rel="noopener noreferrer">timeout</a>).</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TimeoutInterceptor</span> <span class="token keyword">implements</span> <span class="token class-name">NestInterceptor</span> <span class="token punctuation">{</span> <span class="token function">intercept</span><span class="token punctuation">(</span> context<span class="token operator">:</span> ExecutionContext<span class="token punctuation">,</span> call$<span class="token operator">:</span> Observable<span class="token operator"><</span><span class="token builtin">any</span><span class="token operator">></span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token operator">:</span> Observable<span class="token operator"><</span><span class="token builtin">any</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> call$<span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">timeout</span><span class="token punctuation">(</span><span class="token number">5000</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>🐨 Les Interceptors peuvent injecter des dépendances</p> </blockquote> <hr style="margin: 50px 150px;" /> <h4>🛡 Guards</h4> <p>Un Guard détermine si le contrôleur <em>gardé</em> doit traiter la requête entrante. Dans le cas contraire, une exception <code class="language-text">ForbiddenException</code> (<em>aka Error 403</em>) est lancée. Les guards sont exécutés après les <a href="https://docs.nestjs.com/middleware" target="_blank" rel="noopener noreferrer">Middlewares</a> et les <a href="https://docs.nestjs.com/pipes" target="_blank" rel="noopener noreferrer">Pipes</a>, mais avant les <a href="https://docs.nestjs.com/interceptors" target="_blank" rel="noopener noreferrer">Interceptors</a>. </p> <p>Ils permettent de résumer à un ou deux décorateurs le contrôle d’accès à un contrôleur (ou à toute une classe <code class="language-text">Controller</code>, ou à toute l’application) :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Post</span><span class="token punctuation">(</span><span class="token punctuation">)</span> @<span class="token function">UseGuards</span><span class="token punctuation">(</span>RolesGuard<span class="token punctuation">)</span> @<span class="token function">Roles</span><span class="token punctuation">(</span><span class="token string">'admin'</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token function">createCompany</span><span class="token punctuation">(</span><span class="token parameter">@<span class="token function">Body</span><span class="token punctuation">(</span><span class="token punctuation">)</span> createCompanyDto<span class="token operator">:</span> CreateCompanyDto</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>companyService<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>createCompanyDto<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Dans cet exemple, on a créé un décorateur <code class="language-text">Roles</code> qui ajoute de la <a href="https://github.com/rbuckton/reflect-metadata" target="_blank" rel="noopener noreferrer">metadata</a> à notre méthode <code class="language-text">createCompany</code> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> ReflectMetadata <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">Roles</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>roles<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">ReflectMetadata</span><span class="token punctuation">(</span><span class="token string">'roles'</span><span class="token punctuation">,</span> roles<span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>Le guard <code class="language-text">RolesGuard</code> a accès à <code class="language-text">createCompany</code> et peut lire cette metadata. Ainsi, les utilisateurs n’ayant pas le rôle <code class="language-text">admin</code> se verront refuser le traitement de leur requête :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript">@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">RolesGuard</span> <span class="token keyword">implements</span> <span class="token class-name">CanActivate</span> <span class="token punctuation">{</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">private</span> <span class="token keyword">readonly</span> reflector<span class="token operator">:</span> Reflector</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token function">canActivate</span><span class="token punctuation">(</span>context<span class="token operator">:</span> ExecutionContext<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">boolean</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> roles <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>reflector<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token string">'roles'</span><span class="token punctuation">,</span> context<span class="token punctuation">.</span><span class="token function">getHandler</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ... logique d'autorisation</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Bien sûr, on peut imaginer toutes sortes de Guards : utiliser une <em>strategy</em> <a href="http://www.passportjs.org/" target="_blank" rel="noopener noreferrer">passportjs</a>, dépendre d’une base de données ou d’un service tiers, etc.</p> <blockquote> <p>🐨 Les Guards peuvent injecter des dépendances et être asynchrones</p> </blockquote> <hr style="margin: 50px 150px;" /> <h4>🛠 Outils et intégrations</h4> <p>Parce qu’il est impossible de faire le tour complet de Nest en un seul article, mais qu’ils sont quand même un argument majeur du framework, notez qu’il existe des intégrations TypeScript et outils pour : </p> <ul> <li><a href="https://docs.nestjs.com/techniques/database" target="_blank" rel="noopener noreferrer">ORM</a> (<a href="http://typeorm.io" target="_blank" rel="noopener noreferrer">TypeORM</a>, <a href="http://docs.sequelizejs.com/" target="_blank" rel="noopener noreferrer">Sequelize</a>) et <a href="https://docs.nestjs.com/techniques/mongodb" target="_blank" rel="noopener noreferrer">ODM</a> (<a href="http://mongoosejs.com/" target="_blank" rel="noopener noreferrer">Mongoose</a>) </li> <li><a href="https://docs.nestjs.com/graphql/quick-start" target="_blank" rel="noopener noreferrer">GraphQL</a> (Apollo)</li> <li><a href="https://docs.nestjs.com/recipes/swagger" target="_blank" rel="noopener noreferrer">OpenAPI</a> (Swagger)</li> <li><a href="https://docs.nestjs.com/websockets/gateways" target="_blank" rel="noopener noreferrer">Websockets</a> (<a href="https://socket.io/" target="_blank" rel="noopener noreferrer">socket.io</a>)</li> <li><a href="https://docs.nestjs.com/microservices/basics" target="_blank" rel="noopener noreferrer">Microservices</a> (TCP, Redis Pub/Sub, MQTT, gRPC, etc.)</li> <li><a href="https://docs.nestjs.com/techniques/authentication" target="_blank" rel="noopener noreferrer">L’authentification</a> (<a href="http://www.passportjs.org/" target="_blank" rel="noopener noreferrer">Passport</a>) </li> </ul> <p><em>Liste non exhaustive</em></p> <hr /> <p><a name="conclusion"></a></p> <h2>Conclusion <a class="anchor-link" href="#conclusion" style="font-size: 0.5em">📎</a></h2> <p>En résumé, <strong>Nest est</strong> : un cadre structurant encourageant les bonnes pratiques, <em>framework agnostic</em>, portant une architecture modulaire, offrant de l’injection de dépendances et de puissants composants tels que les Pipes et Interceptors, accompagné d’intégrations élégantes, le tout écrit avec/pour TypeScript.</p> <p><em>Bien que ça n’ait pas été abordé, Nest permet aussi la création d’application de type CLI, script, <a href="https://docs.nestjs.com/execution-context" target="_blank" rel="noopener noreferrer">etc</a>.</em></p></div>TypeScript - 3/3 : Installation & Migrationhttps://vinceops.me/typescript-3-installation-migration/2018-04-22T12:02:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAB7CAAAewgFu0HU+AAAA7klEQVQoz22Qz0sCQRiGR/OwsoQWsRdPHSLBJOqWZBAptFBBaih46CDeomPLrJeOHj32P33zR/nsz2bdHXh4v1/zzscoFZ3Q9OEdOqrqhOYUxnBk1bzSnJa82SH5RS/Qc3SiQhkobS7hnnwFQzij/oY+JAvIC3oHP3Bjb+DAGpbwBXP4gD/4htvkERPEF7WZYbaBIDXcsYhnG7bgGj4Z8mlGJs+p8SsGPXSRmk7hihkf3SbbSlTvZmaZHsOIwRP0yXrM5UIT3DRvJyrREo/W/zlFQ20axPXqT87MpVbZyz3koJDFJaSoWv57cSy5xx5wuY69pAY4hwAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/798b8/ts-banner.webp 195w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/0e356/ts-banner.webp 390w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/4c79d/ts-banner.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/25fa3/ts-banner.png 195w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/506f3/ts-banner.png 390w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png" alt="typescript-logo" title="typescript-logo" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><em>Suite de la <a href="https://vinceops.me/typescript-2-pourquoi-l-adopter">deuxième partie</a> du dossier.</em></p> <h2>Installation</h2> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> -g typescript</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Le paquet <a href="https://www.npmjs.com/package/typescript" target="_blank" rel="noopener noreferrer">typescript</a> contient le compilateur <code class="language-text">tsc</code> découvert dans la première partie du dossier.</p> <p>L’installation du paquet <a href="https://www.npmjs.com/package/tslint" target="_blank" rel="noopener noreferrer">tslint</a> est aussi conseillée (globale, ou locale à votre projet).</p> <p>Dans le répertoire du projet, sont requis (<em>recommandés</em>) :</p> <ul> <li>Un fichier <code class="language-text">tsconfig.json</code>, généré avec la commande <code class="language-text">tsc --init</code> ou récupéré d’un projet type <em>boilerplate</em>.</li> <li>Un fichier <code class="language-text">tslint.json</code>, généré avec la commande <code class="language-text">tslint --init</code> ou récupéré d’un projet type <em>boilerplate</em>. Pour ”<em>linter</em>” le projet, on exécute <code class="language-text">tslint -p .</code> à la racine du projet. </li> </ul> <blockquote> <p>💡 <code class="language-text">-p</code> indique l’emplacement de <code class="language-text">tsconfig.json</code> (<a href="https://palantir.github.io/tslint/usage/cli/" target="_blank" rel="noopener noreferrer">détails</a>)</p> </blockquote> <ul> <li>Un éditeur de code avec un support décent de TypeScript : VSCode, WebStorm, …</li> </ul> <h3>Intégration</h3> <h4>Frontend</h4> <p>Il existe des configurations/plugins pour les principaux outils de transpilation et <em>packaging</em>:</p> <ul> <li>Avec <a href="https://babeljs.io/blog/2017/09/12/planning-for-7.0" target="_blank" rel="noopener noreferrer">Babel 7</a> et le <em>preset</em> <a href="https://github.com/babel/babel/tree/master/packages/babel-preset-typescript" target="_blank" rel="noopener noreferrer">babel-preset-typescript</a></li> <li>Avec Webpack et <a href="https://github.com/TypeStrong/ts-loader" target="_blank" rel="noopener noreferrer">ts-loader</a> ou <a href="https://github.com/s-panferov/awesome-typescript-loader" target="_blank" rel="noopener noreferrer">awesome-typescript-loader</a>; d’avantage d’informations dans la <a href="https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html" target="_blank" rel="noopener noreferrer">documentation officielle</a></li> <li>Avec Rollup et <a href="https://github.com/ezolenko/rollup-plugin-typescript2" target="_blank" rel="noopener noreferrer">rollup-plugin-typescript2</a></li> <li>Pour Browserify, Grunt, Gulp, référez-vous à la <a href="https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html" target="_blank" rel="noopener noreferrer">documentation officielle</a> </li> </ul> <h4>Backend et scripts</h4> <p><a href="https://github.com/TypeStrong/ts-node" target="_blank" rel="noopener noreferrer">ts-node</a> accélère légèrement le processus de développement, en permettant à node de charger des modules TypeScript (transpilés en mémoire lors de l’import). </p> <p>Il peut être utilisé en ligne de commande, pour exécuter directement des fichiers TypeScript :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">ts-node index.ts</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>ou bien exécuté comme module, dans un point d’entrée JavaScript classique (<code class="language-text">node index.js</code>) :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'ts-node'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">register</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// on peut ensuite importer un fichier TypeScript (./src/index.ts)</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./src/index'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <h2>Migration</h2> <p>L’intégration progressive de TypeScript dans un projet JavaScript existant est un bon exercice pour adopter le langage. </p> <h3>Adapter le code existant</h3> <ul> <li>Activer <code class="language-text">allowJs</code> (<code class="language-text">tsconfig.json</code>) afin que le code existant soit intégré au code généré par <code class="language-text">tsc</code>.</li> <li>Procéder à la modification du code, fichier par fichier, à l’aide de l’annotation <code class="language-text">// @ts-check</code>.</li> </ul> <h4>@ts-check</h4> <p><code class="language-text">tsc</code> effectue l’analyse statique du code JavaScript contenant le commentaire <code class="language-text">// @ts-check</code>. Il est aussi possible d’utiliser l’option <code class="language-text">--checkJs</code> (CLI de <code class="language-text">tsc</code>, ou dans <code class="language-text">tsconfig.json</code>) pour analyser <strong>tous</strong> les fichiers JavaScript passés en entrée.</p> <blockquote> <p>💡 Si vous optez pour <code class="language-text">checkJs</code>, utilisez <code class="language-text">@ts-nocheck</code> pour exclure certains fichiers JavaScript de l’analyse. Pour exclure seulement certaines lignes de code, utilisez <code class="language-text">@ts-ignore</code> (avant chaque ligne). </p> </blockquote> <p>Mieux encore : TypeScript peut <em>aussi</em> utiliser votre <strong>JSDoc</strong> pour <a href="https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files" target="_blank" rel="noopener noreferrer">trouver des erreurs</a> dans votre code.</p> <p> Voici un exemple, <code class="language-text">normalize-name.js</code> :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">// @ts-check</span> <span class="token comment">/** * @param name Name to be normalized * * @return Normalized name. */</span> <span class="token keyword">function</span> <span class="token function">normalizeName</span><span class="token punctuation">(</span><span class="token parameter">name</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> name<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// Erreur : 5 n'est pas une string</span> <span class="token function">normalizeName</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Erreur : normalizeName retourne une string (calcul impossible)</span> <span class="token keyword">const</span> a <span class="token operator">=</span> <span class="token number">3</span> <span class="token operator">*</span> <span class="token function">normalizeName</span><span class="token punctuation">(</span><span class="token string">'MiKe'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>À la compilation <code class="language-text">tsc</code> renvoie les erreurs suivantes :</p> <blockquote> <p>❌ normalize-name.<strong>js</strong>(13,15): error TS2345: Argument of type ‘5’ is not assignable to parameter of type ‘string’.<br /> ❌ normalize-name.<strong>js</strong>(15,15): error TS2363: The right-hand side of an arithmetic operation must be of type ‘any’, ‘number’ or an enum type.</p> </blockquote> <p>Par contre, dans le code code ci-dessus, passer <code class="language-text">undefined</code> à <code class="language-text">normalizeName</code> n’est pas signalé comme une erreur par <code class="language-text">tsc</code>. En effet, il s’agit de <strong>JavaScript</strong>, non de TypeScript. Il en va de même avec <strong>TypeScript</strong>, si l’option <code class="language-text">strict</code> n’est pas activée : <code class="language-text">tsc</code> transpile l’instruction sans nous avertir. </p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token function">normalizeName</span><span class="token punctuation">(</span><span class="token keyword">undefined</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Cette instruction provoquera l’erreur <code class="language-text">TypeError: Cannot read property 'trim' of undefined</code> à <strong>l’exécution</strong>. Une erreur de <em>runtime</em> facilement évitable avec TypeScript <strong>et</strong> le mode <code class="language-text">strict</code>.</p> <h4>Changement de l’extension “js” en “ts”</h4> <p>En effet, avec l’option <code class="language-text">"strict"</code> à <code class="language-text">true</code> dans <code class="language-text">tsconfig.json</code>, on obtient l’erreur suivante en tentant de transpiler ce code TypeScript (<code class="language-text">normalize-name.ts</code>, notez l’extension <strong><code class="language-text">.ts</code></strong>) :</p> <blockquote> <p>❌ normalize-name.<strong>ts</strong>(13,15): error TS2345: Argument of type ‘undefined’ is not assignable to parameter of type ‘string’</p> </blockquote> <p>Pour un code TypeScript (<strong>.ts</strong>, <strong>.tsx</strong>), <code class="language-text">tsc</code> tient compte de la configuration établie dans <code class="language-text">tsconfig.json</code>.</p> <p>Par conséquent, l’adoption progressive (fichier par fichier) peut se faire en deux temps. D’abord à l’aide de <code class="language-text">@ts-check</code>; ensuite en changeant l’extension <code class="language-text">js</code> du fichier en <code class="language-text">ts</code>.</p> <blockquote> <p>💡 L’option <code class="language-text">sourceMap</code> permet de générer les <strong>source maps</strong> correspondant au code transpilé.</p> </blockquote> <h3>Packages NPM et Imports</h3> <p>Pour les packages NPM importés dans le projet, <code class="language-text">tsc</code> s’attend à ce qu’ils possèdent un <a href="http://www.typescriptlang.org/docs/handbook/declaration-files/consumption.html" target="_blank" rel="noopener noreferrer">fichier de déclaration</a>. Certains packages NPM en sont déjà équipés : c’est le cas idéal. Sinon il reste deux possibilités :</p> <ul> <li>Installer les packages <a href="https://github.com/DefinitelyTyped/DefinitelyTyped" target="_blank" rel="noopener noreferrer">DefinitelyTyped</a> correspondants. Il en existe pour la plupart des packages (<code class="language-text">@types/node</code>, <code class="language-text">@types/express</code>, <code class="language-text">@types/lodash</code>, <code class="language-text">@types/jest</code>, …).</li> <li>Créer des déclarations vides (<em>stubs</em>) de ces packages, pour autoriser leur import sans qu’ils soient signalés comme inconnus (<em>“Could not find a declaration file for module ’…’ […]”</em>) :</li> </ul> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">declare</span> <span class="token keyword">module</span> <span class="token string">'nom-du-module'</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>C’est une solution de “secours” lorsqu’aucun package <code class="language-text">@types</code> correspondant n’est disponible, ou qu’il n’est pas à jour. Vous pouvez aussi définir vous-même la déclaration du package et <a href="http://definitelytyped.org/guides/contributing.html" target="_blank" rel="noopener noreferrer">la partager</a> avec la communauté.</p> <p>Pour permettre l’import de fichiers JSON (<code class="language-text">import * as fileContent from './file.json'</code>) :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">declare</span> <span class="token keyword">module</span> <span class="token string">'*.json'</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> value<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">default</span> value<span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <blockquote> <p>💡 Pour générer les fichiers de déclaration de votre projet (par exemple, si c’est un projet open-source), activez l’option <code class="language-text">declaration</code>.</p> </blockquote> <h3>Conclusion</h3> <p>Vous trouverez d’avantage de détails sur la migration depuis JavaScript dans la <a href="https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html" target="_blank" rel="noopener noreferrer">documentation officielle</a>. </p> <p>Une fois n’est pas coutume, j’insiste sur l’importance de l’option <code class="language-text">strict</code>. Comme le précise la <a href="https://www.typescriptlang.org/docs/handbook/compiler-options.html" target="_blank" rel="noopener noreferrer">documentation</a>, elle entraîne l’activation de plusieurs règles (dont <code class="language-text">noImplicitAny</code> ) qui rendent <code class="language-text">tsc</code> très exigeant sur la qualité (et le typage) de votre code mais permettent de tirer le meilleur profit de TypeScript.</p></div>TypeScript - 2/3 : Pourquoi l'adopterhttps://vinceops.me/typescript-2-pourquoi-l-adopter/2018-04-22T12:01:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAB7CAAAewgFu0HU+AAAA7klEQVQoz22Qz0sCQRiGR/OwsoQWsRdPHSLBJOqWZBAptFBBaih46CDeomPLrJeOHj32P33zR/nsz2bdHXh4v1/zzscoFZ3Q9OEdOqrqhOYUxnBk1bzSnJa82SH5RS/Qc3SiQhkobS7hnnwFQzij/oY+JAvIC3oHP3Bjb+DAGpbwBXP4gD/4htvkERPEF7WZYbaBIDXcsYhnG7bgGj4Z8mlGJs+p8SsGPXSRmk7hihkf3SbbSlTvZmaZHsOIwRP0yXrM5UIT3DRvJyrREo/W/zlFQ20axPXqT87MpVbZyz3koJDFJaSoWv57cSy5xx5wuY69pAY4hwAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/798b8/ts-banner.webp 195w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/0e356/ts-banner.webp 390w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/4c79d/ts-banner.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/25fa3/ts-banner.png 195w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/506f3/ts-banner.png 390w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png" alt="typescript-logo" title="typescript-logo" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><em>Suite de la <a href="https://vinceops.me/typescript-1-presentation/">première partie</a> du dossier.</em></p> <h2>Pourquoi l’adopter</h2> <p>Les articles présentant les avantages et inconvénients de TypeScript ne manquent pas sur la toile. J’aborde ici une liste <strong>non-exhaustive</strong> des points forts qui justifient que l’on s’intéresse au langage.</p> <p>Deux citations d’Anders Hejlsberg au sujet de JavaScript, qui, au risque de sonner comme un <em>troll</em>, auront certainement le mérite de faire sourire les développeurs JavaScript expérimentés :</p> <blockquote> <p><strong>Erik Meijer</strong>: Vous insinuez que l’on ne peut pas écrire de grosses applications en JavaScript ?<br /> <strong>Anders Hejlsberg</strong>: Non, vous pouvez les écrire. Mais vous ne pouvez pas les maintenir.<br /> <a href="https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2012/Panel-Web-and-Cloud-Programming" target="_blank" rel="noopener noreferrer">Lang.NEXT 2012</a></p> </blockquote> <p>Et plus récemment :</p> <blockquote> <p>Les très gros codes source JavaScript ont tendance à finir en “Lecture seule”.<br /> <strong>Anders Hejlsberg</strong>, <a href="https://channel9.msdn.com/Events/Build/2016/B881" target="_blank" rel="noopener noreferrer">Microsoft Build 2016</a></p> </blockquote> <br /> <h3>Avantages du Typage</h3> <p>Le typage, bien qu’optionnel, apporte de nombreux avantages, dont :</p> <ul> <li>L’analyse statique (sans exécution) du code</li> <li>L’amélioration significative de la capacité à maintenir et réusiner (<em>refactor</em>) le code</li> <li>Un code plus lisible et plus rapidement compréhensible (les annotations de type jouant aussi un rôle de documentation)</li> </ul> <h4>Analyse statique</h4> <p>TypeScript est accompagné de fichiers de <a href="http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html" target="_blank" rel="noopener noreferrer"><strong>déclaration</strong></a> des API d’ES5, ES6, du DOM, etc. La plupart des packages (npm) majeurs contiennent aussi des fichiers de déclaration (sinon, on peut souvent les trouver dans les packages tiers, comme <code class="language-text">@types/express</code>). <code class="language-text">tsc</code> est capable d’en tirer parti pour anticiper des erreurs éventuelles de programmation, au-delà de la simple analyse (élémentaire) inhérente aux règles du langage (comme, par exemple, l’impossibilité de multiplier deux <code class="language-text">string</code>, ou de passer une <code class="language-text">string</code> en argument d’une fonction attendant un <code class="language-text">number</code>).</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> diacritics <span class="token keyword">from</span> <span class="token string">'diacritics'</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">normalizeName</span><span class="token punctuation">(</span><span class="token parameter">name<span class="token operator">:</span> <span class="token builtin">string</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> diacritics<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span>name<span class="token punctuation">.</span><span class="token function">toLocalUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">normalizeName</span><span class="token punctuation">(</span><span class="token keyword">undefined</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><code class="language-text">tsc</code> (ou votre IDE, indirectement) signale une erreur à la ligne 4 :</p> <blockquote> <p><i class="em em-x"></i> error TS2551: Property ‘toLocalUpperCase’ does not exist on type ‘string’. Did you mean ‘toLocaleUpperCase’?</p> </blockquote> <p>Après correction, <code class="language-text">tsc</code> signale une autre erreur, ligne 7 (seulement si le mode <code class="language-text">strict</code> est activé) :</p> <blockquote> <p><i class="em em-x"></i> error TS2345: Argument of type ‘undefined’ is not assignable to parameter of type ‘string’</p> </blockquote> <p>Ce qui marche avec les types basiques, ici, marche avec tous les types :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">import</span> _ <span class="token keyword">from</span> <span class="token string">'lodash'</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">Person</span> <span class="token punctuation">{</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">readonly</span> firstName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> <span class="token keyword">readonly</span> lastName<span class="token operator">:</span> <span class="token builtin">string</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> mike <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">'Mike'</span><span class="token punctuation">,</span> <span class="token string">'Smith'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> partialMike <span class="token operator">=</span> _<span class="token punctuation">.</span>pick<span class="token operator"><</span>Person<span class="token punctuation">,</span> <span class="token keyword">keyof</span> Person<span class="token operator">></span><span class="token punctuation">(</span>mike<span class="token punctuation">,</span> <span class="token string">'firstName'</span><span class="token punctuation">,</span> <span class="token string">'age'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>VS Code signale une erreur ligne 8, sur <code class="language-text">'age'</code> :</p> <blockquote> <p><i class="em em-x"></i> error TS2345: Argument of type ‘“age”’ is not assignable to parameter of type ‘Many<“firstName” | “lastName”>‘.</p> </blockquote> <p>La <strong>définition</strong> de <code class="language-text">pick</code> dans <code class="language-text">@types/lodash</code> spécifie que seules les propriétés listées dans le second paramètre de type (<code class="language-text">keyof Person</code>) sont acceptées (<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html" target="_blank" rel="noopener noreferrer">présentation de keyof</a>). Ce morceau de code est donc “robuste” à toute évolution de <code class="language-text">Person</code> (propriété renommée, supprimée, etc.) et évite aussi certaines étourderies comme les fautes de frappe dans les noms de propriétés/méthodes..</p> <h4>Refactoring</h4> <p>Le typage permet aux IDE de mieux nous servir. L’<em>autocompletion</em>, la propagation du renommage de méthodes, propriétés, variables, la fonctionnalité “Find usages” (ou “Find references”)… Ces outils fonctionnent souvent bien mieux une fois la <em>codebase</em> typée.</p> <p>Aussi, bien que cela soit de moins en moins marqué grâce à l’<a href="https://github.com/Microsoft/TypeScript/wiki/JavaScript-Language-Service-in-Visual-Studio" target="_blank" rel="noopener noreferrer">excellent support</a> d’ES6 par les IDE, le <em>refactoring</em> prend une autre ampleur grâce à l’analyse statique : on peut renommer une propriété, une méthode ou une variable, sans craindre d’induire des erreurs de <em>runtime</em> comme <code class="language-text">TypeError: cannot read property 'x' of undefined</code>, ou encore <code class="language-text">TypeError: x.y() is not a function</code>. En effet, la transpilation échoue si l’on tente d’accéder à une propriété/méthode/variable non définie.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/30e1ee33cafceb7b2762e8716ff858fc/64756/56-refactor-man.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 102.05128205128204%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsSAAALEgHS3X78AAAECElEQVQ4y42Ue0xTVxjAT1icE+eiMWNLFBwkbm5qNpepzAF939v3i95bKEpxasKiQoYbyRxjDGTLHiBSQ4wmSzaYmQuJyx7CspTVLJEmAn9AGQgZtL2Vvu5tS9t7W/q6O31oluyfffny5bsn5/e9zrkHBHMSCARCoRBFUXmfJP3xeJxhYnTIHA8N0JTJ4xhKrA/FQt/GggOx4BfxyL04QwKGYViWzeSEfSSZTBraSJgK2g+zkaKfftjJ5x+5+9s2Nlq8QYCU/7V0kE/72kEsFkulUg6Hw+Vy+Xw+u90OM+dD0HQkSrzKxotmrj33454DI5UVDyxPsPHSNFnJUjsZ6pMsDHMuLCysrq4uLS3Nzs5COJ8ZwvRadcYP1ue/SngDxK2bf17cm14DGepZltzOkO9lYbgvnRNYAgyUSCRisWwv0WiU9oiSns3XTcMdzdaeK27rjTeTLsAGKtjgfiZgKsD5tsfGxkZHR5PJJPzK9hyJpslXJv8o635a1r9FUFyyeqdkP/09SNFPZh4CxnemMLCNjQ047YmJCZvNBovPL0Yj4YSv3O3c3SruNJb1SDBLuGR77DDY8G9h488wVFcBhoe0vLzsdrthzePj46FQMNszE3Ha1Hbrjv5OtK+0ZkS277a+9JfyXXdqnp/5tYhaay3AcNQEQSwuLprNZovFkj+2cCY6Uqc1gYNXtx76Grx8Y+tLvS++8T4QfrOpvB28cP/DtkLPNE1DIBKJrKyshNfDuRmwwUz8Zw3y+6Yd35VVmIW7VpqKrd175j9/6sGlbVPvbnbfbgZer9f10OV2r0Hr8XhIivT6fR6P2+f1EB7fveHu+RE1cVdlX8DXHHKf63iIwoMUEggYSOoWuDzY907LmZa2s23tLc3nTrdeOGs83VBbp8IMGtygNZzC9acMuiYD1qDX6nENplPX4modLkTRoWvXwaXPuuQaFDOodfUqrV6hq1drcLlCK4Yq16IyFSJXi6AqtKjBqDMYseMn8eZzb2sw6YCpD3T3fsxDq7JbNaiyVgJVpZM+1vxKfrHBiNc36upO1DacrNM3YleHroCeT7sQOV+DK9SYXI3JHut/eRhdpkYwvbK5qZFbeWgwD0uVovoTuFav/BecI7M2R+qymTWYDFUKOy6ct9wcllUd6TP1g4+6LvIRDpyCRIGIpDxULhBJOYiUj8h4XFEVT1SNyPhCCUcoroFWIK5RaCSvcwd27xUPDvaCjs4PjtUcFYq5QpQjQLl8UTVPVCVAOdCp5lVyBG/xRVwBws2RHBgdwgePnT+w7+jlL3uAn/Q7nHbC5SQIpzOnxCMLb13WcTnnbHOT1snpmemp6am5udm/5u8v/70Inx7A/g/J/63JnMCLm04X3px/ADUy7faKxxPEAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/30e1ee33cafceb7b2762e8716ff858fc/798b8/56-refactor-man.webp 195w, /static/30e1ee33cafceb7b2762e8716ff858fc/0e356/56-refactor-man.webp 390w, /static/30e1ee33cafceb7b2762e8716ff858fc/23bbb/56-refactor-man.webp 780w, /static/30e1ee33cafceb7b2762e8716ff858fc/b47fd/56-refactor-man.webp 1170w, /static/30e1ee33cafceb7b2762e8716ff858fc/cbd37/56-refactor-man.webp 1200w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/30e1ee33cafceb7b2762e8716ff858fc/25fa3/56-refactor-man.png 195w, /static/30e1ee33cafceb7b2762e8716ff858fc/506f3/56-refactor-man.png 390w, /static/30e1ee33cafceb7b2762e8716ff858fc/8a72f/56-refactor-man.png 780w, /static/30e1ee33cafceb7b2762e8716ff858fc/77666/56-refactor-man.png 1170w, /static/30e1ee33cafceb7b2762e8716ff858fc/64756/56-refactor-man.png 1200w" sizes="(max-width: 780px) 100vw, 780px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/30e1ee33cafceb7b2762e8716ff858fc/8a72f/56-refactor-man.png" alt="refactor-man" title="refactor-man" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <h3>Les nouveautés d’ECMAScript</h3> <p>À l’heure où cet article est écrit, TypeScript 2.8 est vieux de seulement quelques semaines. Pourtant, on ne compte plus les <em>proposals</em> du comité de normalisation d’ECMAScript (<a href="https://github.com/tc39" target="_blank" rel="noopener noreferrer">TC39</a>) ayant été intégrées assez tôt dans le langage :</p> <ul> <li><a href="https://github.com/tc39/proposal-class-fields" target="_blank" rel="noopener noreferrer">Class fields</a> : Depuis toujours. TypeScript est un langage de programmation orienté objet (cf. section suivante).</li> <li><a href="https://github.com/tc39/proposal-template-literal-revision" target="_blank" rel="noopener noreferrer">Template Strings</a> : TypeScript <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-4.html" target="_blank" rel="noopener noreferrer">1.4</a> <em>16/01/2015</em></li> <li><a href="https://github.com/tc39/proposal-decorators" target="_blank" rel="noopener noreferrer">Decorators</a> : TypeScript <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-5.html" target="_blank" rel="noopener noreferrer">1.5</a> <em>20/07/2015</em></li> <li><a href="https://github.com/tc39/ecmascript-asyncawait" target="_blank" rel="noopener noreferrer">async/await</a> : TypeScript <a href="http://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-7.html" target="_blank" rel="noopener noreferrer">1.7</a> <em>30/11/2015</em> (Node v4+, ES6), <a href="http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html" target="_blank" rel="noopener noreferrer">2.1</a> (ES3 / ES5, avec un polyfill pour <code class="language-text">Promise</code>).</li> <li><a href="https://github.com/tc39/proposal-object-rest-spread" target="_blank" rel="noopener noreferrer">Object Rest/Spread</a>: TypeScript <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html" target="_blank" rel="noopener noreferrer">2.1</a> <em>07/12/2016</em></li> <li><a href="https://github.com/tc39/proposal-dynamic-import" target="_blank" rel="noopener noreferrer">Dynamic import</a> : TypeScript <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html" target="_blank" rel="noopener noreferrer">2.4</a> <em>27/06/2017</em></li> <li><a href="https://github.com/tc39/proposal-optional-catch-binding" target="_blank" rel="noopener noreferrer">Optional catch clause</a> : TypeScript <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-5.html" target="_blank" rel="noopener noreferrer">2.5</a> <em>31/08/2017</em></li> <li><a href="https://github.com/tc39/proposal-numeric-separator" target="_blank" rel="noopener noreferrer">Numeric separators</a> :TypeScript <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html" target="_blank" rel="noopener noreferrer">2.7</a> <em>31/01/2018</em></li> <li>etc.</li> </ul> <p>On est aussi en mesure d’utiliser la plupart de ces fonctionnalités (et plus encore) avec <strong>Babel</strong>. Cependant, il est impossible pour le moment (Babel 6+) d’intégrer du code TypeScript à votre chaîne de transpilation Babel. Il faudra donc attendre que <a href="https://babeljs.io/blog/2017/09/12/planning-for-7.0" target="_blank" rel="noopener noreferrer"><strong>Babel 7.0</strong></a> soit publiée ! <i class="em em-tada"></i></p> <blockquote> <p><i class="em em-bulb"></i> You can now use <code class="language-text">babel-preset-typescript</code> to allow Babel to strip types similar to how <code class="language-text">babel-preset-flow</code> works! […] you setup TS with <code class="language-text">--no-emit</code> or use it in the editor/watch mode so that you can use preset-env and other Babel plugins.</p> </blockquote> <img src="https://media.giphy.com/media/RrVzUOXldFe8M/giphy.gif" alt="yeah" style="width: 200px; margin: 20px auto; display: block;" /> <h3>Programmation orientée Objet</h3> <p><em>Les concepts de la POO listés ci-après n’ont pas vocation à être présentés dans cet article. Si certains vous sont inconnus, découvrez-les dans la <a href="https://www.typescriptlang.org/docs/handbook/basic-types.html" target="_blank" rel="noopener noreferrer">documentation officielle</a></em></p> <h4>Interface</h4> <p>Concept bien connu de la programmation orientée objet que l’on retrouve dans TypeScript. Les <a href="https://www.typescriptlang.org/docs/handbook/interfaces.html" target="_blank" rel="noopener noreferrer">interfaces</a> n’existent que dans le code TypeScript : il n’en reste rien dans la version transpilée en JavaScript. Cependant, elles ont de multiples avantages :</p> <ul> <li>Documenter le code en spécifiant la structure attendues des données passées en paramètres ou retournées par les fonctions.</li> <li>Permettre la validation des déclarations de classes qui les implémentent (rôle de Contrat).</li> <li>Étendre les interfaces de modules existants grâce à la <a href="https://www.typescriptlang.org/docs/handbook/declaration-merging.html" target="_blank" rel="noopener noreferrer">Combinaison de déclaration</a> ou <em>Declaration merging</em> (concept <strong>propre à TypeScript</strong> dont je recommande la lecture)</li> </ul> <h4>Class</h4> <p>L’arrivée d’ECMAScript 2015 a apporté le support des classes en JavaScript. Cependant, certaines notions de la POO sont manquantes. C’est le cas notamment :</p> <ul> <li>Des classes et méthodes <strong>abstraites</strong></li> <li>Des attributs de visibilité (ou accessibilité) <code class="language-text">private</code>, <code class="language-text">protected</code>, <code class="language-text">public</code></li> </ul> <p>Que l’<a href="https://www.typescriptlang.org/docs/handbook/classes.html" target="_blank" rel="noopener noreferrer">on retrouve</a> dans TypeScript.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 300px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/9aee32aaa4c5f50a107d2ea5d6a9024e/0cdb7/oop_meme.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 82.56410256410257%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAARABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAMCBQT/xAAXAQEBAQEAAAAAAAAAAAAAAAADAQAC/9oADAMBAAIQAxAAAAHVo+obz1ScdCQSbF3/xAAdEAABAwUBAAAAAAAAAAAAAAABAgMRABASEzEy/9oACAEBAAEFAkZmiFQdhLO2TyHzZfgc/8QAFxEBAAMAAAAAAAAAAAAAAAAAAQAQE//aAAgBAwEBPwECZ3//xAAZEQACAwEAAAAAAAAAAAAAAAAAAgEQERP/2gAIAQIBAT8BZn06EV//xAAfEAACAQIHAAAAAAAAAAAAAAAAARACMRESISJBcYH/2gAIAQEABj8CwZdFjfS+zlmmX2HH/8QAHBABAAICAwEAAAAAAAAAAAAAAQARITFBYaGx/9oACAEBAAE/IcKg41NPg6gQ+IgSrVWT5C3b6ouag1huczwzRP/aAAwDAQACAAMAAAAQY+f/AP/EABcRAQEBAQAAAAAAAAAAAAAAAAEAECH/2gAIAQMBAT8QGdkM5//EABoRAAICAwAAAAAAAAAAAAAAAAABETEhkbH/2gAIAQIBAT8QpFquCaMpFhn/xAAdEAEAAwADAAMAAAAAAAAAAAABABEhMUGhUXHw/9oACAEBAAE/ELYDCiFc/cyyQnBo/V/ETAu6bB5LZQlQGtY3eJ1XqqFm9DkRheKBoa98sY9meAn/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/9aee32aaa4c5f50a107d2ea5d6a9024e/798b8/oop_meme.webp 195w, /static/9aee32aaa4c5f50a107d2ea5d6a9024e/77434/oop_meme.webp 300w" sizes="(max-width: 300px) 100vw, 300px" type="image/webp" /> <source srcset="/static/9aee32aaa4c5f50a107d2ea5d6a9024e/0e421/oop_meme.jpg 195w, /static/9aee32aaa4c5f50a107d2ea5d6a9024e/0cdb7/oop_meme.jpg 300w" sizes="(max-width: 300px) 100vw, 300px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/9aee32aaa4c5f50a107d2ea5d6a9024e/0cdb7/oop_meme.jpg" alt="ok" title="ok" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <h4>Mais aussi…</h4> <p>Les <a href="https://www.typescriptlang.org/docs/handbook/enums.html" target="_blank" rel="noopener noreferrer">Énumérations</a> et les <a href="https://www.typescriptlang.org/docs/handbook/generics.html" target="_blank" rel="noopener noreferrer">Génériques</a> (ou Types paramétrés / Templates). Bien que n’étant pas des concepts propres à la POO, ils existent dans la plupart des grands langages orientés object.</p> <h5>Un mot sur le <em>generic programming</em></h5> <p>Le support des Génériques (et TypeScript 2.8, avec ses types conditionnels) permet de définir des types avancés comme :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">type</span> StringProps<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">P</span> <span class="token keyword">in</span> <span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">P</span><span class="token punctuation">]</span> <span class="token keyword">extends</span> <span class="token class-name">string</span> <span class="token operator">?</span> <span class="token constant">P</span> <span class="token operator">:</span> <span class="token builtin">never</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token keyword">keyof</span> <span class="token constant">T</span><span class="token punctuation">]</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>regroupant toutes les propriétés de type <code class="language-text">string</code> de <code class="language-text">T</code>.</p> <p>Exemple d’utilisation :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">class</span> <span class="token class-name">Person</span> <span class="token punctuation">{</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span> <span class="token parameter"><span class="token keyword">public</span> <span class="token keyword">readonly</span> firstName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> <span class="token keyword">public</span> <span class="token keyword">readonly</span> email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> <span class="token keyword">public</span> <span class="token keyword">readonly</span> age<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">,</span> <span class="token keyword">public</span> <span class="token keyword">readonly</span> isMarried<span class="token operator">:</span> <span class="token builtin">boolean</span><span class="token punctuation">,</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> serialize<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">(</span>obj<span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">,</span> fields<span class="token operator">:</span> <span class="token builtin">Array</span><span class="token operator"><</span>StringProps<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">>></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// ...</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Ici, si l’on passe un objet <code class="language-text">Person</code> (notre paramètre de type <code class="language-text">T</code>) à la fonction <code class="language-text">serialize</code>, le paramètre <code class="language-text">fields</code> n’acceptera qu’un tableau de ses propriétés de type <code class="language-text">string</code>.</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">const</span> mike <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">'Mike'</span><span class="token punctuation">,</span> <span class="token string">'Smith'</span><span class="token punctuation">,</span> <span class="token number">25</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// ✓</span> <span class="token function">serialize</span><span class="token punctuation">(</span>mike<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'firstName'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">serialize</span><span class="token punctuation">(</span>mike<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'firstName'</span><span class="token punctuation">,</span> <span class="token string">'email'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// error TS2345: Type '"firstname"' is not assignable to type '"firstName" | "email"'</span> <span class="token function">serialize</span><span class="token punctuation">(</span>mike<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'firstname'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// error TS2345: Type '"age"' is not assignable to type '"firstName" | "email"'</span> <span class="token function">serialize</span><span class="token punctuation">(</span>mike<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'firstName'</span><span class="token punctuation">,</span> <span class="token string">'age'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <h3>Support de JSX</h3> <p>TypeScript supporte <a href="https://reactjs.org/docs/introducing-jsx.html" target="_blank" rel="noopener noreferrer">JSX</a> intégralement (typage, analyse et transpilation). Pour l’exploiter, les fichiers contenant du JSX doivent avoir l’extension <code class="language-text">.tsx</code> et l’option de compilation <code class="language-text">jsx</code> (<code class="language-text">tsconfig.json</code> > <code class="language-text">compilerOptions</code>) doit être activée. Trois valeurs sont disponibles pour cette dernière :</p> <ul> <li><code class="language-text">preserve</code> : Le code JSX n’est pas transpilé et le fichier de sortie a pour extension <code class="language-text">.jsx</code> - utile si vous souhaitez confier le traitement de JSX à un autre outil (Babel, par exemple)</li> <li><code class="language-text">react</code> : Le code JSX est transpilé vers du code React (<code class="language-text">React.createElement</code>) et le fichier de sortie a pour extension <code class="language-text">.js</code></li> <li><code class="language-text">react-native</code> : comme <code class="language-text">preserve</code>, conserve le code JSX dans l’état, mais le fichier de sortie a pour extension <code class="language-text">.js</code></li> </ul> <p>Prenons le composant React suivant, représentant une cellule cliquable sur un plateau de jeu :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Cell</span><span class="token punctuation">(</span><span class="token parameter">props<span class="token operator">:</span> <span class="token punctuation">{</span>player<span class="token operator">?</span><span class="token operator">:</span> PlayerEnum<span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> baseClass <span class="token operator">=</span> <span class="token string">'c4__cell'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> cellClasses <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseClass<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">classNames</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseClass<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">--empty</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>props<span class="token punctuation">.</span>player <span class="token operator">===</span> PlayerEnum<span class="token punctuation">.</span><span class="token constant">NONE</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseClass<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">--a</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>props<span class="token punctuation">.</span>player <span class="token operator">===</span> PlayerEnum<span class="token punctuation">.</span><span class="token constant">A</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseClass<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">--b</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>props<span class="token punctuation">.</span>player <span class="token operator">===</span> PlayerEnum<span class="token punctuation">.</span><span class="token constant">B</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token operator"><</span>button className<span class="token operator">=</span><span class="token punctuation">{</span>cellClasses<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le composant <code class="language-text">Cell</code> accepte une <code class="language-text">prop</code> <code class="language-text">player</code>, dont la valeur peut être <code class="language-text">undefined</code> ou bien compatible avec l’énumération <code class="language-text">PlayerEnum</code> :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">enum</span> PlayerEnum <span class="token punctuation">{</span> <span class="token constant">NONE</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token constant">A</span> <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token constant">B</span> <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>La ligne suivante provoquera une erreur de compilation (un IDE compatible vous signalera l’erreur en amont) :</p> <div class="gatsby-highlight" data-language="jsx"><pre style="counter-reset: linenumber NaN" class="language-jsx line-numbers"><code class="language-jsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Cell</span></span> <span class="token attr-name">key</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">c-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>x<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>y<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">}</span></span> <span class="token attr-name">player</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token string">'A'</span><span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <blockquote> <p>Types of property ‘player’ are incompatible. Type ‘string’ is not assignable to type ‘PlayerEnum | undefined’.</p> </blockquote> <p>Grâce à ce typage, VS Code sait nous dire quel type de propriété est attendue :</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/30c926a1b2e22223bd2eea6ec31a901f/89360/2018-03-18_20-16-05.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 13.846153846153847%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAADABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAceQsD//xAAXEAADAQAAAAAAAAAAAAAAAAAAARAR/9oACAEBAAEFAtFP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAGRAAAQUAAAAAAAAAAAAAAAAAAAEQESFR/9oACAEBAAE/IZaKtv/aAAwDAQACAAMAAAAQ8A//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAYEAEBAQEBAAAAAAAAAAAAAAABIQAxkf/aAAgBAQABPxCnTkby1h5v/9k='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/30c926a1b2e22223bd2eea6ec31a901f/798b8/2018-03-18_20-16-05.webp 195w, /static/30c926a1b2e22223bd2eea6ec31a901f/0e356/2018-03-18_20-16-05.webp 390w, /static/30c926a1b2e22223bd2eea6ec31a901f/23bbb/2018-03-18_20-16-05.webp 780w, /static/30c926a1b2e22223bd2eea6ec31a901f/b20d2/2018-03-18_20-16-05.webp 1006w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/30c926a1b2e22223bd2eea6ec31a901f/0e421/2018-03-18_20-16-05.jpg 195w, /static/30c926a1b2e22223bd2eea6ec31a901f/ce2e0/2018-03-18_20-16-05.jpg 390w, /static/30c926a1b2e22223bd2eea6ec31a901f/485b7/2018-03-18_20-16-05.jpg 780w, /static/30c926a1b2e22223bd2eea6ec31a901f/89360/2018-03-18_20-16-05.jpg 1006w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/30c926a1b2e22223bd2eea6ec31a901f/485b7/2018-03-18_20-16-05.jpg" alt="ts-jsx-intellisense-1" title="ts-jsx-intellisense-1" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Et bien évidemment, avec l’<em>autocompletion</em> : <span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/d5ec8c219a3545470832b03d37e19409/d6f38/2018-03-18_20-19-41.jpg" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 14.358974358974358%; position: relative; bottom: 0; left: 0; background-image: url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAADABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHHuEBP/8QAFRABAQAAAAAAAAAAAAAAAAAAAEH/2gAIAQEAAQUCR//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAABD/2gAIAQEABj8Cf//EABkQAAEFAAAAAAAAAAAAAAAAAAABEBFBUf/aAAgBAQABPyGV0o3/2gAMAwEAAgADAAAAEAP/AP/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABsQAAIBBQAAAAAAAAAAAAAAAAABkSExQWGB/9oACAEBAAE/ENwV3LVZHyD/2Q=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/d5ec8c219a3545470832b03d37e19409/798b8/2018-03-18_20-19-41.webp 195w, /static/d5ec8c219a3545470832b03d37e19409/0e356/2018-03-18_20-19-41.webp 390w, /static/d5ec8c219a3545470832b03d37e19409/23bbb/2018-03-18_20-19-41.webp 780w, /static/d5ec8c219a3545470832b03d37e19409/2c8f1/2018-03-18_20-19-41.webp 927w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/d5ec8c219a3545470832b03d37e19409/0e421/2018-03-18_20-19-41.jpg 195w, /static/d5ec8c219a3545470832b03d37e19409/ce2e0/2018-03-18_20-19-41.jpg 390w, /static/d5ec8c219a3545470832b03d37e19409/485b7/2018-03-18_20-19-41.jpg 780w, /static/d5ec8c219a3545470832b03d37e19409/d6f38/2018-03-18_20-19-41.jpg 927w" sizes="(max-width: 780px) 100vw, 780px" type="image/jpeg" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/d5ec8c219a3545470832b03d37e19409/485b7/2018-03-18_20-19-41.jpg" alt="ts-jsx-intellisense-2" title="ts-jsx-intellisense-2" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p>Le code JSX de notre composant <code class="language-text">Cell</code> (<code class="language-text">return <button...</code>) est transpilé ainsi :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">return</span> React<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"button"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> className<span class="token operator">:</span> cellClasses <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>On utilise <code class="language-text">JSX.Element</code> pour typer des éléments JSX (par exemple, retournés par une fonction). Le namespace <code class="language-text">JSX</code> est défini dans le package <code class="language-text">@types/react</code>, qui contient (<strong>entre autres</strong>) la définition de tous les éléments HTML adaptée au JSX :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">interface</span> <span class="token class-name">IntrinsicElements</span> <span class="token punctuation">{</span> <span class="token comment">// HTML</span> a<span class="token operator">:</span> React<span class="token punctuation">.</span>DetailedHTMLProps<span class="token operator"><</span>React<span class="token punctuation">.</span>AnchorHTMLAttributes<span class="token operator"><</span>HTMLAnchorElement<span class="token operator">></span><span class="token punctuation">,</span> HTMLAnchorElement<span class="token operator">></span><span class="token punctuation">;</span> abbr<span class="token operator">:</span> React<span class="token punctuation">.</span>DetailedHTMLProps<span class="token operator"><</span>React<span class="token punctuation">.</span>HTMLAttributes<span class="token operator"><</span>HTMLElement<span class="token operator">></span><span class="token punctuation">,</span> HTMLElement<span class="token operator">></span><span class="token punctuation">;</span> address<span class="token operator">:</span> React<span class="token punctuation">.</span>DetailedHTMLProps<span class="token operator"><</span>React<span class="token punctuation">.</span>HTMLAttributes<span class="token operator"><</span>HTMLElement<span class="token operator">></span><span class="token punctuation">,</span> HTMLElement<span class="token operator">></span><span class="token punctuation">;</span> area<span class="token operator">:</span> React<span class="token punctuation">.</span>DetailedHTMLProps<span class="token operator"><</span>React<span class="token punctuation">.</span>AreaHTMLAttributes<span class="token operator"><</span>HTMLAreaElement<span class="token operator">></span><span class="token punctuation">,</span> HTMLAreaElement<span class="token operator">></span><span class="token punctuation">;</span> article<span class="token operator">:</span> React<span class="token punctuation">.</span>DetailedHTMLProps<span class="token operator"><</span>React<span class="token punctuation">.</span>HTMLAttributes<span class="token operator"><</span>HTMLElement<span class="token operator">></span><span class="token punctuation">,</span> HTMLElement<span class="token operator">></span><span class="token punctuation">;</span> <span class="token comment">// ...</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <hr /> <p><em>La liste n’est évidemment pas exhaustive et les arguments en faveur de TypeScript (pour le frontend, comme pour le backend) ne manquent pas.</em></p> <h2>Installation et Migration</h2> <p>À suivre dans la <a href="https://vinceops.me/typescript-3-installation-migration/">troisième partie</a> du dossier.</p></div>TypeScript - 1/3 : Présentationhttps://vinceops.me/typescript-1-presentation/2018-04-22T12:00:10+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAB7CAAAewgFu0HU+AAAA7klEQVQoz22Qz0sCQRiGR/OwsoQWsRdPHSLBJOqWZBAptFBBaih46CDeomPLrJeOHj32P33zR/nsz2bdHXh4v1/zzscoFZ3Q9OEdOqrqhOYUxnBk1bzSnJa82SH5RS/Qc3SiQhkobS7hnnwFQzij/oY+JAvIC3oHP3Bjb+DAGpbwBXP4gD/4htvkERPEF7WZYbaBIDXcsYhnG7bgGj4Z8mlGJs+p8SsGPXSRmk7hihkf3SbbSlTvZmaZHsOIwRP0yXrM5UIT3DRvJyrREo/W/zlFQ20axPXqT87MpVbZyz3koJDFJaSoWv57cSy5xx5wuY69pAY4hwAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/798b8/ts-banner.webp 195w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/0e356/ts-banner.webp 390w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/4c79d/ts-banner.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/25fa3/ts-banner.png 195w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/506f3/ts-banner.png 390w, /static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/32a5fd2ca6d335e4c7fa8f061be0e7ca/ca1dc/ts-banner.png" alt="typescript-logo" title="typescript-logo" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><em>Première partie du dossier TypeScript</em></p> <p>Le langage <a href="https://www.typescriptlang.org" target="_blank" rel="noopener noreferrer">TypeScript</a> est l’une des technologies les plus appréciées de ces dernières années, dans le développement <em>Frontend</em> comme dans le <em>Backend</em>. <a href="https://insights.stackoverflow.com/survey/2017#technology" target="_blank" rel="noopener noreferrer">Sa popularité</a> ne cesse de <a href="https://slack.engineering/typescript-at-slack-a81307fa288d" target="_blank" rel="noopener noreferrer">croître</a> et il est au cœur de nombreux projets : <a href="https://github.com/angular/angular" target="_blank" rel="noopener noreferrer">Angular</a>, <a href="https://github.com/NativeScript/NativeScript" target="_blank" rel="noopener noreferrer">NativeScript</a>, <a href="https://github.com/ionic-team/ionic" target="_blank" rel="noopener noreferrer">Ionic</a>, <a href="https://github.com/microsoft/vscode" target="_blank" rel="noopener noreferrer">VS Code</a>, <a href="https://github.com/apollographql/apollo-server" target="_blank" rel="noopener noreferrer">Apollo GraphQL</a>, <a href="https://github.com/BabylonJS/Babylon.js" target="_blank" rel="noopener noreferrer">Babylon.js</a>, <a href="https://github.com/ReactiveX/rxjs" target="_blank" rel="noopener noreferrer">RxJS</a>, <a href="https://github.com/nestjs/nest" target="_blank" rel="noopener noreferrer">Nest</a>, <a href="https://github.com/typeorm/typeorm" target="_blank" rel="noopener noreferrer">TypeORM</a>, etc.</p> <p>Il n’existe à l’heure actuelle aucune alternative, même si certaines technologies partagent des objectifs communs ou proposent des avantages similaires. C’est le cas de <a href="https://flow.org/" target="_blank" rel="noopener noreferrer">Flow</a>, le <em>type checker</em> développé par Facebook, ou (plus éloigné) du langage <a href="http://elm-lang.org/" target="_blank" rel="noopener noreferrer">elm</a>.</p> <hr /> <h2>Présentation</h2> <blockquote> <p>“JavaScript that scales.”</p> </blockquote> <p>TypeScript est un langage <a href="https://github.com/Microsoft/TypeScript" target="_blank" rel="noopener noreferrer">open source</a>, créé par Microsoft, transpilable en JavaScript. Pour l’anecdote, on retrouve <a href="https://en.wikipedia.org/wiki/Anders_Hejlsberg" target="_blank" rel="noopener noreferrer">Anders Hejlsberg</a>, créateur de C# (entre autres), derrière le projet.</p> <p>Ses principaux objectifs sont :</p> <ul> <li>Le support des propositions existantes <em>et à venir</em> d’EcmaScript.</li> <li>L’apport du typage <em>optionnel</em> à JavaScript.</li> <li>L’identification anticipée <em>(static)</em> de codes potentiellement invalides.</li> <li>La compilation vers du JavaScript optimisé, avec pour cible, au choix: ES3, ES5, ES6…</li> </ul> <p>Ce que TypeScript n’ambitionne pas de faire, entre <a href="https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals" target="_blank" rel="noopener noreferrer">autres</a> :</p> <ul> <li>Imiter les langages existants; mais plutôt exploiter la nature de JavaScript et les usages des développeurs comme guide pour rendre le langage pertinent</li> <li>Utiliser un système de types “sage” (<em>sound</em>) :</li> </ul> <p>Le système de types de TypeScript n’est <strong>pas</strong> “sage”, ce qui signifie qu’il autorise certaines opérations qui ne peuvent être vérifiées au moment de la compilation (plus d’informations <a href="https://www.typescriptlang.org/docs/handbook/type-compatibility.html" target="_blank" rel="noopener noreferrer">ici</a> : <em>“A Note on Soundness”</em>). C’est l’une des grandes différences majeures avec <a href="https://flow.org/" target="_blank" rel="noopener noreferrer">Flow</a>, comme expliquée par James Kyle <a href="https://discuss.reactjs.org/t/if-typescript-is-so-great-how-come-all-notable-reactjs-projects-use-babel/4887/2" target="_blank" rel="noopener noreferrer">sur le forum de React</a>.</p> <p><em>Pour être en mesure d’exécuter les lignes de commande citées dans la suite de l’article, installez typescript :</em></p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> -g typescript</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <h2>“Superset” et Transpilation</h2> <p>Entrons dans le vif du sujet : “TypeScript est un superset de JavaScript”, c’est la <em>punchline</em> popularisée par Microsoft et partagée par de nombreux enthousiastes. Un superset apportant le typage, les interfaces, les énumérations, les types génériques, les décorateurs… Si vous avez déjà lu des articles sur le langage, vous avez certainement aperçu un graphique similaire :</p> <span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 300px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/b8ccf9ae84a72cf8037a1ff6341dd73e/a8a0d/ts-circles.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEAklEQVQ4y02UDUxTVxSAb4GxLFnGkiVbsrGZAaVFhaLRYciYEGIcKSC00NoWyYZgGNBCyxrUMCv0h0LBgoIGJxEKpaUKMjYzYbitGTINSGn7oEX+VkCYZA4Ig8CgZefxSufNyXv3nne+83ffvWhnb7hcO1tOFzFfXd9+9nztydgKiH1ubWV9i9Bvb7tcLg+xgzwkoX42v153fza7zs4uxxgKS7Lcwiq3Zl2z1XTNjs2tgYFzz9IN4yR4dbpaf/0zXT0aX2JOkltAUpVWjgpLKbMyFVY6KGWWhp75zX+dBILDhBMgr3Q6AFMY/igzTItvj0u0kwrDtFQ/xa8fk2invmmZVHfOpFWNlOqmNwgeh3fp+gfPwffpCkxwcwzSVt511HTNye84VB0zFzVTFzWTVfcc1d/NnKu1xUmGwQsRHE97YHwFyuOqMIA5qpH0ajtTbooR/xYlfPhpwcMT5/sSS4Z4VbZUJcYut/IqR07JzEZsyV3zpZYpqBBgbuUoS2mNFPSEpOn3MTX+SY37GE0fMZpgGZnXzVKawQC6AJHEtyegUjT9Yp1b6Y7JlJnCz94LYrcEsLQxgi66+H40vzM2v4vK0wWymsO+bE+WPgUzTgXGUmK22TXUPfQyUQouR4A/lNkZkNocwtP7M5qyVcasCuPXdY9F1/ppX7QFsbW7fAeROSAdvy+ill8W6CXD6Wp7rNgYkKqhcFqDOa1kri7i3N1jmbqjGdpPMtsOpBtASeG2gkG06Od0tS2+ZBh6jBp+mk8oNYMzCAsJU7i6YK7uvc+rfQ4VoRABCuGTQgv9jiuCWBoqTw8GkDxHZQWk9odZdKfvRaLUkqIwwTfyaS2Fp387ugyF5PqEF/geLvQOF/nQhLB8/ciFQMiLqwNJlg6eklkbe+dRv22ZWYYllQ6S2VpqWtv79DrSQQEKzk3Kb0gtbGQIG+Jy6lFoAdqf5xclo/Kg+JYEyWOGAus1v0RLq1tnr9oTLg/iVfH0b0ZK0MF8ROEf46lBIs/UwNObJgKlV7goIKURItMvPTlzxbbw9wa+z7e6oWxTeEY7ePU9ch4dEKAwIaLkISoff4JA5F35IOEGLaOdLnl69ftZ90+yuLyZVTsWkfOAzG72PXoBQdqhBV5hQkJI4GgP9o+/HpHzI3R77q8NHHbunuGB8dV4ySA1zeD3mRQivwp45LXD4iBW88niAaN1mTibiHjBeDS6Elfc/87J6wB704TQZ9JuWC98XoioOW9EymOL+nqHlzyI+zIgFo7FzaJbI9BwFJhDCvoKNhntF5DIuejj7Lei5ILaoYmFDY/x/zCuwu8I/JRijn8k3z6Kybzx4YnL7x4vjuCqxdU9pollF34MXc5X7qH/ALYKvdV0TLHqAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span> <picture> <source srcset="/static/b8ccf9ae84a72cf8037a1ff6341dd73e/798b8/ts-circles.webp 195w, /static/b8ccf9ae84a72cf8037a1ff6341dd73e/77434/ts-circles.webp 300w" sizes="(max-width: 300px) 100vw, 300px" type="image/webp" /> <source srcset="/static/b8ccf9ae84a72cf8037a1ff6341dd73e/25fa3/ts-circles.png 195w, /static/b8ccf9ae84a72cf8037a1ff6341dd73e/a8a0d/ts-circles.png 300w" sizes="(max-width: 300px) 100vw, 300px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/b8ccf9ae84a72cf8037a1ff6341dd73e/a8a0d/ts-circles.png" alt="typescript-superset" title="typescript-superset" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span> <br /> <p>Il n’est pas totalement faux, mais à nuancer. En effet, avec l’option <code class="language-text">allowJs</code>, Le compilateur TypeScript (<code class="language-text">tsc</code>) accepte le code JavaScript en entrée et produit du code transpilé (selon les paramètres fournis) en sortie.</p> <p>Un code passé en entrée :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">// person.js</span> <span class="token keyword">class</span> <span class="token class-name">Person</span> <span class="token punctuation">{</span> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">firstName</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>firstName <span class="token operator">=</span> firstName<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">'John'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Person { firstName: 'John' }</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Sa compilation (ou plutôt, <em>transpilation</em>) :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">tsc --allowJs person.js --outDir dist</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Le résultat ainsi transpilé, avec l’option <code class="language-text">target</code> à <code class="language-text">"es5"</code> (valeur par défaut) :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">// dist/person.js</span> <span class="token keyword">var</span> Person <span class="token operator">=</span> <span class="token comment">/** @class */</span> <span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">function</span> <span class="token function">Person</span><span class="token punctuation">(</span><span class="token parameter">firstName</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>firstName <span class="token operator">=</span> firstName<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> Person<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">'John'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Person { firstName: 'John' }</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Remarquez la lisibilité du code généré.</p> <p>Par contre, si l’on renomme <code class="language-text">person.js</code> (contenant notre code ES6) en <code class="language-text">person.ts</code>, la compilation n’est plus possible :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">tsc person.ts</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <blockquote> <p>person.ts(3,10): error TS2339: Property ‘firstName’ does not exist on type ‘Person’.</p> </blockquote> <p>Le fait de passer ce fichier en tant que code TypeScript (et non JavaScript) à <code class="language-text">tsc</code> fait échouer la transpilation. Le compilateur s’attend à analyser du code TypeScript valide. <strong>Ce code n’est pas du TypeScript valide, bien qu’il soit du JavaScript valide</strong>. Par conséquent, la notion de “Superset” telle que vulgarisée par le graphique des 3 ensembles est imprécise : on ne peut pas considérer ES6 comme un sous-ensemble de TypeScript.</p> <p>Une syntaxe TypeScript valide, parmi d’autres :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token comment">// person.ts</span> <span class="token keyword">class</span> <span class="token class-name">Person</span> <span class="token punctuation">{</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">public</span> firstName</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">'John'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Person { firstName: 'John' }</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>en utilisant <code class="language-text">firstName</code> comme une <em>parameter property</em> (fonctionnalité de TypeScript <a href="https://vinceops.me/babel-plugin-param-props/">abordée et “babelisée”</a> sur ce blog). Sinon, il est nécessaire de déclarer <code class="language-text">firstName</code> comme une <a href="https://babeljs.io/docs/plugins/transform-class-properties/" target="_blank" rel="noopener noreferrer">class property</a> et de l’affecter manuellement dans le constructeur. La transpilation de ce code TypeScript produit le même code ES5 vu précédemment dans <code class="language-text">dist/person.js</code>.</p> <hr /> <h2>Pourquoi l’adopter</h2> <p>À suivre dans la <a href="https://vinceops.me/typescript-2-pourquoi-l-adopter">deuxième partie</a> du dossier.</p></div>Créer un plugin Babel : Parameter Propertieshttps://vinceops.me/babel-plugin-param-props/2017-11-15T00:00:00+00:00<div style="text-align:justify"><p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/778bc5dd099744afacfcd2918aa6821a/ca1dc/babel-plugin.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 36.92307692307692%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVQY022PvU/CQBjG+wfrqDFxUQZXdDImxsQ4GExcHZxM3Eop6RellpaDHJRSaautx/W+6kGVRX/LPe9z7/NcTqnruvyYxF4rCc5T+EgQmHvtJLhIJzeL4cnM70RBRwi5VZOyREnyFccM4wJCUhTKxkUwGhzl4Cyd3sbDQ+i2odOC5v4SdIrktVhpm6jg9R82YZy/zN8up8YBMFuL0ZXfOx2Z1766F4Fn6D/MRverhS5+9nen2IbF2lOPA/suW5qfqQeDpzgaEIIwWlL8TqqS4JzRqv4PhdL1KgkRIrKLElwWuXQpo4wLSjmlTPqM8aqqyBbGmNRUXjCmNB22bfX7OgDAdV1d70lcd6CqquM4YRgahtHtdi3Lks54PJZa07Qsy5TfL4imjHMu6xu9Yzc2LzdCpr4BoaN+wNopyTYAAAAASUVORK5CYII='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/778bc5dd099744afacfcd2918aa6821a/798b8/babel-plugin.webp 195w, /static/778bc5dd099744afacfcd2918aa6821a/0e356/babel-plugin.webp 390w, /static/778bc5dd099744afacfcd2918aa6821a/4c79d/babel-plugin.webp 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/webp" /> <source srcset="/static/778bc5dd099744afacfcd2918aa6821a/25fa3/babel-plugin.png 195w, /static/778bc5dd099744afacfcd2918aa6821a/506f3/babel-plugin.png 390w, /static/778bc5dd099744afacfcd2918aa6821a/ca1dc/babel-plugin.png 680w" sizes="(max-width: 680px) 100vw, 680px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/778bc5dd099744afacfcd2918aa6821a/ca1dc/babel-plugin.png" alt="Babel-plugin" title="Babel-plugin" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <p><a href="https://babeljs.io" target="_blank" rel="noopener noreferrer">Babel</a> est un compilateur “source à source” (ou <em>transpileur</em>) : il analyse du code JavaScript, le transforme et en génère un nouveau. Les phases d’analyse et de transformation sont effectuées par des plugins (<em>plug-ins</em>) que l’on déclare dans la configuration de Babel. La partie qui nous intéresse dans cet article concerne les plugins dits “customs”, qui ne correspondent pas à une proposition d’évolution d’EcmaScript. Ici, j’ai décidé d’implémenter une fonctionnalité de TypeScript : les propriétés-paramètre (<em>parameter properties</em>).</p> <hr /> <h2>Contexte</h2> <p>Les développeurs ayant eu le <em>grand</em> plaisir de réécrire leurs contrôleurs et services AngularJS <em>ES5-style</em> en <code class="language-text">class</code> <em>ES6</em> ont probablement été frustrés par l’écriture d’une affectation en propriété de chaque dépendance injectée dans les constructeurs. C’est un problème que TypeScript gère très bien grâce aux “parameter properties” ou “propriétés-paramètre”.</p> <p><em>Dans les exemples qui suivent, on admet l’utilisation de <a href="https://github.com/olov/ng-annotate" target="_blank" rel="noopener noreferrer">ng-annotate</a>, afin de ne pas avoir à injecter “manuellement” les dépendances via la propriété <a href="https://docs.angularjs.org/guide/di#-inject-property-annotation" target="_blank" rel="noopener noreferrer">$inject</a>, ou directement dans la déclaration du service. Il existe d’ailleurs un <a href="https://github.com/schmod/babel-plugin-angularjs-annotate" target="_blank" rel="noopener noreferrer">plugin Babel pour ng-annotate</a>.</em></p> <h3>ES5</h3> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token string">'use strict'</span><span class="token punctuation">;</span> angular <span class="token punctuation">.</span><span class="token function">module</span><span class="token punctuation">(</span><span class="token string">'MyAppModule'</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string">'DragService'</span><span class="token punctuation">,</span> DragService<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">DragService</span><span class="token punctuation">(</span><span class="token parameter">GestureListener<span class="token punctuation">,</span> InterfaceManager</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">activate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">activate</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'DragService instantiated'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Ce à quoi peut ressembler un service AngularJS écrit en ES5, si l’on suit les <a href="https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md" target="_blank" rel="noopener noreferrer">recommandations de John Papa</a>.</p> <h3>ES6</h3> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">class</span> <span class="token class-name">DragService</span> <span class="token punctuation">{</span> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">GestureListener<span class="token punctuation">,</span> InterfaceManager</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>GestureListener <span class="token operator">=</span> GestureListener<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>InterfaceManager <span class="token operator">=</span> InterfaceManager<span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'DragService instantiated'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> angular <span class="token punctuation">.</span><span class="token function">module</span><span class="token punctuation">(</span><span class="token string">'MyAppModule'</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string">'DragService'</span><span class="token punctuation">,</span> DragService<span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Et sa transformation en <code class="language-text">class</code>. Notez les affectations dans le constructeur :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">this</span><span class="token punctuation">.</span>GestureListener <span class="token operator">=</span> GestureListener<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>InterfaceManager <span class="token operator">=</span> InterfaceManager<span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>C’est de cette partie du code que l’on souhaite s’affranchir (surtout lorsque les dépendances sont nombreuses et qu’il est difficile d’y remédier sans réécrire une quantité importante de code).</p> <h3>TypeScript</h3> <p>Avec TypeScript, les <a href="https://www.typescriptlang.org/docs/handbook/classes.html" target="_blank" rel="noopener noreferrer">Parameter Properties</a> permettent d’automatiser ces affectations, en ajoutant un niveau d’accessibilité (<code class="language-text">public</code>, <code class="language-text">private</code>…) aux paramètres :</p> <div class="gatsby-highlight" data-language="typescript"><pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"><code class="language-typescript"><span class="token keyword">class</span> <span class="token class-name">DragService</span> <span class="token punctuation">{</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">private</span> GestureListener<span class="token punctuation">,</span> <span class="token keyword">private</span> InterfaceManager</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'DragService instantiated'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>L’accessibilité (<a href="https://github.com/tc39/proposal-private-fields" target="_blank" rel="noopener noreferrer">dont <code class="language-text">private</code></a>) n’étant pas encore au programme des normalisations EcmaScript, comme les <em>parameter properties</em>, j’ai décidé de créer un (petit) plugin Babel permettant de générer automatiquement ces affectations :</p> <h3>ES6 + <a href="https://github.com/VinceOPS/babel-plugin-proposal-parameter-properties" target="_blank" rel="noopener noreferrer">babel-plugin-proposal-parameter-properties</a></h3> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">@paramProperties <span class="token keyword">class</span> <span class="token class-name">DragService</span> <span class="token punctuation">{</span> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">@pp GestureListener<span class="token punctuation">,</span> @pp InterfaceManager</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'DragService instantiated'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> angular <span class="token punctuation">.</span><span class="token function">module</span><span class="token punctuation">(</span><span class="token string">'MyAppModule'</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string">'DragService'</span><span class="token punctuation">,</span> DragService<span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Lors de l’étape de Transformation, le plugin génère les affectations des paramètres décorés par <code class="language-text">@pp</code>, les insére dans le constructeur, puis supprime les décorateurs <code class="language-text">@paramProperties</code> et <code class="language-text">@pp</code>. Ce qui a pour effet de générer le même code que celui présenté plus haut (cf <strong>ES6</strong>).</p> <hr /> <h2>Fonctionnement de Babel</h2> <blockquote> <p>💡 Pour des raisons de lisibilité/clarté, beaucoup de détails ont été laissés de côté, sur le fonctionnement de Babel et sur ses modules “clés”. Pour un guide détaillé sur le transpileur et sur la création de plugin, référez-vous au <a href="https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md" target="_blank" rel="noopener noreferrer">babel-handbook</a>.</p> </blockquote> <p>Babel analyse un code existant, le transforme puis le génère à nouveau. Pour ce faire, le transpileur construit un <a href="https://fr.wikipedia.org/wiki/Arbre_syntaxique_abstrait" target="_blank" rel="noopener noreferrer">AST</a> à partir du code <strong>analysé</strong>. Chaque nœud de l’AST est d’un type particulier, dont on retrouve la <a href="https://babeljs.io/docs/core-packages/babel-types" target="_blank" rel="noopener noreferrer">liste exhaustive</a> dans la documentation de Babel. On peut générer l’AST d’un script à l’aide du <em>parser</em> de Babel, <a href="https://www.npmjs.com/package/babylon" target="_blank" rel="noopener noreferrer">babylon</a> :</p> <div class="gatsby-highlight" data-language="bash"><pre style="counter-reset: linenumber NaN" class="language-bash line-numbers"><code class="language-bash">babylon script.js <span class="token operator">></span> ast.json</code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <p>Il existe aussi AST Explorer, un outil en ligne qui permet d’accomplir le même travail en mode “playground”. <a href="https://astexplorer.net/" target="_blank" rel="noopener noreferrer">https://astexplorer.net/</a></p> <p>L’AST ainsi créé peut être <strong>transformé</strong> par Babel et par ses plugins. Une fois la transformation terminée, Babel <strong>génère</strong> un nouveau code.</p> <p><span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 780px;"> <a class="gatsby-resp-image-link" href="https://vinceops.me/static/60de370dc8df62ff3f31c9ec556cb6a3/c3dd4/babel-chain-3.png" style="display: block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom: 20.51282051282051%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAuJAAALiQE3ycutAAABMUlEQVQY02M4fvlZzO5z74ov3XmT+O7ZZQ4GKFBwm6hqaDPTmAENmFlM1jRxnG4A4////591/+VnaZtOPy+68fBNOsOppw/r1zy8vvjK21ctRWlZShKKGrpHGRjY+FXeVjGYvd8M0qSlZqylrW5sxODIwM5g+aGVQe/dMpC4vJSCXnRehsbzI1f6H60/vuz922/dDG/Pna16vWXjwi+vn1anlxUqqDGoyIhm8/swTOIq2yciWPWVgTGcwcZCmMHERPEsJ7vLfWbumssRAmW3YtkjGBgkxUPiM3T/X9/Z+f/Kspn///+qY7hz+V7lraN3Zj2887zhxOcTPCCb2SczeDEsZFj9hJdh3n8Ghm6Y9/4xMIT8ZmDY/C+BYfafdIYaqJc5rj963nTx4av+Z68/tAIAnKaB5AJPo94AAAAASUVORK5CYII='); background-size: cover; display: block;"></span> <picture> <source srcset="/static/60de370dc8df62ff3f31c9ec556cb6a3/798b8/babel-chain-3.webp 195w, /static/60de370dc8df62ff3f31c9ec556cb6a3/0e356/babel-chain-3.webp 390w, /static/60de370dc8df62ff3f31c9ec556cb6a3/23bbb/babel-chain-3.webp 780w, /static/60de370dc8df62ff3f31c9ec556cb6a3/8b443/babel-chain-3.webp 788w" sizes="(max-width: 780px) 100vw, 780px" type="image/webp" /> <source srcset="/static/60de370dc8df62ff3f31c9ec556cb6a3/25fa3/babel-chain-3.png 195w, /static/60de370dc8df62ff3f31c9ec556cb6a3/506f3/babel-chain-3.png 390w, /static/60de370dc8df62ff3f31c9ec556cb6a3/8a72f/babel-chain-3.png 780w, /static/60de370dc8df62ff3f31c9ec556cb6a3/c3dd4/babel-chain-3.png 788w" sizes="(max-width: 780px) 100vw, 780px" type="image/png" /> <img class="gatsby-resp-image-image" src="https://vinceops.me/static/60de370dc8df62ff3f31c9ec556cb6a3/8a72f/babel-chain-3.png" alt="Babel-chain" title="Babel-chain" loading="lazy" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" /> </picture> </a> </span></p> <h2>Création du plugin</h2> <p>Conformément au <em>babel-handbook</em>, on démarre avec le squelette suivant :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> types<span class="token operator">:</span> t <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> visitor<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>C’est en réalité <code class="language-text">babel</code> qui est passé en paramètre de notre plugin, mais on n’utilisera que sa propriété <code class="language-text">types</code>.</p> <h3> La traversée</h3> <p>L’AST est traversé en utilisant le <em>pattern</em> <a href="https://fr.wikipedia.org/wiki/Visiteur_(patron_de_conception)" target="_blank" rel="noopener noreferrer">Visitor</a>. Pour chaque type de nœud que l’on souhaite traiter, il suffit d’ajouter une méthode correspondante. Dans notre cas, on va s’intéresser au type <code class="language-text">ClassDeclaration</code>, puisque c’est sur les déclarations de <code class="language-text">class</code> que porte notre décorateur <code class="language-text">@paramProperties</code> :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">visitor<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token function">ClassDeclaration</span><span class="token punctuation">(</span><span class="token parameter">path<span class="token punctuation">,</span> state</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>path<span class="token punctuation">.</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/* peut aussi s'écrire : ClassDeclaration: { enter(path, state) { console.log(path.node); } } */</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>En plus de la propriété <code class="language-text">visitor</code>, les plugins peuvent aussi déclarer les fonctions <code class="language-text">pre(state)</code> et <code class="language-text">post(state)</code> qui permettent d’exécuter du code avant et après leur initialisation. Notez que dans le cadre de cet article, l’objet <code class="language-text">state</code> sera ignoré (inutilisé).</p> <p>Notre plugin va effectuer les actions suivantes :</p> <ul> <li>Vérifier que la classe déclarée soit bien décorée avec <code class="language-text">@paramProperties</code>.</li> <li>Supprimer le décorateur (pour le retirer de l’AST, et donc du code généré par Babel).</li> <li>Récupérer le nœud correspondant au constructeur de la classe.</li> <li>Insérer dans le corps du constructeur une affectation pour chacun de ses paramètres décorés avec <code class="language-text">@pp</code>. Les affectations doivent nécessairement être placées <em>après</em> l’appel éventuel de <code class="language-text">super()</code>.</li> </ul> <p>Je recommande l’utilisation du <em>package</em> <a href="https://www.npmjs.com/package/@types/babel-types" target="_blank" rel="noopener noreferrer">@types/babel-types</a> qui permet une autocompletion exhaustive et très utile.</p> <h3>Trouver et supprimer le décorateur</h3> <p>Pour que Babel accepte d’<strong>analyser</strong> notre code décoré, il faut d’abord ajouter le plugin permettant au <em>parser</em> de “comprendre” les décorateurs : <a href="https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-decorators" target="_blank" rel="noopener noreferrer">babel-plugin-syntax-decorators</a>. Le plugin ajoute une propriété (array) <code class="language-text">decorators</code> aux nœuds de l’AST.</p> <p>On peut ensuite facilement récupérer notre décorateur, s’il existe :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">DECORATOR</span> <span class="token operator">=</span> <span class="token string">'paramProperties'</span><span class="token punctuation">;</span> <span class="token comment">/** * Look for the expected decorator and return it (if declared). * * @param node ClassDeclaration node. * * @return The plugin-specific decorator (or `undefined`). */</span> <span class="token function">getDecorator</span><span class="token punctuation">(</span><span class="token parameter">node</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">(</span>node<span class="token punctuation">.</span>decorators <span class="token operator">||</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token parameter">decorator</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> decorator<span class="token punctuation">.</span>expression<span class="token punctuation">.</span>name <span class="token operator">===</span> <span class="token constant">DECORATOR</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><em>On se réfère à la <a href="https://babeljs.io/docs/core-packages/babel-types/#apidecorator" target="_blank" rel="noopener noreferrer">documentation</a> des types de Babel pour les propriétés de chaque type de nœud.</em></p> <p>Si aucun décorateur n’est trouvé, le traitement de ce nœud est interrompu. Sinon, on le supprime de notre noeud, pour qu’il n’apparaisse pas dans le code généré par Babel :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">/** * Remove paramProperties decorators from the class * declaration. * * @param node Node decorated with 'paramProperties'. */</span> <span class="token function">undecorate</span><span class="token punctuation">(</span><span class="token parameter">node</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> node<span class="token punctuation">.</span>decorators <span class="token operator">=</span> node<span class="token punctuation">.</span>decorators<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">d</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> d<span class="token punctuation">.</span>expression <span class="token operator">&&</span> d<span class="token punctuation">.</span>expression<span class="token punctuation">.</span>name <span class="token operator">!==</span> <span class="token constant">DECORATOR</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <h3>Trouver le constructeur et le modifier</h3> <p>Le constructeur peut être identifié grâce à son type <code class="language-text">ClassMethod</code>. Il est en effet une méthode de la classe déclarée, et peut être trouvé dans son corps (propriété <code class="language-text">body</code>), qui, conformément à la documentation, est de type <code class="language-text">ClassBody</code>, et contient une propriété <code class="language-text">body</code> qui est un tableau de <code class="language-text">ClassMethod</code>. On a donc des nœuds de type <code class="language-text">ClassMethod</code> dans <code class="language-text">node.body.body</code>.</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">/** * Get the constructor of the `node` class. * * @param node ClassDeclaration node. * * @return `node` class constructor. */</span> <span class="token function">getConstructor</span><span class="token punctuation">(</span><span class="token parameter">node</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> classBody <span class="token operator">=</span> node<span class="token punctuation">.</span>body<span class="token punctuation">;</span> <span class="token keyword">return</span> classBody<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token parameter">subNode</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>types<span class="token punctuation">.</span><span class="token function">isClassMethod</span><span class="token punctuation">(</span>subNode<span class="token punctuation">)</span> <span class="token operator">&&</span> subNode<span class="token punctuation">.</span>kind <span class="token operator">===</span> <span class="token string">'constructor'</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><code class="language-text">this.types</code> correspond au paramètre <code class="language-text">types</code> passé dans le squelette de notre plugin.</p> <p><em>Conformément à la documentation, la propriété <code class="language-text">kind</code> d’un nœud <code class="language-text">ClassMethod</code> est égale à ‘get’, ‘set’, ‘method’, ou ’<strong>constructor</strong>‘.</em></p> <p>On peut ensuite modifier le corps du constructeur : y ajouter une affectation par paramètre. Si un appel à <code class="language-text">super()</code> (il est forcément unique) est trouvé, alors on prend soin d’insérer les affectations <em>après</em> ce dernier (sinon, une exception sera levée). <code class="language-text">ClassMethod</code> possède une propriété <code class="language-text">body</code> de type <code class="language-text">BlockStatement</code> qui possède une propriété <code class="language-text">body</code> étant un tableau de <code class="language-text">Statement</code>. On tente donc de trouver dans <code class="language-text">constructeur.body.body</code> un nœud <code class="language-text">CallExpression</code> dont la fonction cible est de type <code class="language-text">Super</code>.</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">/** * Insert the parameter properties assignments in the constructor body. * If a call to `super` is found, append the statements just after it, * otherwise, "prepend" the statements. * * @param ctor Class constructor. */</span> <span class="token function">insertAssignments</span><span class="token punctuation">(</span><span class="token parameter">ctor</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> ctorBody <span class="token operator">=</span> ctor<span class="token punctuation">.</span>body<span class="token punctuation">.</span>body<span class="token punctuation">;</span> <span class="token keyword">const</span> superIndex <span class="token operator">=</span> ctorBody<span class="token punctuation">.</span><span class="token function">findIndex</span><span class="token punctuation">(</span><span class="token parameter">n</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>types<span class="token punctuation">.</span><span class="token function">isExpressionStatement</span><span class="token punctuation">(</span>n<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token keyword">this</span><span class="token punctuation">.</span>types<span class="token punctuation">.</span><span class="token function">isCallExpression</span><span class="token punctuation">(</span>n<span class="token punctuation">.</span>expression<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token keyword">this</span><span class="token punctuation">.</span>types<span class="token punctuation">.</span><span class="token function">isSuper</span><span class="token punctuation">(</span>n<span class="token punctuation">.</span>expression<span class="token punctuation">.</span>callee<span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">let</span> assignmentCount <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> ctor<span class="token punctuation">.</span>params<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">param</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">isParamProperty</span><span class="token punctuation">(</span>param<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> assignment <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAssignmentStatement</span><span class="token punctuation">(</span>param<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> index <span class="token operator">=</span> superIndex <span class="token operator">+</span> <span class="token number">1</span> <span class="token operator">+</span> assignmentCount<span class="token punctuation">;</span> ctorBody<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>index<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> assignment<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token operator">++</span>assignmentCount<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">undecorateParameter</span><span class="token punctuation">(</span>param<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p><em><code class="language-text">isParameterProperty</code> filtre le tableau <code class="language-text">decorators</code> du <code class="language-text">param</code> à la recherche d’un décorateur <code class="language-text">@pp</code>. Et <code class="language-text">undecorateParameter</code> supprime <code class="language-text">@pp</code> du tableau.</em></p> <p>La méthode <a href="https://github.com/VinceOPS/babel-plugin-proposal-parameter-properties/blob/master/src/parameter-properties.js#L96" target="_blank" rel="noopener noreferrer"><code class="language-text">getAssignmentStatement</code></a> permet de générer une affectation de type :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">this</span><span class="token punctuation">.</span>param <span class="token operator">=</span> param<span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span></span></pre></div> <h3>Test du plugin</h3> <p>J’ai testé (avec <a href="https://facebook.github.io/jest/" target="_blank" rel="noopener noreferrer">Jest</a>) deux axes du plugin : d’abord la génération de la transformation induite par le plugin, à l’aide des <em>snapshots</em> (gérés par Jest). Puis, plus dans le détail, les transformations faites dans l’AST.</p> <h4>Snapshots</h4> <p>Voici un exemple de test unitaire validant un <em>snapshot</em> :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token keyword">const</span> babel <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'babel-core'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> plugin <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'../'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>default<span class="token punctuation">;</span> <span class="token keyword">const</span> syntaxDecorators <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'babel-plugin-syntax-decorators'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">'Decorated classes only: '</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">'does transform when decorator is valid'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> example <span class="token operator">=</span> <span class="token string">'@paramProperties class DoCare { constructor(@pp prop1) {} }'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token punctuation">{</span>code<span class="token punctuation">}</span> <span class="token operator">=</span> babel<span class="token punctuation">.</span><span class="token function">transform</span><span class="token punctuation">(</span>example<span class="token punctuation">,</span> <span class="token punctuation">{</span>plugins<span class="token operator">:</span> <span class="token punctuation">[</span>syntaxDecorators<span class="token punctuation">,</span> plugin<span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span>code<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toMatchSnapshot</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le premier appel à <code class="language-text">toMatchSnapshot</code> va créer un fichier dans un répertoire <code class="language-text">__snapshots__</code>, contenant le code que Babel doit générer s’il utilise notre plugin:</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">exports<span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Decorated classes only: does transform when decorator is valid 1</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> "class DoCare { constructor(prop1) { this.prop1 = prop1; } }" </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>On assure ainsi l’échec du test en cas de modification du plugin ayant un impact sur le code de sortie, puisque les prochains appels à <code class="language-text">toMatchSnapshot</code> ne correspondront plus au fichier généré. En cas d’évolution, le snapshot peut être mis à jour en exécutant <code class="language-text">jest -u</code>.</p> <h4>Transformations de l’AST</h4> <p>Les tests se déroulent en deux temps : d’abord, on génère un morceau d’AST “manuellement” (que l’on pourrait aussi obtenir avec du code JavaScript (sous forme de string) passé à <strong>babylon</strong>). Ensuite, on s’assure du bon fonctionnement de nos transformations.</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token comment">// avant le test unitaire</span> <span class="token keyword">const</span> <span class="token constant">CLASS_DECORATOR</span> <span class="token operator">=</span> <span class="token string">'paramProperties'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token constant">PARAM_DECORATOR</span> <span class="token operator">=</span> <span class="token string">'pp'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> identifier <span class="token operator">=</span> t<span class="token punctuation">.</span><span class="token function">identifier</span><span class="token punctuation">(</span><span class="token string">'MyClass'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> decorator <span class="token operator">=</span> t<span class="token punctuation">.</span><span class="token function">decorator</span><span class="token punctuation">(</span>t<span class="token punctuation">.</span><span class="token function">identifier</span><span class="token punctuation">(</span><span class="token constant">CLASS_DECORATOR</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> classBody <span class="token operator">=</span> t<span class="token punctuation">.</span><span class="token function">classBody</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> classDeclaration <span class="token operator">=</span> t<span class="token punctuation">.</span><span class="token function">classDeclaration</span><span class="token punctuation">(</span>identifier<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">,</span> classBody<span class="token punctuation">,</span> <span class="token punctuation">[</span>decorator<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> paramDecorators <span class="token operator">=</span> <span class="token punctuation">[</span>t<span class="token punctuation">.</span><span class="token function">decorator</span><span class="token punctuation">(</span>t<span class="token punctuation">.</span><span class="token function">identifier</span><span class="token punctuation">(</span><span class="token constant">PARAM_DECORATOR</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Correspond à :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">@paramProperties <span class="token keyword">class</span> <span class="token class-name">MyClass</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span></span></pre></div> <p>Puis vient le test :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">'inserts the correct amount of assignments'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> paramsCount <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span> <span class="token comment">// constructor parameters names: prop0, prop1, prop2</span> <span class="token keyword">const</span> params <span class="token operator">=</span> <span class="token function">Array</span><span class="token punctuation">(</span>paramsCount<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">fill</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">e<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">prop</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>i<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> ctor <span class="token operator">=</span> t<span class="token punctuation">.</span><span class="token function">classMethod</span><span class="token punctuation">(</span><span class="token string">'constructor'</span><span class="token punctuation">,</span> t<span class="token punctuation">.</span><span class="token function">identifier</span><span class="token punctuation">(</span><span class="token string">'constructor'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> params<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">e</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> identifier <span class="token operator">=</span> t<span class="token punctuation">.</span><span class="token function">identifier</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span> identifier<span class="token punctuation">.</span>decorators <span class="token operator">=</span> paramDecorators<span class="token punctuation">;</span> <span class="token keyword">return</span> identifier<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// empty body: constructor(@pp prop0, @pp prop1, @pp prop2) {}</span> t<span class="token punctuation">.</span><span class="token function">blockStatement</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> ctorBody <span class="token operator">=</span> ctor<span class="token punctuation">.</span>body<span class="token punctuation">.</span>body<span class="token punctuation">;</span> classBody<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>ctor<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// [...]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Cette partie “complète” notre AST :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript">@paramProperties <span class="token keyword">class</span> <span class="token class-name">MyClass</span> <span class="token punctuation">{</span> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">@pp prop0<span class="token punctuation">,</span> @pp prop1<span class="token punctuation">,</span> @pp prop2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span></span></pre></div> <p>Que l’on va pouvoir transformer puis tester :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">'inserts the correct amount of assignments'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// [...]</span> paramProps<span class="token punctuation">.</span><span class="token function">insertAssignments</span><span class="token punctuation">(</span>ctor<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span>ctorBody<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBe</span><span class="token punctuation">(</span>paramsCount<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Ici, on s’assure que le nœud du constructeur ainsi transformé contiennent bien 3 <code class="language-text">Statement</code> (contre 0 avant), conformément au résultat attendu :</p> <div class="gatsby-highlight" data-language="javascript"><pre style="counter-reset: linenumber NaN" class="language-javascript line-numbers"><code class="language-javascript"><span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">prop0<span class="token punctuation">,</span> prop1<span class="token punctuation">,</span> prop2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>prop0 <span class="token operator">=</span> prop0<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>prop1 <span class="token operator">=</span> prop1<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>prop2 <span class="token operator">=</span> prop2<span class="token punctuation">;</span> <span class="token punctuation">}</span></code><span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"><span></span><span></span><span></span><span></span><span></span></span></pre></div> <p>Le code du plugin est disponible ici : <a href="https://github.com/VinceOPS/babel-plugin-proposal-parameter-properties" target="_blank" rel="noopener noreferrer">VinceOPS/babel-plugin-proposal-parameter-properties</a>.</p></div>