Packager son application AngularJS avec NPM & Browserify

 

Introduction

Aujourd’hui, pour développer une application web, il est difficile d’échapper à l’utilisation de frameworks MV* ou MVW (Model View Wathever) tels que AngularJS, Ember ou Backbone. En effet, ces derniers permettent une abstraction de la manipulation directe du DOM et apportent une meilleure organisation du code.

De plus, les nouveaux standards HTML5 (application cache, indexedDb etc.) nous offrent la possibilité de développer des applications web accessibles sans connexion internet. Cela nous amène à déplacer la logique métier de nos applications côté client.

Le code source côté client va ainsi être composé de nombreux fichiers (JS, CSS, HTML). Afin de nous éviter l’inclusion manuelle des références à ces fichiers dans notre « index.html », nous allons devoir packager (« rassembler ») ces derniers en un ou plusieurs fichiers. Pour cela nous allons passer par une phase de « pseudo-compilation » de nos sources.

Cependant, nous pouvons tirer partie de cette phase supplémentaire pour traduire notre code d’un langage à un autre. Cela nous permettra ainsi d’utiliser des langages ou de nouveaux standards pas forcément interprétables par le navigateur tels LESS et ES2015 par exemple.

Voyons donc aujourd’hui cette phase de « pseudo-compilation » permettant de « packager » une application AngularJS 1.5.x écrite en ES2015.

Outils

work-864960_1920

Task runner : NPM

Le Task runner est l’outil qui va nous servir à orchestrer l’ensemble des tâches (copie de fichiers, minification, compilation des LESS, packaging des sources JS etc.) à effectuer pour « construire » notre application.

NPM

NPM est le gestionnaire de package officiel de Node.js sur lequel nous pouvons retrouver toutes les librairies utiles au développement Front-end (AngularJS, Bootstrap, etc.). Il devient donc petit à petit un outil indispensable dans notre workflow. De plus, il peut être utilisé comme « Task runner » grâce à sa directive Script.

Bien sûr, d’autres outils existent tels que Grunt, ou Gulp (qui se targue d’exploiter les capacités des streams Node.js). Mais après avoir eu l’occasion de travailler un peu avec tous, ma préférence s’est tournée vers la simplicité de  NPM qui par son utilisation en tant que Task runner apporte :

  • une diminution du nombre de dépendances du projet,
  • une diminution du nombre de fichier de config (les tâches sont définies dans le package.json et non dans un fichier externe g***.conf.js),
  • une centralisation de la configuration du projet dans le package.json

De plus, tout ce que l’on peut faire avec Grunt ou Gulp est faisable avec NPM.

Le bundler : Browserify

Le bundler est l’outil qui aura pour rôle d’assembler tous les fichiers de notre application en un ou plusieurs fichiers JavaScript afin d’éviter l’inclusion multiple de références à nos scripts dans notre index.html. Pour cela, nous pouvons soit utiliser un simple outil de concaténation de fichier ou bien utiliser des outils comme Webpack ou Browserify qui tire partie de la gestion de module de Node.js.

Browserify

L’idée de Browserify est de permettre aux développeurs front d’organiser leur code comme il le ferait pour développer leur code Back-end via l’utilisation de la gestion de module Node.js.
On aura donc un fichier JavaScript principal dans lequel on importera les modules dont il dépend qui peuvent eux même dépendre d’autres modules etc.
Browserify interprétera ces chargements/imports de modules pour construire le fichier final.

Bien sûr nous aurions également pu utiliser Webpack, mais dans un souci de centraliser la configuration du projet, ma préférence se tourne vers Browserify.
La configuration de ce dernier peut s’effectuer directement dans le fichier package.json contrairement à Webpack qui se base sur un fichier webpack.conf.js.

Browserify s’utilise de la manière suivante dans la console :

browserify input.js -o ouput.js

Il prend en entrée un fichier et va résoudre à partir de ce fichier tous nos imports/exports en se basant sur la gestion de module Node.js pour construire un fichier de sortie intégrant tous les fichiers sources nécessaires.

Le traducteur : Babel

Comme évoqué précédemment, la phase de pseudo-compilation va nous permettre également de traduire notre code. Ainsi nous allons pouvoir par exemple écrire notre code en utilisant les nouveaux standards JavaScript (ES2015) non interprétable pour l’instant par les navigateurs.

Babel

Babel est la librairie capable de d’interpréter et de traduire un code JavaScript écrit avec une syntaxe particulière (ES2015, JSX, Flow) en un code JavaScript avec une syntaxe interprétable par les navigateurs d’aujourd’hui.

Notre application AngularsJS

create-865017_1920

Construisons une petite application AngularJS qui aura pour simple fonctionnalité l’affichage de la mention « Hello world » . Pour cela nous allons définir un component AngularJS <hello-world/>Ce component fera appel à un service pour obtenir le texte à afficher.

Version très simple

Cette application pourrait s’écrire très simplement de la manière suivante :

Arborescence

public
   / index.html
   / script.js
   / angular.js

index.html

<!DOCTYPE html>
<html ng-app="my-app">
 <head>
  <meta charset="UTF-8">
  <title>My app</title>
  <script type="text/javascript" src="angular.js"></script>
  <script type="text/javascript" src="script.js"></script>
 </head>

 <body>
  <hello-world/>
 </body>
</html>

script.js

(function(){
 // Création de notre module
 angular.module(‘my-app’, [])
        // Déclaration de notre service
        .service( ‘helloWorldService’, 
                   function(){ 
                    this.sayHello = function(){ return 'HELLO!'; }
                   }
        )
        // Déclarion de notre component
        .component(‘helloWorld’, 
                    { template: ‘<h1> {{$ctrl.text}} </h1>’,
                      controller: function(helloWorldService){
                                   this.text = helloWorldService.sayHello();
                                  }
                    }
       );
})()

Cependant, vous serez d’accord avec moi que si cette application venait à grandir elle deviendrait difficilement maintenable.

Je vous propose d’en faire un version plus « modulaire » écrite avec les standards ES2015 en séparant en plusieurs fichiers les services, components et templates pour une meilleure visibilité. Mais également en définissant un module par répertoire, cela aura l’avantage de faciliter la réutilisation d’une partie de votre application .

Version modulaire

Arborescence

public // répertoire qui contiendra fichiers static (html,js,css)
  / index.html
  / bundle.js
src // répertoire contenant nos fichiers sources
  / app
    / helloWorldModule // repertoire contenant notre module hello wolrd
      / helloWorld.service.js
      / helloWorld.component.js
      / helloWorld.template.html
      / index.js // fichier contenant la définition de notre module helloWorldModule
   / index.js
   / index.html

src/index.html

Commençons par créer notre fichier index.html dans lequel nous allons inclure :

  • la directive ng-app pour réaliser le bootstrap (« amorçage ») de notre module AngularJS principale « my-app »,
  •  la balise permettant d’inclure notre fichier JavaScript bundle.js
  •  l’élément <hello-world/> qui correspond à notre component AngularJS.
<html ng-app="my-app"/>
 ...
 <body>
  <hello-world/>
  <script src="bundle.js" type="text/javascript">
 </body>
 ..
</html>

src/app/index.js

Ce fichier est le point d’entrée de notre application. C’est ce dernier que nous fournirons à Browserify pour reconstruire notre application.

import angular from 'angular';
import helloWorldModule from './helloWorldModule';

angular.module('my-app', [ helloWorldModule.name ]);

« import.. from .. » est la syntaxe ES2015 permettant d’importer des modules.

Nous allons importer :

  • angular (récupérer à partir de notre gestionnaire de package NPM),
  • notre module helloWorldModule .

Notons que :

import helloWorldModule from './helloWorldModule';

Est équivalant à

import helloWorldModule from './helloWorldModule/index.js';

Déclarons donc dans notre  fichier index.js notre module principale « my-app » qui dépend de notre module helloWorldModule.

La déclaration de modules dépendants se fait par nom c’est pourquoi nous avons « helloWorldModule.name » qui permet d’obtenir simplement le nom de notre module importé dans la variable helloWorldModule.

Notons ici, que nous aurions pu directement importer et lier nos éléments AngularJS à notre module principal. Le contenu du fichier aurait était le suivant :

import angular from 'angular';
import helloWorldService from './helloWorldModule/helloWorld.service.js';
import helloWorldComponent from './helloWorldModule/helloWorld.component.js'

angular.module('my-app', [ ])
       .service('helloWorldService', helloWorldService)
       .component('helloWorld', helloWolrdComponent);

Cependant à l’inverse de la première solution, cette dernière ne facilite pas la réutilisation d’une fonctionnalité de l’application (ici l’affichage de  « Hello world« ) et démultiplie le nombre d’imports. Dans notre première solution nous n’aurons qu’à importer notre fichier qui déclare notre sous-module.

src/app/helloWorldModule/index.js

Définissons maintenant  un  nouveau module  correspondant au module helloWolrdModule dont dépend notre module principal. Tous nos éléments AngularJS permettant de réaliser notre fonctionnalité (affichage de « Hello world ») seront liés à ce module.

import angular from 'angular';
import helloWorldService from './helloWorld.service.js';
import helloWorldComponent from './helloWolrd.component.js';

angular.module('helloWorldModule', [])
       .service('helloWolrdService', helloWorldService )
       .component('helloWorld', helloWorldComponent);

Dans ce fichier nous déclarons  notre module helloWorldModule ainsi que  les services et components liés à ce module.

Le fichier helloworld.service.js devra donc exporter un objet instanciable pour cela nous définirons une classe ES2015.

Et le fichier helloWolrd.component.js devra exporter un simple objet.

src/app/helloWorlModule/helloWorl.service.js

Notre service correspond donc à un classe contenant la méthode sayHello();

export default class {
 sayHello(){
  return "Hello world !";
 }
}

Notons l’utilisation de l’expression « export default » qui permet d’exporter un unique élément sans avoir à spécifier de nom. Le nom sera spécifié lors de l’import du module. L’import s’en retrouve ainsi facilité puisqu’il s’effectuera de la manière suivante :

import HelloWorldService from '/helloWorl.service.js';
let instance = new HelloWorldService();

src/app/helloWorlModule/helloWorld.component.html

Notre component AngularJS correspond simplement à un objet que nous exporterons.

import myTemplate from './helloWolrd.template.html';

let component = {
 template: myTemplate,
 controller: function(helloWorldService){
  let ctrl = this;
  ctrl.text = helloWorldService.sayHelloWorld();
 }
};

export default component;

Remarquons ici l’import du template. Nous verrons par la suite que Browserify est capable d’interpréter les imports de fichier HTML pour en faire des chaînes de caractères.
Cela nous permet déclarer nos templates dans des fichiers HTML et de les inclure sous forme de chaîne de caractères dans notre fichier JavaScript.

src/app/helloWorlModule/helloWorld.template.html

Définissons le template de notre component AngularJS :

<h1> {{$ctrl.text}}</h1>

Voila, nous avons terminé d’écrire notre application. Nous allons maintenant définir les scripts permettant d’effectuer les différentes tâches pour « packager » notre application.

Scripts NPM

balance-865087_1920

Comment ça marche ?

Avec NPM il est possible d’utiliser la directive script dans le fichier package.json afin de définir des lignes commandes à exécuter lors de l’appel du script NPM.

Un script NPM se lance dans la console de la manière suivante :

npm run [Nom du script];

Ainsi un autre script pourra appeler un script NPM en utilisant la commande précédente.

Dans notre cas, un script correspondra à une tâche.

Comment chaîner nos tâches ?

Afin de réaliser notre package nous allons avoir besoins de chaîner nos tâches. Pour cela plusieurs solutions existent :

  • L’utilisation de & :
{
  ...
  "script": {
    "script_a" : "...",
    "script_b" : "...",
    "start" : "npm run script_a &amp; npm run script_b"
    ...
  }
  ...
}
  • L’utilisation de mots clé ‘pre’ et ‘post’ dans les noms des scripts :
{
  ...
  "script": {
    "script_a" : "...",
    "script_b" : "...",

    "prestart": "npm run script_a",
    "start": "npm run script_b",
    ...
  }
  ...
}

Dans les deux cas, l’appel de la commande npm run start exécutera  le script_a puis le script_b.

Définition de nos scripts

Définissons nos scripts dans le fichier package json :

{
  ...
  "script": {

    "clean": "rimraf -r public/*",
    "copy-index" : "npc src/index.html public/index.html",
    "build-app" : "browserify src/app/index.js -o public/bundle.js",

    "prebuild": "npm run clean",
    "build": "npm run copy-index & npm run build-app",
    ...
  }
  ...
}

Afin de packager notre application nous n’aurons plus qu’à exécuter la commande npm run build.

Cette dernière aura pour effet de:

  1. Exécuter le script clean qui va nettoyer notre répertoire public,
  2. Exécuter le script copy-index qui va copier le fichier index.html dans public,
  3. Exécuter le script build-app qui fera appelle à Browserify pour packager notre application dans le fichier bundle.js.

Configuration de Browserify

Nous allons maintenant devoir configurer Browserify afin qu’ il traduise nos sources en Javascript interprétable par le navigateur et qu’il interprète  nos « modules HTML » en chaînes de caractères  lors de la phase de packaging de l’application, .

Pour cela nous allons utiliser les directives suivantes du fichier package.json :

  • browserify  qui va nous permettre de définir les transformations à effectuer par Browserify. Ici nous utiliserons  les transformations babelify (qui se base sur la librairie babel) avec le preset es2015 (pour traduire nos scripts es2015), et stringify,
  •  stringify qui va nous permettre de paramétrer Stringify (librairie utilisée par Browserify pour traduire des modules en chaînes de caractères) pour lui indiquer de n’effectuer la transformation que sur des fichiers HTML.
{
  ...
  "browserify" : {
    "transform" : [

      [ "babelify", { "presets": ["es2015"] }],
      ["stringify"]
    ]
  },
  "stringify" : {
    "appliesTo" : { "includeExtensions": [".html"] },
    "minify" : false
  },
  ...
}

Pour finir

Eh hop, votre appli AngularJS est prête ! Toutes les informations (dépendances, workflow etc) sont centralisées dans votre package.json. Le découpage des dossiers en modules nous permettant la réutilisation simple d’une fonctionnalité.

Concernant NPM,  l’avantage de l’utiliser comme Task Runner est qu’une simple tâche n’est que l’exécution d’une ligne commande Shell/Batchs. Il devient alors facile d’intégrer dans notre workflow quelques étapes supplémentaire pour automatiser  le déploiement par exemple (utile pour de l’intégration continue).

Cependant, avant de l’écriture des scripts NPM, il conviendra de bien se poser la question  « sur quels environnements seront exécutés mes scripts? » afin d’éviter des problèmes de comptabilité des commandes utilisées.

 

Quelques références

Laisser un commentaire