Les modules en Typescript en 5 min

Dans un article précédent, j’avais eu l’occasion d’évoquer les points les plus essentiels de la syntaxe Typescript (cf. L’essentiel de la syntaxe Typescript en 10 min). Volontairement, cet article ne traitait pas des modules, des exports et des imports d’objets de façon à parler de ces sujets dans un article à part entière. La raison principale qui a motivé un article séparé est que les modules correspondent à un sujet plus complexe que les différents éléments de syntaxe Typescript.

En effet, la définition des modules en Typescript hérite de la complexité des modules en Javascript. Comme on a pu l’indiquer dans un article consacré aux modules Javascript, il y a différentes méthodes pour définir des modules en Javascript. Etant donné que ce langage ne prévoyait pas de façon native les modules, ces différentes méthodes correspondent à autant de solutions pour tenter de les implémenter. A partir d’ECMAScript 2015 (i.e. ES6), l’implémentation de modules en Javascript peut se faire de façon native. Même si ES2015 a rendu obsolètes les solutions précédentes d’implémentation de modules, on ne peut pas complétement les ignorer car beaucoup de code existant les utilise encore.

Les modules Typescript sont moins complexes qu’ils y paraissent

De même que pour Javascript avant ES2015, Typescript a aussi, de son coté, dû trouver des éléments syntaxiques pour définir et implémenter des modules. Quand Javascript a implémenté ES2015, la syntaxe de Typescript s’est naturellement rapprochée de celle d’ES2015. La conséquence de cette évolution est que la notion de module peut être implémentée d’une part, avec des éléments syntaxiques spécifiques à Typescript et d’autre part, avec une syntaxe similaire à celle d’ES2015.

En préambule, cet article expliquera quelques éléments nécessaires à la compréhension des modules en Typescript. Dans un 2e temps, on indiquera quelques solutions pour implémenter des modules avant et après ES2015.

Préambule

Avant de rentrer dans le détail des syntaxes Typescript permettant d’implémenter des modules, il faut savoir comment ces modules sont transpilés en code Javascript. Avant ES2015, la notion de module n’était pas native à Javascript, il a donc fallu trouver une façon d’implémenter des modules. La méthode a été d’utiliser le pattern module et des IIFE (i.e. Immediatly-Invoked Function Expression ou Expression de fonction invoquée immédiatement).

Pattern module

Sans rentrer dans les détails, le pattern module permet d’implémenter la notion de module en Javascript avant ES2015. Ce pattern utilise une IIFE (i.e. Immediatly-Invoked Function Expression ou Expression de fonction invoquée immédiatement) pour définir le contenu du module. Une IIFE est un bloc de code qui est exécuté tout de suite après avoir été parsé. Dans ce bloc de code, on peut ainsi définir ce qui sera le module.

Une IIFE s’écrit de cette façon:

var Module = (function() {  
    // Bloc de code à exécuter 
})();

Pour utiliser une IIFE pour définir un module, on peut écrire par exemple:

var Module = (function() {  
    var self = {};  
    function privateFunc() {  
        // ...  
    };  

    self.publicFunc = function() {  
        privateFunc();  
    };  

    return self;  
})();  

Cette écriture permet de définir une variable appelée Module qui va contenir des membres et des fonctions, ce qui correspond à la notion de module:

  • privateFunc() est une fonction privée.
  • publicFunc() est une fonction publique.

Pour utiliser ce type de module, on peut l’instancier et l’appeler de cette façon:

var moduleInstance = new Module();  
moduleInstance.publicFunc();  

Pour être compatible avec ES5, les modules Typescript sont transpilés en utilisant le pattern module (pour plus de détails sur la syntaxe Javascript du pattern module, voir Les modules en Javascript en 5 min).

Directive “Triple-slash”

Cette directive dans l’en-tête d’un fichier Typescript permet d’indiquer au compilateur une référence vers un autre fichier en indiquant son emplacement physique. Le chemin est relatif par rapport au fichier contenant la référence. Cette directive doit être placée dans l’en-tête du fichier pour être valable, elle est de type:

/// <reference path="fichier.ts" /> 

Ce type de directive est essentiellement utilisé pour référencer des fichiers Typescript externes à un projet. La plupart du temps, avec les fichiers de configuration tsconfig.json, elle n’est plus nécessaire puisqu’il est possible d’indiquer dans ce fichier les emplacements des fichiers à compiler.

Par exemple, pour ne pas utiliser de directives triple-slash, on peut utilser les éléments de configuration "files" ou "include" dans un fichier tsconfig.json:

{ 
    ... 
    "files": [ 
        "fichier1.ts", 
        "fichier2.ts", 
        "fichier3.ts" 
    ], 
    "include": [ 
        "src/**/*" 
    ], 
    ... 
} 

Plus de détails sur ces éléments de configuration dans la partie sur la résolution des modules.

Un exemple d’utilisation d’une directive triple-slash se trouve dans le dépôt Github suivant: typescript_modules/typescript_triple-slash.

Cet exemple permet d’illustrer des appels d’un module à l’autre:

  • le module NamespaceForModule1 dans example/module1.ts appelle le module NamespaceForModule2 dans other_modules/modules2.ts. La directive est:
    /// <reference path="../other_modules/module2.ts" />
    
  • le module NamespaceForModule2 appelle le module NamespaceForModule3 dans other_modules/modules3.ts. La directive est:
    /// <reference path="module3.ts" /> 
    

Pour exécuter cet exemple, il faut:

  1. Aller dans le répertoire typescript_triple-slash/example et exécuter:
    user@debian:~/typescript_modules/typescript_triple-slash% npm install
    
  2. Compiler en exécutant la commande:
    user@debian:~/typescript_modules/typescript_triple-slash% npm run build
    
  3. Exécuter le serveur de développement avec l’instruction:
    user@debian:~/typescript_modules/typescript_triple-slash% npm start
    
  4. Ouvrir la page http://localhost:8080 dans un browser
  5. Enfin, il faut afficher la console de développement.
Pour afficher la console de développement dans un browser

Pour tous les exemples présentés dans cet article, pour voir les résultats d’exécution, il faut afficher la console de développement:

  • Sous Firefox: on peut utiliser la raccourci [Ctrl] + [Maj] + [J] (sous MacOS: [⌘] + [Maj] + [J], sous Linux: [Ctrl] + [Maj] + [K]) ou en allant dans le menu “Développement web” ⇒ “Console du navigateur”.
  • Sous Chrome: utiliser le raccourci [F12] (sous MacOS: [⌥] + [⌘] + [I], sous Linux: [Ctrl] + [Maj] + [I]) puis cliquer sur l’onglet “Console”. A partir du menu, il faut aller dans “Plus d’outils” ⇒ “Outils de développement”.
  • Sous EDGE: utiliser le raccourci [F12] puis naviguer jusqu’à l’onglet “Console”.

Le résultat de la compilation est de type:

Executed from module1.privateFunc() 
Executed from module2.privateFunc() 
Executed from module2.publicFunc() 
Executed from module3.privateFunc() 
Executed from module3.publicFunc() 
Executed from module1.publicFunc()
Suppression du cache du browser

Sachant que tous les exemples présentés dans cet article utilisent la même URL http://localhost:8080, il peut subvenir certaines erreurs pendant leur exécution. Ces erreurs peuvent être dues au cache utilisé dans les browsers, en particulier pour les fichiers Javascript. Pour éviter que d’anciennes versions du fichiers restées dans le cache ne soient exécutées à la place de nouveaux fichiers:

  • On peut forcer le rechargement de la page avec le raccourci [Ctrl] + [Maj] + R
  • On peut aussi supprimer les fichiers se trouvant dans le cache en exécutant le raccourci [Ctrl] + [Maj] + [Supp]

Implémenter des modules en Typescript

Il existe plusieurs méthodes pour implémenter des modules en Typescript:

  • Utiliser des namespaces: ils se rapprochent de la notion de namespace en C#. Ils sont transpilés en modules en Javascript.
  • Avant ES2015/ES6: il faut utiliser des formats pour étendre la syntaxe Javascript pour permettre l’implémentation de modules. Ces formats peuvent CommonJS, AMD (i.e. Asynchronous Module Definition) ou UMD (i.e. Universal Module Definition). Ces formats doivent être associés à des loaders qui permettent de charger les modules transpilés en Javascript au runtime.
  • Utiliser la syntaxe ES2015/ES6: à partir d’ES2015, Javascript prévoit des éléments de syntaxe pour implémenter nativement des modules. En Typescript, la syntaxe permettant d’implémenter ce type de module est très proche de celle en Javascript.

Namespaces

Les namespaces correspondent à une notion spécifique à Typescript pour implémenter des modules en Javascript. Les namespaces en Typescript sont très semblables aux namespaces en C#: il s’agit d’un découpage du code en bloc correspondant à des modules logiques.

Comme pour les namespaces en C#, les namespaces Typescript:

  • Ont des noms qui se définissent avec une hiérarchie du type <namespace1>.<namespace2>.<namespace3>, par exemple:
    mainNs.intermediateNs.innerNs 
    
  • Les blocs de code sont inclus dans les namespaces, par exemple:
    namespace nsName { 
        // Bloc de code 
    } 
    
  • Sont indépendants des fichiers Typescript: on peut utiliser un même namespace dans des fichiers différents. Les éléments dans le namespace seront, ainsi, regroupés.

Les namespaces sont une approche permettant d’implémenter des modules en Javascript mais elle n’est pas la seule. D’autres approches permettent d’implémenter cette notion.

Avant Typescript 1.5, les namespaces Typescript étaient appelés “internal modules“. Ce terme prête particulièrement à confusion puisqu’il facilite la confusion entre la notion de module Typescript et les modules à proprement parlé en Javascript. Il est important d’avoir en tête que cette notion de namespace n’est pas tout à fait semblable aux modules en Javascript.

Mot clé namespace et module sont strictement équivalents

Au niveau de la syntaxe Typescript, le mot clé namespace peut être remplacé par le mot clé module. Dans ce contexte, les 2 mots clé sont équivalents.

Un exemple d’implémentation de namespaces se trouvent dans le dépôt Github suivant: typescript_modules/typescript_namespace.

Cet exemple permet d’illuster des appels d’un module à l’autre. Les différents modules sont définis dans des namespaces différents.

Par exemple, le namespace module1Namespace qui se trouve dans le fichier module1.ts:

namespace module1Namespace { 
    function privateFunc() { 
        console.log("Executed from module1.privateFunc()"); 
    } 

    export function publicFunc() { 
        privateFunc(); 

        console.log("Executed from module1.publicFunc()"); 

        namespaceForModule2.publicFunc(); 
    } 
} 

Dans ce fichier, on importe le namespace module2Namespace se trouvant dans le fichier module2.ts de cette façon:

import namespaceForModule2 = module2Namespace; 

On peut ensuite appeler une fonction se trouvant dans le namespace module2Namespace en écrivant:

namespaceForModule2.publicFunc(); 

L’utilisation de la directive import est complétement facultative et sert uniquement à définir l’alias namespaceForModule2, on peut se contenter d’appeler directement:

module2Namespace.publicFunc(); 

De même que précédemment, pour compiler cet exemple:

  1. Il faut exécuter les instructions suivantes pour, respectivement, installer les packages npm, compiler et démarrer le serveur de développement:
    user@debian:~/typescript_modules/typescript_namespace% npm install && npm run build && npm start
    
  2. Ouvrir la page http://localhost:8080 dans un browser.
  3. Afficher la console de développement.

Le résultat de l’exécution est semblable à celui de l’exemple précédent.

On peut remarquer que le résultat de la compilation produit un module au sens Javascript en utilisant le pattern module. Par exemple, si on regarde le résultat de la compilation du contenu du fichier module1.ts dans le fichier public/module1.js:

 var namespaceForModule2 = module2Namespace; 
var module1Namespace; 
(function (module1Namespace) { 
    function privateFunc() { 
        console.log("Executed from module1.privateFunc()"); 
    } 

    function publicFunc() { 
        privateFunc(); 
        console.log("Executed from module1.publicFunc()"); 
        namespaceForModule2.publicFunc(); 
    } 

    module1Namespace.publicFunc = publicFunc; 
})(module1Namespace || (module1Namespace = {}));

Les namespaces peuvent être mergés

Les namespaces peuvent être mergés. Ainsi on peut définir des objets dans un même namespace réparti sur plusieurs fichiers Typescript à condition de ne pas avoir de collision dans le nommage des objets exportés.

Dans le répertoire typescript_modules/typescript_mergin_namespace se trouve un exemple d’un namespace défini sur plusieurs fichiers Typescript.

Par exemple, on définit le namespace module2Namespace dans le fichier module2.ts:

namespace module2Namespace { 
    function privateFunc() { 
        console.log("Executed from module2.privateFunc()"); 
    } 

    export function publicFunc() { 
        privateFunc(); 
        console.log("Executed from module2.publicFunc()"); 
        otherPublicFunc(); 
    } 
}

Et dans le fichier module3.ts:

namespace module2Namespace { 
    function privateFunc() { 
        console.log("Executed from privateFunc() in module3.ts"); 
    } 

    export function otherPublicFunc() { 
        privateFunc(); 
        console.log("Executed from otherPublicFunc() in module3.ts"); 
    } 
} 

A condition d’exporter des éléments avec le mot clé export, on peut utiliser plusieurs fichiers pour définir un namespace. Les fichiers module2.ts et module3.ts seront transpilés en 2 fichiers Javascript qui définissent un seul module Javascript.

Pour compiler, il faut exécuter:

user@debian:~/typescript_modules/typescript_merging_namespace% npm install && npm run build

Parmi les fichiers générés, on obtient module2.js:

var module2Namespace; 
(function (module2Namespace) { 
    function privateFunc() { 
        console.log("Executed from module2.privateFunc()"); 
    } 

    function publicFunc() { 
        privateFunc(); 
        console.log("Executed from module2.publicFunc()"); 
        module2Namespace.otherPublicFunc(); 
    } 

    module2Namespace.publicFunc = publicFunc; 
})(module2Namespace || (module2Namespace = {})); 

Et module3.js:

var module2Namespace; 
(function (module2Namespace) { 
    function privateFunc() { 
        console.log("Executed from privateFunc() in module3.ts"); 
    } 

    function otherPublicFunc() { 
        privateFunc(); 
        console.log("Executed from otherPublicFunc() in module3.ts"); 
    } 

    module2Namespace.otherPublicFunc = otherPublicFunc; 
})(module2Namespace || (module2Namespace = {})); 

On remarque que ces fichiers permettent de définir un même module nommé module2Namespace.

Pour exécuter cet exemple, il faut exécuter:

user@debian:~/typescript_modules/typescript_merging_namespace% npm start 

Il faut ensuite ouvrir un browser à l’adresse http://localhost:8080 et afficher la console de développement.

Implémenter des modules avant ES2015

Les namespaces Typescript ne correspondent pas tout à fait aux modules Javascript. Ils correspondent à un découpage logique du code qui peut être séparé en plusieurs fichiers. A l’opposé les modules Javascript sont découpés par fichier: un fichier correspond à un module le plus souvent.

Avant Typescript 1.5, namespace et module s’appelaient différemment:

  • Les namespaces s’appelaient “internal modules” et
  • Les modules s’appelaient “external modules“.

Même si les notions de namespace Typescript et de module sont différentes, après compilation du code, les namespaces Typescript sont transpilés en module Javascript en utilisant le pattern module. En effet, avant ES2015, Javascript ne gérait pas de façon native la notion de module. Le pattern module est un moyen de contournement pour définir des modules. Pour implémenter ces modules, on peut s’aider de formats qui vont apporter des mot-clés pour enrichir le langage Javascript et permettre une implémentation plus facile des modules sans écrire explicitement le pattern module.

En Typescript, on bénéficie de ces formats et on peut, comme en Javascript, les utiliser pour implémenter des modules Javascript avant ES2015. Au runtime, pour charger les modules en utilisant le format, il faut s’aider de loaders. Les formats les plus connus sont CommonJS, AMD (i.e. Asynchronous Module Definition) et UMD (i.e. Universal Module Definition). Les loaders les plus connus sont RequireJS (utlisant le format AMD), SystemJS (qui permet de gérer plusieurs formats).

CommonJS + SystemJS

CommonJS et SystemJS sont respectivement un format et un loader permettant d’implémenter les modules avant ES2015. Il existe d’autres formats comme AMD ou UMD. Un exemple d’implémentation de module en utilisant CommonJS se trouve dans le dépôt Github suivant: typescript_modules/typescript_commonjs. Cet exemple permet d’illustrer des appels entre modules.

Comme en Javascript, CommonJS permet de définir un format pour enrichir le langage avec une fonction qui va permettre d’effectuer des références vers un autre module. La fonction utilisée pour inclure des modules provenant d’un autre fichier est require.

Par exemple, si on écrit:

var moduleInFile = require('./file.js');  

On définit une variable locale contenant le module se trouvant dans le fichier file.js. On peut directement utiliser les éléments se trouvant dans le module en les préfixant avec moduleInFile.

CommonJS sert de format pour la compilation. Pour l’exécution du code, il est nécessaire d’utiliser un loader qui va interpréter la syntaxe définie par le format CommonJS et charger les modules Javascript. SystemJS est un loader qui permet d’interpréter le format CommonJS.

Pour plus de détails sur CommonJS et SystemJS, voir CommonJS + SystemJS.

Dans le cas de notre exemple, si on regarde le code du module se trouvant dans le fichier module1.ts:

import importedModule2 = require('./module2'); 

function privateFunc() { 
    console.log("Executed from module1.privateFunc()"); 

    importedModule2.publicFunc(); 
} 

export function publicFunc() { 
    privateFunc(); 

    console.log("Executed from module1.publicFunc()");
} 

On importe le module2 en utilisant la fonction require du format CommonJS:

import importedModule2 = require('./module2'); 

Puis on utilise l’alias défini pour appeler une fonction se trouvant dans le module2:

importedModule2.publicFunc(); 

Pour compiler, il faut exécuter:

user@debian:~/typescript_modules/typescript_commonjs% npm install && npm run build

Après compilation, la syntaxe produit des modules compatibles avec le format CommonJS:

"use strict"; 
Object.defineProperty(exports, "__esModule", { value: true }); 
var importedModule2 = require("./module2"); 
function privateFunc() { 
    console.log("Executed from module1.privateFunc()"); 
    importedModule2.publicFunc(); 
} 

function publicFunc() { 
    privateFunc(); 

    console.log("Executed from module1.publicFunc()"); 
} 

exports.publicFunc = publicFunc; 

Pour exécuter ce code, il faut effectuer quelques ajouts dans la page HTML principale en important systemJS puis en le configurant:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8">  
        <title>Test module Typescript with /// (triple-slash)</title> 
        <script src="node_modules/systemjs/dist/system.js"></script>  
        <script> 
            // Le code suivant correspond à de la configuration  
            SystemJS.config({  
                meta:{     
                    format: 'cjs'  // cjs correspond à la configuration pour le format commonjs  
                },     

                packages: { 
                "/public": { defaultExtension: "js" }   // pour charger par défaut les fichiers avec l'extension JS 
                }
            });  

            SystemJS.import('public/index.js'); 
        </script>
    </head>    
    <body>
    </body> 
</html> 

Pour exécuter cet exemple, il faut exécuter la commande:

  1. user@debian:~/typescript_modules/typescript_commonjs% npm start
    
  2. Se connecter avec un browser à l’adresse http://localhost:8080
  3. Afficher la console de développement.

Le résultat de l’exécution est semblable à celui des exemples précédents.

Modules ES2015/ES6

Il est possible d’utiliser la syntaxe ES2015 dans le code Typescript pour définir des modules Javascript. Les modules Javascript ainsi compilés ont les mêmes caractéristiques que des modules ES2015 classiques. Ils sont supportés nativement par Node.js et par les browsers à partir d’une certaine version:

  • Chrome à partir de la version 63.
  • Firefox à partir de la version 60,
  • Edge à partir de la version 16,
  • Safari à partir de la version 11,
  • Node.js à partir de la version 10.

Pour plus d’informations sur les compatibilités des browsers:

Le code Javascript comportant des modules ES2015 s’exécute de plusieurs façons:

  • Exécuter le code Javascript sur un serveur Node.js: à partir de la version 10, Node.js gère nativement le chargement de modules au format ES2015.
  • Il est aussi possible d’associer un loader comme RequireJS ou SystemJS qui va charger lui-même les modules.

Les propriétés les plus importantes à avoir en tête avec les modules ES2015 sont les suivantes:

  • Par défaut, tous les modules ES2015 sont privés.
  • Un fichier Javascript correspond à un module.
  • Pour exposer des éléments comme des variables, des fonctions ou des classes à l’extérieur d’un module, il faut utiliser le mot-clé export.

Exporter un module

La syntaxe Typescript utilisant des modules est semblable à celle en Javascript. Il faut exporter explicitement les éléments pour les utiliser à l’extérieur d’un module.

Pour exporter, on utilise le mot-clé export.

Par exemple:

export function func1() {  
    // ...  
}  

Dans cet exemple, on exporte une fonction à l’extérieur du module.

D’autres formes de syntaxe sont possibles pour exporter plusieurs éléments du module en une fois:

function func1() {  
    // ...  
}  

function func2() {  
    // ...  
}  

export { func1, func2 };

On peut aussi indiquer l’instruction export avant la déclaration des éléments à exporter:

export { func1, func2 };

function func1() {  
    // ...  
}  

function func2() {  
     // ...  
}  

On peut indiquer un alias lors de l’export:

export { func1 as exporterFunc1 };  

function func1() {  
    // ...  
}

Importer un module

L’import d’un module correspond à importer les éléments exportés par ce module dans un autre.

Pour importer tous les éléments exportés d’un module dans une variable, on peut utiliser la syntaxe:

import * as moduleInFile from './file'; 

Dans ce cas, la variable est moduleInFile, on peut l’utiliser directement pour accéder aux éléments du module, par exemple:

moduleInFile.func1();  

On peut importer un élément spécifique et non pas tous les éléments exportés du module en précisant les éléments à importer à partir de leur nom, par exemple:

import { func1 as funcFromOuterModule, func2 } from './file';  

func1 as funcFromOuterModule permet de renommer le nom de l’élément importé. Cette déclaration est facultative, ainsi elle n’est pas utilisée pour func2.

Dans ce cas, on peut appeler directement les fonctions:

func2();  
funcFromOuterModule();  

Export par défaut

L’export d’un élément par défaut permet d’indiquer l’élément qui sera importé dans un module si ce dernier ne précise pas ce qui doit être importé. Ainsi, au moment d’importer l’élément d’un module, il ne sera pas nécessaire de préciser le nom de l’élément à importer, l’élément par défaut sera le seul élément importé même si le fichier comporte d’autres éléments.

Par exemple:

function func1() {  
    // ...  
}  

function func2() {  
    // ...  
}  

export default func1;  

Au moment d’importer, le seul élément importé sera l’élément exporté par défaut:

import func1 from "file";   

L’import ne comporte pas d’accolades. C’est l’élément par défaut qui est importé.

L’élément importé par défaut peut être utilisable directement:

func1();  

Export nommé

On peut nommer un export de façon à modifier le nom de l’élément qui est exporté.

Par exemple, dans un premier temps on exporte un élément en le renommant:

function func1() {  
    // ...  
}  

function func2() {  
    // ...  
}  

export default func1;  
export var finalFunc = func2;

En plus de l’élément par défaut, on décide de renommer un élément exporté.

A l’import, il faut indiquer le nouveau nom de l’élément:

import { finalFunc } from "./file";  

On peut directement utilisé le nouveau nom de l’élément:

finalFunc();  

Dans cet exemple, si on souhaite importer l’élément par défaut, on peut écrire:

import func1, { finalFunc } from "./file";  

Configuration du compilateur

Il existe une option de compilation qui permet d’indiquer la syntaxe à utiliser pour les modules dans le code Javascript et pour générer du code Javascript avec une syntaxe différente. L’option module permet d’indiquer le format à utiliser pour la génération des modules:

{ 
    "compilerOptions": { 
        "module": "es6"
    } 
} 

Les valeurs possibles sont "CommonJS", "AMD", "UMD", "ES6", "ES2015", "ESNEXT" ou "None". Si on utilise "None", on ne peut pas déclarer des modules dans le code Typescript. Il ne faut pas confondre ce paramètre avec target qui indique la version d’ECMAScript cible de compilation. La configuration module ne concerne que le format utilisé pour les modules.

Ainsi, si on utilise l’option:

  • "es6" ou "es2015": il faut préciser l’extension ".js" dans les déclarations import dans les fichiers Typescript, par exemple:
    import { finalFunc } from "./file.js";
    

    Cette déclaration sera transpilée tel quel dans le fichier Javascript. Au runtime, pour que le browser ou Node.js puisse trouver le fichier à importer, il faut qu’il comporte l’extension du fichier Javascript.

  • "CommonJS": le format généré sera CommonJS. Au runtime, il faut associer ce format à un loader comme par exemple SystemJS.
  • "AMD": le format généré sera AMD. De même au runtime, il faut associer ce format à un loader comme par exemple RequireJS.
  • "UMD": le format généré sera UMD (i.e. Universal Module Definition). Ce format permet de générer compatible avec plusieurs formats comme AMD et CommonJS.

On peut aussi préciser le format au moment d’exécuter le compilateur:

tsc --module <nom du format>

Par exemple:

tsc --module es6

Exemple avec la syntaxe ES2015/ES6

Un exemple d’implémentation de modules en utilisant la syntaxe ES2015/ES6 se trouvent dans le dépôt Github suivant: typescript_modules/typescript_es6. Cet exemple permet d’illustrer des appels d’un module à l’autre en utilisant la syntaxe ES6.

Pour exécuter cet exemple:

  1. Il faut exécuter les instructions suivantes pour, respectivement, installer les packages npm, compiler et démarrer le serveur de développement:
    user@debian:~/typescript_modules/typescript_es6% npm install && npm run build && npm start
    
  2. Ouvrir la page http://localhost:8080 dans un browser.
  3. Afficher la console de développement.

Le résultat de l’exécution est semblable à celui des exemples précédents.

Exemple avec la syntaxe AMD

Un exemple d’implémentation de modules générés au format AMD se trouve dans le dépôt Github suivant: typescript_modules/typescript_es6_amd. Cet exemple permet d’illustrer la génération des modules au format AMD.

Pour exécuter cet exemple, il faut installer les packages en exécutant les commandes:

  1. Il faut exécuter les instructions suivantes pour, respectivement, installer les packages npm, compiler et démarrer le serveur de développement:
    user@debian:~/typescript_modules/typescript_es6_amd% npm install && npm run build && npm start
    
  2. Ouvrir la page http://localhost:8080 dans un browser.
  3. Afficher la console de développement.

Le format AMD ajoute une fonction qui n’existe pas dans la syntaxe Javascript: define. Cette fonction comporte 2 paramètres:

  • Une tableau de dépendances: ce sont les fichiers nécessaires à l’exécution de la fonction.
  • Une fonction: cette fonction est le module à définir.

On peut voir un exemple d’utilisation de la fonction define en regardant le résultat de la compilation, par exemple dans le répertoire typescript_es6_amd/build/index.js:

define(["require", "exports", "./module1.js"], function (require, exports, module1_js_1) { 
    "use strict"; 
    Object.defineProperty(exports, "__esModule", { value: true }); 
    var Startup = /** @class */ (function () { 
        function Startup() { 
        } 

        Startup.main = function () { 
            module1_js_1.publicFunc(); 
            return 0; 
        }; 

        return Startup; 
    }()); 

    Startup.main(); 
}); 

Avec ce format, le loader utilisé est RequireJS. Il est inclus dans le code HTML de la page principale index.html:

<script data-main="app" src="node_modules/requirejs/require.js"></script> 

L’attribut data-main="app" correspond au point d’entrée de l’application qui est à un fichier Javascript app.js contenant la configuration de requireJS.

Le résultat de l’exécution est semblable à celui des exemples précédents.

Génération de code compatible avec ES5

Comme on a pu le voir pour l’exemple utilisant le format AMD, on a utilisé dans le code Typescript une syntaxe ES2015. Pourtant les modules ont été générés au format AMD qui compatible avec ES5. Il en est de même si on génère les modules en utilisant le format CommonJS.

Utilisation de “bundlers”

Les solutions présentées jusqu’içi ont un gros inconvénient: elles nécessitent d’inclure tous les fichiers Javascript générés dans le fichier HTML principal. Dans les exemples, on déclare les scripts Javascript correspondant aux différents modules de cette façon:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8">  
        <title>Test module Typescript ES6</title> 
    </head>    
    <body> 
        <script src="module3.js" ></script> 
        <script src="module2.js" ></script> 
        <script src="module1.js" ></script> 
        <script src="index.js" ></script>
    </body> 
</html> 

Ce type de déclaration peut être difficile à maintenir à l’échelle d’un grand projet.

Une solution pour palier à ce problème peut être d’utiliser des bundlers qui vont générer un fichier Javascript unique en incluant toutes les dépendances au moment de la compilation. Le code produit sera compatible ES5 et utilisable sur tous les browsers supportant ES5. Le gros inconvénient des bundlers est qu’ils vont générer un seul fichier contenant tout le code Javascript. Dans le cas d’une grosse application et si l’application comporte beaucoup de code Javascript, le temps de chargement du fichier bundle peut prendre du temps et ralentir le chargement de la page principale.

Les bundlers les plus connus sont Browserify ou webpack.

Exemple d’utilisation de webpack avec la syntaxe ES2015

Un exemple d’implémentation de modules en utilisant le bundler webpack se trouve dans le dépôt Github suivant: typescript_modules/webpack_es6. Cet exemple permet d’illustrer la génération d’un bundle contenant des modules implémentés au format ES2015.

Pour utiliser le bundler webpack, on a installé les packages webpack et ts-loader. Le package ts-loader permet à webpack de s’interfacer avec le compilateur Typescript. Pour exécuter webpack avec un serveur de développement, on a installé le package webpack-dev-server. Pour effectuer ces installations, on a exécuté la commande suivante:

npm install --save-dev webpack ts-loader webpack-dev-server 

Le fichier de configuration de webpack qui se trouve dans typescript_modules/webpack_es6/webpack.config.js permet d’indiquer qu’on souhaite utiliser le loader ts-loader et que le résultat de la compilation se fasse dans le fichier public/app.js. Le contenu de ce fichier est:

const path = require('path'); 

module.exports = { 
    entry: './index.ts', 
    module: { 
      rules: [ 
        { 
          use: 'ts-loader', 
          exclude: /node_modules/ 
        } 
      ] 
    }, 
    resolve: { 
      extensions: [ '.tsx', '.ts', '.js' ] 
    }, 
    output: { 
      filename: 'app.js', 
      path: path.resolve(__dirname, 'public') 
    } 
}; 

Pour lancer la compilation avec webpack en utilisant le fichier de configuration, on peut exécuter la commande suivante:

webpack ./webpack.config.js --mode development 

Enfin pour utiliser le bundle, il suffit de rajouter le script correspondant au bundle dans le fichier HTML principal:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8">  
        <title>Test module TS with webpack</title> 
    </head>
    <body>
        <script src="app.js" ></script>
    </body> 
</html> 

Pour lancer l’exécution avec le serveur de développement de webpack, on peut exécuter la commande suivante:

webpack-dev-server ./webpack.config.js --content-base ./public --mode development 

Plus simplement dans le cadre de cet exemple, on peut aussi:

  1. Exécuter les commandes suivantes qui effectuent toutes ces opérations:
    user@debian:~/typescript_modules/webpack_es6% npm install && npm run build && npm start
  2. Charger la page http://localhost:8080 sur un browser.
  3. Afficher la console de développement.

Le résultat de l’exécution est semblable à celui des exemples précédents.

Exemple d’utilisation de webpack avec la syntaxe “namespace”

Les différentes syntaxes d’implémentation de modules sont possibles avec webpack. On a montré précédemment un exemple avec le format ES2015. Il est facile de transposer cet exemple en utilisant le format namespace spécifique à Typescript.

Un exemple d’implémentation avec la syntaxe des namespaces se trouve dans le dépôt Github suivant: typescript_modules/webpack_es6_namespace. L’exemple est le même que précédemment, à la différence de l’utilisation de namespaces pour déclarer des modules (la syntaxe générale des namespaces a été explicitée plus haut).

Comment le compilateur parcourt les modules ?

Pour trouver les modules suivant les références faites avec la directive triple-slash ou avec import, le compilateur effectue une étape de résolution. Cette résolution des modules se fait en partie en fonction de la configuration. De nombreux éléments de configuration peuvent avoir un impact sur la façon de réaliser cette résolution.

D’une façon générale, le compilateur Typescript tsc inclue dans son étape de compilation tous les fichiers qui se trouvent dans le répertoire du fichier de configuration tsconfig.json.

Avant de compiler ces fichiers, le compilateur va résoudre les dépendances de modules en fonction des directives import et triple-slash.

Paramètres de compilation “files”, “include” et “exclude”

Le comportement du compilateur lors de la résolution de modules peut être modifié par l’utilisation de certains paramètres dans le fichier tsconfig.json:

  • "files": ce paramètre permet d’indiquer les chemins absolus ou relatifs des fichiers à compiler:
    { 
       "compilerOptions": {}, 
       "files": [ 
           "file1.ts", 
           "file2.ts", 
           "file3.ts"
       ] 
    }
    

    Quand ce paramètre est présent, le compilateur ne va pas parcourir tous les fichiers .ts/.tsx/.d.ts présents dans le répertoire du fichier de configuration tsconfig.json mais seulement les fichiers précisés et leur références.

  • "include": permet d’indiquer des fichiers en utilisant des wildcards:
    { 
       "compilerOptions": {}, 
       "include": [ 
           "src/**/*"
       ] 
    } 
    

    Les wildcards utilisables sont:

    • "*" pour indiquer un ensemble de caractères.
    • "?" pour indiquer un seul caractère.
    • "**/" pour parcourir récursivement tous les répertoires enfants.

    Dans l’exemple, le compilateur va donc parcourir récursivement tous les fichiers et répertoires se trouvant dans le répertoire src.

  • "exclude": permet d’exclure des fichiers se trouvant dans le parcours des fichiers .ts/.tsx/.d.ts du compilateur:
    { 
       "compilerOptions": {}, 
       "exclude": [ 
           "folder", 
           "file*.ts, 
           "**/innerFile*.ts"
       ] 
    } 
    

    Les wildcards indiquées plus haut sont aussi utilisables avec le paramètre "exclude".

Import relatif et non-relatif

Il faut distinguer 2 types d’import:

  • Les imports relatifs: ces imports se font de façon relatives au fichier dans lequel on veut effectuer l’import. Ainsi si on écrit dans le fichier fileA.ts:
    import * from './fileB.ts';
    

    La recherche de la dépendance se fera dans le même répertoire que le fichier fileA.ts

  • Les imports non-relatifs: ces imports ne se font pas en fonction de l’emplacement du fichier dans lequel on effectue l’import. Il se fait dans des répertoires définis suivant une stratégie de recherche. Cette stratégie dépend de la configuration.

    Pour ce type d’import, on indique directement le nom du module (sans indication de répertoire ou d’extension de fichier).

    Par exemple:

    import * from 'moduleName'; 
    

Stratégies de résolution des modules

Il existe 2 stratégies de résolution des modules:

  • "Classic": correspond à un parcours du répertoire de compilation et des répertoires parents.
  • "Node": cette stratégie imite la stratégie de résolution de Node.js avec les fichiers Javascript. Elle permet de parcourir le répertoire où se trouve le fichier .ts d’où se fait l’import ou le répertoire "node_modules".

On peut préciser la stratégie de résolution en ajoutant l’option --moduleResolution au lancement du compilateur:

tsc --moduleResolution "Node" 

Par défaut, la stratégie est classique si le type de module choisi dans tsconfig.json est "AMD", "System" ou "ES6". Si un autre type de module est choisi, la stratégie est "Node".

Stratégie classique: “Classic”

La stratégie classique consiste à parcourir le répertoire de compilation du fichier d’où se fait l’import en cherchant les fichiers avec une extension .ts ou .d.ts:

  • Dans le cas d’un import relatif: la recherche se fait seulement dans le répertoire cible de l’import. Par exemple si le fichier dans /folder/file1.ts effectue un import:
    import { module2 } from '../folder2/moduleName'; 
    

    Alors le parcours se fera en cherchant successivement les fichiers /folder2/moduleName.ts et /folder2/moduleName.d.ts.

  • Dans le cas d’un import non-relatif: la recherche se fait dans le répertoire du fichier d’où se fait l’import et dans les répertoires parents.

    Par exemple si le fichier dans /folder/file1.ts effectue un import:

    import { module2 } from 'moduleName'; 
    

    Alors le parcours se fera en cherchant successivement les fichiers: /folder/moduleName.ts, /folder/moduleName.d.ts, /folder/moduleName.ts et /folder/moduleName.d.ts.

Stratégie “Node”

Cette stratégie imite celle de Node.js pour la résolution des imports des fichiers Javascript:

  • Dans le cas d’un import relatif: le parcours se fait dans le répertoire du fichier d’où se fait l’import. Par exemple si le fichier dans /folder/file1.ts effectue un import:
    import { module2 } from '../folder2/moduleName'; 
    

    Alors le parcours se fera successivement en cherchant les fichiers:

    • /folder2/moduleName.ts, /folder2/moduleName.tsx ou /folder2/moduleName.d.ts
    • /folder2/moduleName/package.json (en utilisant la propriété "types" dans ce fichier)
    • /folder2/moduleName/index.ts, /folder2/moduleName/index.tsx ou /folder2/moduleName/index.d.ts.
  • Dans le cas d’un import non-relatif: le parcours se fait dans le répertoire node_modules (ce répertoire correspond au répertoire d’installation des packages NPM).

    Par exemple si le fichier dans /folder/file1.ts effectue un import:

    import { module2 } from 'moduleName'; 
    

    Alors le parcours se fera dans le répertoire node_modules se trouvant dans le répertoire du fichier en cherchant un fichier correspondant aux chemins suivants:

    • /folder/node_modules/moduleName.ts, /folder/node_modules/moduleName.tsx ou /folder/node_modules/moduleName.d.ts,
    • /folder/node_modules/moduleName/package.json,
    • /folder/node_modules/moduleName.index.ts, /folder/node_modules/moduleName.index.tsx ou /folder/node_modules/moduleName.index.d.ts.

    Le parcours se fera, ensuite, dans le répertoire node_modules se trouvant dans un répertoire parent:

    • /node_modules/moduleName.ts, /node_modules/moduleName.tsx ou /node_modules/moduleName.d.ts,
    • /node_modules/moduleName/package.json,
    • /node_modules/moduleName.index.ts, /node_modules/moduleName.index.tsx ou /node_modules/moduleName.index.d.ts.

Paramètres de compilation “baseUrl”, “paths” et “rootDirs”

Ces paramètres peuvent modifier les chemins utilisés lors de l’application de la stratégie de recherche des modules à importer.

“baseUrl”
Il permet d’indiquer un préfixe que le compilateur appliquera systématiquement aux imports non-relatifs. Ce paramètre peut être précisé à l’exécution du compilateur:

tsc --baseUrl "../moduleFolder/" 

Il peut aussi être précisé dans le fichier tsconfig.json:

{ 
   "compilerOptions": { 
       "baseUrl": "../moduleFolder/"
   } 
} 

“paths”
Ce paramètre permet de préciser un mapping entre des noms de module et leur chemin effectif. La valeur du paramètre "baseUrl" est rajouté aux chemins des mappings précisés dans "paths".

Par exemple pour la configuration suivante:

{ 
   "compilerOptions": { 
       "baseUrl": "../moduleFolder/", 
       "paths": { 
            "moduleName": [ "otherModules/moduleName" ] 
       }
   } 
} 

Si on effectue l’import non-relatif suivant:

import { module2 } from 'moduleName'; 

L’import sera fait dans le répertoire (par rapport à l’emplacement du fichier tsconfig.json):

../moduleFolder/otherModules/moduleName 

Il est possible d’utiliser plusieurs répertoires de mapping:

{ 
  "compilerOptions": { 
    "baseUrl": "../moduleFolder/", 
    "paths": { 
      "moduleName": [  
        "otherModuleFolder1/moduleName", 
        "otherModuleFolder2/moduleName"
      ] 
    } 
  } 
} 

L’import sera fait en cherchant dans les répertoires: ../moduleFolder/otherModuleFolder1/moduleName et ../moduleFolder/otherModuleFolder2/moduleName.

Il est aussi possible d’utiliser le caractère "*" pour introduire plus de flexiblité au moment de l’import. Par exemple si le fichier de configuration est:

{ 
  "compilerOptions": { 
    "baseUrl": "../moduleFolder/", 
    "paths": { 
      "*": [  
        "otherModuleFolder1/*", 
        "otherModuleFolder2/*"
      ] 
    } 
  } 
} 

Si on effectue l’import:

import 'folder/moduleName'; 

L’import sera fait en cherchant dans les répertoires: ../moduleFolder/otherModuleFolder1/folder/moduleName et ../moduleFolder/otherModuleFolder2/folder/moduleName.

“rootDirs”
Ce paramètre permet d’indiquer au compilateur des répertoires virtuels qui doivent être mergés après le déploiement de l’application.

Par exemple, s’il existe plusieurs répertoires indiqués dans le paramètre "rootDirs": "folder1", "folder2" et "folder3". A chaque fois qu’un import relatif est effectué à partir de l’un de ces répertoires, le compilateur va effectuer une recherche des modules dans tous les répertoires comme si leur contenu était mergé.

Si la configuration est:

{ 
  "compilerOptions": { 
    "rootDirs": [  
      "otherModuleFolder1", 
      "otherModuleFolder2"
    ] 
  } 
} 

Et si un import est effectué de cette façon:

import 'otherModuleFolder1/moduleName'; 

Le compilateur va effectuer la recherche dans les répertoires otherModuleFolder1/moduleName et otherModuleFolder2/moduleName.

Débugger la résolution de modules

En rajoutant l’option suivante au compilateur, on peut avoir des messages de logs lors de la résolution des modules:

tsc --traceResolution 

Pour conclure…

Comme en Javascript, les modules en Typescript peuvent être implémentés de façon très différente. Il est possible de passer par les namespaces qui correspondent à une séparation logique du code et qui sont spécifiques à Typescript. On peut aussi utiliser des syntaxes plus proches du Javascript avec des formats comme CommonJS, AMD ou ES2015. L’intérêt d’utiliser une syntaxe plus proche de celle en Javascript permet de coller davantage à la notion des modules Javascript séparant le code en fichiers.
D’une façon générale, il est conseillé de privilégier la notion des modules Javascript par rapport aux namespaces. En effet, les namespaces ajoutent une couche hiérarchique aux objets dans le but d’éviter les collisions de nom. A l’opposé les modules Javascript, du fait de leur séparation en fichiers, évitent les collisions de nom sans ajouter de couche hiérarchique.
Comme on a pu le voir, la définition de modules en Typescript hérite de la syntaxe Javascript et ajoute la notion de namespace. En revanche la notion d’import de modules externes n’a pas été abordé dans cet article. Ce point fera l’objet d’un article ultérieur.

Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Téléchargement de composants .NET hors-ligne

Le plus souvent, les fichiers d’installation des composants .NET impliquent d’être connecté à internet au moment de l’installation. Par exemple, c’est le cas pour Visual Studio Community ou les Build tools. Parfois, on peut ne pas disposer d’une connexion au moment de l’installation, en particulier si on effectue l’installation sur un serveur qui se trouve sur un réseau fermé.

Ces fichiers d’installation peuvent être exécutés hors-ligne toutefois ils nécessitent une manipulation au préalable.

Préparation du fichier d’installation

Pour utiliser les fichiers d’installation hors-ligne, il faut, d’abord, télécharger toutes les dépendances du fichier en exécutant la ligne:

<fichier d'installation> --layout <répertoire de sortie> --lang <code langue> 

Par exemple, dans le cadre des Build Tools, pour télécharger tous les fichiers d’installation, il faut exécuter la ligne suivante:

vs_buildtools.exe --layout C:\OfflineBuildTools --lang en-US 

D’autres langues sont possibles:

  • pour le français, il faut utiliser le code fr-FR.
  • D’autres codes de langues sont disponibles sur la page suivante: List of language locales.

Le gros inconvénient de cette méthode est qu’elle entraîne le téléchargement de toutes les dépendances. Ce qui représente 12GB de fichiers d’installation dans le cas des Build Tools.

Pour éviter de tout télécharger, il est possible d’indiquer les composants à télécharger en les indiquant en utilisant l’option --add <nom du composant>.

Par exemple:

<fichier d'installation> --layout <répertoire de sortie> --lang <code langue> 
    --add <composant 1> 
    --add <composant 2>

Une liste exhaustive des composants par fichier d’installation se trouve sur la page suivante:
Visual Studio 2017 workload and component IDs

On peut ensuite ajouter des options pour télécharger certaines dépendances des composants comme:

  • --includeRecommended pour télécharger les composants conseillés
  • --includeOptional pour télécharger les composants optionels.
Téléchargement interrompu

Si jamais le téléchargement s’interrompt, il suffit de ré-exécuter la commande avec les mêmes arguments. Le téléchargement reprendra où il s’est interrompu. Les fichiers déjà téléchargés ne seront, alors, pas re-téléchargés.

Exécuter l’installateur hors-ligne

Après avoir copié les fichiers d’installation sur la machine cible, il faut d’abord installer les certificats:

  1. Aller dans le répertoire téléchargé:
    <répertoire des fichiers d'installation>\certificates
  2. Double-cliquer sur chaque fichier de certificat (fichiers avec une extension .p12).
    Dans le cas où l’assistant demande un mot de passe, il faut le laisser vide.
  3. Ensuite, on peut exécuter l’installateur qui se trouve dans le répertoire téléchargé.
    Dans le cas des Build Tools, le fichier est vs_buildtools.exe.

Si tous les composants ont été correctement téléchargés, une connexion internet n’est pas requise.

Dans le cas où on souhaite modifier, réparer, mettre à jour ou désinstaller une installation existante, on peut exécuter l’exécutable dans le répertoire téléchargé en double-cliquant dessus ou exécuter à la ligne de commandes une instruction du type:

<répertoire des fichiers d'installation>\<fichier d'installation> <commande>

Par exemple, pour les Build Tools:

vs_buildtools.exe <commande> 

Les commandes possibles sont:

  • modify pour modifier une installation existante.
  • update pour mettre à jour une composant déjà installé
  • repair pour réparer une installation existante
  • uninstall pour désinstaller.
Erreur quand on modifie une installation existante

Si un erreur survient au cours de l’exécution d’une commande, il faut essayer de désinstaller complétement le composant en allant dans le panneau de configuration. Puis le réinstaller sans utiliser de commande particulière.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Les modules en Javascript en 5 min

A l’origine Javascript était utilisé en tant que langage de script pour des pages web. Au fil du temps, ce langage est devenu le langage de base de nombreuses applications web. Jusqu’à très récemment il n’existait pas de concept de module en Javascript à proprement parlé. Par exemple, il était impossible de référencer directement un script Javascript à partir d’un autre. Dans un premier temps, cette limitation a eu pour conséquence de devoir stocker le code Javascript dans un même fichier, ce qui est possible pour de simples scripts mais peut s’avérer très vite compliqué pour des applications de grandes tailles.

Découper du code en plusieurs parties apportent de nombreux avantages par rapport à un code monobloc:

  • Le premier avantage est de rendre le code plus lisible puisqu’on peut le séparer, par exemple, en fonctionnalités.
  • Le code séparé en modules facilite sa réutilisation dans le reste de l’application mais aussi dans d’autres applications.
  • La gestion des dépendances entre modules est simplifiée: chaque module indique clairement ses dépendances. En cas de problème ou de dépendances cassés, on peut identifier plus facilement le ou les modules qui font défaut.
  • Les modules rendent le code plus facilement extensible car on peut plus aisément rajouter un ou plusieurs modules et les utiliser.
  • Enfin, les modules favorisent l’encapsulation pour éviter d’exposer une complexité qui n’est pas nécessaire.

Même si la notion de module n’existait pas nativement en Javascript, il a fallu trouvé un moyen de moduler le code pour le rendre plus facilement lisible. Plusieurs outils sont apparus pour étendre le Javascript et permettre une implémentation facile des modules.

Cet article a pour but de montrer quelques solutions pour implémenter des modules en Javascript:

  • En s’aidant d’outils externes pour les versions de Javascript antérieures à EcmaScript 2015.
  • En profitant de la gestion native des modules à partir d’EcmaScript 2015 (i.e. ES2015 ou ES6).

Dans premier temps, on va présenter quelques caractéristiques historiques du langage Javascript qui justifient une syntaxe particulière pour l’implémentation des modules. On va, ensuite, indiquer des solutions pour implémenter des modules avant ES2015. Pour terminer, on va expliciter la syntaxe désormais native, des modules avec ES2015.

Quelques caractéristiques de la syntaxe Javascript

En préambule, on peut expliquer quelques caractéristiques de la syntaxe Javascript qui ont permis l’implémentation du pattern module avant ES2015.

Portée de variables

La portée des variables est l’élément le plus important pour motiver le découpage en module du code en Javascript. Ainsi les variables déclarées sans le mot-clé var ont une portée globale c’est-à-dire que leur valeur persiste tout au long de la durée de vie de l’application jusqu’à leur prochaine affectation.

Cette portée globale peut entraîner plus facilement des bugs:

  • Si on modifie la valeur d’une variable dans un endroit du code, elle sera persistée et pourra être utilisée à d’autres endroits non identifiés.
  • A l’inverse, la valeur d’une variable peut être modifiée par une partie du code qu’on ne maitrise pas, ce qui peut entrainer une valeur différente de celle attendue.

Pour ces raisons:

  • On déclare les variables pour une portée limitée pour en maitriser la valeur.
  • On déclare ces variables dans le corps d’une fonction avec le mot-clé var pour limiter la portée au corps de la fonction.

Par exemple, dans le code suivant, la portée de la variable globalVar est globale (car il n’y a pas le mot clé var):

function customFunction() { 
    globalVar = 1; // la portée est globale 

    var privateVar = 2; // la portée est locale 
}

A l’inverse, la portée de privateVar est locale à la fonction.

Fonction dans une fonction

Il est possible de déclarer des fonctions dans le corps d’une fonction (i.e. closures). Cette caractéristique sert pour la définition du pattern module plus loin.

Par exemple:

function customFunc1() { 
    function customFunc2() { 
         // ...
    }; 
} 

La fonction customFunc2() est encapsulée dans la fonction customFunc1().

Fonctions anonymes

Les fonctions anonymes ou expressions lambda correspondent à la même notion qu’en C#: on affecte une fonction à une variable. L’intérêt est de pouvoir passer cette fonction en paramètre d’une autre fonction par l’intermédiaire de cette variable.

Par exemple, une fonction anonyme se déclare de cette façon:

var anonymousFunc = function() { 
    // ... 
}; 

anonymousFunc est la variable contenant la fonction. Il faut penser à utiliser le caractère “;” à la fin de la déclaration. On peut appeler cette fonction en utilisant directement la variable:

anonymousFunc();  

L’existence de la fonction est liée à la portée de la variable anonymousFunc. Si cette variable est hors de portée, elle peut être supprimée par le garbage collector.

Le “pattern” module

Ce pattern utilise les notions abordées précédemment pour définir une unité de code avec des variables dont la portée est locale à cette unité. On peut encapsuler une partie du code ou au contraire choisir d’exposer d’autres parties ce qui permet de d’implémenter la notion de module sans qu’elle soit nativement et explicitement prévue par le langage.

Pour implémenter cette notion de module:

  • On encapsule une fonction dans une variable,
  • On définit une variable dans le corps de la fonction et on retourne cette variable en retour de la fonction pour en exporter la valeur.

Par exemple:

var Module = (function() { 
    var self ={}; // On définit une variable vide 

    self.publicVariable = 1; // On ajoute des attributs à cette variable 

    var privateVar = 1; // variable privée 

    // On peut affecter une fonction anonyme 
    self.publicMethod = function() { 
        // ...
    }; 

    return self;  // On exporte la variable pour l'utiliser à l'extérieur par l'intermédiaire de la variable Module 
})(); 

L’utilisation des éléments de syntaxe var Module = (function() { ... })(); est importante car elle permet d’indiquer une fonction qui sera exécutée tout de suite c’est-à-dire une IIFE (pour Immediatly Invoked Function Expression). L’IIFE est exécutée en même temps que le code est parsé donc les déclarations dans le module sont exécutées et dans notre cas, les différentes variables sont définies.

Dans le corps du module, la variable self est la plus importante car elle est retournée à la fin de l’exécution. Ainsi les éléments déclarés avec la syntaxe self.element peuvent être accessibles à l’extérieur du module en utilisant la syntaxe Module.element.

A l’opposé, les éléments déclarés dans le corps du module avec une syntaxe du type suivant, ne sont pas accessibles à l’extérieur:

var privateVar = 1;  

Les variables déclarées dans le corps du module ne sont accessibles qu’aux fonctions du module.

Par exemple, si on écrit:

var Module = (function() { 
    var self = {}; 

    function privateFunc() { 
        // ... 
    }; 

    self.publicFunc = function() { 
        privateFunc(); 
    }; 

    return self; 
})(); 

privateFunc() est une fonction privée accessible seulement dans le corps du module. A l’opposé, self.publicFunc est accessible à l’extérieur en utilisant la syntaxe Module.publicFunc().

Le “pattern” Constructor

Il est possible de définir des modules sans les exécuter en utilisant le pattern module et l’utilisation des IIFE comme on l’a vu précédemment.

Un autre type de déclaration permet d’instancier un module de cette façon:

var moduleInstance = new Module(); 

Pour instancier un module de cette façon, sa déclaration est du type:

var Module = function() { 
    var message = "message1"; // élément de construction 

    function printMessage() { 
        console.log(message); 
    } 

    // On utilise ce type de déclaration pour exporter la fonction printMessage. 
    return { 
        printMessage: printMessage 
    } 
}; 

On peut remarquer qu’on n’utilise pas la syntaxe correspondant aux IIFE.

Pour appeler le module avec ce type de déclaration, il faut utiliser la syntaxe:

moduleInstance.printMessage(); 

Déclaration de plusieurs modules

Il est possible d’utiliser un module dans le corps d’un autre module à condition d’avoir définit les modules dans le bon ordre. Ainsi, pour pouvoir utiliser un module, il faut l’avoir défini au préalable.

Par exemple:

var Module1 = (function() { 
    var self = {}; 

    self.publicFunc = function() { … }; 
    return self; 
})(); 

var Module2 = (function() { 
    var self = {}; 

    self.publicFunc = function() { 
        Module1.publicFunc(); 
    }; 

    return self; 
})(); 

Dans le cas de l’exemple, il faut que le module nommé module1 soit définit avant le module nommé module2.

Sous-modules

On peut imbriquer des modules l’un dans l’autre. D’abord, on définit un module parent, ensuite on peut définir un module enfant en ajoutant un attribut au module parent.

Par exemple:

var ModuleParent = (function() { 
    // ... 
})(); 

ModuleParent.SousModule = (function() { 
    // ...
})(); 

Il faut noter quelques règles concernant ces sous-modules:

  • Les sous-modules ne peuvent accéder qu’aux éléments publiques du module parent.
  • Les éléments privés ne sont accessibles que dans le corps du module.

Module parent

On utilise souvent une variable globale qui contient tous les modules pour éviter de définir des modules et de les écraser par la suite (si on le déclare plusieurs fois). On définit, alors, une variable contenant tous les modules.

Par exemple, si on appelle cette variable Application:

var Application = Application || {}; 

L’écriture précédente signifie qu’on définit une variable vide si elle n’existe pas sinon on utilise la variable existante.

Ensuite on définit les modules en utilisant des attributs de la variable Application:

Application.Module1 = (function(){ 
    // ... 
}){}; 

On peut définir un autre module de cette façon:

Application.Module2 = (function(){ 
    // ... 
}){}; 

Enfin, pour éviter d’écraser des modules s’ils ont déjà été définis, on utilise la notation correspondant à l’extension de module:

Application.Module1 = (function(self){ 
    // ...  
    // Pas de déclaration de self car on l'utilise en argument. 
})(Application.Module1 || {}); 

La notation Application.Module1 || {} signifie que s’il n’existe pas de module nommé Module1 alors on crée une variable vide sinon on utilise l’instance existante.

Implémenter des modules avant ES2015

Comme indiqué en préambule, Javascript ne proposait pas de gestion native des modules avant ES2015. Avant cette version, l’implémentation de modules a donc nécessité de tirer partie de la syntaxe existante de Javascript. Ainsi, le pattern module décrit dans la partie précédente, permet d’implémenter des modules avant ES2015. Le gros inconvénient de ce pattern est qu’il nécessite de déclarer tous les modules dans le même fichier Javascript.

On peut trouver un moyen de charger plusieurs fichiers de scripts Javascript en indiquant ces scripts directement dans l’en-tête du fichier HTML principal en utilisant des balises <scripts>, par exemple:

<script type="text/javascript" src="script1.js" /> 
<script type="text/javascript" src="script2.js" /> 
<script type="text/javascript" src="script3.js" /> 
<script type="text/javascript" src="script4.js" /> 

On peut déclarer les différents scripts en omettant l’attribut type="text/javascript" qui est facultatif.

Les modules déclarés en utilisant cette méthode ont de gros inconvénients:

  • Le code est moins lisible puisqu’il faut indiquer tous les scripts au préalable dans le fichier principal.
  • On ne peut pas charger de fichiers de façon asynchrone, tous les fichiers Javascript doivent être chargés au chargement de la page.
  • Il faut qu’ils soient déclarés dans le bon ordre. Si une dépendance n’est pas satisfaite au moment où on souhaite l’utiliser, le code ne s’exécutera pas correctement.
  • Suivant la façon avec laquelle les modules sont déclarés, ils peuvent être écrasés si, par mégarde, on les définit plusieurs fois.

Un exemple d’implémentation de modules en ES5 se trouve dans le dépôt GitHub suivant: es5-browser. Cet exemple permet d’illustrer des appels d’un module à l’autre:

  • le module 1 appelle le module 2 et
  • le module 2 appelle le module 3.

Dans cet exemple:

  • On déclare tous les scripts dans le fichier index.html.
  • Les modules sont définis dans les fichiers module1.js, module2.js et module3.js.
  • La fonction permettant de démarrer l’exécution se trouve dans le fichier app.js.
  • JQuery est seulement utilisé pour lancer l’exécution dans app.js au chargement de la page HTML principale.

Pour exécuter cet exemple, il faut simplement ouvrir le fichier index.html avec un browser et afficher la console de développement.

Affichage de la console de développement dans un browser

Pour tous les exemples provenant du dépôt GitHub https://github.com/msoft/javascript_modules, pour voir les résultats d’exécution, il faut afficher la console de développement:

  • Sous Firefox: on peut utiliser la raccourci [Ctrl] + [Maj] + [J] (sous MacOS: [⌘] + [Maj] + [J], sous Linux: [Ctrl] + [Maj] + [K]) ou en allant dans le menu “Développement web” ⇒ “Console du navigateur”.
  • Sous Chrome: utiliser le raccourci [F12] (sous MacOS: [⌥] + [⌘] + [I], sous Linux: [Ctrl] + [Maj] + [I]) puis cliquer sur l’onglet “Console”. A partir du menu, il faut aller dans “Plus d’outils” ⇒ “Outils de développement”.
  • Sous EDGE: utiliser le raccourci [F12] puis naviguer jusqu’à l’onglet “Console”.

L’exemple précédent affichera dans la console de développement un résultat du type:

Executed from module3.SubModule.init()
Executed from module3.privateFunc()
Executed from module3.init()
Executed from module2.publicFuncModule2()
Executed from module1.privateFunc()
Executed from module1.publicFuncModule1()

Utiliser des “loaders”

Pour éviter tous les inconvénients liés à la déclaration préalable des scripts contenant les modules dans l’entête de la page HTML principale, on peut passer par l’intermédiaire de loaders qui vont apporter une flexibilité supplémentaire en enrichissant la syntaxe Javascript. Pour faciliter le chargement des modules, loaders se basent sur une syntaxe particulière correspondant à un “format” pour savoir comment charger les modules:

  • Format: correspond à la syntaxe utilisée pour indiquer des modules dans le code Javascript. Cette syntaxe est interprétée par le loader.
  • Loader: il aide à interpréter un format de module spécifique.

Du code Javascript peut être exécuté sur un browser directement ou à partir d’un serveur Node.js. Suivant la façon d’exécuter le code, on peut être amené à utiliser un format particulier et un loader spécifique.

Les formats les plus connus sont CommonJS, AMD (i.e. Asynchronous Module Definition) et UMD (i.e. Universal Module Definition). Les loaders les plus connus sont RequireJS (utlisant le format AMD), SystemJS (qui permet de gérer plusieurs formats).

Utiliser des “bundlers” ou des “transpilers”

En plus des loaders, il est possible d’exécuter des outils pour compiler du code Javascript pour générer un code Javascript capable d’être exécuté dans des environnement différents et plus hétérogènes.

Ces outils peuvent être des bundlers qui vont générer un fichier Javascript unique en incluant toutes les dépendances au moment de la compilation. Le code produit sera compatible ES5 et utilisable sur tous les browsers supportant ES5. Le gros inconvénient des bundlers est qu’ils vont générer un seul fichier contenant tout le code Javascript. Dans le cas d’une grosse application et si l’application comporte beaucoup de code Javascript, le temps de chargement du fichier bundle peut prendre du temps et ralentir le chargement de la page principale.

Les bundlers les plus connus sont Browserify ou webpack.

Une autre solution est de passer par des transpilers qui vont, de même, générer un code Javascript compatible avec ES5 après une étape de compilation. Les transpilers proposent une syntaxe plus étendues que celles des bundlers qui sont plus spécifiques à la gestion des modules.

Les transpilers le plus connus sont Babel, Typescript et CoffeeScript.

Pour utiliser le bundler Browserify, il faut:

  1. Installer Browserify avec npm:
    npm install --save-dev browserify
    
  2. L’exécuter en utilisant le module principal et en dirigant le résultat de la génération dans un répertoire de sortie. Par exemple, en exécutant:
    node_modules/.bin/browserify js/app.js --outfile build/bundle.js 
    

    Le résultat de la génération se trouve dans le fichier build/bundle.js

  3. Pour charger le fichier bundle final à partir de la page principale, il faut ajouter la ligne suivante dans le fichier HTML principal:
    <script src = "./build/bundle.js"></script> 
    

Pour utiliser le transpiler webpack et Babel, on peut réaliser les étapes suivantes:

  1. Installer webpack et Babel avec npm en exécutant:
    npm install --save-dev babel-loader babel-core babel-preset-env webpack
    
  2. Configurer le fichier de configuration de webpack webpack.config.js avec les paramètres suivants:
    var path = require('path'); 
    var webpack = require('webpack');
    
    module.exports = { 
       entry: './src/index.js', // point d'entrée de l'application
       module: { 
        rules: [
          {
              test: /\.js$/,
              loader: 'babel-loader',     // On utilise le loader Babel
              query: {
                  presets: ['es2015']   // Paramétrage de Babel
              }
          }
         ]
       }, 
       resolve: { 
         extensions: [ '.tsx', '.ts', '.js' ] 
       }, 
       output: { 
         filename: 'app.js',      // Fichier de sortie
         path: path.resolve(__dirname, 'public')  // Répertoire de sortie
       } 
    };
    
  3. Ajouter la ligne suivante dans le fichier HTML principal:
    <script type="text/javascript" src="public/app.js"></script> 
    

Un exemple plus complet se trouve dans le dépôt GitHub: es6_webpack_babel. Cet exemple permet d’illustrer l’utilisation de webpack et du transpiler Babel pour générer un bundle compatible ES5 à partir du code Javascript ES6/ES2015. Cet exemple permet d’effectuer des appels d’un module à l’autre.

Pour exécuter cet exemple, il faut:

  1. Télécharger les packages npm en exécutant la ligne: npm install dans le répertoire de l’exemple,
  2. Exécuter le commande npm run build pour que webpack génère le bundle,
  3. Lancer le serveur web de développement en exécutant la commande npm run start
  4. Se connecter à l’adresse http://127.0.0.1:8080 pour voir le résultat de l’exécution après avoir ouvert la console de développement.

Le résultat est très similaire à celui obtenu avec les modules ES5.

Synthèse des possibilités

Suivant le cas d’utilisation et la façon d’exécuter le code Javascript, on peut utiliser un format particulier ce qui implique ensuite un bundler et/ou un transpiler particulier.

On peut synthétiser les différentes possibilités dans le tableau suivant:

Format CommonJS AMD UMD
Environnement d’exécution Principalement sur un serveur Node.js mais il est possible de l’exécuter directement sur un browser avec des bundlers. Browser Serveur et browser
Outils utilisables
  • Avec Node.js, il n’est pas nécessaire d’utiliser un loader, CommonJS est géré nativement.
  • Directement sur un browser, on peut utiliser Browserify ou webpack pour générer un bundle.
  • SystemJS en tant que loader de modules
  • RequireJS,
  • SystemJS
  • Avec Node.js, on peut générer des modules compatibles avec CommonJS.
  • Sur un browser, on peut générer des modules AMD et utiliser RequireJS ou SystemJS.
  • On peut aussi générer un bundle avec webpack.
Chargement asynchrone Non Oui Oui
Relation avec le système de fichiers 1 – 1 N – N N – N

Exemples de syntaxe

AMD + RequireJS

AMD est le format utilisé par le loader RequireJS. Il rajoute une fonction qui n’existe pas dans la syntaxe Javascript: define. Cette fonction comporte 2 paramètres:

  • Une tableau de dépendances: ce sont les fichiers nécessaires à l’exécution de la fonction.
  • Une fonction: cette fonction est le module à définir.

La fonction define est interprétée par le loader, ce n’est pas une fonction Javascript.

Par exemple:

define([], function() {  
    function func1() {
         // ...
    }

    function func2() { 
         // ...
    }

    return { 
        func1: func1, 
        func2: func2 
    } 
}); 

define(['./module1', './module2'], 
    function(module1, module2) { 
        // ...
    }); 

Les modules sont injectés dans la fonction en paramètres. module1 et module2 indiquent les fichiers Javascript sans l’extension .js.

Pour utiliser le loader RequireJS:

  1. Il faut l’installer avec npm en exécutant:
    npm install --save requirejs 
    
  2. Il faut l’inclure dans le code HTML de la page principale:
    <script data-main="app" src="node_modules/requirejs/require.js"></script> 
    

    L’attribut data-main="app" correspond au point d’entrée de l’application qui est à un fichier Javascript contenant la configuration de requireJS.

Un exemple de cette implémentation se trouve dans le dépôt GitHub suivant: amd_requirejs-browser. Cet exemple permet d’illustrer l’utilisation d’AMD et du loader RequireJS pour permettre d’effectuer des appels d’un module à l’autre. Cet exemple est exécutable sur un browser.

Pour exécuter cet exemple, il faut:

  1. Exécuter la commande npm install dans le répertoire de l’exemple,
  2. Ouvrir le fichier index.html directement avec un browser après avoir ouvert la console de développement.

Le résultat dans la console de développement est:

Executed from main
main: calling module1 
Executed from module1Func()
main: calling module2
Executed from module2Func()
Executed from module1Func()
Executed from module3Func()

CommonJS + SystemJS

CommonJS est le format utilisé par le loader SystemJS et nativement par Node.js. En utilisant le format CommonJS, la définition est différente de celle d’AMD, on déclare les fonctions et on les ajoute aux modules en tant qu’attribut.

Par exemple:

function func1() { 
    // ... 
} 

function func2() { 
    // ... 
} 

Module.exports = { 
    func1: func1, 
    func2: func2 
}; 

Pour inclure des modules provenant d’un autre fichier, on utilise la fonction require.

Par exemple:

var moduleInFile = require('./file.js'); 

Avec cette déclaration, moduleInFile est une variable locale utilisable directement:

moduleInFile.func1(); 

Pour utiliser le loader systemJS:

  1. Il faut l’installer avec npm en exécutant la commande:
    npm install --save systemjs 
    
  2. Il faut inclure dans le code HTML de la page principale la référence suivante:
    <script src="node_modules/systemjs/dist/system.js"></script> 
    <script  
          // Le code suivant correspond à de la configuration 
          System.config({ 
               meta: {    
                   format: 'cjs'  // cjs correspond à la configuration pour le format commonjs 
               } 
         }); 
    
         System.import('js/module1.js')
           .then(function(module1) {
               module1.publicFuncModule1();
           }); // import de systemjs 
    </script> 
    

System.import permet d’indiquer au loader le module root. Il ne doit pas être une dépendance d’une autre module.

Un exemple de cette implémentation se trouve dans le dépôt GitHub suivant: commonjs_systemjs-node. Cet exemple permet d’illustrer l’utilisation de CommonJS avec le loader SystemJS exécuté sur un serveur Node.js. L’exemple montre des appels d’un module à l’autre.

Pour exécuter cet exemple, il faut:

  1. Exécuter la commande npm install dans le répertoire de l’exemple,
  2. Lancer le serveur web de développement en exécutant la commande npm run start
  3. Se connecter à l’adresse http://127.0.0.1:8080 pour voir le résultat de l’exécution après avoir ouvert la console de développement.

Les modules à partir de ES2015/ES6

La norme ES2015/ES6 ajoute des fonctionnalités par rapport à ES5 comme let, var, const et les modules. Toutefois ES2015 n’est pas compatible avec tous les browsers. Si on implémente une application en ES2015, elle pourrait ne pas s’exécuter sur des browsers non compatibles. Pour palier à ce problème, on peut utiliser des transpilers qui peuvent convertir le code ES2015 en ES5 de façon à ce qu’il soit compatible avec la grande majorité des browsers. On code ainsi, en bénéficiant de la syntaxe ES2015 et le code exécuté est compatible ES5.

Les modules ES2015 sont supportés nativement par:

  • Chrome à partir de la version 63.
  • Firefox à partir de la version 60,
  • Edge à partir de la version 16,
  • Safari à partir de la version 11,
  • Node.js à partir de la version 10.

Pour plus d’informations sur les compatibilités des browsers:

Il existe plusieurs façons d’exécuter du code Javascript comportant des modules ES2015:

  • Exécuter le code Javascript sur un serveur Node.js: à partir de la version 10, Node.js gère nativement le chargement de modules au format ES2015.
  • Il est aussi possible d’associer un loader comme RequireJS ou SystemJS qui va charger lui-même les modules.
  • On peut générer des bundles compatibles ES5 en utilisant Browserify ou webpack.

Export et import de modules

Les propriétés les plus importantes à avoir en tête avec les modules ES2015 sont les suivantes:

  • Par défaut, tous les modules ES2015 sont privés.
  • Un fichier Javascript correspond à un module.
  • Pour exposer des éléments comme des variables, des fonctions ou des classes à l’extérieur d’un module, il faut utiliser le mot-clé export.
Mode strict en ES5

Il est possible d’utiliser le mode strict (i.e. strict mode en ES5). Ce mode permet de ne pas déclarer des variables par inadvertance car il faut obligatoirement déclarer une nouvelle variable en utilisant var. Si on se trompe en effectuant une affectation de variable et en se trompant dans le nom de la variable, il y aura une erreur de façon à éviter qu’une variable avec un mauvais nom ne soit déclarée au niveau global et ainsi, affecter correctement une valeur à la bonne variable.

Pour utiliser le mode strict, il faut l’indiquer dans les premières lignes d’un script Javascript, par exemple:

"use strict"; 

var newValue = 3.14;

Exporter un module

L’export d’éléments permet de les exposer à l’extérieur du module de façon à les rendre accessible à l’extérieur du module.

Pour exporter, on utilise le mot-clé export.

Par exemple:

export function func1() {
    // ... 
}

Dans cet exemple, on exporte une fonction à l’extérieur du module.

D’autres formes de syntaxe sont possibles pour exporter plusieurs éléments du module en une fois:

function func1() {
    // ... 
} 

function func2() { 
    // ... 
} 

export { func1, func2 };

On peut aussi indiquer l’instruction export avant la déclaration des éléments à exporter:

export { func1, func2 };

function func1() { 
    // ... 
} 

function func2() { 
    // ... 
} 

Importer un module

L’import d’un module correspond à importer les éléments exportés par ce module dans un autre.

Pour importer tous les éléments exportés d’un module dans une variable, on peut utiliser la syntaxe:

import * as moduleInFile from './file.js'; 

Dans ce cas, la variable est moduleInFile, on peut l’utiliser directement pour accéder aux éléments du module, par exemple:

moduleInFile.func1();

On peut importer un élément spécifique et non pas tous les éléments exportés du module en précisant les éléments à importer à partir de leur nom, par exemple:

import { func1 as funcFromOuterModule, func2 } from './file.js'; 

func1 as funcFromOuterModule permet de renommer le nom de l’élément importé. Cette déclaration est facultative, ainsi elle n’est pas utilisée pour func2.

Dans ce cas, on peut appeler directement les fonctions:

func2(); 
funcFromOuterModule(); 

Export par défaut

L’export d’un élément par défaut permet d’indiquer l’élément qui sera importé dans un module si ce dernier ne précise pas ce qui doit être importé. Ainsi, au moment d’importer l’élément d’un module, il ne sera pas nécessaire de préciser le nom de l’élément à importer, l’élément par défaut sera le seul élément importé même si le fichier comporte d’autres éléments.

Par exemple:

function func1() {
    // ...
} 

function func2() { 
    // ... 
} 

export default func1;

Au moment d’importer, le seul élément importé sera l’élément exporté par défaut:

import func1 from "file"; // L'import se fait à partir du fichier file.js.  

L’import ne comporte pas d’accolades. C’est l’élément par défaut qui est importé.

L’élément importé par défaut peut être utilisable directement:

func1(); 

Export nommé

On peut nommer un export de façon à modifier le nom de l’élément qui est exporté.

Par exemple, dans un premier temps on exporte un élément en le renommant:

function func1() {
    // ... 
} 

function func2() { 
    // ... 
} 

export default func1;  
export var finalFunc = func2;

En plus de l’élément par défaut, on décide de renommer un élément exporté.

A l’import, il faut indiquer le nouveau nom de l’élément:

import { finalFunc } from "file"; 

On peut directement utilisé le nouveau nom de l’élément:

finalFunc();

Dans cet exemple, si on souhaite importer l’élément par défaut, on peut écrire:

import func1, { finalFunc } from "file"; 

Utilisation des modules ES2015

Dans un browser

Pour utiliser des modules ES2015 directement dans un browser, en plus du support de ces modules suivant la version du browser, il faut inclure les fichiers Javascript en utilisant la syntaxe suivante dans l’entête du fichier HTML principal:

<script type="module" src="./file1.js" /> 

Ou plus directement:

<script type="module"> 
import { elementToImport } from './elementModule.js'; 
// ... 
</script> 

Les browsers qui ne sont pas compatibles ES2015 ne vont pas charger les scripts déclarés avec l’attribut type="module".

Un exemple plus complet se trouve dans le dépôt Github: es6-browser. Cet exemple permet d’illustrer l’utilisation de la syntaxe ES2015 pour effectuer des appels d’un module à l’autre. Cet exemple exécutable à partir d’un browser, toutefois on utilise Node.js pour éviter l’erreur correspondant au chargement de scripts à partir d’un autre domaine (i.e. erreur Cross-origin resource sharing (CORS)).

Pour exécuter cet exemple, il faut:

  1. Télécharger les packages npm en exécutant la ligne: npm install dans le répertoire de l’exemple,
  2. Lancer le serveur web de développement en exécutant la commande npm run start
  3. Se connecter à l’adresse http://127.0.0.1:8080 pour voir le résultat de l’exécution après avoir ouvert la console de développement.

Le résultat de l’exécution est:

Executed from module1.privateFunc()
Executed from module1.publicFuncModule1()
Executed from module2.publicFuncModule2()
Executed from module3.privateFunc()
Executed from module3.publicFuncModule3()

Ordre d’exécution des modules

Suivant la façon de déclarer les modules dans une balise <script>, l’ordre d’exécution est différent:

  1. Balise <script> avec l’attribut src: par exemple:
    <script src="file.js" />
    
  2. Module déclaré dans le corps d’une balise <script> de façon “inline”:
    <script type="module"> 
    // Corps du module 
    </script> 
    
  3. Utilisation de l’attribut defer pour retarder l’exécution du script jusqu’à ce que le document soit entièrement chargé et parsé:
    <script defer src="file.js" /> 
    
  4. Les modules déclarés avec les attributs src et type sont exécutés en dernier:
    <script type="module" src="file.js" /> 
    

Attribut “nomodule”

Suivant les besoins, il peut être nécessaire d’avoir des scripts différents pour les browser qui gèrent les modules ES2015 et pour les browsers qui ne les gèrent pas. La solution est d’utiliser l’attribut nomodule au moment de déclarer le script. Ainsi:

  • Pour déclarer les scripts destinés aux browsers ne gérant pas les modules ES2015: on utilise une balise <script> sans l’indication du type module mais avec l’attribut nomodule. Les browsers gérant les modules n’exécuteront pas ces scripts:
    <script nomodule src="runs_if_module_not_supported.js" /> 
    
  • Pour déclarer les scripts destinés aux browsers gérant les modules ES2015, il suffit d’utiliser l’attribut indiquant le type:

    <script type="module" src="runs_if_module_supported.js" /> 
    

    Les browsers incompatibles avec ES2015 ne chargeront pas ces modules.

Différence d’ordre de chargement des modules entre ES2015 et CommonJS

Il existe une différence dans la façon dont les modules sont chargés entre ES2015 et avec le format CommonJS:

  • Avec CommonJS, les modules chargent leurs dépendances en même temps qu’ils en exécutent le code.
  • Avec ES2015, les modules sont pré-parsés de façon à résoudre tous les imports avant d’exécuter le code.

Ainsi on écrit les modules ES2015 suivant:
Dans le fichier file1.js:

console.log('Exécute file1.js'); 
import { exportedFromTwo} from './file2.js'; 
console.log(exportedFromTwo); 

Dans le fichier file2.js:

console.log('Exécute file2.js'); 
export const exportedFromTwo = "Provient de file2.js"; 

Le résultat de l’exécution sera:

Exécute file2.js 
Exécute file1.js 
Provient de file2.js 

Dans le cas des modules déclarés avec le format CommonJS:
Dans le fichier file1.js:

console.log('Exécute file1.js'); 
const exportedFromTwo = require('./file2.js'); 
console.log(exportedFromTwo); 

Dans le fichier file2.js:

console.log('Exécute file2.js'); 
module.exportedFromTwo = 'Provient de file2.js'; 

Le résultat de l’exécution sera:

Exécute file1.js 
Exécute file2.js 
Provient de file2.js 

Pour conclure…

Comme on a pu le voir, les modules en Javascript s’implémentent de façon très différente. Avant l’apparition de la syntaxe native en ES2015, il a fallu trouver des work-arounds et utiliser différents outils pour implémenter des modules. Désormais, avec la syntaxe ES2015, l’implémentation des modules est plus facile et plus homogène. Toutefois, beaucoup de browsers sont utilisés avec des versions non compatibles avec ES2015. Il faut donc toujours produire du code Javascript permettant d’être exécuté sur ce type de browser. Ainsi des outils comme des bundlers et des transpilers rendent possible l’implémentation du code Javascript avec la syntaxe ES2015 et son exécution avec du code ES5.

Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Sécuriser les chaines de caractères dans un processus .NET

On peut imaginer que stocker un mot de passe dans une chaîne de caractères pendant l’exécution d’un processus, offre une solution sécurisée et qu’il est difficile de lire le contenu de cette chaîne. En réalité, ce type de stockage n’a rien de sécurisé.

On peut trouver dans des applications .NET des mots de passe stockés en clair dans un objet de type System.String. Toutefois avec très peu de moyen, on peut rapidement lire le contenu de n’importe quelle chaîne de caractères stockée dans un processus .NET. Ainsi, si ces mots de passe apparaissent en clair, il sera facile d’en lire le contenu.

Le but de cet article est d’abord de montrer comment on peut facilement lire une chaîne de caractères dans la mémoire d’un processus .NET. Ensuite, on proposera quelques solutions simples pour rendre une chaîne de caractères plus difficilement lisible par un outil externe.

Détecter le contenu d’une chaîne de caractères dans un processus

Dans un premier temps, on va montrer comment on peut facilement lire une chaîne de caractères stockée en clair dans un objet System.String dans un processus .NET.

La première étape consiste à simplement coder un exécutable contenant une chaîne de caractères non statique. Pour illustrer, on considère une application qui vérifie le mot de passe de l’utilisateur à 3 reprises. Si le couple user/password est correct, il répond OK sinon il réponds KO.

Le code correspondant à ce processus se trouve dans le repository suivant: github.com/msoft/secureString.

Les mots de passe sont stockés par un objet satisfaisant IPasswordFinder:

public interface IPasswordFinder 
{
    string GetPassword(string login); 
} 

L’objet satisfaisant IPasswordChecker est de type Mock<IPasswordFinder>. Il ne fait que renvoyer un mot de passe en fonction du nom d’un utilisateur. On l’instancie en utilisant PasswordFinderFactory dont voici l’implémentation:

public class PasswordFinderFactory 
{ 
    private readonly Dictionary<string, string> passwordPerUser = new Dictionary<string, string>(); 

    public void SetPassword(string login, string password) 
    { 
        this.passwordPerUser[login] = password; 
    } 
    
    public IPasswordFinder GetNewPasswordFinder() 
    { 
        var passwordFinderMock = new Mock<IPasswordFinder>(); 
    
        foreach (var user in this.passwordPerUser) 
            passwordFinderMock.Setup(f => f.GetPassword(user.Key)).Returns(user.Value); 
    
        return passwordFinderMock.Object; 
    } 
    
    public static PasswordFinderFactory GetFactory() 
    { 
        var factory = new PasswordFinderFactory(); 
    
        factory.SetPassword(
            "User1", 
            $"{(char)109}{(char)121}{(char)115}{(char)101}{(char)99}{(char)114}{(char)101}
                {(char)116}{(char)112}{(char)97}{(char)115}{(char)115}{(char)119}{(char)111}
                {(char)114}{(char)100}"
        ); 
    
        factory.SetPassword(
            "User2", 
            $"{(char)111}{(char)116}{(char)104}{(char)101}{(char)114}{(char)112}{(char)97}
            {(char)115}{(char)115}{(char)119}{(char)111}{(char)114}{(char)100}"
        ); 
    
        return factory; 
    } 
} 

Comme on peut le voir le mot de passe avec lequel on effectue la comparaison n’est pas écrit sous forme d’une chaîne de caractères unique pour éviter que le compilateur ne l’interprète comme un objet statique:

factory.SetPassword(
    "User1", 
    $"{(char)109}{(char)121}{(char)115}{(char)101}{(char)99}{(char)114}{(char)101}
        {(char)116}{(char)112}{(char)97}{(char)115}{(char)115}{(char)119}{(char)111}
        {(char)114}{(char)100}"
); 

factory.SetPassword(
    "User2", 
    $"{(char)111}{(char)116}{(char)104}{(char)101}{(char)114}{(char)112}{(char)97}
        {(char)115}{(char)115}{(char)119}{(char)111}{(char)114}{(char)100}"
); 

Si les chaînes étaient stockées de façon statique, il n’y aurait pas de nécessité d’exécuter l’application. En décompilant les assemblies, on verrait directement les chaînes de caractères.

La classe qui effectue la vérification du mot de passe est PasswordChecker:

public class PasswordChecker 
{ 
    private IPasswordFinder passwordFinder; 

    public PasswordChecker(PasswordFinderFactory passwordFinderFactory) 
    { 
        this.passwordFinder = passwordFinderFactory.GetNewPasswordFinder(); 
    } 
    
    public bool CheckPassword(string login, string password) 
    { 
        var foundPassword = this.passwordFinder.GetPassword(login); 
    
        return string.IsNullOrEmpty(foundPassword) ? false : foundPassword.Equals(password); 
    } 
} 

L’exécution est sans surprise:

Essai 1
Quel est le login ?
> User1
Quel est le mot de passe ?
> bad password
KO
Essai 2
Quel est le login ?
> User1
Quel est le mot de passe ?
> mysecretpassword
OK

Pour l’exemple avec des chaînes de caractères simples, il faut regarder le contenu du répertoire SecureString/WithSimpleString. Pour utiliser les classes de ce répertoire, la fonction main doit instancier passwordChecker de cette façon:

var passwordChecker = new WithSimpleString.PasswordChecker( 
    WithSimpleString.PasswordFinderFactory.GetFactory()); 

Récupérer le mot de passe avec un outil externe

On propose 2 méthodes pour lire les chaînes de caractères contenant les mots de passe en utilisant des outils externes au processus:

  • Avec ProcessHacker et
  • Avec WinDbg.

Avec ProcessHacker

Pour lire toutes les chaînes de l’assembly, on peut exécuter ProcessHacker (disponible gratuitement):

  1. Il faut chercher l’application dans la liste des processus
  2. Double-cliquer sur l’application “SecureString.exe”:
  3. Aller dans l’onglet Memory:
  4. Cliquer sur “Strings”
  5. Laisser les paramètres par défaut:
  6. Sauvegarder les chaînes dans un fichier texte en cliquant sur “Save”.

En cherchant dans le fichier texte, on peut voir que les chaînes de caractères correspondant à ce qu’a écrit l’utilisateur à chaque tentative, sont visibles même si la variable password est écrasée à chaque itération de la boucle. D’autre part, on peut voir les chaînes correspondant aux mots de passe à trouver en clair.

Par exemple:

0x162266c (84): ... 
0x16228e0 (108): NLS_CodePage_850_3_2_0_0ns.IMocked`1_get_MocktPassword 
0x1622b58 (32): mysecretpassword
0x1622d88 (26): otherpassword 
0x1623e20 (76): Invariant Language (Invariant Country) 
0x1623e90 (36): ...

Avec WinDbg

Une autre méthode pour parcourir la mémoire d’un processus est d’utiliser WinDbg. WinDbg est un outil puissant qui permet de déboguer un processus et de lire un dump de mémoire (cf. Les “dumps” mémoire en 5 min). Même si WinDbg est difficile à utiliser, il permet d’extraire plus d’informations d’un dump mémoire que Visual Studio.

On peut l’installer à partir SDK Windows 10:

Dans notre cas, le gros intérêt de WinDbg est de pouvoir travailler sur un dump mémoire du processus. Ce dump correspond à une photo de la mémoire occupée par un processus. Même si l’exécution du processus s’interrompt, on peut continuer d’exploiter le dump pour en extraire des informations.

Pour générer un dump, il faut ouvrir le gestionnaire de tâches en faisant [Ctrl] + [Maj] + [Echap]:

  1. Sélectionner le processus
  2. Faire un clique droit sur le processus
  3. Cliquer sur “Generate a dump”

Le dump est généralement écrit dans un fichier dans le répertoire:

C:\Users\<Nom utilisateur>\AppData\Local\Temp\SecureString.DMP 

Après génération du dump, il faut le charger avec WinDbg:

  1. Cliquer sur File
  2. Puis sur “Open Crash Dump”.
  3. Sélectionner le fichier de dump
  4. Cliquer sur Window puis “Dock All” pour agrandir la fenêtre.

Une autre méthode sans générer de dump de mémoire, peut consister à s’attacher directement au processus:

  1. Cliquer sur File
  2. Puis sur “Attach to a process…”
  3. Sélectionner le processus puis cliquer sur OK
  4. Cliquer sur Window puis “Dock All” pour agrandir la fenêtre.

Quand le fichier dump est chargé ou quand le processus est attaché, il suffit de chercher une chaîne de caractères correspondant aux mots de passe en tapant la commande suivante:

s –u 0 0FFFFFFF "<chaîne à chercher>" 

Cette instruction permet de chercher une chaîne Unicode (les chaînes de caractères en .NET sont stockées en UTF-16) de l’adresse mémoire 0 à l’adresse 0FFFFFFF (car on ne connait pas l’adresse de fin).

D’autres commandes permettent d’extraire d’autres informations:

  • Pour lister toutes les chaînes de caractères Unicode (la liste peut être très longue): s –su 0 0FFFFFFF
  • Pour lister toutes les chaînes de caractères ASCII: s –sa 0 0FFFFFFF
  • Pour voir le contenu d’une adresse en mémoire: dc <adresse mémoire>

Par exemple:

0:000> s -u 0 0FFFFFFF "mysecretpassword"  
01622b58  006d 0079 0073 0065 0063 0072 0065 0074  m.y.s.e.c.r.e.t. 

0:000> dc 01622b58   
01622b58  0079006d 00650073 00720063 00740065  m.y.s.e.c.r.e.t. 
01622b68  00610070 00730073 006f0077 00640072  p.a.s.s.w.o.r.d. 
01622b78  00000000 00000000 68363bc8 00000003  .........;6h.... 
01622b88  00000001 ffffffff 00000000 00000000  ................ 
01622b98  6835ca98 00000003 016225b8 01622b50  ..5h.....%b.P+b. 
01622ba8  2c1c7fa3 ffffffff 0162264c 01622d80  ...,....L&b..-b. 
01622bb8  2c1c7fa4 ffffffff 00000000 00000000  ...,............ 
01622bc8  00000000 00000000 00000000 68362158  ............X!6h 

On peut chercher toutes les chaînes de caractères contenus dans le processus en exécutant les commandes suivantes:

  • Pour les chaînes ASCII: s –sa 0 0FFFFFFF
  • Pour les chaînes Unicode: s –su 0 0FFFFFFF

Attention, l’exécution de ces commandes peut prendre du temps.

Comme pour ProcessHacker, on s’aperçoit qu’on peut voir le contenu des chaînes du processus en clair.

Quelques précisions sur “System.String” en .NET

Les chaînes de caractères sont le plus souvent stockées en .NET dans un objet de type System.String. Les objets de ce type sont de type référence c’est-à-dire qu’ils sont alloués et stockés dans le tas managé et qu’ils sont libérés lors des passages du garbage collector.

Les objets System.String ont aussi la particularité d’être immutables c’est-à-dire qu’ils sont passés par référence lors d’appels de fonction ou lors d’affectation de variables toutefois toute manipulation sur une chaîne entraîne la création d’une nouvelle instance de chaîne.

Ainsi si on écrit:

string finalString = "chaine1" + "chaine2" + "chaine3";

4 objets de type System.String sont créés dans le tas (finalString et les objets contenant "chaine1", "chaine2" et "chaine3"). Il faudra attendre le passage du garbage collector pour collecter les objets contenant "chaine1", "chaine2" et "chaine3". Ainsi avant le passage du garbage collector, on peut non seulement voir le contenu de finalString mais aussi "chaine1", "chaine2" et "chaine3".

Ainsi, si on stocke des mots de passe ou des sections de mot de passe dans un objet de type System.String, on ne pourra jamais vraiment savoir quand cet objet sera supprimé par le garbage collector, il peut rester dans la mémoire même longtemps après l’avoir utilisé. Avant sa suppression, on peut largement avoir le temps de générer un dump et de l’étudier pour éventuellement en extraire des chaînes avec des mots de passe. C’est la raison pour laquelle il faut éviter d’utiliser ce type d’objet pour stocker des chaînes dont le contenu est sensible.

Quelques solutions pour stocker des mots de passe

Ne pas utiliser de “System.String”

Cette solution peut paraître évidente mais c’est la plus simple pour ne pas dupliquer une chaîne de caractères au contenu sensible dans la mémoire: éviter d’utiliser trop d’instances d’objet de type System.String.

Eviter de conserver des références vers une chaîne de caractères peut, dans certain cas, permettre au garbage collector de “collecter” l’objet correspondant à la chaîne de caractères de façon à libérer l’espace occupé par l’objet et surtout écraser son contenu pour y affecter d’autres données. En effet, même si le garbage collector libère l’espace correspond à l’objet, le contenu en mémoire n’est pas forcément écrasé et la chaîne de caractères peut rester présente en mémoire pour une durée indéfinie.

Etant donné le manque de maitrise quant à la libération d’un objet de type System.String, il est conseillé d’utiliser des objets System.Security.SecureString pour stocker des chaînes de caractères sensibles.

Forcer l’exécution du garbage collector

Si on ne possède plus de références vers un objet de type System.String, le garbage collector est susceptible de s’exécuter et de collecter cet objet pour libérer l’espace mēmoire occupé. Au lieu d’attendre l’exécution du garbage collector, on peut la forcer quand on sait qu’on n’utilisera plus la chaîne de caractères. Pour forcer l’exécution du garbage collector, on peut exécuter l’instruction:

GC.Collect(); 

Utiliser une “SecureString”

L’objet de type System.Security.SecureString est spécialement conçu pour stocker des chaînes de caractères sensibles. Le contenu stocké par cet objet est crypté en utilisant la fonction RtlEncryptMemory du système d’exploitation (code source de System.Security.SecureString sur referencesource.microsoft.com). Ainsi il est beaucoup difficile de lire le contenu de ce type d’objet en mémoire.

La contrepartie des objets SecureString est que leur utilisation n’est pas forcément direct, en particulier si un mot de passe est stocké de façon provisoire dans un objet System.String.

Dans certains cas, on peut directement avoir une instance de SecureString: par exemple en utilisant l’objet System.Windows.Control.PasswordBox en WPF pour permettre à l’utilisateur d’entrer un mot de passe. Dans ce cas, pour récupérer la valeur du mot de passe, on peut utiliser directement la propriété PasswordBox.SecurePassword qui est de type SecureString. La récupération du mot de passe est donc sécurisée.

A l’opposé, dans d’autres cas, par exemple en WinForm, pour récupérer un mot de passe tapé par l’utilisateur, il faut utiliser une TextBox classique et utiliser la propriété TextBox.Text qui est de type System.String. Dans le cas de la TextBox, il existe des solutions pour ce type de control(1) (2) pour éviter de trop exposer ce qui est tapé par l’utilisateur.

Utiliser le contenu d’une SecureString

Utiliser les données contenus dans un objet SecureString n’est pas tout-à-fait direct. Ainsi, pour utiliser le contenu de ce type d’objet en limitant les risques de copie dans des parties de la mémoire dont on ne maîtrise pas la libération, on peut s’inspirer du code suivant:

public static TReturn UseSecureStringContent<TReturn>(
    System.Security.SecureString secureString, 
    Func<char[], TReturn> stringUse) 
{ 
    int stringLength = secureString.Length; 
    char[] bytes = new char[stringLength]; 
    IntPtr ptr = IntPtr.Zero; 

    try 
    { 
        ptr = Marshal.SecureStringToBSTR(secureString); 
        bytes = new char[stringLength]; 
        Marshal.Copy(ptr, bytes, 0, stringLength); 
    } 
    finally 
    { 
        if (ptr != IntPtr.Zero) 
            Marshal.ZeroFreeBSTR(ptr); 
    } 

    // Utiliser le chaîne de caractères sous forme de tableau de caractères  
    TReturn returnValue = stringUse(bytes); 
    for (int i = 0; i < stringLength; i++) 
        bytes[i] = '*'; 

    return returnValue; 
}

Dans cet exemple, au lieu de générer une chaîne de caractères, on extrait un tableau de caractères. L’intérêt d’utiliser un tableau de caractères est qu’on peut facilement écraser le contenu du tableau après utilisation. Pendant l’exécution de la fonction, la chaîne de caractères sera écrite en clair dans le tableau toutefois si l’exécution est rapide, il est plus difficile d’en lire le contenu en utilisant un dump ou en essayant de lire la mémoire.

Convertir une chaîne de caractères en “SecureString”

On peut convertir une chaîne de caractères en tableau de char de cette façon:

public static System.Security.SecureString GetNewSecureString(char[] clearString) 
{ 
    var secureString = new System.Security.SecureString(); 
    foreach (char caracter in clearString) 
    { 
        secureString.AppendChar(caracter); 
    } 

    return secureString; 
} 

Même si un tableau est alloué sur le tas managé, l’intérêt de son utilisation par rapport à la chaîne stockée dans un objet de type System.String est qu’on peut facilement en écraser le contenu.

On peut facilement utiliser une méthode similaire pour convertir un objet System.String en SecureString:

public static System.Security.SecureString GetNewSecureString(string clearString) 
{ 
    return GetNewSecureString(clearString.ToArray()); 
} 

Convertir une “SecureString” en chaîne de caractères

On peut convertir une SecureString en tableau de caractères de cette façon:

public static char[] ConvertToCharArray(SecureString secureString) 
{ 
    int stringLength = secureString.Length; 
    var charArray = new char[stringLength]; 
    IntPtr valuePtr = IntPtr.Zero; 

    try 
    { 
        valuePtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); 

        for (int i = 0; i < stringLength; i++) 
        { 
            charArray[i] = (char)Marshal.ReadInt16(valuePtr, i * 2);
        } 
    } 
    finally 
    { 
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 
    } 

    return charArray; 
} 

Une conversion de SecureString vers System.String se fait plus directement de cette façon:

public static string ConvertToString(SecureString secureString) 
{ 
    IntPtr valuePtr = IntPtr.Zero; 
    try 
    { 
        valuePtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); 
        return Marshal.PtrToStringUni(valuePtr); 
    } 
    finally 
    { 
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 
    } 
} 

Dans le cadre de l’exemple utilisé plus haut dans le repository GitHub SecureString, il faut adapter le code pour utiliser des objets de type SecureString plutôt que System.String. L’adaptation du code se trouve dans le répertoire SecureString/WithSecureString. La fonction main doit utiliser les classes de ce répertoire et instancier passwordChecker de cette façon:

var passwordChecker = new WithSecureString.PasswordChecker( 
    WithSecureString.PasswordFinderFactory.GetFactory()); 

Utiliser “System.String” et écraser son contenu après utilisation

Comme indiqué plus haut, les objets de type System.String sont des objets de type référence alloués dans le tas managé. Leur durée de vie et surtout leur temps de présence en mémoire dépend de l’exécution du garbage collector. Comme on ne maitrise pas complètement l’exécution du garbage collector, stocker des chaînes de caractères sensibles dans un objet System.String présente un risque. Toutefois dans certains cas d’utilisation, on peut être contraint d’utiliser un objet de ce type, par exemple pour transmettre un mot de passe à une bibliothèque dont on ne maitrise pas l’implémentation et qui impose l’utilisation d’objets de type System.String.

Dans ce cas, une solution est d’utiliser un objet de type System.String et d’en effacer le contenu après utilisation. Après écrasement, un mot de passe stocké dans un objet de ce type est effacé de la mémoire et ne peut plus être lu.

Comme on l’a expliqué plus haut, si on écrit:

string password = "mysecretpassword"; 

Et si on écrit ensuite la ligne suivante:

password = "*********"; 

On n’écrase pas le contenu de la mémoire car un objet de type System.String est immutable. Ce qui signifie que:

  • A la déclaration de la variable password: on alloue à la variable password une référence vers la chaîne de caractères "mysecretpassword" stockée dans le tas managé.
  • A l’affectation de "*********" à la variable password: on alloue une référence vers une nouvelle chaîne de caractères stockée dans le tas managé toutefois l’ancienne chaîne de caractères n’est pas modifiée. C’est juste la référence contenue dans la variable password qui est modifiée.

Il faut donc, un code qui permet réellement d’écraser le contenu de la chaîne de caractères comme par exemple, le code suivant:

public static void EraseStringContent(string stringToErase) 
{ 
    unsafe 
    { 
        fixed (char* stringContent = stringToErase) 
        { 
            for (int i = 0; i < stringToErase.Length; i++) 
                stringContent[i] = '*'; 
        } 
    } 
} 

Ce code permet d’écraser le contenu de la chaîne de caractères passée en paramètre:

  • Le mot-clé unsafe: permet d’indiquer que le code suivant contient des instructions manipulant directement des pointeurs.
  • Le mot-clé fixed: permet d’indiquer au garbage collector de ne pas modifier les emplacements en mémoire des objets manipulés. Sans l’utilisation du mot-clé fixed, le CLR pourrait changer l’emplacement de certains objets en fonction de l’exécution du garbage collector. Ces déplacements pourraient intervenir pendant la manipulation de pointeurs rendant, de fait, invalides les adresses pointées.

Le gros inconvénient de cette méthode est qu’elle nécessite que le code exécuté soit “unsafe” ce qui signifie qu’il faut rajouter une option de compilation permettant d’autoriser l’exécution de code “unsafe”.

Pour autoriser l’exécution de code “unsafe” dans une assembly, il faut:

  1. Effectuer un clique droit sur le projet concerné dans Visual Studio
  2. Cliquer sur Propriétés
  3. Dans l’onglet “Build”, il faut cocher “Allow unsafe code”.

En combinant les SecureString et l’écrasement des chaînes de caractères System.String, on peut proposer l’implémentation suivante pour utiliser le contenu d’une SecureString:

public static TReturn UseSecureStringContent<TReturn>(
    SecureString secureString, 
    Func<string, TReturn> useString) 
{ 
    string simpleString = ConvertToString(secureString); 
    var returnValue = useString(simpleString); 
    EraseStringContent(simpleString); 
    return returnValue; 
} 

De même que précédemment, l’utilisation d’un objet de type System.String rend plus facile la lecture des données sensibles en mémoire. Toutefois étant donné qu’on en efface le contenu après utilisation, la chaîne n’apparait pas longtemps en clair dans la mémoire ce qui limite le risque.

Le code correspondant aux manipulations de SecureString se trouve dans SecureStringHelper.

En conclusion

On a vu quelques méthodes pour facilement lire le contenu de chaînes de caractères dans la mémoire d’un processus quand cette chaîne est stockée en clair. On a pu remarquer que les objets System.String ne sont pas adaptés pour stocker des données sensibles comme des mots de passe. Il est préférable d’utiliser des objets comme les SecureString.

On peut aller plus loin pour stocker ces données sensibles en passant par la Data Protection API (i.e. DPAPI) qui est spécialement conçue pour ce type d’usage. On peut facilement trouver de la documentation pour utiliser la DPAPI(3).

(1) – My SecurePasswordTextBox control is famous: https://weblogs.asp.net/pglavich/440052
(2) – SecurePasswordTextBox update: https://weblogs.asp.net/pglavich/440191
(3) – Documentation pour utiliser la Data Protection API:

Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Les commandes npm courantes en 5 min

Dans l’écosystème Typescript et plus généralement Javascript, il existe une multitude d’outils et de bibliothèques. Ces outils et ces bibliothèques peuvent être utilisés pendant l’exécution de l’application, pour aider aux développements, pour exécuter des tests ou pour aider au déploiement. La multitude des fonctionnalités de ces outils implique des méthodes d’installation spécifiques, des configurations particulières et surtout des cas d’utilisation souvent très différents.

Le plus souvent une application générée à partir de code Typescript est exécutée sur un serveur Node.js et dans cet environnement, un outil est incontournable pour aider à utiliser des bibliothèques tierces. Cet outil est npm (pour Node Package Manager), il sert à gérer les dépendances d’une application Typescript en les organisant sous forme de packages. D’autres gestionnaires existent comme Bower ou yarn.

Le but de cet article est d’expliquer le fonctionnement de npm et d’en expliciter les commandes principales.

npm est installé en même temps que Node.js (pour davantage de détails, voir Installation de Node.js).

Répertoires de npm

Installation par défaut

Après installation, npm est utilisable sous forme d’une commande disponible avec un terminal. Dans le cadre de l’installation qu’on avait effectuée dans l’article Installation de Typescript sur Debian/Ubuntu, Node.js et npm avaient été installés dans le répertoire /usr/node.js, ainsi si on écrit la commande suivante, on doit obtenir le répertoire d’installation de npm:

user@debian:~% whereis npm  
npm: /usr/node.js/node-v8.10.0-linux-x64/bin/npm 

Répertoires contenant les packages

Les packages gérés par npm peuvent être installés suivant 2 modes: local ou global:

  • Local: le package installé localement est installé dans un répertoire qui se trouve dans le répertoire de l’application. Ce répertoire est nommé node_modules. Le package est accessible seulement dans le répertoire de l’application. Dans ce mode, chaque application peut utiliser une version spécifique d’un même package.
  • Global: le package est installé de façon globale dans un répertoire accessible dans le répertoire d’installation de Node.js. Dans notre cas, ce répertoire est: /usr/node.js/node-v8.10.0-linux-x64/. Si un package est installé dans ce répertoire, il est partagé par toutes les applications.
    Il est possible de modifier le chemin de ce répertoire, ce qu’on fera dans le paragraphe suivant.

Modifier le répertoire global des packages (facultatif)

Le répertoire global par défaut est dans le répertoire d’installation de Node.js. Pour savoir comment est préfixé le répertoire global, il faut afficher le paramètre config de npm en écrivant:

user@debian:~% npm config get prefix 
/usr/node.js/node-v8.10.0-linux-x64 

Ainsi, le répertoire global est <prefix>/lib/node_modules. Dans notre cas, le chemin précis du répertoire global est:

/usr/node.js/node-v8.10.0-linux-x64/lib/node_modules 

Le gros inconvénient d’utiliser ce répertoire est qu’il est nécessaire d’être l’utilisateur root pour gérer les packages qui s’y trouvent. Une possibilité est de modifier le préfixe pour que le répertoire global se trouve dans le répertoire de l’utilisateur. Les droits administrateurs ne sont donc plus nécessaires et l’utilisateur peut mettre à jour plus facilement les packages installés de façon globale. Les packages ne sont plus installés à l’échelle de la machine mais à celle de l’utilisateur.

Pour afficher les éléments de configuration de l’utilisateur courant, il suffit de taper:

user@debian:~% npm config list 
; cli configs 
metrics-registry = "https://registry.npmjs.org/" 
scope = "" 
user-agent = "npm/5.6.0 node/v8.10.0 linux x64" 
  
; node bin location = /usr/node.js/node-v8.10.0-linux-x64/bin/node 
; cwd = /home/user 
; HOME = /home/user 
; "npm config ls -l" to show all defaults. 

Pour modifier la valeur du préfixe:

  1. Il faut taper:

    user@debian:~% mkdir .node_modules 
    user@debian:~% npm config set prefix=$HOME/.node_modules 
    

    Si on affiche de nouveau le préfixe après la modification:

    user@debian:~% npm config get prefix 
    /home/user/.node_modules 
    

    Cette opération a créé un fichier de configuration nommé .npmrc dans le répertoire de l’utilisateur. Ce fichier contient désormais la valeur du préfixe:

    user@debian:~% cat .npmrc 
    prefix=/home/user/.node_modules 
    
  2. Maintenant si on réinstalle le package npm au niveau global, il sera installé dans le répertoire /home/user/.node_module. Pour installer le package npm au niveau global, on exécute la commande suivante (cette étape se fait sans être l’utilisateur root):
    user@debian:~% npm install npm --global 
    

    En vérifiant, on voit bien que le répertoire global contient des éléments fraîchement installés et il contient aussi les modules installés de façon globale:

    user@debian:~% ls .node_modules 
    bin   etc   lib   share 
    user@debian:~% ls .node_modules/lib/node_modules 
    npm 
    
  3. On peut ensuite, modifier le chemin de Node.js dans la variable d’environnement PATH en éditant le fichier .bashrc avec un éditeur de texte. Par exemple, avec vi, on peut exécuter la commande suivante:
    user@debian:~% vi .bashrc 
    

    A la fin du fichier, on peut remplacer les lignes:

    export NODEJS_HOME=/usr/node.js/node-v8.10.0-linux-x64  
    export PATH=$PATH:NODEJS_HOME/bin  
    

    Avec les lignes suivantes, contenant les nouveaux chemins:

    export NODEJS_HOME=$HOME/.node_modules  
    export PATH=$PATH:NODEJS_HOME/bin  
    

    On enregistre en tapant sur la touche [Echap] puis :wq et la touche [Entrée] pour valider.

  4. Si on ouvre un nouveau terminal, on devrait avoir un nouveau chemin pour npm:
    user@debian:~% whereis npm  
    npm: /home/user/.node_modules/bin 
    
  5. A partir de cette étape, en exécutant npm on peut avoir une erreur du type:
    /usr/bin/env:  'node': No such file or directory 
    

    Cette erreur se produit parce que node n’est plus présent dans le répertoire .node_modules/bin. Il faut donc rajouter un lien symbolique vers l’ancien répertoire en exécutant:

    user@debian:~% cd .node_modules/bin 
    user@debian:~/.node_modules/bin% ln -s  /usr/node.js/node-v8.10.0-linux-x64/bin/node 
    

    On réinstalle ensuite node dans le répertoire global en exécutant:

    user@debian:~/.node_modules/bin% npm install --global node 
    
  6. On peut en profiter pour réinstaller Typescript
    user@debian:~% npm install --global typescript  
    
  7. On peut vérifier l’installation en exécutant:
    user@debian:~% npm version 
    user@debian:~% node –version 
    

Fichier package.json

Dans le cadre d’un projet, npm utilise un fichier au format JSON nommé package.json qui se trouve à la racine du répertoire du projet. Ce fichier contient des informations comme:

  • La description du projet: ce sont des méta-datas comme le nom, la version, la description, la licence et des mots clés associés au projet. Ces informations peuvent être éditées au besoin.
  • Les packages nécessaires: il est possible d’installer des packages et d’indiquer la dépendance du projet vers ce package en le rajoutant dans le package.json. Ainsi le fichier package.json peut contenir toutes les dépendances du projet vers des packages gérés par npm. On distingue 2 types de dépendances:
    • Les dépendances de développement: ces packages ne sont utilisés que pour le développement et ne le sont pas en production. Par exemple, il peut s’agir de packages pour compiler le code Typescript, pour exécuter des tests ou des utilitaires pour aider au développement. Ces packages ne doivent pas être déployés en production.

      Ces dépendances sont indiquées dans le fichier package.json dans l’élément devDependencies.

    • Les dépendances utilisées au runtime: ces packages sont utilisés à l’exécution de l’application. Ils doivent donc être déployés en même temps que l’application.
      Ces dépendances sont indiquées dans le fichier package.json dans l’élément dependencies.
  • Les applications ou scripts à exécuter: suivant certaines opérations exécutées par npm, il faut exécuter des scripts ou des applications. Ces scripts et ces applications peuvent être indiquées dans le fichier package.json.

Générer un fichier package.json

Il est possible de générer un fichier package.json en exécutant:

user@debian:~/Documents/test_package% npm init 

Un certain nombre de questions sont posées pour remplir les méta-datas associées au projet. Un exemple du fichier généré est:

{ 
  "name": "test_package", 
  "version": "1.0.0", 
  "description": "Description du package test_package", 
  "main": "index.js", 
  "scripts": { 
    "test": "echo \"Error: no test specified\" && exit 1" 
  }, 
  "author": "", 
  "license": "ISC" 
} 

Pour ne pas répondre aux différentes questions et générer le fichier directement, on peut écrire:

user@debian:~/Documents/test_package% npm init --yes 

ou

user@debian:~/Documents/test_package% npm init -y 

Paramétrer des “méta-datas” par défaut

Il est possible de paramétrer les méta-datas appliqués au fichier package.json. Pour paramétrer ces méta-datas, il faut exécuter une commande du type:

user@debian:~% npm config set init.author.name <Nom de l'auteur> 
user@debian:~% npm config set init.author.email <Email de l'auteur> 
user@debian:~% npm config set init.author.url <Url du projet> 
user@debian:~% npm config set init.license <Type de license> 

Il est possible de configurer d’autres éléments, on peut avoir une liste plus exhaustive en exécutant:

npm help 7 config 

Les commandes usuelles de npm

On va lister dans cette partie les commandes les plus courantes.

On peut afficher un message d’aide en exécutant:

user@debian:~% npm help 

ou

user@debian:~% npm -h 

Pour afficher de l’aide concernant une commande précise:

user@debian:~% npm help <nom de la commande> 

ou

user@debian:~% npm <nom de la commande> -h 

Installation d’un package

Ces commandes permettent d’installer un package. On peut avoir des détails sur les packages en allant sur le site npmjs.com/.

Si on installe un package localement, il sera placé dans un répertoire appelé node_modules dans le répertoire courant. L’installation d’un package localement se fait en exécutant:

user@debian:~% npm install <nom du package> 

ou

user@debian:~% npm i <nom du package> 

Cette commande installera le package et, le cas échéant, toutes ces dépendances.

Après installation, la dépendance sera ajoutée au fichier package.json dans l’élément de configuration dependencies (correspondant aux packages nécessaires à l’exécution).

Ainsi si on exécute la commande suivante:

user@debian:~% npm install protractor 

Le fichier package.json contiendra:

{ 
  "name": "test_package", 
  ..., 
  "dependencies": { 
    "protractor": "^5.3.2" 
  }
} 

Il est possible d’installer plusieurs packages en une seule fois en les listant à la suite:

user@debian:~% npm install <nom package 1> <nom package 2> <nom package 3> <etc...> 

Installer les dépendances indiquées dans package.json

Il suffit d’exécuter cette commande (on ne précise pas d’arguments) dans le répertoire du projet:

user@debian:~% npm install

ou

user@debian:~% npm i 

Installation d’une dépendance de développement

Si on veut installer un package et indiquer la dépendance sous forme d’une dépendance de développement, il faut exécuter:

npm install <nom du package> --save-dev 

Au lieu de rajouter la dépendance dans l’élément dependencies du fichier package.json, npm rajoutera la dépendance dans l’élément devDependencies.

Pour omettre l’édition de package.json

Pour éviter d’écrire dans le fichier package.json quand on installe un package, il faut ajouter l’argument:

user@debian:~% npm install <nom du package> --no-save 

Installation d’un package dans le répertoire global

Le répertoire global permet de partager un même package parmi plusieurs projets. Pour avoir le préfixe utilisé, il faut exécuter:

user@debian:~% npm config get prefix 

Le répertoire global contenant les packages est <prefixe>/lib/node_modules.

Pour installer un package dans le répertoire global, il faut exécuter:

user@debian:~% npm install --global <nom du package> 

ou

user@debian:~% npm i –g <nom du package> 

Installer une version spécifique

Les instructions précédentes permettent d’installer la dernière version du package toutefois on peut installer une version spécifique:

user@debian:~% npm install <nom du package>@<version du package> 

Par exemple:

user@debian:~% npm install protractor@5.3.0 

Indication de version dans package.json

Par défaut la version est indiquée dans le fichier package.json en utilisant le caractère ^:

{ 
  "name": "test_package", 
  ..., 
  "dependencies": { 
    "protractor": "^5.3.2" 
  } 
} 

Ce caractère permet d’indiquer que npm mettra à jour le package en exécutant npm update en ne considérant que les versions avec le même numéro majeur. Npm utilise le Semantic Versioning avec des numéros de version sous la forme MAJOR.MINOR.PATCH. Ainsi le même numéro majeur signifie dans notre exemple que npm tentera de mettre à jour le package en cherchant des versions de la forme 5.x.y.

Dans le cadre du Semantic Versioning, un changement de version majeure signifie qu’un breaking change a été introduit.

On peut modifier ce comportement en remplaçant le caractère ^ par ~ dans le fichier package.json. Par exemple:

{ 
  "name": "test_package", 
  ..., 
  "dependencies": { 
    "protractor": "~5.3.2" 
  } 
} 

Le caractère ~ permet d’indiquer que npm bloque la version majeure et mineure. Ainsi, le même

Numéro majeur et mineur signifie dans notre exemple que npm tentera de mettre à jour en cherchant des versions de la forme 5.3.x.

Dans le cadre du Semantic Versioning, un changement de version mineure signifie qu’il n’y a pas de breaking changes et qu’une nouvelle fonctionnalité a été rajoutée.

On peut bloquer une version précise en omettant tout caractère devant la version:

{ 
  "name": "test_package", 
  ..., 
  "dependencies": { 
    "protractor": "5.3.2" 
  } 
} 

On peut indiquer à npm d’ajouter un caractère différent de celui par défaut à l’installation d’un nouveau package en exécutant:

user@debian:~% npm config set save-prefix="~"

On peut modifier le comportement par défaut pour bloquer la version d’un package à chaque installation. Il faut exécuter:

user@debian:~% npm config set save-exact true

Utiliser un fichier npm-shrinkwrap.json

Pour figer une version d’un package, on peut utiliser un fichier nommé npm-shrinkwrap.json à ranger dans le répertoire du projet. Depuis la version 5.x.y, ce fichier a été remplacé par le fichier package-lock.json. Le fichier package-lock.json est généré à chaque édition de npm init.

Utiliser npm-shrinkwrap.json est toutefois obsolète.

Si on souhaite vraiment utiliser un fichier npm-shrinkwrap.json, il suffit d’exécuter:

user@debian:~% npm shrinkwrap 
npm notice package-lock.json has been renamed to npm-shrinkwrap.json. npm-shrinkwrap.json 
will be used for future installations. 

Lister les packages installés

Pour lister les packages installés au niveau local, il faut exécuter:

user@debian:~% npm list

ou

user@debian:~% npm ls

Les packages seront listés avec leurs dépendances.

Pour lister les packages sans leurs dépendances:

user@debian:~% npm list --depth=0

Enfin pour lister les packages installés de façon globale:

user@debian:~% npm list --global 

ou

user@debian:~% npm list –g 

On peut afficher d’autres informations en listant les packages:

user@debian:~% npm ll

ou

user@debian:~% npm la

Désintaller un package

Pour désinstaller un package installé localement, il faut exécuter:

user@debian:~% npm uninstall <nom du package> 

ou

user@debian:~% npm un <nom du package> 

Les dépendances seront aussi désinstallées et l’entrée correspondante dans le fichier package.json sera supprimée.

Pour désinstaller un package de façon globale, il faut exécuter:

user@debian:~% npm uninstall --global <nom du package> 

ou

user@debian:~% npm uninstall -g <nom du package> 

Avoir des informations sur un package

On peut avoir des informations sur un package en exécutant:

user@debian:~% npm view <nom du package> 

Pour avoir des informations de version:

user@debian:~% npm view <nom du package>  versions 

Pour voir la page d’accueil du package:

user@debian:~% npm home <nom du package> 

Pour avoir le repository Git:

user@debian:~% npm repo <nom du package> 

Pour avoir de la documentation:

user@debian:~% npm docs <nom du package> 

Pour voir les bugs:

user@debian:~% npm bugs <nom du package> 

Mise à jour d’un package

Afficher les packages à mettre à jour

Pour afficher les packages pour lesquels une nouvelle version existe, il faut exécuter:

user@debian:~% npm outdated

L’affichage se fera de cette façon:

Package     Current  Wanted  Latest  Location 
protractor    5.3.0   5.3.2   5.3.2  test_package 

Effectuer des mises à jour

Pour mettre à jour un package:

user@debian:~% npm update <nom du package>  

ou

user@debian:~% npm up <nom du package> 

Certains éléments peuvent empêcher la mise à jour d’un package comme l’utilisation d’un fichier package-lock.json, npm-shrinkwrap.json, l’absence de caractères ^ ou ~ dans la liste des dépendances dans le fichier package.json. Ces éléments sont précisés plus haut.

Supprimer les packages du cache

A chaque installation d’un package, npm range les packages téléchargés dans le répertoire nommé .npm dans le répertoire home de l’utilisateur. Le répertoire cache est utilisé pour éviter de solliciter le réseau pour réinstaller des packages déjà téléchargés.

Ainsi, le cache se trouve dans le répertoire .npm. Si on liste ce répertoire:

user@debian:~% ls .npm 
anonymous-cli-metrics.json   _cacache   index-v5   _locks   _logs 

Pour supprimer le cache, il faut exécuter la commande:

user@debian:~% npm cache clean

Chercher des packages

Pour chercher des packages se trouvant parmi la liste des packages disponibles, on peut exécuter:

user@debian:~% npm search <nom partiel du package> 

Supprimer des packages non référencés

Certains packages peuvent être installés mais non référencés dans le fichier package.json. Pour supprimer ces packages, on peut exécuter:

user@debian:~% npm prune

Utilisation de scripts

Un des grands intérêts de npm est de pouvoir exécuter des scripts dans le cadre de certaines actions. Pour exécuter un script, il faut exécuter la commande:

user@debian:~% npm run <nom du script> 

ou

user@debian:~% npm run-script <nom du script> 

Pour voir la liste des scripts disponibles, on peut écrire:

user@debian:~% npm run 

ou

user@debian:~% npm run-script 

Certaines actions peuvent être exécutées directement sans forcément utiliser la syntaxe npm run <nom de la commande> ou npm run-script <nom de la commande>.

Par exemple:

  • npm install: permet d’installer des packages.
  • npm pack: produit un fichier compressé des fichiers à déployer.
  • npm publish: publie un package.
  • npm restart: exécute les scripts stop et start dans cet ordre sauf si un script spécifique nommé restart est précisé.
  • npm start: exécute le script nommé start généralement pour démarrer le serveur Node.js.
  • npm stop: exécute le script nommé stop généralement pour stopper le serveur Node.js.
  • npm test: permet d’exécuter les tests.
  • npm uninstall: désinstalle un ou plusieurs packages.
  • npm unpublish: supprime un package qui a été publié.

Les autres scripts doivent être exécutés en utilisant la syntaxe npm run <nom de la commande> ou npm run-script <nom de la commande>.

Le code correspondant aux scripts peut être implémenté dans l’élément de configuration scripts dans le fichier package.json. Par exemple, on peut voir cet élément de configuration dans le fichier généré plus haut (grâce à npm init):

{ 
  "name": "test_package", 
  "version": "1.0.0", 
  "description": "Description du package test_package", 
  "main": "index.js", 
  "scripts": { 
    "test": "echo \"Error: no test specified\" && exit 1" 
  },
  "author": "", 
  "license": "ISC" 
} 

Dans cet exemple, seul le script test est implémenté.

Pour certaines commandes, il n’est pas forcément nécessaire d’implémenter du code pour qu’un traitement soit effectué comme npm install, npm pack, npm publish, npm uninstall et npm unpublish.

npm considère qu’un script qui renvoie 0 a réussi son exécution et à l’opposé, un script qui renvoie 1 a échoué.

Ajout du chemin du répertoire global dans $PATH

Lors de l’exécution des scripts avec npm run, le chemin du répertoire <prefix>/node_modules/.bin (<prefix> étant le chemin du répertoire global) est rajouté à la variable d’environnement $PATH. Il n’est donc pas nécessaire d’indiquer le chemin complet de ce répertoire pour attendre les binaires qui s’y trouvent.

Par exemple, pour démarrer les tests avec protractor, il n’est pas nécessaire d’écrire le chemin <prefix>/node_modules/.bin/protractor protractor-config.js, on peut écrire directement dans le fichier package.json:

"scripts": { 
    "test": "protractor protractor-config.js" 
  } 

Exécution de scripts personnalisés

En plus de tous les scripts décrits plus haut, on peut personnaliser le nom d’un script à exécuter en l’indiquant dans le fichier package.json.
Par exemple, si on nomme un script myscript:

"scripts": { 
    "myscript": "echo 1" 
  } 

On peut appeler ce script directement en utilisant la même syntaxe:

npm run myscript

ou

npm run-script myscript

Dépendances entre les scripts

Pour la plupart des scripts, quand on exécute un script nommé <script>, les scripts suivants seront exécutés s’ils sont implémentés:

  • pre<script>: il sera exécuté avant le script <script>.
  • post<script>: il sera exécuté après le script <script>.

Si un script dans la suite pre<script>, <script> et post<script> échoue (renvoie 1), l’exécution s’interrompt.

Par exemple, pour la plupart des scripts la règle indiquée précédemment est valable:

  • pretesttestposttest
  • prestartstartpoststart
  • prestopstoppoststop
  • prerestartrestartpostrestart
  • preuninstalluninstallpostuninstall

Pour d’autres commandes, le workflow est plus complexe:

  • pack: prepublishprepareprepackpackpostpack
  • publish: prepublishprepareprepublishOnlypublishpostpublish
  • install (sans arguments supplémentaires): prepublishpreparepreinstallinstallpostinstall

Ajouter des arguments en exécutant un script

On peut transmettre des arguments à un script directement à partir de la ligne de commande npm. Par exemple, en exécutant:

npm run test -- --grep="pattern" 

Les arguments --grep="pattern" sont transmis au script test qui sera exécuté.

Exécuter des scripts dans des packages

On peut exécuter des scripts présents dans des packages en exécutant la commande:

npm explore <nom du package> -- npm run <nom du script> 

Exemple de configuration des scripts d’un fichier package.json

Dans le cadre d’un projet Typescript, voici un exemple d’implémentation de scripts dans le fichier package.json:

"scripts": { 
  "info": "npm-scripts-info", 
  "build": "run-s clean && run-p build:*", 
  "build:main": "tsc -p tsconfig.json", 
  "build:module": "tsc -p tsconfig.module.json", 
  "fix": "run-s fix:*", 
  "fix:prettier": "prettier \"src/**/*.ts\" --write", 
  "fix:tslint": "tslint --fix --project .", 
  "test": "run-s build test:*", 
  "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", 
  "test:unit": "nyc --silent ava", 
  "test:nsp": "nsp check", 
  "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", 
  "cov": "run-s build test:unit cov:html && opn coverage/index.html", 
  "cov:html": "nyc report --reporter=html", 
  "cov:send": "nyc report --reporter=lcov > coverage.lcov && codecov", 
  "cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100", 
  "doc": "run-s doc:html && opn build/docs/index.html", 
  "doc:html": "typedoc src/ --target ES6 --mode file --out build/docs", 
  "doc:json": "typedoc src/ --target ES6 --mode file --json build/docs/typedoc.json", 
  "doc:publish": "gh-pages -m \"[ci skip] Updates\" -d build/docs", 
  "version": "standard-version", 
  "reset": "git clean -dfx && git reset --hard && npm i", 
  "clean": "trash build test", 
  "all": "run-s reset test cov:check doc:html", 
  "prepare-release": "run-s all version doc:publish" 
} 

Pour résumer…

Voici un tableau récapitulatif des commandes les plus utiles:

Commande Raccourci

npm install

npm install npm i Installe les packages indiqués dans le fichier package.json
npm install <package> npm i <package> Installe la dernière version d’un package
npm install <package>@<version> npm i <package>@<version> Installe une version particulière d’un package
npm install <package>@">=2 <3.0" npm i <package>@">=2 <3.0" Installe un package en précisant des conditions sur la version
npm install <package> --global npm i <package> -g Installe un package dans le répertoire global
npm install <package> --save-dev npm i <package> --save-dev Installe un package et indique ce package dans la partie devDependencies du fichier package.json
npm install <package> --no-save npm i <package> --no-save Installe un package sans le mentionner dans le fichier package.json
npm install @<nom du domaine>/<package> npm i @<nom du domaine>/<package> Installe un package appartenant à un domaine particulier (i.e. scope).
Par exemple: npm install @types/jquery
npm install <utilisateur>/<repository> npm i <utilisateur>/<repository> Installe un package à partir de GitHub
npm install <utilisateur>/<repository>#<branche> npm i <utilisateur>/<repository>#<branche>
npm install github:<utilisateur>/<repository> npm i github:<utilisateur>/<repository>

npm update

npm update npm up Met à jour les packages listés dans la partie dependencies du fichier package.json
npm update --dev npm up --dev Met à jour les packages listés dans la partie devDependencies du fichier package.json
npm update --global npm up -g Met à jour les packages se trouvant dans le répertoire global
npm update <package> npm up <package> Met à jour un package spécifique
npm outdated Affiche les packages à mettre à jour
npm outdated <package> Affiche si un package spécifique peut être mis à jour
npm uninstall <package> npm un <package> Désinstalle un package
npm uninstall <package> --global npm un <package> -g Désintalle un package du répertoire global

npm list

npm list npm ls Liste les packages et leurs dépendances
npm list --global npm ls -g Liste les packages dans le répertoire global
npm list --depth=0 npm ls --depth=0 Liste les packages sans afficher leurs dépendances
npm ll Liste les packages en affichant plus de détails
npm la

Autres commandes

npm view <package> Affiche les détails d’un package
npm view <package> versions Affiche les versions disponibles d’un package
npm search <nom partiel d'un package> Affiche les packages disponibles à partir d’un nom partiel
Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Interesting reads – .NET Garbage Collector

Some interesting articles about .NET Garbage Collector (i.e. GC):

General documentation

GC settings available from framework 4.5

Visualising the .NET Garbage Collector

Preventing GC or Pause GC

Other articles

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

L’essentiel de la syntaxe Typescript en 10 min

Le but de cet article est de présenter de façon succinte la syntaxe de Typescript. Pour un développeur C#, l’apprentissage de Typescript n’est pas forcément aisé car beaucoup de mots clé se ressemblent et peuvent préter à confusion. On peut facilement tomber dans le piège de croire que Typescript est comme C#. Tout deux étant des langages procéduraux, les notions sont évidemment similaires, toutefois de nombreuses subtilités poussent à rester vigilant et à vérifier tous les détails de syntaxe.

Le code Typescript n’est pas directement exécuté, il est compilé en Javascript. C’est le code Javascript qui sera exécuté par les browsers. Javascript étant exécutable par tous les browsers, le choix de Typescript comme langage de programmation est pertinent puisqu’il sera, de fait, facilement exécutable.

Un autre gros avantage de Typescript par rapport à Javascript est de pouvoir typer fortement les variables ce qui permet de rendre ce langage moins sensible aux bugs révélés au runtime. En effet, de nombreuses erreurs peuvent être décelées directement à la compilation.

D’autres apports syntaxiques sont faits par Typescript comme les classes, les modules ou les interfaces ce qui permet de rendre le langage plus attrayant pour des développeurs habitués à ces notions en C# ou Java.

Typescript étant lié à Javascript, on va, en préambule, donner quelques indications concernant ECMAScript. Ensuite, quelques explications seront données sur les types les plus couramment utilisés, les fonctions, les classes et les interfaces. Enfin, on va décrire quelques instructions courantes.
Volontairement, cet article ne traite pas des exports de classes, des modules, des références et de l’utilisation de bibliothèques tierces. Ces sujets feront l’objet d’un autre article.

Sommaire

ECMAScript

Typage et variables
Typage implicite
  Chaine de caractères sur plusieurs lignes
Types primitifs
  Chaine de caractères avec des expressions incluses
any
object
undefined
null
void
enum
Tuple
Effectuer un “cast” pour changer de type
Différences entre “let” et “var”
Object literal
  Déstructuration
  Déstructuration avec reste
Array
  Déstructuration d’un tableau
  Déstructuration d’un tableau avec reste

Fonctions
Arrow functions
Définir une fonction dans un “object literal”
Paramètre optionel
Utiliser un “object literal”
const

Classe
Instanciation
  Instanciation d’une classe en utilisant “typeof”
Notation abrégée des membres dans le constructeur
Fonctions
Accesseurs
readonly
Propriété statique
  Atteindre une propriété statique avec “typeof”
Héritage
  “Overrider” une méthode
Effectuer un “cast”
Classe abstraite
Generic

Combinaisons de types
Intersection
Union
Utilisation de “type guards”
  is
  typeof
  instanceof

Interface
Définir un objet quelconque satisfaisant une interface
Utilisation des interfaces avec des “object literals”
  Propriété optionelle
  readonly
Utilisation des interfaces avec des classes
Héritage entre interfaces
Déclarer une signature de fonction sous forme d’interface
  Déclarer une fonction d’indexation sous forme d’interface
Interface héritant d’une classe

Instructions courantes
if…then…else
  Opérateurs de comparaison
  Opérateurs logiques
  Comportement avec “null” et “undefined”
  Différences entre “==” et “===”
Boucles
  Boucle “for”
  Boucle “while”
  Boucle “do…while”
  break/continue
  Boucle infinie
  for…of
  for…in
try…catch

Pour conclure…

ECMAScript

ECMAScript est un ensemble de normes édictées par l’ECMA (i.e. European Computer Manufacturers Association) pour standardiser des langages de type script. L’ECMA-262 est le standard publié par l’ECMA concernant les langages scriptés.

Javascript est une implémentation de ce standard, ce qui signifie qu’il implémente une bonne partie des spécifications mais pas toutes. En plus de la conformité de Javascript avec le standard ECMAScript, il faut prendre en compte la compatibilité des browsers. Même si une version de Javascript est compatible avec une version d’ECMAScript, ça ne veut pas dire que tous les browsers supportent toutes les spécifications. Certains sites référencent les spécifications supportées en fonction du browser, par exemple:
http://kangax.github.io/compat-table/es6/.

Les versions actuellement les plus connues d’ECMAScript sont les suivantes:

  • ES5 (pour ECMAScript 5e version): publiée en 2009, elle est la version la plus supportée par les browsers. En paramétrant cette version comme version cible à la compilation, on est sûr de pouvoir être compatible avec la plupart des browsers.
  • ES6/ES2015 (pour ECMAScript 6e version): publiée en 2015 et renommée ES2015. Au fur et à mesure, les browsers se rendent compatibles avec l’ensemble des fonctionnalités du standard. Une technique consiste à utiliser des éléments de syntaxe ES6/ES2015 pour bénéficier des avantages de cette version et à compiler ces éléments en ES5 pour que le code soit exécutable par la plupart des browsers.

D’une façon générale, il est possible de tester la syntaxe Typescript sur le site: typescriptlang.org

Typage et variables

Tous les types primitifs Javascript sont utilisables en Typescript. Comme indiqué plus haut, Typescript est fortement typé, ce qui sous-entend que le compilateur effectue des vérifications dans la cohérence des types utilisés à partir de la définition d’une variable.

En Typescript, il est toutefois possible d’utiliser des types moins précis comme undefined ou any comme en Javascript.

On peut déclarer une variable sans l’initialiser en utilisant la syntaxe générale:

let <nom de la variable>: <type de la variable>  

ou

var <nom de la variable>: <type de la variable>  

Une variable peut être définie et initialisée de cette façon:

let <nom de la variable>: <type de la variable> = <valeur d'initialisation> 

ou

var <nom de la variable>: <type de la variable> = <valeur d'initialisation> 

Par exemple:

let varNum1: number = 2; 
var varNum2: number = 3; 
let simpleString: string = 'chaine de caractères'; 
var otherSimpleString: string = 'autre chaine de caractères'; 

var n’a pas la même signification qu’en C#. On peut le voir dans les exemples précédents, var ne prive de préciser le type d’une variable comme c’est le cas en C#.

Typage implicite

Le type peut être déterminé de façon implicite à partir de la valeur d’initialisation (i.e. type inference). On peut, ainsi, éviter de préciser explicitement le type.

Par exemple:

let varNum1 = 2; // le type est indiqué de façon implicite: number
var varNum2 = 3;  
let simpleString = 'chaine de caractères'; // le type est string

Types primitifs

On peut résumer les types primitifs les plus importants provenant du Javascript dans le tableau suivant:

Déclaration Remarques Exemples
Valeur booléenne boolean Dans les premières versions de Typescript (< version 0.9), un booléen se déclarait avec le mot-clé bool. Toutefois dans les versions récentes, le mot-clé utilisé est boolean let booleanVar: boolean = false;
Chaine de caractères string La valeur d’une chaine de caractères peut être déclarée en utilisant des guillemets "..." (comme en C#) ou des simples quotes '...' let stringVar: string = 'chaine de caractères';
Valeur numérique number Tous les nombres sont des valeurs à virgule flottante let numberVar: number = 76;

Certains opérateurs Javascript existent en Typescript. Par exemple l’opérateur !! qui permet de convertir une chaine de caractère en booléen:

let stringValue: string = 'yes';  
let booleanValue: boolean = !!stringValue; 

La valeur 'yes' est transformée en booléen.

Chaine de caractères sur plusieurs lignes

On peut définir des chaines de caractères sur plusieurs lignes en utilisant le caractère ` (` est le caractère accent grave accessible avec [AltGr] + [7]):

let multipleLineString: string = `Ligne 1 
Ligne 2 
Ligne 3`; 

Chaine de caractères avec des expressions incluses

On peut inclure dans une chaine de caractères des expressions qui seront interprétées. Pour utiliser ces expressions, il faut entourer la chaine de caractère par ` (` est le caractère accent grave accessible avec [AltGr] + [7]) et inclure l’expression dans une déclaration ${...}.

Par exemple:

let numberVar: number = 5; 
let stringWithNumber: string = `La valeur + 1 est: ${ number + 1}`; 

any

Ce type provient du Javascript et indique que le type est indéfini. Si une variable est déclarée avec ce type, on pourra effectuer des affectations avec des types différents.

Par exemple:

let anyVariable; // implicitement le type est any 
let otherVariable: any = 67; // le type est explicitement any 
otherVariable = 'chaine de caractères'; // possible car le type est any 

Même si une variable est de type any, il est possible d’appeler des fonctions dépendantes de son type réel:

let anyVariable: any = 34; // le type réel est number 
let expo = anyVariable.toExponential(2); // Possible 

object

Object est proche du même type en C#. Le type Object permet de déclarer une variable de type indéfini. Comme pour any, on peut y affecter des valeurs de types différents.

La différence avec any est qu’on ne peut pas évoquer une fonction dépendant du type réel:

let objectVariable: Object = 34; // le type réel est number 
let expo = objectVariable.toExponential(2); // Pas possible 

undefined

undefined est une valeur que l’on peut affecter à tous les types:

let variable: number = 34; 
variable = undefined; 

Il s’agit aussi d’un type à part entière. On peut donc déclarer une variable de type undefined:

let undefinedVar: undefined = undefined; // On ne peut rien affecter à cette variable à part undefined et null. 

null

Par défaut, null est une valeur possible pour tous les types de variables. On peut donc affecter la valeur null aux variables de ce type:

let nullNumber: number = null;

On peut aussi affecter null à une variable de type undefined.

Comme undefined, il s’agit aussi d’un type à part entière. On peut donc déclarer une variable de type null:

let nullVar: null = null; // On peut affecter seulement null ou undefined. 

void

Il s’agit d’un type pour indiquer “absence de type”. Comme en C#, il permet d’indiquer qu’une méthode n’a pas de valeur en retour.

Si une variable est de type void, on ne peut y affecter que le type null ou undefined:

let voidVar: void = undefined;

enum

Contrairement aux autres types, ce type n’existe pas en Javascript et est un apport du Typescript. Il est proche du type enum en C#, il permet de définir un ensemble de valeurs:

enum Language { 
    Csharp, Typescript, Javascript 
} 

On peut affecter une variable avec une valeur particulière de l’enum:

let choosedLanguage: Language = Language.Typescript; 

Par défaut, la valeur numérique associée à la 1ère valeur de l’enum est 0 et peut être utilisée de cette façon:

let choosedLanguage: string = Language[0];

On peut aussi affecter une valeur numérique particulière aux valeurs de l’enum:

enum Language { 
    Csharp = 1,  
    Typescript,  // implicitement la valeur sera 2 
    Javascript   // implicitement la valeur sera 3 
} 

On peut aussi affecter des valeurs arbitraires:

enum Language { 
    Csharp = 4,  
    Typescript = 7,  
    Javascript = 9 
}

Tuple

La notion est la même qu’en C#, ce type permet de définir des paires ou des triplets de valeurs:

let tupleValue: [boolean, string, number]; // pour déclarer un triplet  

L’initialisation se fait de cette façon:

tupleValue = [false, 'Chaine de caractère', 34]; 

On peut atteindre une valeur particulière en utilisant un index:

let booleanValue: boolean = tupleValue[0]; 
let stringValue: string = tupleValue[1]; 
let numberValue: number = tupleValue[2];

Effectuer un “cast” pour changer de type

Pour changer le type d’une variable, on peut effectuer un cast avec 2 syntaxes:

let anyValue: any = 'Chaine de caractères'; 
let stringValue = <string>anyValue; 

Ou

let stringValue = anyValue as string; 

Il faut avoir en tête que le cast n’est pris en compte que par le compilateur. Le code Javascript généré n’effectue pas de cast à proprement parlé.

Par exemple, pour le code précédent, le code Javascript généré est:

var anyValue = 'Chaine de caractères'; 
var stringValue = anyValue;

Différences entre “let” et “var”

On peut penser que les mot-clés let et var ont la même signification pourtant il existe une différence importante:

  • Les variables définies avec var ont une portée liée à la fonction entière
  • Les variables définies avec let ont une portée liée au bloc de code.

Par exemple, si on écrit:

var numberValue = 45; 
if (true) { 
    var numberValue = 54; 
} 

console.log(numberValue);   // La valeur est 54 car la portée est liée à la fonction. 

Si on remplace var par let:

let numberValue = 45; 
if (true) { 
    let numberValue = 54; 
} 

console.log(numberValue);   // La valeur est 45 car la portée est liée au bloc de code. 

Pour comprendre il faut regarder le code Javascript généré. Dans le cas du var, on a:

var numberValue = 45; 
if (true) { 
    var numberValue = 54; 
} 

console.log(numberValue); 

Dans le cas du let, on obtient;

var numberValue = 45; 
if (true) { 
    var numberValue_1 = 54; 
} 

console.log(numberValue); 

La variable dans le bloc et celle à l’extérieur du bloc n’ont pas le même nom d’où la différence dans le résultat obtenu à l’exécution.

C’est la raison pour laquelle, il est conseillé d’utiliser let plutôt que var car le fonctionnement de let se rapproche de la gestion des variables en C#.

Object literal

Les object literals correspondent à un type Javascript complexe permettant de définir une structure composée de plusieurs données membres:

let dimensions = { 
    Height: 10, 
    Width: 54 
}; 

Le nom des propriétés doit être définie à l’instanciation de l’object literal.

On peut affecter des valeurs aux propriétés après la définition:

dimensions.Height = 7; 
dimensions.Width = 2; 

Le type de l’object literal est implicite dans le cas précédent. On peut être plus explicite avec la notation suivante:

let dimensions: { 
      Height: number, 
      Width: number 
  } = { 
    Height: 10, 
    Width: 54 
}; 

Un object literal est de type object toutefois si on le déclare en explicitant le type object, on ne pourra pas accéder aux propriétés:

let dimensions: object = { 
    Height: 10, 
    Width: 54 
}; 

dimensions.Height = 7; // cette affectation ne fonctionne pas à cause du type object 
dimensions.Width = 2;  // cette affectation ne fonctionne pas à cause du type object 

Il faut définir l’object literal sans préciser le type pour pouvoir accéder aux propriétés:

let dimensions = { 
    Height: 10, 
    Width: 54 
}; 

On peut définir des méthodes dans un object literal.

Déstructuration

La déclaration d’un object literal correspond à la structuration d’une structure de données. La structure construite contient donc plusieurs propriétés. Par la suite, il est possible de déstructurer l’object literal en plusieurs propriétés.

Par exemple, si on définit l’object literal suivant:

let size = { 
    Height: 4, 
    Width: 8, 
    Depth: 2 
}; 

On peut déstructurer l’object literal en plusieurs propriétés distinctes en utilisant la syntaxe suivante:

let {height, width, depth} = size; 

Ainsi, les propriétés sont utilisables individuellement:

console.log(height); 
console.log(width); 
console.log(depth);

Dans le cas où on veut effectuer une affectation sur des variables déjà existantes, il faut utiliser une autre syntaxe:

({height, width, depth} = size);

Déstructuration avec reste

On peut déstructurer un object literal en ne considérant qu’une certaine partie de ses propriétés. Par exemple, si on définit l’object literal suivant:

let point = { 
    X: 4, 
    Y: 8, 
    Z: 2, 
    A: 8, 
    B: 9 
};

Il est possible de ne considérer que les premières valeurs en utilisant la syntaxe:

let {x, y, ...rest} = point;

Ainsi si on affiche le contenu des variables, on aura:

console.log(x); // affiche 4 
console.log(y); // affiche 8 
console.log(rest); // affiche un object literal avec {Z: 2, A: 8, B: 9}

Array

Un tableau peut être déclaré de cette façon:

let valueList: number[] = [5, 9, 2]; 

ou

let valueList: Array<number> = [5, 9, 2]; 

Les 2 notations sont équivalentes.

Déstructuration d’un tableau

On peut déstructurer les valeurs d’un tableau en plusieurs variables de cette façon:

let points = [5, 9, 2]; 

On peut déstructurer les valeurs du tableau en variables distinctes:

let [x, y, z] = points; 

Les variables peuvent ainsi être utilisées individuellement:

console.log(x); 
console.log(y); 
console.log(z); 

Déstructuration d’un tableau avec reste

On peut déstructurer un tableau en ne considérant qu’une certaine partie de ses valeurs. Par exemple, si on définit le tableau suivant:

let points = [4, 8, 2, 8, 9]; 

Il est possible de ne considérer que les premières valeurs du tableau en utilisant la syntaxe:

let {x, y, ...rest} = points; 

Ainsi si on affiche le contenu des variables on aura:

console.log(x); // affiche 4 
console.log(y); // affiche 8 
console.log(rest); // affiche un tableau contenant [2, 8, 9] 

Fonctions

On peut déclarer une fonction de cette façon:

function <nom de la fonction>(<arguments>): <type de retour> { 
    <Corps de la fonction> 
} 

Par exemple:

function Multiply(val1: number, val2: number): number { 
    return val1*val2; 
} 

Le type de retour peut être omis. Dans ce cas, le type de retour est déterminé de façon implicite:

function Multiply(val1: number, val2: number) { 
    return val1*val2; 
} 

Pour indiquer qu’il n’y a pas de types de retour, on peut utiliser void comme en C#:

function LogResult(val1: number, val2: number): void { 
    console.log(val1); 
    console.log(val2); 
} 

Arrow functions

Les arrow functions sont les équivalents des lambda expressions en C#. On peut déclarer une arrow function de cette façon:

var multiplyValues = function(val1: number, val2: number): number { 
    return val1 * val2; 
} 

Une autre syntaxe plus courte est aussi possible en utilisant => (d’où le terme arrow function):

var multiplyValues = (val1: number, val2: number) => val1*val2; 

Pour déclarer une arrow function qui ne renvoie rien, il faut utiliser le mot clé void:

var logValue = (val1: number): void => console.log(val1);

Définir une fonction dans un “object literal”

L’object literal présenté plus haut peut contenir des fonctions membres en plus des données membres.

Par exemple, si on reprend l’exemple précédent:

let dimensions = { 
    Height: 10, 
    Width: 54, 
    CalculateArea: function() { 
        return this.Height * this.Width; 
    } 
}; 

Il faut remarquer l’utilisation de this qui est obligatoire pour accéder aux données membres de l’object literal.

On peut exécuter la fonction de cette façon:

dimensions.CalculateArea(); 

Paramètre optionel

On peut déclarer des arguments optionels dans une fonction avec la syntaxe <nom de l'argument>?: <type de l'argument>.

Par exemple:

function CalculateArea(height: number; width?: number): number 
{ 
    if(width === undefined) {  
        return height * height;  
    } 

    return height * width; 
} 

Pour savoir si l’argument est défini ou non, on peut tester en utilisant la syntaxe:

if(width === undefined) 
{   
    // ...
} 

L’appel à cette fonction peut se faire de cette façon:

CalculateArea(5, 7); 

Ou

CalculateArea(5); 

Utiliser un “object literal”

On peut utiliser des object literals comme argument ou en retour d’une fonction.

Par exemple en argument:

function CalculateArea(rectangle: { height: number, width?: number}): number 
{ 
    if(rectangle.width === undefined) {  
        return rectangle.height * rectangle.height;  
    } 

    return rectangle.height * rectangle.width; 
} 

L’appel à cette fonction se fait en définissant un object literal respectant la signature:

let rectangleVar = { height: 5, width: 8}; 
let area = CalculateArea(rectangleVar); 

On peut utiliser un object literal en type de retour de cette façon:

function DefineRectangle(height: number, width: number): { h: number; w: number } { 
    return { h: height, w: width }; 
} 

Il n’y a pas de difficulté pour utiliser une fonction de ce type:

let rectangle = DefineRectangle(4, 9); 
console.log(rectangle.h); 
console.log(rectangle.w); 

const

Le mot clé const permet de rendre immutable une variable. Il ne faut pas confondre const avec le mot clé readonly. readonly peut être utilisé pour les propriétés d’une classe alors que const doit être utilisé pour les variables d’une méthode.

Ainsi si on écrit:

const immutableValue: number = 34; 

On ne peut plus modifier la valeur de immutableValue par la suite.

const permet d’empêcher de nouvelle affectation sur une variable toutefois elle ne rends pas la contenu de la variable immutable.

Par exemple, si on définit l’object literal suivant:

const size = { 
Height: 6, 
Width: 9 
}; 

On ne pourra pas affecter une nouvelle valeur à la variable size:

size = { 
Height: 1, 
Width: 2 
}; // provoque une erreur 

Toutefois, on peut modifier le contenu de l’object literal:

size.Height = 3; // Ne provoque pas d'erreur 

Il faut donc utiliser const de préférence pour les types primitifs ou les structures de données immutables.

Classe

Les classes en Typescript correspondent à la même notion qu’en C#:

  • Une classe permet d’encapsuler des données membres privées (i.e. fields), des propriétés et des fonctions membres.
  • On peut considérer une classe sous sa forme statique ou sous la forme d’une instance.
  • Une classe peut être instanciée avec un constructeur.
  • Une classe peut hériter d’une autre classe (le mutlihéritage n’est pas possible en Typescript).
  • Une classe peut implémenter une ou plusieurs interfaces.
  • De la même façon que dans la plupart des langages objet, il existe une notion de portée pour les données et fonctions membres:
    • private: lorsque les membres ne sont accessibles qu’à l’intérieur de la classe.
    • protected: lorsque les membres sont accessibles dans le classe et les classes qui en héritent (i.e. classe “enfant”).
    • public: les membres sont accessibles à l’extérieur de la classe.

Concernant la portée des membres d’une classe, il existe une différence importante avec le C#: par défaut si une opérateur de portée n’est pas précisé les membres sont publics. Pour limiter la portée d’un membre, il faut obligatoirement ajouter le mot clé protected ou private.

Une classe se déclare de cette façon:

class Car { 
    Engine: string; // par défaut, Engine est public 

    constructor(engine: string) { 
        this.Engine = engine; 
    } 
} 

Quelques remarques:

  • Un constructeur s’appelle toujours constructor().
  • Pour atteindre un membre à l’intérieur d’une classe, il faut préfixer avec le mot-clé this qui est obligatoire.
  • Sans utilisation d’opérateur de portée, un membre est public par défaut.

Instanciation

L’instanciation de la classe définie précédemment peut se faire de cette façon:

let bigCar: Car; 
bigCar = new Car('V8'); 
console.Log(bigCar.Engine); 

Plus directement, en définissant et en initialisant en une seule ligne:

let bigCar: Car = new Car('V8'); 

Plus simplement, on peut omettre le type (i.e. type inference):

let bigCar = new Car('V8'); 

Instanciation d’une classe en utilisant “typeof”

Il est possible d’instancier une classe avec le type d’une classe obtenue avec l’opérateur typeof:

let carMaker: typeof Car = Car; // Définition du type Car 
let carInstance : Car = new carMaker('V8'); // instanciation de la classe avec le type 

Notation abrégée des membres dans le constructeur

Une notation plus abrégée permet de déclarer des membres directement dans les paramètres du constructeur.

Par exemple:

class Car { 
    constructor(engine: string) { 
    } 
} 

La déclaration précédente indique que engine est une donnée membre privée de la classe Car (sans précision d’opérateur de portée, engine est privée).

Si on précise la portée de engine avec public:

class Car { 
    constructor(public engine: string) { 
    } 
} 

Avec cette définition, on peut donc écrire:

let littleCar = new Car('2CV'); 
console.Log(LittleCar.engine); 

Fonctions

Comme pour les données membres, les fonctions membres sont publiques par défaut et l’accès aux membres se fait obligatoirement avec this.

Par exemple:

class Car { 
    constructor(public engine: string) { 
    } 

    GetCarEngine(): string { 
        return 'The engine is: ' + this.engine; 
    } 
} 

L’utilisation de la fonction est classique:

let littleCar = new Car('2CV'); 
let carEngine = littleCar.GetCarEngine(); 

De même, on peut rendre la fonction privée en utilisant le mot clé private:

class Car { 
    constructor(public engine: string) { 
    } 
    
    DisplayCarEngine() { 
        console.log(this.GetCarEngine()); 
    } 
    
    private GetCarEngine(): string { 
        return 'The engine is: ' + this.engine; 
    } 
} 

Accesseurs

On peut utiliser des accesseurs à partir de ES5. Ces accesseurs peuvent être définis avec les mot clé get et set:

class Car { 
    private engine: string; 
    
    constructor(newEngine: string) { 
        this.engine = newEngine; 
    } 
    
    get power(): string { 
        return this.engine; 
    } 
    
    set power(newEngine: string) { 
        this.engine = newEngine; 
    } 
}  

L’utilisation des accesseurs se fait directement sans utiliser de parenthèses:

let bigCar = new Car('V12'); 
bigCar.power = 'V8'; 
console.log(bigCar.power); 

readonly

readonly a la même signification qu’en C#, il s’applique aux propriétés de façon à forcer son instanciation au moment de la déclaration ou au plus tard dans le constructeur. Il y aura une erreur de compilation si on tente d’affecter la propriété dans le corps de la classe en dehors du constructeur.

Par exemple:

Class Car { 
    readonly passengerCount: number; 
    readonly plateNumber = 'XXXXX'; 
    
    constructor(readonly engine: string) {  
        // on peut ajouter readonly directement dans le constructeur 
    
        this.number = 4; 
    } 
} 

Propriété statique

La notion de propriété statique permet d’affecter une valeur à une propriété d’une classe en dehors d’une instance de façon statique. Pour déclarer une propriété statique, il suffit d’utiliser le mot clé static. L’accès à la propriété se fait en précédant le nom de la propriété avec le nom de la classe.

Par exemple:

class Car {
    static BigEngine: string; 

    constructor(engine: string) { 
    } 
} 

On peut affecter une valeur à la propriété statique sans instancier la classe:

Car.BigEngine = 'V12'; 

La valeur de la propriété statique n’a pas de lien avec l’instance d’une classe:


let car1 = new Car(Car.BigEngine); 
let car2 = new Car('V8');

Atteindre une propriété statique avec “typeof”

On peut atteindre un propriété statique différemment en définissant un type puis en utilisant ce type pour atteindre la valeur statique:

let carMaker: typeof Car = Car;  // définition du type 
carMaker.BigEngine = 'V8'; // On peut atteindre la propriété statique 
console.log(Car.BigEngine);  // Les 2 notations sont équivalentes 

Héritage

Pour qu’une classe hérite d’une autre classe, il faut utiliser le mot clé extends. L’appel au constructeur de la classe se fait en utilisant le mot clé super().

Par exemple, si on définit la classe parent:

class Vehicle { 
    constructor(protected engine: string) { 
    } 
} 

On peut définir la classe Car héritant de Vehicle de cette façon:

class Car extends Vehicle { 
    constructor(carEngine: string) { 
        super(carEngine); 
    } 
    
    GetCarEngine(): string { 
        return this.engine; 
    } 
} 

“Overrider” une méthode

On peut overrider la méthode de la classe parente, pour ce faire il n’est pas nécessaire d’utiliser un mot clé particulier.

Par exemple:

class Vehicle { 
    protected passagerCount: number; 
    
    AddPassager(newPassagerCount: number): void { 
        this.passagerCount = this.passagerCount + newPassagerCounter; 
    } 
} 

On peut overrider directement la méthode AddPassager():

class Motobike extends Vehicle { 
    AddPassager(newPassagerCount: number): void { 
        let newCount = this.Passager + newPassagerCounter;  
        
        if (newCount <= 2) { 
            this.passagerCount = this.passagerCount + newPassagerCounter; 
        }
    }
} 

Si on veut appeler la méthode dans la classe parente, il faut utiliser l’instruction super.<nom de la méthode>(), par exemple:

class Motobike extends Vehicle { 
    AddPassager(newPassagerCount: number): void { 
        let newCount = this.Passager + newPassagerCounter;  
        
        if (newCount <= 2) { 
            super.AddPassager(newPassagerCount); 
        } 
    } 
} 

Effectuer un “cast”

La notion de cast existe en Typescript, elle correspond à la même notion qu’en C#.

Par exemple, si on définit les classes suivantes:

class A {} 

class B extends A {} 

Si on instancie la classe B de cette façon:

let genericInstance: A = new B(); // classInstance est de type A 

1ère notation:
On peut “caster” la classe A en B avec 2 notations qui sont équivalentes:

let specificInstance = <B>genericInstance; 

2e notation:
L’autre notation est:

let specificInstance = genericInstance as B; 

Classe abstraite

Les classes abstraites en Typescript sont proches de celles en C#:

  • On peut déclarer des méthodes abstraites dans une classe abstraite.
  • Les méthodes abstraites ne possédent pas d’implémentation.
  • Pour qualifier les méthodes abstraites et les classes abstraites, il faut utiliser le mot clé abstract.
  • On ne peut pas instancier directement une classe abstraite.

Par exemple, si on définit la classe abstraite suivante:

abstract class Vehicle { 
    protected passagerCount: number; 
    
    abstract AddPassager(newPassagerCount: number): void; 
} 

La classe dérivant de Vehicle doit implémenter la méthode abstraite AddPassager():

class Car extends Vehicle { 
    AddPassager(newPassagerCount: number): void { 
        this.passagerCount = this.passagerCount + newPassagerCount; 
    } 
} 

Generic

La notion de generic est la même qu’en C#, elle permet de définir une classe, une méthode ou une fonction en utilisant un type générique pour une propriété ou un paramètre.

Par exemple, dans le cas d’une classe:

class SimpleClass { 
    SimpleProperty: number; 
} 

Si on utilise un generic, on peut affecter un type générique à la propriété de façon à permettre d’utiliser d’autres types:

class SimpleClass<T> { 
    SimpleProperty: T; 
    
    constructor(simpleProperty: T) { 
        this.SimpleProperty = simpleProperty; 
    } 
}

On peut instancier la classe de cette façon, en précisant le type précis:

let instance: SimpleClass<string> = new SimpleClass<string>(); 

Et utiliser la propriété typée:

let simpleValue: string = instance.SimpleProperty;

Un generic peut aussi être utilisé seulement à l’échelle d’une méthode ou d’une fonction.

Par exemple:

function funcWithGeneric<T>(simpleParameter: T): T { 
    return simpleParameter; 
} 

Combinaisons de types

Il est possible en Typescript de définir des types qui sont des combinaisons d’autres types.

Intersection

L’intersection correspond à un type cumulant tous les membres d’autres types. Ainsi, si on considère le type A et le type B. Si on écrit le type A&B correspondant à l’intersection de A et B, on obtient un nouveau type cumulant les membres des types A et B.

Par exemple, si on définit les 2 types suivants:

class A {
    constructor(public memberA: number) {
    }
}

class B {
    constructor(public memberB: number) {
    }
}

On peut instancier un nouveau type A&B tel que:

let intersection: = <A&B> {};
intersection.memberA = 4;
intersection.memberB = 7;

Il est aussi possible d’utiliser la définition du type pour des arguments de fonctions, par exemple:

function UseIntersection(intersection: A&B): A&B {
    // ...
}

Union

L’union correspond à un type comprenant les membres d’un type ou de l’autre (mais pas les 2 à la fois comme pour l’intersection). Ainsi, si on considère le type A et le type B. Si on écrit le type A|B, on obtient un nouveau type comprenant soit les membres du type A, soit les membres du type B.

Par exemple, si on définit les 2 types suivants:

class A {
    constructor(public memberA: number) {
    }
}

class B {
    constructor(public memberB: number) {
    }
}

On peut définir une fonction utilisant le type A|B:

function UseUnion(unionInstance: A|B) {
    if (unionInstance instanceof A) {
        console.log("Type A" + unionInstance.memberA);
    } 
    else if (unionInstance instanceof B) {
        console.log("Type B" + unionInstance.memberB);
    }
}

Utilisation de “type guards”

Les protections de type (i.e. type guards) permettent de protéger le code en effectuant des vérifications sur le type des variables. Il existe plusieurs opérateurs.

is

Cet opérateur s’utilise dans le type de retour d’une fonction pour indiquer quel est le type précis dans le cas où on utilise un type union.

Par exemple, si on définit la fonction suivante:

function IsA(unionInstance: A|B): unionInstance is A {
    return unionInstance instanceof A;
}

Le retour de la fonction sera vrai seulement si unionInstance est de type A:

let aInstance: A = new A(3);
if (IsA(aInstance)) {
    // Toujours vrai
}

typeof

Cet opérateur permet de vérifier le type d’une variable en le comparant avec le nom du type sous forme de chaine de caractères. Cet opérateur n’est utilisable que pour les types string, number, boolean et symbol. La comparaison ne peut donc s’effectuer qu’avec les chaines de caractères "string", "number", "boolean" ou "symbol".

Par exemple, on peut écrire la fonction suivante qui va effectuer un certain nombre de tests:

function TestType(parameter: string|number|boolean) {
    if (typeof parameter === "string") {
        console.log("string");
    }
    else if (typeof parameter === "number") {
        console.log("number");
    }
    else if (typeof parameter === "boolean") {
        console.log("boolean");
    }
}

instanceof

Cet opérateur permet de tester le type réel d’une variable. La comparaison peut être effectuée sur n’importe quel type (il n’est pas limité comme typeof).

Par exemple, on peut écrire la fonction suivante:

function TestType(parameter: A|B) {
    if (parameter instanceof A) {
        console.log("A");
    }
    else if (parameter instanceof B) {
        console.log("B");
    }
}

Interface

Les interfaces en Typescript sont légèrement différentes des interfaces en C# car il est possible de satisfaire une classe sans utiliser de classe. En effet en Typescript, un object literal peut satisfaire une interface.

On peut définir dans une interface des propriétés et des méthodes, par exemple:

interface ICalculator { 
    calcValue: number; 
    
    Add(value: number): void; 
} 

Définir un objet quelconque satisfaisant une interface

Si on considère l’interface suivante:

interface ISize {  
    Height: number; 
    Width: number; 
} 

On peut définir un objet satisfaisant une interface directement de cette façon:

let dimension: ISize = <ISize>{}; 
dimension.Height = 6; 
dimension.Width = 9; 

Utilisation des interfaces avec des “object literals”

Un object literal peut satisfaire une interface, il n’est pas obligatoire d’utiliser des classes.

Par exemple, dans le cadre de l’interface définie plus haut, on peut définir un object literal de cette façon:

let calculatorExample = { 
    calcValue: 0, 
    Add: function(value: number): void { 
        this.calcValue = this.calcValue + value; 
    } 
}; 

Si on définit une fonction de cette façon:

function GetCalculator(): ICalculator { 
    // ...  
} 

On peut implémenter cette fonction en utilisant directement l’object literal:

function GetCalculator(): ICalculator { 
    let calculatorExample = { 
        calcValue: 0, 
        Add: function(value: number): void { 
            this.calcValue = this.calcValue + value; 
        } 
    }; 
    
    return calculatorExample;  
} 

Implicitement l’object literal va satisfaire l’interface ICalculator.

Ainsi pour savoir si l’object literal satisfait l’interface, le compilateur effectue les vérifications suivantes:

  • Il faut que toutes les propriétés et méthodes déclarées dans l’interface soient présentes dans l’object literal. Toutefois l’object literal peut comporter d’autres propriétés ou méthodes qui ne sont pas déclarées dans l’interface.
  • L’ordre de définition des propriétés ou des méthodes n’a pas d’importance. Il peut être différent de l’ordre utilisé dans l’interface.
  • Si le type d’une propriété ne vérifie pas la déclaration de l’interface, une erreur se produira.

Propriété optionelle

Une interface peut être définie avec des propriétés optionnelles. Elles sont indiquées avec la notation ?:.

Par exemple:

interface ISize { 
    Height: number; 
    Width: number; 
    Depth?: number; 
} 

Si une propriété est optionnelle, il n’est pas obligatoire de la définir dans l’object literal:

let dimension: ISize = { 
Height: 5, 
Width: 8 
}; 

Pour tester et vérifier que la propriété est définie, on peut utiliser la condition:

if (<instance de l'object literal>.<nom de la propriété>) { 
    // ... 
} 

Cette instruction permet de tester si la propriété a la valeur undefined.

Par exemple avec l’object literal dimension défini précédemment:

if (!dimension.Depth) { 
    dimension.Depth = 4; 
} 

readonly

L’utilisation de l’opérateur readonly permet de rendre un objet immutable en empêchant de pouvoir modifier la valeur de ses propriétés.

Ainsi si on définit l’interface suivante:

interface ISize { 
    readonly Height: number; 
    readonly Width: number; 
} 

Si on définit l’object literal suivant:

let dimension: ISize = { 
    Height: 5, 
    Width: 8 
}; 

Aucun changement de valeur ne sera possible sur les propriétés de l’object literal:

dimension.Height = 7; // entraîne une erreur 
dimension.Width = 2; // entraîne une erreur 

Utilisation des interfaces avec des classes

De façon plus classique que les object literals, on peut utiliser des interfaces avec des classes. Pour qu’une classe puisse satisfaire une interface, il faut utiliser le mot clé implements, par exemple:

interface ICalculator { 
    CalcValue: number; 
    Add(value: number): void; 
    Subtract(value: number): void; 
} 

class Calculator implements ICalculator { 
    CalcValue: number = 0; 
    
    Add(value: number): void { 
        this.CalcValue = this.CalcValue + value; 
    } 
    
    Subtract(value: number): void { 
        this.CalcValue = this.CalcValue - value; 
    } 
} 

Héritage entre interfaces

Une interface peut hériter d’une ou de plusieurs interfaces en utilisant le mot clé extends, par exemple:

interface A { 
    PropertyA: number; 
} 

interface B { 
    PropertyB: number; 
} 

interface C extends A, B { 
} 

Si une classe implémente C, elle doit satisfaire A et B:

class Example implements C { 
    PropertyA: number; 
    PropertyB: number; 
} 

Déclarer une signature de fonction sous forme d’interface

On peut utiliser une interface pour déclarer la signature d’une fonction. Par exemple, si on définit l’interface suivante:

interface AddFunction { 
    (operand1: number, operand2: number): number; 
}

On peut utiliser l’interface de cette façon:

let add: AddFunction; 
add = function(operand1: number, operand2: number): number { 
    return operand1 + operand2; 
} 

On peut omettre les indications de types car ils sont implicitement pris en compte:

let add: AddFunction; 
add = function(operand1, operand2) { 
    return operand1 + operand2; 
} 

Déclarer une fonction d’indexation sous forme d’interface

L’indexation étant une fonction particulière, on peut la déclarer de cette façon:

interface IndexStructure { 
    [index: number]: string; 
} 

L’interface peut être utilisée de cette façon:

let arrayStructure: IndexStructure; 
arrayStructure = ['Value1', 'Value2', 'Value3']; 
let value = arrayStructure[1]; 

Interface héritant d’une classe

Une interface peut hériter d’une classe, dans ce cas elle hérite de tous les membres de la classe sans l’implémentation correspondante.

Par exemple, si on définit la classe:

class Calculator { 
    CalcValue: number; 
    Operator(value: number): number { 
        return this.CalcValue + value; 
    } 
} 

Une interface peut dériver de la classe en utilisant le mot clé extends:

interface IOperator extends Calculator { 
    SetCalcValue(newValue: number): void; 
} 

Si une classe satisfait l’interface, elle doit respecter les déclarations de l’interface IOperator et de la classe Calculator et elle doit implémenter les déclarations correspondantes:

class Subtractor implements IOperator { 
    CalcValue: number; 
    Operator(value: number): number { 
        return this.CalcValue - value; 
    } 
    
    SetCalcValue(newValue: number): void { 
        this.CalcValue = newValue; 
    } 
} 

Instructions courantes

On explicite, dans cette partie, la syntaxe de quelques instructions couramment utilisées.

if…then…else

La syntaxe d’instructions if...then...else est assez classique et très similaires au C#:

if (<condition>) { 
    // instructions si la condition est vraie 
} 
else { 
    // instructions si la condition est fausse 
} 

On peut ajouter un if après le else si nécessaire:

if (<condition 1>) { 
    // instructions si la condition 1 est vraie 
} 
else if (<condition 2>) { 
    // instructions si la condition 2 est vraie 
} 

Opérateurs de comparaison

Les opérateurs de comparaison sont identiques à ceux utilisés en C#:

Opérateur Description
> Strictement supérieur à
< Strictement inférieur à
>= Supérieur ou égal à
<= Inférieur ou égal à
== Est égal à
!= N’est pas égal à

Opérateurs logiques

La syntaxe des opérateurs logiques est aussi semblable à celle utilisée en C#:

Opérateur Description
expr1 && expr2 (and) Vraie si les 2 expressions sont vraies
expr1 || expr2 (or) Vraie si au moins une expression est vraie
!expr (not) Permet d’indiquer l’inverse d’une expression logique

Comportement avec “null” et “undefined”

Pour tester si une variable est définie (i.e. égale ou non à undefined), il suffit de tester directement:

if (<nom de la variable>) { 
    // instructions si la variable est différente de "undefined" 
} 

Par exemple:

let numValue : number; // variable égale à 'undefined' 
if (numValue) { 
    // instructions si numValue est défini 
} 
else { 
    // instructions si numValue n'est pas défini 
} 

Le comportement est le même avec null:

let numValue : number = null; // variable égale à 'null' 
if (numValue) { 
    // instructions si numValue est nul 
} 
else { 
    // instructions si numValue n'est pas nul 
} 

Pour éviter la confusion est une valeur égale à null ou undefined, il est préférable d’effectuer une comparaison avec null explicitement:

if (numValue == null) { 
    // ... 
} 

Avec le paramétrage --strictNullChecks

Dans le cas où on utilise le paramétrage --strictNullChecks, l’utilisation d’une variable avec une valeur undefined provoque une exception de type ReferenceError. Dans ce cas, pour vérifier si une variable est définie (i.e. égale à undefined), il faut effectuer la comparaison:

if (typeof variableName !== 'undefined') { 
    // ... 
} 

null et undefined sont des types à part entière toutefois du point de vue de la comparaison, ils sont considérés comme égaux:

  • undefined == undefined est vrai
  • null == undefined est vrai
  • null == null est vrai

undefined ne signifie pas valeur nulle, chaine vide ou valeur fausse:

  • undefined == 0 est faux
  • undefined == '' est faux
  • undefined == false est faux

De même pour null:

  • null == 0 est faux
  • null == '' est faux
  • null == false est faux

Différences entre “==” et “===”

En Javascript, l’opérateur de comparaison == n’a pas la même signification que ===:

  • ==: compare 2 opérandes en tentent d’effectuer des conversions de type entre les variables si nécessaire.
    Par exemple en Javascript:

    • 2 == "2" est vrai car la 2e opérande est convertie en type number pour effectuer la comparaison avec la 1ère opérande.
    • "2" == 2 est aussi vrai
    • 0 == "" est vrai car "" correspond à 0 après la conversion
    • "" == 0 est aussi vrai
    • "0" == "" est faux, dans ce cas il n’y pas de conversion de type.
    • "" == "0" est aussi faux.
  • ===: compare 2 opérandes sans effectuer de conversion au préalable.
    Par exemple:

    • 2 === "2" est faux.
    • "2" === 2 est faux.
    • 0 === "" est faux.
    • "" === 0 est faux.
    • "0" === "" est faux.
    • "" === "0" est faux.

Dans le cadre de Typescript, la vérification du type des variables entraîne des erreurs de compilation si on tente de comparer des variables dont le type n’est pas le même. Toutefois, il est préférable d’utiliser l’opérateur === pour explicitement effectuer les comparaisons sans conversion de type en Javascript.

L’opposé de l’opérateur == est != et l’opposé de === est !==.

Boucles

La plupart des instructions utilisées en C# pour définir des boucles, fonctionnent aussi en Typescript avec une syntaxe semblable.

Boucle “for”

Pour la boucle for, la syntaxe est semblable au C#:

for (let i=0; i< 10; i++) { 
    // ... 
} 

Boucle “while”

Par exemple:

let i: number = 0; 
while (i < 10) { 
    // ... 
    i++; 
} 

Boucle “do…while”

De même que la boucle while:

let i: number = 0; 
do 
{ 
    // ... 
    i++; 
} 
while (i < 10) 

break/continue

L’utilisation de l’instruction break permet de stopper l’exécution d’une boucle comme en C#.

Par exemple:

let i: number = 0; 
while (i < 10) { 
    // ... 
    if (i == 5) break; 
    i++; 
} 

On peut utiliser break avec tous les types de boucles.

Contrairement à break, continue permet de passer à l’itération suivante sans exécuter les instructions se trouvant dans la boucle et après l’instruction continue.

Par exemple:

for (let i=0; i< 10; i++) { 
    if (i == 5) continue; // si continue est exécuté, le reste de la boucle 
                                 // n'est pas exécuté
    // ... 
} 

De même, on peut utiliser continue avec tous les types de boucles.

Boucle infinie

On peut définir des boucles infinies avec la syntaxe suivante pour une boucle for:

let i: number = 0; 
for(;;) { 
    if (i > 10) break; 
    i++: 
} 

Dans le cas de boucle while, la syntaxe est plus classique, par exemple:

while (true) { 
    // ... 
} 

for…of

Pour itérer les éléments d’un tableau ou les caractères d’une chaine de caractères, on peut utiliser l’instruction for...of qui a l’avantage de nécessiter moins de code à écrire.

Par exemple, pour itérer les éléments d’un tableau:

let simpleArray = [7, 4, 8]; 
for (let element of simpleArray) { 
    console.log(element); 
} 
Si le code cible correspond à une version antérieure à ES6

Pour les versions antérieures à ES6, le code javascript généré d’une boucle for...of correspond à une boucle for itérant en utilisant la longueur de la structure (i.e. propriété length):

La boucle précédente devient, après génération en javascript:

let simpleArray = [7, 4, 8]; 
for (let i=0; i< element.length;i++) { 
    console.log(element[i]); 
} 

Pour que l’exécution fonctionne, il faut que la structure possède une propriété length. Dans la cas contraire, l’exécution provoquera une erreur.

Ne pas confondre “for…of” et “for…in”

Contrairement à for...of, for...in permet d’itérer sur les index d’une structure et non sur les éléments de la structure. Ainsi, pour un tableau, si on écrit:

let simpleArray = [7, 4, 8]; 
for (let element in simpleArray) { 
    console.log(element); 
} 

Le résultat sera 0, 1, 2 et non 7, 4, 8.

for…in

L’intérêt de for...in est de pouvoir itérer facilement suivant les membres d’une classe.

Par exemple, si on définit la classe suivante:

class SimpleClass {
    constructor(public a: number, public b: number, public c: number) 
}

Si on exécute le code suivant, on peut itérer sur les membres de la classe:

let simpleClass: SimpleClass = new SimpleClass(3, 2, 1);

for (let member in simpleClass) {
    console.log(member + ': ' + (<any>simpleClass)[member]);
}

On obtient le résultat:

a: 3
b: 2
c: 1

try…catch

La gestion d’exception se fait avec les instructions try...catch. La syntaxe est semblable au C#: on entoure le code avec une instruction try...catch et on lance des exceptions en utilisant throw.

Par exemple:

try { 
    // ... 
} 
catch(e) { 
    console.log(e); 
}

Pour lancer une exception, on peut écrire:

throw new Error('Message exception'); 

Il existe des types d’erreurs plus précis que Error. Ces types héritent de la classe Error:

  • RangeError: survient dans le cas où on utilise un index en dehors de l’intervalle des éléments d’une structure.
  • ReferenceError: survient si on utilise une référence invalide.
  • SyntaxError: dans le cas d’une erreur de syntaxe.
  • TypeError: ce type d’erreur se produit si le type utilisé n’est pas valide.

Pour conclure…

Comme indiqué plus haut, le but de cet article était d’expliciter certains éléments de la syntaxe de base de Typescript. Les sujets comme l’export de classes, les modules, les interfaces et l’utilisation de bibliothèque Javascript n’ont pas été traités car ils feront l’objet d’un article futur.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Interesting reads – Async/Await

Some interesting articles regarding async/await in .NET:

General explanations

Performance matters

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Installation de Typescript sur Debian/Ubuntu

Le but de cet article est d’installer et de permettre d’exécuter du code Typescript sur Debian GNU/Linux 9.

Il est possible d’utiliser Typescript avec Visual Studio sur Windows qui est un environnement familier pour un développeur .NET. Toutefois d’une façon générale, l’installation et l’implémentation de Typescript se fait plus couramment avec Node.js qui permet d’utiliser ce langage dans des environnements plus hétérogènes.

D’abord, il faut installer Node.js et le compilateur Typescript avec NPM (i.e. Node Package Manager). Ensuite, il est nécessaire de préparer l’environnement pour permettre d’exécuter du code javascript généré à partir du code Typescript.

Installation de Node.js

La première étape consiste à installer Node.js et NPM (i.e. Node Package Manager). Ces 2 éléments s’installent en une seule fois:

  1. Il faut télécharger la version LTS (i.e. Long Term Support) de Node.js sur https://nodejs.org/en/.
    Pour Debian, il faut télécharger la version “Linux Binaries” en 64-bit.
    Dans notre cas, la version LTS est 8.10.0.
  2. Décompresser le fichier dans un répertoire local:
    user@debian:~% tar xf <nom du fichier .tar.xz> 
    

    Dans notre cas:

    user@debian:~% tar xf node-v8.10.0-linux-x64.tar.xz 
    

    L’option x permet d’indiquer qu’on veut extraire l’archive et f indique qu’il s’agit d’un fichier.

  3. Déplacer le répertoire dans un emplacement utilisable par tous les utilisateurs. Pour ce faire, il faut d’abord se connecter en tant qu’utilisateur root:
    user@debian:~% su 
    root@debian:/home/user/% mkdir /usr/node.js 
    

    On déplace ensuite, le répertoire décompressé de Node.js:

    root@debian:/home/user/% mv node-v8.10.0-linux-x64 /usr/node.js 
    
  4. En tant qu’utilisateur normal, on peut ajouter le chemin de Node.js à la variable d’environnement PATH en éditant le fichier .bashrc avec une éditeur de texte.
    Avec vi, on peut exécuter la commande suivante:

    user@debian:~% vi /home/<répertoire de l'utilisateur>/.bashrc 
    

    A la fin du fichier, il faut ajouter les lignes suivantes (pour passer en mode édition dans vi, il faut taper [i]):

    export NODEJS_HOME=/usr/node.js/node-v8.10.0-linux-x64 
    export PATH=$PATH:NODEJS_HOME/bin 
    

    On enregistre en tapant [Echap] puis [:wq] et [Entrée].

Si on ouvre un nouveau terminal, on devrait pouvoir exécuter la commande suivante:

user@debian:~% npm version 

Installation de Typescript

Le compilateur Typescript peut être installé facilement avec npm en suivant les instructions sur la page: https://www.typescriptlang.org/index.html#download-links.

Il faut, ainsi, exécuter l’instruction suivante:

user@debian:~% npm install –g typescript 

Cette instruction permet d’installer Typescript de façon globale (avec l’option -g) dans le répertoire:

<répertoire d'installation de Node.js>/lib/node_modules/typescript/bin

Dans notre cas:

/usr/node.js/node-v8.10.0-linux-x64/lib/node_modules/typescript/bin

Le compilateur est accessible en tapant directement:

user@debian:~% tsc –v 

Préparation de l’environnement de développement

Les étapes suivantes vont permettre d’utiliser le compilateur pour produire du code Javascript exécutable sur la plupart des browsers.

Création d’un projet

Pour créer un projet, il faut exécuter les étapes suivantes:

  1. Créer le répertoire du projet:
    user@debian:~% mkdir Helloworld 
    user@debian:~% cd helloworld
    
  2. Créer un fichier package.json en exécutant l’instruction:
    user@debian:~% npm init –y 
    
  3. On installe le module typescript pour le développement en exécutant:
    user@debian:/home/user/helloworld/% npm install typescript --save-dev 
    
  4. On installe les références de types de node:
    user@debian:/home/user/helloworld/% npm install @types/node --save-dev 
    
  5. Création d’un fichier tsconfig.json contenant la configuration du compilateur pour le projet Typescript:
    user@debian:/home/user/helloworld/% npx tsv --init 
    

    Le fichier doit se présenter de cette façon:

    { 
      "compilerOptions": { 
        "outDir": "./public/", 
        "target": "es5",      
        "module": "es6",   
        "noImplicitAny": true, 
        "esModuleInterop": true, 
        "allowJs": true 
      } 
    } 
    

    La configuration du compilateur contenue dans ce fichier indique les éléments suivants:

    • "outDir" indique le répertoire de sortie de la compilation,
    • "target" précise la version ECMAScript cible,
    • "module": la génération du code des modules se fait de façon native (ECMAScript 6),
    • "noImplicitAny" indique au compilateur de retourner une erreur si une instruction contient le type any,
    • "esModuleInterop" permet de rajouter des directives pour que le code soit compatible avec l’écosystème Babel,
    • "allowJs" indique que les fichiers Javascript peuvent être compilés.
  6. On crée, ensuite, un fichier Typescript index.ts contenant le code à exécuter:
    class Startup { 
        public static main(): number { 
            console.log('Hello World'); 
            return 0; 
        } 
    } 
    
    Startup.main(); 
    
  7. On peut ensuite installer webpack qui permet de générer un bundle avec le code Javascript:
    user@debian:/home/user/helloworld/% npm install webpack --save-dev 
    
  8. Installation de ts-loader qui est nécessaire pour l’exécution de webpack:
    user@debian:/home/user/helloworld/% npm install ts-loader --save-dev 
    
  9. Installation de webpack-dev-server qui est un mini-serveur de développement pour exécuter le code Javascript à chaque fois qu’on modifie le code Typescript:
    user@debian:~% npm install webpack-dev-server --save-dev 
    
  10. Pour que l’on puisse exécuter une commande webpack, il faut installer le package webpack-cli. Le prérequis pour installer ce package est de pouvoir exécuter une commande Git. On va donc installer Git en utilisant une instruction apt-get de Debian:

    Il faut se connecter en tant qu’utilisateur root pour exécuter cette instruction:

    root@debian:/home/user/% apt-get install git 
    

    On installe ensuite le package webpack-cli en tant qu’utilisateur normal dans le répertoire du projet:

    user@debian:/home/user/helloworld/% npm install webpack-cli --save-dev 
    
  11. On complète le fichier de configuration de webpack en créant le fichier webpack.config.js:
    const path = require('path'); 
    
    module.exports = { 
       entry: './index.ts', 
       module: { 
         rules: [ 
             { 
               test: /\.tsx?$/, 
               use: 'ts-loader', 
               exclude: /node_modules/ 
             } 
          ] 
       }, 
       resolve: { 
         extensions: [ '.tsx', '.ts', '.js' ] 
       }, 
       output: { 
         filename: 'app.js', 
         path: path.resolve(__dirname, 'public') 
       } 
    }; 
    

    Dans ce fichier, la ligne entry: './index.ts' indique que le fichier d’entrée est index.ts. Le fichier de sortie contenant le bundle est indiqué par les lignes suivantes:

    output: { 
         filename: 'app.js', 
         path: path.resolve(__dirname, 'public') 
       } 
    
  12. Il faut ensuite compléter le fichier package.json pour qu’il contienne les lignes suivantes:
    {
      "name": "helloworld", 
      "version": "1.0.0", 
      "description": "", 
      "main": "./public/main.html", 
      "scripts": { 
        "build": "webpack ./webpack.config.js --mode development", 
        "start": "webpack-dev-server ./webpack.config.js --content-base ./public --mode development" 
      }, 
      "keywords": [], 
      "author": "", 
      "license": "ISC", 
      "devDependencies": { 
        "@types/node": "^9.4.7", 
        "ts-loader": "^4.0.1", 
        "typescript": "^2.7.2", 
        "webpack": "^4.1.1", 
        "webpack-cli": "^2.0.11", 
        "webpack-dev-server": "^3.1.1" 
      } 
    } 
    

    Les lignes les plus importantes sont celles contenant les instructions suivantes:

    • Pour effectuer l’étape de build:
      "build": "webpack ./webpack.config.js --mode development"
    • Pour démarrer le serveur webpack-dev-server:
      "start": "webpack-dev-server ./webpack.config.js --content-base ./public --mode development" 
      
  13. Enfin dans le répertoire public, on crée un fichier HTML index.html contenant le code suivant:
    <html> 
      <body> 
        <div id="root"></div> 
        <script src="./app.js"></script> 
      </body> 
    </html> 
    

    Ce code permet d’appeler le code contenu dans le fichier Javascript public/app.js.

Exécution du compilateur Typescript

Après installation des différents composants, on peut effectuer l’étape de build avec webpack de façon à construire le fichier bundle contenant le code Javascript.

L’étape de build peut être exécuté avec la commande suivante:

user@debian:/home/user/helloworld/% npm run build 

A la suite de cette étape, un répertoire se nommant public a été créé et il contient le fichier app.js avec le code javascript.

Pour lancer le serveur webpack-dev-server, il faut exécuter la commande suivante:

user@debian:/home/user/helloworld/% npm run start 

Cette commande lance le serveur sur le port 8080:

> helloworld@1.0.0 start /home/mat/Documents/helloworld 
> webpack-dev-server ./webpack.config.js --content-base ./public --mode development 

ℹ 「wds」: Project is running at http://localhost:8080/ 
ℹ 「wds」: webpack output is served from / 
ℹ 「wds」: Content not from webpack is served from /home/mat/Documents/helloworld/public 
ℹ 「wdm」: Hash: 908628beca4c5ae4881d 

En se connectant sur la page http://localhost:8080 avec un browser, on doit voir une page blanche.

La page n’est pas tout à fait vierge. Si on affiche la console du browser en tapant le raccourci [Ctrl] + [Maj] + [i], on doit voir le message:

Hello World  

Si on modifie le code Typescript dans le fichier index.ts, à chaque enregistrement le code Javascript est mis à jour dans public/app.js.

Installation de Visual Studio Code

Visual Studio Code est un éditeur qui permet de facilement développer en Typescript. Pour installer Visual Studio Code, il suffit de suivre les instructions suivantes (qui sont explicitées sur https://code.visualstudio.com/docs/setup/linux:

Il faut exécuter les étapes suivantes en tant qu’utilisateur root:

  1. Editer le fichier /etc/apt/sources.list.d/vscode.list en tapant:
    root@debian:/home/user/% vi /etc/apt/sources.list.d/vscode.list  
    

    Passer en mode insertion en tapant [i] et ajouter la ligne:

    deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main  
    

    Il faut enregistrer et quitter en tapant la touche [echap], en écrivant :wq puis [entrée].

  2. On met à jour la liste des packages présents dans les différentes sources de APT en tapant:
    root@debian:/home/user/% apt-get update  
    
  3. On installe le package code en tapant:
    root@debian:/home/user/% apt-get install code  
    
  4. On peut démarrer Visual Studio Code en tapant:
    user@debian:~% code  
    

Quelques remarques concernant le développement en Typescript en utilisant Visual Studio Code sont explicitées sur cette page: https://code.visualstudio.com/docs/languages/typescript.

Compiler un projet avec Visual Studio Code

Pour compiler un projet Typescript avec VS Code:

  1. D’abord, il faut ouvrir le projet en cliquant sur File ⇒ Open Folder ou avec le raccourci clavier [Ctrl] + [k] et [Ctrl] + [o].
  2. Il faut sélectionner le répertoire contenant le fichier Typescript index.ts.
  3. Pour compiler, cliquer sur Tasks ⇒ Run Build Tasks… puis écrire:
    tsc: build - tsconfig.json
    

    On peut voir les détails de la compilation dans le terminal intégré.

  4. Pour que le code Typescript soit compilé à chaque fois qu’un fichier est sauvegardé, il faut cliquer sur Tasks ⇒ Run Build Tasks… puis écrire:
    tsc: watch - tsconfig.json
    

    A ce moment, chaque fichier Typescript est monitoré et la compilation est effectuée à chaque sauvegarde.
    On peut voir le code Javascript correspondant à une fichier Typescript en cliquant sur View ⇒ Split Editor. Il faut ensuite sélectionner:

    • le fichier Typescript index.ts dans une partie de l’écran et
    • le fichier Javascript correspondant public/index.js dans l’autre partie

    Pour le fichier Typescript index.ts contenant le code:

    class Startup { 
        public static main(): number { 
            console.log('Hello World'); 
            return 0; 
        } 
    } 
    
    Startup.main(); 
    

    On obtient le code Javascript suivant:

    var Startup = /** @class */ (function () {
        function Startup() {
        }
        Startup.main = function () {
            console.log('Hello World');
            return 0;
        };
        return Startup;
    }());
    Startup.main();
    
Le code de cet article se trouve sur Github: HelloworldTS.

En conclusion…

Cet article ne visait qu’à introduire l’implémentation de code en Typescript. L’installation est plutôt rapide et surtout elle est sensiblement la même d’une plateforme à l’autre.
D’autres articles futurs rentreront davantages dans les détails en explicitant la syntaxe Typescript, la gestion des modules et l’ajout de composants Javascript externes à un projet Typescript.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Gestion des packages APT sur Debian/Ubuntu

La gestion des packages Debian est un sujet abordé à maintes reprises sur internet toutefois il est facile d’oublier ces commandes et il faut régulièrement les rechercher. Le but de cet article est donc de lister la plupart de ces commandes pour pouvoir se les remettre en mémoire plus facilement quand on veut s’en servir.

Brièvement, les packages Debian peuvent être gérés au moyen de 3 commandes:

  • apt-get
  • apt-cache
  • dpkg

apt-get et apt-cache sont des outils de haut niveau qui permettent d’effectuer une gestion intelligente de packages en prenant en compte les dépendances, les versions et les éventuels conflits entre packages. Ils utilisent des sources pour les packages. La plupart du temps, ces sources sont des repositories sur internet. Dans la majorité des cas, il est préférable d’utiliser ces commandes car elles garantissent une cohésion dans l’ensemble des packages installés sur le système.

apt-get permet d’éditer la base du gestionnaire de packages en effectuant des opérations d’installation, de suppression, de mise à jour ou de nettoyage (pour supprimer des packages inutiles). Chaque opération se fait en prenant en compte les éventuelles dépendances entre packages. Pour un package donné, cet outil va installer les dépendances correspondantes si nécessaire. apt-cache effectue des opérations sur la base en lecture seule comme la recherche de packages ou l’affichage d’informations concernant un package.

dpkg est un outil plus bas niveau qui permet d’effectuer des opérations basiques sur un package particulier: installation, suppression ou mise à jour d’un package. Cet outil peut être utilisé quand on considère un package qui n’a pas de dépendances ou quand on souhaite installer des packages qui ne sont pas présents dans la distribution et dont on possède le fichier .deb.

Base du gestionnaire de packages

Dans un premier temps, la gestion des packages se fait en utilisant des sources. On peut renseigner et éditer ces sources à partir du fichier /etc/apt/sources.list. Ce fichier contient les liens des sources pour les packages d’une distribution utilisant le système de packages APT (principalement Debian et Ubuntu).

Ce fichier contient un couple de lignes indiquant les sources avec une syntaxe particulière.

  1. Dans l’entête de chaque ligne est indiquée le type de packages concernés:
    • deb: cette ligne indique la source pour les packages binaires.
    • deb-src: cette ligne indique la source pour les packages contenant du code source (utilisés pour effectuer du développement).
  2. Ensuite, chaque ligne indique l’URL de la source
  3. L’indication suivante correspond au nom de la distribution ou de la catégorie de version:
    • Nom de la distribution: si c’est le nom de la distribution qui est indiquée alors seuls les packages de cette distribution seront installés. Si la distribution devient obsolète et est remplacée, les packages ne seront pas mis à jour.

      Les versions des distributions Debian sont:

      Version Nom de la distribution
      Buster Prochaine version
      9 Stretch Version stable actuelle
      8 Jessie
      7 Wheezy
      6 Squeeze
      5.0 Lenny
      4.0 Etch
      3.1 Sarge
      3.0 Woody
      2.2 Potato
      2.1 Slink
      2.0 Hamm
    • Catégorie de version: lorsqu’on utilise la catégorie de version, on permet au gestionnaire de packages de faire évoluer les packages installés en fonction de l’avancement de la version de la distribution:
      • oldstable: version stable précédent la version actuelle.
      • stable: version stable actuelle. Les packages correspondent aux packages stable de la version “Stretch”.
      • testing: packages en phase de test.
      • unstable: packages en cours de développement.
  4. Les indications suivantes indiquent l’ensemble de packages que l’on souhaite télécharger. On peut indiquer un ou plusieurs ensembles:
    • main: packages se conformant aux Principes du logiciel libre selon Debian (Debian Free Software Guidelines ou DFSG) sans autres dépendances.
    • contrib: ensemble de packages se conformant aux DFSG ayant des dépendances en dehors de l’ensemble main.
    • non-free: ensemble de packages qui ne se conforment pas aux DFSG.

    Dans le cas où on veut les packages provenant des 3 ensembles, il faut écrire les ensembles à la suite:

    main contrib non-free
    

Par défaut, le contenu du fichier /etc/apt/sources.list pour la version actuelle “Stretch” est:

deb http://deb.debian.org/debian stretch main  
deb-src http://deb.debian.org/debian stretch main 
 
deb http://deb.debian.org/debian stretch-updates main  
deb-src http://deb.debian.org/debian stretch-updates main  

deb http://security.debian.org/debian-security/ stretch/updates main  
deb-src http://security.debian.org/debian-security/ stretch/updates main 

D’autres fichiers contenant des sources peuvent être renseignés dans le répertoire:

/etc/apt/sources.list.d 

Ce répertoire est généralement utilisé pour indiquer d’autres sources que les sources officielles Debian.

A chaque modification de /etc/apt/sources.list ou d’un fichier dans le répertoire /etc/apt/sources.list.d, il faut mettre à jour la liste de packages disponibles en exécutant en tant qu’utilisateur root:

apt-get update

Les clés de confiance utilisées par APT pour vérifier les fichiers téléchargés se trouvent dans le répertoire:

/etc/apt/trusted.gpg.d

Il faut ajouter une clé dans ce répertoire pour chaque source ajoutée dans le répertoire /etc/apt/sources.list.d.

apt-get

Utilisateur root

Pour exécuter les commandes apt-get, il faut se connecter en tant qu’utilisateur root en exécutant sous Debian:

user@debian:~% su

Sous Ubuntu, pour se connecter en tant qu’utilisateur root, on peut exécuter:

user@ubuntu:~% sudo su

Il est aussi possible de précéder les commandes avec:

user@ubuntu:~% sudo <commande à exécuter>

Pour afficher l’aide concernant cette commande:

user@debian:~% man apt-get

Les commandes principales concernant apt-get sont les suivantes:

Installation de packages

Pour installer un package:

root@debian:~% apt-get install <nom du package> 

Il est possible d’installer plusieurs packages en les écrivant à la suite:

root@debian:~% apt-get install <nom du package 1> <nom du package 2> 

Suppression de packages

Pour supprimer un package:

root@debian:~% apt-get remove <nom du package> 

Pour supprimer un package et ses fichiers de configuration:

root@debian:~% apt-get purge <nom du package> 

Mise à jour de la liste de packages

Pour mettre à jour la liste des packages disponibles dans la base locale du gestionnaire de packages:

root@debian:~% apt-get update 

Il faut exécuter cette commande à chaque fois qu’on met à jour les fichier /etc/apt/sources.list ou un fichier se trouvant dans le répertoire /etc/apt/sources.list.d/.

Mettre à jour des packages

Pour mettre à jour tous les packages installés (dernière version installée à partir des sources):

root@debian:~% apt-get upgrade 

Pour migrer vers une version supérieure de Debian:

root@debian:~% apt-get dist-upgrade 

Pour supprimer les packages inutiles (c’est-à-dire qui ne pourront plus être téléchargés à partir des sources)

root@debian:~% apt-get autoclean 

Le cache de packages

Pour supprimer les packages se trouvant dans le cache (c’est-à-dire dans le répertoire /var/cache/apt/archives):

root@debian:~% apt-get clean 

apt-cache

Il n’est pas nécessaire de se connecter en tant qu’utilisateur root pour exécuter cette commande.

Pour afficher l’aide concernant cette commande:

user@debian:~% man apt-cache

Pour chercher un package dans la base des packages disponibles, utilisez la commande suivante :

user@debian:~% apt-cache search <liste de mots clés> 

Par exemple, pour chercher un compilateur Fortran, tapez:

user@debian:~% apt-cache search fortran compiler 

Informations sur un package

Pour afficher les caractéristiques et la description d’un package:

user@debian:~% apt-cache show <nom du package> 

Liste des packages disponibles

Pour afficher la liste des packages disponibles pour le système:

user@debian:~% apt-cache pkgnames 

dpkg

Comme indiqué plus haut, dpkg doit être utilisé lorsqu’on veut installer un package sous forme de fichier .deb et qu’on ne souhaite pas utiliser les sources indiquées dans le fichier /etc/apt/sources.list.

dpkg peut aussi être utilisé pour afficher des informations relatives à un package installé même s’il a été installé avec apt-get.

Pour afficher l’aide concernant cette commande:

user@debian:~% man dpkg

Installation

Pour installer un package:

root@debian:~% dpkg –i <fichier .deb du package> 

Ou

root@debian:~% dpkg --install <fichier .deb du package> 

Comme dpkg ne gère pas les dépendances, si on souhaite installer un package avec des dépendances, il faut installer le package et ses dépendances en même temps. On indique, ainsi, tous les fichiers .deb à la suite:

root@debian:~% dpkg –i <package1> <package2> 

Suppression

Pour supprimer un package:

root@debian:~% dpkg -r <nom du package> 

Ou

root@debian:~% dpkg --remove <nom du package> 

Pour supprimer un package avec ses fichiers de configuration:

root@debian:~% dpkg --purge <nom du package> 

Reconfiguration d’un package

Pour réconfigurer un package déjà installé:

root@debian:~% dpkg-reconfigure <nom du package> 

Informations concernant un package

Pour indiquer le package qui a installé un fichier particulier:

user@debian:~% dpkg -S <chemin du fichier> 

Par exemple, pour savoir quel est le package qui a installé /usr/bin/vim:

user@debian:~% dpkg -S /usr/bin/vim 

Afficher la liste des fichiers installés par un package:

user@debian:~% dpkg -L <nom du package> 

Afficher la liste des fichiers contenus dans un fichier .deb

user@debian:~% dpkg --contents <fichier .deb> 

Afficher les données relatives à un package:

user@debian:~% dpkg –s <nom du package>  

Informations sur les packages installés

Afficher la liste des packages installés sur le système:

user@debian:~% dpkg –l 

On peut filtrer en utilisant des termes à chercher:

user@debian:~% dpkg –l <termes à chercher> 

On peut aussi utiliser des wildcards:

user@debian:~% dpkg –l "v*" 

Supprimer des “locks” en cas d’erreur

Quand on exécute dpkg ou apt-get on peut tomber sur l’erreur suivante:

E: Could not get lock /var/lib/dpkg/lock - open (11 Resource temporarily unavailable) 
E: Unable to lock the administration directory (/var/lib/dpkg/) is another process using it?

Cette erreur est due à une erreur survenue précédemment lors de l’exécution d’une commande dpkg ou apt-get. Suite à cette erreur, le ou les locks posés n’ont pas été supprimés. Pour réutiliser apt-get ou dpkg, il faut supprimer ces locks.

Pour les supprimer, il faut se connecter en tant qu’utilisateur root et exécuter:

root@debian:~% rm /var/lib/apt/lists/lock 
root@debian:~% rm /var/cache/apt/archives/lock 
root@debian:~% rm /var/lib/dpkg/lock 
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page