Ecrire un plugin Confluence avec React et ES6


Publié par
Christophe PROME

10 juillet 2015

L’utilisation du javascript est de plus en plus importante dans nos add-ons: il permet d’apporter plus de souplesse dans l’utilisation d’un produit et d’offrir une interface réactive aux utilisateurs. De nombreuses librairies JS apparaissent d’ailleurs tous les jours pour en faciliter le développement. De fait, le développement d’une application javascript devient de plus en plus complexe et il n’est pas toujours aisé d’intégrer de nouveaux outils dans nos plugins Confluence.

Dans cet article, vous allez voir comment profiter des nouveautés du langage avec ES6 ainsi que l’intégration de la librairie React, à travers la création d’un plugin Confluence fonctionnel. Mais dans un premier temps, je me dois de faire les présentations.

Présentation

React

React est une librarie javascript créée par Facebook qui permet d’écrire de façon simple des vues en Javascript. Dans une application React, la vue est décomposée en composants de plus ou moins haut niveau. Chaque composant étant composé de sous-composant, le tout formant un arbre. L’idée est de structurer son application de la même façon que le DOM que l’on souhaite produire. Pour en savoir plus, je vous invite à consulter la documentation.

ES6

ES6 (ECMAScript 6 ou ES2015) est la prochaine version de Javascript, c’est un standard depuis juin 2015. Cette version apporte de nombreuses améliorations au langage comme les arrow functions, les classes ou les modules. La version actuelle de javascript est ES5 (ECMAScript 5) et elle date de 2009 ! La prochaine version (ES7, vous l’aurez deviné) est prévue pour 2016, en effet le comité chargé de faire évoluer les specs a décidé de créer des releases plus fréquemment. Si vous souhaitez en savoir plus, je vous suggère cet article qui résume bien les nouveautés ainsi que le livre d’Axel Rauschmayer : Exploring ES6  (consultable gratuitement en ligne).

Sources du tutoriel

Le code source de ce tutoriel est disponible sur le bitbucket de Valiantys. A chaque étape correspond un commit, pour faciliter votre compréhension vous pouvez cloner ce projet et vous positionner sur le commit correspondant à l’étape étudiée.

Prérequis

Afin de suivre ce tutorial, il vous faut installer ces outils :

Il est aussi nécessaire d’avoir des bases en Java, Javascript et dans l’écriture de plugin Confluence. Cette méthode a été testée sur les versions 5.6.6 et 5.7.2 de Confluence.

Etape 1 : Création du plugin

commit:  12ad8dd

La première étape est la création du plugin Confluence.

Génération

Nous allons utiliser l’utilitaire installé par l’Atlassian Plugin SDK. Ouvrez un terminal dans le répertoire où vous souhaitez créer le plugin, tapez atlas-create-confluence-plugin et entrez les valeurs suivantes :

Define value for groupId: : com.valiantys.confluence.plugin.helloreactes6
Define value for artifactId: : hello-react-es6
Define value for package:  com.valiantys.confluence.plugin.helloreactes6

Nettoyage du pom

Pour cet exercice nous avons besoin de peu de dépendences Maven, supprimez les toutes sauf :

<dependency>
    <groupId>com.atlassian.confluence</groupId>
    <artifactId>confluence</artifactId>
    <version>${confluence.version}</version>
    <scope>provided</scope>
</dependency>

Configuration de la macro

Nous allons créer un plugin de type Macro, éditez le fichier atlassian-plugin.xml situé dans le répertoire src/main/resources : supprimez les blocs <web-resource> et <component> et ajoutez un bloc <xhtml-macro> :

<xhtml-macro name='hello-react-es6' class='com.valiantys.confluence.plugin.helloreactes6.HelloMacro' key='hello-react-es6' hasBody="false">
    <parameters></parameters>
</xhtml-macro>

Ce bloc déclare note macro, il fait également référence à une classe Java que nous allons créer : HelloMacro. Supprimez les répertoires src/main/resources/css, src/test/ et le contenu de src/main/resources/js.

Création de la macro Java

Supprimez les classes Java générées par le SDK dans src/main/java et créez la classe HelloMacro.java dans le package com.valiantys.confluence.plugin.helloreactes6. Cette classe doit implémenter l’interface com.atlassian.confluence.macro.Macro ainsi que les méthodes suivantes :

@Override
public BodyType getBodyType() {
    // Notre macro n'a pas de corps.
    return BodyType.NONE;
}

@Override
public OutputType getOutputType() {
    // On génère fragment HTML isolé du reste de la page, entouré de balises <p>
    return OutputType.INLINE;
}

@Override
public String execute(Map<String, String> parameters, String body, ConversionContext conversionContext) throws MacroExecutionException {
    // Cette méthode sera appelé à chaque fois que Confluence croisera notre macro dans un page et la remplacera par la chaîne "Hello"
    return "<div>Hello !</div>";
}

Executer le plugin

La première version de notre plugin est prête, il ne reste plus qu’à l’exécuter. Ouvrez un terminal à la racine du projet :

atlas-run

Cette commande va télécharger les dépendences maven, packager le projet et démarrer un Confluence en mode standalone. Un bout d’un (long) moment, votre Confluence est accessible à l’adresse http://localhost:1990/confluence. Il ne reste plus qu’à créer une page et y insérer notre macro, le résultat est de toute beauté : Capture d'écran de 2015-06-17 18:44:43 Il est temps d’ajouter un peu de Javascript.

Etape 2 : Ajout du javascript

commit e6f847d

Dans cette étape, nous allons ajouter du javascript qui sera automatiquement exécuté dans chaque page où la macro est présente.

hello-react-es6.js

Dans le dossier src/main/resources/js créez le fichier hello-react-es6.js :

AJS.toInit(function() {
    console.log('Hello Javascript');
});

AJS est présent dans toutes les pages Confluence, ici nous passons un callback à la fonction toInit qui est appelée une fois la page chargée. Pour le moment, nous écrirons ‘Hello Javascript’ dans la console du navigateur.

Chargement du javascript

Ajoutez le bloc suivant dans le fichier atlassian-plugin.xml :

<web-resource key="hello-react-es6-resources" name="hello-react-es6 Web Resources">
    <resource type="download" name="hello-react-es6.js" location="js/hello-react-es6.js"/>
    <context>page</context>
</web-resource>

Ici, nous informons Confluence qu’à chaque fois que notre macro est présente dans une page, notre code javascript doit être chargé.

Recharger le plugin

La commande atlas-run tourne toujours, nous devons recharger le plugin afin qu’il prenne en compte les changements. Dans un terminal, depuis la racine du projet :

atlas-cli

puis à l’invite de commande maven>

pi

Confluence Plugin React ES6 - atlas-cli Une fois le plugin rechargé: ouvrez l’outil de debug de votre navigateur, rechargez la page, vous devriez voir le message suivant dans la console : Confluence Plugin React ES6 - Step2 Confluence a détecté notre macro et a chargé le javascript associé. Il est temps de parler ES6 !

Etape 3 : ES6 avec webpack

commit 1866e83

Comme les navigateurs actuels ne comprennent pas (totalement) le javascript ES6, il est nécessaire de le convertir (transpiler) en javascript ES5. L’intérêt est que nous pouvons profiter des fonctionnalités à venir du langage tout en restant compatible, et lorsque les navigateurs seront compatibles avec ES6, cette étape de transpilation ne sera plus nécessaire. Pour ce faire, nous utiliserons webpack.

Initialisation de npm

Dans un terminal, depuis la racine du projet, tapez les commandes suivantes :

# Intialisation de npm, laissez vous guider
npm init
# Ajout de la dépendance babel-loader, utilisée par webpack
npm i -D babel-loader

Ces commandes vont créer le fichier package.json à la racine de notre projet. Ce fichier décrit un module npm ainsi que ses dépendances. Ici nous avons ajouté la dépendance babel-loader qui permet à webpack de transpiler du javascript ES6 en ES5. Pour en savoir plus, je vous invite à consulter le site de Babel.

Premier code ES6

Supprimez le répertoire src/main/resources/js et créez le répertoire src/main/javascript. Créez dans ce répertoire le fichier main.js.

AJS.toInit(function() {
    ['webpack', 'babel', 'es6'].forEach(label => console.log(`Hello ${label}`));
});

Voici nos premières lignes en ES6 ! Ici nous utilisons deux nouveautés :

  • les arrows functions qui simplifient l’écriture des callback
  • les templates strings qui permettent d’inclure des variable dans la génération de strings.

Le but de cet article n’étant pas de vous apprendre ES6, nous en resterons là.

Configuration de webpack

La configuration de webpack se fait via le fichier webpack.config.js situé à la racine du projet :

module.exports = {

    entry: {
        main: "./src/main/javascript/main.js"
    },
    output: {
        path: "./target/generated-resources/js/",
        filename: "hello-react-es6.js"
    },
    module: {
        loaders: [
            { 
                test: /\.js$/, 
                exclude: /node_modules/,
                loader: "babel-loader" 
            }
        ]
    }
};

Le champ entry spécifie le fichier source qui devra être transpilé. Dans output, nous renseignons le nom et l’emplacement du fichier résultat. Et enfin, module est la liste des modules à charger. Chaque fichier ayant l’extension .js sera être traité par le loader babel (nous excluons le dossier /node_modules/ qui contient les dépendances npm).

Mise à jour du pom

Webpack va donc produire un fichier javascript dans le répertoire target/generated-resources/js. Afin que Maven intègre cette dépendance dans son build, il faut ajouter un block <resources> dans le pom.xml :

<build>
	(...)
    <resources>
        <resource>
        	<directory>${project.basedir}/src/main/resources</directory>
        </resource>
        <resource>
        	<directory>${project.basedir}/target/generated-resources</directory>
        </resource>
    </resources>
</build>

Build !

Nous allons maintenant pouvoir lancer le build. Dans un terminal, à la racine du projet lancez le build webpack via la commande du même nom. Confluence Plugin React ES6 - Webpack Cette commande va créer le fichier target/generated-resources/js/hello-react-es6.js, il est intéressant d’y jeter un oeil pour y découvrir le code ES5 généré par babel :

/***/ function(module, exports) {

	'use strict';

	AJS.toInit(function () {
	    ['webpack', 'babel', 'es6'].forEach(function (label) {
	        return console.log('Hello ' + label);
	    });
	});

/***/ }

Vous noterez que le code généré est du Javascript ES5 parfaitement valide. On remarquera également que webpack englobe notre code dans une IIFE, il n’y a donc aucun risque de collision avec un autre plugin présent sur la page. Afin que Confluence prenne en compte la mise à jour du pom, il est nécessaire de relancer Confluence via atlas-run. Rechargez votre page Confluence, et dans les logs vous pouvez voir que votre code fonctionne : Confluence Plugin React ES6 - Step3 log

 Build… automatique

Afin de rendre le développement plus agréable, passez l’option –watch à webpack : il rebuildera le javascript à chaque changement du code source. Ainsi, un simple rafraîchissement de la page est suffisant pour tester une modification.

Etape 4 : Soyons Reactifs

commit f06c0cb

Grâce à ses plugins, webpack est un outil très puissant. Jusqu’à présent, babel nous permet de profiter des fonctionnalités d’ES6. Nous allons aller un peu plus loin en exploitant une autre fonctonnalité de babel : transpiler du jsx.

Jsx ?

Jsx est le langage créé par Facebook qui permet d’écrire la vue des composants React. Sa syntaxe est très proche de l’HTML avec des petits plus. Ce sujet étant hors du scope de cet article, je vous invite à en découvrir plus avec cet article.

Ajouts des dépendances

Afin de pouvoir écrire un composant React, il faut ajouter deux dépendances à notre projet :

npm i -D react lodash

1er composant React

Créez le répertoire src/main/javascript/components. Dans ce répertoire, créez le fichier Hello.jsx :

import React from 'react';

export default React.createClass({

    render: function() {
        const messages = ['webpack', 'babel', 'es2015'].map(label => (
            <div className="aui-message">
                <p className="title">
                    <strong>Hello {label}!</strong>
                </p>
            </div>
        ));
        return (<div>{messages}</div>);
    }

});

La fonction principale du composant est render(). Sa responsabilité est de renvoyer un composant React (ici écrit en jsx) qui sera ensuite utilisé par React pour générer le DOM du composant. Ici nous générons un div qui contient trois p. Afin d’intégrer ce composant, nous allons modifier le fichier main.js :

import React from 'react';
import {forEach} from 'lodash';

import Hello from './components/Hello.jsx';

AJS.toInit(function() {
    const wrappers = document.querySelectorAll('.hello-react-es6');
    _.forEach(wrappers, wrapper => React.render(<Hello/>, wrapper));
});

Ce code va insérer un composant Hello.jsx pour chaque balise portant la classe .hello-react-es2015. Vous noterez l’utilisation des mots clés import et export, c’est également une nouveauté d’ES6 qui permet d’importer des dépendances internes (Hello.jsx) ou externes (react et lodash, installées via npm). Il ne reste plus qu’une chose à faire, éditer le fichier HelloMacro.java :

@Override
public String execute(Map<String, String> parameters, String body, ConversionContext conversionContext) throws MacroExecutionException {
    return "<div class='hello-react-es6'></div>";
}

Ainsi, notre application javascript détectera chaque instance de plugin sur une page et les remplacera par les blocks p générés par notre composant React.

Configuration de webpack

Afin que webpack puisse interpréter les fichiers .jsx, il faut mettre sa configuration à jour, éditez le fichier webpack.conf.js :

module: {
    loaders: [
        { 
            test: /\.(js|jsx)$/, 
            exclude: /node_modules/,
            loader: "babel-loader" 
        }
    ]
}

Et voilà ! Relancez webpack pour prendre en compte la nouvelle configuration, exécuter un pi pour installer la nouvelle version du plugin et enfin actualisez la page pour admirer le résultat : Confluence Plugiin React ES6 - Final result

Intégrer webpack dans le build maven

commit fadf6cc54a

Afin que les javascript soient inclus par maven lors d’un build, il faut ajouter une étape de build au pom.xml, voici le fichier complet :

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.valiantys.confluence.plugin.helloreactes6</groupId>
    <artifactId>hello-react-es6</artifactId>
    <version>1.0-SNAPSHOT</version>

    <organization>
        <name>Valiantys</name>
        <url>http://www.valiantys.com/</url>
    </organization>

    <name>hello-react-es6</name>
    <description>This is the com.valiantys.confluence.plugin.helloreactes6:hello-react-es6 plugin for Atlassian Confluence.</description>
    <packaging>atlassian-plugin</packaging>

    <profiles>
        <profile>
            <id>windows-dev-platform</id>
            <activation>
                <os>
                    <family>windows</family>
                </os>
            </activation>
            <properties>
                <npm.exec>npm.cmd</npm.exec>
            </properties>
        </profile>
    </profiles>

    <dependencies>
        
        <dependency>
            <groupId>com.atlassian.confluence</groupId>
            <artifactId>confluence</artifactId>
            <version>${confluence.version}</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>maven-confluence-plugin</artifactId>
                <version>${amps.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <productVersion>${confluence.version}</productVersion>
                    <productDataVersion>${confluence.data.version}</productDataVersion>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.4.0</version>
                <executions>
                    <!-- Install npm dependencies                        -->
                    <execution>
                        <id>install-npm-deps</id>
                        <phase>initialize</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>${npm.exec}</executable>
                            <commandlineArgs>install</commandlineArgs>
                        </configuration>
                    </execution>

                    <!-- Generate js from resources                        -->
                    <execution>
                        <id>generate-js-from</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>${npm.exec}</executable>
                            <commandlineArgs>run-script buildjs</commandlineArgs>
                        </configuration>
                    </execution>

                </executions>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>${project.basedir}/src/main/resources</directory>
            </resource>
            <resource>
                <directory>${project.basedir}/target/generated-resources</directory>
            </resource>
        </resources>
    </build>

    <properties>
        <confluence.version>5.7.2</confluence.version>
        <confluence.data.version>5.7.2</confluence.data.version>
        <amps.version>5.0.13</amps.version>
        <plugin.testrunner.version>1.2.3</plugin.testrunner.version>
        <npm.exec>npm</npm.exec>
    </properties>

</project>

Nous faisons référence à un script npm qu’il est nécessaire d’ajouter dans le fichier package.json :

// (...)
"scripts": {
    "buildjs": "webpack"
},
// (...)

De cette manière, webpack sera lancé par maven lors d’un packaging :

[INFO] ------------------------------------------------------------------------
[INFO] Building hello-react-es6 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:1.4.0:exec (install-npm-deps) @ hello-react-es6 ---
npm WARN package.json hello-react-es6@1.0.0 No repository field.
[INFO] 
[INFO] --- maven-confluence-plugin:5.0.13:copy-bundled-dependencies (default-copy-bundled-dependencies) @ hello-react-es6 ---
[INFO] 
[INFO] --- exec-maven-plugin:1.4.0:exec (generate-js-from) @ hello-react-es6 ---

> hello-react-es6@1.0.0 buildjs /home/christophe/dev/projects/hello-react-es6
> webpack

Hash: c13fc492965874f0df9d
Version: webpack 1.9.11
Time: 3384ms
             Asset     Size  Chunks             Chunk Names
hello-react-es6.js  1.06 MB       0  [emitted]  main
    + 160 hidden modules
[INFO] 

Et après ?

Avec ce tutoriel, nous avons pu voir comment profiter des dernières technologies Javascript et les intégrer dans un plugin Confluence. Mais nous n’avons fait que survoler les possibilités offertes par ces outils, nous aurions aussi pu voir comment utiliser un préprocesseur CSS ou intégrer un linter Javascript. Maintenant c’est à vous de jouer et n’hésitez pas à commenter pour partager vos expériences :)