Liferay et les projets front end

Qu’est ce que Liferay ?

Liferay est une plateforme web qui propose une multitude de fonctionnalités afin de produire rapidement un environnement collaboratif avec les éléments de base (gestion de pages, CMS, forum, GED, etc.)
Mais c’est aussi une grande communauté qui alimente une marketplace avec des plugins qui peuvent être installés sur le portail, ainsi que des bundles pour réaliser les développements plus facilement.

Une gestion par module

Liferay DXP utilise le framework OSGi (Open Service Gateway initiative) afin de gérer les composants déployés sur la plateforme. Mais, qu'entend-on ici par composant ?
Un composant est un jar avec un manifest généré via un fichier bnd qui spécifie les informations du module (plus d’information ici). Généralement chaque module a une fonction spécifique :

  • API (va par paire avec un service)
  • Service (pour la persistance en BDD ou juste des classes d'implémentation métier)
  • Web (pour les widgets (ou portlet) mis à disposition sur la plateforme)
  • Etc..

Les bundles fournis par Liferay

Liferay propose une liste de bundles pour créer des modules (via Blade CLI, les archetypes Maven ou leur IDE Liferay Dev Studio CE). Pour plus de détails sur ces bundles, rendez-vous sur la doc Liferay via ce lien.
Nous allons nous intéresser en 1er au bundle npm-angular-portlet.

Au commencement, c’était comment ?

Avec le bundle npm-angular-portlet

Ce bundle permet de mettre en place un projet angular facilement dans un module de type portlet avec toutes les fonctionnalités front de Liferay (ici un exemple de projet : liferay-blade-samples).
npm-angular-portlet schemas
Par contre, il y a beaucoup d’inconvénients :

  • La version angular est dépendante de Liferay
  • Le build est long, pas de déploiement à chaud (on doit build le module complètement)
  • Il est difficile de démarrer le projet angular en Standalone à cause des dépendances avec Liferay

En conclusion, ce bundle est bien pour des POC rapides mais assez contraignant pour un projet angular de grande taille.

Avec un portlet embedded

Pour éviter les contraintes avec Liferay, beaucoup de projets utilisent cette solution qui consiste à spliter le module en 2 parties : un projet Angular natif et un module de type portlet où l’on inclut le livrable Angular.
embeded-portlet schemas

L’API REST coté Liferay

Il est facile de mettre en place un module d’API Rest coté Liferay avec JAX-RS (d’autres solutions existent comme utiliser directement l’API web service, les ressources MVC ou autre).
Les modules JAX-RS sont simples à mettre en place, il faut créer un composant OSGi avec dans les propriétés :

  • le nom du service (variable JAX_RS_NAME) qui permettra d’identifier l’API lors de la sélection du périmètre pour l’OAuth.
  • le root path (variable JAX_RS_APP_LOCATION_BASE) pour définir l’URL appelé le prefix de nos endpoints.

Et déclarer les endpoints avec des annotations sur les méthodes. Ici l’URL appelé pour la méthode getLastNews est http://[host]:[port]/o/rest-api/news/12345

@Component(immediate = true,
    Property = {
        JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE + "=/rest-api",
        JaxrsWhiteboardConstants.JAX_RS_NAME + "=News.rest"
    },
    Service = Application.class)
public class NewsRestControllerApplication extends Application {
    @Override
    public Set<Object> getSingletons() { 
        return Collections.singleton(this); 
    }

    @Get
    @Path("/news/{siteId}")
    @Produces(MediaType.APPLICATION_JSON)
    public String getLastNews(@Context HttpServletRequest request, @PathParam("siteId") long siteId) {

(ci-après la documentation liferay : tutorial JAX-RS)

Le projet angular en “Standalone”

La partie angular est totalement indépendante lors du développement et offre donc tous les avantages de ce type de projet.
Mais il y a quand même une dépendance avec l’API qui est sur le serveur Liferay et qui sera notre principal problème (à cause du CORS puisqu'on n’utilise pas le même host:port) car les 2 projets ne sont pas démarrés sur les mêmes ports.

Pour résoudre cela, il faut configurer un server-side proxy sur notre environnement de développement :

  1. Créer un fichier proxy.conf.json dans le projet Angular avec la configuration suivante :
{
    "/o/rest-api": {
        "target": "http://localhost:8080",
        "secure": false
    }
}
  1. Démarrer le serveur avec la commande ng serve --proxy-config proxy.conf.json
    Maintenant, grâce au proxy géré par Angular, nos services Angular peuvent interroger l’API que nous avons créée via le port du serveur angular.

Le portlet embedded

Il faut construire ce portlet avec les fichiers build du projet Angular (cela peut se faire via des actions lors du build). A part cela, il suffit de modifier le fichier view.jsp pour indiquer la balise app-root et intégrer les JS Angular :

<%include file="./init.jsp"%>

<app-root></app-root>

<script type="text/javascript" src="<@=request.getContextPath()%>/runtime.js"></script>
<script type="text/javascript" src="<@=request.getContextPath()%>/polyfills.js"></script>
<script type="text/javascript" src="<@=request.getContextPath()%>/main.js"></script>

Mais lors d’un build de production, les noms des fichiers JS Angular ont du hashing (par exemple : runtime.xxx.js) afin d’éviter les problèmes de cache.
Ce principe devra être conservé mais en utilisant une variable (un timestamp par exemple) qui sera géré côté portlet embedded afin de pouvoir appeler le bon fichier JS dans le fichier view.jsp.
Pour cela, il faut désactiver le hashing lors du build Angular avec le paramètre --output-hashing none et renommer les fichiers lors du build du portlet embedded.

Attention : pour que le Lazy Loading marche correctement avec le portlet embedded, il faut indiquer le web context path (exemple : /o/my-angular-portlet) dans le paramètre deployUrl lors du build Angular.

Les variables d'environnement

Grâce aux fichiers environment.ts et un peu de configuration d’Angular, nous pouvons spécifier des variables qui seront transmises à notre projet Front end depuis le portlet.

  1. Pour commencer, la configuration pour qu’elle change en fonction du build :
{
  "projects": {
    "app-name": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              // ...
  1. Dans environment.prod.ts (qui sera utilisé dans le portlet embeded), nous allons déclarer les différentes variables qui seront récupérées :
//Liferay javascript Object
declare var Liferay;

//Global variables that must be defined in view.jsp
Declare var myVar1;
Declare var myVar2;

export const environment = {
  production: true,
  Liferay: Liferay,
  myVar1: myVar1,
  myVar2: myVar2
}
  1. Coté portlet, dans le fichier view.jsp, les variables sont déclarées de la façon suivante :
<script type="text/javascript">
    var myVar1 = "${myVar1}";
    var myVar2 = "test";
</script>
  1. Dans environment.ts (qui sera utilisé en standalone), on pourra mock ces variables :
export const environment = {
  production: true,
  Liferay: {
    ThemeDisplay: {
      getCompanyId() {
        return 12345;
      }
    }
  },
  myVar1: "test1",
  myVar2: "test2"
}

En conclusion, c’est une solution viable qui se concentre sur le projet Front end mais qui manque d’interaction avec les éléments du portail comme le thème par exemple.
Si vous voulez plus de renseignements sur cette façon de faire, vous pouvez allez lire le retour d’expérience de Thomas Cucchietti lors du Symposium 2019 ici.

Un nouveau bundle orienté JS !

Courant été 2018, la communauté Liferay a travaillé sur un nouveau type de bundle pour les projets Front-End.
Ce bundle permet de construire le module qui sera livré directement avec npm. Il préconise aussi de faire un module provider qui contiendrait toutes les librairies afin d'accélérer le build des autres modules métiers (qui ne contiendront pas les librairies).
Pour vous présenter ce bundle, je me suis basé sur la démo de Olivier Neu lors du Symposium 2019 (ici le lien du Github)
nouveau schemas avec provider

Le module provider

Comme indiqué plus haut, c’est lui qui portera toutes les librairies des projets front dont on aura besoin.
Il peut aussi contenir du CSS global qui est commun et rarement modifié (car ce module est long à build).

Ce module n’a pas beaucoup de fichiers. En plus des fichiers annexes (comme le CSS ou des images par exemple) qu’on peut ajouter dans src ou assets, il n’a que 3 fichiers de configuration :

  • package.json (source)
    Ce fichier de configuration contient les informations pour le build du module avec :

    • Le nom du module
    • La version du module
    • La description du module
    • Les dépendances pour build le module
    • Les librairies à inclure dans le provider
    • Les actions scriptées
      C’est le fichier qui sera le plus modifié pour ajouter les librairies
  • .npmbundlerrc (source)
    Ce fichier de configuration permet de déclarer comment est construit le bundle (hors librairies déclarées dans le package.json) :

    • sources déclare l’emplacement des fichiers à inclure
    • create-jar permet de déclarer où le jar sera créé, ainsi que les options de création OSGi (context web, fichier de langue, etc.)
    • dump-report permet de définir si oui ou non un fichier récapitulatif sera créé
    • rules définit les loader à utiliser en fonction du test. De base, on utilise les dossiers source mais on peut en inclure/exclure.
      Pour plus d'informations sur ce fichier, vous pouvez lire la base de connaissance de Liferay à ce sujet.
  • .npmbuildrc (source)
    Ce fichier de configuration est utilisé lors du build pour indiquer où se trouve le serveur où l’on déploie le module.

Le module Front

C’est ici que l’on va créer notre portlet, à partir d’un langage Front.
Comme le module provider, il y a beaucoup de configuration pour déclarer les librairies utilisées, leur provenance (si elles sont disponibles dans un provider ou pas), etc...

Voici les différentes configurations :

  • package.json (source)
    Ce fichier de configuration contient les informations pour le build du module, on y retrouve les mêmes informations que celui du module provider mais aussi la configuration du portlet Liferay.
    C’est le fichier qui sera le plus modifié pour ajouter les librairies et les informations du portlet.

  • .npmbundlerrc (source)
    Comme pour le provider, ce fichier de configuration permet de définir comment est construit le jar. Il y a aussi la déclaration des dépendances au provider (c’est l'arborescence sous import) qui permet d'alléger le build.

  • .npmbuildrc (source)
    Ce fichier de configuration est utilisé lors du build pour indiquer où se trouve le serveur où l’on déploie le module ainsi que les informations sur comment construire la partie front.

  • .jshintrc (source)
    Ce fichier de configuration sert à configurer JSHint. Ici on indique juste la version de la norme JS.

  • tsconfig.json (source)
    Ce fichier de configuration sert à configurer la transpilation TypeScript.

En plus de ses fichiers de configuration, nous avons 3 dossiers qui sont :

  • assets => pour l'aspect affichage des composants (template html, css, etc.)
  • src => qui contient le code TypeScript des composants, les objets, les variables, etc.
  • features => dossier qu’on a déclaré dans .npmbundlerrc (pour les fichiers de traduction par exemple)
    exemple de structure de code

En conclusion, ce bundle permet de produire directement un projet Front end qui pourra être build et déployé sur le Liferay assez rapidement (lors du développement) grâce au provider qui héberge les différentes librairies utilisées.

En definitive

Nous avons 3 façons de faire des projets front-end avec Liferay :

  • Le bundle portlet est simple à mettre en place mais est long à compiler et n’a pas la possibilité d’utiliser les webtools pour coder en live. Une bonne solution pour faire des POCs mais contraignante pour faire un projet front-end.
  • La séparation du projet front-end et de Liferay permet d’être indépendant au niveau de la réalisation du projet mais cette solution à ses propres inconvénients comme la faible interaction avec les autres composants front de Liferay et un processus de déploiement plus complexe.
  • Le nouveau bundle npm est plus long à mettre en place (beaucoup de modules à créer) mais permet une compilation beaucoup plus rapide que son prédécesseur grâce à la séparation des librairies du code métier. Il permet aussi de spliter beaucoup plus facilement son projet tout en préservant une communication entre les différents modules.