Loupe

[Cordova] VS 2015 + Ionic 2 + Angular 2 + TypeScript

Parmis toutes les façons de créer un projet Cordova, d’utiliser une librairie comme Angular 2, il est parfois difficile de s’y retrouver. Encore plus lorsque l’on souhaite utiliser Ionic – une surcouche à la CLI de Cordova apportant une structure plus stricte et un framework UI multiplateformes – ainsi que notre éditeur fétiche : Visual Studio 2015.

En effet, ce dernier permet de créer des applications hybrides Cordova, mais après avoir essayé à plusieurs reprises d’y intégrer Ionic 2, je trouve que ce n’est pas si simple et que cela mérite bien un petit article.

Pour info, voici les versions des outils et librairies utilisés dans cet article :

  • Visual Studio 2015 Update 1
  • Extension Visual Studio Tools for Apache Cordova : 14.0.60106.1
  • nodeJS : v0.12.2
  • NPM : 2.7.4
  • Cordova CLI 6.0.0
  • Ionic 2 : 2.0.0-beta.17
  • Angular2 : 2.0.0-beta.2
  • Ionic Framework : 2.0.0-alpha.55

Créer son projet Ionic

Tout d’abord, il convient d’installer la CLI de Ionic pour pouvoir créer son projet. Si ce n’est pas déjà fait, il faut bien entendu installer cordova aussi :

npm install -g cordova
npm install -g ionic@beta

Le mieux est d’installer Cordova et Ionic en global d’où l’usage de l’option -g.

Ceci fait, un projet Ionic se crée avec, la CLI Ionic justement, qui vient s’interfacer au-dessus de celle de Cordova :

ionic start <nom du projet> <template> --v2 --ts

Remplacer <nom du projet> par le nom du dossier et du projet dans lequel seront créés les fichiers du projet, <template> est optionnel et peut être remplacé par une des valeurs suivantes :

  • blank
  • tutorial
  • sidemenu
  • tabs

L’option --v2 permet de cibler ionic2 tantdis que l’option --ts permet de générer un projet avec des fichiers TypeScript (à la place de fichier JavaScript sans l’option).

En l’état, le projet Ionic est plus ou moins fonctionnel et utilise Angular 2. Il est possible de lancer la commande suivante pour lancer le projet :

ionic run <platform>

Le paramètre <platform> doit être remplacé par la plateforme cible (windows, ios ou android par exemple).

Le problème principal est que l’on n’utilise pas Visual Studio pour éditer ou lancer la build (qui se fait avec la commande build). Plus gênant encore selon moi, on ne dispose pas du debugger de Visual Studio… Si cela vous suffit, vous pouvez toutefois vous arrêter là et continuer avec l’éditeur VS Code. Pour les autres, lisez la suite !

Intégrer son projet Ionic dans un projet Visual Studio

Une option méconnue (en tout cas de moi) de Visual Studio permet de créer un projet à partir de fichiers existants. Pour cela, utiliser l’entrée File > New > Project From Existing Code :

image

Il faut ensuite choisir le type de projet “Apache Cordova”, renseigner le chemin du projet créé avec la CLI Ionic, le nom du projet et valider. Visual Studio créé ensuite un fichier sln et un fichier jsproj qui vont permettre de compiler et exécuter le projet à travers Visual Studio, en théorie.

Et c’est bien là tout le problème, car si vous tentez de compiler le projet directement, vous verrez certainement une belle erreur de Visual Studio. En effet, celui-ci utilise le fichier tsconfig.json généré par la CLI de Ionic pour compiler les fichiers TypeScript du projet. Toutefois, ce fichier tsconfig.json n’est pas prévu pour être utilisé par Visual Studio. En effet, la CLI Ionic compile déjà les fichiers TypeScript, en plus des fichiers SaaS et en profite même pour créer un bundle de notre code et des librairies utilisées. Bref, Ionic s’en sort bien et il vaut mieux le laisser faire le job.

Pour désactiver la compilation des fichier TypeScript par Visual Studio (oui oui puisque Ionic le fait), il faut éditer le fichier jsproj généré. Il s’agit d’un fichier XML donc un simple éditeur de texte suffit. On y trouve notamment la ligne suivante :

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\ApacheCordovaTools\vs-mda-targets\Microsoft.TypeScript.MDA.targets" />

Il faut donc la commenter afin de retirer les appels aux fonctions de compilations des fichiers TypeScript :

<!--<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\ApacheCordovaTools\vs-mda-targets\Microsoft.TypeScript.MDA.targets" />-->

En l’état, si l’on exécute la fonction de build avec la CLI de Ionic, il est possible d’exécuter l’application sur Android et iOS avec Visual Studio. Toutefois, celui-ci ne fait pas le job d’aller piocher les fichiers de code du dossier app pour les compiler et les copier dans le répertoire www. Pour cela, il faut faire appel à la CLI Ionic… On peut créer une tache Gulp qui se charge de faire ça, ou utiliser les hooks Cordova. Un hooks est un script appelé par la CLI Cordova lors d’un évènement particulier. Le fichier hooks/README.md donne un aperçu de ce qu’il est possible de faire. Dans notre cas, il serait intéressant de se mapper sur l’évènement before_prepare et d’exécuter un script appelant la CLI Ionic. Si l’on en croit la documentation des hooks Cordova, on pourrait créer un dossier nommé before_prepare dans le répertoire hooks et y placer un fichier JavaScript qui serait automatiquement appelé. Pour ma part, cela ne fonctionne pas et ouvre simplement mon programme par défaut pour lire les fichiers JavaScript… J’utilise donc une autre technique consistant à déclarer le hook dans le fichier config.xml comme ceci :

<hook type="before_prepare" src="hooks/ionic2vs_glue_$_ionic_build.js" />

Le fichier ionic2vs_glue_$_ionic_build.js contient le code suivant :

#!/usr/bin/env node

var util = require('./hook_utils.js');

module.exports = function (context) {

    console.log('ionic2vs_glue_$_ionic_build.js is running...');
    var fs = context.requireCordovaModule('fs');
    var path = context.requireCordovaModule('path');
    var exec = context.requireCordovaModule('child_process').exec;
    var Q = context.requireCordovaModule('q');
    var deferral = new Q.defer();

    var projectRoot = context.opts.projectRoot;
    var cmdLine = context.cmdLine;
    var words = util.parseCmd(cmdLine); 
    var cbi = util.extractCordovaBuildInfo(words);

    if (!cbi.platform) {
        // non vs build...
        return 0;
    }

    var ionicPlatform = cbi.platform;
    if (cbi.platform.indexOf("Windows-") === 0) {
        ionicPlatform = "windows";
    } 

    var after = function(error, stdout, sderr) {
        if (error) {
            console.log("error");
            deferral.reject(error);
        } else {
            console.log("ok!");
            deferral.resolve();
        }
    };

    var process = exec("ionic build " + ionicPlatform, function(error, stdout, sderr) {
        if (error) {
            console.log("error");
            deferral.reject(error);
        } else {
            console.log("ok!");
            deferral.resolve();
        }
    });
    process.stdout.on("data", function(data) {
        console.log(data);
    });
    process.stderr.on("data", function(data) {
        console.error(data);
    });

    return deferral.promise;
};

Ce script est dépendant d'un autre module, hook_utils. Ce dernier permet de parser les paramètres de la build Cordova lorsqu’elle est initiée par Visual Studio et de récupérer les informations telles que l’architecture, la plateforme et la configuration visée. Cela permet entre autre de ne pas redéclencher le hook lorsque c’est Ionic qui initie la build. Voici le code du fichier hook_utils :

module.exports = {
    parseCmd: function (cmd) {
        var length = cmd.length;
        var ix = 0;
        var simpleQuoteOpened = false;
        var doubleQuoteOpened = false;
        var words = [];
        var word = "";
        for (var ix = 0; ix < length; ix++) {
            var c = cmd[ix];
            if (c === "'") {
                simpleQuoteOpened = !simpleQuoteOpened;
            } else if (c === '"') {
                doubleQuoteOpened = !doubleQuoteOpened;
            } else if (c === ' ') {
                if (simpleQuoteOpened || doubleQuoteOpened) {
                    // noop
                } else {
                    words.push(word);
                    word = "";
                    continue;
                }
            }
            word += c;
        }

        if (word.length > 0) {
            words.push(word);
        }

        return words;
    },
    extractCordovaBuildInfo: function (words) {

        var platform = null;
        var configuration = null;
        var arch = null;

        for (var i = 0; i < words.length; i++) {
            var word = words[i];
            if (word === "--platform") {
                platform = words[i++ + 1];
            } else if (word === "--configuration") {
                configuration = words[i++ + 1];
            } else if (word === "--x86") {
                arch = "x86";
            } else if (word === "--x64") {
                arch = "x64";
            } else if (word === "--ARM") {
                arch = "ARM";
            } else if (word === "--AnyCPU") {
                arch = "AnyCPU";
            }
        }

        return {
            platform: platform,
            configuration: configuration,
            arch: arch
        }
    }
};

Si tout se passe bien, à chaque fois que Visual Studio fera appel à la CLI de Cordova pour générer le projet, le hook fera appel à la CLI Ionic pour générer le projet avant. Toutefois quelques problèmes persistent. Le premier est que la build pour la plateforme windows (Windows-x86, Windows-ARM, Windows-x64 et Windows-AnyCPU) échoue et indique que le fichier appxrecipe n’a pas été trouvé. En effet, celui-ci n’est pas généré dans le bon répertoire lorsque Ionic compile le projet. On peut encore une fois créer un hook pour remédier à cela. Celui-ci ne devra s’exécuter que pour la plateforme Windows et devra donc être renseigné comme suit dans le fichier config.xml :

<platform name="windows">
  <hook type="after_compile" src="hooks/ionic2vs_glue_$_copy_appxrecipe.js" />
</platform>

Voici le code de ce hook :

#!/usr/bin/env node

var util = require('./hook_utils.js');

module.exports = function (context) {

    console.log('ionic2vs_glue_$_copy_appxrecipe.js is running...');
    var fs = context.requireCordovaModule('fs');
    var path = context.requireCordovaModule('path');
    var Q = context.requireCordovaModule('q');
    var deferral = new Q.defer();

    var projectRoot = context.opts.projectRoot;
    var cmdLine = context.cmdLine;
    var words = util.parseCmd(cmdLine); // parseCmd(cmdLine);
    var cbi = util.extractCordovaBuildInfo(words);

    if (!cbi.platform) {
        // non vs build...
        return 0;
    }

    if (cbi.platform.indexOf("Windows") === 0) {
        var dest = path.join("bin", cbi.platform, cbi.configuration);
        var appxrecipe = path.join("platforms/windows/build/windows", cbi.configuration.toLowerCase(), cbi.arch);
        
        (function copy(test_path, other_tests) {
            fs.readdir(test_path, function(err, files) {
                if (!files) {
                    if (other_tests.length > 0) {
                        copy(path.join(test_path, other_tests.pop()));
                    } else {
                        console.log("no files...");
                    }
                    return;
                }
                console.log(files);
                console.log(files.length);
                if (err) {
                    console.log(err);
                } else {
                    var close_hit = 0;
                    var error_hit = 0;
                    var expec_hit = files.length;
                    var after = function() {
                        if (close_hit + error_hit >= expec_hit) {
                            if (error_hit > 0) {
                                console.log("error on copy...");
                                deferral.reject("error on copy...");
                            } else {
                                console.log("ok!");
                                deferral.resolve();
                            }
                        }
                    }
                    files.forEach(function(file) {
                        var wr = fs.createWriteStream(path.join(dest, file.indexOf(".appxrecipe") >= 0 
                            ? "CordovaApp.windows80.build.appxrecipe" 
                            : file));
                        wr.on("error", function(err) {
                            console.log(err);
                            error_hit++;
                            after();
                        });
                        wr.on("close", function() {
                            close_hit++;
                            after();
                        });
                    
                        fs.createReadStream(path.join(test_path, file)).pipe(wr);
                    });
                }
            });
        })(appxrecipe, ["win8.1"]);

        return deferral.promise;
    }

    setTimeout(function() {
        deferral.resolve();
    }, 0);
    return deferral.promise;
};

Ceci fait, la build pour la plateforme Windows fonctionne parfaitement. Toutefois, lorsqu’on exécute le projet, on se rend compte atRuntime qu’une erreur survient… Android et iOS ne sont pas impactés par ce bug, mais sur Windows seul un écran blanc apparait. Si l’on consulte la fenêtre “JavaScript Console”, on peut y voir une erreur de syntaxe (utilisez window.location.reload() si vous ne le voyez pas), qui vient de la librairie Angular2. En effet, une erreur lors de la compilation des directives fait que le code évaluer n’est pas compilable. Un petit hack JavaScript permet de régler cela, qu’il faut injecter dans le fichier index.html avant l’inclusion du script app.bundle.js :

<script src="cordova.js"></script>
<script>
    if (Function.prototype.name === undefined) {
        Object.defineProperty(Function.prototype, "name", {
            get: function () {
                var str = this.toString();
                return str.substr("function ".length, str.indexOf("(") - "function ".length);
            }
        });
    }
</script>
<script src="build/js/app.bundle.js"></script>

Pas d’inquiétude, ce script permet simplement de rajouter une propriété “name” sur les objets de type “function”. En effet, angular2 se base sur cette propriété pour générer du code dynamique, mais dans le cas du moteur de IE11, c’est le retour de toString() qui est utilisé, et qui ne va pas du tout…

Voilà, l’application Ionic 2 est prête ! Pour la prochaine fois, vous pouvez aussi vous contenter du plugin suivant pour la gestion des hooks : https://github.com/thomasouvre/ionic2vs2015_glue.git. J’y référence le code vu ici et il y a plus de chance qu’il soit à jour que ce billet.

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus