Développer une extension pour navigateur

Dans cet article nous allons vous expliquer comment nous avons créé notre première extension pour navigateur : SideTwitch (disponible pour Firefox et Chrome). Le but de cette extension est de permettre d’embarquer un lecteur Twitch dans votre navigateur. Ce lecteur va vous suivre lorsque vous changez d’onglet et ainsi vous permettre de suivre vos streamers préférés tout en continuant votre navigation. Le code de cette extension est open source et est disponible sur GitHub.

Bien sûr, de nombreuses ressources existent sur le web pour débuter dans la création d’extensions, dans cette aventure vos meilleurs amis vont être :

Ces guides sont très complets et vous permettront de bien commencer votre développement étape par étape. C’est alors que vous allez vous demander : “Comment je fais pour que ça marche sur tous les navigateurs de l’univers ?”. Bonne question et bonne nouvelle, la WebExtensions API est commune pour Chrome, Firefox et Opera. Un seul développement, pour trois d’un coup ! En sachant que Chrome et Firefox sont les navigateurs les plus populaires vous n’avez donc pas réellement besoin de chercher plus loin.

Dans cet article nous allons donc nous focaliser sur les éléments importants d’une extension et notamment sur les communications entre ces derniers.

Les éléments importants

Parlons maintenant des éléments caractéristiques d’une extension.

manifest.json

Le manifest est le principal fichier de votre extension. C’est même l’unique élément obligatoire de celle-ci. Vous pouvez trouver notre fichier complet ici.
La première partie de votre manifest va concerner des informations générales comme la version du manifest que vous utilisez, le nom de votre extension, une description, la version ou encore les permissions.

 "manifest_version": 2,
    "name": "SideTwitch",
    "description": "Embed Twitch in your browser",
    "version": "0.0.3",
    "permissions": [
        "activeTab",
        "tabs"
    ],

Informations générales et permissions pour les onglets

La partie concernant les permissions est très importante car elle va définir les actions de la WebExtensions API que vous allez pouvoir utiliser (et donc des permissions demandées à l’utilisateur lors de l’installation).

Les actions de la WebExtension API sont des méthodes et fonctions vous donnant accès à la manipulation des éléments de votre navigateur. Par exemple, les actions ci-dessous donnent accès à la gestion des événements de création, mise à jour et fermeture des onglets, pour ne citer que ceux-ci. Il en existe de nombreuses autres, la liste complète est disponible ici.

"web_accessible_resources": [
    "html/wrapper.html",
    "css/wrapper.css",
    "img/x-circle.svg",
    "img/move.svg",
    "img/play-button.png"
]

Les ressources

La partie concernant les ressources web accessibles est aussi importante.
Dans une extension, nous ne pouvons pas accéder à des ressources statiques si jamais elles ne sont pas définies dans le manifest (pour des raisons de sécurité). Il s’agit là une source d’erreur très fréquente lorsque l’on débute.

Background

Ce script est, comme son nom l’indique, lancé en arrière plan de votre navigateur. Il reste actif en permanence. C’est un point important, puisque c’est grâce à cette fonctionnalité que nous allons pouvoir définir des comportements qui vont persister sur les différentes pages. Pour utiliser un script background, nous devons le définir dans le manifest de la façon suivante :

"background": {
    "scripts": [
        "js/background.js"
    ]
},

Définition du background dans le manifest

Dans notre cas d’utilisation, ce script va permettre de coordonner les content-scripts (voir plus loin) de nos différents onglets, de recevoir et de stocker des informations issues du formulaire. Par exemple :

chrome.tabs.onCreated.addListener(function(tab) {
   // si l’utilisateur a choisi une chaîne Twitch
    if (!!currentChannel) {
        // on notifie nos content-scripts pour une création dans le DOM
        notifyContainerCreation()
    } else {
        // on notifie nos content-scripts pour une suppression dans le DOM
        notifyContainerDeletion()
    }
});

Listener background.js

Browser action

Le browser action (icône dans toolbar) est ce qui va nous permettre de définir l’interface utilisateur en affichant une pop-up lorsque ce dernier clique dessus. Dans notre cas d’utilisation, il va nous permettre d’afficher un simple formulaire, donnant la possibilité à l’utilisateur de choisir la chaîne à regarder dans le navigateur.

"browser_action": {
    "default_icon": "img/favicon.png",
    "default_title": "Twitch Sideplayer",
    "default_popup": "html/popup.html"
},

Browser Action dans le manifest

Ici encore, la configuration est relativement simple. Nous configurons simplement l’icône à afficher ainsi que le fichier nécessaire à l’affichage de la pop-up. C’est un simple fichier HTML contenant un script JS pour gérer le traitement du formulaire.

Content-scripts

Les content-scripts sont l’ensemble des scripts pouvant s'exécuter sur chacune de nos pages. L’un des aspects importants d’un content-script est son contexte d’exécution. En effet, son contexte d’exécution est local à l’onglet dans lequel il est appelé. De fait, les content-scripts ne peuvent pas garder d’état global à l’extension, au contraire du background. En contrepartie, les content-scripts ont accès au DOM de la page sur laquelle ils s’exécutent.

"content_scripts": [
    {
        "matches": [
            "http://*/*",
            "https://*/*"
        ],
        "js": [
            "lib/twitch-v1.js",
            "js/wrapper.js"
        ],
        "run_at": "document_end",
        "css": [
            "css/wrapper.css"
        ]
    }
],

Configuration Content-Script

Ici nous définissons dans un premier temps les URL patterns pour lesquels l'exécution du content-script est applicable. Dans notre cas nous souhaitons l'exécuter sur toutes les pages HTTP et HTTPS. Nous définissons, de la même manière, la liste des scripts à exécuter sur chaque page “matchée” par le pattern URL.

Ainsi, nous utilisons une librairie JS de Twitch pour l’affichage de l’Iframe ainsi qu’un script custom pour wrapper cette dernière, nommé wrapper.js.

chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
    if (!message.type) {
        return;
   }
   if (message.type === createType) {
       startVideo(message.text)
   } else if (message.type === removeType) {
       clearPage()
   } else if (message.type === pauseType) {
       player.pause()
   }
})

// ……

function startVideo(channelId) {
    let elem = document.getElementById(containerId)
    if (elem === null) {
        createContainer(channelId)
    } else if (player.getChannel() !== channelId) {
        clearPage()
        createContainer(channelId)
    } else if (player && player.isPaused()) {
        player.play()
    }
}
// ...

wrapper.js

Ici, un gestionnaire d'événements envoyés depuis les scripts du background permet au script wrapper.js d’appeler différentes fonctions, selon la manipulation du DOM qui s’impose. Nous reviendrons plus en détail sur la communication entre les différentes entités de notre extension. Mais avant cela voici un récapitulatif de nos composants et les interactions possibles :
extension_interraction

Communication entre scripts

Etant donné que la persistance d’information se fait via le background mais que ce dernier n’a accès à aucun DOM, que le browser action n’a pas accès non plus aux DOM, comment fait-on pour transférer des messages / commandes à nos content-scripts qui eux ont un accès complet ?

Pour cela nous utilisons un mécanisme de gestion des événements entre nos différents composants :
Iframe-creation
Voici la séquence qui a lieu lorsque l’on utilise l’extension.

  1. Dans un premier temps l’utilisateur va cliquer sur l’icône de l’extension et remplir le formulaire.
  2. Le browser action, lors de la soumission du formulaire va récupérer les informations et envoyer un message vers le script du background.
  3. Un listener dans le background va s’occuper de récupérer la liste des onglets ouverts et transmettre le message de création vers chacun d’eux.
  4. Enfin les listeners des contents-scripts s’occupent d’exécuter le code de création de l’iFrame.

La WebExtension permet de définir les listeners et la manière d’envoyer des messages de façon simple. Ainsi nous pouvons définir des messages pour la création de l’iFrame, la fermeture, la pause… Cela nous permet d’avoir des comportements globaux de façon transparente pour l’utilisateur en ayant des DOM différents, et ce simplement.

En bref

La création de cette extension fut une expérience intéressante, tout d’abord car elle représentait un besoin, récréatif certes, mais aussi car nous avons pu appréhender les concepts du fonctionnement d’une extension.
Plusieurs améliorations techniques sont encore en cours :

  • fiabilisation du code en utilisant TypeScript,
  • ajout de tests de l’extension avec par exemple le module sinon-chrome,
  • refonte de l’UI (aide appréciée),

l’ajout d’autres plateformes est aussi envisagé (youtube, dailymotion…). L’extension est assez stable mais des bugs peuvent encore apparaître. N’hésitez donc pas à reporter les bugs et même à contribuer !

Florian Garcia & Thomas Toledo