Simplifier le bricolage de navigateur

Il n'y a à vrai dire pas beaucoup de différences entre Firefox et une application Web riche. Les deux reposent sur quatre technologies:

  • un langage de description d'interface: HTML dans un cas, XUL dans l'autre, mais les deux sont de proches cousins. XUL est juste un peu plus spécialisé, fournit plus d'éléments d'interface riche que HTML[1];
  • des feuilles de styles pour gérer le rendu. Ce sont exactement les mêmes. Un thème Firefox n'est qu'un ensemble de feuilles de style et d'images. Personas, intégré à Firefox 3.6, en est une illustration : on peut modifier le look de son navigateur aussi simplement que celui d'un site Web avec Stylish;
  • JavaScript, un langage pour rendre le tout dynamique et interagir avec l'utilisateur;
  • une plate-forme offrant des fonctionnalités de bas niveau et s'occupant de mettre en musique les trois précédents composants. Dans le cadre d'une application Web, cette plate-forme est le navigateur. Dans le cadre de Firefox, c'est son moteur interne, XulRunner. Mais en fait, c'est la même chose, puisqu'au cœur de Firefox, le même moteur s'occupe aussi bien de l'affichage du Web que de l'interface du navigateur.

La principale différence entre une application Web et Firefox, c'est le langage de description d'interface. Mais avec les évolution de HTML et des bibliothèques de composants JavaScript, HTML peut remplacer XUL dans de nombreux cas. Au passage, Flex d'Adobe utilise exactement la même pile : MXML, un cousin de XUL, CSS, ActionScript (grand frère de JavaScript) et le lecteur Flash comme moteur.

Dès lors, étendre Firefox ne devrait pas être beaucoup plus difficile que développer une application Web, et à la portée de quiconque a des notions de HTML, CSS, JavaScript. En étudiant les extensions les plus populaires sur AMO, des membres des MozLabs se sont aperçu que la vaste majorité ne faisait pas d'appels complexes à des composants bas niveau de la plateforme, et n'avaient pas besoin de toute la puissance du mécanisme actuel d'extension, puissance qui se paie par une petite complexité : même si réaliser une extension n'est pas très compliqué, cela nécessite un apprentissage, aussi bien de nouvelles techniques que d'un environnement de développement spécifique. Jetpack est là pour abaisser cette barrière, rendre beaucoup plus facile, accessible, l'ajout de nouvelles fonctionnalités à Firefox. Il proposera une alternative pour bricoler facilement son navigateur. A terme, le but est que la majorité des extensions utilise cette architecture, le système actuel restant bien sûr disponible pour les développeurs ayant besoin de fonctions plus poussées.

Ainsi, Jetpack permettra à n'importe quel développeur Web d'étendre son navigateur en utilisant les outils qu'il connaît : HTML, CSS, JavaScript, JQuery et Firebug. L'environnement est familier, tout se passe dans Firefox, les mises à jour sont instantanées sans relancer le logiciel. Par ailleurs, JetPack devrait également apporter un gain de sécurité: actuellement, toutes les extensions disponible sur AMO sont contrôlées afin de s'assurer qu'elles ne contiennent pas de code malveillant. C'est un travail long et fastidieux. Jetpack devrait simplifier ce travail, et augmenter le nombre d'internautes qui auront les compétences pour lire le code et s'assurer de son innocuité.

Premiers pas

L'architecture est très similaire à celle de GreaseMonkey : des scripts chargés depuis une page Web s'exécutent dans un bac à sable et interagissent avec le navigateur via une API.

Attention, Jetpack est en plein développement, et certaine des informations indiquées ici seront sans doute obsolète dans quelques mois. Même si les versions se succèdent assez vite, Jetpack est encore assez instable. Il peut ne pas fonctionner avec certaines versions de Firefox, avoir des problèmes de compatibilité avec certaines versions de Firebug, et planter avec certains OS. Comme il embarque des binaires, par exemple pour la gestion de l'audio et de la vidéo, vous risquez d'avoir des soucis sous GNU/Linux en 64bits. C'est parfois frustrant, j'ai eu du mal à le faire fonctionner, mais je suis content de m'être acharné. Par ailleurs, Jetpack étant codé en JavaScript, on peut facilement accéder au code pour comprendre comment il fonctionne et le patcher. Les modifications elles-mêmes sont prises en compte instantanément. Les extraites que je donne plus bas ont été testés avec Jetpack 0.6 alpha et Iceweasel 3.5.3 sous Debian GNU/Linux[2].

Une fois Jetpack installé, vous accédez à sa tour de contrôle en tapant about:jetpack dans la barre d'url. Vous deviez avoir quelque chose comme ça: Jetpack

Si les onglets ne s'affichent pas, c'est qu'il y a un problème, regardez la console d'erreur, allez faire un tour dans le code ou viendez sur IRC.

Les deux principaux onglets sont le 2° et le 3°:

  • Develop fournit un éditeur où développer votre code. Attention, contrairement aux apparences ce n'est pas encore Bespin mais une simple textarea[3];
  • Intalled Features liste les extensions installées et vous permet de les gérer.

Une fois votre script développé, il ne vous reste plus qu'à l'installer. Pour cela, rien de plus simple, comme pour une commande Ubiquity il vous suffit de créer une page HTML contenant un lien vers le fichier : <link rel="jetpack" href="toto.js">. En visitant cette page, Firefox vous proposera d'installer l'extension.

L'API

Toutes les fonctions de Jetpack sont regroupées dans un seul objet, qui sert d'espace de nom de base. On appelle ensuite les propriétés et les méthodes des modules disponibles. Par exemple, on obtiendra l'onglet courant avec jetpack.tabs.focused, soit la propriété focused du module tabs. Pour l'instant, une dizaine de modules sont déjà disponibles "en standard", qui ne cessent de s'enrichir. Les plus intéressants sont:

  • jetpack.info permet d'obtenir des informations sur le système;
  • jetpack.json pour encoder et décoder des données en JSON (euh, pourtant c'est implémenté nativement depuis FF 3.5 ?);
  • jetpack.notifications.show(message) affiche une notification;
  • jetpack.tabs permet d'intéragir avec les onglets du navigateur : obtenir l'onglet courant, ouvrir une page dans un nouvel onglet, et attacher des fonctions à des évènements. On pourra par exemple obtenir le document en cours avec jetpack.tabs.focused.contentDocument;
  • jetpack.statusBar.append(obj) pour ajouter des objets à la barre de status;
  • jetpack.sessionStorage enregistre des informations dans la session.

Outre jetpack, quelques autres objets sont disponibles dans le bac à sable, les plus utiles étant jQuery et console, un dérivé de la fonction du même nom de Firebug. Jetpack utilisera la console de Firebug s'il la détecte, ou à défaut celle du système.

Extensibilité

Jetpack est prévu pour pouvoir être facilement étendu avec des bibliothèques tierces. L'espace de nom jetpack.lib est réservé pour cet usage. A titre d'exemple, une bibliothèque de communication avec Twitter est déjà fournie. Ces bibliothèques peuvent elle-même communiquer avec des composants binaires externes, via un système de membranes.

Le futur

Jetpack veut être à la fois une plate-forme stable et un lieu d'expérimentation. Comme en Python, on peut donc utiliser des fonctionnalités expérimentales en les important. Elles resteront dans cet espace le temps que leur API se stabilise. La JEP 13 définit une syntaxe très simple:

jetpack.future.import("toto");
var titi = jetpack.toto.tata();

Une vingtaine de modules sont en travaux, n'hésitez pas à participer à leur élaboration ou si vous ne trouvez pas votre bonheur, à proposer une JEP. Voici un petit tour de quelques fonctionnalités en cours d'implémentation:

La slidebar

La slidebar est une réinvention de la sitebar qui existe depuis très longtemps. Pour l'instant on y accède via une petite flèche à gauche du premier onglet. Les slides se présentent comme des onglets verticaux: La slidebar de Jetpack Elle peut contenir de nombreux slides. Chacun est un simple objet que l'on ajoute à la barre. Par exemple:

jetpack.future.import("slideBar");
jetpack.slideBar.append({
  icon: "", // URL de l'icône
  html: "", // contenu du slide
  url: "",  // pour charger le contenu depuis une URL distante
  width: 300, // largeur du slide
  persist: true, //
  autoReload: true, //
  onClick: function(slide){},
  onSelect: function(slide){},
  onReady: function(slide){}
});
Du multi-média

Le module audio permet d'accéder au micro de l'ordinateur. Le résultat est soit enregistré dans un fichier au format OGG/Vorbis, soit accessible sous forme de flux. Le module vidéo en fera de même pour la vidéo. Quant à Music, il permettra d'accéder aux fichiers multimédias locaux, en fonctions des interfaces de chaque système d'exploitation (par exemple iTunes sur Mac). On pourra par exemple demander à Firefox de jouer l'Hymne avec ces simples lignes:

 jetpack.future.import('music');
 let tracks = jetpack.music.search('Free Software Song');
 jetpack.music.playTrack(tracks0);
Et le stockage local ?

Jetpack implémentera plusieurs moyens pour enregistrer des données. Un premier, live, permet déjà d'enregistrer des données dans la session (mais qui seront perdues au redémarrage):

jetpack.storage.live.myData = {hello: "world"};

Pour enregistrer localement tout type de données, un premier module est en cours de développement. C'est une simple base de clés/valeur :

jetpack.storage.simple.prop1 = "toto";
jetpack.storage.simple.prop2 = {a: 'b', b: 'a'};
jetpack.storage.simple.prop3 = [1, 3, 3, 7];

Jetpack 0.6 offre les débuts du module de gestion des paramètres, défini par la JEP 24. L'implémentation est balbutiante, mais la spécification prometteuse: il suffit de déclarer au début de votre script un manifeste décrivant les paramètres. Jetpack créera automatiquement la boîte de dialogue pour les gérer (accessible depuis la liste des extensions installée):

 var manifest = {
   settings: [
     {id:"autosave",
      type:"range",
      label:"Autosave",
      min: 10,
      max: 600,
      default: 30
     }
   ]
 };
 (...)
 var toto = jetpack.storage.settings.autosave;

Je pense que d'autres modules de stockage devraient rapidement voir le jour, par exemple pour s'interfacer avec des base SQLite ou CouchDB.

Et encore
  • la JEP 10 définit l'accès au presse-papier via des getter/setter;
  • on accède à la sélection via le module jetpack.selection décrit par la JEP 12;
  • pageMods permet d'associer des scripts à des pages, en fonction d'expressions régulières, comme avec GreaseMonkey;
  • enfin, grosse nouveauté de la 0.6, la gestion de menus qui permet très facilement de rajouter des items dans la barre de menu ou le menu contextuel. Rajouter une action dans le menu contextuel des images est aussi simple que:
jetpack.menu.context.page.on('img').add(function(context)({
  label: "Mon menu",
  command: function(target) {}
}));

Allez voir la vidéo de présentation dans l'annonce de la sortie de la 0.6. Elle est assez bluffante !

A venir

D'autres JEP sont en cours de rédaction. Par exemple pour gérer les barres d'outil. Et bien sûr, chacun est invité, s'il ne trouve pas son bonheur dans l'API existante, à écrire une proposition et une implémentation d'exemple.

Assez causé, du code !!

Quelques bouts de code pour vous donner une idée de sa simplicité. Jetpack permet vraiment de faire beaucoup de choses en quelques lignes. Pour tester différents modules, j'ai commencé à coder une extension stockant des extraits de pages dans la slidebar. Ce sont mes tâtonnements, un travail en cours juste pour apprendre, donc pas très propres ni au point. Pour en voir plus, vous devriez allez jeter un œil aux nombreuses démos disponibles sur le site. Une galerie devrait également voir le jour très prochainement.

On commence par importer tout ce dont on aura besoin. A terme, une fois que l'API de ces modules sera figée, ils seront disponibles par défaut et on pourra se passer de cette phase

 jetpack.future.import("menu");
 jetpack.future.import("selection");
 jetpack.future.import("slideBar");
 jetpack.future.import("storage.settings");
 jetpack.future.import("storage.simple");

Le manifeste décrivant les paramètres de l'extension. Jetpack va créer automatiquement une fenêtre de configuration. En l'état actuel, elle n'est pas encore complètement fonctionnelle. J'utilise ici un range, nouveau type de champ de formulaire défini par HTML5.

 var manifest = {
   settings: [
     {id:"autosave", type:"range", label:"Autosave", min: 10, max: 600, default: 30 }
   ]
 };

On crée d'un slide dans la slidebar. Pour ne pas surcharger le code, le HTML correspondant est dans un fichier à part, qui contient en outre quelques fonctions Javascript pour gérer les extraits. (le code du slide contient un <div id="accordion"></div> à l'intérieur duquel je stocke les extraits). J'utilise ici le stockage local pour initialiser le slide en rechargeant les extraits précédemment enregistrés, et je déclenche une sauvegarde régulière, le délai entre deux enregistrements étant défini dans les paramètres.

 var slider = jetpack.slideBar.append({
   url: "snips.html", // la description est dans un fichier séparé
   width: 500,        // largeur du slide
   persist: true,     // pour qu'il reste ouvert
   autoReload: false, // on ne recharge pas son contenu à chaque fois
   onReady: function(slide){ // fonction appelée à la création du slide
     current = slide;
     var doc = current.contentDocument;
     // on recharge le contenu depuis le stockage local :
     $('#accordion', doc).html(jetpack.storage.simple.content);
     // fonction pour sauvegarder périodiquement le contenu de la diapo
     interval = setInterval(function(){
         jetpack.storage.simple.content = $('#accordion', doc).html();
       }
       , jetpack.storage.settings.autosave);
   }
 });

On ajoute à présent un item dans le menu contextuel pour créer un extrait à partir de la sélection:

 jetpack.menu.context.page.add(function(context)({
   label: "Create snippet",
   command: function(target) {
     // on appelle une méthode pour enregistrer l'élément courant
     // dans une nouvelle diapo
     // la variable current référence notre slide
     // astuce : pour accéder aux objets JS définis dans le code HTML
     // de la diapo, il faut utiliser wrappedJSObject
     current.contentDocument.defaultView.wrappedJSObject.accordionize(jetpack.selection.html, context.document.location.href, null, 'Snippet');
     // on sauvegarde le nouveau contenu du slide
     jetpack.storage.simple.content = $('#accordion', current.contentDocument).html();
   }
 }));

Et c'est tout !

Je me suis amusé à proposer une méthode alternative de sélection de contenus : en cochant une case dans la barre de status, la page passe en édition, c'est à dire qu'on peut créer un extrait à partir de tout élément possédant un identifiant. Voici le code pour ajouter une case à cocher dans la barre de status:

 jetpack.statusBar.append({
   html: 'Snip <input type="checkbox" />',
   onReady: function(widget){
     // lorsqu'un onglet est sélectionné, on décoche la case
     jetpack.tabs.onFocus(function(){
       $("input", widget).attr('checked', false);
     });
     // Gestion du click de la case
     $("input", widget).click(function(){
       // recherche de tous les éléments qui ont un id
       var elmts = $(jetpack.tabs.focused.contentDocument).find('*[id]');
       if( this.checked ){
         elmts.bind('mouseenter', snipable);
       } else {
         elmts.unbind('mouseenter', snipable);
       }
     });
   }
 })

Ca n'a rien à voir avec Jetpack, mais voici juste le code de la fonction utilisée pour sélectionner les éléments:

 function snipable() {
   var elmt = $(this);
   var css = elmt.css('border');
   elmt.css('border', '1px solid red');
   elmt.bind('mouseleave', function(){
     $(this).css('border', css).unbind('mouseleave').unbind('dblclick');
   }).bind('dblclick', function(e){
     e.stopPropagation();
     console.log('click : ' + elmt.attr('id'));
     if (current) {
       current.slide();
       current.contentDocument.defaultView.wrappedJSObject.accordionize(elmt.html(), tabs.focused.url, null, 'Snippet');
     }
     return false;
   });
 }

Et pour installer mon extension, un simple fichier HTML suffit. Si votre extension nécessite d'autres fichiers, référencés dans le script, ils devront être situés au même endroit que le script (pour ce que j'en ai compris, Jetpack cache dans une base SQLite locale les extensions installées. Au moment de l'installation, il vous demande s'il doit se connecter régulièrement au serveur pour récupérer des mises à jour)

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
 <head>
   <title>Snips</title>
   <link rel='jetpack' href='snips.js' />
 </head>
 <body></body>
 </html>

Bon, ça suffit maintenant monsieur

Techniquement, je trouve Jetpack moins révolutionnaire qu'Ubiquity. Ce n'est au final qu'un Greasemonkey complètement intégré à Firefox. Certes très bien intégré, mais le concept n'est pas neuf (le mécanisme d'extensions de Chrome est je crois bâti sur le même modèle). Je ne peux donc m'empêcher de regretter que ce mariage de GreaseMonkey et du Panda n'ait pas eu lieu plus tôt. Car l'idée est excellente et surtout impressionnante de puissance et de simplicité. C'est je crois cette simplicité qui provoque réellement une rupture et mon enthousiasme. Tout développeur Web connaissant JavaScript peut réellement bidouiller le navigateur en quelques minutes. La technique s'efface de plus en plus, pour laisser chacun faire ce qu'il veut de l'outil, pour en démultiplier les possibilités. Et Jetpack est un beau terrain de jeu pour réfléchir et expérimenter de nouvelles pistes pour rendre le bidouillage de Web toujours plus simple, toujours plus accessible à de plus en plus d'internautes. Au final c'est bien ça qui compte. Bon alors, qu'est-ce que vous attendez pour aller essayer ?

Notes

[1] à vrai dire, HTML n'est pas fait pour décrire des interfaces mais des documents. Malheureusement, aucun autre langage spécifique pour les interfaces n'ayant été normalisé, il a été utilisé par défaut et s'est imposé. HTML5 valide cette évolution;

[2] à noter que Jetpack étant prévu pour fonctionner avec toute application basée sur XulRunner, un test est pour l'instant réalisé "en dur" pour différencier Firefox et Thunderbid. Si vous utilisez des variantes portant d'autres nom de ces logiciels, comme par exemple Iceweasel, il vous faudra modifier le fichier modules/xulapp.js situé dans le répertoire de l'extension.

[3] Bespin n'est pour l'instant activé que dans la version Mac OS X, mais j'ai réussi à la faire fonctionner sans trop de problèmes sous Nunux.