Yeoman a été créé dans l’optique de faciliter l’initialisation de web apps JavaScript, en regroupant un ensemble de 3 utilitaires tournant sous Node.js :
- Yo : initialise la structure de la web app.
- Grunt : déclenche les phases de test, preview et build de la web app.
- Bower : assure la gestion des dépendances.
Express est un framework d’application web fonctionnant lui aussi sous Node.js.
Lorsque l’on génère le squelette d’une web app angular, Yo propose de gérer le design avec Twitter Bootstrap et des feuilles de style SASS. Les feuilles de style SASS, bien que très pratiques, doivent être compilées en CSS. Un framework open-source Ruby fait ça très bien et se nomme Compass.
Ruby est installé par défaut sous OS X, mais il est préférable, comme sous d’autres OS d’avoir recours à RVM, un outil en ligne de commande qui installe, gère et permet de travailler facilement avec plusieurs environnements Ruby différents.
Autre outil particulièrement pratique et complémentaire à RVM, il s’agit de Homebrew, un gestionnaire de packages fonctionnant sous Ruby.
L’association de RVM et Homebrew offre la possibilité de créer un environnement de travail personnalisé sans être amené à polluer par accident les librairies systèmes.
Pour installer et utiliser RVM il existe un client graphique très pratique : JewelryBox. Il offre une vision globale de tous les environnements Ruby disponibles et/ou installés ainsi que des gems associées.
Donc, avant de pouvoir s’amuser avec Yeoman pour générer une web app Angular/Twitter Bootstrap/SASS, il y a un petit travail de configuration d’environnement à réaliser.
Mise à jour de XCode
Passer à la version 4.6.1 et installer les « Command line tools » (menu préférences de XCode).
Installation de JewerlyBox
Télécharger l’archive ici, la décompresser et déposer le package dans Applications.
Installation de RVM, Homebrew et de ruby 2.0.0-p0
Pour RVM il suffit d’ouvrir JewerlyBox, une seule action est alors possible, « installer RVM », donc allons-y.
Si tout s’est bien passé vous devriez obtenir l’écran suivant.
Aller ensuite dans l’onglet Requirements du Dashboard. L’installation de Homebrew et des librairies nécessaires à RVM pour bien fonctionner est déclenché automatiquement.
Avant d’ajouter un environnement Ruby il faut réaliser une petite manip dans un Terminal pour configurer RVM :
$ echo '[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm"' >> .bashrc $ source .bash_rc $ rvm autolibs enable # Autorise l'installation automatique des dépendances
Pour éviter de passer du temps à résoudre les « -bash: rvm: command not found
» à chaque nouvelle ouverture de Terminal :
$ echo 'if [ -f $HOME/.bashrc ]; then source $HOME/.bashrc fi' >> $HOME/.bash_profile
Sous Mac, Terminal charge le .bash_profile
et ne tient pas compte du .bashrc
.
Revenir ensuite, à JewerlyBox, aller dans le panel « Add Ruby », sélectionner la version 2.0.0-p0 et cliquer sur « Install ».
JewelryBox va alors résoudre les dépendances nécessaires à la compilation de Ruby, configurer l’environnement Ruby 2.0.0, puis compiler Ruby.
Ce processus prend du temps c’est le moment d’aller prendre un café.
Une fois l’écran ci-dessous obtenu c’est gagné, Ruby 2.0.0 est installé, on peut passer à la suite.
Installation de Compass
Ouvrir un Terminal puis sélectionner l’environnement ruby 2.0.0 :
$ which ruby /usr/bin/ruby $ rvm 2.0.0 $ which ruby /$HOME/.rvm/rubies/ruby-2.0.0-p0/bin/ruby
L’installation de compass se réalise tout simplement comme ceci :
$ gem install compass
Installation de Node.js
Node.js est installé au sein de RVM via :
$ brew install node
Installation yo, grunt, bower
Le gestionnaire de packages npm de Node.js est là pour nous simplifier la tâche :
$ npm install -g yo grunt-cli bower
Les liens vers les binaires de yo, grunt et bower sont définis dans ~/.rvm/share/npm/bin
il faut donc les ajouter à la variable PATH
.
Or, l’instruction PATH=$PATH:$HOME/.rvm/share/npm/bin
dans le .bashrc
est écrasée lorsque l’on exécute rvm 2.0.0
.
En cause, les scripts de démarrage de RVM. Ils nettoient PATH
de toutes les valeurs qui contiennent $HOME/.rvm
.
Pour palier à cela, on utilise donc un ‘hook’ RVM :
$ echo 'PATH=$PATH:$HOME/.rvm/share/npm/bin # Add npm libraries to PATH' >> $HOME/.rvm/hooks/after_use_add_npm_libraries_to_path $ chmod +x $HOME/.rvm/hooks/after_use_add_npm_libraries_to_path
Autre point, il n’est pas nécessaire de sélectionner un environnement ruby pour profiter des outils installés au sein de RVM.
L’instruction [[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm
que l’on a rajouté dans le .bashrc
précédemment permet d’ajouter à la variable PATH
les scripts et binaires présents dans $HOME/.rvm/bin
dont brew
et node
.
Pour pouvoir quand même profiter de yo
, grunt
et bower
sans utiliser Ruby ni Compass, la solution que j’ai trouvée est de charger le hook depuis .bashrc
:
$ echo 'source $HOME/.rvm/hooks/after_use_add_npm_libraries_to_path' >> $HOME/.bashrc
Lorsque l’on exécutera de nouveau rvm 2.0.0
le hook sera écrasé puis de nouveau rechargé.
Si quelqu’un a une meilleure solution, je suis preneur.
It’s Yeoman time!
Pour reprendre l’exemple de l’application phare d’Oxiane, la Cave à vin, je souhaite créer une web app qui liste l’ensemble des bouteilles d’une cave.
La première étape consiste donc à initialiser la web app Angular via Yeoman.
$ mkdir $HOME/path_to/cave-a-vin && cd $HOME/path_to/cave-a-vin $ npm install generator-angular generator-karma # installation des générateurs angular et karma $ yo angular # initialisation d'une web app AngularJS $ npm install && bower install # installation des dépendances par défaut
yo angular
pose une série de questions auxquelles je réponds à toutes ‘Y’.
La web app nouvellement créée fonctionne ainsi avec AngularJS, Twitter Bootstrap et SASS.
La structure de ma web app est définie, je vais donc pouvoir tester son fonctionnement, pour cela, trois instructions importantes, à retenir :
grunt test # teste de la web app via karma grunt server # pré-visualisation de la web app grunt # build de la web app
J’ouvre donc un terminal et exécute grunt server
. Et là, c’est le drame :
$ grunt server Running "clean:server" (clean) task Running "coffee:dist" (coffee) task Running "compass:server" (compass) task Warning: You need to have Ruby and Compass installed and in your system PATH for this task to work. More info: https://github.com/gruntjs/grunt-contrib-compass Use --force to continue. Aborted due to warnings.
Bon, j’ai simplement oublié de lancer rvm 2.0.0
.
Ceci étant fait, je relance grunt server
, un browser s’ouvre automatiquement sur http://localhost:9000
et je peux admirer ma toute nouvelle web app.
La liste des frameworks installés c’est sympa, mais je veux afficher le contenu d’une cave à vin.
Je commence d’abord par modifier la vue cave-a-vin/app/views/main.html
.
Cave à Vin
Nom | Millésime | Région | Appellation | Couleur | Cépage | Producteur |
---|---|---|---|---|---|---|
{{bouteille.nom}} | {{bouteille.millesime}} | {{bouteille.region}} | {{bouteille.appellation}} | {{bouteille.couleur}} | {{bouteille.cepage}} | {{bouteille.producteur}} |
Notez bien la directive Angular ng-repeat
. Elle contient la propriété bouteilles
qui sera alimentée par le controller associé à cette vue.
grunt
s’occupe de scanner les modifications sur les fichiers de la web app et rafraîchit la page automatiquement. Aucunement besoin de tout relancer.
Mon tableau est vide, il faut donc que je l’alimente, je modifie donc le controller cave-a-vin/app/scripts/controllers/main.js
'use strict'; angular.module('caveAVinApp') .controller('MainCtrl', function ($scope) { $scope.bouteilles = [ { nom: "vin1", millesime: "millesime1", region: "region1", appellation: "appellation1", couleur: "couleur1", cepage: "cepage1", producteur: "producteur1" } ]; });
On bascule côté browser pour vérifier que tout est bien câblé.
Maintenant je souhaiterais pouvoir appeler un service web pour me fournir la liste des bouteilles.
Je crée donc un répertoire cave-a-vin/server
dans lequel je stocke :
cave-a-vin.json
: liste des bouteilles de ma cave.cors.js
: script autorisant les appels REST inter-domaines.cave-a-vin.js
: script Node.js contenant la définition de mon service web et l’initialisation d’un serveur web sur le port 1234.
Fichier cave-a-vin.json
:
[ { "id" : "1", "nom": "Château Pierre Bise Beaulieu", "millesime": "2004", "region": "Loire", "appellation": "Coteaux du Layon", "couleur": "Blanc", "cepage": "Chenin", "producteur": "Château Pierre Bise" }, { "id" : "2", "nom": "Château Pierre Bise", "millesime": "2001", "region": "Loire", "appellation": "Clos de Coulaine", "couleur": "Blanc", "cepage": "Chenin", "producteur": "Château Pierre Bise" }, { "id" : "3", "nom": "Chateau Lafitte", "millesime": "2012", "region": "Bordeaux", "appellation": "St Emilion", "couleur": "Rouge", "cepage": "Syrah", "producteur": "Lafite" } ]
Fichier cave-a-vin.js
:
var express = require('express'); var cors = require("./cors"); var fs = require('fs'); var server = express(); server.use(express.bodyParser()); server.use(cors.express); function notSupported(res) { res.status(400).send('Method ' + req.method + 'not supported'); } function newReadFileCallback(res) { return function (err, data) { if (err) { console.log(err); res.send({}); } res.send(JSON.parse(data)); } } server.all('/cave-a-vin', function (req, res) { console.log('/cave-a-vin', req.method, req.body); switch (req.method) { case 'GET': fs.readFile('./cave-a-vin.json', 'utf8', newReadFileCallback(res)); break; default: notSupported(res); } }); console.log('Starting cave-a-vin handler.'); server.listen(1234);
cave-a-vin.js
requiert Express, il faut donc installer ce module via npm et le lié à l’application :
$ npm install -g express $ npm link express
Fichier cors.js
:
exports.express = function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); res.header('Access-Control-Max-Age', 60 * 60 * 24 * 365); // intercept OPTIONS method if ('OPTIONS' == req.method) { res.send(200); } else { next(); } };
Pour démarrer le serveur :
$ node server/cave-a-vin.js
Pour vérifier son bon fonctionnement, ouvrir un browser sur http://localhost:1234
.
Il faut maintenant modifier le code côté client pour faire appel au web service.
Tout d’abord dans cave-a-vin/app/scripts/app.js
où l’on définit un service Angular : CaveAVinService.
Ce service utilise la ressource représentée par l’url 'http://localhost:1234/cave-a-vin'
correspondant à notre web service.
'use strict'; angular.module('caveAVinApp', ['ngResource']) .factory('CaveAVinService', function ($resource) { return $resource('http://localhost\\:1234/cave-a-vin') }) .config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .otherwise({ redirectTo: '/' }); });
Enfin, il suffit de faire appel à CaveAVinService dans le controller cave-a-vin/app/scripts/controllers/main.js
pour pouvoir afficher les données récupérées depuis le serveur.
'use strict'; angular.module('caveAVinApp') .controller('MainCtrl', function ($scope, CaveAVinService) { $scope.bouteilles = CaveAVinService.query(); });
Dernier coup d’oeil sur le browser.
Maintenant que le layout est correct, qu’en est-il des tests unitaires ?
$ grunt test ... Running "karma:unit" (karma) task INFO [karma]: Karma server started at http://localhost:8080/ INFO [launcher]: Starting browser Chrome WARN [launcher]: Chrome have not captured in 5000 ms, killing. INFO [launcher]: Trying to start Chrome again. INFO [Chrome 26.0 (Mac)]: Connected on socket id sFJBVG59APBGDT7PkExe Chrome 26.0 (Mac) Controller: MainCtrl should attach awesome things to the scope FAILED TypeError: Cannot read property 'length' of undefined at null. ($HOME/cave-a-vin/test/spec/controllers/main.js:26:31) Chrome 26.0 (Mac): Executed 1 of 1 (1 FAILED) (0.584 secs / 0.047 secs) Warning: Task "karma:unit" failed. Use --force to continue. Aborted due to warnings.
On s’aperçoit que les tests ne fonctionnent plus, normal puisque l’on a modifié le contenu du squelette créé par Yo
.
Il faut donc les faire évoluer en conséquence.
Le fichier à modifier est cave-a-vin/test/spec/controllers/main.js.
'use strict'; describe('Controller: MainCtrl', function () { // load the controller's module beforeEach(module('caveAVinApp')); var MainCtrl,scope,CaveAVinServiceMock; // Initialize the controller and a mock scope beforeEach(inject(function ($controller, $rootScope) { scope = $rootScope.$new(); CaveAVinServiceMock = new function(){ this.query = function(){ return [{nom:'vin1'},{nom:'vin2'},{nom:'vin3'}] } }; MainCtrl = $controller('MainCtrl', { $scope: scope, CaveAVinService: CaveAVinServiceMock }); })); it('devrait attaché la liste de bouteilles au scope', function () { expect(scope.bouteilles.length).toBe(3); expect(scope.bouteilles[0].nom).toBe("vin1") }); });
Le test est simple, il s’agit de s’assurer que le $scope
de MainCtrl
est bien initialisé lors du démarrage de la web app.
Pour vérifier cela, je crée un mock de CaveAVinService
dont la fonction query()
retourne trois bouteilles. Ce mock est ensuite injecté dans le controller MainCtrl
.
Deux assertions testent ensuite si tout s’est correctement déroulé.
EDIT: Comme on utilise la directive Angular ngRessource
il faut prendre en compte le module angular-resource
dans la configuration de Karma (cave-a-vin/karma.conf.js
), le runner des tests.
... // list of files / patterns to load in the browser files = [ JASMINE, JASMINE_ADAPTER, 'app/components/angular/angular.js', 'app/components/angular-mocks/angular-mocks.js', 'app/components/angular-resource/angular-resource.js', // Ajout module angular-resource 'app/scripts/*.js', 'app/scripts/**/*.js', 'test/mock/**/*.js', 'test/spec/**/*.js' ]; ...
Je relance un grunt test
.
$ grunt test ... Running "karma:unit" (karma) task INFO [karma]: Karma server started at http://localhost:8080/ INFO [launcher]: Starting browser Chrome WARN [launcher]: Chrome have not captured in 5000 ms, killing. INFO [launcher]: Trying to start Chrome again. INFO [Chrome 26.0 (Mac)]: Connected on socket id mg8qmtJxccfLzaDOknbn Chrome 26.0 (Mac): Executed 1 of 1 SUCCESS (0.316 secs / 0.045 secs) Done, without errors.
Cette fois-ci, tout est vert.
Et voilà, la web app est initialisé correctement, il nous faut maintenant ajouter des opérations CRUD et de la persistance.
Ce sera l’objet d’un autre article.
To be continued…