Angular 2 - Ecrire son application avec TypeScript

Cet article va se reposer sur l'application "superheroes" que j'avais écrit pour les articles suivants:

Ici, nous allons voir comment écrire notre application en se basant directement sur TypeScript. En effet, lors des trois articles, nous avions utilisés JSPM afin d'avoir à notre disposition un équivalent de Bower, mais pour faire de ES6 / ES2015.

D'ailleurs, le projet JSPM utilisait TypeScript pour transpilé le code. Sauf que ce dernier se faisait à la volée, lors du chargement de la page et que cette dernière prenait du temps du coup à s'afficher.

De plus, je trouvais que le ticket d'entrée d'utilisation de JSPM n'est pas non plus négligeable. Du coup, je me suis posé la question d'utiliser directement TypeScript, ce que cela pouvait donner.


Avant de voir ce qu'il faut modifier pour la configuration de notre projet, voyons ce que nous devrons modifier dans notre code.

Les modifications du code

Premièrement, l'import des frameworks externes dans le fichier "index.jx" ne sera plus nécessaire.
Deuxièmement, il faudra remplacer l'extension ".js" de nos fichiers en ".ts".

Ensuite, il va falloir modifier un tout petit peu notre code. En effet, par défaut, TypeScript est sensible au "typage fort". Il va essayer de s'assurer au moment de la compilation que les paramètres que nous passons sont bien du bon type.

Du coup, dans le fichier "app/services/superheroes.ts", il va fallor modifier la méthode "createDefaultSuperHero". Nous passons de


/** * Generate an empty @{SuperHero} * * @method * @returns {SuperHero} */createDefaultSuperHero() {
    return new SuperHero(Date.now(), null, null, null);
}


A


/** * Generate an empty @{SuperHero} * * @method * @returns {SuperHero} */createDefaultSuperHero() {
    return new SuperHero(Date.now().toString(), null, null, null);
}


Autrement, le compilateur TypeScript risque de nous lever des erreurs. Nous pourrions passer un paramètre pour cela, mais faisons pour l'instant avec ce mode là. Ce qui nous force quelque part à bien respecter le contrat de nos méthodes / constructeurs (en l’occurrence ici, que le premier paramètre du constructeur "SuperHero" qui attend un identifiant soit bien de type "string").

Ensuite, TypeScript préfère que les propriétés des classes soient déclarés en amont du constructeur.

Du coup, nous allons modifier le fichier "app/models/superhero.ts". Nous passons de:


/** * @class SuperHero */export default class SuperHero {
    /**     * @constructor     * @param {string} [uuid='1']     * @param {string} [firstName='Bruce']     * @param {string} [lastName='Wayne']     * @param {number} [age=36]     */    constructor (uuid = '1', firstName = 'Bruce', lastName = 'Wayne', age = 38) {
        /**         * @property {string} uuid         */        this.uuid = uuid;

        /**         * @property {string} firstName         */        this.firstName = firstName;

        /**         * @property {string} lastName         */        this.lastName = lastName;

        /**         * @property {number} age         */        this.age = age;
    }

    /**     * Return the string representation of a user     * @returns {string}     */    toString() {
        return `(${this.uuid}) ${this.firstName} ${this.lastName}: age ${this.age}`;
    }
}


A


/** * @class SuperHero */export default class SuperHero {
    /**     * @property {string} uuid     */    uuid = null;

    /**     * @property {string} firstName     */    firstName = null;

    /**     * @property {string} lastName     */    lastName = null;

    /**     * @property {number} age     */    age = null;

    /**     * @constructor     * @param {string} [uuid='1']     * @param {string} [firstName='Bruce']     * @param {string} [lastName='Wayne']     * @param {number} [age=36]     */    constructor (uuid = '1', firstName = 'Bruce', lastName = 'Wayne', age = 38) {
        this.uuid = uuid;
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    /**     * Return the string representation of a user     * @returns {string}     */    toString() {
        return `(${this.uuid}) ${this.firstName} ${this.lastName}: age ${this.age}`;
    }
}

Ce qui est plus rigoureux, on se l'accorde.


L'outillage à mettre en oeuvre

Maintenant, nous allons voir ce qui est nécessaire de faire pour que notre projet puisse fonctionner avec TypeScript.

Tout d'abord, il faut créer un fichier "package.json" où nous importerons TypeScript, mais surtout les dépendances nécessaires pour notre projet à savoir:
  • Angular2
  • ZoneJs
  • RxJs
  • SystemJs
  • ...
Ce qui est intéressant ici est que nous utilisons uniquement NPM comme gestionnaire de dépendances, et non pas un outil externe. Ce qui simplifie la compréhension du projet et le travail.

Maintenant, déclarons le fichier "package.json" comme suit:

{
  "name": "angular2-superheroes-typescript",
  "version": "1.0.0",
  "description": "An angular 2.0 application for superheroes",
  "main": "index.js",
  "scripts": {
    "compile": "npm run tsc",
    "tsc": "node ./node_modules/typescript/bin/tsc",
    "test": "echo \"Error: no test specified\" && exit 1"  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "eslint": "1.10.3",
    "typescript": "1.7.5"  },
  "dependencies": {
    "angular2": "2.0.0-beta.1",
    "es6-promise": "3.0.2",
    "es6-shim": "0.33.13",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.0",
    "systemjs": "0.19.17",
    "zone.js": "0.5.10"  }
}


Ensuite, nous allons générer un fichier qui se nomme "tsconfig.json". Ce dernier sera plutôt utilisé lorsque nous voudrons produire / compiler notre application.

Pour le générer, tapez la ligne de commande suivante:

npm run tsc -- --init --target es5 --sourceMap --experimentalDecorators --emitDecoratorMetadata


Comme vous pouvez le voir, nous demandons à TypeScript
  • De transpiler en ES5
  • En générant les SourceMaps
  • En activant les features expérimentales d'ES6 / ES7
  • En activant les décorateurs JavaScript (c'est à dire, les annotations)

Du coup, nous obtenons le fichier "tsconfig.json" suivant (nous verrons pour le modifier dans un second temps):

{
    "compilerOptions": {
        "target": "es5",
        "sourceMap": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "module": "commonjs",
        "noImplicitAny": false,
        "outDir": "built",
        "rootDir": "."    },
    "exclude": [
        "node_modules"    ]
}


Travaillant avec WebStorm, je fais activer un "watcher" afin de compiler mon code à chaque fois que ce dernier est changé (évitant de langer le watcher de TypeScript).

Pour ce faire, je me dirige vers "Settings/Tools/File Watchers", j'en créé un TypeScript, où je vais entrer:

1) Le répertoire à observer (notion de "scope")



2) La ligne de commande à lancer lorsqu'un de ces fichiers sera modifié. En gros:
--module commonjs --sourcemap --experimentalDecorators --emitDecoratorMetadata $FilePath$




Et pour finir ?

La dernière étape va se situer dans notre fichier "index.html". En effet, c'est là-dedans que nous importions avec JSPM:
  • Des dépendances externes
  • Un gros fichier de configuration pour SystemJS (vraiment gros)
  • Que nous chargions notre application

Cela ressemblé à ça:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Angular 2.0 application for superheroes (with ES6 and JSPM)</title>

        <!-- Import styles -->        <link rel="stylesheet" href="styles/index.css" />

        <!-- Base imports -->        <script type="text/javascript" charset="utf-8" src="jspm_packages/system.js"></script>
        <script type="text/javascript" charset="utf-8" src="jspm_packages/npm/zone.js@0.5.10/dist/zone.js"></script>
        <script type="text/javascript" charset="utf-8" src="config.js"></script>

        <!-- Import the application -->        <script type="text/javascript" charset="utf-8" async="async">
            System
                .import('./app/index')
                .then(function () {
                    console.info('Application loaded');
                })
                .catch(function (ex) {
                    console.error(ex.stack || ex);
                });
        </script>
    </head>

    <body>
        <superheroes-app></superheroes-app>
    </body>
</html>


Maintenant, regardons la version avec TypeScript:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Angular 2.0 application for superheroes (with ES6 and JSPM)</title>

        <!-- Import styles -->        <link rel="stylesheet" href="styles/index.css" />

        <!-- Base imports -->        <script type="text/javascript" charset="utf-8" src="./node_modules/systemjs/dist/system.js"></script>

        <!-- Import the application -->        <script type="text/javascript" charset="utf-8" async="async">
            System.config({
                // we want to import modules without writing .js at the end                'defaultJSExtensions': true,
                // the app will need the following dependencies                'map': {
                    'angular2': 'node_modules/angular2',
                    'rxjs': 'node_modules/rxjs'                }
            });

            System                .import('./app/index')
                .then(function () {
                    console.info('Application loaded');
                })
                .catch(function (ex) {
                    console.error(ex.stack || ex);
                });
        </script>
    </head>

    <body>
        <superheroes-app></superheroes-app>
    </body>
</html>

La différence ici se situe au niveau de la configuration pour SystemJs: Nous pouvons simplement indiquer les modules externes afin d'y accéder dans notre code. Et grâce à cette simple configuration, nous n'avons pas à changer le nom des imports dans nos fichiers: tout est compatible !

Et notre application de super-héros fonctionne aussi bien :)

Commentaires

Posts les plus consultés de ce blog

ISO: liens & outils utiles

NodeJs et SSL: une petite analyse

Créer sa commande slack en quelques minutes