Publié par

Il y a 7 ans -

Temps de lecture 11 minutes

Précompiler et minifier les templates Ember avec Wro4j

Ember est un framework Javascript dont l’objectif principal est de faciliter la mise en place d’architectures MVC dans les navigateurs. L’utilisation d’Ember sur un projet pose toutefois un double problème :

  • Les fichiers Javascript sont faciles à découper (un fichier par classe Javascript par exemple), mais le fait de charger des dizaines de fichiers différents est contre-performant ;
  • Les templates Ember sont tous présents directement dans la page html, ce qui mène rapidement à avoir une page de plusieurs milliers de lignes, et donc difficilement maintenable.

Cet article se propose donc de montrer, via l’utilisation de wro4j, comment résoudre ces deux problèmes. 

Survol de Ember

De plus en plus de projets se lancent en choisissant une architecture de type mono-page. Ember est un framework Javascript MVC fullstack, facilitant la création de ce genre d’application. Pour afficher les vues, Ember s’appuie sur des templates Handlebars légèrement modifiés pour pouvoir y ajouter du « binding » : lorsqu’une valeur du modèle Javascript est modifiée, elle est automatiquement mise à jour dans la vue, et vice-versa.
Ces templates doivent être présents dans le corps de la page et s’écrivent sous la forme suivante:

<script type="text/x-handlebars" data-template-name="nomDuTemplate">
Exemple de template affichant une variable : {{variable}}
</script>

Lorsqu’Ember a besoin d’afficher un template, il va d’abord le compiler sous forme de fonction Javascript, puis ensuite enregistrer dans l’objet global Ember.TEMPLATES le template compilé. 

Cependant, cette façon de faire pose deux problèmes :

  • les templates se trouvent tous directement dans la page principale, qui finit vite par faire des milliers de lignes ;
  • bien que la compilation soit relativement rapide, chaque utilisateur doit à chaque fois repasser par cette étape.

De plus, sur une application de taille moyenne, on arrive assez vite à avoir plusieurs dizaines de fichiers Javascript pour les modèles, vues, contrôleurs, etc.

Pour résoudre ces problèmes, nous proposons dans ce billet de :

  • précompiler coté serveur les templates qui seraient stockés dans des fichiers Javascript ;
  • réunir et minifier tous les fichiers Javascript (templates précompilés compris) en un seul fichier.

Pour la démonstration ci-dessous, nous partirons de l’exemple TODO MVC fait avec Ember, et nous nous contenterons de le transformer en projet java / maven en créant un pom.xml minimal et en déplaçant les sources vers src/main/webapp.

wro4j

wro4j est une librairie dont l’objectif principal est de prétraiter les ressources Javascript et CSS d’une application web, puis de les réunir sous forme de bundle. Malheureusement, bien qu’ayant par défaut un précompilateur Handlebars, wro4j ne fournit pas de précompilateur pour les templates Ember. Nous allons donc développer le nôtre.

Par ailleurs, bien que wro4j permette d’effectuer la précompilation à la construction de l’application, nous utiliserons plutôt une approche basée sur les filtres, permettant le rechargement à chaud des fichiers Javascript et des templates.

Description du projet initial

On supposera donc que l’application TODO MVC existe déjà dans un projet maven et fonctionne. La structure du projet devrait donc ressembler à :

  • src/main/webapp 

    • js

      • controllers

        • entries.js
        • todo.js
      • libs

        • ember-latest.js
      • models

        • store.js
        • todo.js
      • views

        • application.js
        • todo.js
      • router.js
      • app.js
    • assets

      • base.css
      • base.js
      • bg.png
      • handlebars.min.js
      • jquery.min.js
    • index.html
  • pom.xml

 

On voit donc que même sur une application simple, on charge au démarrage 12 fichiers Javascript et 4 templates présents dans le fichier index.html.

Mise en place de wro4j

La première étape consiste à ajouter wro4j aux dépendances du projet dans le pom.xml :

<dependencies>
  <!-- other dependencies... -->
  <dependency>
    <groupId>ro.isdc.wro4j</groupId>
    <artifactId>wro4j-core</artifactId>
    <version>1.6.1</version>
  </dependency>
  <dependency>
    <groupId>ro.isdc.wro4j</groupId>
    <artifactId>wro4j-extensions</artifactId>
    <version>1.6.1</version>
  </dependency>
  <!-- other dependencies... -->
</dependencies>

 

On peut ensuite éditer (ou créer) le fichier web.xml dans le répertoire src/main/webapp/WEB-INF pour ajouter le filtre :

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 version="2.5">
  <!-- other things if you need it -->
  <filter>
    <filter-name>WebResourceOptimizer</filter-name>
    <filter-class>ro.isdc.wro.http.WroFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>WebResourceOptimizer</filter-name>
    <url-pattern>/wro/*</url-pattern>
  </filter-mapping>
  <!-- other things if you need it -->
</web-app>

 

On indique ensuite que toutes les URL commençant par /wro/ iront chercher les ressources définies dans le fichier wro.xml présent dans le répertoire src/main/webapp/WEB-INF, fichier que nous allons créer :

<groups xmlns="http://www.isdc.ro/wro"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.isdc.ro/wro wro.xsd">

  <group name="todo">
   <js>/js/app.js</js>
   <js>/js/router.js</js>
   <js>/js/models/*</js>
   <js>/js/controllers/*</js>
    <js>/js/views/*</js>
  </group>
</groups>

Il existe de nombreuses façons de configurer les groupes Wro4j, comme indiqué dans la documentation, le fichier proposé ci-dessus n’est qu’un exemple simple, permettant de rassembler toutes les ressources Javascript applicatives dans un « fichier » nommé todo.js 

Dans le fichier index.html, on remplace les appels Javascript ci-dessous…

<script src="js/app.js"></script>
<script src="js/router.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/models/store.js"></script>
<script src="js/controllers/entries.js"></script>
<script src="js/controllers/todos.js"></script>
<script src="js/views/application.js"></script>
<script src="js/views/todos.js"></script>

… par l’unique appel à la ressource wro compilée :

<script src="wro/todo.js"></script>

L’application est toujours fonctionnelle, mais il n’y a plus qu’un fichier Javascript correspondant à notre application, et celui-ci est minifié.

Si l’on a résolu le problème des multiples appels de fichiers, il nous reste encore à résoudre le problème des templates présents dans le fichier index.html

Création d’un compilateur spécifique pour les templates Ember

Nous allons ici créer un compilateur de template Ember. Pour cela, il faut créer 3 classes :

  • src/main/java/fr/xebia/wro/processor/ember/EmberJs.java, classe qui effectuera la compilation proprement dite :
package fr.xebia.wro.processor.ember;
import java.io.InputStream;
import java.io.SequenceInputStream;
import ro.isdc.wro.extensions.processor.support.template.AbstractJsTemplateCompiler;
 
public class EmberJs extends AbstractJsTemplateCompiler {
    @Override
    public String compile(final String content, final String name) {
        return "(function() {Ember.TEMPLATES[" + name + "] = Ember.Handlebars.template(" + super.compile(content, "")
                + ")})();";
    }
    @Override
    protected String getCompileCommand() {
        // Function present in headless-ember
        return "precompileEmberHandlebars";
    }
    @Override
    protected InputStream getCompilerAsStream() {
        final ClassLoader classLoader = EmberJs.class.getClassLoader();
        final InputStream handlebars = classLoader.getResourceAsStream("fr/xebia/wro/processor/ember/handlebars.min.js");
        final InputStream headlessEmber = classLoader.getResourceAsStream("fr/xebia/wro/processor/ember/headless-ember.js");
        final InputStream ember = classLoader.getResourceAsStream("fr/xebia/wro/processor/ember/ember-latest.js");
        return new SequenceInputStream(new SequenceInputStream(handlebars, headlessEmber), ember);
    }
}
  • src/main/java/fr/xebia/wro/processor/js/EmberProcessor.java, le processeur, au sens wro4j qui fera l’appel à la classe EmberJs pour précompiler
package fr.xebia.wro.processor.js;

import org.apache.commons.io.FilenameUtils;
import ro.isdc.wro.extensions.processor.js.JsTemplateCompilerProcessor;
import ro.isdc.wro.extensions.processor.support.template.AbstractJsTemplateCompiler;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.SupportedResourceType;
import fr.xebia.wro.processor.ember.EmberJs;

@SupportedResourceType(ResourceType.JS)
public class EmberProcessor extends JsTemplateCompilerProcessor {

    public static final String ALIAS = "emberJs";


    @Override
    protected AbstractJsTemplateCompiler createCompiler() {
        return new EmberJs();
    }


    @Override
    protected String getArgument(Resource resource) {
        final String name = resource == null ? "" : FilenameUtils.getBaseName(resource.getUri());
        return String.format("'%s'", name);
    }
}
  • src/main/java/fr/xebia/wro/manager/factory/MyConfigurableWroManagerFactory.java, la factory permettant d’ajouter notre processeur à la liste de ceux déjà fournis par wro4j
package fr.xebia.wro.manager.factory;

import java.util.Map;
import ro.isdc.wro.manager.factory.ConfigurableWroManagerFactory;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import fr.xebia.wro.processor.js.EmberProcessor;

public class MyConfigurableWroManagerFactory extends ConfigurableWroManagerFactory {

    @Override
    protected void contributePreProcessors(Map<String, ResourcePreProcessor> map) {
        map.put(EmberProcessor.ALIAS, new EmberProcessor());
    }
}

Par ailleurs, il est nécessaire de fournir à ces classes les fichiers Ember permettant la précompilation :

  • src/main/resources/fr/xebia/wro/processor/ember/ember-latest.js
  • src/main/resources/fr/xebia/wro/processor/ember/handlebars.min.js
  • src/main/resources/fr/xebia/wro/processor/ember/headless-ember.js

Les deux premiers fichiers peuvent se copier/coller depuis les répertoires src/main/webapp/js et src/main/webapp/asset

Le fichier headless-ember.js se trouve directement dans le repository github de ember : https://github.com/emberjs/ember.js/blob/master/lib/headless-ember.js , et permet d’executer Ember coté serveur en simulant un environnement de navigateur.

Une fois tout cela déclaré, il faut indiquer à wro4j qu’il doit utiliser notre ManagerFactory pour executer ses processeurs. Pour cela nous allons éditer le fichier wro.properties qu’il faut ajouter dans le répertoire src/main/webapp/WEB-INF

managerFactoryClassName=fr.xebia.wro.manager.factory.MyConfigurableWroManagerFactory
preProcessors=cssUrlRewriting,cssImport,emberJs.emtpl,semicolonAppender,cssMin,jsMin

Par défaut, wro4j exploitera tous les fichiers, sans tenir compte du type ; or dans notre cas, on ne veut pour notre précompilateur Ember que les fichiers ayant l’extension emtpl (le nom de l’extension est arbitraire). La notation « .emtpl » permet de dire que l’on ne compilera avec notre préprocesseur que les fichiers finissant en « .emtpl« . Il existe beaucoup d’autres options de configuration, comme l’indique la documentation.

Nous allons ensuite créer un répertoire pour les templates,
/src/main/webapp/templates

et y déplacer de index.html nos templates (sans les balises scripts).

  • /src/main/webapp/templates/statsTemplate.emtpl
  • /src/main/webapp/templates/filtersTemplate.emtpl
  • /src/main/webapp/templates/clearBtnTemplate.emtpl
  • /src/main/webapp/templates/todosTemplate.emtpl

Le nom du template correspondra alors au nom du fichier, sans l’extension.

Il n’y a plus qu’à ajouter notre répertoire dans la configuration de wro.xml

<groups xmlns="http://www.isdc.ro/wro"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.isdc.ro/wro wro.xsd">

  <group name="todo">
   <js>/js/app.js</js>
   <js>/js/router.js</js>
   <js>/js/models/*</js>
   <js>/js/controllers/*</js>
    <js>/js/views/*</js>
    <js>/templates/**</js>
  </group>
</groups>

Notre application doit toujours fonctionner, mais les templates sont maintenant séparés proprement dans les fichiers .emtpl, et les fichiers sont correctement compilés, minifiés et mis en cache par wro4j.

Conclusion

La librairie wro4j est relativement simple d’utilisation, mais d’une grande puissance ; on aura vu qu’avec les quelques lignes ci-dessus on a réussi à améliorer les performances globales de l’application (précompilation, minification et gestion du cache), mais aussi à organiser de façon beaucoup plus propre les fichiers. Cependant, ce tutoriel n’est qu’une première étape dans l’industrialisation de projet Java / Ember, car il y a encore beaucoup de voies d’amélioration comme par exemple penser à une gestion plus fine des noms de templates dans le préprocesseur, ou encore à un moyen d’éviter la duplication des ressources js entre la webapp et le précompilateur.

N.B. : À partir de la version 1.6.2, Wro4J devrait embarquer par défaut le précompilateur Ember.

Publié par

Publié par Benoît Lemoine

Développeur et fier de l'être, Benoit s'intéresse de près à tout ce qui peut permettre de créer une application web, du HTML aux sources de données, en passant par le javascript et les framework haute productivité. twitter : @benoit_lemoine

Commentaire

2 réponses pour " Précompiler et minifier les templates Ember avec Wro4j "

  1. Publié par , Il y a 7 ans

    Hi Benoit,

    thank you for this useful blog post. Have you considered to create a pull request with emberJs contribution? I think it would be useful to have ember processor provided by default.

  2. Publié par , Il y a 7 ans

    Hi Alex,

    it’s a great idea, and I’ll start working on it right away

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.