Commentaires
Tips & Tricks : AngularJS & Symfony2
À la lecture des derniers articles, vous avez pu voir les bonnes pratiques pour le déploiement d'une API REST avec Symfony2, et l'utilisation de Symfony2 dans le développement d'applications Javascript.
Aujourd'hui, nous vous proposons de reprendre cette architecture mais "depuis les tranchées" : un premier retour, sous forme de tips, sur nos expériences de déploiement d'une application cliente AngularJS avec une API REST Symfony2.
Cet article n'est pas un tutoriel : nous souhaitons présenter les différentes problématiques rencontrées au jour le jour et quelques pistes pour optimiser le développement et la maintenance d’une telle application.
En vrac, 5 Tips qui pourront (peut-être) vous aider.
Tip 0 : Organisation
En fonction de la façon dont vous souhaitez organiser votre code et vos deux applications, on peut envisager plusieurs façons d'utiliser AngularJS :
- Développer une application indépendante et autonome ("Full AngularJS"). Toute les logiques métiers seront réalisées en Javascript ou via des appels à l'API REST.
- De manière localisée dans votre page pour un widget par exemple. Vous aurez donc votre application Symfony et vos templates (Twig?) bootstrapant AngularJS.
Par soucis pratique c'est cette dernière solution que nous avons utilisée dans notre application de démo, dispo sur GitHub.
API Rest et AngularJs : Mise en place
Revenons sur les articles précédents :
Jour 6 : Il y a plusieurs bundles Symfony2 qui vous permettront de développer une API REST rapidement et proprement.
D'ailleurs, si vous utilisez Behat le WebApiContext peut vous aider à tester votre API.
Jour 7 : nous avons pu comprendre l'intérêt d'utiliser des frameworks JS full-stack.
Dans notre cas, nous utiliserons AngularJS.
AngularJs est un framework Javascript MVC opensource soutenu par Google.
Il permet de manipuler très facilement le DOM d'une page web. Par exemple, il offre la possibilité de synchroniser (de façon transparente) un modèle de données avec le rendu HTML (c’est le paradigme MVVM, pour ModelView - ViewModel).
Parmi les points forts d'AngularJS, nous noterons : sa philosophie "full-stack" (pas ou peu besoin de librairies externes), un système de directives puissant, une documentation très bien fournie, une communauté très active et beaucoup de contributions et ressources.
Pour finir de vous convaincre, vous pouvez lire ce court article sur Sitepoint 10 Reasons Why you should use AngularJs, ou vous pouvez consulter cette galerie de projets réalisés avec AngularJs.
Intégrer AngularJS dans une application Symfony
Comme dans notre démo, il peut s'avérer intéressant d'inclure AngularJS dans un applicatif Symfony existant (bien que criticable ;p).
Pour l'exemple, si veut simplement ajouter un widget profitant d'AngularJS dans son template Twig, il n'y a qu'à inclure angular.js
et ajouter l'attribut ng-app
au div qui contiendra le widget :
<!doctype html>
<html>
<head>
{% javascripts
'@AcmeFooBundle/Resources/public/js/angularjs/1.2.4/angular.min.js'
%}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}
</head>
<body>
<div id="name-widget" ng-app>
<!-- simple demo http://angularjs.org/#the-basics -->
<!-- affiche le contenu de l'input dans le h1 -->
<label>Name:</label>
<input type="text" ng-model="yourName" placeholder="Enter a name here">
<h1>Hello {{yourName}}!</h1>
</div>
</body>
</html>
Notons que le language de template d'AngularJS et celui de Twig rentrent en conflit avec l'utilisation des {{ }}
.
Il est donc important d'isoler votre code Angular dans les templates Twig en utilisant le tag verbatim
.
(Autre solution : remplacer les tags d'interpolation d'AngularJS, plus de détails ici)
Tip 1 : Profiter des outils des devs Frontend
Les développeurs frontend possèdent de nombreux outils hyper-puissants pour le développement et le déploiement.
Parmi ces outils, on citera :
- Bower
Un outil pour gérer les dépendances et l'installation de composants "Frontend" (une sorte de composer pour le front). Il est possible d'intégrer facilement le gestionnaire Bower dans une application Symfony2 en utilisant, par exemple le bundle SpBowerBundle.
- Grunt
Une boite à outil pour gérer toutes les tâches utiles pour le dev, telles que compiler des fichiers SASS ou minifier les fichiers Javascripts (il y en a beaucoup d'autres !). Sur certains aspects, ça peut ressembler à un Makefile orienté Javascript. Bien que nous ne l'avons pas testé, l'usage de Grunt dans Symfony est envisageable (Adieu Assetic ?!). Cet [article](http://wozbe.com/fr/blog/2013-08-07-integration-grunt-et-bower-au-sein-application-symfony) est un bon point de départ.
Vous pouvez voir un exemple d'intégration de Bower dans notre démo
Tip 2 : Gestion des exceptions "Cross Domain Scripting"
Pour des raisons de sécurité, les navigateurs web empêchent le code Javascript de faire des requêtes Ajax (XMLHttpRequest) vers d'autres domaines (Same-origin policy) .
Un exemple d'exception lancée par le navigateur :
XMLHttpRequest cannot load http://api.mondomaine.com/v1/maressource.json. Invalid HTTP status code 405
Dans notre cas, notre application front ne pourra donc pas requêter l'API.
Plusieurs solutions sont possibles :
Utilisation de JSONP
JSONP (JSON with padding) est une extension du format JSON. En résumé, il permet d'encapsuler du JSON dans du Javascript :
Requêter une ressource au format JSONP revient à demander au navigateur de récupérer une ressource Javascript, ce qui l'autorisera sans problème.
FOSRestBundle permet d'implémenter rapidement le format JSONP sur notre API.
Attention, il y a plusieurs contraintes dans l'utilisation de JSONP. La plus importante : JSONP est supporté uniquement dans le cadre de requêtes HTTP de type GET.
Configurer son serveur web (simple & stupid)
Il suffit de mettre en place les bonnes configurations pour que les deux applications puissent fonctionner sur le même domaine. Avec Apache, on pourra notamment jouer avec les directives Alias.
<VirtualHost *:80>
ServerName mon-appli-angular.com
DocumentRoot /var/www/mon-appli-angular/
Alias /api /var/www/mon-appli-symfony/
<Directory xxxx>
</Directory>
</VirtualHost>
Dans ce cas l'URL http://mon-appli-angular-com/api sera pris en compte par notre API Symfony2.
Utilisation de CORS
CORS (Cross-origin resource sharing) est une réponse élégante et standardisée pour autoriser les requêtes Cross-domain.
Attention tout de même, le mécanisme CORS n'est pas supporté par tous les navigateurs (devinez lesquels) ...
On peut facilement mettre en place le mécanisme CORS dans Symfony2 avec le bundle NelmioCORSBundle. Ce bundle va ajouter de façon transparente les entêtes "CORS" dans les requêtes HTTP sortantes de notre applicatif Symfony.
Installez le bundle :
$ composer require nelmio/cors-bundle:~1.0
//app/appKernel.php
public function registerBundles()
{
$bundles = array(
...
new Nelmio\CorsBundle\NelmioCorsBundle(),
...
);
...
}
puis ajoutez ces quelques lignes dans votre config.yml
#app/config/config.yml
nelmio_cors:
paths:
'^/api':
allow_origin: ['*']
allow_headers: ['x-requested-with']
allow_methods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS']
max_age: 3600
Tip 3: Gestion des routes
Avec nos deux applications, nous aimerions pouvoir travailler sur notre API sans avoir à changer notre application frontend.
La gestion des routes peut être un problème dans ce cas : il faut que nous puissions modifier les routes de l'API (montée de version par exemple), sans avoir à déployer une nouvelle version de notre application frontend.
Dans l'article du 12e jour, vous pouvez voir que le bundle FOSJsRoutingBundle
répond à nos besoins.
Dans notre cas, nous allons utiliser les routes exposées par le bundle dans l'application Angular via le service Restangular (Un must-have pour Angular ;) ).
Tous les appels à l'API seront donc configurés via le Routing exposé par FOSJsRoutingBundle.
Côté Symfony, il faut exposer les routes de notre API :
#app/config/routing.yml
la_netscouade_api_demo_bundle:
resource: LaNetscouade\ApiDemoBundle\Controller\CocktailController
type: rest
options:
expose: true
prefix: api
Côté application Angular, il faut appeler la librairie Routing, fournie par le bundle
/**
* Les ressources javascript de FosJsRoutingBundle doivent être chargées
*/
(function () {
'use strict';
angular.module('cocktails',['restangular']).controller('cocktailsIndexController',function($scope, Restangular){
$scope.pageTitle = "Cocktail List";
$scope.cocktailsList = Restangular.all(getRoute('get_cocktails')).getList();
});
var app = angular.module('demoApp', ['cocktails']);
app.config(function(RestangularProvider) {
RestangularProvider.setBaseUrl('http://localhost:8000'); //wOOt, ugly ...but that's a demo ;)
RestangularProvider.setRequestSuffix('.json');
});
/**
* transform http://localhost:8000//app_dev.php => http://localhost:8000/app_dev.php
* as Restangular and Routing components add '/' to baseUrl, we have to remove it
* @param routeName
* @returns {string}
*/
var getRoute = function(routeName){
return Routing.generate(routeName,{},false).slice(1);
}
})();
Tip 4 : Rendre notre application indexable par les moteurs de recherche
Il est bien connu que les applications full-Javascript ne sont pas indexables par les moteurs de recherche.
Il existe de nombreuses solutions pour rendre son application indexable comme par exemple Prerender.io (présenté par Rémi précédemment ).
On peut envisager une solution plus simple (mais plus compliquée à maintenir). Il s'agit de déléguer le rendu de la partie indexable à Symfony.
Etape 1 : Le hashbang
Le hashbang #!
, utilisé dans les URLs, permet d'indiquer aux bots qu'il s'agit d'une application "Ajax" Google Ajax Crawling Scheme.
Automatiquemnt à la vue de l'URL http://example.com/#!/ma-page, le bot va crawler http://example.com/?escaped_fragment=/ma-page
Sans configuration particulière, cette page n'existe pas !
C'est donc à nous de générer le contenu de chaque page via votre application côté serveur et de faire les redirections correspondantes.
Avec AngularJS, vous pouvez activer simplement le hashbang en ajoutant dans votre app.js
$locationProvider.hashPrefix('!');
Etape 2 : Générer les contenus indexables de notre application.
La méthode la plus simple est d'utiliser les mécanismes de FosRestBundle. Il est possible d'accéder à une ressource de notre API, en requêtant le format HTML.
Par exemple l'URL suivante
http://example.com/api/cocktails.json peut-être aussi consultable en HTML
http://example.com/api/cocktails.html
Tout d'abord activons cette fonctionnalité dans FosRestBundle :
#app/config/config.yml
fos_rest:
#...
view:
templating_formats:
html: true
Puis dans le controller, on pourra spécifier le template Twig que nous souhaitons utiliser.
use FOS\RestBundle\Controller\FOSRestController;
class UsersController extends FOSRestController
{
public function getUsersAction()
{
$data = // get data, in this case list of users.
$view = $this->view($data, 200)
->setTemplate("MyBundle:Users:getUsers.html.twig")
->setTemplateVar('users')
;
return $this->handleView($view);
}
}
Etape 3: Gérer la redirection
Maintenant que nos pages sont accessibles, il faut que les URLs contenant le paramètre _escaped_fragment_
soit redirigées sur notre API Symfony2.
Ajoutez ces lignes dans votre .htaccess :
RewriteCond %{QUERY_STRING} ^_escaped_fragment_=%2F(.*)$
RewriteRule ^$ api/%1.html [QSA,L]
Quand le bot arrivera sur http://example.com/#!/ma-page, il consultera maintenant le contenu de http://example.com/seo/ma-page et notre page pourra être référencée.
Ca fonctionne aussi pout le crawler de Facebook !
Tip 5 : Gestion des traductions
AngularJS dispose de son propre système de traduction i18n/l10n. Mais Il peut-être intéressant de contrôler et de centraliser ces traductions à partir de votre backend Symfony.
Pour cela, c'est très simple et la manière de faire ressemble beaucoup à la gestion des routes vue précédemment.
Cette fois-ci, on utilise ExposeTranslationBundle.
Reportez vous à l'article du 12 décembre pour son installation.
On pourra alors facilement utiliser l'objet Javascript Translator
du bundle dans nos templates AngularJS via un filtre AngularJS.
{{'category.list' | trans}}
app.filter('trans', function () {
return function (input) {
return Translator.get(input);
};
});
Bonus Tip : JMSTranslationBundle pour utiliser une interface de traduction dans Symfony.
Conclusion
Voici quelques Tips pour aller plus vite dans l'intégration d'une application AngularJS et d'une API REST en Symfony2.
Les quelques exemples ici sont restés assez simple... Vous l'aurez compris, la séparation de ces deux applications est à réflechir bien en amont. Bien qu'il puisse paraitre plus judicieux de séparer complétement les deux applications, il peut s'avérer intéressant dans certains cas d'avoir plus de souplesse dans cette organisation.